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