@codehz/ecs 0.9.0 → 0.10.1
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 +68 -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 +1150 -888
- 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 +121 -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 +100 -0
- package/src/world/singleton.ts +52 -0
- package/src/world/world.ts +194 -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,84 +27,89 @@ 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
|
|
64
52
|
* Manages entities and components
|
|
65
53
|
*/
|
|
66
54
|
export class World {
|
|
55
|
+
private static readonly DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING =
|
|
56
|
+
"world.set(componentId, value) for singleton components is deprecated; use world.singleton(componentId).set(value) or world.set(componentId, componentId, value) instead.";
|
|
57
|
+
|
|
67
58
|
// Core data structures for entity and archetype management
|
|
68
59
|
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
60
|
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
61
|
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
77
62
|
private readonly sparseStore = new SparseStoreImpl();
|
|
78
63
|
/** Component entity (singleton) storage */
|
|
79
64
|
private readonly componentEntities = new ComponentEntityStore();
|
|
80
65
|
|
|
66
|
+
// Archetype storage, indexes, creation/removal, and referencing are now encapsulated
|
|
67
|
+
// in ArchetypeManager (extracted to reduce World line count and improve cohesion).
|
|
68
|
+
private archetypeManager!: ArchetypeManager;
|
|
69
|
+
|
|
70
|
+
// Temporary forwarding accessors so the rest of World (and its internal collaborators)
|
|
71
|
+
// can continue using the familiar names with almost zero call-site changes.
|
|
72
|
+
// These can be removed in a follow-up cleanup pass if desired.
|
|
73
|
+
private get archetypes() {
|
|
74
|
+
return this.archetypeManager.archetypes;
|
|
75
|
+
}
|
|
76
|
+
private get entityToArchetype() {
|
|
77
|
+
return this.archetypeManager.entityToArchetype;
|
|
78
|
+
}
|
|
79
|
+
private get archetypesByComponent() {
|
|
80
|
+
return this.archetypeManager.archetypesByComponent;
|
|
81
|
+
}
|
|
82
|
+
private get entityToReferencingArchetypes() {
|
|
83
|
+
return this.archetypeManager.entityToReferencingArchetypes;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
// Query registry – manages caching, ref counts, and archetype notifications
|
|
82
87
|
private readonly queryRegistry = new QueryRegistry();
|
|
83
88
|
|
|
84
89
|
// Lifecycle hooks (declared before cached contexts that reference them)
|
|
85
90
|
private hooks: Set<LifecycleHookEntry> = new Set();
|
|
86
91
|
|
|
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
|
-
};
|
|
92
|
+
// Debug observability (extracted to DebugStatsManager to reduce World line count)
|
|
93
|
+
private readonly debugStats = new DebugStatsManager();
|
|
94
|
+
|
|
95
|
+
// Command execution (orchestration extracted to CommandExecutor)
|
|
96
|
+
private commandBuffer!: CommandBuffer;
|
|
97
|
+
private commandExecutor!: CommandExecutor;
|
|
113
98
|
|
|
114
99
|
constructor(snapshot?: SerializedWorld) {
|
|
100
|
+
// Must create the manager before any code that may invoke ensureArchetype
|
|
101
|
+
// (including the snapshot deserialization path below, and the closures
|
|
102
|
+
// captured in the field-initialized _commandCtx).
|
|
103
|
+
this.archetypeManager = new ArchetypeManager(
|
|
104
|
+
{
|
|
105
|
+
queryRegistry: this.queryRegistry,
|
|
106
|
+
hooks: this.hooks,
|
|
107
|
+
recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
|
|
108
|
+
recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved(),
|
|
109
|
+
},
|
|
110
|
+
this.sparseStore,
|
|
111
|
+
);
|
|
112
|
+
|
|
115
113
|
if (snapshot && typeof snapshot === "object") {
|
|
116
114
|
deserializeWorld(
|
|
117
115
|
{
|
|
@@ -119,15 +117,34 @@ export class World {
|
|
|
119
117
|
componentEntities: this.componentEntities,
|
|
120
118
|
entityReferences: this.entityReferences,
|
|
121
119
|
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
122
|
-
setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch),
|
|
120
|
+
setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch),
|
|
123
121
|
},
|
|
124
122
|
snapshot,
|
|
125
123
|
);
|
|
126
124
|
}
|
|
127
|
-
}
|
|
128
125
|
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
// CommandExecutor must be created after archetypeManager (and debugStats) because its context
|
|
127
|
+
// closes over several pieces and the destroy fast path.
|
|
128
|
+
const execCtx: CommandExecutorContext = {
|
|
129
|
+
componentEntities: this.componentEntities,
|
|
130
|
+
entityReferences: this.entityReferences,
|
|
131
|
+
hooks: this.hooks,
|
|
132
|
+
entityToArchetype: this.entityToArchetype,
|
|
133
|
+
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
134
|
+
sparseStore: this.sparseStore,
|
|
135
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
136
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
137
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
138
|
+
destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
|
|
139
|
+
incrementMigrations: () => this.debugStats.incrementMigrations(),
|
|
140
|
+
triggerLifecycleHooks,
|
|
141
|
+
triggerRemoveHooksForEntityDeletion,
|
|
142
|
+
};
|
|
143
|
+
this.commandExecutor = new CommandExecutor(execCtx);
|
|
144
|
+
|
|
145
|
+
this.commandBuffer = new CommandBuffer((entityId, commands) =>
|
|
146
|
+
this.commandExecutor.executeEntityCommands(entityId, commands),
|
|
147
|
+
);
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
/**
|
|
@@ -255,71 +272,6 @@ export class World {
|
|
|
255
272
|
return this.entityToArchetype.has(entityId);
|
|
256
273
|
}
|
|
257
274
|
|
|
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
275
|
/**
|
|
324
276
|
* Adds or updates a component on an entity (or marks void component as present).
|
|
325
277
|
* The change is buffered and takes effect after calling `world.sync()`.
|
|
@@ -328,30 +280,39 @@ export class World {
|
|
|
328
280
|
* @overload set(entityId: EntityId, componentType: EntityId<void>): void
|
|
329
281
|
* Marks a void component as present on the entity
|
|
330
282
|
*
|
|
283
|
+
* @overload set<T>(componentId: ComponentId<T>, component: Exclude<NoInfer<T>, number>): void
|
|
284
|
+
* @deprecated Use `world.singleton(componentId).set(value)` or `world.set(componentId, componentId, value)` instead.
|
|
285
|
+
* Compatibility shorthand for singleton component data when the second argument is not a number
|
|
286
|
+
*
|
|
331
287
|
* @overload set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void
|
|
332
288
|
* Adds or updates a component with data on the entity
|
|
333
289
|
*
|
|
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
290
|
* @throws {Error} If the entity does not exist
|
|
338
291
|
* @throws {Error} If the component type is invalid or is a wildcard relation
|
|
339
292
|
*
|
|
340
293
|
* @example
|
|
341
294
|
* world.set(entity, Position, { x: 10, y: 20 });
|
|
342
295
|
* world.set(entity, Marker); // void component
|
|
343
|
-
* world.set(
|
|
296
|
+
* world.singleton(GlobalConfig).set({ debug: true }); // singleton component
|
|
297
|
+
* world.set(GlobalConfig, { debug: true }); // deprecated singleton compatibility shorthand
|
|
344
298
|
* world.sync(); // Apply changes
|
|
345
299
|
*/
|
|
346
300
|
set(entityId: EntityId, componentType: EntityId<void>): void;
|
|
301
|
+
/** @deprecated Use `world.singleton(componentId).set(value)` or `world.set(componentId, componentId, value)` instead. */
|
|
302
|
+
set<T>(componentId: ComponentId<T>, component: Exclude<NoInfer<T>, number>): void;
|
|
347
303
|
set<T>(entityId: EntityId, componentType: EntityId<T>, component: NoInfer<T>): void;
|
|
348
|
-
set<T>(componentId: ComponentId<T>, component: NoInfer<T>): void;
|
|
349
304
|
set(entityId: EntityId | ComponentId, componentTypeOrComponent?: EntityId | any, maybeComponent?: any): void {
|
|
350
305
|
const {
|
|
351
306
|
entityId: targetEntityId,
|
|
352
307
|
componentType,
|
|
353
308
|
component,
|
|
354
|
-
|
|
309
|
+
deprecatedSingletonShorthand,
|
|
310
|
+
} = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, arguments.length, (id) =>
|
|
311
|
+
this.exists(id),
|
|
312
|
+
);
|
|
313
|
+
if (deprecatedSingletonShorthand) {
|
|
314
|
+
console.warn(World.DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING);
|
|
315
|
+
}
|
|
355
316
|
this.commandBuffer.set(targetEntityId, componentType, component);
|
|
356
317
|
}
|
|
357
318
|
|
|
@@ -381,9 +342,10 @@ export class World {
|
|
|
381
342
|
remove<T>(componentId: ComponentId<T>): void;
|
|
382
343
|
remove<T>(entityId: EntityId, componentType: EntityId<T>): void;
|
|
383
344
|
remove<T>(entityId: EntityId | ComponentId, componentType?: EntityId<T>): void {
|
|
384
|
-
const { entityId: targetEntityId, componentType: targetComponentType } =
|
|
345
|
+
const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(
|
|
385
346
|
entityId,
|
|
386
347
|
componentType,
|
|
348
|
+
(id) => this.exists(id),
|
|
387
349
|
);
|
|
388
350
|
this.commandBuffer.remove(targetEntityId, targetComponentType);
|
|
389
351
|
}
|
|
@@ -403,6 +365,32 @@ export class World {
|
|
|
403
365
|
this.commandBuffer.delete(entityId);
|
|
404
366
|
}
|
|
405
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Returns an explicit handle for a singleton component (component-as-entity).
|
|
370
|
+
*
|
|
371
|
+
* This is the preferred API for singleton components.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* const config = world.singleton(GlobalConfig);
|
|
375
|
+
* config.set({ debug: true });
|
|
376
|
+
* world.sync();
|
|
377
|
+
* console.log(config.get());
|
|
378
|
+
*/
|
|
379
|
+
singleton<T>(componentId: ComponentId<T>): SingletonHandle<T> {
|
|
380
|
+
assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
|
|
381
|
+
assertSetComponentTypeValid(componentId);
|
|
382
|
+
|
|
383
|
+
return new SingletonHandle(componentId, {
|
|
384
|
+
has: () => this.componentEntities.hasSingleton(componentId),
|
|
385
|
+
get: () => this.get(componentId),
|
|
386
|
+
getOptional: () => this.getOptional(componentId),
|
|
387
|
+
remove: () => this.commandBuffer.remove(componentId, componentId),
|
|
388
|
+
set: (value) => {
|
|
389
|
+
this.commandBuffer.set(componentId, componentId as EntityId<any>, value as any);
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
406
394
|
/**
|
|
407
395
|
* Checks if a specific **component** is present on an entity.
|
|
408
396
|
*
|
|
@@ -612,7 +600,7 @@ export class World {
|
|
|
612
600
|
entityId: EntityId,
|
|
613
601
|
relationComp: ComponentId<T>,
|
|
614
602
|
): [target: EntityId<unknown>, data: T | undefined][] {
|
|
615
|
-
|
|
603
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
616
604
|
|
|
617
605
|
const wildcard = relation(relationComp, "*") as WildcardRelationId<T>;
|
|
618
606
|
|
|
@@ -659,7 +647,7 @@ export class World {
|
|
|
659
647
|
* given base component.
|
|
660
648
|
*/
|
|
661
649
|
hasRelation(entityId: EntityId, relationComp: ComponentId<any>, targetId?: EntityId): boolean {
|
|
662
|
-
|
|
650
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
663
651
|
|
|
664
652
|
if (targetId !== undefined) {
|
|
665
653
|
const specific = relation(relationComp, targetId);
|
|
@@ -675,7 +663,7 @@ export class World {
|
|
|
675
663
|
* Returns the number of relations of the given base component held by the entity.
|
|
676
664
|
*/
|
|
677
665
|
countRelations(entityId: EntityId, relationComp: ComponentId<any>): number {
|
|
678
|
-
|
|
666
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
679
667
|
const targets = this.getRelationTargets(entityId, relationComp);
|
|
680
668
|
return targets.length;
|
|
681
669
|
}
|
|
@@ -922,88 +910,7 @@ export class World {
|
|
|
922
910
|
* This is intended for development/debugging and leak detection.
|
|
923
911
|
*/
|
|
924
912
|
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
|
-
}
|
|
913
|
+
return this.debugStats.createCollector(callback);
|
|
1007
914
|
}
|
|
1008
915
|
|
|
1009
916
|
/**
|
|
@@ -1017,29 +924,56 @@ export class World {
|
|
|
1017
924
|
* world.sync(); // Apply all buffered changes
|
|
1018
925
|
*/
|
|
1019
926
|
sync(): void {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (hasCollectors) {
|
|
1025
|
-
this._resetDebugActivityCounters();
|
|
927
|
+
if (!this.debugStats.hasActiveCollectors()) {
|
|
928
|
+
// Fast path: no debug collectors, skip all timing and stats work
|
|
929
|
+
this.commandBuffer.execute();
|
|
930
|
+
return;
|
|
1026
931
|
}
|
|
1027
932
|
|
|
1028
|
-
|
|
933
|
+
// Slow path: full instrumentation for active debug stats collectors
|
|
934
|
+
const syncStart = performance.now();
|
|
935
|
+
this.debugStats.resetActivity();
|
|
936
|
+
|
|
937
|
+
const commandBufferStart = performance.now();
|
|
1029
938
|
const commandIterations = this.commandBuffer.execute();
|
|
1030
|
-
const commandBufferEnd =
|
|
939
|
+
const commandBufferEnd = performance.now();
|
|
1031
940
|
|
|
1032
|
-
const syncEnd =
|
|
941
|
+
const syncEnd = performance.now();
|
|
1033
942
|
|
|
1034
|
-
|
|
1035
|
-
|
|
943
|
+
// Build the data bag for the extracted manager (keeps it decoupled from internal maps)
|
|
944
|
+
const entityCount = this.entityToArchetype.size;
|
|
945
|
+
let emptyArchetypes = 0;
|
|
946
|
+
for (const arch of this.archetypes) {
|
|
947
|
+
if (arch.size === 0) emptyArchetypes++;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
let archetypesByComponentSize = 0;
|
|
951
|
+
for (const set of this.archetypesByComponent.values()) {
|
|
952
|
+
archetypesByComponentSize += set.size;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
this.debugStats.deliver(
|
|
956
|
+
{
|
|
1036
957
|
syncStart,
|
|
1037
958
|
syncEnd,
|
|
1038
959
|
commandBufferStart,
|
|
1039
960
|
commandBufferEnd,
|
|
1040
961
|
commandIterations,
|
|
1041
|
-
}
|
|
1042
|
-
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
entityCount,
|
|
965
|
+
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
966
|
+
nextId: this.entityIdManager.getNextId(),
|
|
967
|
+
archetypeCount: this.archetypes.length,
|
|
968
|
+
emptyArchetypes,
|
|
969
|
+
archetypesByComponentSize,
|
|
970
|
+
cachedQueryCount: (this.queryRegistry as any).cache?.size ?? 0,
|
|
971
|
+
registeredQueryCount: (this.queryRegistry as any).queries?.size ?? 0,
|
|
972
|
+
hookCount: this.hooks.size,
|
|
973
|
+
entityReferencesSize: this.entityReferences.size,
|
|
974
|
+
entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size,
|
|
975
|
+
},
|
|
976
|
+
);
|
|
1043
977
|
}
|
|
1044
978
|
|
|
1045
979
|
/**
|
|
@@ -1076,7 +1010,7 @@ export class World {
|
|
|
1076
1010
|
createQuery(componentTypes: EntityId<any>[], filter: QueryFilter = {}): Query {
|
|
1077
1011
|
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
1078
1012
|
const filterKey = serializeQueryFilter(filter);
|
|
1079
|
-
const key = `${
|
|
1013
|
+
const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
|
|
1080
1014
|
return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
|
|
1081
1015
|
}
|
|
1082
1016
|
|
|
@@ -1146,71 +1080,7 @@ export class World {
|
|
|
1146
1080
|
* @internal
|
|
1147
1081
|
*/
|
|
1148
1082
|
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);
|
|
1083
|
+
return this.archetypeManager.getMatchingArchetypes(componentTypes);
|
|
1214
1084
|
}
|
|
1215
1085
|
|
|
1216
1086
|
/**
|
|
@@ -1268,287 +1138,17 @@ export class World {
|
|
|
1268
1138
|
}
|
|
1269
1139
|
}
|
|
1270
1140
|
|
|
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
|
-
|
|
1141
|
+
// Delegators to the extracted ArchetypeManager (keeps public + internal call sites unchanged).
|
|
1409
1142
|
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
|
-
);
|
|
1143
|
+
return this.archetypeManager.ensureArchetype(componentTypes);
|
|
1508
1144
|
}
|
|
1509
1145
|
|
|
1510
1146
|
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);
|
|
1147
|
+
this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
|
|
1521
1148
|
}
|
|
1522
1149
|
|
|
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);
|
|
1150
|
+
private archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
|
|
1151
|
+
return this.archetypeManager.archetypeMatchesHook(archetype, entry);
|
|
1552
1152
|
}
|
|
1553
1153
|
|
|
1554
1154
|
/**
|
|
@@ -1572,7 +1172,28 @@ export class World {
|
|
|
1572
1172
|
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
1573
1173
|
* const newWorld = new World(savedData);
|
|
1574
1174
|
*/
|
|
1175
|
+
// Thin delegator to the extracted CommandExecutor for cascade paths (destroy* methods).
|
|
1176
|
+
private removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
|
|
1177
|
+
this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private createHooksContext(): HooksContext {
|
|
1181
|
+
// The executor owns the current HooksContext; expose for any remaining internal use.
|
|
1182
|
+
return (
|
|
1183
|
+
(this.commandExecutor as any).getHooksContext?.() ?? {
|
|
1184
|
+
multiHooks: this.hooks,
|
|
1185
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
1186
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
1187
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1575
1192
|
serialize(): SerializedWorld {
|
|
1576
|
-
return serializeWorld(
|
|
1193
|
+
return serializeWorld(
|
|
1194
|
+
this.archetypeManager.archetypes as Archetype[],
|
|
1195
|
+
this.componentEntities,
|
|
1196
|
+
this.entityIdManager,
|
|
1197
|
+
);
|
|
1577
1198
|
}
|
|
1578
1199
|
}
|