@codehz/ecs 0.7.2 → 0.7.3
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/examples/advanced-scheduling.ts +96 -0
- package/examples/collision-detection.ts +229 -0
- package/examples/inventory-system-relations.ts +108 -0
- package/examples/parent-child-hierarchy.ts +206 -0
- package/examples/serialization.ts +337 -0
- package/examples/simple.ts +96 -0
- package/examples/spatial-grid.ts +276 -0
- package/examples/state-machine.ts +273 -0
- package/examples/tag-filtering.ts +266 -0
- package/package.json +58 -12
- package/src/__tests__/commands/buffer-limits.test.ts +72 -0
- package/src/__tests__/commands/buffer.test.ts +195 -0
- package/src/__tests__/component/singleton.test.ts +148 -0
- package/src/__tests__/core/archetype.test.ts +247 -0
- package/src/__tests__/core/bitset.test.ts +171 -0
- package/src/__tests__/core/changeset.test.ts +254 -0
- package/src/__tests__/core/multi-map.test.ts +74 -0
- package/src/__tests__/entity/component-registry.test.ts +66 -0
- package/src/__tests__/entity/entity.test.ts +520 -0
- package/src/__tests__/entity/id-manager.test.ts +157 -0
- package/src/__tests__/entity/id-system.test.ts +260 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
- package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
- package/src/__tests__/query/basic.test.ts +341 -0
- package/src/__tests__/query/caching.test.ts +112 -0
- package/src/__tests__/query/filter.test.ts +111 -0
- package/src/__tests__/query/optional.test.ts +231 -0
- package/src/__tests__/query/perf.test.ts +99 -0
- package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
- package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
- package/src/__tests__/relations/wildcard.test.ts +179 -0
- package/src/__tests__/serialization/bounds.test.ts +237 -0
- package/src/__tests__/testing/assertions.test.ts +224 -0
- package/src/__tests__/testing/entity-builder.test.ts +84 -0
- package/src/__tests__/testing/snapshot.test.ts +150 -0
- package/src/__tests__/testing/world-fixture.test.ts +73 -0
- package/src/__tests__/world/component-hooks.test.ts +185 -0
- package/src/__tests__/world/component-management.test.ts +447 -0
- package/src/__tests__/world/entity-management.test.ts +86 -0
- package/src/__tests__/world/get-optional.test.ts +96 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
- package/src/__tests__/world/perf.test.ts +93 -0
- package/src/__tests__/world/query.test.ts +223 -0
- package/src/__tests__/world/serialize.test.ts +83 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
- package/src/archetype/archetype.ts +472 -0
- package/src/archetype/helpers.ts +186 -0
- package/src/archetype/store.ts +33 -0
- package/src/commands/buffer.ts +110 -0
- package/src/commands/changeset.ts +104 -0
- package/src/component/entity-store.ts +223 -0
- package/src/component/registry.ts +657 -0
- package/src/component/type-utils.ts +9 -0
- package/src/entity/index.ts +63 -0
- package/src/entity/manager.ts +115 -0
- package/src/entity/relation.ts +319 -0
- package/src/entity/types.ts +135 -0
- package/src/index.ts +41 -0
- package/src/query/filter.ts +75 -0
- package/src/query/query.ts +313 -0
- package/src/query/registry.ts +101 -0
- package/src/storage/serialization.ts +130 -0
- package/src/testing/index.ts +634 -0
- package/src/types/index.ts +99 -0
- package/src/utils/bit-set.ts +133 -0
- package/src/utils/multi-map.ts +96 -0
- package/src/utils/utils.ts +19 -0
- package/src/world/builder.ts +100 -0
- package/src/world/commands.ts +378 -0
- package/src/world/hooks.ts +358 -0
- package/src/world/references.ts +38 -0
- package/src/world/serialization.ts +122 -0
- package/src/world/world.ts +1201 -0
- /package/{builder.d.mts → dist/builder.d.mts} +0 -0
- /package/{index.d.mts → dist/index.d.mts} +0 -0
- /package/{index.mjs → dist/index.mjs} +0 -0
- /package/{testing.d.mts → dist/testing.d.mts} +0 -0
- /package/{testing.mjs → dist/testing.mjs} +0 -0
- /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
- /package/{world.mjs → dist/world.mjs} +0 -0
- /package/{world.mjs.map → dist/world.mjs.map} +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { normalizeComponentTypes } from "../component/type-utils";
|
|
2
|
+
import type { EntityId, WildcardRelationId } from "../entity";
|
|
3
|
+
import {
|
|
4
|
+
getComponentIdFromRelationId,
|
|
5
|
+
getDetailedIdType,
|
|
6
|
+
getIdType,
|
|
7
|
+
isDontFragmentComponent,
|
|
8
|
+
isWildcardRelationId,
|
|
9
|
+
} from "../entity";
|
|
10
|
+
import { isOptionalEntityId, type ComponentTuple, type ComponentType, type LifecycleHookEntry } from "../types";
|
|
11
|
+
import { getOrCompute } from "../utils/utils";
|
|
12
|
+
import {
|
|
13
|
+
buildCacheKey,
|
|
14
|
+
buildSingleComponent,
|
|
15
|
+
findMatchingDontFragmentRelations,
|
|
16
|
+
getWildcardRelationDataSource,
|
|
17
|
+
isRelationType,
|
|
18
|
+
} from "./helpers";
|
|
19
|
+
import type { DontFragmentStore } from "./store";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Special value to represent missing component data
|
|
23
|
+
*/
|
|
24
|
+
export const MISSING_COMPONENT = Symbol("missing component");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Archetype class for ECS architecture
|
|
28
|
+
* Represents a group of entities that share the same set of components
|
|
29
|
+
* Optimized for fast iteration and component access
|
|
30
|
+
*/
|
|
31
|
+
export class Archetype {
|
|
32
|
+
/**
|
|
33
|
+
* The component types that define this archetype
|
|
34
|
+
*/
|
|
35
|
+
public readonly componentTypes: EntityId<any>[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set version of componentTypes for O(1) lookups in hot paths
|
|
39
|
+
*/
|
|
40
|
+
public readonly componentTypeSet: ReadonlySet<EntityId<any>>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List of entities in this archetype
|
|
44
|
+
*/
|
|
45
|
+
private entities: EntityId[] = [];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Component data storage - maps component type to array of component data
|
|
49
|
+
* Each array index corresponds to the entity index in the entities array
|
|
50
|
+
*/
|
|
51
|
+
private componentData: Map<EntityId<any>, any[]> = new Map();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reverse mapping from entity to its index in this archetype
|
|
55
|
+
*/
|
|
56
|
+
private entityToIndex: Map<EntityId, number> = new Map();
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* DontFragmentStore for relation data keyed by entity ID.
|
|
60
|
+
* This allows entities with different relation targets to share the same archetype
|
|
61
|
+
* without migration overhead when entities change archetypes.
|
|
62
|
+
*/
|
|
63
|
+
private dontFragmentRelations: DontFragmentStore;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Multi-hooks that match this archetype
|
|
67
|
+
*/
|
|
68
|
+
public readonly matchingMultiHooks: Set<LifecycleHookEntry> = new Set();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
72
|
+
*/
|
|
73
|
+
private componentDataSourcesCache: Map<string, (any[] | EntityId<any>[] | undefined)[]> = new Map();
|
|
74
|
+
|
|
75
|
+
constructor(componentTypes: EntityId<any>[], dontFragmentRelations: DontFragmentStore) {
|
|
76
|
+
this.componentTypes = normalizeComponentTypes(componentTypes);
|
|
77
|
+
this.componentTypeSet = new Set(this.componentTypes);
|
|
78
|
+
this.dontFragmentRelations = dontFragmentRelations;
|
|
79
|
+
|
|
80
|
+
for (const componentType of this.componentTypes) {
|
|
81
|
+
this.componentData.set(componentType, []);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get size(): number {
|
|
86
|
+
return this.entities.length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if the given component types match this archetype
|
|
91
|
+
* @param componentTypes - Component types to check (can be in any order)
|
|
92
|
+
* @returns true if the types match this archetype's component set
|
|
93
|
+
* @note This method handles unsorted input by internally sorting for comparison
|
|
94
|
+
*/
|
|
95
|
+
matches(componentTypes: EntityId<any>[]): boolean {
|
|
96
|
+
if (this.componentTypes.length !== componentTypes.length) return false;
|
|
97
|
+
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
98
|
+
return this.componentTypes.every((type, index) => type === sortedTypes[index]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
addEntity(entityId: EntityId, componentData: Map<EntityId<any>, any>): void {
|
|
102
|
+
if (this.entityToIndex.has(entityId)) {
|
|
103
|
+
throw new Error(`Entity ${entityId} is already in this archetype`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const index = this.entities.length;
|
|
107
|
+
this.entities.push(entityId);
|
|
108
|
+
this.entityToIndex.set(entityId, index);
|
|
109
|
+
|
|
110
|
+
// Add component data for regular components
|
|
111
|
+
for (const componentType of this.componentTypes) {
|
|
112
|
+
const data = componentData.get(componentType);
|
|
113
|
+
this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Add dontFragment relations separately
|
|
117
|
+
this.addDontFragmentRelations(entityId, componentData);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private addDontFragmentRelations(entityId: EntityId, componentData: Map<EntityId<any>, any>): void {
|
|
121
|
+
const dontFragmentData = new Map<EntityId<any>, any>();
|
|
122
|
+
|
|
123
|
+
for (const [componentType, data] of componentData) {
|
|
124
|
+
if (this.componentTypeSet.has(componentType)) continue;
|
|
125
|
+
|
|
126
|
+
const detailedType = getDetailedIdType(componentType);
|
|
127
|
+
if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
|
|
128
|
+
dontFragmentData.set(componentType, data);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (dontFragmentData.size > 0) {
|
|
133
|
+
this.dontFragmentRelations.set(entityId, dontFragmentData);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined {
|
|
138
|
+
const index = this.entityToIndex.get(entityId);
|
|
139
|
+
if (index === undefined) return undefined;
|
|
140
|
+
|
|
141
|
+
const entityData = new Map<EntityId<any>, any>();
|
|
142
|
+
|
|
143
|
+
// Add regular components
|
|
144
|
+
for (const componentType of this.componentTypes) {
|
|
145
|
+
const data = this.getComponentData(componentType)[index];
|
|
146
|
+
entityData.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add dontFragment relations
|
|
150
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
151
|
+
if (dontFragmentData) {
|
|
152
|
+
for (const [componentType, data] of dontFragmentData) {
|
|
153
|
+
entityData.set(componentType, data);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return entityData;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined {
|
|
161
|
+
return this.dontFragmentRelations.get(entityId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
dump(): Array<{ entity: EntityId; components: Map<EntityId<any>, any> }> {
|
|
165
|
+
return this.entities.map((entity, i) => {
|
|
166
|
+
const components = new Map<EntityId<any>, any>();
|
|
167
|
+
|
|
168
|
+
for (const componentType of this.componentTypes) {
|
|
169
|
+
const data = this.getComponentData(componentType)[i];
|
|
170
|
+
components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const dontFragmentData = this.dontFragmentRelations.get(entity);
|
|
174
|
+
if (dontFragmentData) {
|
|
175
|
+
for (const [componentType, data] of dontFragmentData) {
|
|
176
|
+
components.set(componentType, data);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { entity, components };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
removeEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined {
|
|
185
|
+
const index = this.entityToIndex.get(entityId);
|
|
186
|
+
if (index === undefined) return undefined;
|
|
187
|
+
|
|
188
|
+
// Extract component data before removal
|
|
189
|
+
const removedData = new Map<EntityId<any>, any>();
|
|
190
|
+
for (const componentType of this.componentTypes) {
|
|
191
|
+
removedData.set(componentType, this.getComponentData(componentType)[index]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Include dontFragment relations
|
|
195
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
196
|
+
if (dontFragmentData) {
|
|
197
|
+
for (const [componentType, data] of dontFragmentData) {
|
|
198
|
+
removedData.set(componentType, data);
|
|
199
|
+
}
|
|
200
|
+
this.dontFragmentRelations.delete(entityId);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.entityToIndex.delete(entityId);
|
|
204
|
+
|
|
205
|
+
// Swap-and-pop for O(1) removal
|
|
206
|
+
const lastIndex = this.entities.length - 1;
|
|
207
|
+
if (index !== lastIndex) {
|
|
208
|
+
const lastEntity = this.entities[lastIndex]!;
|
|
209
|
+
this.entities[index] = lastEntity;
|
|
210
|
+
this.entityToIndex.set(lastEntity, index);
|
|
211
|
+
|
|
212
|
+
for (const componentType of this.componentTypes) {
|
|
213
|
+
const dataArray = this.getComponentData(componentType);
|
|
214
|
+
dataArray[index] = dataArray[lastIndex];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.entities.pop();
|
|
219
|
+
for (const componentType of this.componentTypes) {
|
|
220
|
+
this.getComponentData(componentType).pop();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return removedData;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
exists(entityId: EntityId): boolean {
|
|
227
|
+
return this.entityToIndex.has(entityId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
get<T>(entityId: EntityId, componentType: WildcardRelationId<T>): [EntityId<unknown>, any][];
|
|
231
|
+
get<T>(entityId: EntityId, componentType: EntityId<T>): T;
|
|
232
|
+
get<T>(entityId: EntityId, componentType: EntityId<T> | WildcardRelationId<T>): T | [EntityId<unknown>, any][] {
|
|
233
|
+
const index = this.entityToIndex.get(entityId);
|
|
234
|
+
if (index === undefined) {
|
|
235
|
+
throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (isWildcardRelationId(componentType)) {
|
|
239
|
+
return this.getWildcardRelations(entityId, index, componentType);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return this.getRegularComponent(entityId, index, componentType);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private getWildcardRelations<T>(
|
|
246
|
+
entityId: EntityId,
|
|
247
|
+
index: number,
|
|
248
|
+
componentType: WildcardRelationId<T>,
|
|
249
|
+
): [EntityId<unknown>, any][] {
|
|
250
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
251
|
+
const relations: [EntityId<unknown>, any][] = [];
|
|
252
|
+
|
|
253
|
+
// Check regular archetype components
|
|
254
|
+
for (const relType of this.componentTypes) {
|
|
255
|
+
const relDetailed = getDetailedIdType(relType);
|
|
256
|
+
if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
|
|
257
|
+
const dataArray = this.getComponentData(relType);
|
|
258
|
+
if (dataArray && dataArray[index] !== undefined) {
|
|
259
|
+
const data = dataArray[index];
|
|
260
|
+
relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check dontFragment relations
|
|
266
|
+
if (componentId !== undefined) {
|
|
267
|
+
findMatchingDontFragmentRelations(this.dontFragmentRelations.get(entityId), componentId, relations);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return relations;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private getRegularComponent<T>(entityId: EntityId, index: number, componentType: EntityId<T>): T {
|
|
274
|
+
if (this.componentTypeSet.has(componentType)) {
|
|
275
|
+
const data = this.getComponentData(componentType)[index]!;
|
|
276
|
+
if (data === MISSING_COMPONENT) {
|
|
277
|
+
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
278
|
+
}
|
|
279
|
+
return data as T;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
283
|
+
if (dontFragmentData?.has(componentType)) {
|
|
284
|
+
return dontFragmentData.get(componentType);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
getOptional<T>(entityId: EntityId, componentType: EntityId<T>): { value: T } | undefined {
|
|
291
|
+
const index = this.entityToIndex.get(entityId);
|
|
292
|
+
if (index === undefined) {
|
|
293
|
+
throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (this.componentTypeSet.has(componentType)) {
|
|
297
|
+
const data = this.getComponentData(componentType)[index]!;
|
|
298
|
+
if (data === MISSING_COMPONENT) return undefined;
|
|
299
|
+
return { value: data as T };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
303
|
+
if (dontFragmentData?.has(componentType)) {
|
|
304
|
+
return { value: dontFragmentData.get(componentType) };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
set<T>(entityId: EntityId, componentType: EntityId<T>, data: T): void {
|
|
311
|
+
const index = this.entityToIndex.get(entityId);
|
|
312
|
+
if (index === undefined) {
|
|
313
|
+
throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (this.componentData.has(componentType)) {
|
|
317
|
+
this.getComponentData(componentType)[index] = data;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const detailedType = getDetailedIdType(componentType);
|
|
322
|
+
if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
|
|
323
|
+
let dontFragmentData = this.dontFragmentRelations.get(entityId);
|
|
324
|
+
if (!dontFragmentData) {
|
|
325
|
+
dontFragmentData = new Map();
|
|
326
|
+
this.dontFragmentRelations.set(entityId, dontFragmentData);
|
|
327
|
+
}
|
|
328
|
+
dontFragmentData.set(componentType, data);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
getEntities(): EntityId[] {
|
|
336
|
+
return this.entities;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getEntityToIndexMap(): Map<EntityId, number> {
|
|
340
|
+
return this.entityToIndex;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getComponentData<T>(componentType: EntityId<T>): T[] {
|
|
344
|
+
const data = this.componentData.get(componentType);
|
|
345
|
+
if (!data) {
|
|
346
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
347
|
+
}
|
|
348
|
+
return data;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
getOptionalComponentData<T>(componentType: EntityId<T>): T[] | undefined {
|
|
352
|
+
return this.componentData.get(componentType);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private getCachedComponentDataSources<const T extends readonly ComponentType<any>[]>(
|
|
356
|
+
componentTypes: T,
|
|
357
|
+
): (any[] | EntityId<any>[] | undefined)[] {
|
|
358
|
+
const cacheKey = buildCacheKey(componentTypes);
|
|
359
|
+
return getOrCompute(this.componentDataSourcesCache, cacheKey, () =>
|
|
360
|
+
componentTypes.map((compType) => this.getComponentDataSource(compType)),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private getComponentDataSource(compType: ComponentType<any>): any[] | EntityId<any>[] | undefined {
|
|
365
|
+
const optional = isOptionalEntityId(compType);
|
|
366
|
+
const actualType = optional ? compType.optional : compType;
|
|
367
|
+
const idType = getIdType(actualType);
|
|
368
|
+
|
|
369
|
+
if (idType === "wildcard-relation") {
|
|
370
|
+
const componentId = getComponentIdFromRelationId(actualType)!;
|
|
371
|
+
return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private buildComponentsForIndex<const T extends readonly ComponentType<any>[]>(
|
|
378
|
+
componentTypes: T,
|
|
379
|
+
componentDataSources: (any[] | EntityId<any>[] | undefined)[],
|
|
380
|
+
entityIndex: number,
|
|
381
|
+
entityId: EntityId,
|
|
382
|
+
): ComponentTuple<T> {
|
|
383
|
+
return componentDataSources.map((dataSource, i) =>
|
|
384
|
+
buildSingleComponent(
|
|
385
|
+
componentTypes[i]!,
|
|
386
|
+
dataSource,
|
|
387
|
+
entityIndex,
|
|
388
|
+
entityId,
|
|
389
|
+
(type) => this.getComponentData(type),
|
|
390
|
+
this.dontFragmentRelations,
|
|
391
|
+
),
|
|
392
|
+
) as ComponentTuple<T>;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
getEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(
|
|
396
|
+
componentTypes: T,
|
|
397
|
+
): Array<{ entity: EntityId; components: ComponentTuple<T> }> {
|
|
398
|
+
const result: Array<{ entity: EntityId; components: ComponentTuple<T> }> = [];
|
|
399
|
+
this.appendEntitiesWithComponents(componentTypes, result);
|
|
400
|
+
return result;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
appendEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(
|
|
404
|
+
componentTypes: T,
|
|
405
|
+
result: Array<{ entity: EntityId; components: ComponentTuple<T> }>,
|
|
406
|
+
): void {
|
|
407
|
+
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
408
|
+
result.push({ entity, components });
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
*iterateWithComponents<const T extends readonly ComponentType<any>[]>(
|
|
413
|
+
componentTypes: T,
|
|
414
|
+
): IterableIterator<[EntityId, ...ComponentTuple<T>]> {
|
|
415
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
416
|
+
|
|
417
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
418
|
+
const entity = this.entities[entityIndex]!;
|
|
419
|
+
const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity);
|
|
420
|
+
yield [entity, ...components];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
forEachWithComponents<const T extends readonly ComponentType<any>[]>(
|
|
425
|
+
componentTypes: T,
|
|
426
|
+
callback: (entity: EntityId, ...components: ComponentTuple<T>) => void,
|
|
427
|
+
): void {
|
|
428
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
429
|
+
|
|
430
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
431
|
+
const entity = this.entities[entityIndex]!;
|
|
432
|
+
const components = this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity);
|
|
433
|
+
callback(entity, ...components);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void {
|
|
438
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
439
|
+
const components = new Map<EntityId<any>, any>();
|
|
440
|
+
for (const componentType of this.componentTypes) {
|
|
441
|
+
const data = this.getComponentData(componentType)[i];
|
|
442
|
+
components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
443
|
+
}
|
|
444
|
+
callback(this.entities[i]!, components);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
hasRelationWithComponentId(componentId: EntityId<any>): boolean {
|
|
449
|
+
// Check regular archetype components
|
|
450
|
+
for (const componentType of this.componentTypes) {
|
|
451
|
+
const detailedType = getDetailedIdType(componentType);
|
|
452
|
+
if (isRelationType(detailedType) && detailedType.componentId === componentId) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Check dontFragment relations
|
|
458
|
+
for (const entityId of this.entities) {
|
|
459
|
+
const entityDontFragmentRelations = this.dontFragmentRelations.get(entityId);
|
|
460
|
+
if (entityDontFragmentRelations) {
|
|
461
|
+
for (const relationType of entityDontFragmentRelations.keys()) {
|
|
462
|
+
const detailedType = getDetailedIdType(relationType);
|
|
463
|
+
if (isRelationType(detailedType) && detailedType.componentId === componentId) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { ComponentId, EntityId, WildcardRelationId } from "../entity";
|
|
2
|
+
import {
|
|
3
|
+
getComponentIdFromRelationId,
|
|
4
|
+
getDetailedIdType,
|
|
5
|
+
getIdType,
|
|
6
|
+
getTargetIdFromRelationId,
|
|
7
|
+
isRelationId,
|
|
8
|
+
} from "../entity";
|
|
9
|
+
import { isOptionalEntityId, type ComponentType } from "../types";
|
|
10
|
+
import { MISSING_COMPONENT } from "./archetype";
|
|
11
|
+
import type { DontFragmentStore } from "./store";
|
|
12
|
+
|
|
13
|
+
type DetailedIdType = ReturnType<typeof getDetailedIdType>;
|
|
14
|
+
|
|
15
|
+
type RelationDetailedType =
|
|
16
|
+
| { type: "entity-relation"; componentId: ComponentId<any>; targetId: EntityId<any> }
|
|
17
|
+
| { type: "component-relation"; componentId: ComponentId<any>; targetId: ComponentId<any> };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Find all wildcard relations matching a specific component ID from a components map
|
|
21
|
+
* @param components - Component entity's components map
|
|
22
|
+
* @param wildcardComponentId - The component ID to match (relation part)
|
|
23
|
+
* @returns Array of matching relation IDs
|
|
24
|
+
*/
|
|
25
|
+
export function findWildcardRelations(components: Map<EntityId, any>, wildcardComponentId: EntityId): EntityId<any>[] {
|
|
26
|
+
const result: EntityId<any>[] = [];
|
|
27
|
+
for (const [relId] of components) {
|
|
28
|
+
if (isRelationId(relId)) {
|
|
29
|
+
const decoded = getComponentIdFromRelationId(relId);
|
|
30
|
+
if (decoded === wildcardComponentId) {
|
|
31
|
+
result.push(relId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a components map has any wildcard relations matching a component ID
|
|
40
|
+
* @param components - Component entity's components map
|
|
41
|
+
* @param wildcardComponentId - The component ID to match
|
|
42
|
+
* @returns True if at least one matching relation exists
|
|
43
|
+
*/
|
|
44
|
+
export function hasWildcardRelation(components: Map<EntityId, any>, wildcardComponentId: EntityId): boolean {
|
|
45
|
+
for (const relId of components.keys()) {
|
|
46
|
+
if (isRelationId(relId)) {
|
|
47
|
+
const decoded = getComponentIdFromRelationId(relId);
|
|
48
|
+
if (decoded === wildcardComponentId) return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a detailed type represents a relation (entity or component)
|
|
56
|
+
*/
|
|
57
|
+
export function isRelationType(detailedType: DetailedIdType): detailedType is RelationDetailedType {
|
|
58
|
+
return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a component type matches a given component ID for relations
|
|
63
|
+
*/
|
|
64
|
+
export function matchesRelationComponentId(componentType: EntityId<any>, componentId: EntityId<any>): boolean {
|
|
65
|
+
const detailedType = getDetailedIdType(componentType);
|
|
66
|
+
return isRelationType(detailedType) && detailedType.componentId === componentId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find all relations in dontFragment data that match a component ID
|
|
71
|
+
*/
|
|
72
|
+
export function findMatchingDontFragmentRelations(
|
|
73
|
+
dontFragmentData: Map<EntityId<any>, any> | undefined,
|
|
74
|
+
componentId: EntityId<any>,
|
|
75
|
+
relations: [EntityId<unknown>, any][] = [],
|
|
76
|
+
): [EntityId<unknown>, any][] {
|
|
77
|
+
if (!dontFragmentData) return relations;
|
|
78
|
+
|
|
79
|
+
for (const [relType, data] of dontFragmentData) {
|
|
80
|
+
const relDetailed = getDetailedIdType(relType);
|
|
81
|
+
if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
|
|
82
|
+
relations.push([relDetailed.targetId, data]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return relations;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build cache key for component types
|
|
90
|
+
*/
|
|
91
|
+
export function buildCacheKey(componentTypes: readonly ComponentType<any>[]): string {
|
|
92
|
+
return componentTypes.map((id) => (isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`)).join(",");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get data source for wildcard relations from component types
|
|
97
|
+
*/
|
|
98
|
+
export function getWildcardRelationDataSource(
|
|
99
|
+
componentTypes: EntityId<any>[],
|
|
100
|
+
componentId: EntityId<any>,
|
|
101
|
+
optional: boolean,
|
|
102
|
+
): EntityId<any>[] | undefined {
|
|
103
|
+
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
104
|
+
return optional ? (matchingRelations.length > 0 ? matchingRelations : undefined) : matchingRelations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build wildcard relation value from matching relations
|
|
109
|
+
*/
|
|
110
|
+
export function buildWildcardRelationValue(
|
|
111
|
+
wildcardRelationType: WildcardRelationId<any>,
|
|
112
|
+
matchingRelations: EntityId<any>[] | undefined,
|
|
113
|
+
getDataAtIndex: (relType: EntityId<any>) => any,
|
|
114
|
+
dontFragmentData: Map<EntityId<any>, any> | undefined,
|
|
115
|
+
entityId: EntityId,
|
|
116
|
+
optional: boolean,
|
|
117
|
+
): any {
|
|
118
|
+
const relations: [EntityId<unknown>, any][] = [];
|
|
119
|
+
const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
120
|
+
|
|
121
|
+
// Add regular archetype relations
|
|
122
|
+
for (const relType of matchingRelations || []) {
|
|
123
|
+
const data = getDataAtIndex(relType);
|
|
124
|
+
const targetId = getTargetIdFromRelationId(relType)!;
|
|
125
|
+
relations.push([targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Add dontFragment relations
|
|
129
|
+
if (targetComponentId !== undefined) {
|
|
130
|
+
findMatchingDontFragmentRelations(dontFragmentData, targetComponentId, relations);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (relations.length === 0) {
|
|
134
|
+
if (!optional) {
|
|
135
|
+
const componentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
136
|
+
throw new Error(
|
|
137
|
+
`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return optional ? { value: relations } : relations;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Build regular component value from data source
|
|
148
|
+
*/
|
|
149
|
+
export function buildRegularComponentValue(dataSource: any[] | undefined, entityIndex: number, optional: boolean): any {
|
|
150
|
+
if (dataSource === undefined) {
|
|
151
|
+
if (optional) return undefined;
|
|
152
|
+
throw new Error(`Component data not found for mandatory component type`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const data = dataSource[entityIndex];
|
|
156
|
+
const result = data === MISSING_COMPONENT ? undefined : data;
|
|
157
|
+
return optional ? { value: result } : result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build a single component value based on its type
|
|
162
|
+
*/
|
|
163
|
+
export function buildSingleComponent(
|
|
164
|
+
compType: ComponentType<any>,
|
|
165
|
+
dataSource: any[] | EntityId<any>[] | undefined,
|
|
166
|
+
entityIndex: number,
|
|
167
|
+
entityId: EntityId,
|
|
168
|
+
getComponentData: (type: EntityId<any>) => any[],
|
|
169
|
+
dontFragmentRelations: DontFragmentStore,
|
|
170
|
+
): any {
|
|
171
|
+
const optional = isOptionalEntityId(compType);
|
|
172
|
+
const actualType = optional ? compType.optional : compType;
|
|
173
|
+
|
|
174
|
+
if (getIdType(actualType) === "wildcard-relation") {
|
|
175
|
+
return buildWildcardRelationValue(
|
|
176
|
+
actualType as WildcardRelationId<any>,
|
|
177
|
+
dataSource as EntityId<any>[] | undefined,
|
|
178
|
+
(relType) => getComponentData(relType)[entityIndex],
|
|
179
|
+
dontFragmentRelations.get(entityId),
|
|
180
|
+
entityId,
|
|
181
|
+
optional,
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
return buildRegularComponentValue(dataSource as any[] | undefined, entityIndex, optional);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { EntityId } from "../entity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal interface for storing dontFragment relation data keyed by entity ID.
|
|
5
|
+
*
|
|
6
|
+
* Using an interface here decouples `Archetype` (and `world-commands.ts`) from
|
|
7
|
+
* the concrete `Map` used by `World`, making archetypes independently testable.
|
|
8
|
+
*/
|
|
9
|
+
export interface DontFragmentStore {
|
|
10
|
+
get(entityId: EntityId): Map<EntityId<any>, any> | undefined;
|
|
11
|
+
set(entityId: EntityId, data: Map<EntityId<any>, any>): void;
|
|
12
|
+
delete(entityId: EntityId): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default implementation backed by a plain `Map`.
|
|
17
|
+
* Created once by `World` and shared with every `Archetype`.
|
|
18
|
+
*/
|
|
19
|
+
export class DontFragmentStoreImpl implements DontFragmentStore {
|
|
20
|
+
private readonly data: Map<EntityId, Map<EntityId<any>, any>> = new Map();
|
|
21
|
+
|
|
22
|
+
get(entityId: EntityId): Map<EntityId<any>, any> | undefined {
|
|
23
|
+
return this.data.get(entityId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set(entityId: EntityId, data: Map<EntityId<any>, any>): void {
|
|
27
|
+
this.data.set(entityId, data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
delete(entityId: EntityId): void {
|
|
31
|
+
this.data.delete(entityId);
|
|
32
|
+
}
|
|
33
|
+
}
|