@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
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { Archetype } from "../archetype/archetype";
|
|
2
|
+
import type { SparseStore } from "../archetype/store";
|
|
3
|
+
import { normalizeComponentTypes } from "../component/type-utils";
|
|
4
|
+
import type { EntityId } from "../entity";
|
|
5
|
+
import {
|
|
6
|
+
getComponentIdFromRelationId,
|
|
7
|
+
getDetailedIdType,
|
|
8
|
+
isSparseRelation,
|
|
9
|
+
isSparseWildcard,
|
|
10
|
+
isWildcardRelationId,
|
|
11
|
+
} from "../entity";
|
|
12
|
+
import { matchesFilter } from "../query/filter";
|
|
13
|
+
import type { QueryRegistry } from "../query/registry";
|
|
14
|
+
import type { LifecycleHookEntry } from "../types";
|
|
15
|
+
import { getOrCompute } from "../utils/utils";
|
|
16
|
+
import { filterRegularComponentTypes } from "./commands";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Context provided to ArchetypeManager for notifying dependent systems
|
|
20
|
+
* (query caching and lifecycle hooks) without creating tight coupling or cycles.
|
|
21
|
+
* Follows the same callback/context injection pattern used by CommandProcessorContext,
|
|
22
|
+
* HooksContext, and WorldDeserializationContext.
|
|
23
|
+
*/
|
|
24
|
+
export interface ArchetypeManagerContext {
|
|
25
|
+
queryRegistry: QueryRegistry;
|
|
26
|
+
hooks: Set<LifecycleHookEntry>;
|
|
27
|
+
/** Called only when debug collectors are active (mirrors original guard in World) */
|
|
28
|
+
recordArchetypeCreated?: () => void;
|
|
29
|
+
recordArchetypeRemoved?: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Encapsulates all archetype storage, indexing, creation, removal, and reverse
|
|
34
|
+
* referencing logic that was previously scattered as private methods + maps
|
|
35
|
+
* directly on the World class.
|
|
36
|
+
*
|
|
37
|
+
* Responsibilities:
|
|
38
|
+
* - Archetype memoization by signature
|
|
39
|
+
* - Component-type reverse index (archetypesByComponent)
|
|
40
|
+
* - Entity → current Archetype map
|
|
41
|
+
* - Reverse "who references this entity via component/relation" index
|
|
42
|
+
* - Creation + removal with proper notifications to QueryRegistry + hook matching
|
|
43
|
+
* - Cleanup of empty archetypes after entity cascades
|
|
44
|
+
*
|
|
45
|
+
* This extraction shrinks World while keeping the same behavior and hot-path characteristics.
|
|
46
|
+
*/
|
|
47
|
+
export class ArchetypeManager {
|
|
48
|
+
// Public for performance (hot paths access these maps frequently).
|
|
49
|
+
// This intentionally breaks encapsulation a bit for speed, as requested.
|
|
50
|
+
readonly archetypes: Archetype[] = [];
|
|
51
|
+
readonly archetypeBySignature = new Map<string, Archetype>();
|
|
52
|
+
readonly entityToArchetype = new Map<EntityId, Archetype>();
|
|
53
|
+
readonly archetypesByComponent = new Map<EntityId<any>, Set<Archetype>>();
|
|
54
|
+
readonly entityToReferencingArchetypes = new Map<EntityId, Set<Archetype>>();
|
|
55
|
+
|
|
56
|
+
private readonly sparseStore: SparseStore;
|
|
57
|
+
private readonly ctx: ArchetypeManagerContext;
|
|
58
|
+
|
|
59
|
+
constructor(ctx: ArchetypeManagerContext, sparseStore: SparseStore) {
|
|
60
|
+
this.ctx = ctx;
|
|
61
|
+
this.sparseStore = sparseStore;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ------------------------------------------------------------------
|
|
65
|
+
// Public / package-internal surface used by World and its close collaborators
|
|
66
|
+
// (commands.ts applyChangeset, serialization deserialization context, etc.)
|
|
67
|
+
// ------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/** Primary entry point — memoized archetype creation/lookup. */
|
|
70
|
+
ensureArchetype(componentTypes: Iterable<EntityId<any>>): Archetype {
|
|
71
|
+
const regularTypes = filterRegularComponentTypes(componentTypes);
|
|
72
|
+
const sortedTypes = normalizeComponentTypes(regularTypes);
|
|
73
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
74
|
+
|
|
75
|
+
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getArchetypeForEntity(entityId: EntityId): Archetype | undefined {
|
|
79
|
+
return this.entityToArchetype.get(entityId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setEntityToArchetype(entityId: EntityId, archetype: Archetype): void {
|
|
83
|
+
this.entityToArchetype.set(entityId, archetype);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Query helpers (moved from World for cohesion)
|
|
87
|
+
getMatchingArchetypes(componentTypes: EntityId<any>[]): Archetype[] {
|
|
88
|
+
if (componentTypes.length === 0) {
|
|
89
|
+
return [...this.archetypes];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const regularComponents: EntityId<any>[] = [];
|
|
93
|
+
const wildcardRelations: { componentId: EntityId<any>; relationId: EntityId<any> }[] = [];
|
|
94
|
+
|
|
95
|
+
for (const componentType of componentTypes) {
|
|
96
|
+
if (isWildcardRelationId(componentType)) {
|
|
97
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
98
|
+
if (componentId !== undefined) {
|
|
99
|
+
wildcardRelations.push({ componentId, relationId: componentType });
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
regularComponents.push(componentType);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
107
|
+
|
|
108
|
+
for (const { componentId, relationId } of wildcardRelations) {
|
|
109
|
+
const markerSet = this.archetypesByComponent.get(relationId);
|
|
110
|
+
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
111
|
+
matchingArchetypes =
|
|
112
|
+
matchingArchetypes.length === 0
|
|
113
|
+
? archetypesWithMarker
|
|
114
|
+
: matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return matchingArchetypes;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getArchetypesWithComponents(componentTypes: EntityId<any>[]): Archetype[] {
|
|
121
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
122
|
+
if (componentTypes.length === 1) {
|
|
123
|
+
const set = this.archetypesByComponent.get(componentTypes[0]!);
|
|
124
|
+
return set ? Array.from(set) : [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Sort by Set size, intersect starting from the smallest
|
|
128
|
+
const sets = componentTypes
|
|
129
|
+
.map((type) => this.archetypesByComponent.get(type))
|
|
130
|
+
.filter((s): s is Set<Archetype> => s !== undefined && s.size > 0)
|
|
131
|
+
.sort((a, b) => a.size - b.size);
|
|
132
|
+
|
|
133
|
+
if (sets.length === 0) return [];
|
|
134
|
+
if (sets.length < componentTypes.length) return []; // One component has no matching archetypes
|
|
135
|
+
|
|
136
|
+
const smallest = sets[0]!;
|
|
137
|
+
|
|
138
|
+
// 2-component fast path
|
|
139
|
+
if (sets.length === 2) {
|
|
140
|
+
const other = sets[1]!;
|
|
141
|
+
return Array.from(smallest).filter((a) => other.has(a));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Multi-component intersection
|
|
145
|
+
let result = new Set(smallest);
|
|
146
|
+
for (let i = 1; i < sets.length; i++) {
|
|
147
|
+
for (const item of result) {
|
|
148
|
+
if (!sets[i]!.has(item)) result.delete(item);
|
|
149
|
+
}
|
|
150
|
+
if (result.size === 0) return [];
|
|
151
|
+
}
|
|
152
|
+
return Array.from(result);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ------------------------------------------------------------------
|
|
156
|
+
// Internal creation / removal (core of the original cluster)
|
|
157
|
+
// ------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
private createArchetypeSignature(componentTypes: EntityId<any>[]): string {
|
|
160
|
+
return componentTypes.join(",");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Deduplicated version of the original pair of methods. */
|
|
164
|
+
private updateReferencingIndex(componentType: EntityId<any>, archetype: Archetype, isAdd: boolean): void {
|
|
165
|
+
const detailedType = getDetailedIdType(componentType);
|
|
166
|
+
let entityId: EntityId | undefined;
|
|
167
|
+
|
|
168
|
+
if (detailedType.type === "entity") {
|
|
169
|
+
entityId = componentType as EntityId;
|
|
170
|
+
} else if (detailedType.type === "entity-relation") {
|
|
171
|
+
entityId = detailedType.targetId;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (entityId !== undefined) {
|
|
175
|
+
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
176
|
+
if (isAdd) {
|
|
177
|
+
if (!refs) {
|
|
178
|
+
refs = new Set();
|
|
179
|
+
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
180
|
+
}
|
|
181
|
+
refs.add(archetype);
|
|
182
|
+
} else {
|
|
183
|
+
if (refs) {
|
|
184
|
+
refs.delete(archetype);
|
|
185
|
+
if (refs.size === 0) {
|
|
186
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private createNewArchetype(componentTypes: EntityId<any>[]): Archetype {
|
|
194
|
+
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
195
|
+
this.archetypes.push(newArchetype);
|
|
196
|
+
|
|
197
|
+
this.ctx.recordArchetypeCreated?.();
|
|
198
|
+
|
|
199
|
+
for (const componentType of componentTypes) {
|
|
200
|
+
let archetypes = this.archetypesByComponent.get(componentType);
|
|
201
|
+
if (!archetypes) {
|
|
202
|
+
archetypes = new Set();
|
|
203
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
204
|
+
}
|
|
205
|
+
archetypes.add(newArchetype);
|
|
206
|
+
|
|
207
|
+
// Update reverse index (deduped)
|
|
208
|
+
this.updateReferencingIndex(componentType, newArchetype, true);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.ctx.queryRegistry.onNewArchetype(newArchetype);
|
|
212
|
+
this.updateArchetypeHookMatches(newArchetype);
|
|
213
|
+
|
|
214
|
+
return newArchetype;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private updateArchetypeHookMatches(archetype: Archetype): void {
|
|
218
|
+
for (const entry of this.ctx.hooks) {
|
|
219
|
+
if (this.archetypeMatchesHook(archetype, entry)) {
|
|
220
|
+
archetype.matchingMultiHooks.add(entry);
|
|
221
|
+
if (entry.matchedArchetypes) {
|
|
222
|
+
entry.matchedArchetypes.add(archetype);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public archetypeMatchesHook(archetype: Archetype, entry: LifecycleHookEntry): boolean {
|
|
229
|
+
return (
|
|
230
|
+
entry.requiredComponents.every((c: EntityId<any>) => {
|
|
231
|
+
if (isWildcardRelationId(c)) {
|
|
232
|
+
if (isSparseWildcard(c)) return true;
|
|
233
|
+
const componentId = getComponentIdFromRelationId(c);
|
|
234
|
+
return componentId !== undefined && archetype.hasRelationWithComponentId(componentId);
|
|
235
|
+
}
|
|
236
|
+
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
237
|
+
}) && matchesFilter(archetype, entry.filter)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Called during cascade deletion cleanup. */
|
|
242
|
+
cleanupArchetypesReferencingEntity(entityId: EntityId): void {
|
|
243
|
+
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
244
|
+
if (!refs) return;
|
|
245
|
+
|
|
246
|
+
for (const archetype of refs) {
|
|
247
|
+
if (archetype.getEntities().length === 0) {
|
|
248
|
+
this.removeArchetype(archetype);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// removeArchetype already cleans up the reverse index entries for the archetypes themselves
|
|
252
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private removeArchetype(archetype: Archetype): void {
|
|
256
|
+
const index = this.archetypes.indexOf(archetype);
|
|
257
|
+
if (index !== -1) {
|
|
258
|
+
// swap-and-pop: O(1) removal
|
|
259
|
+
const last = this.archetypes[this.archetypes.length - 1]!;
|
|
260
|
+
this.archetypes[index] = last;
|
|
261
|
+
this.archetypes.pop();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.ctx.recordArchetypeRemoved?.();
|
|
265
|
+
|
|
266
|
+
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
267
|
+
|
|
268
|
+
for (const componentType of archetype.componentTypes) {
|
|
269
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
270
|
+
if (archetypes) {
|
|
271
|
+
archetypes.delete(archetype);
|
|
272
|
+
if (archetypes.size === 0) {
|
|
273
|
+
this.archetypesByComponent.delete(componentType);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Clean up reverse index (deduped)
|
|
278
|
+
this.updateReferencingIndex(componentType, archetype, false);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.ctx.queryRegistry.onArchetypeRemoved(archetype);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { Archetype } from "../archetype/archetype";
|
|
2
|
+
import type { SparseStore } from "../archetype/store";
|
|
3
|
+
import type { Command } from "../commands/buffer";
|
|
4
|
+
import { ComponentChangeset } from "../commands/changeset";
|
|
5
|
+
import type { ComponentEntityStore } from "../component/entity-store";
|
|
6
|
+
import {
|
|
7
|
+
getComponentIdFromRelationId,
|
|
8
|
+
getTargetIdFromRelationId,
|
|
9
|
+
isEntityRelation,
|
|
10
|
+
isExclusiveComponent,
|
|
11
|
+
type EntityId,
|
|
12
|
+
} from "../entity";
|
|
13
|
+
import type { LifecycleHookEntry } from "../types";
|
|
14
|
+
import {
|
|
15
|
+
applyChangeset,
|
|
16
|
+
maybeRemoveWildcardMarker,
|
|
17
|
+
processCommands,
|
|
18
|
+
removeMatchingRelations,
|
|
19
|
+
type CommandProcessorContext,
|
|
20
|
+
} from "./commands";
|
|
21
|
+
import type { triggerLifecycleHooks } from "./hooks";
|
|
22
|
+
import { type HooksContext } from "./hooks";
|
|
23
|
+
import { trackEntityReference, untrackEntityReference, type EntityReferencesMap } from "./references";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Dependencies provided by World to the CommandExecutor.
|
|
27
|
+
* Keeps the executor decoupled while allowing efficient hot-path access
|
|
28
|
+
* (maps passed by reference where hot code already expects them).
|
|
29
|
+
*/
|
|
30
|
+
export interface CommandExecutorContext {
|
|
31
|
+
/** For component-entity (singleton) fast path in execute */
|
|
32
|
+
componentEntities: ComponentEntityStore;
|
|
33
|
+
|
|
34
|
+
/** Reverse reference index for cascades and entity-valued components */
|
|
35
|
+
entityReferences: EntityReferencesMap;
|
|
36
|
+
|
|
37
|
+
/** For the no-hooks fast path guard */
|
|
38
|
+
hooks: Set<LifecycleHookEntry>;
|
|
39
|
+
|
|
40
|
+
/** Direct map access (hot path in applyChangeset and various places) */
|
|
41
|
+
entityToArchetype: Map<EntityId, Archetype>;
|
|
42
|
+
|
|
43
|
+
/** Archetype creation/lookup (passed through CommandProcessorContext) */
|
|
44
|
+
ensureArchetype: (componentTypes: Iterable<EntityId<any>>) => Archetype;
|
|
45
|
+
|
|
46
|
+
/** Sparse store (needed for CommandProcessorContext) */
|
|
47
|
+
sparseStore: SparseStore;
|
|
48
|
+
|
|
49
|
+
/** Factories for the HooksContext (used by triggerLifecycleHooks) */
|
|
50
|
+
has: (entityId: EntityId, componentType: EntityId<any>) => boolean;
|
|
51
|
+
get: <T>(entityId: EntityId, componentType: EntityId<T>) => T;
|
|
52
|
+
getOptional: <T>(entityId: EntityId, componentType: EntityId<T>) => { value: T } | undefined;
|
|
53
|
+
|
|
54
|
+
/** Destroy fast-path delegation (BFS + cascade logic stays in World) */
|
|
55
|
+
destroyEntityImmediate: (entityId: EntityId) => void;
|
|
56
|
+
|
|
57
|
+
/** Debug migration counter (now routed through DebugStatsManager) */
|
|
58
|
+
incrementMigrations: () => void;
|
|
59
|
+
|
|
60
|
+
/** Hook triggering (the function from hooks.ts) */
|
|
61
|
+
triggerLifecycleHooks: typeof triggerLifecycleHooks;
|
|
62
|
+
|
|
63
|
+
/** Remove hook fast path for full entity deletion (used by destroy paths in World) */
|
|
64
|
+
triggerRemoveHooksForEntityDeletion: (
|
|
65
|
+
entityId: EntityId,
|
|
66
|
+
removedComponents: Map<EntityId<any>, any>,
|
|
67
|
+
oldArchetype: Archetype,
|
|
68
|
+
) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Encapsulates the command execution pipeline, reusable changesets,
|
|
73
|
+
* and related orchestration that was previously private methods + fields on World.
|
|
74
|
+
*
|
|
75
|
+
* Responsibilities:
|
|
76
|
+
* - executeEntityCommands (routing for singletons / destroy / structural changes)
|
|
77
|
+
* - applyEntityCommands (changeset processing + exclusive relations + apply + refs + hooks)
|
|
78
|
+
* - removeComponentImmediate (used by cascade deletion)
|
|
79
|
+
* - updateEntityReferences (keeps the reverse index in sync)
|
|
80
|
+
*
|
|
81
|
+
* This extraction significantly reduces World line count while preserving
|
|
82
|
+
* every fast-path branch and allocation-avoidance characteristic.
|
|
83
|
+
*/
|
|
84
|
+
export class CommandExecutor {
|
|
85
|
+
private readonly _changeset = new ComponentChangeset();
|
|
86
|
+
private readonly _removeChangeset = new ComponentChangeset();
|
|
87
|
+
|
|
88
|
+
private readonly _commandCtx: CommandProcessorContext;
|
|
89
|
+
private readonly _hooksCtx: HooksContext;
|
|
90
|
+
|
|
91
|
+
constructor(private readonly ctx: CommandExecutorContext) {
|
|
92
|
+
this._commandCtx = {
|
|
93
|
+
sparseStore: ctx.sparseStore,
|
|
94
|
+
ensureArchetype: ctx.ensureArchetype,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this._hooksCtx = {
|
|
98
|
+
multiHooks: ctx.hooks,
|
|
99
|
+
has: ctx.has,
|
|
100
|
+
get: ctx.get,
|
|
101
|
+
getOptional: ctx.getOptional,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Entry point used by the CommandBuffer.
|
|
107
|
+
* Routes to singleton handling, destroy fast path, or structural apply.
|
|
108
|
+
*/
|
|
109
|
+
executeEntityCommands(entityId: EntityId, commands: Command[]): void {
|
|
110
|
+
this._changeset.clear();
|
|
111
|
+
|
|
112
|
+
// 1. Route: component entities use flat-map storage
|
|
113
|
+
if (this.ctx.componentEntities.exists(entityId)) {
|
|
114
|
+
this.ctx.componentEntities.executeCommands(entityId, commands);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 2. Route: destroy uses fast path (BFS/cascade stays in World)
|
|
119
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
120
|
+
this.ctx.destroyEntityImmediate(entityId);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Apply structural changes
|
|
125
|
+
this.applyEntityCommands(entityId, commands);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private applyEntityCommands(entityId: EntityId, commands: Command[]): void {
|
|
129
|
+
const currentArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
130
|
+
if (!currentArchetype) return;
|
|
131
|
+
|
|
132
|
+
const changeset = this._changeset;
|
|
133
|
+
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
134
|
+
if (isExclusiveComponent(compId)) {
|
|
135
|
+
removeMatchingRelations(eid, arch, compId, changeset);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
140
|
+
|
|
141
|
+
if (this.ctx.hooks.size === 0) {
|
|
142
|
+
// Fast path: no hooks, skip removedComponents map allocation and hook triggering
|
|
143
|
+
const newArchetype = applyChangeset(
|
|
144
|
+
this._commandCtx,
|
|
145
|
+
entityId,
|
|
146
|
+
currentArchetype,
|
|
147
|
+
changeset,
|
|
148
|
+
this.ctx.entityToArchetype,
|
|
149
|
+
null,
|
|
150
|
+
);
|
|
151
|
+
if (hasStructuralChange) {
|
|
152
|
+
this.updateEntityReferences(entityId, changeset);
|
|
153
|
+
}
|
|
154
|
+
if (newArchetype !== currentArchetype) {
|
|
155
|
+
this.ctx.incrementMigrations();
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const removedComponents = new Map<EntityId<any>, any>();
|
|
161
|
+
const newArchetype = applyChangeset(
|
|
162
|
+
this._commandCtx,
|
|
163
|
+
entityId,
|
|
164
|
+
currentArchetype,
|
|
165
|
+
changeset,
|
|
166
|
+
this.ctx.entityToArchetype,
|
|
167
|
+
removedComponents,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (hasStructuralChange) {
|
|
171
|
+
this.updateEntityReferences(entityId, changeset);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (newArchetype !== currentArchetype) {
|
|
175
|
+
this.ctx.incrementMigrations();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.ctx.triggerLifecycleHooks(
|
|
179
|
+
this._hooksCtx,
|
|
180
|
+
entityId,
|
|
181
|
+
changeset.adds,
|
|
182
|
+
removedComponents,
|
|
183
|
+
currentArchetype,
|
|
184
|
+
newArchetype,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Immediate (non-buffered) component removal used during cascade deletion.
|
|
190
|
+
* Called from destroy* paths (which remain in World).
|
|
191
|
+
*/
|
|
192
|
+
removeComponentImmediate(entityId: EntityId, componentType: EntityId<any>, targetEntityId: EntityId): void {
|
|
193
|
+
const sourceArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
194
|
+
if (!sourceArchetype) return;
|
|
195
|
+
|
|
196
|
+
const changeset = this._removeChangeset;
|
|
197
|
+
changeset.clear();
|
|
198
|
+
changeset.delete(componentType);
|
|
199
|
+
maybeRemoveWildcardMarker(
|
|
200
|
+
entityId,
|
|
201
|
+
sourceArchetype,
|
|
202
|
+
componentType,
|
|
203
|
+
getComponentIdFromRelationId(componentType),
|
|
204
|
+
changeset,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
208
|
+
const newArchetype = applyChangeset(
|
|
209
|
+
this._commandCtx,
|
|
210
|
+
entityId,
|
|
211
|
+
sourceArchetype,
|
|
212
|
+
changeset,
|
|
213
|
+
this.ctx.entityToArchetype,
|
|
214
|
+
null,
|
|
215
|
+
);
|
|
216
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetEntityId);
|
|
217
|
+
|
|
218
|
+
this.ctx.triggerLifecycleHooks(
|
|
219
|
+
this._hooksCtx,
|
|
220
|
+
entityId,
|
|
221
|
+
new Map(),
|
|
222
|
+
new Map([[componentType, removedComponent]]),
|
|
223
|
+
sourceArchetype,
|
|
224
|
+
newArchetype,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Keeps the entity reference reverse index in sync after structural changes.
|
|
230
|
+
* Called from apply paths.
|
|
231
|
+
*/
|
|
232
|
+
updateEntityReferences(entityId: EntityId, changeset: ComponentChangeset): void {
|
|
233
|
+
for (const componentType of changeset.removes) {
|
|
234
|
+
if (isEntityRelation(componentType)) {
|
|
235
|
+
const targetId = getTargetIdFromRelationId(componentType)!;
|
|
236
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
237
|
+
} else if (componentType >= 1024) {
|
|
238
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const [componentType] of changeset.adds) {
|
|
243
|
+
if (isEntityRelation(componentType)) {
|
|
244
|
+
const targetId = getTargetIdFromRelationId(componentType)!;
|
|
245
|
+
trackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
246
|
+
} else if (componentType >= 1024) {
|
|
247
|
+
trackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Exposed for any future direct needs (currently not required outside the executor).
|
|
254
|
+
*/
|
|
255
|
+
getHooksContext(): HooksContext {
|
|
256
|
+
return this._hooksCtx;
|
|
257
|
+
}
|
|
258
|
+
}
|