archetype-ecs 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -34
- package/bench/allocations-1m.js +1 -1
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +81 -81
- package/bench/vs-bitecs.js +31 -56
- package/dist/ComponentRegistry.d.ts +18 -0
- package/dist/ComponentRegistry.js +29 -0
- package/dist/EntityManager.d.ts +52 -0
- package/dist/EntityManager.js +891 -0
- package/dist/Profiler.d.ts +12 -0
- package/dist/Profiler.js +38 -0
- package/dist/System.d.ts +41 -0
- package/dist/System.js +159 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -0
- package/dist/src/ComponentRegistry.d.ts +18 -0
- package/dist/src/ComponentRegistry.js +29 -0
- package/dist/src/EntityManager.d.ts +49 -0
- package/dist/src/EntityManager.js +853 -0
- package/dist/src/Profiler.d.ts +12 -0
- package/dist/src/Profiler.js +38 -0
- package/dist/src/System.d.ts +37 -0
- package/dist/src/System.js +139 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +29 -0
- package/dist/tests/EntityManager.test.d.ts +1 -0
- package/dist/tests/EntityManager.test.js +651 -0
- package/dist/tests/System.test.d.ts +1 -0
- package/dist/tests/System.test.js +630 -0
- package/dist/tests/types.d.ts +1 -0
- package/dist/tests/types.js +129 -0
- package/package.json +9 -7
- package/src/ComponentRegistry.ts +49 -0
- package/src/EntityManager.ts +1018 -0
- package/src/{Profiler.js → Profiler.ts} +18 -5
- package/src/System.ts +226 -0
- package/src/index.ts +44 -0
- package/tests/{EntityManager.test.js → EntityManager.test.ts} +338 -70
- package/tests/System.test.ts +730 -0
- package/tests/types.ts +67 -66
- package/tsconfig.json +8 -5
- package/tsconfig.test.json +13 -0
- package/src/ComponentRegistry.js +0 -21
- package/src/EntityManager.js +0 -578
- package/src/index.d.ts +0 -118
- package/src/index.js +0 -37
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
import { TYPED, componentSchemas, toSym, type ComponentDef, type TypeSpec, type FieldRef } from './ComponentRegistry.js';
|
|
2
|
+
|
|
3
|
+
export type { FieldRef } from './ComponentRegistry.js';
|
|
4
|
+
|
|
5
|
+
export type EntityId = number;
|
|
6
|
+
|
|
7
|
+
export type SoAArrayValue = Float32Array | Float64Array | Int8Array | Int16Array | Int32Array
|
|
8
|
+
| Uint8Array | Uint16Array | Uint32Array | unknown[];
|
|
9
|
+
|
|
10
|
+
export interface ArchetypeView {
|
|
11
|
+
readonly id: number;
|
|
12
|
+
readonly entityIds: EntityId[];
|
|
13
|
+
readonly count: number;
|
|
14
|
+
readonly snapshotEntityIds: EntityId[] | null;
|
|
15
|
+
readonly snapshotCount: number;
|
|
16
|
+
field(ref: FieldRef): SoAArrayValue | undefined;
|
|
17
|
+
fieldStride(ref: FieldRef): number;
|
|
18
|
+
snapshot(ref: FieldRef): SoAArrayValue | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SerializedData {
|
|
22
|
+
nextId: number;
|
|
23
|
+
entities: EntityId[];
|
|
24
|
+
components: Record<string, Record<string, unknown>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type ComponentData = Record<string, number | string | ArrayLike<number>> | null | undefined;
|
|
28
|
+
|
|
29
|
+
export interface EntityManager {
|
|
30
|
+
createEntity(): EntityId;
|
|
31
|
+
destroyEntity(id: EntityId): void;
|
|
32
|
+
addComponent(entityId: EntityId, type: ComponentDef, data?: ComponentData): void;
|
|
33
|
+
removeComponent(entityId: EntityId, type: ComponentDef): void;
|
|
34
|
+
getComponent(entityId: EntityId, type: ComponentDef): Record<string, number | string | number[]> | undefined;
|
|
35
|
+
get(entityId: EntityId, fieldRef: FieldRef): number | string | undefined;
|
|
36
|
+
set(entityId: EntityId, fieldRef: FieldRef, value: number | string | ArrayLike<number>): void;
|
|
37
|
+
hasComponent(entityId: EntityId, type: ComponentDef): boolean;
|
|
38
|
+
query(include: ComponentDef[], exclude?: ComponentDef[]): EntityId[];
|
|
39
|
+
getAllEntities(): EntityId[];
|
|
40
|
+
createEntityWith(...args: unknown[]): EntityId;
|
|
41
|
+
count(include: ComponentDef[], exclude?: ComponentDef[]): number;
|
|
42
|
+
forEach(include: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
|
|
43
|
+
onAdd(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
|
|
44
|
+
onRemove(type: ComponentDef, callback: (entityId: EntityId) => void): () => void;
|
|
45
|
+
flushHooks(): void;
|
|
46
|
+
commitRemovals(): void;
|
|
47
|
+
enableTracking(filterComponent: ComponentDef): void;
|
|
48
|
+
flushChanges(): { created: Set<EntityId>; destroyed: Set<EntityId> };
|
|
49
|
+
flushSnapshots(): void;
|
|
50
|
+
serialize(
|
|
51
|
+
symbolToName: Map<symbol, string>,
|
|
52
|
+
stripComponents?: ComponentDef[],
|
|
53
|
+
skipEntitiesWith?: ComponentDef[],
|
|
54
|
+
options?: { serializers?: Map<string, (data: unknown) => unknown> }
|
|
55
|
+
): SerializedData;
|
|
56
|
+
deserialize(
|
|
57
|
+
data: SerializedData,
|
|
58
|
+
nameToSymbol: Record<string, ComponentDef>,
|
|
59
|
+
options?: { deserializers?: Map<string, (data: unknown) => unknown> }
|
|
60
|
+
): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Internal types ───────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
interface SoAStore {
|
|
66
|
+
[TYPED]: true;
|
|
67
|
+
_schema: Record<string, TypeSpec>;
|
|
68
|
+
_capacity: number;
|
|
69
|
+
_arraySizes: Record<string, number>;
|
|
70
|
+
_fields: Record<string, SoAArrayValue>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type SnapshotStore = Record<string, SoAArrayValue>;
|
|
74
|
+
|
|
75
|
+
interface Archetype {
|
|
76
|
+
key: Uint32Array;
|
|
77
|
+
id: number;
|
|
78
|
+
types: Set<symbol>;
|
|
79
|
+
entityIds: EntityId[];
|
|
80
|
+
components: Map<symbol, SoAStore | null>;
|
|
81
|
+
snapshots: Map<symbol, SnapshotStore> | null;
|
|
82
|
+
snapshotEntityIds: EntityId[] | null;
|
|
83
|
+
snapshotCount: number;
|
|
84
|
+
entityToIndex: Map<EntityId, number>;
|
|
85
|
+
count: number;
|
|
86
|
+
capacity: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type HookCallback = (entityId: EntityId) => void;
|
|
90
|
+
|
|
91
|
+
interface Hooks {
|
|
92
|
+
addCbs: Map<symbol, HookCallback[]>;
|
|
93
|
+
removeCbs: Map<symbol, HookCallback[]>;
|
|
94
|
+
pendingAdd: Map<symbol, EntityId[]>;
|
|
95
|
+
pendingRemove: Map<symbol, EntityId[]>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const INITIAL_CAPACITY = 64;
|
|
99
|
+
|
|
100
|
+
// ── Array-based bitmask helpers ──────────────────────────
|
|
101
|
+
|
|
102
|
+
function slotsNeeded(bitCount: number): number {
|
|
103
|
+
return ((bitCount - 1) >>> 5) + 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createMask(slots: number): Uint32Array {
|
|
107
|
+
return new Uint32Array(slots);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function maskSetBit(mask: Uint32Array, bit: number): Uint32Array {
|
|
111
|
+
const slot = bit >>> 5;
|
|
112
|
+
if (slot >= mask.length) {
|
|
113
|
+
const grown = new Uint32Array(slot + 1);
|
|
114
|
+
grown.set(mask);
|
|
115
|
+
grown[slot] |= (1 << (bit & 31));
|
|
116
|
+
return grown;
|
|
117
|
+
}
|
|
118
|
+
mask[slot] |= (1 << (bit & 31));
|
|
119
|
+
return mask;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function maskContains(a: Uint32Array, b: Uint32Array): boolean {
|
|
123
|
+
for (let i = 0; i < b.length; i++) {
|
|
124
|
+
const av = i < a.length ? a[i] : 0;
|
|
125
|
+
if ((av & b[i]) !== b[i]) return false;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function maskDisjoint(a: Uint32Array, b: Uint32Array): boolean {
|
|
131
|
+
const len = Math.min(a.length, b.length);
|
|
132
|
+
for (let i = 0; i < len; i++) {
|
|
133
|
+
if ((a[i] & b[i]) !== 0) return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function maskOverlaps(a: Uint32Array, b: Uint32Array): boolean {
|
|
139
|
+
const len = Math.min(a.length, b.length);
|
|
140
|
+
for (let i = 0; i < len; i++) {
|
|
141
|
+
if ((a[i] & b[i]) !== 0) return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function maskKey(mask: Uint32Array): string {
|
|
147
|
+
let key = '';
|
|
148
|
+
for (let i = 0; i < mask.length; i++) {
|
|
149
|
+
if (i > 0) key += ',';
|
|
150
|
+
key += mask[i];
|
|
151
|
+
}
|
|
152
|
+
return key;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── SoA helpers ──────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
function unpackSpec(spec: TypeSpec): [{ new(len: number): SoAArrayValue }, number] {
|
|
158
|
+
if (Array.isArray(spec)) return spec;
|
|
159
|
+
return [spec, 0];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function createSoAStore(schema: Record<string, TypeSpec>, capacity: number): SoAStore {
|
|
163
|
+
const fields: Record<string, SoAArrayValue> = {};
|
|
164
|
+
const arraySizes: Record<string, number> = {};
|
|
165
|
+
for (const [field, spec] of Object.entries(schema)) {
|
|
166
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
167
|
+
if (size > 0) {
|
|
168
|
+
fields[field] = new Ctor(capacity * size);
|
|
169
|
+
arraySizes[field] = size;
|
|
170
|
+
} else {
|
|
171
|
+
fields[field] = new Ctor(capacity);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: arraySizes, _fields: fields };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function growSoAStore(store: SoAStore, newCapacity: number): void {
|
|
178
|
+
store._capacity = newCapacity;
|
|
179
|
+
for (const [field, spec] of Object.entries(store._schema)) {
|
|
180
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
181
|
+
const old = store._fields[field];
|
|
182
|
+
const allocSize = size > 0 ? newCapacity * size : newCapacity;
|
|
183
|
+
const grown = new Ctor(allocSize);
|
|
184
|
+
if (Ctor === Array) {
|
|
185
|
+
for (let i = 0; i < old.length; i++) (grown as unknown[])[i] = (old as unknown[])[i];
|
|
186
|
+
} else {
|
|
187
|
+
(grown as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
|
|
188
|
+
}
|
|
189
|
+
store._fields[field] = grown;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function soaWrite(store: SoAStore, idx: number, data: ComponentData): void {
|
|
194
|
+
if (!data) {
|
|
195
|
+
for (const field in store._schema) {
|
|
196
|
+
const arr = store._fields[field];
|
|
197
|
+
const size = store._arraySizes[field] || 0;
|
|
198
|
+
if (size > 0) {
|
|
199
|
+
const base = idx * size;
|
|
200
|
+
for (let j = 0; j < size; j++) (arr as never[])[base + j] = 0 as never;
|
|
201
|
+
} else {
|
|
202
|
+
(arr as never[])[idx] = 0 as never;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const field in store._schema) {
|
|
208
|
+
const arr = store._fields[field];
|
|
209
|
+
const size = store._arraySizes[field] || 0;
|
|
210
|
+
if (size > 0) {
|
|
211
|
+
const base = idx * size;
|
|
212
|
+
const src = data[field];
|
|
213
|
+
if (src) {
|
|
214
|
+
for (let j = 0; j < size; j++) {
|
|
215
|
+
(arr as never[])[base + j] = ((src as ArrayLike<number>)[j] ?? 0) as never;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
(arr as never[])[idx] = data[field] as never;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function soaRead(store: SoAStore, idx: number): Record<string, number | string | number[]> {
|
|
225
|
+
const obj: Record<string, number | string | number[]> = {};
|
|
226
|
+
for (const field in store._schema) {
|
|
227
|
+
const arr = store._fields[field];
|
|
228
|
+
const size = store._arraySizes[field] || 0;
|
|
229
|
+
if (size > 0) {
|
|
230
|
+
const base = idx * size;
|
|
231
|
+
obj[field] = Array.from((arr as Float32Array).subarray(base, base + size));
|
|
232
|
+
} else {
|
|
233
|
+
obj[field] = (arr as never[])[idx];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return obj;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function soaSwap(store: SoAStore, idxA: number, idxB: number): void {
|
|
240
|
+
for (const field in store._schema) {
|
|
241
|
+
const arr = store._fields[field] as never[];
|
|
242
|
+
const size = store._arraySizes[field] || 0;
|
|
243
|
+
if (size > 0) {
|
|
244
|
+
const baseA = idxA * size;
|
|
245
|
+
const baseB = idxB * size;
|
|
246
|
+
for (let j = 0; j < size; j++) {
|
|
247
|
+
const tmp = arr[baseA + j];
|
|
248
|
+
arr[baseA + j] = arr[baseB + j];
|
|
249
|
+
arr[baseB + j] = tmp;
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
const tmp = arr[idxA];
|
|
253
|
+
arr[idxA] = arr[idxB];
|
|
254
|
+
arr[idxB] = tmp;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function createSnapshotStore(schema: Record<string, TypeSpec>, capacity: number): SnapshotStore {
|
|
260
|
+
const snap: SnapshotStore = {};
|
|
261
|
+
for (const [field, spec] of Object.entries(schema)) {
|
|
262
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
263
|
+
snap[field] = new Ctor(size > 0 ? capacity * size : capacity);
|
|
264
|
+
}
|
|
265
|
+
return snap;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function growSnapshotStore(snap: SnapshotStore, schema: Record<string, TypeSpec>, newCapacity: number): void {
|
|
269
|
+
for (const [field, spec] of Object.entries(schema)) {
|
|
270
|
+
const [Ctor, size] = unpackSpec(spec);
|
|
271
|
+
const old = snap[field];
|
|
272
|
+
const grown = new Ctor(size > 0 ? newCapacity * size : newCapacity);
|
|
273
|
+
if (Ctor === Array) {
|
|
274
|
+
for (let i = 0; i < old.length; i++) (grown as unknown[])[i] = (old as unknown[])[i];
|
|
275
|
+
} else {
|
|
276
|
+
(grown as Exclude<SoAArrayValue, unknown[]>).set(old as Exclude<SoAArrayValue, unknown[]>);
|
|
277
|
+
}
|
|
278
|
+
snap[field] = grown;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Entity Manager ───────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
export function createEntityManager(): EntityManager {
|
|
285
|
+
let nextId: EntityId = 1;
|
|
286
|
+
let nextArchId = 1;
|
|
287
|
+
const allEntityIds = new Set<EntityId>();
|
|
288
|
+
|
|
289
|
+
let trackFilter: Uint32Array | null = null;
|
|
290
|
+
let createdSet: Set<EntityId> | null = null;
|
|
291
|
+
let destroyedSet: Set<EntityId> | null = null;
|
|
292
|
+
const trackedArchetypes: Archetype[] = [];
|
|
293
|
+
|
|
294
|
+
let hooks: Hooks | null = null;
|
|
295
|
+
|
|
296
|
+
const removedData = new Map<EntityId, Map<symbol, Record<string, number | string | number[]>>>();
|
|
297
|
+
|
|
298
|
+
// Deferred structural changes during forEach iteration
|
|
299
|
+
type DeferredOp =
|
|
300
|
+
| { kind: 'add'; entityId: EntityId; comp: ComponentDef; data?: ComponentData }
|
|
301
|
+
| { kind: 'remove'; entityId: EntityId; comp: ComponentDef }
|
|
302
|
+
| { kind: 'destroy'; entityId: EntityId };
|
|
303
|
+
let iterating = 0;
|
|
304
|
+
const deferred: DeferredOp[] = [];
|
|
305
|
+
|
|
306
|
+
const componentBitIndex = new Map<symbol, number>();
|
|
307
|
+
let nextBitIndex = 0;
|
|
308
|
+
|
|
309
|
+
function getBit(type: symbol | ComponentDef): number {
|
|
310
|
+
const sym = toSym(type);
|
|
311
|
+
let bit = componentBitIndex.get(sym);
|
|
312
|
+
if (bit === undefined) {
|
|
313
|
+
bit = nextBitIndex++;
|
|
314
|
+
componentBitIndex.set(sym, bit);
|
|
315
|
+
}
|
|
316
|
+
return bit;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function computeMask(types: (symbol | ComponentDef)[]): Uint32Array {
|
|
320
|
+
const slots = nextBitIndex > 0 ? slotsNeeded(nextBitIndex) : 1;
|
|
321
|
+
let mask = createMask(slots);
|
|
322
|
+
for (const t of types) {
|
|
323
|
+
mask = maskSetBit(mask, getBit(t));
|
|
324
|
+
}
|
|
325
|
+
return mask;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const archetypes = new Map<string, Archetype>();
|
|
329
|
+
const entityArchetype = new Map<EntityId, Archetype>();
|
|
330
|
+
|
|
331
|
+
let queryCacheVersion = 0;
|
|
332
|
+
const queryCache = new Map<string, { version: number; archetypes: Archetype[] }>();
|
|
333
|
+
|
|
334
|
+
function getOrCreateArchetype(types: symbol[]): Archetype {
|
|
335
|
+
const mask = computeMask(types);
|
|
336
|
+
const key = maskKey(mask);
|
|
337
|
+
let arch = archetypes.get(key);
|
|
338
|
+
if (!arch) {
|
|
339
|
+
const tracked = trackFilter !== null && maskOverlaps(mask, trackFilter);
|
|
340
|
+
arch = {
|
|
341
|
+
key: mask,
|
|
342
|
+
id: nextArchId++,
|
|
343
|
+
types: new Set(types),
|
|
344
|
+
entityIds: [],
|
|
345
|
+
components: new Map(),
|
|
346
|
+
snapshots: tracked ? new Map() : null,
|
|
347
|
+
snapshotEntityIds: tracked ? [] : null,
|
|
348
|
+
snapshotCount: 0,
|
|
349
|
+
entityToIndex: new Map(),
|
|
350
|
+
count: 0,
|
|
351
|
+
capacity: INITIAL_CAPACITY
|
|
352
|
+
};
|
|
353
|
+
for (const t of types) {
|
|
354
|
+
const schema = componentSchemas.get(t);
|
|
355
|
+
const store = schema ? createSoAStore(schema, INITIAL_CAPACITY) : null;
|
|
356
|
+
arch.components.set(t, store);
|
|
357
|
+
if (tracked && store) {
|
|
358
|
+
arch.snapshots!.set(t, createSnapshotStore(schema!, INITIAL_CAPACITY));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
archetypes.set(key, arch);
|
|
362
|
+
if (tracked) trackedArchetypes.push(arch);
|
|
363
|
+
queryCacheVersion++;
|
|
364
|
+
}
|
|
365
|
+
return arch;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function ensureCapacity(arch: Archetype): void {
|
|
369
|
+
if (arch.count < arch.capacity) return;
|
|
370
|
+
const newCap = arch.capacity * 2;
|
|
371
|
+
arch.capacity = newCap;
|
|
372
|
+
for (const [type, store] of arch.components) {
|
|
373
|
+
if (store) {
|
|
374
|
+
growSoAStore(store, newCap);
|
|
375
|
+
if (arch.snapshots) {
|
|
376
|
+
const snap = arch.snapshots.get(type);
|
|
377
|
+
if (snap) growSnapshotStore(snap, store._schema, newCap);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function addToArchetype(arch: Archetype, entityId: EntityId, componentMap: Map<symbol, ComponentData>): void {
|
|
384
|
+
ensureCapacity(arch);
|
|
385
|
+
const idx = arch.count;
|
|
386
|
+
arch.entityIds[idx] = entityId;
|
|
387
|
+
for (const t of arch.types) {
|
|
388
|
+
const store = arch.components.get(t);
|
|
389
|
+
if (store) soaWrite(store, idx, componentMap.get(t));
|
|
390
|
+
}
|
|
391
|
+
arch.entityToIndex.set(entityId, idx);
|
|
392
|
+
arch.count++;
|
|
393
|
+
entityArchetype.set(entityId, arch);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function removeFromArchetype(arch: Archetype, entityId: EntityId): void {
|
|
397
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
398
|
+
const lastIdx = arch.count - 1;
|
|
399
|
+
|
|
400
|
+
if (idx !== lastIdx) {
|
|
401
|
+
const lastEntity = arch.entityIds[lastIdx];
|
|
402
|
+
arch.entityIds[idx] = lastEntity;
|
|
403
|
+
for (const [, store] of arch.components) {
|
|
404
|
+
if (store) soaSwap(store, idx, lastIdx);
|
|
405
|
+
}
|
|
406
|
+
arch.entityToIndex.set(lastEntity, idx);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
arch.entityIds.length = lastIdx;
|
|
410
|
+
arch.entityToIndex.delete(entityId);
|
|
411
|
+
arch.count--;
|
|
412
|
+
entityArchetype.delete(entityId);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function readComponentData(arch: Archetype, type: symbol, idx: number): Record<string, number | string | number[]> | undefined {
|
|
416
|
+
const store = arch.components.get(type);
|
|
417
|
+
if (!store) return undefined;
|
|
418
|
+
return soaRead(store, idx);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function getMatchingArchetypes(types: (symbol | ComponentDef)[], excludeTypes?: (symbol | ComponentDef)[]): Archetype[] {
|
|
422
|
+
const includeMask = computeMask(types);
|
|
423
|
+
const excludeMask = excludeTypes && excludeTypes.length > 0 ? computeMask(excludeTypes) : null;
|
|
424
|
+
const queryStr = maskKey(includeMask) + ':' + (excludeMask ? maskKey(excludeMask) : '');
|
|
425
|
+
|
|
426
|
+
const cached = queryCache.get(queryStr);
|
|
427
|
+
if (cached && cached.version === queryCacheVersion) {
|
|
428
|
+
return cached.archetypes;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const matching: Archetype[] = [];
|
|
432
|
+
for (const arch of archetypes.values()) {
|
|
433
|
+
if (maskContains(arch.key, includeMask) &&
|
|
434
|
+
(!excludeMask || maskDisjoint(arch.key, excludeMask))) {
|
|
435
|
+
matching.push(arch);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
queryCache.set(queryStr, { version: queryCacheVersion, archetypes: matching });
|
|
440
|
+
return matching;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function doDestroyEntity(id: EntityId): void {
|
|
444
|
+
const arch = entityArchetype.get(id);
|
|
445
|
+
if (arch) {
|
|
446
|
+
if (hooks) {
|
|
447
|
+
for (const type of arch.types) {
|
|
448
|
+
const pending = hooks.pendingRemove.get(type);
|
|
449
|
+
if (pending) pending.push(id);
|
|
450
|
+
}
|
|
451
|
+
if (hooks.removeCbs.size > 0) {
|
|
452
|
+
const idx = arch.entityToIndex.get(id)!;
|
|
453
|
+
let entitySnap: Map<symbol, Record<string, number | string | number[]>> | undefined;
|
|
454
|
+
for (const type of arch.types) {
|
|
455
|
+
if (hooks.removeCbs.has(type)) {
|
|
456
|
+
const store = arch.components.get(type);
|
|
457
|
+
if (store) {
|
|
458
|
+
if (!entitySnap) { entitySnap = new Map(); removedData.set(id, entitySnap); }
|
|
459
|
+
entitySnap.set(type, soaRead(store, idx));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(id);
|
|
466
|
+
removeFromArchetype(arch, id);
|
|
467
|
+
}
|
|
468
|
+
allEntityIds.delete(id);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function doAddComponent(entityId: EntityId, comp: ComponentDef, data?: ComponentData): void {
|
|
472
|
+
const type = toSym(comp);
|
|
473
|
+
const arch = entityArchetype.get(entityId);
|
|
474
|
+
|
|
475
|
+
if (!arch) {
|
|
476
|
+
const newArch = getOrCreateArchetype([type]);
|
|
477
|
+
addToArchetype(newArch, entityId, new Map([[type, data]]));
|
|
478
|
+
if (hooks) {
|
|
479
|
+
const pending = hooks.pendingAdd.get(type);
|
|
480
|
+
if (pending) pending.push(entityId);
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (arch.types.has(type)) {
|
|
486
|
+
const store = arch.components.get(type);
|
|
487
|
+
if (store) {
|
|
488
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
489
|
+
soaWrite(store, idx, data);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const newTypes = [...arch.types, type];
|
|
495
|
+
const newArch = getOrCreateArchetype(newTypes);
|
|
496
|
+
|
|
497
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
498
|
+
const map = new Map<symbol, ComponentData>([[type, data]]);
|
|
499
|
+
for (const t of arch.types) {
|
|
500
|
+
map.set(t, readComponentData(arch, t, idx));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
removeFromArchetype(arch, entityId);
|
|
504
|
+
addToArchetype(newArch, entityId, map);
|
|
505
|
+
if (hooks) {
|
|
506
|
+
const pending = hooks.pendingAdd.get(type);
|
|
507
|
+
if (pending) pending.push(entityId);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function doRemoveComponent(entityId: EntityId, comp: ComponentDef): void {
|
|
512
|
+
const type = toSym(comp);
|
|
513
|
+
const arch = entityArchetype.get(entityId);
|
|
514
|
+
if (!arch || !arch.types.has(type)) return;
|
|
515
|
+
|
|
516
|
+
if (hooks) {
|
|
517
|
+
const pending = hooks.pendingRemove.get(type);
|
|
518
|
+
if (pending) pending.push(entityId);
|
|
519
|
+
if (hooks.removeCbs.has(type)) {
|
|
520
|
+
const store = arch.components.get(type);
|
|
521
|
+
if (store) {
|
|
522
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
523
|
+
if (!removedData.has(entityId)) removedData.set(entityId, new Map());
|
|
524
|
+
removedData.get(entityId)!.set(type, soaRead(store, idx));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter)) destroyedSet.add(entityId);
|
|
530
|
+
|
|
531
|
+
if (arch.types.size === 1) {
|
|
532
|
+
removeFromArchetype(arch, entityId);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const newTypes: symbol[] = [];
|
|
537
|
+
for (const t of arch.types) {
|
|
538
|
+
if (t !== type) newTypes.push(t);
|
|
539
|
+
}
|
|
540
|
+
const newArch = getOrCreateArchetype(newTypes);
|
|
541
|
+
|
|
542
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
543
|
+
const map = new Map<symbol, ComponentData>();
|
|
544
|
+
for (const t of newTypes) {
|
|
545
|
+
map.set(t, readComponentData(arch, t, idx));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
removeFromArchetype(arch, entityId);
|
|
549
|
+
addToArchetype(newArch, entityId, map);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function flushDeferred(): void {
|
|
553
|
+
const ops = deferred.splice(0);
|
|
554
|
+
for (const op of ops) {
|
|
555
|
+
switch (op.kind) {
|
|
556
|
+
case 'add': doAddComponent(op.entityId, op.comp, op.data); break;
|
|
557
|
+
case 'remove': doRemoveComponent(op.entityId, op.comp); break;
|
|
558
|
+
case 'destroy': doDestroyEntity(op.entityId); break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
createEntity(): EntityId {
|
|
565
|
+
const id = nextId++;
|
|
566
|
+
allEntityIds.add(id);
|
|
567
|
+
return id;
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
destroyEntity(id: EntityId): void {
|
|
571
|
+
if (iterating > 0) {
|
|
572
|
+
deferred.push({ kind: 'destroy', entityId: id });
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
doDestroyEntity(id);
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
addComponent(entityId: EntityId, comp: ComponentDef, data?: ComponentData): void {
|
|
579
|
+
if (iterating > 0) {
|
|
580
|
+
// In-place overwrite (entity already has component) is safe — no migration
|
|
581
|
+
const type = toSym(comp);
|
|
582
|
+
const arch = entityArchetype.get(entityId);
|
|
583
|
+
if (arch && arch.types.has(type)) {
|
|
584
|
+
const store = arch.components.get(type);
|
|
585
|
+
if (store) {
|
|
586
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
587
|
+
soaWrite(store, idx, data);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
// Migration required — defer
|
|
592
|
+
deferred.push({ kind: 'add', entityId, comp, data });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
doAddComponent(entityId, comp, data);
|
|
596
|
+
},
|
|
597
|
+
|
|
598
|
+
removeComponent(entityId: EntityId, comp: ComponentDef): void {
|
|
599
|
+
if (iterating > 0) {
|
|
600
|
+
deferred.push({ kind: 'remove', entityId, comp });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
doRemoveComponent(entityId, comp);
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
getComponent(entityId: EntityId, comp: ComponentDef): Record<string, number | string | number[]> | undefined {
|
|
607
|
+
const type = toSym(comp);
|
|
608
|
+
const arch = entityArchetype.get(entityId);
|
|
609
|
+
if (arch) {
|
|
610
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
611
|
+
if (idx !== undefined) return readComponentData(arch, type, idx);
|
|
612
|
+
}
|
|
613
|
+
// Fallback: check recently-removed data (accessible during @OnRemoved hooks)
|
|
614
|
+
const removed = removedData.get(entityId);
|
|
615
|
+
if (removed) return removed.get(type);
|
|
616
|
+
return undefined;
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
get(entityId: EntityId, fieldRef: FieldRef): number | string | undefined {
|
|
620
|
+
const arch = entityArchetype.get(entityId);
|
|
621
|
+
if (arch) {
|
|
622
|
+
const store = arch.components.get(fieldRef._sym);
|
|
623
|
+
if (store) {
|
|
624
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
625
|
+
const size = store._arraySizes[fieldRef._field] || 0;
|
|
626
|
+
if (size > 0) {
|
|
627
|
+
const base = idx * size;
|
|
628
|
+
return (store._fields[fieldRef._field] as Float32Array).subarray(base, base + size) as unknown as number;
|
|
629
|
+
}
|
|
630
|
+
return (store._fields[fieldRef._field] as never[])[idx];
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
// Fallback: check recently-removed data (accessible during @OnRemoved hooks)
|
|
634
|
+
const removed = removedData.get(entityId);
|
|
635
|
+
if (removed) {
|
|
636
|
+
const compData = removed.get(fieldRef._sym);
|
|
637
|
+
if (compData) return compData[fieldRef._field] as number | string;
|
|
638
|
+
}
|
|
639
|
+
return undefined;
|
|
640
|
+
},
|
|
641
|
+
|
|
642
|
+
set(entityId: EntityId, fieldRef: FieldRef, value: number | string | ArrayLike<number>): void {
|
|
643
|
+
const arch = entityArchetype.get(entityId);
|
|
644
|
+
if (!arch) return;
|
|
645
|
+
const store = arch.components.get(fieldRef._sym);
|
|
646
|
+
if (!store) return;
|
|
647
|
+
const idx = arch.entityToIndex.get(entityId)!;
|
|
648
|
+
const size = store._arraySizes[fieldRef._field] || 0;
|
|
649
|
+
if (size > 0) {
|
|
650
|
+
(store._fields[fieldRef._field] as Float32Array).set(value as ArrayLike<number>, idx * size);
|
|
651
|
+
} else {
|
|
652
|
+
(store._fields[fieldRef._field] as never[])[idx] = value as never;
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
|
|
656
|
+
hasComponent(entityId: EntityId, comp: ComponentDef): boolean {
|
|
657
|
+
const type = toSym(comp);
|
|
658
|
+
const arch = entityArchetype.get(entityId);
|
|
659
|
+
return arch ? arch.types.has(type) : false;
|
|
660
|
+
},
|
|
661
|
+
|
|
662
|
+
query(includeTypes: ComponentDef[], excludeTypes?: ComponentDef[]): EntityId[] {
|
|
663
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
664
|
+
const result: EntityId[] = [];
|
|
665
|
+
for (let a = 0; a < matching.length; a++) {
|
|
666
|
+
const arch = matching[a];
|
|
667
|
+
const ids = arch.entityIds;
|
|
668
|
+
for (let i = 0; i < arch.count; i++) {
|
|
669
|
+
result.push(ids[i]);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
getAllEntities(): EntityId[] {
|
|
676
|
+
return [...allEntityIds];
|
|
677
|
+
},
|
|
678
|
+
|
|
679
|
+
createEntityWith(...args: unknown[]): EntityId {
|
|
680
|
+
const id = nextId++;
|
|
681
|
+
allEntityIds.add(id);
|
|
682
|
+
|
|
683
|
+
const types: symbol[] = [];
|
|
684
|
+
const map = new Map<symbol, ComponentData>();
|
|
685
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
686
|
+
const sym = toSym(args[i] as ComponentDef);
|
|
687
|
+
types.push(sym);
|
|
688
|
+
map.set(sym, args[i + 1] as ComponentData);
|
|
689
|
+
}
|
|
690
|
+
const arch = getOrCreateArchetype(types);
|
|
691
|
+
addToArchetype(arch, id, map);
|
|
692
|
+
|
|
693
|
+
if (hooks) {
|
|
694
|
+
for (let i = 0; i < types.length; i++) {
|
|
695
|
+
const pending = hooks.pendingAdd.get(types[i]);
|
|
696
|
+
if (pending) pending.push(id);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (createdSet && trackFilter && maskOverlaps(arch.key, trackFilter)) createdSet.add(id);
|
|
701
|
+
return id;
|
|
702
|
+
},
|
|
703
|
+
|
|
704
|
+
count(includeTypes: ComponentDef[], excludeTypes?: ComponentDef[]): number {
|
|
705
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
706
|
+
let total = 0;
|
|
707
|
+
for (let a = 0; a < matching.length; a++) {
|
|
708
|
+
total += matching[a].count;
|
|
709
|
+
}
|
|
710
|
+
return total;
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
forEach(includeTypes: ComponentDef[], callback: (view: ArchetypeView) => void, excludeTypes?: ComponentDef[]): void {
|
|
714
|
+
const matching = getMatchingArchetypes(includeTypes, excludeTypes);
|
|
715
|
+
iterating++;
|
|
716
|
+
try {
|
|
717
|
+
for (let a = 0; a < matching.length; a++) {
|
|
718
|
+
const arch = matching[a];
|
|
719
|
+
if (arch.count === 0) continue;
|
|
720
|
+
const snaps = arch.snapshots;
|
|
721
|
+
const view: ArchetypeView = {
|
|
722
|
+
id: arch.id,
|
|
723
|
+
entityIds: arch.entityIds,
|
|
724
|
+
count: arch.count,
|
|
725
|
+
snapshotEntityIds: arch.snapshotEntityIds,
|
|
726
|
+
snapshotCount: arch.snapshotCount,
|
|
727
|
+
field(ref: FieldRef) {
|
|
728
|
+
const store = arch.components.get(ref._sym);
|
|
729
|
+
if (!store) return undefined;
|
|
730
|
+
return store._fields[ref._field];
|
|
731
|
+
},
|
|
732
|
+
fieldStride(ref: FieldRef) {
|
|
733
|
+
const store = arch.components.get(ref._sym);
|
|
734
|
+
if (!store) return 1;
|
|
735
|
+
return store._arraySizes[ref._field] || 1;
|
|
736
|
+
},
|
|
737
|
+
snapshot(ref: FieldRef) {
|
|
738
|
+
if (!snaps) return undefined;
|
|
739
|
+
const snap = snaps.get(ref._sym);
|
|
740
|
+
if (!snap) return undefined;
|
|
741
|
+
return snap[ref._field];
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
callback(view);
|
|
745
|
+
}
|
|
746
|
+
} finally {
|
|
747
|
+
iterating--;
|
|
748
|
+
if (iterating === 0 && deferred.length > 0) {
|
|
749
|
+
flushDeferred();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
|
|
754
|
+
enableTracking(filterComponent: ComponentDef): void {
|
|
755
|
+
const bit = getBit(filterComponent);
|
|
756
|
+
const slots = slotsNeeded(bit + 1);
|
|
757
|
+
trackFilter = createMask(slots);
|
|
758
|
+
trackFilter = maskSetBit(trackFilter, bit);
|
|
759
|
+
createdSet = new Set();
|
|
760
|
+
destroyedSet = new Set();
|
|
761
|
+
for (const arch of archetypes.values()) {
|
|
762
|
+
if (maskOverlaps(arch.key, trackFilter) && !arch.snapshots) {
|
|
763
|
+
arch.snapshots = new Map();
|
|
764
|
+
arch.snapshotEntityIds = [];
|
|
765
|
+
arch.snapshotCount = 0;
|
|
766
|
+
for (const [t, store] of arch.components) {
|
|
767
|
+
if (store) {
|
|
768
|
+
arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
trackedArchetypes.push(arch);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
|
|
776
|
+
flushChanges() {
|
|
777
|
+
const result = { created: createdSet!, destroyed: destroyedSet! };
|
|
778
|
+
createdSet = new Set();
|
|
779
|
+
destroyedSet = new Set();
|
|
780
|
+
return result;
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
flushSnapshots(): void {
|
|
784
|
+
for (let a = 0; a < trackedArchetypes.length; a++) {
|
|
785
|
+
const arch = trackedArchetypes[a];
|
|
786
|
+
const count = arch.count;
|
|
787
|
+
const eids = arch.entityIds;
|
|
788
|
+
const snapEids = arch.snapshotEntityIds!;
|
|
789
|
+
for (let i = 0; i < count; i++) snapEids[i] = eids[i];
|
|
790
|
+
arch.snapshotCount = count;
|
|
791
|
+
for (const [type, store] of arch.components) {
|
|
792
|
+
if (!store) continue;
|
|
793
|
+
const snap = arch.snapshots!.get(type);
|
|
794
|
+
if (!snap) continue;
|
|
795
|
+
for (const field in store._schema) {
|
|
796
|
+
const src = store._fields[field];
|
|
797
|
+
const dst = snap[field];
|
|
798
|
+
const size = store._arraySizes[field] || 0;
|
|
799
|
+
const len = size > 0 ? count * size : count;
|
|
800
|
+
if ('set' in src) {
|
|
801
|
+
(dst as Exclude<SoAArrayValue, unknown[]>).set((src as Float32Array).subarray(0, len));
|
|
802
|
+
} else {
|
|
803
|
+
for (let i = 0; i < len; i++) (dst as unknown[])[i] = (src as unknown[])[i];
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
onAdd(comp: ComponentDef, callback: HookCallback): () => void {
|
|
811
|
+
const type = toSym(comp);
|
|
812
|
+
if (!hooks) {
|
|
813
|
+
hooks = {
|
|
814
|
+
addCbs: new Map(),
|
|
815
|
+
removeCbs: new Map(),
|
|
816
|
+
pendingAdd: new Map(),
|
|
817
|
+
pendingRemove: new Map(),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (!hooks.addCbs.has(type)) {
|
|
821
|
+
hooks.addCbs.set(type, []);
|
|
822
|
+
hooks.pendingAdd.set(type, []);
|
|
823
|
+
}
|
|
824
|
+
hooks.addCbs.get(type)!.push(callback);
|
|
825
|
+
return () => {
|
|
826
|
+
const cbs = hooks && hooks.addCbs.get(type);
|
|
827
|
+
if (!cbs) return;
|
|
828
|
+
const idx = cbs.indexOf(callback);
|
|
829
|
+
if (idx !== -1) cbs.splice(idx, 1);
|
|
830
|
+
if (cbs.length === 0) {
|
|
831
|
+
hooks!.addCbs.delete(type);
|
|
832
|
+
hooks!.pendingAdd.delete(type);
|
|
833
|
+
}
|
|
834
|
+
if (hooks!.addCbs.size === 0 && hooks!.removeCbs.size === 0) hooks = null;
|
|
835
|
+
};
|
|
836
|
+
},
|
|
837
|
+
|
|
838
|
+
onRemove(comp: ComponentDef, callback: HookCallback): () => void {
|
|
839
|
+
const type = toSym(comp);
|
|
840
|
+
if (!hooks) {
|
|
841
|
+
hooks = {
|
|
842
|
+
addCbs: new Map(),
|
|
843
|
+
removeCbs: new Map(),
|
|
844
|
+
pendingAdd: new Map(),
|
|
845
|
+
pendingRemove: new Map(),
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
if (!hooks.removeCbs.has(type)) {
|
|
849
|
+
hooks.removeCbs.set(type, []);
|
|
850
|
+
hooks.pendingRemove.set(type, []);
|
|
851
|
+
}
|
|
852
|
+
hooks.removeCbs.get(type)!.push(callback);
|
|
853
|
+
return () => {
|
|
854
|
+
const cbs = hooks && hooks.removeCbs.get(type);
|
|
855
|
+
if (!cbs) return;
|
|
856
|
+
const idx = cbs.indexOf(callback);
|
|
857
|
+
if (idx !== -1) cbs.splice(idx, 1);
|
|
858
|
+
if (cbs.length === 0) {
|
|
859
|
+
hooks!.removeCbs.delete(type);
|
|
860
|
+
hooks!.pendingRemove.delete(type);
|
|
861
|
+
}
|
|
862
|
+
if (hooks!.addCbs.size === 0 && hooks!.removeCbs.size === 0) hooks = null;
|
|
863
|
+
};
|
|
864
|
+
},
|
|
865
|
+
|
|
866
|
+
flushHooks(): void {
|
|
867
|
+
if (!hooks) return;
|
|
868
|
+
for (const [sym, pending] of hooks.pendingAdd) {
|
|
869
|
+
if (pending.length === 0) continue;
|
|
870
|
+
const cbs = hooks.addCbs.get(sym)!;
|
|
871
|
+
for (let c = 0; c < cbs.length; c++) {
|
|
872
|
+
for (let i = 0; i < pending.length; i++) cbs[c](pending[i]);
|
|
873
|
+
}
|
|
874
|
+
pending.length = 0;
|
|
875
|
+
}
|
|
876
|
+
for (const [sym, pending] of hooks.pendingRemove) {
|
|
877
|
+
if (pending.length === 0) continue;
|
|
878
|
+
const cbs = hooks.removeCbs.get(sym)!;
|
|
879
|
+
for (let c = 0; c < cbs.length; c++) {
|
|
880
|
+
for (let i = 0; i < pending.length; i++) cbs[c](pending[i]);
|
|
881
|
+
}
|
|
882
|
+
pending.length = 0;
|
|
883
|
+
}
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
commitRemovals(): void {
|
|
887
|
+
removedData.clear();
|
|
888
|
+
},
|
|
889
|
+
|
|
890
|
+
serialize(
|
|
891
|
+
symbolToName: Map<symbol, string>,
|
|
892
|
+
stripComponents: ComponentDef[] = [],
|
|
893
|
+
skipEntitiesWith: ComponentDef[] = [],
|
|
894
|
+
{ serializers }: { serializers?: Map<string, (data: unknown) => unknown> } = {}
|
|
895
|
+
): SerializedData {
|
|
896
|
+
const stripSymbols = new Set(stripComponents.map(toSym));
|
|
897
|
+
const skipSymbols = new Set(skipEntitiesWith.map(toSym));
|
|
898
|
+
const skipEntityIds = new Set<EntityId>();
|
|
899
|
+
|
|
900
|
+
if (skipSymbols.size > 0) {
|
|
901
|
+
for (const arch of archetypes.values()) {
|
|
902
|
+
let hasSkip = false;
|
|
903
|
+
for (const sym of skipSymbols) {
|
|
904
|
+
if (arch.types.has(sym)) { hasSkip = true; break; }
|
|
905
|
+
}
|
|
906
|
+
if (!hasSkip) continue;
|
|
907
|
+
for (let i = 0; i < arch.count; i++) {
|
|
908
|
+
skipEntityIds.add(arch.entityIds[i]);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const serializedComponents: Record<string, Record<string, unknown>> = {};
|
|
914
|
+
|
|
915
|
+
for (const arch of archetypes.values()) {
|
|
916
|
+
for (const [sym, store] of arch.components) {
|
|
917
|
+
if (!store) continue;
|
|
918
|
+
if (stripSymbols.has(sym) || skipSymbols.has(sym)) continue;
|
|
919
|
+
const name = symbolToName.get(sym);
|
|
920
|
+
if (!name) continue;
|
|
921
|
+
|
|
922
|
+
if (!serializedComponents[name]) {
|
|
923
|
+
serializedComponents[name] = {};
|
|
924
|
+
}
|
|
925
|
+
const entries = serializedComponents[name];
|
|
926
|
+
|
|
927
|
+
const customSerializer = serializers && serializers.get(name);
|
|
928
|
+
|
|
929
|
+
for (let i = 0; i < arch.count; i++) {
|
|
930
|
+
const entityId = arch.entityIds[i];
|
|
931
|
+
if (skipEntityIds.has(entityId)) continue;
|
|
932
|
+
const value = soaRead(store, i);
|
|
933
|
+
entries[entityId] = customSerializer ? customSerializer(value) : value;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
for (const name of Object.keys(serializedComponents)) {
|
|
939
|
+
if (Object.keys(serializedComponents[name]).length === 0) {
|
|
940
|
+
delete serializedComponents[name];
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const serializedEntities: EntityId[] = [];
|
|
945
|
+
for (const id of allEntityIds) {
|
|
946
|
+
if (!skipEntityIds.has(id)) serializedEntities.push(id);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
nextId,
|
|
951
|
+
entities: serializedEntities,
|
|
952
|
+
components: serializedComponents
|
|
953
|
+
};
|
|
954
|
+
},
|
|
955
|
+
|
|
956
|
+
deserialize(
|
|
957
|
+
data: SerializedData,
|
|
958
|
+
nameToSymbol: Record<string, ComponentDef>,
|
|
959
|
+
{ deserializers }: { deserializers?: Map<string, (data: unknown) => unknown> } = {}
|
|
960
|
+
): void {
|
|
961
|
+
allEntityIds.clear();
|
|
962
|
+
archetypes.clear();
|
|
963
|
+
entityArchetype.clear();
|
|
964
|
+
queryCache.clear();
|
|
965
|
+
queryCacheVersion = 0;
|
|
966
|
+
|
|
967
|
+
nextId = data.nextId;
|
|
968
|
+
|
|
969
|
+
const entityComponents = new Map<EntityId, Map<symbol, ComponentData>>();
|
|
970
|
+
|
|
971
|
+
for (const id of data.entities) {
|
|
972
|
+
allEntityIds.add(id);
|
|
973
|
+
entityComponents.set(id, new Map());
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
for (const [name, store] of Object.entries(data.components)) {
|
|
977
|
+
const entry = nameToSymbol[name];
|
|
978
|
+
if (!entry) continue;
|
|
979
|
+
const sym = toSym(entry);
|
|
980
|
+
|
|
981
|
+
const customDeserializer = deserializers && deserializers.get(name);
|
|
982
|
+
|
|
983
|
+
for (const [entityIdStr, compData] of Object.entries(store as Record<string, unknown>)) {
|
|
984
|
+
const entityId = Number(entityIdStr);
|
|
985
|
+
const obj = entityComponents.get(entityId);
|
|
986
|
+
if (!obj) continue;
|
|
987
|
+
|
|
988
|
+
if (customDeserializer) {
|
|
989
|
+
obj.set(sym, customDeserializer(compData) as ComponentData);
|
|
990
|
+
} else {
|
|
991
|
+
obj.set(sym, compData as ComponentData);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const groupedByKey = new Map<string, { entityId: EntityId; compMap: Map<symbol, ComponentData> }[]>();
|
|
997
|
+
for (const [entityId, compMap] of entityComponents) {
|
|
998
|
+
const types = [...compMap.keys()];
|
|
999
|
+
if (types.length === 0) continue;
|
|
1000
|
+
|
|
1001
|
+
const key = maskKey(computeMask(types));
|
|
1002
|
+
if (!groupedByKey.has(key)) {
|
|
1003
|
+
groupedByKey.set(key, []);
|
|
1004
|
+
}
|
|
1005
|
+
groupedByKey.get(key)!.push({ entityId, compMap });
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
for (const [, entries] of groupedByKey) {
|
|
1009
|
+
const types = [...entries[0].compMap.keys()];
|
|
1010
|
+
const arch = getOrCreateArchetype(types);
|
|
1011
|
+
|
|
1012
|
+
for (const { entityId, compMap } of entries) {
|
|
1013
|
+
addToArchetype(arch, entityId, compMap);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
}
|