@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,75 @@
|
|
|
1
|
+
import type { Archetype } from "../archetype/archetype";
|
|
2
|
+
import type { EntityId } from "../entity";
|
|
3
|
+
import {
|
|
4
|
+
getComponentIdFromRelationId,
|
|
5
|
+
getDetailedIdType,
|
|
6
|
+
isDontFragmentComponent,
|
|
7
|
+
isRelationId,
|
|
8
|
+
relation,
|
|
9
|
+
} from "../entity";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Filter options for queries
|
|
13
|
+
*/
|
|
14
|
+
export interface QueryFilter {
|
|
15
|
+
negativeComponentTypes?: EntityId<any>[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Serialize a QueryFilter into a deterministic string suitable for cache keys.
|
|
20
|
+
* Currently only serializes `negativeComponentTypes`.
|
|
21
|
+
*/
|
|
22
|
+
export function serializeQueryFilter(filter: QueryFilter = {}): string {
|
|
23
|
+
const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
|
|
24
|
+
if (negative.length === 0) return "";
|
|
25
|
+
return `neg:${negative.join(",")}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if an archetype matches the given component types
|
|
30
|
+
*/
|
|
31
|
+
export function matchesComponentTypes(archetype: Archetype, componentTypes: EntityId<any>[]): boolean {
|
|
32
|
+
return componentTypes.every((type) => {
|
|
33
|
+
const detailedType = getDetailedIdType(type);
|
|
34
|
+
if (detailedType.type === "wildcard-relation") {
|
|
35
|
+
// For wildcard relations, check if archetype contains the component relation
|
|
36
|
+
return archetype.componentTypes.some((archetypeType) => {
|
|
37
|
+
if (!isRelationId(archetypeType)) return false;
|
|
38
|
+
const componentId = getComponentIdFromRelationId(archetypeType);
|
|
39
|
+
return componentId === detailedType.componentId;
|
|
40
|
+
});
|
|
41
|
+
} else if (
|
|
42
|
+
(detailedType.type === "entity-relation" || detailedType.type === "component-relation") &&
|
|
43
|
+
detailedType.componentId !== undefined &&
|
|
44
|
+
isDontFragmentComponent(detailedType.componentId)
|
|
45
|
+
) {
|
|
46
|
+
// For specific dontFragment relations, check if archetype has the wildcard marker
|
|
47
|
+
const wildcardMarker = relation(detailedType.componentId, "*");
|
|
48
|
+
return archetype.componentTypeSet.has(wildcardMarker);
|
|
49
|
+
} else {
|
|
50
|
+
// For regular components and non-dontFragment relations, check direct inclusion
|
|
51
|
+
return archetype.componentTypeSet.has(type);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if an archetype matches the filter conditions (only filtering logic)
|
|
58
|
+
*/
|
|
59
|
+
export function matchesFilter(archetype: Archetype, filter: QueryFilter): boolean {
|
|
60
|
+
const negativeTypes = filter.negativeComponentTypes || [];
|
|
61
|
+
return negativeTypes.every((type) => {
|
|
62
|
+
const detailedType = getDetailedIdType(type);
|
|
63
|
+
if (detailedType.type === "wildcard-relation") {
|
|
64
|
+
// For wildcard relations in negative filter, exclude archetypes that contain ANY relation with the same component
|
|
65
|
+
return !archetype.componentTypes.some((archetypeType) => {
|
|
66
|
+
if (!isRelationId(archetypeType)) return false;
|
|
67
|
+
const componentId = getComponentIdFromRelationId(archetypeType);
|
|
68
|
+
return componentId === detailedType.componentId;
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
// For regular components, check direct exclusion
|
|
72
|
+
return !archetype.componentTypeSet.has(type);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import type { Archetype } from "../archetype/archetype";
|
|
2
|
+
import { normalizeComponentTypes } from "../component/type-utils";
|
|
3
|
+
import type { EntityId, WildcardRelationId } from "../entity";
|
|
4
|
+
import { getDetailedIdType, isDontFragmentComponent } from "../entity";
|
|
5
|
+
import type { ComponentTuple, ComponentType } from "../types";
|
|
6
|
+
import type { World } from "../world/world";
|
|
7
|
+
import { matchesComponentTypes, matchesFilter, type QueryFilter } from "./filter";
|
|
8
|
+
import type { QueryRegistry } from "./registry";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cached query for efficiently iterating entities with specific components.
|
|
12
|
+
*
|
|
13
|
+
* Queries are created via {@link World.createQuery} and should be **reused across frames**
|
|
14
|
+
* for optimal performance. The world automatically keeps the query's internal archetype cache
|
|
15
|
+
* up to date as entities are created and destroyed.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
19
|
+
*
|
|
20
|
+
* // In the game loop
|
|
21
|
+
* movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
|
|
22
|
+
* pos.x += vel.x;
|
|
23
|
+
* pos.y += vel.y;
|
|
24
|
+
* });
|
|
25
|
+
*/
|
|
26
|
+
export class Query {
|
|
27
|
+
private world: World;
|
|
28
|
+
private componentTypes: EntityId<any>[];
|
|
29
|
+
private filter: QueryFilter;
|
|
30
|
+
private cachedArchetypes: Archetype[] = [];
|
|
31
|
+
private isDisposed = false;
|
|
32
|
+
/** Cache key assigned by World for O(1) releaseQuery lookup */
|
|
33
|
+
_cacheKey: string | undefined;
|
|
34
|
+
/** Cached wildcard component types for faster entity filtering */
|
|
35
|
+
private wildcardTypes: WildcardRelationId<any>[];
|
|
36
|
+
/** Cached specific dontFragment relation types that need entity-level filtering */
|
|
37
|
+
private specificDontFragmentTypes: EntityId<any>[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
|
|
41
|
+
*/
|
|
42
|
+
constructor(world: World, componentTypes: EntityId<any>[], filter: QueryFilter = {}, registry?: QueryRegistry) {
|
|
43
|
+
this.world = world;
|
|
44
|
+
this.componentTypes = normalizeComponentTypes(componentTypes);
|
|
45
|
+
this.filter = filter;
|
|
46
|
+
// Pre-compute wildcard types once
|
|
47
|
+
this.wildcardTypes = this.componentTypes.filter(
|
|
48
|
+
(ct) => getDetailedIdType(ct).type === "wildcard-relation",
|
|
49
|
+
) as WildcardRelationId<any>[];
|
|
50
|
+
// Pre-compute specific dontFragment relation types that need entity-level filtering
|
|
51
|
+
this.specificDontFragmentTypes = this.componentTypes.filter((ct) => {
|
|
52
|
+
const detailedType = getDetailedIdType(ct);
|
|
53
|
+
return (
|
|
54
|
+
(detailedType.type === "entity-relation" || detailedType.type === "component-relation") &&
|
|
55
|
+
detailedType.componentId !== undefined &&
|
|
56
|
+
isDontFragmentComponent(detailedType.componentId)
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
this.updateCache();
|
|
60
|
+
// Register with registry for archetype updates
|
|
61
|
+
if (registry) {
|
|
62
|
+
registry.register(this);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if query is disposed and throw error if so
|
|
68
|
+
*/
|
|
69
|
+
private ensureNotDisposed(): void {
|
|
70
|
+
if (this.isDisposed) {
|
|
71
|
+
throw new Error("Query has been disposed");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns all entity IDs that match this query.
|
|
77
|
+
*
|
|
78
|
+
* @returns Array of matching entity IDs
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* const entities = query.getEntities();
|
|
82
|
+
* for (const entity of entities) {
|
|
83
|
+
* const pos = world.get(entity, Position);
|
|
84
|
+
* }
|
|
85
|
+
*/
|
|
86
|
+
getEntities(): EntityId[] {
|
|
87
|
+
this.ensureNotDisposed();
|
|
88
|
+
|
|
89
|
+
// Fast path: no wildcard relations and no specific dontFragment relations
|
|
90
|
+
if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
|
|
91
|
+
const result: EntityId[] = [];
|
|
92
|
+
for (const archetype of this.cachedArchetypes) {
|
|
93
|
+
for (const entity of archetype.getEntities()) {
|
|
94
|
+
result.push(entity);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Slow path: need to filter entities that actually have the required relations
|
|
101
|
+
// This is necessary for:
|
|
102
|
+
// 1. Wildcard relations where an archetype can contain entities with/without the relation
|
|
103
|
+
// 2. Specific dontFragment relations where the archetype only has the wildcard marker
|
|
104
|
+
const result: EntityId[] = [];
|
|
105
|
+
for (const archetype of this.cachedArchetypes) {
|
|
106
|
+
for (const entity of archetype.getEntities()) {
|
|
107
|
+
if (this.entityMatchesQuery(archetype, entity)) {
|
|
108
|
+
result.push(entity);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Check if entity matches all query requirements (wildcards and specific dontFragment relations)
|
|
117
|
+
*/
|
|
118
|
+
private entityMatchesQuery(archetype: Archetype, entity: EntityId): boolean {
|
|
119
|
+
// Check wildcard relations
|
|
120
|
+
for (const wildcardType of this.wildcardTypes) {
|
|
121
|
+
const relations = archetype.get(entity, wildcardType);
|
|
122
|
+
if (!relations || relations.length === 0) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check specific dontFragment relations
|
|
128
|
+
for (const specificType of this.specificDontFragmentTypes) {
|
|
129
|
+
const result = archetype.getOptional(entity, specificType);
|
|
130
|
+
if (result === undefined) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns all matching entities along with their component data.
|
|
140
|
+
*
|
|
141
|
+
* @param componentTypes - Array of component types to retrieve
|
|
142
|
+
* @returns Array of objects containing the entity ID and its component tuple
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const results = query.getEntitiesWithComponents([Position, Velocity]);
|
|
146
|
+
* results.forEach(({ entity, components: [pos, vel] }) => {
|
|
147
|
+
* pos.x += vel.x;
|
|
148
|
+
* });
|
|
149
|
+
*/
|
|
150
|
+
getEntitiesWithComponents<const T extends readonly ComponentType<any>[]>(
|
|
151
|
+
componentTypes: T,
|
|
152
|
+
): Array<{
|
|
153
|
+
entity: EntityId;
|
|
154
|
+
components: ComponentTuple<T>;
|
|
155
|
+
}> {
|
|
156
|
+
this.ensureNotDisposed();
|
|
157
|
+
|
|
158
|
+
const result: Array<{
|
|
159
|
+
entity: EntityId;
|
|
160
|
+
components: ComponentTuple<T>;
|
|
161
|
+
}> = [];
|
|
162
|
+
|
|
163
|
+
for (const archetype of this.cachedArchetypes) {
|
|
164
|
+
archetype.appendEntitiesWithComponents(componentTypes, result);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Iterates over all matching entities and invokes the callback with their component data.
|
|
172
|
+
* This is the preferred way to read and mutate components in a hot loop.
|
|
173
|
+
*
|
|
174
|
+
* @param componentTypes - Array of component types to retrieve
|
|
175
|
+
* @param callback - Function called for each matching entity with its components
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* query.forEach([Position, Velocity], (entity, pos, vel) => {
|
|
179
|
+
* pos.x += vel.x;
|
|
180
|
+
* pos.y += vel.y;
|
|
181
|
+
* });
|
|
182
|
+
*/
|
|
183
|
+
forEach<const T extends readonly ComponentType<any>[]>(
|
|
184
|
+
componentTypes: T,
|
|
185
|
+
callback: (entity: EntityId, ...components: ComponentTuple<T>) => void,
|
|
186
|
+
): void {
|
|
187
|
+
this.ensureNotDisposed();
|
|
188
|
+
|
|
189
|
+
for (const archetype of this.cachedArchetypes) {
|
|
190
|
+
archetype.forEachWithComponents(componentTypes, callback);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Generator that yields each matching entity together with its component data.
|
|
196
|
+
*
|
|
197
|
+
* @param componentTypes - Array of component types to retrieve
|
|
198
|
+
* @yields Tuples of `[entityId, ...components]`
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
|
|
202
|
+
* pos.x += vel.x;
|
|
203
|
+
* }
|
|
204
|
+
*/
|
|
205
|
+
*iterate<const T extends readonly ComponentType<any>[]>(
|
|
206
|
+
componentTypes: T,
|
|
207
|
+
): IterableIterator<[EntityId, ...ComponentTuple<T>]> {
|
|
208
|
+
this.ensureNotDisposed();
|
|
209
|
+
|
|
210
|
+
for (const archetype of this.cachedArchetypes) {
|
|
211
|
+
yield* archetype.iterateWithComponents(componentTypes);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns an array containing the data of a single component for every matching entity.
|
|
217
|
+
*
|
|
218
|
+
* @param componentType - The component type to retrieve
|
|
219
|
+
* @returns Array of component data (one entry per matching entity)
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* const positions = query.getComponentData(Position);
|
|
223
|
+
*/
|
|
224
|
+
getComponentData<T>(componentType: EntityId<T>): T[] {
|
|
225
|
+
this.ensureNotDisposed();
|
|
226
|
+
|
|
227
|
+
const result: T[] = [];
|
|
228
|
+
for (const archetype of this.cachedArchetypes) {
|
|
229
|
+
for (const data of archetype.getComponentData(componentType)) {
|
|
230
|
+
result.push(data);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @internal Rebuilds the cached archetype list. Called automatically by the world.
|
|
238
|
+
*/
|
|
239
|
+
updateCache(): void {
|
|
240
|
+
if (this.isDisposed) return;
|
|
241
|
+
|
|
242
|
+
this.cachedArchetypes = this.world
|
|
243
|
+
.getMatchingArchetypes(this.componentTypes)
|
|
244
|
+
.filter((archetype: Archetype) => matchesFilter(archetype, this.filter));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @internal Called by the world when a new archetype is created.
|
|
249
|
+
*/
|
|
250
|
+
checkNewArchetype(archetype: Archetype): void {
|
|
251
|
+
if (this.isDisposed) return;
|
|
252
|
+
if (
|
|
253
|
+
matchesComponentTypes(archetype, this.componentTypes) &&
|
|
254
|
+
matchesFilter(archetype, this.filter) &&
|
|
255
|
+
!this.cachedArchetypes.includes(archetype)
|
|
256
|
+
) {
|
|
257
|
+
this.cachedArchetypes.push(archetype);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @internal Called by the world when an archetype is destroyed.
|
|
263
|
+
*/
|
|
264
|
+
removeArchetype(archetype: Archetype): void {
|
|
265
|
+
if (this.isDisposed) return;
|
|
266
|
+
const index = this.cachedArchetypes.indexOf(archetype);
|
|
267
|
+
if (index !== -1) {
|
|
268
|
+
this.cachedArchetypes.splice(index, 1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Request disposal of this query.
|
|
274
|
+
* This will decrement the world's reference count for the query.
|
|
275
|
+
* The query will only be fully disposed when the ref count reaches zero.
|
|
276
|
+
*/
|
|
277
|
+
dispose(): void {
|
|
278
|
+
// Ask the world to release this query (decrement refcount and fully dispose when zero)
|
|
279
|
+
this.world.releaseQuery(this);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @internal Fully disposes the query when the world's refCount reaches zero.
|
|
284
|
+
*/
|
|
285
|
+
_disposeInternal(registry?: QueryRegistry): void {
|
|
286
|
+
if (!this.isDisposed) {
|
|
287
|
+
// Unregister from registry (remove from notification list)
|
|
288
|
+
if (registry) {
|
|
289
|
+
registry.unregister(this);
|
|
290
|
+
}
|
|
291
|
+
this.cachedArchetypes = [];
|
|
292
|
+
this.isDisposed = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Using-with-disposals support. Calls {@link dispose} automatically.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* using query = world.createQuery([Position]);
|
|
301
|
+
* // query is released automatically when the block exits
|
|
302
|
+
*/
|
|
303
|
+
[Symbol.dispose](): void {
|
|
304
|
+
this.dispose();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Whether the query has been disposed and can no longer be used.
|
|
309
|
+
*/
|
|
310
|
+
get disposed(): boolean {
|
|
311
|
+
return this.isDisposed;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Archetype } from "../archetype/archetype";
|
|
2
|
+
import type { EntityId } from "../entity";
|
|
3
|
+
import type { QueryFilter } from "../query/filter";
|
|
4
|
+
import { Query } from "../query/query";
|
|
5
|
+
import type { World } from "../world/world";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages the lifecycle and caching of `Query` instances.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* - Create / reuse cached queries keyed by component-type + filter signature.
|
|
12
|
+
* - Track reference counts so queries are only disposed when truly unused.
|
|
13
|
+
* - Notify registered queries when new archetypes are created or destroyed.
|
|
14
|
+
*
|
|
15
|
+
* The `_cacheKey` string that was previously attached directly to `Query` is now
|
|
16
|
+
* kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
|
|
17
|
+
*/
|
|
18
|
+
export class QueryRegistry {
|
|
19
|
+
/** All live queries that should receive archetype notifications. */
|
|
20
|
+
private readonly queries = new Set<Query>();
|
|
21
|
+
/** Cache of reusable queries keyed by a deterministic signature string. */
|
|
22
|
+
private readonly cache = new Map<string, { query: Query; refCount: number }>();
|
|
23
|
+
/** Maps each query to its cache key without polluting the Query public API. */
|
|
24
|
+
private readonly cacheKeys = new WeakMap<Query, string>();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns (or creates) a cached query for the given component types and filter.
|
|
28
|
+
* Increments the reference count on cache hits.
|
|
29
|
+
*
|
|
30
|
+
* @param world The world that owns this registry.
|
|
31
|
+
* @param sortedTypes Normalized (sorted) component types.
|
|
32
|
+
* @param key Combined cache key (`types|filter`).
|
|
33
|
+
* @param filter The raw query filter (used when creating a new Query).
|
|
34
|
+
*/
|
|
35
|
+
getOrCreate(world: World, sortedTypes: EntityId<any>[], key: string, filter: QueryFilter): Query {
|
|
36
|
+
const cached = this.cache.get(key);
|
|
37
|
+
if (cached) {
|
|
38
|
+
cached.refCount++;
|
|
39
|
+
return cached.query;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const query = new Query(world, sortedTypes, filter, this);
|
|
43
|
+
this.cacheKeys.set(query, key);
|
|
44
|
+
this.cache.set(key, { query, refCount: 1 });
|
|
45
|
+
return query;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Decrements the reference count for the given query.
|
|
50
|
+
* When the count reaches zero the query is fully disposed.
|
|
51
|
+
*/
|
|
52
|
+
release(query: Query): void {
|
|
53
|
+
const key = this.cacheKeys.get(query);
|
|
54
|
+
if (!key) return;
|
|
55
|
+
|
|
56
|
+
const cached = this.cache.get(key);
|
|
57
|
+
if (!cached || cached.query !== query) return;
|
|
58
|
+
|
|
59
|
+
cached.refCount--;
|
|
60
|
+
if (cached.refCount <= 0) {
|
|
61
|
+
this.cache.delete(key);
|
|
62
|
+
cached.query._disposeInternal(this);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Registers a query so it receives future archetype notifications.
|
|
68
|
+
* Called automatically by the `Query` constructor via `world._registerQuery`.
|
|
69
|
+
*/
|
|
70
|
+
register(query: Query): void {
|
|
71
|
+
this.queries.add(query);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Removes a query from the notification list.
|
|
76
|
+
* Called by `Query._disposeInternal` via `world._unregisterQuery`.
|
|
77
|
+
*/
|
|
78
|
+
unregister(query: Query): void {
|
|
79
|
+
this.queries.delete(query);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Notifies all live queries that a new archetype has been created.
|
|
84
|
+
* Queries will add the archetype to their cache if it matches.
|
|
85
|
+
*/
|
|
86
|
+
onNewArchetype(archetype: Archetype): void {
|
|
87
|
+
for (const query of this.queries) {
|
|
88
|
+
query.checkNewArchetype(archetype);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Notifies all live queries that an archetype has been destroyed.
|
|
94
|
+
* Queries will remove the archetype from their internal cache.
|
|
95
|
+
*/
|
|
96
|
+
onArchetypeRemoved(archetype: Archetype): void {
|
|
97
|
+
for (const query of this.queries) {
|
|
98
|
+
query.removeArchetype(archetype);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ComponentId, EntityId } from "../entity";
|
|
2
|
+
import { getComponentIdByName, getComponentNameById, getDetailedIdType, relation } from "../entity";
|
|
3
|
+
|
|
4
|
+
// -----------------------------------------------------------------------------
|
|
5
|
+
// Serialization helpers for IDs
|
|
6
|
+
// -----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export type SerializedEntityId = number | string | { component: string; target: number | string | "*" };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serialized state of EntityIdManager
|
|
12
|
+
*/
|
|
13
|
+
export interface SerializedEntityIdManager {
|
|
14
|
+
nextId: number;
|
|
15
|
+
freelist?: number[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SerializedWorld = {
|
|
19
|
+
version: number;
|
|
20
|
+
entityManager: SerializedEntityIdManager;
|
|
21
|
+
entities: SerializedEntity[];
|
|
22
|
+
componentEntities?: SerializedEntity[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SerializedEntity = {
|
|
26
|
+
id: SerializedEntityId;
|
|
27
|
+
components: SerializedComponent[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type SerializedComponent = {
|
|
31
|
+
type: SerializedEntityId;
|
|
32
|
+
value: any;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Encode an internal EntityId into a SerializedEntityId for snapshots
|
|
37
|
+
*/
|
|
38
|
+
export function encodeEntityId(id: EntityId<any>): SerializedEntityId {
|
|
39
|
+
const detailed = getDetailedIdType(id);
|
|
40
|
+
switch (detailed.type) {
|
|
41
|
+
case "component": {
|
|
42
|
+
const name = getComponentNameById(id as ComponentId);
|
|
43
|
+
if (!name) {
|
|
44
|
+
// Warn if component doesn't have a name; keep numeric fallback
|
|
45
|
+
console.warn(`Component ID ${id} has no registered name, serializing as number`);
|
|
46
|
+
}
|
|
47
|
+
return name || (id as number);
|
|
48
|
+
}
|
|
49
|
+
case "entity-relation": {
|
|
50
|
+
const componentName = getComponentNameById(detailed.componentId);
|
|
51
|
+
if (!componentName) {
|
|
52
|
+
console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
|
|
53
|
+
}
|
|
54
|
+
// Safe: targetId is guaranteed to exist for entity-relation type
|
|
55
|
+
return { component: componentName || (detailed.componentId as number).toString(), target: detailed.targetId };
|
|
56
|
+
}
|
|
57
|
+
case "component-relation": {
|
|
58
|
+
const componentName = getComponentNameById(detailed.componentId);
|
|
59
|
+
// Safe: targetId is guaranteed to exist for component-relation type
|
|
60
|
+
const targetName = getComponentNameById(detailed.targetId as ComponentId);
|
|
61
|
+
if (!componentName) {
|
|
62
|
+
console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
|
|
63
|
+
}
|
|
64
|
+
if (!targetName) {
|
|
65
|
+
console.warn(`Target component ID ${detailed.targetId} in relation has no registered name`);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
component: componentName || (detailed.componentId as number).toString(),
|
|
69
|
+
target: targetName || (detailed.targetId as number),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case "wildcard-relation": {
|
|
73
|
+
const componentName = getComponentNameById(detailed.componentId);
|
|
74
|
+
if (!componentName) {
|
|
75
|
+
console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
|
|
76
|
+
}
|
|
77
|
+
return { component: componentName || (detailed.componentId as number).toString(), target: "*" };
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
return id as number;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Decode a SerializedEntityId back into an internal EntityId
|
|
86
|
+
*/
|
|
87
|
+
export function decodeSerializedId(sid: SerializedEntityId): EntityId<any> {
|
|
88
|
+
if (typeof sid === "number") {
|
|
89
|
+
return sid as EntityId<any>;
|
|
90
|
+
}
|
|
91
|
+
if (typeof sid === "string") {
|
|
92
|
+
const id = getComponentIdByName(sid);
|
|
93
|
+
if (id === undefined) {
|
|
94
|
+
const num = parseInt(sid, 10);
|
|
95
|
+
if (!isNaN(num)) return num as EntityId<any>;
|
|
96
|
+
throw new Error(`Unknown component name in snapshot: ${sid}`);
|
|
97
|
+
}
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
if (typeof sid === "object" && sid !== null && typeof sid.component === "string") {
|
|
101
|
+
let compId = getComponentIdByName(sid.component);
|
|
102
|
+
if (compId === undefined) {
|
|
103
|
+
const num = parseInt(sid.component, 10);
|
|
104
|
+
if (!isNaN(num)) compId = num as ComponentId;
|
|
105
|
+
}
|
|
106
|
+
if (compId === undefined) {
|
|
107
|
+
throw new Error(`Unknown component name in snapshot: ${sid.component}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (sid.target === "*") {
|
|
111
|
+
return relation(compId, "*");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let targetId: EntityId<any>;
|
|
115
|
+
if (typeof sid.target === "string") {
|
|
116
|
+
const tid = getComponentIdByName(sid.target);
|
|
117
|
+
if (tid === undefined) {
|
|
118
|
+
const num = parseInt(sid.target, 10);
|
|
119
|
+
if (!isNaN(num)) targetId = num as EntityId<any>;
|
|
120
|
+
else throw new Error(`Unknown target component name in snapshot: ${sid.target}`);
|
|
121
|
+
} else {
|
|
122
|
+
targetId = tid;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
targetId = sid.target as EntityId<any>;
|
|
126
|
+
}
|
|
127
|
+
return relation(compId, targetId as any);
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Invalid ID in snapshot: ${JSON.stringify(sid)}`);
|
|
130
|
+
}
|