@codehz/ecs 0.8.2 → 0.10.0
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.en.md +26 -3
- package/README.md +41 -4
- package/dist/builder.d.mts +348 -83
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/world.mjs +1922 -1400
- package/dist/world.mjs.map +1 -1
- package/examples/debug-observability.ts +92 -0
- package/examples/inventory-system-relations.ts +1 -1
- package/examples/parent-child-hierarchy.ts +18 -38
- package/examples/spatial-grid.ts +1 -1
- package/package.json +1 -1
- package/skills/ecs/SKILL.md +4 -4
- package/src/__tests__/component/singleton.test.ts +116 -35
- package/src/__tests__/core/archetype.test.ts +155 -13
- package/src/__tests__/core/bitset.test.ts +12 -0
- package/src/__tests__/entity/entity.test.ts +33 -0
- package/src/__tests__/entity/id-system.test.ts +40 -0
- package/src/__tests__/perf/comprehensive.perf.test.ts +6 -9
- package/src/__tests__/perf/serialization.perf.test.ts +242 -0
- package/src/__tests__/perf/{dontfragment-wildcard.perf.test.ts → sparse-wildcard.perf.test.ts} +13 -16
- package/src/__tests__/query/caching.test.ts +62 -0
- package/src/__tests__/query/filter.test.ts +16 -22
- package/src/__tests__/query/perf.test.ts +3 -5
- package/src/__tests__/relations/hierarchy.test.ts +208 -0
- package/src/__tests__/relations/{dont-fragment → sparse}/basic.test.ts +64 -69
- package/src/__tests__/relations/{dont-fragment → sparse}/query-notification.test.ts +17 -9
- package/src/__tests__/serialization/bounds.test.ts +133 -1
- package/src/__tests__/world/commands.test.ts +337 -0
- package/src/__tests__/world/component-management.test.ts +6 -5
- package/src/__tests__/world/debug-stats.test.ts +206 -0
- package/src/__tests__/world/multi-component-hooks.test.ts +44 -0
- package/src/__tests__/world/serialize.test.ts +17 -0
- package/src/__tests__/world/wildcard-relation-hooks.test.ts +127 -0
- package/src/archetype/archetype.ts +96 -46
- package/src/archetype/helpers.ts +7 -29
- package/src/archetype/store.ts +35 -20
- package/src/commands/buffer.ts +5 -2
- package/src/commands/changeset.ts +0 -31
- package/src/component/registry.ts +64 -63
- package/src/entity/index.ts +6 -3
- package/src/index.ts +15 -0
- package/src/query/filter.ts +4 -10
- package/src/query/query.ts +12 -12
- package/src/storage/serialization.ts +29 -2
- package/src/types/index.ts +71 -0
- package/src/world/archetype-manager.ts +283 -0
- package/src/world/command-executor.ts +258 -0
- package/src/world/commands.ts +44 -56
- package/src/world/debug-stats.ts +147 -0
- package/src/world/hooks.ts +8 -0
- package/src/world/operations.ts +88 -0
- package/src/world/serialization.ts +32 -18
- package/src/world/singleton.ts +51 -0
- package/src/world/world.ts +429 -457
package/dist/world.mjs
CHANGED
|
@@ -366,7 +366,7 @@ const ComponentIdForNames = /* @__PURE__ */ new Map();
|
|
|
366
366
|
const componentNames = new Array(COMPONENT_ID_MAX + 1);
|
|
367
367
|
const exclusiveFlags = new BitSet(COMPONENT_ID_MAX + 1);
|
|
368
368
|
const cascadeDeleteFlags = new BitSet(COMPONENT_ID_MAX + 1);
|
|
369
|
-
const
|
|
369
|
+
const sparseFlags = new BitSet(COMPONENT_ID_MAX + 1);
|
|
370
370
|
const componentMerges = new Array(COMPONENT_ID_MAX + 1);
|
|
371
371
|
/**
|
|
372
372
|
* Allocate a new component ID from the global allocator.
|
|
@@ -399,7 +399,7 @@ function component(nameOrOptions) {
|
|
|
399
399
|
if (options) {
|
|
400
400
|
if (options.exclusive) exclusiveFlags.set(id);
|
|
401
401
|
if (options.cascadeDelete) cascadeDeleteFlags.set(id);
|
|
402
|
-
if (options.dontFragment)
|
|
402
|
+
if (options.sparse || options.dontFragment) sparseFlags.set(id);
|
|
403
403
|
if (options.merge) componentMerges[id] = options.merge;
|
|
404
404
|
}
|
|
405
405
|
return id;
|
|
@@ -468,20 +468,21 @@ function isExclusiveComponent(id) {
|
|
|
468
468
|
return exclusiveFlags.has(id);
|
|
469
469
|
}
|
|
470
470
|
/**
|
|
471
|
-
* Check if a component is marked as `
|
|
471
|
+
* Check if a component is marked as `sparse` (sparse storage for relations).
|
|
472
472
|
*
|
|
473
|
-
* When a component has `
|
|
474
|
-
*
|
|
475
|
-
*
|
|
473
|
+
* When a component has `sparse: true`, relations using it do not cause archetype
|
|
474
|
+
* fragmentation — entities with different relation targets can share the same
|
|
475
|
+
* archetype. This is a fast O(1) bitset lookup. The legacy `dontFragment` key
|
|
476
|
+
* is still accepted and sets the same internal flag.
|
|
476
477
|
*
|
|
477
478
|
* @param id - The component ID to check.
|
|
478
|
-
* @returns `true` if the component was created with `
|
|
479
|
+
* @returns `true` if the component was created with `sparse: true` (or the
|
|
480
|
+
* legacy `dontFragment: true`).
|
|
479
481
|
*
|
|
480
|
-
* @see {@link ComponentOptions.
|
|
481
|
-
* `dontFragment` prevents archetype fragmentation.
|
|
482
|
+
* @see {@link ComponentOptions.sparse} for the full explanation of sparse storage.
|
|
482
483
|
*/
|
|
483
|
-
function
|
|
484
|
-
return
|
|
484
|
+
function isSparseComponent(id) {
|
|
485
|
+
return sparseFlags.has(id);
|
|
485
486
|
}
|
|
486
487
|
/**
|
|
487
488
|
* Generic optimized function to check whether a relation ID's base component
|
|
@@ -491,9 +492,8 @@ function isDontFragmentComponent(id) {
|
|
|
491
492
|
* ID and checking: (1) the ID is a valid relation, (2) the component ID is in the
|
|
492
493
|
* valid range, (3) the target satisfies the condition, and (4) the flag bit is set.
|
|
493
494
|
*
|
|
494
|
-
* Used as the fast-path implementation for `
|
|
495
|
-
* `
|
|
496
|
-
* and `isCascadeDeleteRelation`.
|
|
495
|
+
* Used as the fast-path implementation for `isSparseRelation`, `isSparseWildcard`,
|
|
496
|
+
* `isExclusiveRelation`, `isExclusiveWildcard`, and `isCascadeDeleteRelation`.
|
|
497
497
|
*
|
|
498
498
|
* @param id - The entity/relation ID to check.
|
|
499
499
|
* @param flagBitSet - The bitset tracking which component IDs have the flag.
|
|
@@ -509,52 +509,40 @@ function checkRelationFlag(id, flagBitSet, targetCondition) {
|
|
|
509
509
|
return isValidComponentId(componentId) && targetCondition(targetId) && flagBitSet.has(componentId);
|
|
510
510
|
}
|
|
511
511
|
/**
|
|
512
|
-
* Check if an ID is a specific (non-wildcard) relation backed by a `
|
|
513
|
-
* component.
|
|
512
|
+
* Check if an ID is a specific (non-wildcard) relation backed by a `sparse`
|
|
513
|
+
* component (i.e. stored in the side sparse store rather than the archetype).
|
|
514
514
|
*
|
|
515
515
|
* This is used in hot paths (archetype resolution, command processing) to determine
|
|
516
|
-
* whether a relation should be excluded from the archetype signature.
|
|
517
|
-
* `dontFragment` components are stored in the shared {@link DontFragmentStore} instead
|
|
518
|
-
* of being part of the archetype's component type list.
|
|
519
|
-
*
|
|
520
|
-
* This is an optimized function that avoids the overhead of `getDetailedIdType`
|
|
521
|
-
* by directly decoding and checking the relation's component ID against the
|
|
522
|
-
* `dontFragment` bitset.
|
|
516
|
+
* whether a relation should be excluded from the archetype signature.
|
|
523
517
|
*
|
|
524
518
|
* @param id - The entity/relation ID to check (must be a relation ID, not a plain
|
|
525
519
|
* component ID).
|
|
526
520
|
* @returns `true` if this is a specific-target relation (not wildcard) whose base
|
|
527
|
-
* component was created with `dontFragment: true
|
|
521
|
+
* component was created with `sparse: true` (or legacy `dontFragment: true`).
|
|
528
522
|
*
|
|
529
|
-
* @see {@link
|
|
530
|
-
* @see {@link ComponentOptions.
|
|
523
|
+
* @see {@link isSparseWildcard} for the wildcard variant.
|
|
524
|
+
* @see {@link ComponentOptions.sparse} for the full explanation.
|
|
531
525
|
*/
|
|
532
|
-
function
|
|
533
|
-
return checkRelationFlag(id,
|
|
526
|
+
function isSparseRelation(id) {
|
|
527
|
+
return checkRelationFlag(id, sparseFlags, (targetId) => targetId !== 0);
|
|
534
528
|
}
|
|
535
529
|
/**
|
|
536
530
|
* Check if an ID is a wildcard relation (`relation(Comp, "*")`) backed by a
|
|
537
|
-
* `
|
|
538
|
-
*
|
|
539
|
-
* Wildcard markers for `dontFragment` components are placed in the archetype
|
|
540
|
-
* component list so that queries can discover archetypes containing entities
|
|
541
|
-
* with that relation type. This function is used in `filterRegularComponentTypes`
|
|
542
|
-
* to **keep** these wildcard markers in the archetype signature while stripping
|
|
543
|
-
* out specific-target `dontFragment` relations.
|
|
531
|
+
* `sparse` component.
|
|
544
532
|
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
533
|
+
* Wildcard markers for sparse components are placed in the archetype component
|
|
534
|
+
* list so that queries can discover archetypes containing entities with that
|
|
535
|
+
* relation type.
|
|
548
536
|
*
|
|
549
537
|
* @param id - The entity/relation ID to check.
|
|
550
538
|
* @returns `true` if this is a wildcard relation (`"*"` target) whose base
|
|
551
|
-
* component was created with `dontFragment: true
|
|
539
|
+
* component was created with `sparse: true` (or legacy `dontFragment: true`).
|
|
552
540
|
*
|
|
553
|
-
* @see {@link
|
|
554
|
-
* @see {@link ComponentOptions.
|
|
541
|
+
* @see {@link isSparseRelation} for the specific-target variant.
|
|
542
|
+
* @see {@link ComponentOptions.sparse} for the full explanation.
|
|
555
543
|
*/
|
|
556
|
-
function
|
|
557
|
-
return checkRelationFlag(id,
|
|
544
|
+
function isSparseWildcard(id) {
|
|
545
|
+
return checkRelationFlag(id, sparseFlags, (targetId) => targetId === 0);
|
|
558
546
|
}
|
|
559
547
|
/**
|
|
560
548
|
* Check if a relation ID is a cascade delete entity-relation.
|
|
@@ -634,497 +622,125 @@ var EntityBuilder = class {
|
|
|
634
622
|
}
|
|
635
623
|
};
|
|
636
624
|
//#endregion
|
|
637
|
-
//#region src/
|
|
638
|
-
/**
|
|
639
|
-
* Normalize component type collections into a stable ascending order.
|
|
640
|
-
* This keeps cache keys and archetype signatures deterministic.
|
|
641
|
-
*/
|
|
642
|
-
function normalizeComponentTypes(componentTypes) {
|
|
643
|
-
return [...componentTypes].sort((a, b) => a - b);
|
|
644
|
-
}
|
|
645
|
-
//#endregion
|
|
646
|
-
//#region src/types/index.ts
|
|
647
|
-
function isOptionalEntityId(type) {
|
|
648
|
-
return typeof type === "object" && type !== null && "optional" in type;
|
|
649
|
-
}
|
|
650
|
-
//#endregion
|
|
651
|
-
//#region src/utils/utils.ts
|
|
652
|
-
/**
|
|
653
|
-
* Utility functions for ECS library
|
|
654
|
-
*/
|
|
625
|
+
//#region src/world/singleton.ts
|
|
655
626
|
/**
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
627
|
+
* Explicit handle for a singleton component (component-as-entity).
|
|
628
|
+
*
|
|
629
|
+
* This provides an explicit and concise API for singleton components without
|
|
630
|
+
* overloading `world.set()` semantics.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* const config = world.singleton(Config);
|
|
634
|
+
* config.set({ debug: true });
|
|
635
|
+
* world.sync();
|
|
636
|
+
* console.log(config.get());
|
|
661
637
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
638
|
+
var SingletonHandle = class {
|
|
639
|
+
componentId;
|
|
640
|
+
ops;
|
|
641
|
+
constructor(componentId, ops) {
|
|
642
|
+
this.componentId = componentId;
|
|
643
|
+
this.ops = ops;
|
|
667
644
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
//#endregion
|
|
671
|
-
//#region src/archetype/helpers.ts
|
|
672
|
-
/**
|
|
673
|
-
* Check if a components map has any wildcard relations matching a component ID
|
|
674
|
-
* @param components - Component entity's components map
|
|
675
|
-
* @param wildcardComponentId - The component ID to match
|
|
676
|
-
* @returns True if at least one matching relation exists
|
|
677
|
-
*/
|
|
678
|
-
function hasWildcardRelation(components, wildcardComponentId) {
|
|
679
|
-
for (const relId of components.keys()) if (isRelationId(relId)) {
|
|
680
|
-
if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
|
|
645
|
+
has() {
|
|
646
|
+
return this.ops.has();
|
|
681
647
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
/**
|
|
685
|
-
* Check if a detailed type represents a relation (entity or component)
|
|
686
|
-
*/
|
|
687
|
-
function isRelationType(detailedType) {
|
|
688
|
-
return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Check if a component type matches a given component ID for relations
|
|
692
|
-
*/
|
|
693
|
-
function matchesRelationComponentId(componentType, componentId) {
|
|
694
|
-
const detailedType = getDetailedIdType(componentType);
|
|
695
|
-
return isRelationType(detailedType) && detailedType.componentId === componentId;
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Build cache key for component types
|
|
699
|
-
*/
|
|
700
|
-
function buildCacheKey(componentTypes) {
|
|
701
|
-
return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Get data source for wildcard relations from component types
|
|
705
|
-
*/
|
|
706
|
-
function getWildcardRelationDataSource(componentTypes, componentId, optional) {
|
|
707
|
-
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
708
|
-
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* Build wildcard relation value from matching relations.
|
|
712
|
-
* Now receives the DontFragmentStore directly for efficient per-component lookups.
|
|
713
|
-
*/
|
|
714
|
-
function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, dontFragmentStore, entityId, optional) {
|
|
715
|
-
const relations = [];
|
|
716
|
-
const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
717
|
-
for (const relType of matchingRelations || []) {
|
|
718
|
-
const data = getDataAtIndex(relType);
|
|
719
|
-
const targetId = getTargetIdFromRelationId(relType);
|
|
720
|
-
relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
648
|
+
get() {
|
|
649
|
+
return this.ops.get();
|
|
721
650
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
for (const m of dfMatches) relations.push(m);
|
|
651
|
+
getOptional() {
|
|
652
|
+
return this.ops.getOptional();
|
|
725
653
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const componentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
729
|
-
throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
|
|
730
|
-
}
|
|
731
|
-
return;
|
|
654
|
+
remove() {
|
|
655
|
+
this.ops.remove();
|
|
732
656
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Build regular component value from data source
|
|
737
|
-
*/
|
|
738
|
-
function buildRegularComponentValue(dataSource, entityIndex, optional) {
|
|
739
|
-
if (dataSource === void 0) {
|
|
740
|
-
if (optional) return void 0;
|
|
741
|
-
throw new Error(`Component data not found for mandatory component type`);
|
|
657
|
+
set(...args) {
|
|
658
|
+
this.ops.set(args[0]);
|
|
742
659
|
}
|
|
743
|
-
|
|
744
|
-
const result = data === MISSING_COMPONENT ? void 0 : data;
|
|
745
|
-
return optional ? { value: result } : result;
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Build a single component value based on its type
|
|
749
|
-
*/
|
|
750
|
-
function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, dontFragmentRelations) {
|
|
751
|
-
const optional = isOptionalEntityId(compType);
|
|
752
|
-
const actualType = optional ? compType.optional : compType;
|
|
753
|
-
if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], dontFragmentRelations, entityId, optional);
|
|
754
|
-
else return buildRegularComponentValue(dataSource, entityIndex, optional);
|
|
755
|
-
}
|
|
660
|
+
};
|
|
756
661
|
//#endregion
|
|
757
|
-
//#region src/archetype/
|
|
758
|
-
/**
|
|
759
|
-
* Special value to represent missing component data
|
|
760
|
-
*/
|
|
761
|
-
const MISSING_COMPONENT = Symbol("missing component");
|
|
662
|
+
//#region src/archetype/store.ts
|
|
762
663
|
/**
|
|
763
|
-
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
664
|
+
* Production implementation of SparseStore.
|
|
665
|
+
*
|
|
666
|
+
* Internal layout (optimized):
|
|
667
|
+
* - byComponent: baseComponentId → (entityId → RelationEntry)
|
|
668
|
+
* RelationEntry uses a single-value form for the common exclusive case (1 target),
|
|
669
|
+
* avoiding Map allocation for the vast majority of usage.
|
|
670
|
+
* - entityIndex: entityId → Set<baseComponentId>
|
|
671
|
+
* Lightweight reverse index.
|
|
766
672
|
*/
|
|
767
|
-
var
|
|
768
|
-
/**
|
|
769
|
-
* The component types that define this archetype
|
|
770
|
-
*/
|
|
771
|
-
componentTypes;
|
|
772
|
-
/**
|
|
773
|
-
* Set version of componentTypes for O(1) lookups in hot paths
|
|
774
|
-
*/
|
|
775
|
-
componentTypeSet;
|
|
776
|
-
/**
|
|
777
|
-
* List of entities in this archetype
|
|
778
|
-
*/
|
|
779
|
-
entities = [];
|
|
780
|
-
/**
|
|
781
|
-
* Component data storage - maps component type to array of component data
|
|
782
|
-
* Each array index corresponds to the entity index in the entities array
|
|
783
|
-
*/
|
|
784
|
-
componentData = /* @__PURE__ */ new Map();
|
|
785
|
-
/**
|
|
786
|
-
* Reverse mapping from entity to its index in this archetype
|
|
787
|
-
*/
|
|
788
|
-
entityToIndex = /* @__PURE__ */ new Map();
|
|
789
|
-
/**
|
|
790
|
-
* DontFragmentStore (keyed primarily by relation ComponentId).
|
|
791
|
-
* Uses optimized RelationEntry (single/multi) for the common exclusive case.
|
|
792
|
-
* See store.ts for implementation details.
|
|
793
|
-
*/
|
|
794
|
-
dontFragmentRelations;
|
|
795
|
-
/**
|
|
796
|
-
* Multi-hooks that match this archetype
|
|
797
|
-
*/
|
|
798
|
-
matchingMultiHooks = /* @__PURE__ */ new Set();
|
|
673
|
+
var SparseStoreImpl = class {
|
|
799
674
|
/**
|
|
800
|
-
*
|
|
675
|
+
* Primary storage, keyed by the base relation component ID.
|
|
801
676
|
*/
|
|
802
|
-
|
|
803
|
-
constructor(componentTypes, dontFragmentRelations) {
|
|
804
|
-
this.componentTypes = normalizeComponentTypes(componentTypes);
|
|
805
|
-
this.componentTypeSet = new Set(this.componentTypes);
|
|
806
|
-
this.dontFragmentRelations = dontFragmentRelations;
|
|
807
|
-
for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
|
|
808
|
-
}
|
|
809
|
-
get size() {
|
|
810
|
-
return this.entities.length;
|
|
811
|
-
}
|
|
677
|
+
byComponent = /* @__PURE__ */ new Map();
|
|
812
678
|
/**
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
816
|
-
* @note This method handles unsorted input by internally sorting for comparison
|
|
679
|
+
* Reverse index: which base component kinds an entity participates in.
|
|
680
|
+
* Only required to support getAllForEntity and deleteEntity efficiently.
|
|
681
|
+
* The primary storage (byComponent) is deliberately not optimized for these operations.
|
|
817
682
|
*/
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
683
|
+
entityIndex = /* @__PURE__ */ new Map();
|
|
684
|
+
getValue(entityId, relationType) {
|
|
685
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
686
|
+
if (componentId === void 0) return void 0;
|
|
687
|
+
const entities = this.byComponent.get(componentId);
|
|
688
|
+
if (!entities) return void 0;
|
|
689
|
+
const entry = entities.get(entityId);
|
|
690
|
+
if (!entry) return void 0;
|
|
691
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
692
|
+
if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
|
|
693
|
+
else {
|
|
694
|
+
const item = entry.targets.get(targetId);
|
|
695
|
+
return item ? item.data : void 0;
|
|
831
696
|
}
|
|
832
|
-
this.addDontFragmentRelations(entityId, componentData);
|
|
833
697
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
698
|
+
setValue(entityId, relationType, data) {
|
|
699
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
700
|
+
if (componentId === void 0) throw new Error("setValue called with a non-relation type on SparseStore");
|
|
701
|
+
let entities = this.byComponent.get(componentId);
|
|
702
|
+
if (!entities) {
|
|
703
|
+
entities = /* @__PURE__ */ new Map();
|
|
704
|
+
this.byComponent.set(componentId, entities);
|
|
839
705
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
706
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
707
|
+
let entry = entities.get(entityId);
|
|
708
|
+
if (!entry) {
|
|
709
|
+
entry = {
|
|
710
|
+
type: "single",
|
|
711
|
+
relationType,
|
|
712
|
+
target: targetId,
|
|
713
|
+
data
|
|
714
|
+
};
|
|
715
|
+
entities.set(entityId, entry);
|
|
716
|
+
} else if (entry.type === "single") if (entry.target === targetId) {
|
|
717
|
+
entry.data = data;
|
|
718
|
+
entry.relationType = relationType;
|
|
719
|
+
} else {
|
|
720
|
+
const targets = /* @__PURE__ */ new Map();
|
|
721
|
+
targets.set(entry.target, {
|
|
722
|
+
relationType: entry.relationType,
|
|
723
|
+
data: entry.data
|
|
724
|
+
});
|
|
725
|
+
targets.set(targetId, {
|
|
726
|
+
relationType,
|
|
727
|
+
data
|
|
728
|
+
});
|
|
729
|
+
entities.set(entityId, {
|
|
730
|
+
type: "multi",
|
|
731
|
+
targets
|
|
732
|
+
});
|
|
848
733
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
getEntityDontFragmentRelations(entityId) {
|
|
860
|
-
const tuples = this.dontFragmentRelations.getAllForEntity(entityId);
|
|
861
|
-
if (tuples.length === 0) return void 0;
|
|
862
|
-
const map = /* @__PURE__ */ new Map();
|
|
863
|
-
for (const [relType, data] of tuples) map.set(relType, data);
|
|
864
|
-
return map;
|
|
865
|
-
}
|
|
866
|
-
dump() {
|
|
867
|
-
return this.entities.map((entity, i) => {
|
|
868
|
-
const components = /* @__PURE__ */ new Map();
|
|
869
|
-
for (const componentType of this.componentTypes) {
|
|
870
|
-
const data = this.getComponentData(componentType)[i];
|
|
871
|
-
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
872
|
-
}
|
|
873
|
-
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
|
|
874
|
-
for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
|
|
875
|
-
return {
|
|
876
|
-
entity,
|
|
877
|
-
components
|
|
878
|
-
};
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
removeEntity(entityId) {
|
|
882
|
-
const index = this.entityToIndex.get(entityId);
|
|
883
|
-
if (index === void 0) return void 0;
|
|
884
|
-
const removedData = /* @__PURE__ */ new Map();
|
|
885
|
-
for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
|
|
886
|
-
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entityId);
|
|
887
|
-
for (const [componentType, data] of dontFragmentTuples) removedData.set(componentType, data);
|
|
888
|
-
this.dontFragmentRelations.deleteEntity(entityId);
|
|
889
|
-
this.entityToIndex.delete(entityId);
|
|
890
|
-
const lastIndex = this.entities.length - 1;
|
|
891
|
-
if (index !== lastIndex) {
|
|
892
|
-
const lastEntity = this.entities[lastIndex];
|
|
893
|
-
this.entities[index] = lastEntity;
|
|
894
|
-
this.entityToIndex.set(lastEntity, index);
|
|
895
|
-
for (const componentType of this.componentTypes) {
|
|
896
|
-
const dataArray = this.getComponentData(componentType);
|
|
897
|
-
dataArray[index] = dataArray[lastIndex];
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
this.entities.pop();
|
|
901
|
-
for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
|
|
902
|
-
return removedData;
|
|
903
|
-
}
|
|
904
|
-
exists(entityId) {
|
|
905
|
-
return this.entityToIndex.has(entityId);
|
|
906
|
-
}
|
|
907
|
-
get(entityId, componentType) {
|
|
908
|
-
const index = this.entityToIndex.get(entityId);
|
|
909
|
-
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
910
|
-
if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
|
|
911
|
-
return this.getRegularComponent(entityId, index, componentType);
|
|
912
|
-
}
|
|
913
|
-
getWildcardRelations(entityId, index, componentType) {
|
|
914
|
-
const componentId = getComponentIdFromRelationId(componentType);
|
|
915
|
-
const relations = [];
|
|
916
|
-
for (const relType of this.componentTypes) {
|
|
917
|
-
const relDetailed = getDetailedIdType(relType);
|
|
918
|
-
if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
|
|
919
|
-
const dataArray = this.getComponentData(relType);
|
|
920
|
-
if (dataArray && dataArray[index] !== void 0) {
|
|
921
|
-
const data = dataArray[index];
|
|
922
|
-
relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
if (componentId !== void 0) {
|
|
927
|
-
const matches = this.dontFragmentRelations.getRelationsForComponent(entityId, componentId);
|
|
928
|
-
for (const m of matches) relations.push(m);
|
|
929
|
-
}
|
|
930
|
-
return relations;
|
|
931
|
-
}
|
|
932
|
-
getRegularComponent(entityId, index, componentType) {
|
|
933
|
-
if (this.componentTypeSet.has(componentType)) {
|
|
934
|
-
const data = this.getComponentData(componentType)[index];
|
|
935
|
-
if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
936
|
-
return data;
|
|
937
|
-
}
|
|
938
|
-
if (this.dontFragmentRelations.getValue(entityId, componentType) !== void 0 || this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.dontFragmentRelations.getValue(entityId, componentType);
|
|
939
|
-
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
940
|
-
}
|
|
941
|
-
getOptional(entityId, componentType) {
|
|
942
|
-
const index = this.entityToIndex.get(entityId);
|
|
943
|
-
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
944
|
-
if (this.componentTypeSet.has(componentType)) {
|
|
945
|
-
const data = this.getComponentData(componentType)[index];
|
|
946
|
-
if (data === MISSING_COMPONENT) return void 0;
|
|
947
|
-
return { value: data };
|
|
948
|
-
}
|
|
949
|
-
const value = this.dontFragmentRelations.getValue(entityId, componentType);
|
|
950
|
-
if (value !== void 0) return { value };
|
|
951
|
-
if (this.dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.dontFragmentRelations.getValue(entityId, componentType) };
|
|
952
|
-
}
|
|
953
|
-
set(entityId, componentType, data) {
|
|
954
|
-
const index = this.entityToIndex.get(entityId);
|
|
955
|
-
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
956
|
-
if (this.componentData.has(componentType)) {
|
|
957
|
-
this.getComponentData(componentType)[index] = data;
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
const detailedType = getDetailedIdType(componentType);
|
|
961
|
-
if (isRelationType(detailedType) && isDontFragmentComponent(detailedType.componentId)) {
|
|
962
|
-
this.dontFragmentRelations.setValue(entityId, componentType, data);
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
966
|
-
}
|
|
967
|
-
getEntities() {
|
|
968
|
-
return this.entities;
|
|
969
|
-
}
|
|
970
|
-
getEntityToIndexMap() {
|
|
971
|
-
return this.entityToIndex;
|
|
972
|
-
}
|
|
973
|
-
getComponentData(componentType) {
|
|
974
|
-
const data = this.componentData.get(componentType);
|
|
975
|
-
if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
976
|
-
return data;
|
|
977
|
-
}
|
|
978
|
-
getOptionalComponentData(componentType) {
|
|
979
|
-
return this.componentData.get(componentType);
|
|
980
|
-
}
|
|
981
|
-
getCachedComponentDataSources(componentTypes) {
|
|
982
|
-
const cacheKey = buildCacheKey(componentTypes);
|
|
983
|
-
return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
|
|
984
|
-
}
|
|
985
|
-
getComponentDataSource(compType) {
|
|
986
|
-
const optional = isOptionalEntityId(compType);
|
|
987
|
-
const actualType = optional ? compType.optional : compType;
|
|
988
|
-
if (getIdType(actualType) === "wildcard-relation") {
|
|
989
|
-
const componentId = getComponentIdFromRelationId(actualType);
|
|
990
|
-
return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
|
|
991
|
-
}
|
|
992
|
-
return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
993
|
-
}
|
|
994
|
-
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
|
|
995
|
-
return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.dontFragmentRelations));
|
|
996
|
-
}
|
|
997
|
-
getEntitiesWithComponents(componentTypes) {
|
|
998
|
-
const result = [];
|
|
999
|
-
this.appendEntitiesWithComponents(componentTypes, result);
|
|
1000
|
-
return result;
|
|
1001
|
-
}
|
|
1002
|
-
appendEntitiesWithComponents(componentTypes, result) {
|
|
1003
|
-
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
1004
|
-
result.push({
|
|
1005
|
-
entity,
|
|
1006
|
-
components
|
|
1007
|
-
});
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
*iterateWithComponents(componentTypes) {
|
|
1011
|
-
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1012
|
-
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1013
|
-
const entity = this.entities[entityIndex];
|
|
1014
|
-
yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
forEachWithComponents(componentTypes, callback) {
|
|
1018
|
-
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1019
|
-
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1020
|
-
const entity = this.entities[entityIndex];
|
|
1021
|
-
callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
forEach(callback) {
|
|
1025
|
-
for (let i = 0; i < this.entities.length; i++) {
|
|
1026
|
-
const entity = this.entities[i];
|
|
1027
|
-
const components = /* @__PURE__ */ new Map();
|
|
1028
|
-
for (const componentType of this.componentTypes) {
|
|
1029
|
-
const data = this.getComponentData(componentType)[i];
|
|
1030
|
-
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1031
|
-
}
|
|
1032
|
-
const dontFragmentTuples = this.dontFragmentRelations.getAllForEntity(entity);
|
|
1033
|
-
for (const [componentType, data] of dontFragmentTuples) components.set(componentType, data);
|
|
1034
|
-
callback(entity, components);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
hasRelationWithComponentId(componentId) {
|
|
1038
|
-
for (const componentType of this.componentTypes) {
|
|
1039
|
-
const detailedType = getDetailedIdType(componentType);
|
|
1040
|
-
if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
|
|
1041
|
-
}
|
|
1042
|
-
for (const entityId of this.entities) if (this.dontFragmentRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
|
|
1043
|
-
return false;
|
|
1044
|
-
}
|
|
1045
|
-
};
|
|
1046
|
-
//#endregion
|
|
1047
|
-
//#region src/archetype/store.ts
|
|
1048
|
-
/**
|
|
1049
|
-
* Production implementation of DontFragmentStore.
|
|
1050
|
-
*
|
|
1051
|
-
* Internal layout (optimized):
|
|
1052
|
-
* - byComponent: baseComponentId → (entityId → RelationEntry)
|
|
1053
|
-
* RelationEntry uses a single-value form for the common exclusive case (1 target),
|
|
1054
|
-
* avoiding Map allocation entirely for the vast majority of dontFragment usage.
|
|
1055
|
-
* - entityIndex: entityId → Set<baseComponentId>
|
|
1056
|
-
* Lightweight reverse index.
|
|
1057
|
-
*/
|
|
1058
|
-
var DontFragmentStoreImpl = class {
|
|
1059
|
-
/**
|
|
1060
|
-
* Primary storage, keyed by the base relation component ID.
|
|
1061
|
-
*/
|
|
1062
|
-
byComponent = /* @__PURE__ */ new Map();
|
|
1063
|
-
/**
|
|
1064
|
-
* Reverse index: which base component kinds an entity participates in.
|
|
1065
|
-
* Used only by the infrequent getAllForEntity / deleteEntity paths.
|
|
1066
|
-
*/
|
|
1067
|
-
entityIndex = /* @__PURE__ */ new Map();
|
|
1068
|
-
getValue(entityId, relationType) {
|
|
1069
|
-
const componentId = getComponentIdFromRelationId(relationType);
|
|
1070
|
-
if (componentId === void 0) return void 0;
|
|
1071
|
-
const entities = this.byComponent.get(componentId);
|
|
1072
|
-
if (!entities) return void 0;
|
|
1073
|
-
const entry = entities.get(entityId);
|
|
1074
|
-
if (!entry) return void 0;
|
|
1075
|
-
const targetId = getTargetIdFromRelationId(relationType);
|
|
1076
|
-
if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
|
|
1077
|
-
else {
|
|
1078
|
-
const item = entry.targets.get(targetId);
|
|
1079
|
-
return item ? item.data : void 0;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
setValue(entityId, relationType, data) {
|
|
1083
|
-
const componentId = getComponentIdFromRelationId(relationType);
|
|
1084
|
-
if (componentId === void 0) throw new Error("setValue called with a non-relation type on DontFragmentStore");
|
|
1085
|
-
let entities = this.byComponent.get(componentId);
|
|
1086
|
-
if (!entities) {
|
|
1087
|
-
entities = /* @__PURE__ */ new Map();
|
|
1088
|
-
this.byComponent.set(componentId, entities);
|
|
1089
|
-
}
|
|
1090
|
-
const targetId = getTargetIdFromRelationId(relationType);
|
|
1091
|
-
let entry = entities.get(entityId);
|
|
1092
|
-
if (!entry) {
|
|
1093
|
-
entry = {
|
|
1094
|
-
type: "single",
|
|
1095
|
-
relationType,
|
|
1096
|
-
target: targetId,
|
|
1097
|
-
data
|
|
1098
|
-
};
|
|
1099
|
-
entities.set(entityId, entry);
|
|
1100
|
-
} else if (entry.type === "single") if (entry.target === targetId) {
|
|
1101
|
-
entry.data = data;
|
|
1102
|
-
entry.relationType = relationType;
|
|
1103
|
-
} else {
|
|
1104
|
-
const targets = /* @__PURE__ */ new Map();
|
|
1105
|
-
targets.set(entry.target, {
|
|
1106
|
-
relationType: entry.relationType,
|
|
1107
|
-
data: entry.data
|
|
1108
|
-
});
|
|
1109
|
-
targets.set(targetId, {
|
|
1110
|
-
relationType,
|
|
1111
|
-
data
|
|
1112
|
-
});
|
|
1113
|
-
entities.set(entityId, {
|
|
1114
|
-
type: "multi",
|
|
1115
|
-
targets
|
|
1116
|
-
});
|
|
1117
|
-
}
|
|
1118
|
-
else entry.targets.set(targetId, {
|
|
1119
|
-
relationType,
|
|
1120
|
-
data
|
|
1121
|
-
});
|
|
1122
|
-
let components = this.entityIndex.get(entityId);
|
|
1123
|
-
if (!components) {
|
|
1124
|
-
components = /* @__PURE__ */ new Set();
|
|
1125
|
-
this.entityIndex.set(entityId, components);
|
|
1126
|
-
}
|
|
1127
|
-
components.add(componentId);
|
|
734
|
+
else entry.targets.set(targetId, {
|
|
735
|
+
relationType,
|
|
736
|
+
data
|
|
737
|
+
});
|
|
738
|
+
let components = this.entityIndex.get(entityId);
|
|
739
|
+
if (!components) {
|
|
740
|
+
components = /* @__PURE__ */ new Set();
|
|
741
|
+
this.entityIndex.set(entityId, components);
|
|
742
|
+
}
|
|
743
|
+
components.add(componentId);
|
|
1128
744
|
}
|
|
1129
745
|
deleteValue(entityId, relationType) {
|
|
1130
746
|
const componentId = getComponentIdFromRelationId(relationType);
|
|
@@ -1199,6 +815,14 @@ var DontFragmentStoreImpl = class {
|
|
|
1199
815
|
}
|
|
1200
816
|
this.entityIndex.delete(entityId);
|
|
1201
817
|
}
|
|
818
|
+
getAllForEntities(entityIds) {
|
|
819
|
+
const result = /* @__PURE__ */ new Map();
|
|
820
|
+
for (const eid of entityIds) {
|
|
821
|
+
const data = this.getAllForEntity(eid);
|
|
822
|
+
if (data.length > 0) result.set(eid, data);
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
1202
826
|
};
|
|
1203
827
|
//#endregion
|
|
1204
828
|
//#region src/commands/buffer.ts
|
|
@@ -1249,7 +873,8 @@ var CommandBuffer = class {
|
|
|
1249
873
|
});
|
|
1250
874
|
}
|
|
1251
875
|
/**
|
|
1252
|
-
* Execute all commands and clear the buffer
|
|
876
|
+
* Execute all commands and clear the buffer.
|
|
877
|
+
* Returns the number of iterations performed (for debug stats).
|
|
1253
878
|
*/
|
|
1254
879
|
execute() {
|
|
1255
880
|
let iterations = 0;
|
|
@@ -1269,6 +894,7 @@ var CommandBuffer = class {
|
|
|
1269
894
|
for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
|
|
1270
895
|
entityCommands.clear();
|
|
1271
896
|
}
|
|
897
|
+
return iterations;
|
|
1272
898
|
}
|
|
1273
899
|
/**
|
|
1274
900
|
* Get current commands (for testing)
|
|
@@ -1284,727 +910,1315 @@ var CommandBuffer = class {
|
|
|
1284
910
|
}
|
|
1285
911
|
};
|
|
1286
912
|
//#endregion
|
|
1287
|
-
//#region src/
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
913
|
+
//#region src/types/index.ts
|
|
914
|
+
function isOptionalEntityId(type) {
|
|
915
|
+
return typeof type === "object" && type !== null && "optional" in type;
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/component/type-utils.ts
|
|
919
|
+
/**
|
|
920
|
+
* Normalize component type collections into a stable ascending order.
|
|
921
|
+
* This keeps cache keys and archetype signatures deterministic.
|
|
922
|
+
*/
|
|
923
|
+
function normalizeComponentTypes(componentTypes) {
|
|
924
|
+
return [...componentTypes].sort((a, b) => a - b);
|
|
925
|
+
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/utils/utils.ts
|
|
928
|
+
/**
|
|
929
|
+
* Utility functions for ECS library
|
|
930
|
+
*/
|
|
931
|
+
/**
|
|
932
|
+
* Get a value from cache or compute and cache it if not present
|
|
933
|
+
* @param cache The cache map
|
|
934
|
+
* @param key The cache key
|
|
935
|
+
* @param compute Function to compute the value if not cached (may have side effects)
|
|
936
|
+
* @returns The cached or computed value
|
|
937
|
+
*/
|
|
938
|
+
function getOrCompute(cache, key, compute) {
|
|
939
|
+
let value = cache.get(key);
|
|
940
|
+
if (value === void 0) {
|
|
941
|
+
value = compute();
|
|
942
|
+
cache.set(key, value);
|
|
1300
943
|
}
|
|
944
|
+
return value;
|
|
945
|
+
}
|
|
946
|
+
//#endregion
|
|
947
|
+
//#region src/archetype/archetype.ts
|
|
948
|
+
/**
|
|
949
|
+
* Special value to represent missing component data
|
|
950
|
+
*/
|
|
951
|
+
const MISSING_COMPONENT = Symbol("missing component");
|
|
952
|
+
/**
|
|
953
|
+
* Archetype class for ECS architecture
|
|
954
|
+
* Represents a group of entities that share the same set of components
|
|
955
|
+
* Optimized for fast iteration and component access
|
|
956
|
+
*/
|
|
957
|
+
var Archetype = class {
|
|
1301
958
|
/**
|
|
1302
|
-
*
|
|
959
|
+
* The component types that define this archetype
|
|
1303
960
|
*/
|
|
1304
|
-
|
|
1305
|
-
this.removes.add(componentType);
|
|
1306
|
-
this.adds.delete(componentType);
|
|
1307
|
-
}
|
|
961
|
+
componentTypes;
|
|
1308
962
|
/**
|
|
1309
|
-
*
|
|
963
|
+
* Set version of componentTypes for O(1) lookups in hot paths
|
|
1310
964
|
*/
|
|
1311
|
-
|
|
1312
|
-
return this.adds.size > 0 || this.removes.size > 0;
|
|
1313
|
-
}
|
|
965
|
+
componentTypeSet;
|
|
1314
966
|
/**
|
|
1315
|
-
*
|
|
967
|
+
* List of entities in this archetype
|
|
1316
968
|
*/
|
|
1317
|
-
|
|
1318
|
-
this.adds.clear();
|
|
1319
|
-
this.removes.clear();
|
|
1320
|
-
}
|
|
969
|
+
entities = [];
|
|
1321
970
|
/**
|
|
1322
|
-
*
|
|
971
|
+
* Component data storage - maps component type to array of component data
|
|
972
|
+
* Each array index corresponds to the entity index in the entities array
|
|
1323
973
|
*/
|
|
1324
|
-
|
|
1325
|
-
for (const [componentType, component] of other.adds) {
|
|
1326
|
-
this.adds.set(componentType, component);
|
|
1327
|
-
this.removes.delete(componentType);
|
|
1328
|
-
}
|
|
1329
|
-
for (const componentType of other.removes) {
|
|
1330
|
-
this.removes.add(componentType);
|
|
1331
|
-
this.adds.delete(componentType);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
974
|
+
componentData = /* @__PURE__ */ new Map();
|
|
1334
975
|
/**
|
|
1335
|
-
*
|
|
976
|
+
* Reverse mapping from entity to its index in this archetype
|
|
1336
977
|
*/
|
|
1337
|
-
|
|
1338
|
-
for (const componentType of this.removes) existingComponents.delete(componentType);
|
|
1339
|
-
for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
|
|
1340
|
-
return existingComponents;
|
|
1341
|
-
}
|
|
978
|
+
entityToIndex = /* @__PURE__ */ new Map();
|
|
1342
979
|
/**
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
* @returns The final component types or undefined if no changes
|
|
980
|
+
* SparseStore used for relations declared with `sparse: true`.
|
|
981
|
+
* See store.ts for implementation details.
|
|
1346
982
|
*/
|
|
1347
|
-
|
|
1348
|
-
const finalComponentTypes = new Set(existingComponentTypes);
|
|
1349
|
-
let changed = false;
|
|
1350
|
-
for (const componentType of this.removes) {
|
|
1351
|
-
if (!finalComponentTypes.has(componentType)) {
|
|
1352
|
-
this.removes.delete(componentType);
|
|
1353
|
-
continue;
|
|
1354
|
-
}
|
|
1355
|
-
changed = true;
|
|
1356
|
-
finalComponentTypes.delete(componentType);
|
|
1357
|
-
}
|
|
1358
|
-
for (const componentType of this.adds.keys()) {
|
|
1359
|
-
if (finalComponentTypes.has(componentType)) continue;
|
|
1360
|
-
changed = true;
|
|
1361
|
-
finalComponentTypes.add(componentType);
|
|
1362
|
-
}
|
|
1363
|
-
return changed ? Array.from(finalComponentTypes) : void 0;
|
|
1364
|
-
}
|
|
1365
|
-
};
|
|
1366
|
-
//#endregion
|
|
1367
|
-
//#region src/component/entity-store.ts
|
|
1368
|
-
/**
|
|
1369
|
-
* Manages component entity (singleton) storage.
|
|
1370
|
-
*
|
|
1371
|
-
* Component entities use a flat Map-based storage rather than the Archetype-based
|
|
1372
|
-
* storage used for regular entities. Their IDs are in the component ID range
|
|
1373
|
-
* (or are relation IDs), distinguishing them from regular entity IDs.
|
|
1374
|
-
*/
|
|
1375
|
-
var ComponentEntityStore = class {
|
|
1376
|
-
componentEntityComponents = /* @__PURE__ */ new Map();
|
|
1377
|
-
relationEntityIdsByTarget = /* @__PURE__ */ new Map();
|
|
983
|
+
sparseRelations;
|
|
1378
984
|
/**
|
|
1379
|
-
*
|
|
1380
|
-
* Returns true for component IDs, component-relation IDs, and entity-relation IDs —
|
|
1381
|
-
* i.e. anything that is NOT a plain entity or an invalid ID.
|
|
985
|
+
* Multi-hooks that match this archetype
|
|
1382
986
|
*/
|
|
1383
|
-
|
|
1384
|
-
const detailed = getDetailedIdType(entityId);
|
|
1385
|
-
return detailed.type !== "entity" && detailed.type !== "invalid";
|
|
1386
|
-
}
|
|
987
|
+
matchingMultiHooks = /* @__PURE__ */ new Set();
|
|
1387
988
|
/**
|
|
1388
|
-
*
|
|
989
|
+
* Cache for pre-computed component data sources to avoid repeated calculations
|
|
1389
990
|
*/
|
|
1390
|
-
|
|
1391
|
-
|
|
991
|
+
componentDataSourcesCache = /* @__PURE__ */ new Map();
|
|
992
|
+
constructor(componentTypes, sparseStore) {
|
|
993
|
+
this.componentTypes = normalizeComponentTypes(componentTypes);
|
|
994
|
+
this.componentTypeSet = new Set(this.componentTypes);
|
|
995
|
+
this.sparseRelations = sparseStore;
|
|
996
|
+
for (const componentType of this.componentTypes) this.componentData.set(componentType, []);
|
|
1392
997
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
* In singleton usage the entity ID and the component type are the same value.
|
|
1396
|
-
*/
|
|
1397
|
-
hasSingleton(componentId) {
|
|
1398
|
-
return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
|
|
998
|
+
get size() {
|
|
999
|
+
return this.entities.length;
|
|
1399
1000
|
}
|
|
1400
1001
|
/**
|
|
1401
|
-
* Check if
|
|
1002
|
+
* Check if the given component types match this archetype
|
|
1003
|
+
* @param componentTypes - Component types to check (can be in any order)
|
|
1004
|
+
* @returns true if the types match this archetype's component set
|
|
1005
|
+
* @note This method handles unsorted input by internally sorting for comparison
|
|
1402
1006
|
*/
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
return
|
|
1007
|
+
matches(componentTypes) {
|
|
1008
|
+
if (this.componentTypes.length !== componentTypes.length) return false;
|
|
1009
|
+
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
1010
|
+
return this.componentTypes.every((type, index) => type === sortedTypes[index]);
|
|
1407
1011
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1012
|
+
addEntity(entityId, componentData) {
|
|
1013
|
+
if (this.entityToIndex.has(entityId)) throw new Error(`Entity ${entityId} is already in this archetype`);
|
|
1014
|
+
const index = this.entities.length;
|
|
1015
|
+
this.entities.push(entityId);
|
|
1016
|
+
this.entityToIndex.set(entityId, index);
|
|
1017
|
+
for (const componentType of this.componentTypes) {
|
|
1018
|
+
const data = componentData.get(componentType);
|
|
1019
|
+
this.getComponentData(componentType).push(!componentData.has(componentType) ? MISSING_COMPONENT : data);
|
|
1020
|
+
}
|
|
1021
|
+
this.addSparseRelations(entityId, componentData);
|
|
1416
1022
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
if (!data || !data.has(componentType)) return void 0;
|
|
1424
|
-
return { value: data.get(componentType) };
|
|
1023
|
+
addSparseRelations(entityId, componentData) {
|
|
1024
|
+
for (const [componentType, data] of componentData) {
|
|
1025
|
+
if (this.componentTypeSet.has(componentType)) continue;
|
|
1026
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1027
|
+
if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) this.sparseRelations.setValue(entityId, componentType, data);
|
|
1028
|
+
}
|
|
1425
1029
|
}
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
const relations = [];
|
|
1434
|
-
for (const [key, value] of data.entries()) {
|
|
1435
|
-
if (getComponentIdFromRelationId(key) !== componentId) continue;
|
|
1436
|
-
const detailed = getDetailedIdType(key);
|
|
1437
|
-
if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
|
|
1030
|
+
getEntity(entityId) {
|
|
1031
|
+
const index = this.entityToIndex.get(entityId);
|
|
1032
|
+
if (index === void 0) return void 0;
|
|
1033
|
+
const entityData = /* @__PURE__ */ new Map();
|
|
1034
|
+
for (const componentType of this.componentTypes) {
|
|
1035
|
+
const data = this.getComponentData(componentType)[index];
|
|
1036
|
+
entityData.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1438
1037
|
}
|
|
1439
|
-
|
|
1038
|
+
const sparseTuples = this.sparseRelations.getAllForEntity(entityId);
|
|
1039
|
+
for (const [componentType, data] of sparseTuples) entityData.set(componentType, data);
|
|
1040
|
+
return entityData;
|
|
1440
1041
|
}
|
|
1441
1042
|
/**
|
|
1442
|
-
*
|
|
1043
|
+
* Returns all sparse-stored relations for the given entity.
|
|
1044
|
+
* Internal helper used by command processing and tests.
|
|
1443
1045
|
*/
|
|
1444
|
-
|
|
1445
|
-
|
|
1046
|
+
getEntitySparseRelations(entityId) {
|
|
1047
|
+
const tuples = this.sparseRelations.getAllForEntity(entityId);
|
|
1048
|
+
if (tuples.length === 0) return void 0;
|
|
1049
|
+
const map = /* @__PURE__ */ new Map();
|
|
1050
|
+
for (const [relType, data] of tuples) map.set(relType, data);
|
|
1051
|
+
return map;
|
|
1446
1052
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1053
|
+
dump() {
|
|
1054
|
+
return this.entities.map((entity, i) => {
|
|
1055
|
+
const components = /* @__PURE__ */ new Map();
|
|
1056
|
+
for (const componentType of this.componentTypes) {
|
|
1057
|
+
const data = this.getComponentData(componentType)[i];
|
|
1058
|
+
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1059
|
+
}
|
|
1060
|
+
const sparseTuples = this.sparseRelations.getAllForEntity(entity);
|
|
1061
|
+
for (const [componentType, data] of sparseTuples) components.set(componentType, data);
|
|
1062
|
+
return {
|
|
1063
|
+
entity,
|
|
1064
|
+
components
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1456
1067
|
}
|
|
1457
1068
|
/**
|
|
1458
|
-
*
|
|
1069
|
+
* @internal Serialization fast-path.
|
|
1070
|
+
*
|
|
1071
|
+
* Appends SerializedEntity records directly from the archetype's column storage
|
|
1072
|
+
* (componentData arrays) plus sparse relations, avoiding per-entity Map
|
|
1073
|
+
* allocation and repeated Array.from(entries()).
|
|
1074
|
+
*
|
|
1075
|
+
* Component type IDs should be pre-encoded by the caller (once per archetype)
|
|
1076
|
+
* and passed in `encodedComponentTypes` (same order and length as this.componentTypes).
|
|
1077
|
+
*
|
|
1078
|
+
* The provided `encode` function should be the cached variant for best performance
|
|
1079
|
+
* on entity IDs and any sparse relation type IDs.
|
|
1080
|
+
*
|
|
1081
|
+
* `sparseByEntity` is an optional pre-fetched map from a bulk
|
|
1082
|
+
* `SparseStore.getAllForEntities` call (further reduces per-entity calls).
|
|
1459
1083
|
*/
|
|
1460
|
-
|
|
1461
|
-
if (
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
let data = this.componentEntityComponents.get(entityId);
|
|
1472
|
-
if (!data) {
|
|
1473
|
-
data = /* @__PURE__ */ new Map();
|
|
1474
|
-
this.componentEntityComponents.set(entityId, data);
|
|
1475
|
-
this.registerRelationEntityId(entityId);
|
|
1084
|
+
appendSerializedEntities(out, encode, encodedComponentTypes, sparseByEntity) {
|
|
1085
|
+
if (encodedComponentTypes.length !== this.componentTypes.length) throw new Error("encodedComponentTypes length must match archetype componentTypes");
|
|
1086
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
1087
|
+
const entity = this.entities[i];
|
|
1088
|
+
const components = [];
|
|
1089
|
+
for (let c = 0; c < this.componentTypes.length; c++) {
|
|
1090
|
+
const data = this.getComponentData(this.componentTypes[c])[i];
|
|
1091
|
+
components.push({
|
|
1092
|
+
type: encodedComponentTypes[c],
|
|
1093
|
+
value: data === MISSING_COMPONENT ? void 0 : data
|
|
1094
|
+
});
|
|
1476
1095
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1096
|
+
const sparseTuples = sparseByEntity?.get(entity) ?? this.sparseRelations.getAllForEntity(entity);
|
|
1097
|
+
for (const [componentType, data] of sparseTuples) components.push({
|
|
1098
|
+
type: encode(componentType),
|
|
1099
|
+
value: data
|
|
1100
|
+
});
|
|
1101
|
+
out.push({
|
|
1102
|
+
id: encode(entity),
|
|
1103
|
+
components
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
removeEntity(entityId) {
|
|
1108
|
+
const index = this.entityToIndex.get(entityId);
|
|
1109
|
+
if (index === void 0) return void 0;
|
|
1110
|
+
const removedData = /* @__PURE__ */ new Map();
|
|
1111
|
+
for (const componentType of this.componentTypes) removedData.set(componentType, this.getComponentData(componentType)[index]);
|
|
1112
|
+
const sparseTuples = this.sparseRelations.getAllForEntity(entityId);
|
|
1113
|
+
for (const [componentType, data] of sparseTuples) removedData.set(componentType, data);
|
|
1114
|
+
this.sparseRelations.deleteEntity(entityId);
|
|
1115
|
+
this.entityToIndex.delete(entityId);
|
|
1116
|
+
const lastIndex = this.entities.length - 1;
|
|
1117
|
+
if (index !== lastIndex) {
|
|
1118
|
+
const lastEntity = this.entities[lastIndex];
|
|
1119
|
+
this.entities[index] = lastEntity;
|
|
1120
|
+
this.entityToIndex.set(lastEntity, index);
|
|
1121
|
+
for (const componentType of this.componentTypes) {
|
|
1122
|
+
const dataArray = this.getComponentData(componentType);
|
|
1123
|
+
dataArray[index] = dataArray[lastIndex];
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
this.entities.pop();
|
|
1127
|
+
for (const componentType of this.componentTypes) this.getComponentData(componentType).pop();
|
|
1128
|
+
return removedData;
|
|
1129
|
+
}
|
|
1130
|
+
exists(entityId) {
|
|
1131
|
+
return this.entityToIndex.has(entityId);
|
|
1132
|
+
}
|
|
1133
|
+
get(entityId, componentType) {
|
|
1134
|
+
const index = this.entityToIndex.get(entityId);
|
|
1135
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1136
|
+
if (isWildcardRelationId(componentType)) return this.getWildcardRelations(entityId, index, componentType);
|
|
1137
|
+
return this.getRegularComponent(entityId, index, componentType);
|
|
1138
|
+
}
|
|
1139
|
+
getWildcardRelations(entityId, index, componentType) {
|
|
1140
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1141
|
+
const relations = [];
|
|
1142
|
+
for (const relType of this.componentTypes) {
|
|
1143
|
+
const relDetailed = getDetailedIdType(relType);
|
|
1144
|
+
if (isRelationType(relDetailed) && relDetailed.componentId === componentId) {
|
|
1145
|
+
const dataArray = this.getComponentData(relType);
|
|
1146
|
+
if (dataArray && dataArray[index] !== void 0) {
|
|
1147
|
+
const data = dataArray[index];
|
|
1148
|
+
relations.push([relDetailed.targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
1487
1149
|
}
|
|
1488
|
-
} else {
|
|
1489
|
-
data?.delete(command.componentType);
|
|
1490
|
-
pendingSetValues.delete(command.componentType);
|
|
1491
1150
|
}
|
|
1492
|
-
if (data?.size === 0) this.clear(entityId);
|
|
1493
1151
|
}
|
|
1152
|
+
if (componentId !== void 0) {
|
|
1153
|
+
const matches = this.sparseRelations.getRelationsForComponent(entityId, componentId);
|
|
1154
|
+
for (const m of matches) relations.push(m);
|
|
1155
|
+
}
|
|
1156
|
+
return relations;
|
|
1494
1157
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1158
|
+
getRegularComponent(entityId, index, componentType) {
|
|
1159
|
+
if (this.componentTypeSet.has(componentType)) {
|
|
1160
|
+
const data = this.getComponentData(componentType)[index];
|
|
1161
|
+
if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
1162
|
+
return data;
|
|
1163
|
+
}
|
|
1164
|
+
if (this.sparseRelations.getValue(entityId, componentType) !== void 0 || this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.sparseRelations.getValue(entityId, componentType);
|
|
1165
|
+
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
1501
1166
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1167
|
+
getOptional(entityId, componentType) {
|
|
1168
|
+
const index = this.entityToIndex.get(entityId);
|
|
1169
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1170
|
+
if (this.componentTypeSet.has(componentType)) {
|
|
1171
|
+
const data = this.getComponentData(componentType)[index];
|
|
1172
|
+
if (data === MISSING_COMPONENT) return void 0;
|
|
1173
|
+
return { value: data };
|
|
1174
|
+
}
|
|
1175
|
+
const value = this.sparseRelations.getValue(entityId, componentType);
|
|
1176
|
+
if (value !== void 0) return { value };
|
|
1177
|
+
if (this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.sparseRelations.getValue(entityId, componentType) };
|
|
1508
1178
|
}
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
if (
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
const existing = this.relationEntityIdsByTarget.get(targetId);
|
|
1515
|
-
if (existing) {
|
|
1516
|
-
existing.add(entityId);
|
|
1179
|
+
set(entityId, componentType, data) {
|
|
1180
|
+
const index = this.entityToIndex.get(entityId);
|
|
1181
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1182
|
+
if (this.componentData.has(componentType)) {
|
|
1183
|
+
this.getComponentData(componentType)[index] = data;
|
|
1517
1184
|
return;
|
|
1518
1185
|
}
|
|
1519
|
-
|
|
1186
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1187
|
+
if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) {
|
|
1188
|
+
this.sparseRelations.setValue(entityId, componentType, data);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1520
1192
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
if (
|
|
1193
|
+
getEntities() {
|
|
1194
|
+
return this.entities;
|
|
1195
|
+
}
|
|
1196
|
+
getEntityToIndexMap() {
|
|
1197
|
+
return this.entityToIndex;
|
|
1198
|
+
}
|
|
1199
|
+
getComponentData(componentType) {
|
|
1200
|
+
const data = this.componentData.get(componentType);
|
|
1201
|
+
if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1202
|
+
return data;
|
|
1203
|
+
}
|
|
1204
|
+
getOptionalComponentData(componentType) {
|
|
1205
|
+
return this.componentData.get(componentType);
|
|
1206
|
+
}
|
|
1207
|
+
getCachedComponentDataSources(componentTypes) {
|
|
1208
|
+
const cacheKey = buildCacheKey(componentTypes);
|
|
1209
|
+
return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
|
|
1210
|
+
}
|
|
1211
|
+
getComponentDataSource(compType) {
|
|
1212
|
+
const optional = isOptionalEntityId(compType);
|
|
1213
|
+
const actualType = optional ? compType.optional : compType;
|
|
1214
|
+
if (getIdType(actualType) === "wildcard-relation") {
|
|
1215
|
+
const componentId = getComponentIdFromRelationId(actualType);
|
|
1216
|
+
return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
|
|
1217
|
+
}
|
|
1218
|
+
return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
1219
|
+
}
|
|
1220
|
+
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
|
|
1221
|
+
return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.sparseRelations));
|
|
1222
|
+
}
|
|
1223
|
+
getEntitiesWithComponents(componentTypes) {
|
|
1224
|
+
const result = [];
|
|
1225
|
+
this.appendEntitiesWithComponents(componentTypes, result);
|
|
1226
|
+
return result;
|
|
1227
|
+
}
|
|
1228
|
+
appendEntitiesWithComponents(componentTypes, result) {
|
|
1229
|
+
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
1230
|
+
result.push({
|
|
1231
|
+
entity,
|
|
1232
|
+
components
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
*iterateWithComponents(componentTypes) {
|
|
1237
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1238
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1239
|
+
const entity = this.entities[entityIndex];
|
|
1240
|
+
yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
forEachWithComponents(componentTypes, callback) {
|
|
1244
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1245
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1246
|
+
const entity = this.entities[entityIndex];
|
|
1247
|
+
callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
forEach(callback) {
|
|
1251
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
1252
|
+
const entity = this.entities[i];
|
|
1253
|
+
const components = /* @__PURE__ */ new Map();
|
|
1254
|
+
for (const componentType of this.componentTypes) {
|
|
1255
|
+
const data = this.getComponentData(componentType)[i];
|
|
1256
|
+
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1257
|
+
}
|
|
1258
|
+
const sparseTuples = this.sparseRelations.getAllForEntity(entity);
|
|
1259
|
+
for (const [componentType, data] of sparseTuples) components.set(componentType, data);
|
|
1260
|
+
callback(entity, components);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
hasRelationWithComponentId(componentId) {
|
|
1264
|
+
for (const componentType of this.componentTypes) {
|
|
1265
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1266
|
+
if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
|
|
1267
|
+
}
|
|
1268
|
+
for (const entityId of this.entities) if (this.sparseRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
|
|
1269
|
+
return false;
|
|
1530
1270
|
}
|
|
1531
1271
|
};
|
|
1532
1272
|
//#endregion
|
|
1533
|
-
//#region src/
|
|
1273
|
+
//#region src/archetype/helpers.ts
|
|
1534
1274
|
/**
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1275
|
+
* Check if a components map has any wildcard relations matching a component ID
|
|
1276
|
+
* @param components - Component entity's components map
|
|
1277
|
+
* @param wildcardComponentId - The component ID to match
|
|
1278
|
+
* @returns True if at least one matching relation exists
|
|
1537
1279
|
*/
|
|
1538
|
-
function
|
|
1539
|
-
const
|
|
1540
|
-
|
|
1541
|
-
|
|
1280
|
+
function hasWildcardRelation(components, wildcardComponentId) {
|
|
1281
|
+
for (const relId of components.keys()) if (isRelationId(relId)) {
|
|
1282
|
+
if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
|
|
1283
|
+
}
|
|
1284
|
+
return false;
|
|
1542
1285
|
}
|
|
1543
1286
|
/**
|
|
1544
|
-
* Check if
|
|
1287
|
+
* Check if a detailed type represents a relation (entity or component)
|
|
1545
1288
|
*/
|
|
1546
|
-
function
|
|
1547
|
-
return
|
|
1548
|
-
const detailedType = getDetailedIdType(type);
|
|
1549
|
-
if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
|
|
1550
|
-
if (!isRelationId(archetypeType)) return false;
|
|
1551
|
-
return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
|
|
1552
|
-
});
|
|
1553
|
-
else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId)) {
|
|
1554
|
-
const wildcardMarker = relation(detailedType.componentId, "*");
|
|
1555
|
-
return archetype.componentTypeSet.has(wildcardMarker);
|
|
1556
|
-
} else return archetype.componentTypeSet.has(type);
|
|
1557
|
-
});
|
|
1289
|
+
function isRelationType(detailedType) {
|
|
1290
|
+
return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
|
|
1558
1291
|
}
|
|
1559
1292
|
/**
|
|
1560
|
-
* Check if
|
|
1293
|
+
* Check if a component type matches a given component ID for relations
|
|
1561
1294
|
*/
|
|
1562
|
-
function
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
|
|
1566
|
-
if (!isRelationId(archetypeType)) return false;
|
|
1567
|
-
return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
|
|
1568
|
-
});
|
|
1569
|
-
else return !archetype.componentTypeSet.has(type);
|
|
1570
|
-
});
|
|
1295
|
+
function matchesRelationComponentId(componentType, componentId) {
|
|
1296
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1297
|
+
return isRelationType(detailedType) && detailedType.componentId === componentId;
|
|
1571
1298
|
}
|
|
1572
|
-
//#endregion
|
|
1573
|
-
//#region src/query/query.ts
|
|
1574
1299
|
/**
|
|
1575
|
-
*
|
|
1576
|
-
*
|
|
1577
|
-
* Queries are created via {@link World.createQuery} and should be **reused across frames**
|
|
1578
|
-
* for optimal performance. The world automatically keeps the query's internal archetype cache
|
|
1579
|
-
* up to date as entities are created and destroyed.
|
|
1580
|
-
*
|
|
1581
|
-
* @example
|
|
1582
|
-
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
1583
|
-
*
|
|
1584
|
-
* // In the game loop
|
|
1585
|
-
* movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
|
|
1586
|
-
* pos.x += vel.x;
|
|
1587
|
-
* pos.y += vel.y;
|
|
1588
|
-
* });
|
|
1300
|
+
* Build cache key for component types
|
|
1589
1301
|
*/
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const detailedType = getDetailedIdType(ct);
|
|
1612
|
-
return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isDontFragmentComponent(detailedType.componentId);
|
|
1613
|
-
});
|
|
1614
|
-
this.updateCache();
|
|
1615
|
-
if (registry) registry.register(this);
|
|
1616
|
-
}
|
|
1617
|
-
/**
|
|
1618
|
-
* Check if query is disposed and throw error if so
|
|
1619
|
-
*/
|
|
1620
|
-
ensureNotDisposed() {
|
|
1621
|
-
if (this.isDisposed) throw new Error("Query has been disposed");
|
|
1302
|
+
function buildCacheKey(componentTypes) {
|
|
1303
|
+
return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Get data source for wildcard relations from component types
|
|
1307
|
+
*/
|
|
1308
|
+
function getWildcardRelationDataSource(componentTypes, componentId, optional) {
|
|
1309
|
+
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
1310
|
+
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Build wildcard relation value from matching relations.
|
|
1314
|
+
* Receives the SparseStore directly for efficient per-component lookups.
|
|
1315
|
+
*/
|
|
1316
|
+
function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, sparseStore, entityId, optional) {
|
|
1317
|
+
const relations = [];
|
|
1318
|
+
const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
1319
|
+
for (const relType of matchingRelations || []) {
|
|
1320
|
+
const data = getDataAtIndex(relType);
|
|
1321
|
+
const targetId = getTargetIdFromRelationId(relType);
|
|
1322
|
+
relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
1622
1323
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
* @returns Array of matching entity IDs
|
|
1627
|
-
*
|
|
1628
|
-
* @example
|
|
1629
|
-
* const entities = query.getEntities();
|
|
1630
|
-
* for (const entity of entities) {
|
|
1631
|
-
* const pos = world.get(entity, Position);
|
|
1632
|
-
* }
|
|
1633
|
-
*/
|
|
1634
|
-
getEntities() {
|
|
1635
|
-
this.ensureNotDisposed();
|
|
1636
|
-
if (this.wildcardTypes.length === 0 && this.specificDontFragmentTypes.length === 0) {
|
|
1637
|
-
const result = [];
|
|
1638
|
-
for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
|
|
1639
|
-
return result;
|
|
1640
|
-
}
|
|
1641
|
-
const result = [];
|
|
1642
|
-
for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityMatchesQuery(archetype, entity)) result.push(entity);
|
|
1643
|
-
return result;
|
|
1324
|
+
if (targetComponentId !== void 0) {
|
|
1325
|
+
const dfMatches = sparseStore.getRelationsForComponent(entityId, targetComponentId);
|
|
1326
|
+
for (const m of dfMatches) relations.push(m);
|
|
1644
1327
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
for (const wildcardType of this.wildcardTypes) {
|
|
1650
|
-
const relations = archetype.get(entity, wildcardType);
|
|
1651
|
-
if (!relations || relations.length === 0) return false;
|
|
1328
|
+
if (relations.length === 0) {
|
|
1329
|
+
if (!optional) {
|
|
1330
|
+
const componentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
1331
|
+
throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
|
|
1652
1332
|
}
|
|
1653
|
-
|
|
1654
|
-
return true;
|
|
1333
|
+
return;
|
|
1655
1334
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
* pos.x += vel.x;
|
|
1666
|
-
* });
|
|
1667
|
-
*/
|
|
1668
|
-
getEntitiesWithComponents(componentTypes) {
|
|
1669
|
-
this.ensureNotDisposed();
|
|
1670
|
-
const result = [];
|
|
1671
|
-
for (const archetype of this.cachedArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
|
|
1672
|
-
return result;
|
|
1335
|
+
return optional ? { value: relations } : relations;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Build regular component value from data source
|
|
1339
|
+
*/
|
|
1340
|
+
function buildRegularComponentValue(dataSource, entityIndex, optional) {
|
|
1341
|
+
if (dataSource === void 0) {
|
|
1342
|
+
if (optional) return void 0;
|
|
1343
|
+
throw new Error(`Component data not found for mandatory component type`);
|
|
1673
1344
|
}
|
|
1345
|
+
const data = dataSource[entityIndex];
|
|
1346
|
+
const result = data === MISSING_COMPONENT ? void 0 : data;
|
|
1347
|
+
return optional ? { value: result } : result;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Build a single component value based on its type
|
|
1351
|
+
*/
|
|
1352
|
+
function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, sparseRelations) {
|
|
1353
|
+
const optional = isOptionalEntityId(compType);
|
|
1354
|
+
const actualType = optional ? compType.optional : compType;
|
|
1355
|
+
if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], sparseRelations, entityId, optional);
|
|
1356
|
+
else return buildRegularComponentValue(dataSource, entityIndex, optional);
|
|
1357
|
+
}
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/component/entity-store.ts
|
|
1360
|
+
/**
|
|
1361
|
+
* Manages component entity (singleton) storage.
|
|
1362
|
+
*
|
|
1363
|
+
* Component entities use a flat Map-based storage rather than the Archetype-based
|
|
1364
|
+
* storage used for regular entities. Their IDs are in the component ID range
|
|
1365
|
+
* (or are relation IDs), distinguishing them from regular entity IDs.
|
|
1366
|
+
*/
|
|
1367
|
+
var ComponentEntityStore = class {
|
|
1368
|
+
componentEntityComponents = /* @__PURE__ */ new Map();
|
|
1369
|
+
relationEntityIdsByTarget = /* @__PURE__ */ new Map();
|
|
1674
1370
|
/**
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
* @param componentTypes - Array of component types to retrieve
|
|
1679
|
-
* @param callback - Function called for each matching entity with its components
|
|
1680
|
-
*
|
|
1681
|
-
* @example
|
|
1682
|
-
* query.forEach([Position, Velocity], (entity, pos, vel) => {
|
|
1683
|
-
* pos.x += vel.x;
|
|
1684
|
-
* pos.y += vel.y;
|
|
1685
|
-
* });
|
|
1371
|
+
* Check if an entity ID is a component entity type.
|
|
1372
|
+
* Returns true for component IDs, component-relation IDs, and entity-relation IDs —
|
|
1373
|
+
* i.e. anything that is NOT a plain entity or an invalid ID.
|
|
1686
1374
|
*/
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1375
|
+
exists(entityId) {
|
|
1376
|
+
const detailed = getDetailedIdType(entityId);
|
|
1377
|
+
return detailed.type !== "entity" && detailed.type !== "invalid";
|
|
1690
1378
|
}
|
|
1691
1379
|
/**
|
|
1692
|
-
*
|
|
1693
|
-
*
|
|
1694
|
-
* @param componentTypes - Array of component types to retrieve
|
|
1695
|
-
* @yields Tuples of `[entityId, ...components]`
|
|
1696
|
-
*
|
|
1697
|
-
* @example
|
|
1698
|
-
* for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
|
|
1699
|
-
* pos.x += vel.x;
|
|
1700
|
-
* }
|
|
1380
|
+
* Check if a component entity has a specific component.
|
|
1701
1381
|
*/
|
|
1702
|
-
|
|
1703
|
-
this.
|
|
1704
|
-
for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
|
|
1382
|
+
has(entityId, componentType) {
|
|
1383
|
+
return this.componentEntityComponents.get(entityId)?.has(componentType) ?? false;
|
|
1705
1384
|
}
|
|
1706
1385
|
/**
|
|
1707
|
-
*
|
|
1708
|
-
*
|
|
1709
|
-
* @param componentType - The component type to retrieve
|
|
1710
|
-
* @returns Array of component data (one entry per matching entity)
|
|
1711
|
-
*
|
|
1712
|
-
* @example
|
|
1713
|
-
* const positions = query.getComponentData(Position);
|
|
1386
|
+
* Check if a singleton component has data — the has(componentId) overload.
|
|
1387
|
+
* In singleton usage the entity ID and the component type are the same value.
|
|
1714
1388
|
*/
|
|
1715
|
-
|
|
1716
|
-
this.
|
|
1717
|
-
const result = [];
|
|
1718
|
-
for (const archetype of this.cachedArchetypes) for (const data of archetype.getComponentData(componentType)) result.push(data);
|
|
1719
|
-
return result;
|
|
1389
|
+
hasSingleton(componentId) {
|
|
1390
|
+
return this.componentEntityComponents.get(componentId)?.has(componentId) ?? false;
|
|
1720
1391
|
}
|
|
1721
1392
|
/**
|
|
1722
|
-
*
|
|
1393
|
+
* Check if a component entity has any wildcard relations matching a component ID.
|
|
1723
1394
|
*/
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1395
|
+
hasWildcard(entityId, componentId) {
|
|
1396
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
1397
|
+
if (!data) return false;
|
|
1398
|
+
return hasWildcardRelation(data, componentId);
|
|
1727
1399
|
}
|
|
1728
1400
|
/**
|
|
1729
|
-
*
|
|
1401
|
+
* Get a component value from a component entity.
|
|
1402
|
+
* Throws if the component does not exist.
|
|
1730
1403
|
*/
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
if (
|
|
1404
|
+
get(entityId, componentType) {
|
|
1405
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
1406
|
+
if (!data || !data.has(componentType)) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
|
|
1407
|
+
return data.get(componentType);
|
|
1734
1408
|
}
|
|
1735
1409
|
/**
|
|
1736
|
-
*
|
|
1410
|
+
* Get an optional component value from a component entity.
|
|
1411
|
+
* Returns undefined if the component does not exist.
|
|
1737
1412
|
*/
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1413
|
+
getOptional(entityId, componentType) {
|
|
1414
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
1415
|
+
if (!data || !data.has(componentType)) return void 0;
|
|
1416
|
+
return { value: data.get(componentType) };
|
|
1742
1417
|
}
|
|
1743
1418
|
/**
|
|
1744
|
-
*
|
|
1745
|
-
* This will decrement the world's reference count for the query.
|
|
1746
|
-
* The query will only be fully disposed when the ref count reaches zero.
|
|
1419
|
+
* Get all wildcard relations of a given type from a component entity.
|
|
1747
1420
|
*/
|
|
1748
|
-
|
|
1749
|
-
|
|
1421
|
+
getWildcard(entityId, wildcardComponentType) {
|
|
1422
|
+
const componentId = getComponentIdFromRelationId(wildcardComponentType);
|
|
1423
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
1424
|
+
if (componentId === void 0 || !data) return [];
|
|
1425
|
+
const relations = [];
|
|
1426
|
+
for (const [key, value] of data.entries()) {
|
|
1427
|
+
if (getComponentIdFromRelationId(key) !== componentId) continue;
|
|
1428
|
+
const detailed = getDetailedIdType(key);
|
|
1429
|
+
if (detailed.type === "entity-relation" || detailed.type === "component-relation") relations.push([detailed.targetId, value]);
|
|
1430
|
+
}
|
|
1431
|
+
return relations;
|
|
1750
1432
|
}
|
|
1751
1433
|
/**
|
|
1752
|
-
*
|
|
1434
|
+
* Clear all data for a component entity.
|
|
1753
1435
|
*/
|
|
1754
|
-
|
|
1755
|
-
if (
|
|
1756
|
-
if (registry) registry.unregister(this);
|
|
1757
|
-
this.cachedArchetypes = [];
|
|
1758
|
-
this.isDisposed = true;
|
|
1759
|
-
}
|
|
1436
|
+
clear(entityId) {
|
|
1437
|
+
if (this.componentEntityComponents.delete(entityId)) this.unregisterRelationEntityId(entityId);
|
|
1760
1438
|
}
|
|
1761
1439
|
/**
|
|
1762
|
-
*
|
|
1763
|
-
*
|
|
1764
|
-
* @example
|
|
1765
|
-
* using query = world.createQuery([Position]);
|
|
1766
|
-
* // query is released automatically when the block exits
|
|
1440
|
+
* Cleanup all component entities that reference a given target entity.
|
|
1441
|
+
* Called when a target entity is destroyed.
|
|
1767
1442
|
*/
|
|
1768
|
-
|
|
1769
|
-
this.
|
|
1443
|
+
cleanupReferencesTo(targetId) {
|
|
1444
|
+
const relationEntities = this.relationEntityIdsByTarget.get(targetId);
|
|
1445
|
+
if (!relationEntities) return;
|
|
1446
|
+
for (const relationEntityId of relationEntities) this.componentEntityComponents.delete(relationEntityId);
|
|
1447
|
+
this.relationEntityIdsByTarget.delete(targetId);
|
|
1770
1448
|
}
|
|
1771
1449
|
/**
|
|
1772
|
-
*
|
|
1450
|
+
* Execute a batch of commands for a component entity.
|
|
1773
1451
|
*/
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1452
|
+
executeCommands(entityId, commands) {
|
|
1453
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
1454
|
+
this.clear(entityId);
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const pendingSetValues = /* @__PURE__ */ new Map();
|
|
1458
|
+
for (const command of commands) if (command.type === "set" && command.componentType) {
|
|
1459
|
+
const merge = getComponentMerge(command.componentType);
|
|
1460
|
+
let nextValue = command.component;
|
|
1461
|
+
if (merge !== void 0 && pendingSetValues.has(command.componentType)) nextValue = merge(pendingSetValues.get(command.componentType), command.component);
|
|
1462
|
+
pendingSetValues.set(command.componentType, nextValue);
|
|
1463
|
+
let data = this.componentEntityComponents.get(entityId);
|
|
1464
|
+
if (!data) {
|
|
1465
|
+
data = /* @__PURE__ */ new Map();
|
|
1466
|
+
this.componentEntityComponents.set(entityId, data);
|
|
1467
|
+
this.registerRelationEntityId(entityId);
|
|
1468
|
+
}
|
|
1469
|
+
data.set(command.componentType, nextValue);
|
|
1470
|
+
} else if (command.type === "delete" && command.componentType) {
|
|
1471
|
+
const data = this.componentEntityComponents.get(entityId);
|
|
1472
|
+
if (isWildcardRelationId(command.componentType)) {
|
|
1473
|
+
const componentId = getComponentIdFromRelationId(command.componentType);
|
|
1474
|
+
if (componentId !== void 0) {
|
|
1475
|
+
if (data) {
|
|
1476
|
+
for (const key of Array.from(data.keys())) if (getComponentIdFromRelationId(key) === componentId) data.delete(key);
|
|
1477
|
+
}
|
|
1478
|
+
for (const key of Array.from(pendingSetValues.keys())) if (getComponentIdFromRelationId(key) === componentId) pendingSetValues.delete(key);
|
|
1479
|
+
}
|
|
1480
|
+
} else {
|
|
1481
|
+
data?.delete(command.componentType);
|
|
1482
|
+
pendingSetValues.delete(command.componentType);
|
|
1483
|
+
}
|
|
1484
|
+
if (data?.size === 0) this.clear(entityId);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Initialize a component entity from a deserialization snapshot.
|
|
1489
|
+
*/
|
|
1490
|
+
initFromSnapshot(entityId, componentMap) {
|
|
1491
|
+
this.componentEntityComponents.set(entityId, componentMap);
|
|
1492
|
+
this.registerRelationEntityId(entityId);
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Iterate over all component entity entries.
|
|
1496
|
+
* Used for serialization.
|
|
1497
|
+
*/
|
|
1498
|
+
entries() {
|
|
1499
|
+
return this.componentEntityComponents.entries();
|
|
1500
|
+
}
|
|
1501
|
+
registerRelationEntityId(entityId) {
|
|
1502
|
+
const detailed = getDetailedIdType(entityId);
|
|
1503
|
+
if (detailed.type !== "entity-relation") return;
|
|
1504
|
+
const targetId = detailed.targetId;
|
|
1505
|
+
if (targetId === void 0) return;
|
|
1506
|
+
const existing = this.relationEntityIdsByTarget.get(targetId);
|
|
1507
|
+
if (existing) {
|
|
1508
|
+
existing.add(entityId);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
this.relationEntityIdsByTarget.set(targetId, new Set([entityId]));
|
|
1512
|
+
}
|
|
1513
|
+
unregisterRelationEntityId(entityId) {
|
|
1514
|
+
const detailed = getDetailedIdType(entityId);
|
|
1515
|
+
if (detailed.type !== "entity-relation") return;
|
|
1516
|
+
const targetId = detailed.targetId;
|
|
1517
|
+
if (targetId === void 0) return;
|
|
1518
|
+
const existing = this.relationEntityIdsByTarget.get(targetId);
|
|
1519
|
+
if (!existing) return;
|
|
1520
|
+
existing.delete(entityId);
|
|
1521
|
+
if (existing.size === 0) this.relationEntityIdsByTarget.delete(targetId);
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
//#endregion
|
|
1525
|
+
//#region src/query/filter.ts
|
|
1526
|
+
/**
|
|
1527
|
+
* Serialize a QueryFilter into a deterministic string suitable for cache keys.
|
|
1528
|
+
* Currently only serializes `negativeComponentTypes`.
|
|
1529
|
+
*/
|
|
1530
|
+
function serializeQueryFilter(filter = {}) {
|
|
1531
|
+
const negative = (filter.negativeComponentTypes || []).slice().sort((a, b) => a - b);
|
|
1532
|
+
if (negative.length === 0) return "";
|
|
1533
|
+
return `neg:${negative.join(",")}`;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Check if an archetype matches the given component types
|
|
1537
|
+
*/
|
|
1538
|
+
function matchesComponentTypes(archetype, componentTypes) {
|
|
1539
|
+
return componentTypes.every((type) => {
|
|
1540
|
+
const detailedType = getDetailedIdType(type);
|
|
1541
|
+
if (detailedType.type === "wildcard-relation") return archetype.componentTypes.some((archetypeType) => {
|
|
1542
|
+
if (!isRelationId(archetypeType)) return false;
|
|
1543
|
+
return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
|
|
1544
|
+
});
|
|
1545
|
+
else if ((detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isSparseComponent(detailedType.componentId)) {
|
|
1546
|
+
const wildcardMarker = relation(detailedType.componentId, "*");
|
|
1547
|
+
return archetype.componentTypeSet.has(wildcardMarker);
|
|
1548
|
+
} else return archetype.componentTypeSet.has(type);
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Check if an archetype matches the filter conditions (only filtering logic)
|
|
1553
|
+
*/
|
|
1554
|
+
function matchesFilter(archetype, filter) {
|
|
1555
|
+
return (filter.negativeComponentTypes || []).every((type) => {
|
|
1556
|
+
const detailedType = getDetailedIdType(type);
|
|
1557
|
+
if (detailedType.type === "wildcard-relation") return !archetype.componentTypes.some((archetypeType) => {
|
|
1558
|
+
if (!isRelationId(archetypeType)) return false;
|
|
1559
|
+
return getComponentIdFromRelationId(archetypeType) === detailedType.componentId;
|
|
1560
|
+
});
|
|
1561
|
+
else return !archetype.componentTypeSet.has(type);
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
//#endregion
|
|
1565
|
+
//#region src/query/query.ts
|
|
1566
|
+
/**
|
|
1567
|
+
* Cached query for efficiently iterating entities with specific components.
|
|
1782
1568
|
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*
|
|
1786
|
-
* - Notify registered queries when new archetypes are created or destroyed.
|
|
1569
|
+
* Queries are created via {@link World.createQuery} and should be **reused across frames**
|
|
1570
|
+
* for optimal performance. The world automatically keeps the query's internal archetype cache
|
|
1571
|
+
* up to date as entities are created and destroyed.
|
|
1787
1572
|
*
|
|
1788
|
-
*
|
|
1789
|
-
*
|
|
1573
|
+
* @example
|
|
1574
|
+
* const movementQuery = world.createQuery([Position, Velocity]);
|
|
1575
|
+
*
|
|
1576
|
+
* // In the game loop
|
|
1577
|
+
* movementQuery.forEach([Position, Velocity], (entity, pos, vel) => {
|
|
1578
|
+
* pos.x += vel.x;
|
|
1579
|
+
* pos.y += vel.y;
|
|
1580
|
+
* });
|
|
1790
1581
|
*/
|
|
1791
|
-
var
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1582
|
+
var Query = class {
|
|
1583
|
+
world;
|
|
1584
|
+
componentTypes;
|
|
1585
|
+
filter;
|
|
1586
|
+
cachedArchetypes = [];
|
|
1587
|
+
isDisposed = false;
|
|
1588
|
+
/** Cache key assigned by World for O(1) releaseQuery lookup */
|
|
1589
|
+
_cacheKey;
|
|
1590
|
+
/** Cached wildcard component types for faster entity filtering */
|
|
1591
|
+
wildcardTypes;
|
|
1592
|
+
/** Cached specific sparse relation types that need entity-level filtering */
|
|
1593
|
+
specificSparseRelationTypes;
|
|
1798
1594
|
/**
|
|
1799
|
-
*
|
|
1800
|
-
|
|
1595
|
+
* @internal Queries should be created via {@link World.createQuery}, not instantiated directly.
|
|
1596
|
+
*/
|
|
1597
|
+
constructor(world, componentTypes, filter = {}, registry) {
|
|
1598
|
+
this.world = world;
|
|
1599
|
+
this.componentTypes = normalizeComponentTypes(componentTypes);
|
|
1600
|
+
this.filter = filter;
|
|
1601
|
+
this.wildcardTypes = this.componentTypes.filter((ct) => getDetailedIdType(ct).type === "wildcard-relation");
|
|
1602
|
+
this.specificSparseRelationTypes = this.componentTypes.filter((ct) => {
|
|
1603
|
+
const detailedType = getDetailedIdType(ct);
|
|
1604
|
+
return (detailedType.type === "entity-relation" || detailedType.type === "component-relation") && detailedType.componentId !== void 0 && isSparseComponent(detailedType.componentId);
|
|
1605
|
+
});
|
|
1606
|
+
this.updateCache();
|
|
1607
|
+
if (registry) registry.register(this);
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Check if query is disposed and throw error if so
|
|
1611
|
+
*/
|
|
1612
|
+
ensureNotDisposed() {
|
|
1613
|
+
if (this.isDisposed) throw new Error("Query has been disposed");
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Returns all entity IDs that match this query.
|
|
1801
1617
|
*
|
|
1802
|
-
* @
|
|
1803
|
-
*
|
|
1804
|
-
* @
|
|
1805
|
-
*
|
|
1618
|
+
* @returns Array of matching entity IDs
|
|
1619
|
+
*
|
|
1620
|
+
* @example
|
|
1621
|
+
* const entities = query.getEntities();
|
|
1622
|
+
* for (const entity of entities) {
|
|
1623
|
+
* const pos = world.get(entity, Position);
|
|
1624
|
+
* }
|
|
1806
1625
|
*/
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if (
|
|
1810
|
-
|
|
1811
|
-
|
|
1626
|
+
getEntities() {
|
|
1627
|
+
this.ensureNotDisposed();
|
|
1628
|
+
if (this.wildcardTypes.length === 0 && this.specificSparseRelationTypes.length === 0) {
|
|
1629
|
+
const result = [];
|
|
1630
|
+
for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) result.push(entity);
|
|
1631
|
+
return result;
|
|
1812
1632
|
}
|
|
1813
|
-
const
|
|
1814
|
-
this.
|
|
1815
|
-
|
|
1816
|
-
query,
|
|
1817
|
-
refCount: 1
|
|
1818
|
-
});
|
|
1819
|
-
return query;
|
|
1633
|
+
const result = [];
|
|
1634
|
+
for (const archetype of this.cachedArchetypes) for (const entity of archetype.getEntities()) if (this.entityMatchesQuery(archetype, entity)) result.push(entity);
|
|
1635
|
+
return result;
|
|
1820
1636
|
}
|
|
1821
1637
|
/**
|
|
1822
|
-
*
|
|
1823
|
-
* When the count reaches zero the query is fully disposed.
|
|
1638
|
+
* Check if entity matches all query requirements (wildcards and specific sparse relations)
|
|
1824
1639
|
*/
|
|
1825
|
-
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
if (!cached || cached.query !== query) return;
|
|
1830
|
-
cached.refCount--;
|
|
1831
|
-
if (cached.refCount <= 0) {
|
|
1832
|
-
this.cache.delete(key);
|
|
1833
|
-
cached.query._disposeInternal(this);
|
|
1640
|
+
entityMatchesQuery(archetype, entity) {
|
|
1641
|
+
for (const wildcardType of this.wildcardTypes) {
|
|
1642
|
+
const relations = archetype.get(entity, wildcardType);
|
|
1643
|
+
if (!relations || relations.length === 0) return false;
|
|
1834
1644
|
}
|
|
1645
|
+
for (const specificType of this.specificSparseRelationTypes) if (archetype.getOptional(entity, specificType) === void 0) return false;
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Returns all matching entities along with their component data.
|
|
1650
|
+
*
|
|
1651
|
+
* @param componentTypes - Array of component types to retrieve
|
|
1652
|
+
* @returns Array of objects containing the entity ID and its component tuple
|
|
1653
|
+
*
|
|
1654
|
+
* @example
|
|
1655
|
+
* const results = query.getEntitiesWithComponents([Position, Velocity]);
|
|
1656
|
+
* results.forEach(({ entity, components: [pos, vel] }) => {
|
|
1657
|
+
* pos.x += vel.x;
|
|
1658
|
+
* });
|
|
1659
|
+
*/
|
|
1660
|
+
getEntitiesWithComponents(componentTypes) {
|
|
1661
|
+
this.ensureNotDisposed();
|
|
1662
|
+
const result = [];
|
|
1663
|
+
for (const archetype of this.cachedArchetypes) archetype.appendEntitiesWithComponents(componentTypes, result);
|
|
1664
|
+
return result;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Iterates over all matching entities and invokes the callback with their component data.
|
|
1668
|
+
* This is the preferred way to read and mutate components in a hot loop.
|
|
1669
|
+
*
|
|
1670
|
+
* @param componentTypes - Array of component types to retrieve
|
|
1671
|
+
* @param callback - Function called for each matching entity with its components
|
|
1672
|
+
*
|
|
1673
|
+
* @example
|
|
1674
|
+
* query.forEach([Position, Velocity], (entity, pos, vel) => {
|
|
1675
|
+
* pos.x += vel.x;
|
|
1676
|
+
* pos.y += vel.y;
|
|
1677
|
+
* });
|
|
1678
|
+
*/
|
|
1679
|
+
forEach(componentTypes, callback) {
|
|
1680
|
+
this.ensureNotDisposed();
|
|
1681
|
+
for (const archetype of this.cachedArchetypes) archetype.forEachWithComponents(componentTypes, callback);
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Generator that yields each matching entity together with its component data.
|
|
1685
|
+
*
|
|
1686
|
+
* @param componentTypes - Array of component types to retrieve
|
|
1687
|
+
* @yields Tuples of `[entityId, ...components]`
|
|
1688
|
+
*
|
|
1689
|
+
* @example
|
|
1690
|
+
* for (const [entity, pos, vel] of query.iterate([Position, Velocity])) {
|
|
1691
|
+
* pos.x += vel.x;
|
|
1692
|
+
* }
|
|
1693
|
+
*/
|
|
1694
|
+
*iterate(componentTypes) {
|
|
1695
|
+
this.ensureNotDisposed();
|
|
1696
|
+
for (const archetype of this.cachedArchetypes) yield* archetype.iterateWithComponents(componentTypes);
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Returns an array containing the data of a single component for every matching entity.
|
|
1700
|
+
*
|
|
1701
|
+
* @param componentType - The component type to retrieve
|
|
1702
|
+
* @returns Array of component data (one entry per matching entity)
|
|
1703
|
+
*
|
|
1704
|
+
* @example
|
|
1705
|
+
* const positions = query.getComponentData(Position);
|
|
1706
|
+
*/
|
|
1707
|
+
getComponentData(componentType) {
|
|
1708
|
+
this.ensureNotDisposed();
|
|
1709
|
+
const result = [];
|
|
1710
|
+
for (const archetype of this.cachedArchetypes) for (const data of archetype.getComponentData(componentType)) result.push(data);
|
|
1711
|
+
return result;
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* @internal Rebuilds the cached archetype list. Called automatically by the world.
|
|
1715
|
+
*/
|
|
1716
|
+
updateCache() {
|
|
1717
|
+
if (this.isDisposed) return;
|
|
1718
|
+
this.cachedArchetypes = this.world.getMatchingArchetypes(this.componentTypes).filter((archetype) => matchesFilter(archetype, this.filter));
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* @internal Called by the world when a new archetype is created.
|
|
1722
|
+
*/
|
|
1723
|
+
checkNewArchetype(archetype) {
|
|
1724
|
+
if (this.isDisposed) return;
|
|
1725
|
+
if (matchesComponentTypes(archetype, this.componentTypes) && matchesFilter(archetype, this.filter) && !this.cachedArchetypes.includes(archetype)) this.cachedArchetypes.push(archetype);
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* @internal Called by the world when an archetype is destroyed.
|
|
1729
|
+
*/
|
|
1730
|
+
removeArchetype(archetype) {
|
|
1731
|
+
if (this.isDisposed) return;
|
|
1732
|
+
const index = this.cachedArchetypes.indexOf(archetype);
|
|
1733
|
+
if (index !== -1) this.cachedArchetypes.splice(index, 1);
|
|
1734
|
+
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Request disposal of this query.
|
|
1737
|
+
* This will decrement the world's reference count for the query.
|
|
1738
|
+
* The query will only be fully disposed when the ref count reaches zero.
|
|
1739
|
+
*/
|
|
1740
|
+
dispose() {
|
|
1741
|
+
this.world.releaseQuery(this);
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* @internal Fully disposes the query when the world's refCount reaches zero.
|
|
1745
|
+
*/
|
|
1746
|
+
_disposeInternal(registry) {
|
|
1747
|
+
if (!this.isDisposed) {
|
|
1748
|
+
if (registry) registry.unregister(this);
|
|
1749
|
+
this.cachedArchetypes = [];
|
|
1750
|
+
this.isDisposed = true;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Using-with-disposals support. Calls {@link dispose} automatically.
|
|
1755
|
+
*
|
|
1756
|
+
* @example
|
|
1757
|
+
* using query = world.createQuery([Position]);
|
|
1758
|
+
* // query is released automatically when the block exits
|
|
1759
|
+
*/
|
|
1760
|
+
[Symbol.dispose]() {
|
|
1761
|
+
this.dispose();
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Whether the query has been disposed and can no longer be used.
|
|
1765
|
+
*/
|
|
1766
|
+
get disposed() {
|
|
1767
|
+
return this.isDisposed;
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
//#endregion
|
|
1771
|
+
//#region src/query/registry.ts
|
|
1772
|
+
/**
|
|
1773
|
+
* Manages the lifecycle and caching of `Query` instances.
|
|
1774
|
+
*
|
|
1775
|
+
* Responsibilities:
|
|
1776
|
+
* - Create / reuse cached queries keyed by component-type + filter signature.
|
|
1777
|
+
* - Track reference counts so queries are only disposed when truly unused.
|
|
1778
|
+
* - Notify registered queries when new archetypes are created or destroyed.
|
|
1779
|
+
*
|
|
1780
|
+
* The `_cacheKey` string that was previously attached directly to `Query` is now
|
|
1781
|
+
* kept in a private `WeakMap` so the `Query` class doesn't need to expose it.
|
|
1782
|
+
*/
|
|
1783
|
+
var QueryRegistry = class {
|
|
1784
|
+
/** All live queries that should receive archetype notifications. */
|
|
1785
|
+
queries = /* @__PURE__ */ new Set();
|
|
1786
|
+
/** Cache of reusable queries keyed by a deterministic signature string. */
|
|
1787
|
+
cache = /* @__PURE__ */ new Map();
|
|
1788
|
+
/** Maps each query to its cache key without polluting the Query public API. */
|
|
1789
|
+
cacheKeys = /* @__PURE__ */ new WeakMap();
|
|
1790
|
+
/**
|
|
1791
|
+
* Returns (or creates) a cached query for the given component types and filter.
|
|
1792
|
+
* Increments the reference count on cache hits.
|
|
1793
|
+
*
|
|
1794
|
+
* @param world The world that owns this registry.
|
|
1795
|
+
* @param sortedTypes Normalized (sorted) component types.
|
|
1796
|
+
* @param key Combined cache key (`types|filter`).
|
|
1797
|
+
* @param filter The raw query filter (used when creating a new Query).
|
|
1798
|
+
*/
|
|
1799
|
+
getOrCreate(world, sortedTypes, key, filter) {
|
|
1800
|
+
const cached = this.cache.get(key);
|
|
1801
|
+
if (cached) {
|
|
1802
|
+
cached.refCount++;
|
|
1803
|
+
return cached.query;
|
|
1804
|
+
}
|
|
1805
|
+
const query = new Query(world, sortedTypes, filter, this);
|
|
1806
|
+
this.cacheKeys.set(query, key);
|
|
1807
|
+
this.cache.set(key, {
|
|
1808
|
+
query,
|
|
1809
|
+
refCount: 1
|
|
1810
|
+
});
|
|
1811
|
+
return query;
|
|
1812
|
+
}
|
|
1813
|
+
/**
|
|
1814
|
+
* Decrements the reference count for the given query.
|
|
1815
|
+
* When the count reaches zero the query is fully disposed.
|
|
1816
|
+
*/
|
|
1817
|
+
release(query) {
|
|
1818
|
+
const key = this.cacheKeys.get(query);
|
|
1819
|
+
if (!key) return;
|
|
1820
|
+
const cached = this.cache.get(key);
|
|
1821
|
+
if (!cached || cached.query !== query) return;
|
|
1822
|
+
cached.refCount--;
|
|
1823
|
+
if (cached.refCount <= 0) {
|
|
1824
|
+
this.cache.delete(key);
|
|
1825
|
+
cached.query._disposeInternal(this);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Registers a query so it receives future archetype notifications.
|
|
1830
|
+
* Called automatically by the `Query` constructor via `world._registerQuery`.
|
|
1831
|
+
*/
|
|
1832
|
+
register(query) {
|
|
1833
|
+
this.queries.add(query);
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Removes a query from the notification list.
|
|
1837
|
+
* Called by `Query._disposeInternal` via `world._unregisterQuery`.
|
|
1838
|
+
*/
|
|
1839
|
+
unregister(query) {
|
|
1840
|
+
this.queries.delete(query);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Notifies all live queries that a new archetype has been created.
|
|
1844
|
+
* Queries will add the archetype to their cache if it matches.
|
|
1845
|
+
*/
|
|
1846
|
+
onNewArchetype(archetype) {
|
|
1847
|
+
for (const query of this.queries) query.checkNewArchetype(archetype);
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Notifies all live queries that an archetype has been destroyed.
|
|
1851
|
+
* Queries will remove the archetype from their internal cache.
|
|
1852
|
+
*/
|
|
1853
|
+
onArchetypeRemoved(archetype) {
|
|
1854
|
+
for (const query of this.queries) query.removeArchetype(archetype);
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
//#endregion
|
|
1858
|
+
//#region src/world/commands.ts
|
|
1859
|
+
function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
|
|
1860
|
+
for (const command of commands) if (command.type === "set") processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
|
|
1861
|
+
else if (command.type === "delete") processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
|
|
1862
|
+
}
|
|
1863
|
+
function processSetCommand(entityId, currentArchetype, componentType, component, changeset, handleExclusiveRelation) {
|
|
1864
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1865
|
+
if (componentId !== void 0) {
|
|
1866
|
+
handleExclusiveRelation(entityId, currentArchetype, componentId);
|
|
1867
|
+
if (isSparseComponent(componentId)) {
|
|
1868
|
+
const wildcardMarker = relation(componentId, "*");
|
|
1869
|
+
if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const merge = getComponentMerge(componentType);
|
|
1873
|
+
if (merge !== void 0 && changeset.adds.has(componentType)) {
|
|
1874
|
+
const prev = changeset.adds.get(componentType);
|
|
1875
|
+
changeset.set(componentType, merge(prev, component));
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
changeset.set(componentType, component);
|
|
1879
|
+
}
|
|
1880
|
+
function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
|
|
1881
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
1882
|
+
if (isWildcardRelationId(componentType) && componentId !== void 0) removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
|
|
1883
|
+
else {
|
|
1884
|
+
changeset.delete(componentType);
|
|
1885
|
+
maybeRemoveWildcardMarker(entityId, currentArchetype, componentType, componentId, changeset);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function removeMatchingRelations(entityId, archetype, baseComponentId, changeset) {
|
|
1889
|
+
for (const componentType of archetype.componentTypes) {
|
|
1890
|
+
if (isWildcardRelationId(componentType)) continue;
|
|
1891
|
+
if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
|
|
1892
|
+
}
|
|
1893
|
+
const sparseData = archetype.getEntitySparseRelations(entityId);
|
|
1894
|
+
if (sparseData) {
|
|
1895
|
+
for (const componentType of sparseData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
|
|
1899
|
+
removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
|
|
1900
|
+
if (isSparseComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
|
|
1901
|
+
}
|
|
1902
|
+
function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
|
|
1903
|
+
if (componentId === void 0 || !isSparseComponent(componentId)) return;
|
|
1904
|
+
const wildcardMarker = relation(componentId, "*");
|
|
1905
|
+
for (const otherComponentType of archetype.componentTypes) {
|
|
1906
|
+
if (otherComponentType === removedComponentType) continue;
|
|
1907
|
+
if (otherComponentType === wildcardMarker) continue;
|
|
1908
|
+
if (changeset.removes.has(otherComponentType)) continue;
|
|
1909
|
+
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1910
|
+
}
|
|
1911
|
+
const sparseData = archetype.getEntitySparseRelations(entityId);
|
|
1912
|
+
if (sparseData) for (const otherComponentType of sparseData.keys()) {
|
|
1913
|
+
if (otherComponentType === removedComponentType) continue;
|
|
1914
|
+
if (changeset.removes.has(otherComponentType)) continue;
|
|
1915
|
+
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1916
|
+
}
|
|
1917
|
+
for (const addedType of changeset.adds.keys()) {
|
|
1918
|
+
if (addedType === removedComponentType) continue;
|
|
1919
|
+
if (getComponentIdFromRelationId(addedType) === componentId) return;
|
|
1920
|
+
}
|
|
1921
|
+
changeset.delete(wildcardMarker);
|
|
1922
|
+
}
|
|
1923
|
+
function hasEntityComponent(archetype, entityId, componentType) {
|
|
1924
|
+
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
1925
|
+
return archetype.getEntitySparseRelations(entityId)?.has(componentType) ?? false;
|
|
1926
|
+
}
|
|
1927
|
+
function pruneMissingRemovals(changeset, archetype, entityId) {
|
|
1928
|
+
let toPrune;
|
|
1929
|
+
for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
|
|
1930
|
+
if (toPrune === void 0) toPrune = [];
|
|
1931
|
+
toPrune.push(componentType);
|
|
1932
|
+
}
|
|
1933
|
+
if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
|
|
1934
|
+
}
|
|
1935
|
+
function hasArchetypeStructuralChange(changeset, currentArchetype) {
|
|
1936
|
+
for (const componentType of changeset.removes) if (!isSparseRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1937
|
+
for (const componentType of changeset.adds.keys()) if (!isSparseRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
function buildFinalRegularComponentTypes(currentArchetype, changeset) {
|
|
1941
|
+
const finalRegularTypes = new Set(currentArchetype.componentTypes);
|
|
1942
|
+
for (const componentType of changeset.removes) if (!isSparseRelation(componentType)) finalRegularTypes.delete(componentType);
|
|
1943
|
+
for (const [componentType] of changeset.adds) if (!isSparseRelation(componentType)) finalRegularTypes.add(componentType);
|
|
1944
|
+
return Array.from(finalRegularTypes);
|
|
1945
|
+
}
|
|
1946
|
+
function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
|
|
1947
|
+
pruneMissingRemovals(changeset, currentArchetype, entityId);
|
|
1948
|
+
if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
|
|
1949
|
+
const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
|
|
1950
|
+
const newArchetype = ctx.ensureArchetype(finalRegularTypes);
|
|
1951
|
+
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1952
|
+
if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
|
|
1953
|
+
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1954
|
+
entityToArchetype.set(entityId, newArchetype);
|
|
1955
|
+
return newArchetype;
|
|
1956
|
+
}
|
|
1957
|
+
if (removedComponents !== null) applySparseChanges(ctx.sparseStore, entityId, changeset, removedComponents);
|
|
1958
|
+
else applySparseChangesNoHooks(ctx.sparseStore, entityId, changeset);
|
|
1959
|
+
for (const [componentType, component] of changeset.adds) {
|
|
1960
|
+
if (isSparseRelation(componentType)) continue;
|
|
1961
|
+
currentArchetype.set(entityId, componentType, component);
|
|
1962
|
+
}
|
|
1963
|
+
return currentArchetype;
|
|
1964
|
+
}
|
|
1965
|
+
function applySparseChanges(sparseStore, entityId, changeset, removedComponents) {
|
|
1966
|
+
for (const componentType of changeset.removes) if (isSparseRelation(componentType)) {
|
|
1967
|
+
const removedValue = sparseStore.getValue(entityId, componentType);
|
|
1968
|
+
if (removedValue !== void 0 || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
|
|
1969
|
+
sparseStore.deleteValue(entityId, componentType);
|
|
1970
|
+
}
|
|
1971
|
+
for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
|
|
1972
|
+
}
|
|
1973
|
+
function applySparseChangesNoHooks(sparseStore, entityId, changeset) {
|
|
1974
|
+
for (const componentType of changeset.removes) if (isSparseRelation(componentType)) sparseStore.deleteValue(entityId, componentType);
|
|
1975
|
+
for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
|
|
1976
|
+
}
|
|
1977
|
+
function filterRegularComponentTypes(componentTypes) {
|
|
1978
|
+
const regularTypes = [];
|
|
1979
|
+
for (const componentType of componentTypes) {
|
|
1980
|
+
if (isSparseWildcard(componentType)) {
|
|
1981
|
+
regularTypes.push(componentType);
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (isSparseRelation(componentType)) continue;
|
|
1985
|
+
regularTypes.push(componentType);
|
|
1986
|
+
}
|
|
1987
|
+
return regularTypes;
|
|
1988
|
+
}
|
|
1989
|
+
//#endregion
|
|
1990
|
+
//#region src/world/archetype-manager.ts
|
|
1991
|
+
/**
|
|
1992
|
+
* Encapsulates all archetype storage, indexing, creation, removal, and reverse
|
|
1993
|
+
* referencing logic that was previously scattered as private methods + maps
|
|
1994
|
+
* directly on the World class.
|
|
1995
|
+
*
|
|
1996
|
+
* Responsibilities:
|
|
1997
|
+
* - Archetype memoization by signature
|
|
1998
|
+
* - Component-type reverse index (archetypesByComponent)
|
|
1999
|
+
* - Entity → current Archetype map
|
|
2000
|
+
* - Reverse "who references this entity via component/relation" index
|
|
2001
|
+
* - Creation + removal with proper notifications to QueryRegistry + hook matching
|
|
2002
|
+
* - Cleanup of empty archetypes after entity cascades
|
|
2003
|
+
*
|
|
2004
|
+
* This extraction shrinks World while keeping the same behavior and hot-path characteristics.
|
|
2005
|
+
*/
|
|
2006
|
+
var ArchetypeManager = class {
|
|
2007
|
+
archetypes = [];
|
|
2008
|
+
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
2009
|
+
entityToArchetype = /* @__PURE__ */ new Map();
|
|
2010
|
+
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
2011
|
+
entityToReferencingArchetypes = /* @__PURE__ */ new Map();
|
|
2012
|
+
sparseStore;
|
|
2013
|
+
ctx;
|
|
2014
|
+
constructor(ctx, sparseStore) {
|
|
2015
|
+
this.ctx = ctx;
|
|
2016
|
+
this.sparseStore = sparseStore;
|
|
2017
|
+
}
|
|
2018
|
+
/** Primary entry point — memoized archetype creation/lookup. */
|
|
2019
|
+
ensureArchetype(componentTypes) {
|
|
2020
|
+
const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
|
|
2021
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
2022
|
+
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
2023
|
+
}
|
|
2024
|
+
getArchetypeForEntity(entityId) {
|
|
2025
|
+
return this.entityToArchetype.get(entityId);
|
|
2026
|
+
}
|
|
2027
|
+
setEntityToArchetype(entityId, archetype) {
|
|
2028
|
+
this.entityToArchetype.set(entityId, archetype);
|
|
2029
|
+
}
|
|
2030
|
+
getMatchingArchetypes(componentTypes) {
|
|
2031
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2032
|
+
const regularComponents = [];
|
|
2033
|
+
const wildcardRelations = [];
|
|
2034
|
+
for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
|
|
2035
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
2036
|
+
if (componentId !== void 0) wildcardRelations.push({
|
|
2037
|
+
componentId,
|
|
2038
|
+
relationId: componentType
|
|
2039
|
+
});
|
|
2040
|
+
} else regularComponents.push(componentType);
|
|
2041
|
+
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
2042
|
+
for (const { componentId, relationId } of wildcardRelations) {
|
|
2043
|
+
const markerSet = this.archetypesByComponent.get(relationId);
|
|
2044
|
+
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
2045
|
+
matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
2046
|
+
}
|
|
2047
|
+
return matchingArchetypes;
|
|
2048
|
+
}
|
|
2049
|
+
getArchetypesWithComponents(componentTypes) {
|
|
2050
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2051
|
+
if (componentTypes.length === 1) {
|
|
2052
|
+
const set = this.archetypesByComponent.get(componentTypes[0]);
|
|
2053
|
+
return set ? Array.from(set) : [];
|
|
2054
|
+
}
|
|
2055
|
+
const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
|
|
2056
|
+
if (sets.length === 0) return [];
|
|
2057
|
+
if (sets.length < componentTypes.length) return [];
|
|
2058
|
+
const smallest = sets[0];
|
|
2059
|
+
if (sets.length === 2) {
|
|
2060
|
+
const other = sets[1];
|
|
2061
|
+
return Array.from(smallest).filter((a) => other.has(a));
|
|
2062
|
+
}
|
|
2063
|
+
let result = new Set(smallest);
|
|
2064
|
+
for (let i = 1; i < sets.length; i++) {
|
|
2065
|
+
for (const item of result) if (!sets[i].has(item)) result.delete(item);
|
|
2066
|
+
if (result.size === 0) return [];
|
|
2067
|
+
}
|
|
2068
|
+
return Array.from(result);
|
|
2069
|
+
}
|
|
2070
|
+
createArchetypeSignature(componentTypes) {
|
|
2071
|
+
return componentTypes.join(",");
|
|
2072
|
+
}
|
|
2073
|
+
/** Deduplicated version of the original pair of methods. */
|
|
2074
|
+
updateReferencingIndex(componentType, archetype, isAdd) {
|
|
2075
|
+
const detailedType = getDetailedIdType(componentType);
|
|
2076
|
+
let entityId;
|
|
2077
|
+
if (detailedType.type === "entity") entityId = componentType;
|
|
2078
|
+
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
2079
|
+
if (entityId !== void 0) {
|
|
2080
|
+
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2081
|
+
if (isAdd) {
|
|
2082
|
+
if (!refs) {
|
|
2083
|
+
refs = /* @__PURE__ */ new Set();
|
|
2084
|
+
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
2085
|
+
}
|
|
2086
|
+
refs.add(archetype);
|
|
2087
|
+
} else if (refs) {
|
|
2088
|
+
refs.delete(archetype);
|
|
2089
|
+
if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
createNewArchetype(componentTypes) {
|
|
2094
|
+
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
2095
|
+
this.archetypes.push(newArchetype);
|
|
2096
|
+
this.ctx.recordArchetypeCreated?.();
|
|
2097
|
+
for (const componentType of componentTypes) {
|
|
2098
|
+
let archetypes = this.archetypesByComponent.get(componentType);
|
|
2099
|
+
if (!archetypes) {
|
|
2100
|
+
archetypes = /* @__PURE__ */ new Set();
|
|
2101
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
2102
|
+
}
|
|
2103
|
+
archetypes.add(newArchetype);
|
|
2104
|
+
this.updateReferencingIndex(componentType, newArchetype, true);
|
|
2105
|
+
}
|
|
2106
|
+
this.ctx.queryRegistry.onNewArchetype(newArchetype);
|
|
2107
|
+
this.updateArchetypeHookMatches(newArchetype);
|
|
2108
|
+
return newArchetype;
|
|
2109
|
+
}
|
|
2110
|
+
updateArchetypeHookMatches(archetype) {
|
|
2111
|
+
for (const entry of this.ctx.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
|
|
2112
|
+
archetype.matchingMultiHooks.add(entry);
|
|
2113
|
+
if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
archetypeMatchesHook(archetype, entry) {
|
|
2117
|
+
return entry.requiredComponents.every((c) => {
|
|
2118
|
+
if (isWildcardRelationId(c)) {
|
|
2119
|
+
if (isSparseWildcard(c)) return true;
|
|
2120
|
+
const componentId = getComponentIdFromRelationId(c);
|
|
2121
|
+
return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
|
|
2122
|
+
}
|
|
2123
|
+
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
2124
|
+
}) && matchesFilter(archetype, entry.filter);
|
|
2125
|
+
}
|
|
2126
|
+
/** Called during cascade deletion cleanup. */
|
|
2127
|
+
cleanupArchetypesReferencingEntity(entityId) {
|
|
2128
|
+
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2129
|
+
if (!refs) return;
|
|
2130
|
+
for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
|
|
2131
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
2132
|
+
}
|
|
2133
|
+
removeArchetype(archetype) {
|
|
2134
|
+
const index = this.archetypes.indexOf(archetype);
|
|
2135
|
+
if (index !== -1) {
|
|
2136
|
+
const last = this.archetypes[this.archetypes.length - 1];
|
|
2137
|
+
this.archetypes[index] = last;
|
|
2138
|
+
this.archetypes.pop();
|
|
2139
|
+
}
|
|
2140
|
+
this.ctx.recordArchetypeRemoved?.();
|
|
2141
|
+
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
2142
|
+
for (const componentType of archetype.componentTypes) {
|
|
2143
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
2144
|
+
if (archetypes) {
|
|
2145
|
+
archetypes.delete(archetype);
|
|
2146
|
+
if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
|
|
2147
|
+
}
|
|
2148
|
+
this.updateReferencingIndex(componentType, archetype, false);
|
|
2149
|
+
}
|
|
2150
|
+
this.ctx.queryRegistry.onArchetypeRemoved(archetype);
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
//#endregion
|
|
2154
|
+
//#region src/commands/changeset.ts
|
|
2155
|
+
/**
|
|
2156
|
+
* @internal Represents a set of component changes to be applied to an entity
|
|
2157
|
+
*/
|
|
2158
|
+
var ComponentChangeset = class {
|
|
2159
|
+
adds = /* @__PURE__ */ new Map();
|
|
2160
|
+
removes = /* @__PURE__ */ new Set();
|
|
2161
|
+
/**
|
|
2162
|
+
* Add a component to the changeset
|
|
2163
|
+
*/
|
|
2164
|
+
set(componentType, component) {
|
|
2165
|
+
this.adds.set(componentType, component);
|
|
2166
|
+
this.removes.delete(componentType);
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Remove a component from the changeset
|
|
2170
|
+
*/
|
|
2171
|
+
delete(componentType) {
|
|
2172
|
+
this.removes.add(componentType);
|
|
2173
|
+
this.adds.delete(componentType);
|
|
1835
2174
|
}
|
|
1836
2175
|
/**
|
|
1837
|
-
*
|
|
1838
|
-
* Called automatically by the `Query` constructor via `world._registerQuery`.
|
|
2176
|
+
* Check if the changeset has any changes
|
|
1839
2177
|
*/
|
|
1840
|
-
|
|
1841
|
-
this.
|
|
2178
|
+
hasChanges() {
|
|
2179
|
+
return this.adds.size > 0 || this.removes.size > 0;
|
|
1842
2180
|
}
|
|
1843
2181
|
/**
|
|
1844
|
-
*
|
|
1845
|
-
* Called by `Query._disposeInternal` via `world._unregisterQuery`.
|
|
2182
|
+
* Clear all changes
|
|
1846
2183
|
*/
|
|
1847
|
-
|
|
1848
|
-
this.
|
|
2184
|
+
clear() {
|
|
2185
|
+
this.adds.clear();
|
|
2186
|
+
this.removes.clear();
|
|
1849
2187
|
}
|
|
1850
2188
|
/**
|
|
1851
|
-
*
|
|
1852
|
-
* Queries will add the archetype to their cache if it matches.
|
|
2189
|
+
* Merge another changeset into this one
|
|
1853
2190
|
*/
|
|
1854
|
-
|
|
1855
|
-
for (const
|
|
2191
|
+
merge(other) {
|
|
2192
|
+
for (const [componentType, component] of other.adds) {
|
|
2193
|
+
this.adds.set(componentType, component);
|
|
2194
|
+
this.removes.delete(componentType);
|
|
2195
|
+
}
|
|
2196
|
+
for (const componentType of other.removes) {
|
|
2197
|
+
this.removes.add(componentType);
|
|
2198
|
+
this.adds.delete(componentType);
|
|
2199
|
+
}
|
|
1856
2200
|
}
|
|
1857
2201
|
/**
|
|
1858
|
-
*
|
|
1859
|
-
* Queries will remove the archetype from their internal cache.
|
|
2202
|
+
* Apply the changeset to existing components and return the final state
|
|
1860
2203
|
*/
|
|
1861
|
-
|
|
1862
|
-
for (const
|
|
2204
|
+
applyTo(existingComponents) {
|
|
2205
|
+
for (const componentType of this.removes) existingComponents.delete(componentType);
|
|
2206
|
+
for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
|
|
2207
|
+
return existingComponents;
|
|
1863
2208
|
}
|
|
1864
2209
|
};
|
|
1865
2210
|
//#endregion
|
|
1866
|
-
//#region src/world/
|
|
1867
|
-
function processCommands(entityId, currentArchetype, commands, changeset, handleExclusiveRelation) {
|
|
1868
|
-
for (const command of commands) if (command.type === "set") processSetCommand(entityId, currentArchetype, command.componentType, command.component, changeset, handleExclusiveRelation);
|
|
1869
|
-
else if (command.type === "delete") processDeleteCommand(entityId, currentArchetype, command.componentType, changeset);
|
|
1870
|
-
}
|
|
1871
|
-
function processSetCommand(entityId, currentArchetype, componentType, component, changeset, handleExclusiveRelation) {
|
|
1872
|
-
const componentId = getComponentIdFromRelationId(componentType);
|
|
1873
|
-
if (componentId !== void 0) {
|
|
1874
|
-
handleExclusiveRelation(entityId, currentArchetype, componentId);
|
|
1875
|
-
if (isDontFragmentComponent(componentId)) {
|
|
1876
|
-
const wildcardMarker = relation(componentId, "*");
|
|
1877
|
-
if (!currentArchetype.componentTypeSet.has(wildcardMarker)) changeset.set(wildcardMarker, void 0);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
const merge = getComponentMerge(componentType);
|
|
1881
|
-
if (merge !== void 0 && changeset.adds.has(componentType)) {
|
|
1882
|
-
const prev = changeset.adds.get(componentType);
|
|
1883
|
-
changeset.set(componentType, merge(prev, component));
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
changeset.set(componentType, component);
|
|
1887
|
-
}
|
|
1888
|
-
function processDeleteCommand(entityId, currentArchetype, componentType, changeset) {
|
|
1889
|
-
const componentId = getComponentIdFromRelationId(componentType);
|
|
1890
|
-
if (isWildcardRelationId(componentType) && componentId !== void 0) removeWildcardRelations(entityId, currentArchetype, componentId, changeset);
|
|
1891
|
-
else {
|
|
1892
|
-
changeset.delete(componentType);
|
|
1893
|
-
maybeRemoveWildcardMarker(entityId, currentArchetype, componentType, componentId, changeset);
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
function removeMatchingRelations(entityId, archetype, baseComponentId, changeset) {
|
|
1897
|
-
for (const componentType of archetype.componentTypes) {
|
|
1898
|
-
if (isWildcardRelationId(componentType)) continue;
|
|
1899
|
-
if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
|
|
1900
|
-
}
|
|
1901
|
-
const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
|
|
1902
|
-
if (dontFragmentData) {
|
|
1903
|
-
for (const componentType of dontFragmentData.keys()) if (getComponentIdFromRelationId(componentType) === baseComponentId) changeset.delete(componentType);
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
function removeWildcardRelations(entityId, currentArchetype, baseComponentId, changeset) {
|
|
1907
|
-
removeMatchingRelations(entityId, currentArchetype, baseComponentId, changeset);
|
|
1908
|
-
if (isDontFragmentComponent(baseComponentId)) changeset.delete(relation(baseComponentId, "*"));
|
|
1909
|
-
}
|
|
1910
|
-
function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, componentId, changeset) {
|
|
1911
|
-
if (componentId === void 0 || !isDontFragmentComponent(componentId)) return;
|
|
1912
|
-
const wildcardMarker = relation(componentId, "*");
|
|
1913
|
-
for (const otherComponentType of archetype.componentTypes) {
|
|
1914
|
-
if (otherComponentType === removedComponentType) continue;
|
|
1915
|
-
if (otherComponentType === wildcardMarker) continue;
|
|
1916
|
-
if (changeset.removes.has(otherComponentType)) continue;
|
|
1917
|
-
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1918
|
-
}
|
|
1919
|
-
const dontFragmentData = archetype.getEntityDontFragmentRelations(entityId);
|
|
1920
|
-
if (dontFragmentData) for (const otherComponentType of dontFragmentData.keys()) {
|
|
1921
|
-
if (otherComponentType === removedComponentType) continue;
|
|
1922
|
-
if (changeset.removes.has(otherComponentType)) continue;
|
|
1923
|
-
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1924
|
-
}
|
|
1925
|
-
for (const addedType of changeset.adds.keys()) {
|
|
1926
|
-
if (addedType === removedComponentType) continue;
|
|
1927
|
-
if (getComponentIdFromRelationId(addedType) === componentId) return;
|
|
1928
|
-
}
|
|
1929
|
-
changeset.delete(wildcardMarker);
|
|
1930
|
-
}
|
|
1931
|
-
function hasEntityComponent(archetype, entityId, componentType) {
|
|
1932
|
-
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
1933
|
-
return archetype.getEntityDontFragmentRelations(entityId)?.has(componentType) ?? false;
|
|
1934
|
-
}
|
|
1935
|
-
function pruneMissingRemovals(changeset, archetype, entityId) {
|
|
1936
|
-
let toPrune;
|
|
1937
|
-
for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
|
|
1938
|
-
if (toPrune === void 0) toPrune = [];
|
|
1939
|
-
toPrune.push(componentType);
|
|
1940
|
-
}
|
|
1941
|
-
if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
|
|
1942
|
-
}
|
|
1943
|
-
function hasArchetypeStructuralChange(changeset, currentArchetype) {
|
|
1944
|
-
for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1945
|
-
for (const componentType of changeset.adds.keys()) if (!isDontFragmentRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1946
|
-
return false;
|
|
1947
|
-
}
|
|
1948
|
-
function buildFinalRegularComponentTypes(currentArchetype, changeset) {
|
|
1949
|
-
const finalRegularTypes = new Set(currentArchetype.componentTypes);
|
|
1950
|
-
for (const componentType of changeset.removes) if (!isDontFragmentRelation(componentType)) finalRegularTypes.delete(componentType);
|
|
1951
|
-
for (const [componentType] of changeset.adds) if (!isDontFragmentRelation(componentType)) finalRegularTypes.add(componentType);
|
|
1952
|
-
return Array.from(finalRegularTypes);
|
|
1953
|
-
}
|
|
1954
|
-
function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
|
|
1955
|
-
pruneMissingRemovals(changeset, currentArchetype, entityId);
|
|
1956
|
-
if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
|
|
1957
|
-
const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
|
|
1958
|
-
const newArchetype = ctx.ensureArchetype(finalRegularTypes);
|
|
1959
|
-
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1960
|
-
if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
|
|
1961
|
-
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1962
|
-
entityToArchetype.set(entityId, newArchetype);
|
|
1963
|
-
return newArchetype;
|
|
1964
|
-
}
|
|
1965
|
-
if (removedComponents !== null) applyDontFragmentChanges(ctx.dontFragmentStore, entityId, changeset, removedComponents);
|
|
1966
|
-
else applyDontFragmentChangesNoHooks(ctx.dontFragmentStore, entityId, changeset);
|
|
1967
|
-
for (const [componentType, component] of changeset.adds) {
|
|
1968
|
-
if (isDontFragmentRelation(componentType)) continue;
|
|
1969
|
-
currentArchetype.set(entityId, componentType, component);
|
|
1970
|
-
}
|
|
1971
|
-
return currentArchetype;
|
|
1972
|
-
}
|
|
2211
|
+
//#region src/world/hooks.ts
|
|
1973
2212
|
/**
|
|
1974
|
-
*
|
|
1975
|
-
*
|
|
1976
|
-
* Rewritten for the new DontFragmentStore interface (ComponentId-primary storage).
|
|
2213
|
+
* Debug-only counter incremented on every invokeHook call when armed.
|
|
2214
|
+
* World reads and resets this during armed syncs.
|
|
1977
2215
|
*/
|
|
1978
|
-
|
|
1979
|
-
for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) {
|
|
1980
|
-
const removedValue = dontFragmentRelations.getValue(entityId, componentType);
|
|
1981
|
-
if (removedValue !== void 0 || dontFragmentRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
|
|
1982
|
-
dontFragmentRelations.deleteValue(entityId, componentType);
|
|
1983
|
-
}
|
|
1984
|
-
for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) dontFragmentRelations.setValue(entityId, componentType, component);
|
|
1985
|
-
}
|
|
1986
|
-
function applyDontFragmentChangesNoHooks(dontFragmentRelations, entityId, changeset) {
|
|
1987
|
-
for (const componentType of changeset.removes) if (isDontFragmentRelation(componentType)) dontFragmentRelations.deleteValue(entityId, componentType);
|
|
1988
|
-
for (const [componentType, component] of changeset.adds) if (isDontFragmentRelation(componentType)) dontFragmentRelations.setValue(entityId, componentType, component);
|
|
1989
|
-
}
|
|
1990
|
-
function filterRegularComponentTypes(componentTypes) {
|
|
1991
|
-
const regularTypes = [];
|
|
1992
|
-
for (const componentType of componentTypes) {
|
|
1993
|
-
if (isDontFragmentWildcard(componentType)) {
|
|
1994
|
-
regularTypes.push(componentType);
|
|
1995
|
-
continue;
|
|
1996
|
-
}
|
|
1997
|
-
if (isDontFragmentRelation(componentType)) continue;
|
|
1998
|
-
regularTypes.push(componentType);
|
|
1999
|
-
}
|
|
2000
|
-
return regularTypes;
|
|
2001
|
-
}
|
|
2002
|
-
//#endregion
|
|
2003
|
-
//#region src/world/hooks.ts
|
|
2216
|
+
const debugHookExecutionCounter = { value: 0 };
|
|
2004
2217
|
/**
|
|
2005
2218
|
* Unified hook invocation: prefers entry.callback (callback style) over hook.on_* (object style).
|
|
2006
2219
|
*/
|
|
2007
2220
|
function invokeHook(entry, event, entityId, components) {
|
|
2221
|
+
debugHookExecutionCounter.value++;
|
|
2008
2222
|
if (entry.callback) {
|
|
2009
2223
|
entry.callback(event, entityId, ...components);
|
|
2010
2224
|
return;
|
|
@@ -2301,11 +2515,275 @@ function getEntityReferences(entityReferences, targetEntityId) {
|
|
|
2301
2515
|
return entityReferences.get(targetEntityId) ?? new MultiMap();
|
|
2302
2516
|
}
|
|
2303
2517
|
//#endregion
|
|
2518
|
+
//#region src/world/command-executor.ts
|
|
2519
|
+
/**
|
|
2520
|
+
* Encapsulates the command execution pipeline, reusable changesets,
|
|
2521
|
+
* and related orchestration that was previously private methods + fields on World.
|
|
2522
|
+
*
|
|
2523
|
+
* Responsibilities:
|
|
2524
|
+
* - executeEntityCommands (routing for singletons / destroy / structural changes)
|
|
2525
|
+
* - applyEntityCommands (changeset processing + exclusive relations + apply + refs + hooks)
|
|
2526
|
+
* - removeComponentImmediate (used by cascade deletion)
|
|
2527
|
+
* - updateEntityReferences (keeps the reverse index in sync)
|
|
2528
|
+
*
|
|
2529
|
+
* This extraction significantly reduces World line count while preserving
|
|
2530
|
+
* every fast-path branch and allocation-avoidance characteristic.
|
|
2531
|
+
*/
|
|
2532
|
+
var CommandExecutor = class {
|
|
2533
|
+
ctx;
|
|
2534
|
+
_changeset = new ComponentChangeset();
|
|
2535
|
+
_removeChangeset = new ComponentChangeset();
|
|
2536
|
+
_commandCtx;
|
|
2537
|
+
_hooksCtx;
|
|
2538
|
+
constructor(ctx) {
|
|
2539
|
+
this.ctx = ctx;
|
|
2540
|
+
this._commandCtx = {
|
|
2541
|
+
sparseStore: ctx.sparseStore,
|
|
2542
|
+
ensureArchetype: ctx.ensureArchetype
|
|
2543
|
+
};
|
|
2544
|
+
this._hooksCtx = {
|
|
2545
|
+
multiHooks: ctx.hooks,
|
|
2546
|
+
has: ctx.has,
|
|
2547
|
+
get: ctx.get,
|
|
2548
|
+
getOptional: ctx.getOptional
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Entry point used by the CommandBuffer.
|
|
2553
|
+
* Routes to singleton handling, destroy fast path, or structural apply.
|
|
2554
|
+
*/
|
|
2555
|
+
executeEntityCommands(entityId, commands) {
|
|
2556
|
+
this._changeset.clear();
|
|
2557
|
+
if (this.ctx.componentEntities.exists(entityId)) {
|
|
2558
|
+
this.ctx.componentEntities.executeCommands(entityId, commands);
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
2562
|
+
this.ctx.destroyEntityImmediate(entityId);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
this.applyEntityCommands(entityId, commands);
|
|
2566
|
+
}
|
|
2567
|
+
applyEntityCommands(entityId, commands) {
|
|
2568
|
+
const currentArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
2569
|
+
if (!currentArchetype) return;
|
|
2570
|
+
const changeset = this._changeset;
|
|
2571
|
+
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
2572
|
+
if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
|
|
2573
|
+
});
|
|
2574
|
+
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
2575
|
+
if (this.ctx.hooks.size === 0) {
|
|
2576
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, null);
|
|
2577
|
+
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2578
|
+
if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const removedComponents = /* @__PURE__ */ new Map();
|
|
2582
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, removedComponents);
|
|
2583
|
+
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2584
|
+
if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
|
|
2585
|
+
this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Immediate (non-buffered) component removal used during cascade deletion.
|
|
2589
|
+
* Called from destroy* paths (which remain in World).
|
|
2590
|
+
*/
|
|
2591
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
2592
|
+
const sourceArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
2593
|
+
if (!sourceArchetype) return;
|
|
2594
|
+
const changeset = this._removeChangeset;
|
|
2595
|
+
changeset.clear();
|
|
2596
|
+
changeset.delete(componentType);
|
|
2597
|
+
maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
|
|
2598
|
+
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
2599
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.ctx.entityToArchetype, null);
|
|
2600
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetEntityId);
|
|
2601
|
+
this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Keeps the entity reference reverse index in sync after structural changes.
|
|
2605
|
+
* Called from apply paths.
|
|
2606
|
+
*/
|
|
2607
|
+
updateEntityReferences(entityId, changeset) {
|
|
2608
|
+
for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
|
|
2609
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
2610
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
2611
|
+
} else if (componentType >= 1024) untrackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
2612
|
+
for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
|
|
2613
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
2614
|
+
trackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
2615
|
+
} else if (componentType >= 1024) trackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Exposed for any future direct needs (currently not required outside the executor).
|
|
2619
|
+
*/
|
|
2620
|
+
getHooksContext() {
|
|
2621
|
+
return this._hooksCtx;
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
//#endregion
|
|
2625
|
+
//#region src/world/debug-stats.ts
|
|
2626
|
+
/**
|
|
2627
|
+
* Manages debug stats collectors and transient activity counters for World#sync().
|
|
2628
|
+
*
|
|
2629
|
+
* Extracted from World to shrink the main class while keeping the entire debug/observability
|
|
2630
|
+
* path isolated, zero-cost when no collectors are active, and easy to test/maintain.
|
|
2631
|
+
*
|
|
2632
|
+
* Follows the same context/callback injection style as ArchetypeManager, CommandProcessorContext,
|
|
2633
|
+
* and HooksContext to avoid tight coupling.
|
|
2634
|
+
*
|
|
2635
|
+
* All collectors receive the *exact same* stats object for a given sync (as before).
|
|
2636
|
+
* Exceptions in user callbacks are swallowed (as before).
|
|
2637
|
+
*/
|
|
2638
|
+
var DebugStatsManager = class {
|
|
2639
|
+
collectors = /* @__PURE__ */ new Set();
|
|
2640
|
+
migrations = 0;
|
|
2641
|
+
archetypesCreated = 0;
|
|
2642
|
+
archetypesRemoved = 0;
|
|
2643
|
+
/** Fast check used to arm timing + reset + counting in hot paths. */
|
|
2644
|
+
hasActiveCollectors() {
|
|
2645
|
+
return this.collectors.size > 0;
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Registers a collector. Returns a disposable handle (supports `using`).
|
|
2649
|
+
* Collection stops when the handle is disposed.
|
|
2650
|
+
*/
|
|
2651
|
+
createCollector(callback) {
|
|
2652
|
+
this.collectors.add(callback);
|
|
2653
|
+
return { [Symbol.dispose]: () => {
|
|
2654
|
+
this.collectors.delete(callback);
|
|
2655
|
+
} };
|
|
2656
|
+
}
|
|
2657
|
+
recordArchetypeCreated() {
|
|
2658
|
+
if (this.hasActiveCollectors()) this.archetypesCreated++;
|
|
2659
|
+
}
|
|
2660
|
+
recordArchetypeRemoved() {
|
|
2661
|
+
if (this.hasActiveCollectors()) this.archetypesRemoved++;
|
|
2662
|
+
}
|
|
2663
|
+
incrementMigrations() {
|
|
2664
|
+
if (this.hasActiveCollectors()) this.migrations++;
|
|
2665
|
+
}
|
|
2666
|
+
/** Reset all activity counters + the shared hook execution counter. Called at start of an armed sync. */
|
|
2667
|
+
resetActivity() {
|
|
2668
|
+
this.migrations = 0;
|
|
2669
|
+
this.archetypesCreated = 0;
|
|
2670
|
+
this.archetypesRemoved = 0;
|
|
2671
|
+
debugHookExecutionCounter.value = 0;
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Build and deliver a SyncDebugStats payload to every active collector.
|
|
2675
|
+
* World supplies the pre-computed snapshot numbers (keeps debug manager decoupled from
|
|
2676
|
+
* internal World maps/registries while preserving exact original stats shape and values).
|
|
2677
|
+
*/
|
|
2678
|
+
deliver(timings, data) {
|
|
2679
|
+
const stats = {
|
|
2680
|
+
timestamps: {
|
|
2681
|
+
syncStart: timings.syncStart,
|
|
2682
|
+
syncEnd: timings.syncEnd,
|
|
2683
|
+
commandBufferStart: timings.commandBufferStart,
|
|
2684
|
+
commandBufferEnd: timings.commandBufferEnd
|
|
2685
|
+
},
|
|
2686
|
+
commandIterations: timings.commandIterations,
|
|
2687
|
+
entities: {
|
|
2688
|
+
total: data.entityCount,
|
|
2689
|
+
freelistSize: data.freelistSize,
|
|
2690
|
+
nextId: data.nextId
|
|
2691
|
+
},
|
|
2692
|
+
archetypes: {
|
|
2693
|
+
total: data.archetypeCount,
|
|
2694
|
+
empty: data.emptyArchetypes
|
|
2695
|
+
},
|
|
2696
|
+
queries: {
|
|
2697
|
+
cached: data.cachedQueryCount,
|
|
2698
|
+
registered: data.registeredQueryCount
|
|
2699
|
+
},
|
|
2700
|
+
hooks: { total: data.hookCount },
|
|
2701
|
+
indices: {
|
|
2702
|
+
entityReferences: data.entityReferencesSize,
|
|
2703
|
+
entityToReferencingArchetypes: data.entityToReferencingArchetypesSize,
|
|
2704
|
+
archetypesByComponent: data.archetypesByComponentSize
|
|
2705
|
+
},
|
|
2706
|
+
activity: {
|
|
2707
|
+
migrations: this.migrations,
|
|
2708
|
+
hooksExecuted: debugHookExecutionCounter.value,
|
|
2709
|
+
archetypesCreated: this.archetypesCreated,
|
|
2710
|
+
archetypesRemoved: this.archetypesRemoved
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
for (const cb of this.collectors) try {
|
|
2714
|
+
cb(stats);
|
|
2715
|
+
} catch {}
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
//#endregion
|
|
2719
|
+
//#region src/world/operations.ts
|
|
2720
|
+
/**
|
|
2721
|
+
* Validation and overload-resolution helpers extracted from World.
|
|
2722
|
+
*
|
|
2723
|
+
* These were previously private methods on World. Moving them reduces line count
|
|
2724
|
+
* in the core class with almost zero coupling (the only dep is a liveness predicate
|
|
2725
|
+
* for assertEntityExists, supplied by the caller).
|
|
2726
|
+
*
|
|
2727
|
+
* Pure type checks (assert*TypeValid) and the resolve* helpers for set/remove
|
|
2728
|
+
* overloads live here.
|
|
2729
|
+
*/
|
|
2730
|
+
/**
|
|
2731
|
+
* Assert that an entity (or component-entity) is alive in the world.
|
|
2732
|
+
* The caller supplies the liveness check (World.exists or equivalent) to keep
|
|
2733
|
+
* this module free of direct references to stores.
|
|
2734
|
+
*/
|
|
2735
|
+
function assertEntityExists(entityId, label, exists) {
|
|
2736
|
+
if (!exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
|
|
2737
|
+
}
|
|
2738
|
+
function assertComponentTypeValid(componentType) {
|
|
2739
|
+
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2740
|
+
}
|
|
2741
|
+
function assertSetComponentTypeValid(componentType) {
|
|
2742
|
+
const detailedType = getDetailedIdType(componentType);
|
|
2743
|
+
if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2744
|
+
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Resolve the (entity, componentType, value) for a set() call.
|
|
2748
|
+
*/
|
|
2749
|
+
function resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, exists = () => true) {
|
|
2750
|
+
const targetEntityId = entityId;
|
|
2751
|
+
const componentType = componentTypeOrComponent;
|
|
2752
|
+
assertEntityExists(targetEntityId, "Entity", exists);
|
|
2753
|
+
assertSetComponentTypeValid(componentType);
|
|
2754
|
+
return {
|
|
2755
|
+
entityId: targetEntityId,
|
|
2756
|
+
componentType,
|
|
2757
|
+
component: maybeComponent
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* Resolve the (entity, componentType) for a remove() call, handling the
|
|
2762
|
+
* singleton component overload (remove(componentId)).
|
|
2763
|
+
*/
|
|
2764
|
+
function resolveRemoveOperation(entityId, componentType, exists = () => true) {
|
|
2765
|
+
if (componentType === void 0) {
|
|
2766
|
+
const componentId = entityId;
|
|
2767
|
+
assertEntityExists(componentId, "Component entity", exists);
|
|
2768
|
+
return {
|
|
2769
|
+
entityId: componentId,
|
|
2770
|
+
componentType: componentId
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
const targetEntityId = entityId;
|
|
2774
|
+
assertEntityExists(targetEntityId, "Entity", exists);
|
|
2775
|
+
assertComponentTypeValid(componentType);
|
|
2776
|
+
return {
|
|
2777
|
+
entityId: targetEntityId,
|
|
2778
|
+
componentType
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
//#endregion
|
|
2304
2782
|
//#region src/storage/serialization.ts
|
|
2305
2783
|
/**
|
|
2306
|
-
*
|
|
2784
|
+
* Core encoding logic (no cache). Extracted so cached wrapper can reuse it without duplication.
|
|
2307
2785
|
*/
|
|
2308
|
-
function
|
|
2786
|
+
function encodeEntityIdCore(id) {
|
|
2309
2787
|
const detailed = getDetailedIdType(id);
|
|
2310
2788
|
switch (detailed.type) {
|
|
2311
2789
|
case "component": {
|
|
@@ -2343,6 +2821,20 @@ function encodeEntityId(id) {
|
|
|
2343
2821
|
}
|
|
2344
2822
|
}
|
|
2345
2823
|
/**
|
|
2824
|
+
* Encode an EntityId, using an optional cache Map to avoid repeated getDetailedIdType
|
|
2825
|
+
* + name lookup work for IDs that appear many times (typical during full world snapshot).
|
|
2826
|
+
*/
|
|
2827
|
+
function encodeEntityIdCached(id, cache) {
|
|
2828
|
+
if (cache) {
|
|
2829
|
+
const cached = cache.get(id);
|
|
2830
|
+
if (cached !== void 0) return cached;
|
|
2831
|
+
const result = encodeEntityIdCore(id);
|
|
2832
|
+
cache.set(id, result);
|
|
2833
|
+
return result;
|
|
2834
|
+
}
|
|
2835
|
+
return encodeEntityIdCore(id);
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2346
2838
|
* Decode a SerializedEntityId back into an internal EntityId
|
|
2347
2839
|
*/
|
|
2348
2840
|
function decodeSerializedId(sid) {
|
|
@@ -2383,24 +2875,16 @@ function decodeSerializedId(sid) {
|
|
|
2383
2875
|
* Serializes the full world state to a plain JS object suitable for JSON encoding.
|
|
2384
2876
|
*/
|
|
2385
2877
|
function serializeWorld(archetypes, componentEntities, entityIdManager) {
|
|
2878
|
+
const idCache = /* @__PURE__ */ new Map();
|
|
2386
2879
|
const entities = [];
|
|
2387
2880
|
for (const archetype of archetypes) {
|
|
2388
|
-
const
|
|
2389
|
-
|
|
2390
|
-
id: encodeEntityId(entity),
|
|
2391
|
-
components: Array.from(components.entries()).map(([rawType, value]) => ({
|
|
2392
|
-
type: encodeEntityId(rawType),
|
|
2393
|
-
value: value === MISSING_COMPONENT ? void 0 : value
|
|
2394
|
-
}))
|
|
2395
|
-
});
|
|
2881
|
+
const encodedComponentTypes = archetype.componentTypes.map((t) => encodeEntityIdCached(t, idCache));
|
|
2882
|
+
archetype.appendSerializedEntities(entities, (id) => encodeEntityIdCached(id, idCache), encodedComponentTypes);
|
|
2396
2883
|
}
|
|
2397
2884
|
const componentEntitiesArr = [];
|
|
2398
2885
|
for (const [entityId, components] of componentEntities.entries()) componentEntitiesArr.push({
|
|
2399
|
-
id:
|
|
2400
|
-
components:
|
|
2401
|
-
type: encodeEntityId(rawType),
|
|
2402
|
-
value: value === MISSING_COMPONENT ? void 0 : value
|
|
2403
|
-
}))
|
|
2886
|
+
id: encodeEntityIdCached(entityId, idCache),
|
|
2887
|
+
components: serializeComponentsFromMap(components, idCache)
|
|
2404
2888
|
});
|
|
2405
2889
|
return {
|
|
2406
2890
|
version: 1,
|
|
@@ -2409,6 +2893,15 @@ function serializeWorld(archetypes, componentEntities, entityIdManager) {
|
|
|
2409
2893
|
componentEntities: componentEntitiesArr
|
|
2410
2894
|
};
|
|
2411
2895
|
}
|
|
2896
|
+
/** Small helper to avoid duplicating the "Map → SerializedComponent[] with cache" pattern. */
|
|
2897
|
+
function serializeComponentsFromMap(components, idCache) {
|
|
2898
|
+
const result = [];
|
|
2899
|
+
for (const [rawType, value] of components) result.push({
|
|
2900
|
+
type: encodeEntityIdCached(rawType, idCache),
|
|
2901
|
+
value: value === MISSING_COMPONENT ? void 0 : value
|
|
2902
|
+
});
|
|
2903
|
+
return result;
|
|
2904
|
+
}
|
|
2412
2905
|
/**
|
|
2413
2906
|
* Restores world state from a snapshot into the provided context.
|
|
2414
2907
|
* Intended to be called from `World`'s constructor.
|
|
@@ -2430,12 +2923,11 @@ function deserializeWorld(ctx, snapshot) {
|
|
|
2430
2923
|
const entityId = decodeSerializedId(entry.id);
|
|
2431
2924
|
const componentsArray = entry.components || [];
|
|
2432
2925
|
const componentMap = /* @__PURE__ */ new Map();
|
|
2433
|
-
const componentTypes = [];
|
|
2434
2926
|
for (const componentEntry of componentsArray) {
|
|
2435
2927
|
const componentType = decodeSerializedId(componentEntry.type);
|
|
2436
2928
|
componentMap.set(componentType, componentEntry.value);
|
|
2437
|
-
componentTypes.push(componentType);
|
|
2438
2929
|
}
|
|
2930
|
+
const componentTypes = Array.from(componentMap.keys());
|
|
2439
2931
|
const archetype = ctx.ensureArchetype(componentTypes);
|
|
2440
2932
|
archetype.addEntity(entityId, componentMap);
|
|
2441
2933
|
ctx.setEntityToArchetype(entityId, archetype);
|
|
@@ -2454,45 +2946,60 @@ function deserializeWorld(ctx, snapshot) {
|
|
|
2454
2946
|
*/
|
|
2455
2947
|
var World = class {
|
|
2456
2948
|
entityIdManager = new EntityIdManager();
|
|
2457
|
-
archetypes = [];
|
|
2458
|
-
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
2459
|
-
entityToArchetype = /* @__PURE__ */ new Map();
|
|
2460
|
-
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
2461
2949
|
entityReferences = /* @__PURE__ */ new Map();
|
|
2462
|
-
/**
|
|
2463
|
-
|
|
2464
|
-
/** DontFragment relation storage, shared with all Archetype instances */
|
|
2465
|
-
dontFragmentStore = new DontFragmentStoreImpl();
|
|
2950
|
+
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
2951
|
+
sparseStore = new SparseStoreImpl();
|
|
2466
2952
|
/** Component entity (singleton) storage */
|
|
2467
2953
|
componentEntities = new ComponentEntityStore();
|
|
2954
|
+
archetypeManager;
|
|
2955
|
+
get archetypes() {
|
|
2956
|
+
return this.archetypeManager.archetypes;
|
|
2957
|
+
}
|
|
2958
|
+
get entityToArchetype() {
|
|
2959
|
+
return this.archetypeManager.entityToArchetype;
|
|
2960
|
+
}
|
|
2961
|
+
get archetypesByComponent() {
|
|
2962
|
+
return this.archetypeManager.archetypesByComponent;
|
|
2963
|
+
}
|
|
2964
|
+
get entityToReferencingArchetypes() {
|
|
2965
|
+
return this.archetypeManager.entityToReferencingArchetypes;
|
|
2966
|
+
}
|
|
2468
2967
|
queryRegistry = new QueryRegistry();
|
|
2469
2968
|
hooks = /* @__PURE__ */ new Set();
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
/** Cached command processor context to avoid per-entity object allocation */
|
|
2474
|
-
_commandCtx = {
|
|
2475
|
-
dontFragmentStore: this.dontFragmentStore,
|
|
2476
|
-
ensureArchetype: (ct) => this.ensureArchetype(ct)
|
|
2477
|
-
};
|
|
2478
|
-
/** Cached hooks context to avoid per-entity object allocation */
|
|
2479
|
-
_hooksCtx = {
|
|
2480
|
-
multiHooks: this.hooks,
|
|
2481
|
-
has: (eid, ct) => this.has(eid, ct),
|
|
2482
|
-
get: (eid, ct) => this.get(eid, ct),
|
|
2483
|
-
getOptional: (eid, ct) => this.getOptional(eid, ct)
|
|
2484
|
-
};
|
|
2969
|
+
debugStats = new DebugStatsManager();
|
|
2970
|
+
commandBuffer;
|
|
2971
|
+
commandExecutor;
|
|
2485
2972
|
constructor(snapshot) {
|
|
2973
|
+
this.archetypeManager = new ArchetypeManager({
|
|
2974
|
+
queryRegistry: this.queryRegistry,
|
|
2975
|
+
hooks: this.hooks,
|
|
2976
|
+
recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
|
|
2977
|
+
recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved()
|
|
2978
|
+
}, this.sparseStore);
|
|
2486
2979
|
if (snapshot && typeof snapshot === "object") deserializeWorld({
|
|
2487
2980
|
entityIdManager: this.entityIdManager,
|
|
2488
2981
|
componentEntities: this.componentEntities,
|
|
2489
2982
|
entityReferences: this.entityReferences,
|
|
2490
2983
|
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
2491
|
-
setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch)
|
|
2984
|
+
setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch)
|
|
2492
2985
|
}, snapshot);
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2986
|
+
const execCtx = {
|
|
2987
|
+
componentEntities: this.componentEntities,
|
|
2988
|
+
entityReferences: this.entityReferences,
|
|
2989
|
+
hooks: this.hooks,
|
|
2990
|
+
entityToArchetype: this.entityToArchetype,
|
|
2991
|
+
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
2992
|
+
sparseStore: this.sparseStore,
|
|
2993
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
2994
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
2995
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
2996
|
+
destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
|
|
2997
|
+
incrementMigrations: () => this.debugStats.incrementMigrations(),
|
|
2998
|
+
triggerLifecycleHooks,
|
|
2999
|
+
triggerRemoveHooksForEntityDeletion
|
|
3000
|
+
};
|
|
3001
|
+
this.commandExecutor = new CommandExecutor(execCtx);
|
|
3002
|
+
this.commandBuffer = new CommandBuffer((entityId, commands) => this.commandExecutor.executeEntityCommands(entityId, commands));
|
|
2496
3003
|
}
|
|
2497
3004
|
/**
|
|
2498
3005
|
* Creates a new entity.
|
|
@@ -2588,64 +3095,12 @@ var World = class {
|
|
|
2588
3095
|
if (this.componentEntities.exists(entityId)) return true;
|
|
2589
3096
|
return this.entityToArchetype.has(entityId);
|
|
2590
3097
|
}
|
|
2591
|
-
assertEntityExists(entityId, label) {
|
|
2592
|
-
if (!this.exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
|
|
2593
|
-
}
|
|
2594
|
-
assertComponentTypeValid(componentType) {
|
|
2595
|
-
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2596
|
-
}
|
|
2597
|
-
assertSetComponentTypeValid(componentType) {
|
|
2598
|
-
const detailedType = getDetailedIdType(componentType);
|
|
2599
|
-
if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2600
|
-
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
2601
|
-
}
|
|
2602
|
-
resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent) {
|
|
2603
|
-
if (maybeComponent === void 0 && componentTypeOrComponent !== void 0) {
|
|
2604
|
-
const detailedType = getDetailedIdType(entityId);
|
|
2605
|
-
if (detailedType.type === "component" || detailedType.type === "component-relation") {
|
|
2606
|
-
const componentId = entityId;
|
|
2607
|
-
this.assertEntityExists(componentId, "Component entity");
|
|
2608
|
-
this.assertSetComponentTypeValid(componentId);
|
|
2609
|
-
return {
|
|
2610
|
-
entityId: componentId,
|
|
2611
|
-
componentType: componentId,
|
|
2612
|
-
component: componentTypeOrComponent
|
|
2613
|
-
};
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
const targetEntityId = entityId;
|
|
2617
|
-
const componentType = componentTypeOrComponent;
|
|
2618
|
-
this.assertEntityExists(targetEntityId, "Entity");
|
|
2619
|
-
this.assertSetComponentTypeValid(componentType);
|
|
2620
|
-
return {
|
|
2621
|
-
entityId: targetEntityId,
|
|
2622
|
-
componentType,
|
|
2623
|
-
component: maybeComponent
|
|
2624
|
-
};
|
|
2625
|
-
}
|
|
2626
|
-
resolveRemoveOperation(entityId, componentType) {
|
|
2627
|
-
if (componentType === void 0) {
|
|
2628
|
-
const componentId = entityId;
|
|
2629
|
-
this.assertEntityExists(componentId, "Component entity");
|
|
2630
|
-
return {
|
|
2631
|
-
entityId: componentId,
|
|
2632
|
-
componentType: componentId
|
|
2633
|
-
};
|
|
2634
|
-
}
|
|
2635
|
-
const targetEntityId = entityId;
|
|
2636
|
-
this.assertEntityExists(targetEntityId, "Entity");
|
|
2637
|
-
this.assertComponentTypeValid(componentType);
|
|
2638
|
-
return {
|
|
2639
|
-
entityId: targetEntityId,
|
|
2640
|
-
componentType
|
|
2641
|
-
};
|
|
2642
|
-
}
|
|
2643
3098
|
set(entityId, componentTypeOrComponent, maybeComponent) {
|
|
2644
|
-
const { entityId: targetEntityId, componentType, component } =
|
|
3099
|
+
const { entityId: targetEntityId, componentType, component } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
|
|
2645
3100
|
this.commandBuffer.set(targetEntityId, componentType, component);
|
|
2646
3101
|
}
|
|
2647
3102
|
remove(entityId, componentType) {
|
|
2648
|
-
const { entityId: targetEntityId, componentType: targetComponentType } =
|
|
3103
|
+
const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(entityId, componentType, (id) => this.exists(id));
|
|
2649
3104
|
this.commandBuffer.remove(targetEntityId, targetComponentType);
|
|
2650
3105
|
}
|
|
2651
3106
|
/**
|
|
@@ -2662,6 +3117,30 @@ var World = class {
|
|
|
2662
3117
|
delete(entityId) {
|
|
2663
3118
|
this.commandBuffer.delete(entityId);
|
|
2664
3119
|
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Returns an explicit handle for a singleton component (component-as-entity).
|
|
3122
|
+
*
|
|
3123
|
+
* This is the preferred API for singleton components.
|
|
3124
|
+
*
|
|
3125
|
+
* @example
|
|
3126
|
+
* const config = world.singleton(GlobalConfig);
|
|
3127
|
+
* config.set({ debug: true });
|
|
3128
|
+
* world.sync();
|
|
3129
|
+
* console.log(config.get());
|
|
3130
|
+
*/
|
|
3131
|
+
singleton(componentId) {
|
|
3132
|
+
assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
|
|
3133
|
+
assertSetComponentTypeValid(componentId);
|
|
3134
|
+
return new SingletonHandle(componentId, {
|
|
3135
|
+
has: () => this.componentEntities.hasSingleton(componentId),
|
|
3136
|
+
get: () => this.get(componentId),
|
|
3137
|
+
getOptional: () => this.getOptional(componentId),
|
|
3138
|
+
remove: () => this.commandBuffer.remove(componentId, componentId),
|
|
3139
|
+
set: (value) => {
|
|
3140
|
+
this.commandBuffer.set(componentId, componentId, value);
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2665
3144
|
has(entityId, componentType) {
|
|
2666
3145
|
if (componentType === void 0) {
|
|
2667
3146
|
const componentId = entityId;
|
|
@@ -2678,9 +3157,9 @@ var World = class {
|
|
|
2678
3157
|
const archetype = this.entityToArchetype.get(entityId);
|
|
2679
3158
|
if (!archetype) return false;
|
|
2680
3159
|
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
2681
|
-
if (
|
|
2682
|
-
if (this.
|
|
2683
|
-
return this.
|
|
3160
|
+
if (isSparseRelation(componentType)) {
|
|
3161
|
+
if (this.sparseStore.getValue(entityId, componentType) !== void 0) return true;
|
|
3162
|
+
return this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType);
|
|
2684
3163
|
}
|
|
2685
3164
|
return false;
|
|
2686
3165
|
}
|
|
@@ -2693,8 +3172,8 @@ var World = class {
|
|
|
2693
3172
|
if (!archetype) throw new Error(`Entity ${entityId} does not exist`);
|
|
2694
3173
|
if (componentType >= 0 || componentType % RELATION_SHIFT !== 0) {
|
|
2695
3174
|
const inArchetype = archetype.componentTypeSet.has(componentType);
|
|
2696
|
-
const
|
|
2697
|
-
if (!(inArchetype ||
|
|
3175
|
+
const hasSparse = isSparseRelation(componentType);
|
|
3176
|
+
if (!(inArchetype || hasSparse && (this.sparseStore.getValue(entityId, componentType) !== void 0 || this.sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)))) throw new Error(`Entity ${entityId} does not have component ${componentType}. Use has() to check component existence before calling get().`);
|
|
2698
3177
|
}
|
|
2699
3178
|
return archetype.get(entityId, componentType);
|
|
2700
3179
|
}
|
|
@@ -2716,6 +3195,163 @@ var World = class {
|
|
|
2716
3195
|
}
|
|
2717
3196
|
return archetype.getOptional(entityId, componentType);
|
|
2718
3197
|
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Retrieves all targets (and their associated data) for relations of a given
|
|
3200
|
+
* base component on an entity.
|
|
3201
|
+
*
|
|
3202
|
+
* This is the ergonomic replacement for the common pattern:
|
|
3203
|
+
* world.get(entity, relation(Comp, "*"))
|
|
3204
|
+
*
|
|
3205
|
+
* @example
|
|
3206
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
3207
|
+
* const children = world.getRelationTargets(parent, ChildOf); // usually []
|
|
3208
|
+
* const items = world.getRelationTargets(player, InInventory);
|
|
3209
|
+
*
|
|
3210
|
+
* // For common hierarchy use cases, prefer the higher-level helpers:
|
|
3211
|
+
* // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
|
|
3212
|
+
*/
|
|
3213
|
+
getRelationTargets(entityId, relationComp) {
|
|
3214
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
3215
|
+
const wildcard = relation(relationComp, "*");
|
|
3216
|
+
if (this.componentEntities.exists(entityId)) return this.componentEntities.getWildcard(entityId, wildcard);
|
|
3217
|
+
return this.get(entityId, wildcard);
|
|
3218
|
+
}
|
|
3219
|
+
/**
|
|
3220
|
+
* Returns every entity that currently holds a relation of the given base
|
|
3221
|
+
* component pointing at `targetId`.
|
|
3222
|
+
*
|
|
3223
|
+
* This is the efficient **reverse** lookup. For common hierarchy cases,
|
|
3224
|
+
* prefer the higher-level `world.getChildren(parent, ChildOf)` instead.
|
|
3225
|
+
*
|
|
3226
|
+
* @example
|
|
3227
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
3228
|
+
* const directChildren = world.getRelationSources(ship, ChildOf);
|
|
3229
|
+
*/
|
|
3230
|
+
getRelationSources(targetId, relationComp) {
|
|
3231
|
+
const refs = getEntityReferences(this.entityReferences, targetId);
|
|
3232
|
+
const result = [];
|
|
3233
|
+
for (const [source, relType] of refs) {
|
|
3234
|
+
if (!this.entityToArchetype.has(source) && !this.componentEntities.exists(source)) continue;
|
|
3235
|
+
if (getComponentIdFromRelationId(relType) === relationComp) result.push(source);
|
|
3236
|
+
}
|
|
3237
|
+
return result;
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Returns true if the entity has any (or a specific-target) relation of the
|
|
3241
|
+
* given base component.
|
|
3242
|
+
*/
|
|
3243
|
+
hasRelation(entityId, relationComp, targetId) {
|
|
3244
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
3245
|
+
if (targetId !== void 0) {
|
|
3246
|
+
const specific = relation(relationComp, targetId);
|
|
3247
|
+
return this.has(entityId, specific);
|
|
3248
|
+
}
|
|
3249
|
+
return this.getRelationTargets(entityId, relationComp).length > 0;
|
|
3250
|
+
}
|
|
3251
|
+
/**
|
|
3252
|
+
* Returns the number of relations of the given base component held by the entity.
|
|
3253
|
+
*/
|
|
3254
|
+
countRelations(entityId, relationComp) {
|
|
3255
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
3256
|
+
return this.getRelationTargets(entityId, relationComp).length;
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* For an *exclusive* relation (e.g. ChildOf, Owner), returns the single
|
|
3260
|
+
* target entity (or undefined if none).
|
|
3261
|
+
*
|
|
3262
|
+
* When the component was declared `exclusive: true`, this is the preferred
|
|
3263
|
+
* accessor (clearer intent than array destructuring).
|
|
3264
|
+
*/
|
|
3265
|
+
getSingleRelationTarget(entityId, relationComp) {
|
|
3266
|
+
const targets = this.getRelationTargets(entityId, relationComp);
|
|
3267
|
+
return targets.length > 0 ? targets[0][0] : void 0;
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Returns the direct children of `parent` for the given relationship component
|
|
3271
|
+
* (typically a `ChildOf` or similar exclusive `sparse` relation).
|
|
3272
|
+
*
|
|
3273
|
+
* This is the recommended high-level API for hierarchy traversal.
|
|
3274
|
+
* It uses the internal reverse reference index for efficiency.
|
|
3275
|
+
*
|
|
3276
|
+
* @example
|
|
3277
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
3278
|
+
* const kids = world.getChildren(ship, ChildOf);
|
|
3279
|
+
*/
|
|
3280
|
+
getChildren(parent, childOf) {
|
|
3281
|
+
return this.getRelationSources(parent, childOf);
|
|
3282
|
+
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Returns the parent of `child` for the given relationship component
|
|
3285
|
+
* (typically an exclusive `ChildOf` relation).
|
|
3286
|
+
*
|
|
3287
|
+
* @example
|
|
3288
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
3289
|
+
* const parent = world.getParent(turret, ChildOf);
|
|
3290
|
+
*/
|
|
3291
|
+
getParent(child, childOf) {
|
|
3292
|
+
return this.getSingleRelationTarget(child, childOf);
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* Returns the ancestor chain from the immediate parent up to (but not
|
|
3296
|
+
* including) the root for the given relationship component.
|
|
3297
|
+
*
|
|
3298
|
+
* @example
|
|
3299
|
+
* const ChildOf = component({ exclusive: true, sparse: true });
|
|
3300
|
+
* const ancestors = world.getAncestors(muzzle, ChildOf); // [turret, ship]
|
|
3301
|
+
*/
|
|
3302
|
+
getAncestors(entity, childOf) {
|
|
3303
|
+
const ancestors = [];
|
|
3304
|
+
let cur = this.getParent(entity, childOf);
|
|
3305
|
+
while (cur !== void 0) {
|
|
3306
|
+
ancestors.push(cur);
|
|
3307
|
+
cur = this.getParent(cur, childOf);
|
|
3308
|
+
}
|
|
3309
|
+
return ancestors;
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Iteratively traverses all descendants of `root` in DFS pre-order.
|
|
3313
|
+
* This is a generator and is safe for very deep hierarchies.
|
|
3314
|
+
*
|
|
3315
|
+
* @example
|
|
3316
|
+
* for (const { entity, depth, parent } of world.iterateDescendants(root, ChildOf)) {
|
|
3317
|
+
* console.log(depth, entity);
|
|
3318
|
+
* }
|
|
3319
|
+
*/
|
|
3320
|
+
*iterateDescendants(root, childOf, opts = {}) {
|
|
3321
|
+
const { includeSelf = false, maxDepth } = opts;
|
|
3322
|
+
const stack = [];
|
|
3323
|
+
if (includeSelf) stack.push({
|
|
3324
|
+
entity: root,
|
|
3325
|
+
depth: 0,
|
|
3326
|
+
parent: null
|
|
3327
|
+
});
|
|
3328
|
+
else for (const child of this.getChildren(root, childOf)) stack.push({
|
|
3329
|
+
entity: child,
|
|
3330
|
+
depth: 1,
|
|
3331
|
+
parent: root
|
|
3332
|
+
});
|
|
3333
|
+
while (stack.length > 0) {
|
|
3334
|
+
const current = stack.pop();
|
|
3335
|
+
if (maxDepth !== void 0 && current.depth > maxDepth) continue;
|
|
3336
|
+
yield current;
|
|
3337
|
+
const kids = this.getChildren(current.entity, childOf);
|
|
3338
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
3339
|
+
const k = kids[i];
|
|
3340
|
+
stack.push({
|
|
3341
|
+
entity: k,
|
|
3342
|
+
depth: current.depth + 1,
|
|
3343
|
+
parent: current.entity
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Callback-based descendant traversal (hot path friendly).
|
|
3350
|
+
* Return `false` from the visitor to stop early.
|
|
3351
|
+
*/
|
|
3352
|
+
traverseDescendants(root, childOf, visitor, opts = {}) {
|
|
3353
|
+
for (const { entity, depth, parent } of this.iterateDescendants(root, childOf, opts)) if (visitor(entity, depth, parent) === false) return;
|
|
3354
|
+
}
|
|
2719
3355
|
hook(componentTypes, hook, filter) {
|
|
2720
3356
|
const isCallback = typeof hook === "function";
|
|
2721
3357
|
const callback = isCallback ? hook : void 0;
|
|
@@ -2751,6 +3387,21 @@ var World = class {
|
|
|
2751
3387
|
};
|
|
2752
3388
|
}
|
|
2753
3389
|
/**
|
|
3390
|
+
* Creates a debug stats collector that will receive a `SyncDebugStats` payload
|
|
3391
|
+
* after every subsequent `sync()`.
|
|
3392
|
+
*
|
|
3393
|
+
* The returned object is a pure lifecycle handle. It does not store data.
|
|
3394
|
+
* Collection stops when you call `[Symbol.dispose]()` (or use a `using` declaration).
|
|
3395
|
+
*
|
|
3396
|
+
* All active collectors receive the exact same stats object for a given sync.
|
|
3397
|
+
* Exceptions thrown by callbacks are ignored.
|
|
3398
|
+
*
|
|
3399
|
+
* This is intended for development/debugging and leak detection.
|
|
3400
|
+
*/
|
|
3401
|
+
createDebugStatsCollector(callback) {
|
|
3402
|
+
return this.debugStats.createCollector(callback);
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
2754
3405
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
2755
3406
|
* This method must be called after making changes via `set()`, `remove()`, or `delete()` for them to take effect.
|
|
2756
3407
|
* Typically called once per frame at the end of your game loop.
|
|
@@ -2761,7 +3412,40 @@ var World = class {
|
|
|
2761
3412
|
* world.sync(); // Apply all buffered changes
|
|
2762
3413
|
*/
|
|
2763
3414
|
sync() {
|
|
2764
|
-
this.
|
|
3415
|
+
if (!this.debugStats.hasActiveCollectors()) {
|
|
3416
|
+
this.commandBuffer.execute();
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
const syncStart = performance.now();
|
|
3420
|
+
this.debugStats.resetActivity();
|
|
3421
|
+
const commandBufferStart = performance.now();
|
|
3422
|
+
const commandIterations = this.commandBuffer.execute();
|
|
3423
|
+
const commandBufferEnd = performance.now();
|
|
3424
|
+
const syncEnd = performance.now();
|
|
3425
|
+
const entityCount = this.entityToArchetype.size;
|
|
3426
|
+
let emptyArchetypes = 0;
|
|
3427
|
+
for (const arch of this.archetypes) if (arch.size === 0) emptyArchetypes++;
|
|
3428
|
+
let archetypesByComponentSize = 0;
|
|
3429
|
+
for (const set of this.archetypesByComponent.values()) archetypesByComponentSize += set.size;
|
|
3430
|
+
this.debugStats.deliver({
|
|
3431
|
+
syncStart,
|
|
3432
|
+
syncEnd,
|
|
3433
|
+
commandBufferStart,
|
|
3434
|
+
commandBufferEnd,
|
|
3435
|
+
commandIterations
|
|
3436
|
+
}, {
|
|
3437
|
+
entityCount,
|
|
3438
|
+
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
3439
|
+
nextId: this.entityIdManager.getNextId(),
|
|
3440
|
+
archetypeCount: this.archetypes.length,
|
|
3441
|
+
emptyArchetypes,
|
|
3442
|
+
archetypesByComponentSize,
|
|
3443
|
+
cachedQueryCount: this.queryRegistry.cache?.size ?? 0,
|
|
3444
|
+
registeredQueryCount: this.queryRegistry.queries?.size ?? 0,
|
|
3445
|
+
hookCount: this.hooks.size,
|
|
3446
|
+
entityReferencesSize: this.entityReferences.size,
|
|
3447
|
+
entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size
|
|
3448
|
+
});
|
|
2765
3449
|
}
|
|
2766
3450
|
/**
|
|
2767
3451
|
* Creates a cached query for efficiently iterating entities with specific components.
|
|
@@ -2797,7 +3481,7 @@ var World = class {
|
|
|
2797
3481
|
createQuery(componentTypes, filter = {}) {
|
|
2798
3482
|
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
2799
3483
|
const filterKey = serializeQueryFilter(filter);
|
|
2800
|
-
const key = `${
|
|
3484
|
+
const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
|
|
2801
3485
|
return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
|
|
2802
3486
|
}
|
|
2803
3487
|
/**
|
|
@@ -2863,44 +3547,7 @@ var World = class {
|
|
|
2863
3547
|
* @internal
|
|
2864
3548
|
*/
|
|
2865
3549
|
getMatchingArchetypes(componentTypes) {
|
|
2866
|
-
|
|
2867
|
-
const regularComponents = [];
|
|
2868
|
-
const wildcardRelations = [];
|
|
2869
|
-
for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
|
|
2870
|
-
const componentId = getComponentIdFromRelationId(componentType);
|
|
2871
|
-
if (componentId !== void 0) wildcardRelations.push({
|
|
2872
|
-
componentId,
|
|
2873
|
-
relationId: componentType
|
|
2874
|
-
});
|
|
2875
|
-
} else regularComponents.push(componentType);
|
|
2876
|
-
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
2877
|
-
for (const { componentId, relationId } of wildcardRelations) {
|
|
2878
|
-
const markerSet = this.archetypesByComponent.get(relationId);
|
|
2879
|
-
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
2880
|
-
matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
2881
|
-
}
|
|
2882
|
-
return matchingArchetypes;
|
|
2883
|
-
}
|
|
2884
|
-
getArchetypesWithComponents(componentTypes) {
|
|
2885
|
-
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2886
|
-
if (componentTypes.length === 1) {
|
|
2887
|
-
const set = this.archetypesByComponent.get(componentTypes[0]);
|
|
2888
|
-
return set ? Array.from(set) : [];
|
|
2889
|
-
}
|
|
2890
|
-
const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
|
|
2891
|
-
if (sets.length === 0) return [];
|
|
2892
|
-
if (sets.length < componentTypes.length) return [];
|
|
2893
|
-
const smallest = sets[0];
|
|
2894
|
-
if (sets.length === 2) {
|
|
2895
|
-
const other = sets[1];
|
|
2896
|
-
return Array.from(smallest).filter((a) => other.has(a));
|
|
2897
|
-
}
|
|
2898
|
-
let result = new Set(smallest);
|
|
2899
|
-
for (let i = 1; i < sets.length; i++) {
|
|
2900
|
-
for (const item of result) if (!sets[i].has(item)) result.delete(item);
|
|
2901
|
-
if (result.size === 0) return [];
|
|
2902
|
-
}
|
|
2903
|
-
return Array.from(result);
|
|
3550
|
+
return this.archetypeManager.getMatchingArchetypes(componentTypes);
|
|
2904
3551
|
}
|
|
2905
3552
|
query(componentTypes, includeComponents) {
|
|
2906
3553
|
const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
|
|
@@ -2914,150 +3561,14 @@ var World = class {
|
|
|
2914
3561
|
return result;
|
|
2915
3562
|
}
|
|
2916
3563
|
}
|
|
2917
|
-
executeEntityCommands(entityId, commands) {
|
|
2918
|
-
this._changeset.clear();
|
|
2919
|
-
if (this.componentEntities.exists(entityId)) {
|
|
2920
|
-
this.componentEntities.executeCommands(entityId, commands);
|
|
2921
|
-
return;
|
|
2922
|
-
}
|
|
2923
|
-
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
2924
|
-
this.destroyEntityImmediate(entityId);
|
|
2925
|
-
return;
|
|
2926
|
-
}
|
|
2927
|
-
this.applyEntityCommands(entityId, commands);
|
|
2928
|
-
}
|
|
2929
|
-
applyEntityCommands(entityId, commands) {
|
|
2930
|
-
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
2931
|
-
if (!currentArchetype) return;
|
|
2932
|
-
const changeset = this._changeset;
|
|
2933
|
-
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
2934
|
-
if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
|
|
2935
|
-
});
|
|
2936
|
-
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
2937
|
-
if (this.hooks.size === 0) {
|
|
2938
|
-
applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
|
|
2939
|
-
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2940
|
-
return;
|
|
2941
|
-
}
|
|
2942
|
-
const removedComponents = /* @__PURE__ */ new Map();
|
|
2943
|
-
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, removedComponents);
|
|
2944
|
-
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2945
|
-
triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
|
|
2946
|
-
}
|
|
2947
|
-
createHooksContext() {
|
|
2948
|
-
return this._hooksCtx;
|
|
2949
|
-
}
|
|
2950
|
-
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
2951
|
-
const sourceArchetype = this.entityToArchetype.get(entityId);
|
|
2952
|
-
if (!sourceArchetype) return;
|
|
2953
|
-
const changeset = this._removeChangeset;
|
|
2954
|
-
changeset.clear();
|
|
2955
|
-
changeset.delete(componentType);
|
|
2956
|
-
maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
|
|
2957
|
-
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
2958
|
-
const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype, null);
|
|
2959
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
|
|
2960
|
-
triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
|
|
2961
|
-
}
|
|
2962
|
-
updateEntityReferences(entityId, changeset) {
|
|
2963
|
-
for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
|
|
2964
|
-
const targetId = getTargetIdFromRelationId(componentType);
|
|
2965
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
2966
|
-
} else if (componentType >= 1024) untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
2967
|
-
for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
|
|
2968
|
-
const targetId = getTargetIdFromRelationId(componentType);
|
|
2969
|
-
trackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
2970
|
-
} else if (componentType >= 1024) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
2971
|
-
}
|
|
2972
3564
|
ensureArchetype(componentTypes) {
|
|
2973
|
-
|
|
2974
|
-
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
2975
|
-
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
2976
|
-
}
|
|
2977
|
-
/** Add componentType to the reverse index if it contains an entity ID */
|
|
2978
|
-
addToReferencingIndex(componentType, archetype) {
|
|
2979
|
-
const detailedType = getDetailedIdType(componentType);
|
|
2980
|
-
let entityId;
|
|
2981
|
-
if (detailedType.type === "entity") entityId = componentType;
|
|
2982
|
-
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
2983
|
-
if (entityId !== void 0) {
|
|
2984
|
-
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2985
|
-
if (!refs) {
|
|
2986
|
-
refs = /* @__PURE__ */ new Set();
|
|
2987
|
-
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
2988
|
-
}
|
|
2989
|
-
refs.add(archetype);
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
/** Remove componentType from the reverse index */
|
|
2993
|
-
removeFromReferencingIndex(componentType, archetype) {
|
|
2994
|
-
const detailedType = getDetailedIdType(componentType);
|
|
2995
|
-
let entityId;
|
|
2996
|
-
if (detailedType.type === "entity") entityId = componentType;
|
|
2997
|
-
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
2998
|
-
if (entityId !== void 0) {
|
|
2999
|
-
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
3000
|
-
if (refs) {
|
|
3001
|
-
refs.delete(archetype);
|
|
3002
|
-
if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
|
|
3003
|
-
}
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
3006
|
-
createNewArchetype(componentTypes) {
|
|
3007
|
-
const newArchetype = new Archetype(componentTypes, this.dontFragmentStore);
|
|
3008
|
-
this.archetypes.push(newArchetype);
|
|
3009
|
-
for (const componentType of componentTypes) {
|
|
3010
|
-
let archetypes = this.archetypesByComponent.get(componentType);
|
|
3011
|
-
if (!archetypes) {
|
|
3012
|
-
archetypes = /* @__PURE__ */ new Set();
|
|
3013
|
-
this.archetypesByComponent.set(componentType, archetypes);
|
|
3014
|
-
}
|
|
3015
|
-
archetypes.add(newArchetype);
|
|
3016
|
-
this.addToReferencingIndex(componentType, newArchetype);
|
|
3017
|
-
}
|
|
3018
|
-
this.queryRegistry.onNewArchetype(newArchetype);
|
|
3019
|
-
this.updateArchetypeHookMatches(newArchetype);
|
|
3020
|
-
return newArchetype;
|
|
3021
|
-
}
|
|
3022
|
-
updateArchetypeHookMatches(archetype) {
|
|
3023
|
-
for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
|
|
3024
|
-
archetype.matchingMultiHooks.add(entry);
|
|
3025
|
-
if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
archetypeMatchesHook(archetype, entry) {
|
|
3029
|
-
return entry.requiredComponents.every((c) => {
|
|
3030
|
-
if (isWildcardRelationId(c)) {
|
|
3031
|
-
if (isDontFragmentWildcard(c)) return true;
|
|
3032
|
-
const componentId = getComponentIdFromRelationId(c);
|
|
3033
|
-
return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
|
|
3034
|
-
}
|
|
3035
|
-
return archetype.componentTypeSet.has(c) || isDontFragmentRelation(c);
|
|
3036
|
-
}) && matchesFilter(archetype, entry.filter);
|
|
3565
|
+
return this.archetypeManager.ensureArchetype(componentTypes);
|
|
3037
3566
|
}
|
|
3038
3567
|
cleanupArchetypesReferencingEntity(entityId) {
|
|
3039
|
-
|
|
3040
|
-
if (!refs) return;
|
|
3041
|
-
for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
|
|
3042
|
-
this.entityToReferencingArchetypes.delete(entityId);
|
|
3568
|
+
this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
|
|
3043
3569
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
if (index !== -1) {
|
|
3047
|
-
const last = this.archetypes[this.archetypes.length - 1];
|
|
3048
|
-
this.archetypes[index] = last;
|
|
3049
|
-
this.archetypes.pop();
|
|
3050
|
-
}
|
|
3051
|
-
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
3052
|
-
for (const componentType of archetype.componentTypes) {
|
|
3053
|
-
const archetypes = this.archetypesByComponent.get(componentType);
|
|
3054
|
-
if (archetypes) {
|
|
3055
|
-
archetypes.delete(archetype);
|
|
3056
|
-
if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
|
|
3057
|
-
}
|
|
3058
|
-
this.removeFromReferencingIndex(componentType, archetype);
|
|
3059
|
-
}
|
|
3060
|
-
this.queryRegistry.onArchetypeRemoved(archetype);
|
|
3570
|
+
archetypeMatchesHook(archetype, entry) {
|
|
3571
|
+
return this.archetypeManager.archetypeMatchesHook(archetype, entry);
|
|
3061
3572
|
}
|
|
3062
3573
|
/**
|
|
3063
3574
|
* Serializes the entire world state to a plain JavaScript object.
|
|
@@ -3080,11 +3591,22 @@ var World = class {
|
|
|
3080
3591
|
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
3081
3592
|
* const newWorld = new World(savedData);
|
|
3082
3593
|
*/
|
|
3594
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
3595
|
+
this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
|
|
3596
|
+
}
|
|
3597
|
+
createHooksContext() {
|
|
3598
|
+
return this.commandExecutor.getHooksContext?.() ?? {
|
|
3599
|
+
multiHooks: this.hooks,
|
|
3600
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
3601
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
3602
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct)
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3083
3605
|
serialize() {
|
|
3084
|
-
return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
|
|
3606
|
+
return serializeWorld(this.archetypeManager.archetypes, this.componentEntities, this.entityIdManager);
|
|
3085
3607
|
}
|
|
3086
3608
|
};
|
|
3087
3609
|
//#endregion
|
|
3088
|
-
export {
|
|
3610
|
+
export { component as a, isSparseComponent as c, decodeRelationId as d, isWildcardRelationId as f, isRelationId as g, isEntityId as h, EntityBuilder as i, isSparseRelation as l, isComponentId as m, Query as n, getComponentIdByName as o, relation as p, SingletonHandle as r, getComponentNameById as s, World as t, isSparseWildcard as u };
|
|
3089
3611
|
|
|
3090
3612
|
//# sourceMappingURL=world.mjs.map
|