@codehz/ecs 0.7.6 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder.d.mts +41 -19
- package/dist/world.mjs +260 -99
- package/dist/world.mjs.map +1 -1
- package/package.json +2 -1
- package/skills/ecs/SKILL.md +333 -0
- package/src/__tests__/core/archetype.test.ts +4 -2
- package/src/__tests__/perf/dontfragment-wildcard.perf.test.ts +107 -0
- package/src/__tests__/query/filter.test.ts +3 -2
- package/src/archetype/archetype.ts +64 -60
- package/src/archetype/helpers.ts +13 -6
- package/src/archetype/store.ts +222 -15
- package/src/world/commands.ts +22 -34
- package/src/world/references.ts +59 -0
- package/src/world/world.ts +22 -12
|
@@ -9,13 +9,7 @@ import {
|
|
|
9
9
|
} from "../entity";
|
|
10
10
|
import { isOptionalEntityId, type ComponentTuple, type ComponentType, type LifecycleHookEntry } from "../types";
|
|
11
11
|
import { getOrCompute } from "../utils/utils";
|
|
12
|
-
import {
|
|
13
|
-
buildCacheKey,
|
|
14
|
-
buildSingleComponent,
|
|
15
|
-
findMatchingDontFragmentRelations,
|
|
16
|
-
getWildcardRelationDataSource,
|
|
17
|
-
isRelationType,
|
|
18
|
-
} from "./helpers";
|
|
12
|
+
import { buildCacheKey, buildSingleComponent, getWildcardRelationDataSource, isRelationType } from "./helpers";
|
|
19
13
|
import type { DontFragmentStore } from "./store";
|
|
20
14
|
|
|
21
15
|
/**
|
|
@@ -56,9 +50,9 @@ export class Archetype {
|
|
|
56
50
|
private entityToIndex: Map<EntityId, number> = new Map();
|
|
57
51
|
|
|
58
52
|
/**
|
|
59
|
-
* DontFragmentStore
|
|
60
|
-
*
|
|
61
|
-
*
|
|
53
|
+
* DontFragmentStore (keyed primarily by relation ComponentId).
|
|
54
|
+
* Uses optimized RelationEntry (single/multi) for the common exclusive case.
|
|
55
|
+
* See store.ts for implementation details.
|
|
62
56
|
*/
|
|
63
57
|
private dontFragmentRelations: DontFragmentStore;
|
|
64
58
|
|
|
@@ -118,20 +112,14 @@ export class Archetype {
|
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
private addDontFragmentRelations(entityId: EntityId, componentData: Map<EntityId<any>, any>): void {
|
|
121
|
-
const dontFragmentData = new Map<EntityId<any>, any>();
|
|
122
|
-
|
|
123
115
|
for (const [componentType, data] of componentData) {
|
|
124
116
|
if (this.componentTypeSet.has(componentType)) continue;
|
|
125
117
|
|
|
126
118
|
const detailedType = getDetailedIdType(componentType);
|
|
127
119
|
if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
|
|
128
|
-
|
|
120
|
+
this.dontFragmentRelations.setValue(entityId, componentType, data);
|
|
129
121
|
}
|
|
130
122
|
}
|
|
131
|
-
|
|
132
|
-
if (dontFragmentData.size > 0) {
|
|
133
|
-
this.dontFragmentRelations.set(entityId, dontFragmentData);
|
|
134
|
-
}
|
|
135
123
|
}
|
|
136
124
|
|
|
137
125
|
getEntity(entityId: EntityId): Map<EntityId<any>, any> | undefined {
|
|
@@ -147,18 +135,29 @@ export class Archetype {
|
|
|
147
135
|
}
|
|
148
136
|
|
|
149
137
|
// Add dontFragment relations
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
entityData.set(componentType, data);
|
|
154
|
-
}
|
|
138
|
+
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
|
|
139
|
+
for (const [componentType, data] of dontFragmentTuples) {
|
|
140
|
+
entityData.set(componentType, data);
|
|
155
141
|
}
|
|
156
142
|
|
|
157
143
|
return entityData;
|
|
158
144
|
}
|
|
159
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Returns all dontFragment relations for the given entity as an array of tuples.
|
|
148
|
+
* This is a compatibility adapter during the store refactor.
|
|
149
|
+
*
|
|
150
|
+
* Prefer the new DontFragmentStore methods when possible.
|
|
151
|
+
*/
|
|
160
152
|
getEntityDontFragmentRelations(entityId: EntityId): Map<EntityId<any>, any> | undefined {
|
|
161
|
-
|
|
153
|
+
const tuples = this.dontFragmentRelations.getAllForEntity(entityId);
|
|
154
|
+
if (tuples.length === 0) return undefined;
|
|
155
|
+
|
|
156
|
+
const map = new Map<EntityId<any>, any>();
|
|
157
|
+
for (const [relType, data] of tuples) {
|
|
158
|
+
map.set(relType, data);
|
|
159
|
+
}
|
|
160
|
+
return map;
|
|
162
161
|
}
|
|
163
162
|
|
|
164
163
|
dump(): Array<{ entity: EntityId; components: Map<EntityId<any>, any> }> {
|
|
@@ -170,11 +169,9 @@ export class Archetype {
|
|
|
170
169
|
components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
171
170
|
}
|
|
172
171
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
components.set(componentType, data);
|
|
177
|
-
}
|
|
172
|
+
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
|
|
173
|
+
for (const [componentType, data] of dontFragmentTuples) {
|
|
174
|
+
components.set(componentType, data);
|
|
178
175
|
}
|
|
179
176
|
|
|
180
177
|
return { entity, components };
|
|
@@ -192,13 +189,11 @@ export class Archetype {
|
|
|
192
189
|
}
|
|
193
190
|
|
|
194
191
|
// Include dontFragment relations
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
removedData.set(componentType, data);
|
|
199
|
-
}
|
|
200
|
-
this.dontFragmentRelations.delete(entityId);
|
|
192
|
+
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
|
|
193
|
+
for (const [componentType, data] of dontFragmentTuples) {
|
|
194
|
+
removedData.set(componentType, data);
|
|
201
195
|
}
|
|
196
|
+
this.dontFragmentRelations.deleteEntity(entityId);
|
|
202
197
|
|
|
203
198
|
this.entityToIndex.delete(entityId);
|
|
204
199
|
|
|
@@ -262,9 +257,10 @@ export class Archetype {
|
|
|
262
257
|
}
|
|
263
258
|
}
|
|
264
259
|
|
|
265
|
-
// Check dontFragment relations
|
|
260
|
+
// Check dontFragment relations (now uses the efficient per-component path)
|
|
266
261
|
if (componentId !== undefined) {
|
|
267
|
-
|
|
262
|
+
const matches = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
|
|
263
|
+
for (const m of matches) relations.push(m);
|
|
268
264
|
}
|
|
269
265
|
|
|
270
266
|
return relations;
|
|
@@ -279,9 +275,14 @@ export class Archetype {
|
|
|
279
275
|
return data as T;
|
|
280
276
|
}
|
|
281
277
|
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
284
|
-
|
|
278
|
+
const value = this.dontFragmentRelations.getValue(entityId, componentType);
|
|
279
|
+
if (
|
|
280
|
+
value !== undefined ||
|
|
281
|
+
this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)
|
|
282
|
+
) {
|
|
283
|
+
// Note: the extra check above handles the (rare) case where `undefined` is a legitimate stored value.
|
|
284
|
+
// For the common case we just return whatever getValue gave us.
|
|
285
|
+
return this.dontFragmentRelations.getValue(entityId, componentType);
|
|
285
286
|
}
|
|
286
287
|
|
|
287
288
|
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
@@ -299,11 +300,15 @@ export class Archetype {
|
|
|
299
300
|
return { value: data as T };
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
const value = this.dontFragmentRelations.getValue(entityId, componentType);
|
|
304
|
+
// We use getAllForEntity only as a presence check when the value itself might be undefined.
|
|
305
|
+
if (value !== undefined) {
|
|
306
|
+
return { value };
|
|
307
|
+
}
|
|
308
|
+
const all = this.dontFragmentRelations.getAllForEntity(entityId);
|
|
309
|
+
if (all.some(([t]) => t === componentType)) {
|
|
310
|
+
return { value: this.dontFragmentRelations.getValue(entityId, componentType) };
|
|
305
311
|
}
|
|
306
|
-
|
|
307
312
|
return undefined;
|
|
308
313
|
}
|
|
309
314
|
|
|
@@ -320,12 +325,7 @@ export class Archetype {
|
|
|
320
325
|
|
|
321
326
|
const detailedType = getDetailedIdType(componentType);
|
|
322
327
|
if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId!)) {
|
|
323
|
-
|
|
324
|
-
if (!dontFragmentData) {
|
|
325
|
-
dontFragmentData = new Map();
|
|
326
|
-
this.dontFragmentRelations.set(entityId, dontFragmentData);
|
|
327
|
-
}
|
|
328
|
-
dontFragmentData.set(componentType, data);
|
|
328
|
+
this.dontFragmentRelations.setValue(entityId, componentType, data);
|
|
329
329
|
return;
|
|
330
330
|
}
|
|
331
331
|
|
|
@@ -436,12 +436,20 @@ export class Archetype {
|
|
|
436
436
|
|
|
437
437
|
forEach(callback: (entityId: EntityId, components: Map<EntityId<any>, any>) => void): void {
|
|
438
438
|
for (let i = 0; i < this.entities.length; i++) {
|
|
439
|
+
const entity = this.entities[i]!;
|
|
439
440
|
const components = new Map<EntityId<any>, any>();
|
|
440
441
|
for (const componentType of this.componentTypes) {
|
|
441
442
|
const data = this.getComponentData(componentType)[i];
|
|
442
443
|
components.set(componentType, data === MISSING_COMPONENT ? undefined : data);
|
|
443
444
|
}
|
|
444
|
-
|
|
445
|
+
|
|
446
|
+
// Append dontFragment relations (Y-class path, acceptable cost)
|
|
447
|
+
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
|
|
448
|
+
for (const [componentType, data] of dontFragmentTuples) {
|
|
449
|
+
components.set(componentType, data);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
callback(entity, components);
|
|
445
453
|
}
|
|
446
454
|
}
|
|
447
455
|
|
|
@@ -454,19 +462,15 @@ export class Archetype {
|
|
|
454
462
|
}
|
|
455
463
|
}
|
|
456
464
|
|
|
457
|
-
// Check dontFragment relations
|
|
465
|
+
// Check dontFragment relations only for entities that actually belong to *this* archetype.
|
|
466
|
+
// We must not use the global hasAnyForComponent here, otherwise unrelated archetypes
|
|
467
|
+
// can be incorrectly pulled into wildcard queries when any entity in the world has the relation.
|
|
458
468
|
for (const entityId of this.entities) {
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
const detailedType = getDetailedIdType(relationType);
|
|
463
|
-
if (isRelationType(detailedType) && detailedType.componentId === componentId) {
|
|
464
|
-
return true;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
469
|
+
const rels = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
|
|
470
|
+
if (rels.length > 0) {
|
|
471
|
+
return true;
|
|
467
472
|
}
|
|
468
473
|
}
|
|
469
|
-
|
|
470
474
|
return false;
|
|
471
475
|
}
|
|
472
476
|
}
|
package/src/archetype/helpers.ts
CHANGED
|
@@ -67,7 +67,10 @@ export function matchesRelationComponentId(componentType: EntityId<any>, compone
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Find all relations in dontFragment data that match a component ID
|
|
70
|
+
* Find all relations in dontFragment data that match a component ID.
|
|
71
|
+
*
|
|
72
|
+
* @deprecated Prefer calling `DontFragmentStore.getRelationsForComponent` directly.
|
|
73
|
+
* This helper is kept temporarily for any remaining call sites during the refactor.
|
|
71
74
|
*/
|
|
72
75
|
export function findMatchingDontFragmentRelations(
|
|
73
76
|
dontFragmentData: Map<EntityId<any>, any> | undefined,
|
|
@@ -105,13 +108,14 @@ export function getWildcardRelationDataSource(
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
108
|
-
* Build wildcard relation value from matching relations
|
|
111
|
+
* Build wildcard relation value from matching relations.
|
|
112
|
+
* Now receives the DontFragmentStore directly for efficient per-component lookups.
|
|
109
113
|
*/
|
|
110
114
|
export function buildWildcardRelationValue(
|
|
111
115
|
wildcardRelationType: WildcardRelationId<any>,
|
|
112
116
|
matchingRelations: EntityId<any>[] | undefined,
|
|
113
117
|
getDataAtIndex: (relType: EntityId<any>) => any,
|
|
114
|
-
|
|
118
|
+
dontFragmentStore: DontFragmentStore,
|
|
115
119
|
entityId: EntityId,
|
|
116
120
|
optional: boolean,
|
|
117
121
|
): any {
|
|
@@ -125,9 +129,12 @@ export function buildWildcardRelationValue(
|
|
|
125
129
|
relations.push([targetId, data === MISSING_COMPONENT ? undefined : data]);
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
// Add dontFragment relations
|
|
132
|
+
// Add dontFragment relations using the efficient store API (key win for X-class workload)
|
|
129
133
|
if (targetComponentId !== undefined) {
|
|
130
|
-
|
|
134
|
+
const dfMatches = dontFragmentStore.getRelationsForComponent(entityId, targetComponentId);
|
|
135
|
+
for (const m of dfMatches) {
|
|
136
|
+
relations.push(m);
|
|
137
|
+
}
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
if (relations.length === 0) {
|
|
@@ -176,7 +183,7 @@ export function buildSingleComponent(
|
|
|
176
183
|
actualType as WildcardRelationId<any>,
|
|
177
184
|
dataSource as EntityId<any>[] | undefined,
|
|
178
185
|
(relType) => getComponentData(relType)[entityIndex],
|
|
179
|
-
dontFragmentRelations
|
|
186
|
+
dontFragmentRelations,
|
|
180
187
|
entityId,
|
|
181
188
|
optional,
|
|
182
189
|
);
|
package/src/archetype/store.ts
CHANGED
|
@@ -1,33 +1,240 @@
|
|
|
1
1
|
import type { EntityId } from "../entity";
|
|
2
|
+
import { getComponentIdFromRelationId, getTargetIdFromRelationId } from "../entity";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Internal representation for the relations an entity has under one component kind.
|
|
6
|
+
* 'single' is the optimized form for exclusive relations (vast majority of cases).
|
|
7
|
+
*/
|
|
8
|
+
type RelationEntry =
|
|
9
|
+
| { type: "single"; relationType: EntityId<any>; target: EntityId; data: any }
|
|
10
|
+
| { type: "multi"; targets: Map<EntityId, { relationType: EntityId<any>; data: any }> };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Interface for storing dontFragment relation data.
|
|
5
14
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
15
|
+
* Storage is now primarily keyed by relation ComponentId (the "kind" of relation)
|
|
16
|
+
* rather than by entity. This provides O(1) or near-O(1) answers for the hot
|
|
17
|
+
* wildcard-related paths (hasRelationWithComponentId, wildcard materialization
|
|
18
|
+
* during iteration, hook matching, etc.).
|
|
19
|
+
*
|
|
20
|
+
* A lightweight reverse index (entity -> Set of base ComponentIds) is maintained
|
|
21
|
+
* to efficiently support the infrequent "get all dontFragment data for this entity"
|
|
22
|
+
* operations (removeEntity, dump, getEntity, serialization).
|
|
23
|
+
*
|
|
24
|
+
* The interface no longer leaks internal Map structures. Callers work with
|
|
25
|
+
* semantic operations only.
|
|
8
26
|
*/
|
|
9
27
|
export interface DontFragmentStore {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
// High-frequency operations (used by get/set/getOptional and structural changes)
|
|
29
|
+
getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined;
|
|
30
|
+
setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void;
|
|
31
|
+
deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean;
|
|
32
|
+
|
|
33
|
+
// Wildcard / filtering hot paths (X-class priority)
|
|
34
|
+
hasAnyForComponent(componentId: EntityId<any>): boolean;
|
|
35
|
+
getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [target: EntityId, data: any][];
|
|
36
|
+
|
|
37
|
+
// Low-frequency "get everything for entity" paths (Y-class, acceptable cost)
|
|
38
|
+
getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]>;
|
|
39
|
+
deleteEntity(entityId: EntityId): void;
|
|
13
40
|
}
|
|
14
41
|
|
|
15
42
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
43
|
+
* Production implementation of DontFragmentStore.
|
|
44
|
+
*
|
|
45
|
+
* Internal layout (optimized):
|
|
46
|
+
* - byComponent: baseComponentId → (entityId → RelationEntry)
|
|
47
|
+
* RelationEntry uses a single-value form for the common exclusive case (1 target),
|
|
48
|
+
* avoiding Map allocation entirely for the vast majority of dontFragment usage.
|
|
49
|
+
* - entityIndex: entityId → Set<baseComponentId>
|
|
50
|
+
* Lightweight reverse index.
|
|
18
51
|
*/
|
|
19
52
|
export class DontFragmentStoreImpl implements DontFragmentStore {
|
|
20
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Primary storage, keyed by the base relation component ID.
|
|
55
|
+
*/
|
|
56
|
+
private byComponent = new Map<
|
|
57
|
+
EntityId<any>, // base componentId
|
|
58
|
+
Map<EntityId, RelationEntry>
|
|
59
|
+
>();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Reverse index: which base component kinds an entity participates in.
|
|
63
|
+
* Used only by the infrequent getAllForEntity / deleteEntity paths.
|
|
64
|
+
*/
|
|
65
|
+
private entityIndex = new Map<EntityId, Set<EntityId<any>>>();
|
|
66
|
+
|
|
67
|
+
getValue(entityId: EntityId, relationType: EntityId<any>): any | undefined {
|
|
68
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
69
|
+
if (componentId === undefined) return undefined;
|
|
70
|
+
|
|
71
|
+
const entities = this.byComponent.get(componentId);
|
|
72
|
+
if (!entities) return undefined;
|
|
21
73
|
|
|
22
|
-
|
|
23
|
-
|
|
74
|
+
const entry = entities.get(entityId);
|
|
75
|
+
if (!entry) return undefined;
|
|
76
|
+
|
|
77
|
+
const targetId = getTargetIdFromRelationId(relationType)!;
|
|
78
|
+
|
|
79
|
+
if (entry.type === "single") {
|
|
80
|
+
return entry.target === targetId ? entry.data : undefined;
|
|
81
|
+
} else {
|
|
82
|
+
const item = entry.targets.get(targetId);
|
|
83
|
+
return item ? item.data : undefined;
|
|
84
|
+
}
|
|
24
85
|
}
|
|
25
86
|
|
|
26
|
-
|
|
27
|
-
|
|
87
|
+
setValue(entityId: EntityId, relationType: EntityId<any>, data: any): void {
|
|
88
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
89
|
+
if (componentId === undefined) {
|
|
90
|
+
throw new Error("setValue called with a non-relation type on DontFragmentStore");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let entities = this.byComponent.get(componentId);
|
|
94
|
+
if (!entities) {
|
|
95
|
+
entities = new Map();
|
|
96
|
+
this.byComponent.set(componentId, entities);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const targetId = getTargetIdFromRelationId(relationType)!;
|
|
100
|
+
let entry = entities.get(entityId);
|
|
101
|
+
|
|
102
|
+
if (!entry) {
|
|
103
|
+
// First relation for this (entity, component) — use single form (big win for exclusive)
|
|
104
|
+
entry = { type: "single", relationType, target: targetId, data };
|
|
105
|
+
entities.set(entityId, entry);
|
|
106
|
+
} else if (entry.type === "single") {
|
|
107
|
+
if (entry.target === targetId) {
|
|
108
|
+
entry.data = data;
|
|
109
|
+
entry.relationType = relationType; // update in case it changed
|
|
110
|
+
} else {
|
|
111
|
+
// Promote to multi
|
|
112
|
+
const targets = new Map();
|
|
113
|
+
targets.set(entry.target, { relationType: entry.relationType, data: entry.data });
|
|
114
|
+
targets.set(targetId, { relationType, data });
|
|
115
|
+
entities.set(entityId, { type: "multi", targets });
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
entry.targets.set(targetId, { relationType, data });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Maintain reverse index
|
|
122
|
+
let components = this.entityIndex.get(entityId);
|
|
123
|
+
if (!components) {
|
|
124
|
+
components = new Set();
|
|
125
|
+
this.entityIndex.set(entityId, components);
|
|
126
|
+
}
|
|
127
|
+
components.add(componentId);
|
|
28
128
|
}
|
|
29
129
|
|
|
30
|
-
|
|
31
|
-
|
|
130
|
+
deleteValue(entityId: EntityId, relationType: EntityId<any>): boolean {
|
|
131
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
132
|
+
if (componentId === undefined) return false;
|
|
133
|
+
|
|
134
|
+
const entities = this.byComponent.get(componentId);
|
|
135
|
+
if (!entities) return false;
|
|
136
|
+
|
|
137
|
+
const entry = entities.get(entityId);
|
|
138
|
+
if (!entry) return false;
|
|
139
|
+
|
|
140
|
+
const targetId = getTargetIdFromRelationId(relationType)!;
|
|
141
|
+
let existed = false;
|
|
142
|
+
|
|
143
|
+
if (entry.type === "single") {
|
|
144
|
+
if (entry.target === targetId) {
|
|
145
|
+
existed = true;
|
|
146
|
+
entities.delete(entityId);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
existed = entry.targets.delete(targetId);
|
|
150
|
+
if (entry.targets.size === 0) {
|
|
151
|
+
entities.delete(entityId);
|
|
152
|
+
} else if (entry.targets.size === 1) {
|
|
153
|
+
// Demote to single
|
|
154
|
+
const [first] = entry.targets.entries();
|
|
155
|
+
const [t, item] = first!;
|
|
156
|
+
entities.set(entityId, { type: "single", relationType: item.relationType, target: t, data: item.data });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!entities.has(entityId) && entities.size === 0) {
|
|
161
|
+
this.byComponent.delete(componentId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update reverse index
|
|
165
|
+
const components = this.entityIndex.get(entityId);
|
|
166
|
+
if (components && !entities.has(entityId)) {
|
|
167
|
+
components.delete(componentId);
|
|
168
|
+
if (components.size === 0) {
|
|
169
|
+
this.entityIndex.delete(entityId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return existed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
hasAnyForComponent(componentId: EntityId<any>): boolean {
|
|
177
|
+
const entities = this.byComponent.get(componentId);
|
|
178
|
+
return entities !== undefined && entities.size > 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getRelationsForComponent(entityId: EntityId, componentId: EntityId<any>): [EntityId, any][] {
|
|
182
|
+
const result: [EntityId, any][] = [];
|
|
183
|
+
|
|
184
|
+
const entities = this.byComponent.get(componentId);
|
|
185
|
+
if (!entities) return result;
|
|
186
|
+
|
|
187
|
+
const entry = entities.get(entityId);
|
|
188
|
+
if (!entry) return result;
|
|
189
|
+
|
|
190
|
+
if (entry.type === "single") {
|
|
191
|
+
result.push([entry.target, entry.data]);
|
|
192
|
+
} else {
|
|
193
|
+
for (const [target, item] of entry.targets) {
|
|
194
|
+
result.push([target, item.data]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getAllForEntity(entityId: EntityId): Array<[relationType: EntityId<any>, data: any]> {
|
|
202
|
+
const components = this.entityIndex.get(entityId);
|
|
203
|
+
if (!components || components.size === 0) return [];
|
|
204
|
+
|
|
205
|
+
const result: Array<[EntityId<any>, any]> = [];
|
|
206
|
+
|
|
207
|
+
for (const componentId of components) {
|
|
208
|
+
const entities = this.byComponent.get(componentId);
|
|
209
|
+
const entry = entities?.get(entityId);
|
|
210
|
+
if (entry) {
|
|
211
|
+
if (entry.type === "single") {
|
|
212
|
+
result.push([entry.relationType, entry.data]);
|
|
213
|
+
} else {
|
|
214
|
+
for (const item of entry.targets.values()) {
|
|
215
|
+
result.push([item.relationType, item.data]);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
deleteEntity(entityId: EntityId): void {
|
|
225
|
+
const components = this.entityIndex.get(entityId);
|
|
226
|
+
if (!components) return;
|
|
227
|
+
|
|
228
|
+
for (const componentId of components) {
|
|
229
|
+
const entities = this.byComponent.get(componentId);
|
|
230
|
+
if (entities) {
|
|
231
|
+
entities.delete(entityId);
|
|
232
|
+
if (entities.size === 0) {
|
|
233
|
+
this.byComponent.delete(componentId);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.entityIndex.delete(entityId);
|
|
32
239
|
}
|
|
33
240
|
}
|
package/src/world/commands.ts
CHANGED
|
@@ -172,6 +172,15 @@ export function maybeRemoveWildcardMarker(
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// Also check if this changeset itself is adding another relation of the same kind
|
|
176
|
+
// (common in exclusive dontFragment flips: remove old target + add new target in one batch)
|
|
177
|
+
for (const addedType of changeset.adds.keys()) {
|
|
178
|
+
if (addedType === removedComponentType) continue;
|
|
179
|
+
if (getComponentIdFromRelationId(addedType) === componentId) {
|
|
180
|
+
return; // Replacement is being added in the same changeset, keep the marker
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
175
184
|
changeset.delete(wildcardMarker);
|
|
176
185
|
}
|
|
177
186
|
|
|
@@ -280,6 +289,8 @@ export function applyChangeset(
|
|
|
280
289
|
|
|
281
290
|
/**
|
|
282
291
|
* No-hooks variant of applyDontFragmentChanges that skips tracking removed component data.
|
|
292
|
+
*
|
|
293
|
+
* Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
|
|
283
294
|
*/
|
|
284
295
|
function applyDontFragmentChanges(
|
|
285
296
|
dontFragmentRelations: DontFragmentStore,
|
|
@@ -287,35 +298,25 @@ function applyDontFragmentChanges(
|
|
|
287
298
|
changeset: ComponentChangeset,
|
|
288
299
|
removedComponents: Map<EntityId<any>, any>,
|
|
289
300
|
): void {
|
|
290
|
-
// Get or create the entity's dontFragment relations map
|
|
291
|
-
let entityRelations = dontFragmentRelations.get(entityId);
|
|
292
|
-
|
|
293
301
|
for (const componentType of changeset.removes) {
|
|
294
302
|
if (isDontFragmentRelation(componentType)) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
303
|
+
const removedValue = dontFragmentRelations.getValue(entityId, componentType);
|
|
304
|
+
// Record for hooks if we are actually removing something
|
|
305
|
+
if (
|
|
306
|
+
removedValue !== undefined ||
|
|
307
|
+
dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)
|
|
308
|
+
) {
|
|
309
|
+
removedComponents.set(componentType, removedValue);
|
|
301
310
|
}
|
|
311
|
+
dontFragmentRelations.deleteValue(entityId, componentType);
|
|
302
312
|
}
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
for (const [componentType, component] of changeset.adds) {
|
|
306
316
|
if (isDontFragmentRelation(componentType)) {
|
|
307
|
-
|
|
308
|
-
entityRelations = new Map();
|
|
309
|
-
dontFragmentRelations.set(entityId, entityRelations);
|
|
310
|
-
}
|
|
311
|
-
entityRelations.set(componentType, component);
|
|
317
|
+
dontFragmentRelations.setValue(entityId, componentType, component);
|
|
312
318
|
}
|
|
313
319
|
}
|
|
314
|
-
|
|
315
|
-
// Clean up empty map
|
|
316
|
-
if (entityRelations && entityRelations.size === 0) {
|
|
317
|
-
dontFragmentRelations.delete(entityId);
|
|
318
|
-
}
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
function applyDontFragmentChangesNoHooks(
|
|
@@ -323,30 +324,17 @@ function applyDontFragmentChangesNoHooks(
|
|
|
323
324
|
entityId: EntityId,
|
|
324
325
|
changeset: ComponentChangeset,
|
|
325
326
|
): void {
|
|
326
|
-
let entityRelations = dontFragmentRelations.get(entityId);
|
|
327
|
-
|
|
328
327
|
for (const componentType of changeset.removes) {
|
|
329
328
|
if (isDontFragmentRelation(componentType)) {
|
|
330
|
-
|
|
331
|
-
entityRelations.delete(componentType);
|
|
332
|
-
}
|
|
329
|
+
dontFragmentRelations.deleteValue(entityId, componentType);
|
|
333
330
|
}
|
|
334
331
|
}
|
|
335
332
|
|
|
336
333
|
for (const [componentType, component] of changeset.adds) {
|
|
337
334
|
if (isDontFragmentRelation(componentType)) {
|
|
338
|
-
|
|
339
|
-
entityRelations = new Map();
|
|
340
|
-
dontFragmentRelations.set(entityId, entityRelations);
|
|
341
|
-
}
|
|
342
|
-
entityRelations.set(componentType, component);
|
|
335
|
+
dontFragmentRelations.setValue(entityId, componentType, component);
|
|
343
336
|
}
|
|
344
337
|
}
|
|
345
|
-
|
|
346
|
-
// Clean up empty map
|
|
347
|
-
if (entityRelations && entityRelations.size === 0) {
|
|
348
|
-
dontFragmentRelations.delete(entityId);
|
|
349
|
-
}
|
|
350
338
|
}
|
|
351
339
|
|
|
352
340
|
export function filterRegularComponentTypes(componentTypes: Iterable<EntityId<any>>): EntityId<any>[] {
|