@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.
- package/README.en.md +26 -3
- package/README.md +41 -4
- package/dist/builder.d.mts +348 -83
- 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 +1922 -1400
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/examples/spatial-grid.ts +1 -1
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +4 -4
- package/src/__tests__/component/singleton.test.ts +116 -35
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +133 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/component-management.test.ts +6 -5
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +15 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/archetype-manager.ts +283 -0
- package/src/world/command-executor.ts +258 -0
- package/src/world/commands.ts +44 -56
- package/src/world/debug-stats.ts +147 -0
- package/src/world/hooks.ts +8 -0
- package/src/world/operations.ts +88 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/singleton.ts +51 -0
- package/src/world/world.ts +429 -457
package/src/world/world.ts
CHANGED
|
@@ -1,40 +1,36 @@
|
|
|
1
|
-
import { Archetype } from "../archetype/archetype";
|
|
2
|
-
import {
|
|
3
|
-
import { CommandBuffer
|
|
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
|
-
|
|
17
|
-
isDontFragmentWildcard,
|
|
18
|
-
isEntityRelation,
|
|
19
|
-
isExclusiveComponent,
|
|
12
|
+
isSparseRelation,
|
|
20
13
|
isWildcardRelationId,
|
|
14
|
+
relation,
|
|
21
15
|
} from "../entity";
|
|
22
|
-
import {
|
|
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 {
|
|
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 {
|
|
30
|
+
import { ArchetypeManager } from "./archetype-manager";
|
|
29
31
|
import { EntityBuilder } from "./builder";
|
|
30
|
-
import {
|
|
31
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} from "./
|
|
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
|
-
/**
|
|
65
|
-
private
|
|
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
|
-
//
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
private
|
|
82
|
-
private
|
|
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
|
-
|
|
112
|
-
|
|
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
|
|
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(
|
|
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
|
-
} =
|
|
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 } =
|
|
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 (
|
|
436
|
+
if (isSparseRelation(componentType)) {
|
|
447
437
|
// Use getValue; presence check via getAllForEntity only if value can legitimately be undefined
|
|
448
|
-
const val = this.
|
|
438
|
+
const val = this.sparseStore.getValue(entityId, componentType);
|
|
449
439
|
if (val !== undefined) return true;
|
|
450
|
-
return this.
|
|
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
|
|
488
|
+
const hasSparse = isSparseRelation(componentType);
|
|
499
489
|
const hasComponent =
|
|
500
490
|
inArchetype ||
|
|
501
|
-
(
|
|
502
|
-
(this.
|
|
503
|
-
this.
|
|
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.
|
|
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 = `${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1161
|
-
|
|
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(
|
|
1177
|
+
return serializeWorld(
|
|
1178
|
+
this.archetypeManager.archetypes as Archetype[],
|
|
1179
|
+
this.componentEntities,
|
|
1180
|
+
this.entityIdManager,
|
|
1181
|
+
);
|
|
1210
1182
|
}
|
|
1211
1183
|
}
|