@codehz/ecs 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/builder.d.mts +68 -44
- 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 +1150 -888
- package/dist/world.mjs.map +1 -1
- package/examples/spatial-grid.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/component/singleton.test.ts +121 -34
- package/src/__tests__/serialization/bounds.test.ts +2 -3
- package/src/__tests__/world/component-management.test.ts +6 -5
- package/src/index.ts +2 -0
- package/src/world/archetype-manager.ts +283 -0
- package/src/world/command-executor.ts +258 -0
- package/src/world/debug-stats.ts +147 -0
- package/src/world/operations.ts +100 -0
- package/src/world/singleton.ts +52 -0
- package/src/world/world.ts +194 -573
package/dist/world.mjs
CHANGED
|
@@ -622,124 +622,327 @@ var EntityBuilder = class {
|
|
|
622
622
|
}
|
|
623
623
|
};
|
|
624
624
|
//#endregion
|
|
625
|
-
//#region src/
|
|
626
|
-
/**
|
|
627
|
-
* Normalize component type collections into a stable ascending order.
|
|
628
|
-
* This keeps cache keys and archetype signatures deterministic.
|
|
629
|
-
*/
|
|
630
|
-
function normalizeComponentTypes(componentTypes) {
|
|
631
|
-
return [...componentTypes].sort((a, b) => a - b);
|
|
632
|
-
}
|
|
633
|
-
//#endregion
|
|
634
|
-
//#region src/types/index.ts
|
|
635
|
-
function isOptionalEntityId(type) {
|
|
636
|
-
return typeof type === "object" && type !== null && "optional" in type;
|
|
637
|
-
}
|
|
638
|
-
//#endregion
|
|
639
|
-
//#region src/utils/utils.ts
|
|
625
|
+
//#region src/world/singleton.ts
|
|
640
626
|
/**
|
|
641
|
-
*
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
* @
|
|
648
|
-
*
|
|
627
|
+
* Explicit handle for a singleton component (component-as-entity).
|
|
628
|
+
*
|
|
629
|
+
* This is the preferred API for singleton components.
|
|
630
|
+
* `world.set(componentId, value)` remains available only as a deprecated
|
|
631
|
+
* compatibility shorthand.
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* const config = world.singleton(Config);
|
|
635
|
+
* config.set({ debug: true });
|
|
636
|
+
* world.sync();
|
|
637
|
+
* console.log(config.get());
|
|
649
638
|
*/
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
639
|
+
var SingletonHandle = class {
|
|
640
|
+
componentId;
|
|
641
|
+
ops;
|
|
642
|
+
constructor(componentId, ops) {
|
|
643
|
+
this.componentId = componentId;
|
|
644
|
+
this.ops = ops;
|
|
655
645
|
}
|
|
656
|
-
|
|
657
|
-
|
|
646
|
+
has() {
|
|
647
|
+
return this.ops.has();
|
|
648
|
+
}
|
|
649
|
+
get() {
|
|
650
|
+
return this.ops.get();
|
|
651
|
+
}
|
|
652
|
+
getOptional() {
|
|
653
|
+
return this.ops.getOptional();
|
|
654
|
+
}
|
|
655
|
+
remove() {
|
|
656
|
+
this.ops.remove();
|
|
657
|
+
}
|
|
658
|
+
set(...args) {
|
|
659
|
+
this.ops.set(args[0]);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
658
662
|
//#endregion
|
|
659
|
-
//#region src/archetype/
|
|
663
|
+
//#region src/archetype/store.ts
|
|
660
664
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
+
* Production implementation of SparseStore.
|
|
666
|
+
*
|
|
667
|
+
* Internal layout (optimized):
|
|
668
|
+
* - byComponent: baseComponentId → (entityId → RelationEntry)
|
|
669
|
+
* RelationEntry uses a single-value form for the common exclusive case (1 target),
|
|
670
|
+
* avoiding Map allocation for the vast majority of usage.
|
|
671
|
+
* - entityIndex: entityId → Set<baseComponentId>
|
|
672
|
+
* Lightweight reverse index.
|
|
665
673
|
*/
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
674
|
+
var SparseStoreImpl = class {
|
|
675
|
+
/**
|
|
676
|
+
* Primary storage, keyed by the base relation component ID.
|
|
677
|
+
*/
|
|
678
|
+
byComponent = /* @__PURE__ */ new Map();
|
|
679
|
+
/**
|
|
680
|
+
* Reverse index: which base component kinds an entity participates in.
|
|
681
|
+
* Only required to support getAllForEntity and deleteEntity efficiently.
|
|
682
|
+
* The primary storage (byComponent) is deliberately not optimized for these operations.
|
|
683
|
+
*/
|
|
684
|
+
entityIndex = /* @__PURE__ */ new Map();
|
|
685
|
+
getValue(entityId, relationType) {
|
|
686
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
687
|
+
if (componentId === void 0) return void 0;
|
|
688
|
+
const entities = this.byComponent.get(componentId);
|
|
689
|
+
if (!entities) return void 0;
|
|
690
|
+
const entry = entities.get(entityId);
|
|
691
|
+
if (!entry) return void 0;
|
|
692
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
693
|
+
if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
|
|
694
|
+
else {
|
|
695
|
+
const item = entry.targets.get(targetId);
|
|
696
|
+
return item ? item.data : void 0;
|
|
697
|
+
}
|
|
669
698
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
699
|
+
setValue(entityId, relationType, data) {
|
|
700
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
701
|
+
if (componentId === void 0) throw new Error("setValue called with a non-relation type on SparseStore");
|
|
702
|
+
let entities = this.byComponent.get(componentId);
|
|
703
|
+
if (!entities) {
|
|
704
|
+
entities = /* @__PURE__ */ new Map();
|
|
705
|
+
this.byComponent.set(componentId, entities);
|
|
706
|
+
}
|
|
707
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
708
|
+
let entry = entities.get(entityId);
|
|
709
|
+
if (!entry) {
|
|
710
|
+
entry = {
|
|
711
|
+
type: "single",
|
|
712
|
+
relationType,
|
|
713
|
+
target: targetId,
|
|
714
|
+
data
|
|
715
|
+
};
|
|
716
|
+
entities.set(entityId, entry);
|
|
717
|
+
} else if (entry.type === "single") if (entry.target === targetId) {
|
|
718
|
+
entry.data = data;
|
|
719
|
+
entry.relationType = relationType;
|
|
720
|
+
} else {
|
|
721
|
+
const targets = /* @__PURE__ */ new Map();
|
|
722
|
+
targets.set(entry.target, {
|
|
723
|
+
relationType: entry.relationType,
|
|
724
|
+
data: entry.data
|
|
725
|
+
});
|
|
726
|
+
targets.set(targetId, {
|
|
727
|
+
relationType,
|
|
728
|
+
data
|
|
729
|
+
});
|
|
730
|
+
entities.set(entityId, {
|
|
731
|
+
type: "multi",
|
|
732
|
+
targets
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
else entry.targets.set(targetId, {
|
|
736
|
+
relationType,
|
|
737
|
+
data
|
|
738
|
+
});
|
|
739
|
+
let components = this.entityIndex.get(entityId);
|
|
740
|
+
if (!components) {
|
|
741
|
+
components = /* @__PURE__ */ new Set();
|
|
742
|
+
this.entityIndex.set(entityId, components);
|
|
743
|
+
}
|
|
744
|
+
components.add(componentId);
|
|
745
|
+
}
|
|
746
|
+
deleteValue(entityId, relationType) {
|
|
747
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
748
|
+
if (componentId === void 0) return false;
|
|
749
|
+
const entities = this.byComponent.get(componentId);
|
|
750
|
+
if (!entities) return false;
|
|
751
|
+
const entry = entities.get(entityId);
|
|
752
|
+
if (!entry) return false;
|
|
753
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
754
|
+
let existed = false;
|
|
755
|
+
if (entry.type === "single") {
|
|
756
|
+
if (entry.target === targetId) {
|
|
757
|
+
existed = true;
|
|
758
|
+
entities.delete(entityId);
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
existed = entry.targets.delete(targetId);
|
|
762
|
+
if (entry.targets.size === 0) entities.delete(entityId);
|
|
763
|
+
else if (entry.targets.size === 1) {
|
|
764
|
+
const [first] = entry.targets.entries();
|
|
765
|
+
const [t, item] = first;
|
|
766
|
+
entities.set(entityId, {
|
|
767
|
+
type: "single",
|
|
768
|
+
relationType: item.relationType,
|
|
769
|
+
target: t,
|
|
770
|
+
data: item.data
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
if (!entities.has(entityId) && entities.size === 0) this.byComponent.delete(componentId);
|
|
775
|
+
const components = this.entityIndex.get(entityId);
|
|
776
|
+
if (components && !entities.has(entityId)) {
|
|
777
|
+
components.delete(componentId);
|
|
778
|
+
if (components.size === 0) this.entityIndex.delete(entityId);
|
|
779
|
+
}
|
|
780
|
+
return existed;
|
|
781
|
+
}
|
|
782
|
+
hasAnyForComponent(componentId) {
|
|
783
|
+
const entities = this.byComponent.get(componentId);
|
|
784
|
+
return entities !== void 0 && entities.size > 0;
|
|
785
|
+
}
|
|
786
|
+
getRelationsForComponent(entityId, componentId) {
|
|
787
|
+
const result = [];
|
|
788
|
+
const entities = this.byComponent.get(componentId);
|
|
789
|
+
if (!entities) return result;
|
|
790
|
+
const entry = entities.get(entityId);
|
|
791
|
+
if (!entry) return result;
|
|
792
|
+
if (entry.type === "single") result.push([entry.target, entry.data]);
|
|
793
|
+
else for (const [target, item] of entry.targets) result.push([target, item.data]);
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
getAllForEntity(entityId) {
|
|
797
|
+
const components = this.entityIndex.get(entityId);
|
|
798
|
+
if (!components || components.size === 0) return [];
|
|
799
|
+
const result = [];
|
|
800
|
+
for (const componentId of components) {
|
|
801
|
+
const entry = this.byComponent.get(componentId)?.get(entityId);
|
|
802
|
+
if (entry) if (entry.type === "single") result.push([entry.relationType, entry.data]);
|
|
803
|
+
else for (const item of entry.targets.values()) result.push([item.relationType, item.data]);
|
|
804
|
+
}
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
deleteEntity(entityId) {
|
|
808
|
+
const components = this.entityIndex.get(entityId);
|
|
809
|
+
if (!components) return;
|
|
810
|
+
for (const componentId of components) {
|
|
811
|
+
const entities = this.byComponent.get(componentId);
|
|
812
|
+
if (entities) {
|
|
813
|
+
entities.delete(entityId);
|
|
814
|
+
if (entities.size === 0) this.byComponent.delete(componentId);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
this.entityIndex.delete(entityId);
|
|
818
|
+
}
|
|
819
|
+
getAllForEntities(entityIds) {
|
|
820
|
+
const result = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const eid of entityIds) {
|
|
822
|
+
const data = this.getAllForEntity(eid);
|
|
823
|
+
if (data.length > 0) result.set(eid, data);
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/commands/buffer.ts
|
|
691
830
|
/**
|
|
692
|
-
*
|
|
831
|
+
* Maximum number of command buffer execution iterations to prevent infinite loops
|
|
693
832
|
*/
|
|
694
|
-
|
|
695
|
-
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
696
|
-
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
697
|
-
}
|
|
833
|
+
const MAX_COMMAND_ITERATIONS = 100;
|
|
698
834
|
/**
|
|
699
|
-
*
|
|
700
|
-
* Receives the SparseStore directly for efficient per-component lookups.
|
|
835
|
+
* Command buffer for deferred structural changes
|
|
701
836
|
*/
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
837
|
+
var CommandBuffer = class {
|
|
838
|
+
commands = [];
|
|
839
|
+
swapBuffer = [];
|
|
840
|
+
/** Reusable map to group commands by entity, avoids per-sync allocations */
|
|
841
|
+
entityCommands = /* @__PURE__ */ new Map();
|
|
842
|
+
executeEntityCommands;
|
|
843
|
+
/**
|
|
844
|
+
* Create a command buffer with an executor function
|
|
845
|
+
*/
|
|
846
|
+
constructor(executeEntityCommands) {
|
|
847
|
+
this.executeEntityCommands = executeEntityCommands;
|
|
709
848
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
849
|
+
set(entityId, componentType, component) {
|
|
850
|
+
this.commands.push({
|
|
851
|
+
type: "set",
|
|
852
|
+
entityId,
|
|
853
|
+
componentType,
|
|
854
|
+
component
|
|
855
|
+
});
|
|
713
856
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
857
|
+
/**
|
|
858
|
+
* Remove a component from an entity (deferred)
|
|
859
|
+
*/
|
|
860
|
+
remove(entityId, componentType) {
|
|
861
|
+
this.commands.push({
|
|
862
|
+
type: "delete",
|
|
863
|
+
entityId,
|
|
864
|
+
componentType
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Destroy an entity (deferred)
|
|
869
|
+
*/
|
|
870
|
+
delete(entityId) {
|
|
871
|
+
this.commands.push({
|
|
872
|
+
type: "destroy",
|
|
873
|
+
entityId
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Execute all commands and clear the buffer.
|
|
878
|
+
* Returns the number of iterations performed (for debug stats).
|
|
879
|
+
*/
|
|
880
|
+
execute() {
|
|
881
|
+
let iterations = 0;
|
|
882
|
+
while (this.commands.length > 0) {
|
|
883
|
+
if (iterations >= MAX_COMMAND_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
884
|
+
iterations++;
|
|
885
|
+
const currentCommands = this.commands;
|
|
886
|
+
this.commands = this.swapBuffer;
|
|
887
|
+
const entityCommands = this.entityCommands;
|
|
888
|
+
for (const cmd of currentCommands) {
|
|
889
|
+
const existing = entityCommands.get(cmd.entityId);
|
|
890
|
+
if (existing !== void 0) existing.push(cmd);
|
|
891
|
+
else entityCommands.set(cmd.entityId, [cmd]);
|
|
892
|
+
}
|
|
893
|
+
currentCommands.length = 0;
|
|
894
|
+
this.swapBuffer = currentCommands;
|
|
895
|
+
for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
|
|
896
|
+
entityCommands.clear();
|
|
718
897
|
}
|
|
719
|
-
return;
|
|
898
|
+
return iterations;
|
|
720
899
|
}
|
|
721
|
-
|
|
900
|
+
/**
|
|
901
|
+
* Get current commands (for testing)
|
|
902
|
+
*/
|
|
903
|
+
getCommands() {
|
|
904
|
+
return [...this.commands];
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Clear all commands
|
|
908
|
+
*/
|
|
909
|
+
clear() {
|
|
910
|
+
this.commands = [];
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/types/index.ts
|
|
915
|
+
function isOptionalEntityId(type) {
|
|
916
|
+
return typeof type === "object" && type !== null && "optional" in type;
|
|
917
|
+
}
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/component/type-utils.ts
|
|
920
|
+
/**
|
|
921
|
+
* Normalize component type collections into a stable ascending order.
|
|
922
|
+
* This keeps cache keys and archetype signatures deterministic.
|
|
923
|
+
*/
|
|
924
|
+
function normalizeComponentTypes(componentTypes) {
|
|
925
|
+
return [...componentTypes].sort((a, b) => a - b);
|
|
722
926
|
}
|
|
927
|
+
//#endregion
|
|
928
|
+
//#region src/utils/utils.ts
|
|
723
929
|
/**
|
|
724
|
-
*
|
|
930
|
+
* Utility functions for ECS library
|
|
725
931
|
*/
|
|
726
|
-
function buildRegularComponentValue(dataSource, entityIndex, optional) {
|
|
727
|
-
if (dataSource === void 0) {
|
|
728
|
-
if (optional) return void 0;
|
|
729
|
-
throw new Error(`Component data not found for mandatory component type`);
|
|
730
|
-
}
|
|
731
|
-
const data = dataSource[entityIndex];
|
|
732
|
-
const result = data === MISSING_COMPONENT ? void 0 : data;
|
|
733
|
-
return optional ? { value: result } : result;
|
|
734
|
-
}
|
|
735
932
|
/**
|
|
736
|
-
*
|
|
933
|
+
* Get a value from cache or compute and cache it if not present
|
|
934
|
+
* @param cache The cache map
|
|
935
|
+
* @param key The cache key
|
|
936
|
+
* @param compute Function to compute the value if not cached (may have side effects)
|
|
937
|
+
* @returns The cached or computed value
|
|
737
938
|
*/
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
939
|
+
function getOrCompute(cache, key, compute) {
|
|
940
|
+
let value = cache.get(key);
|
|
941
|
+
if (value === void 0) {
|
|
942
|
+
value = compute();
|
|
943
|
+
cache.set(key, value);
|
|
944
|
+
}
|
|
945
|
+
return value;
|
|
743
946
|
}
|
|
744
947
|
//#endregion
|
|
745
948
|
//#region src/archetype/archetype.ts
|
|
@@ -957,424 +1160,202 @@ var Archetype = class {
|
|
|
957
1160
|
if (this.componentTypeSet.has(componentType)) {
|
|
958
1161
|
const data = this.getComponentData(componentType)[index];
|
|
959
1162
|
if (data === MISSING_COMPONENT) throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
960
|
-
return data;
|
|
961
|
-
}
|
|
962
|
-
if (this.sparseRelations.getValue(entityId, componentType) !== void 0 || this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.sparseRelations.getValue(entityId, componentType);
|
|
963
|
-
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
964
|
-
}
|
|
965
|
-
getOptional(entityId, componentType) {
|
|
966
|
-
const index = this.entityToIndex.get(entityId);
|
|
967
|
-
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
968
|
-
if (this.componentTypeSet.has(componentType)) {
|
|
969
|
-
const data = this.getComponentData(componentType)[index];
|
|
970
|
-
if (data === MISSING_COMPONENT) return void 0;
|
|
971
|
-
return { value: data };
|
|
972
|
-
}
|
|
973
|
-
const value = this.sparseRelations.getValue(entityId, componentType);
|
|
974
|
-
if (value !== void 0) return { value };
|
|
975
|
-
if (this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.sparseRelations.getValue(entityId, componentType) };
|
|
976
|
-
}
|
|
977
|
-
set(entityId, componentType, data) {
|
|
978
|
-
const index = this.entityToIndex.get(entityId);
|
|
979
|
-
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
980
|
-
if (this.componentData.has(componentType)) {
|
|
981
|
-
this.getComponentData(componentType)[index] = data;
|
|
982
|
-
return;
|
|
983
|
-
}
|
|
984
|
-
const detailedType = getDetailedIdType(componentType);
|
|
985
|
-
if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) {
|
|
986
|
-
this.sparseRelations.setValue(entityId, componentType, data);
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
990
|
-
}
|
|
991
|
-
getEntities() {
|
|
992
|
-
return this.entities;
|
|
993
|
-
}
|
|
994
|
-
getEntityToIndexMap() {
|
|
995
|
-
return this.entityToIndex;
|
|
996
|
-
}
|
|
997
|
-
getComponentData(componentType) {
|
|
998
|
-
const data = this.componentData.get(componentType);
|
|
999
|
-
if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1000
|
-
return data;
|
|
1001
|
-
}
|
|
1002
|
-
getOptionalComponentData(componentType) {
|
|
1003
|
-
return this.componentData.get(componentType);
|
|
1004
|
-
}
|
|
1005
|
-
getCachedComponentDataSources(componentTypes) {
|
|
1006
|
-
const cacheKey = buildCacheKey(componentTypes);
|
|
1007
|
-
return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
|
|
1008
|
-
}
|
|
1009
|
-
getComponentDataSource(compType) {
|
|
1010
|
-
const optional = isOptionalEntityId(compType);
|
|
1011
|
-
const actualType = optional ? compType.optional : compType;
|
|
1012
|
-
if (getIdType(actualType) === "wildcard-relation") {
|
|
1013
|
-
const componentId = getComponentIdFromRelationId(actualType);
|
|
1014
|
-
return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
|
|
1015
|
-
}
|
|
1016
|
-
return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
1017
|
-
}
|
|
1018
|
-
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
|
|
1019
|
-
return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.sparseRelations));
|
|
1020
|
-
}
|
|
1021
|
-
getEntitiesWithComponents(componentTypes) {
|
|
1022
|
-
const result = [];
|
|
1023
|
-
this.appendEntitiesWithComponents(componentTypes, result);
|
|
1024
|
-
return result;
|
|
1025
|
-
}
|
|
1026
|
-
appendEntitiesWithComponents(componentTypes, result) {
|
|
1027
|
-
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
1028
|
-
result.push({
|
|
1029
|
-
entity,
|
|
1030
|
-
components
|
|
1031
|
-
});
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
*iterateWithComponents(componentTypes) {
|
|
1035
|
-
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1036
|
-
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1037
|
-
const entity = this.entities[entityIndex];
|
|
1038
|
-
yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
forEachWithComponents(componentTypes, callback) {
|
|
1042
|
-
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1043
|
-
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1044
|
-
const entity = this.entities[entityIndex];
|
|
1045
|
-
callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
forEach(callback) {
|
|
1049
|
-
for (let i = 0; i < this.entities.length; i++) {
|
|
1050
|
-
const entity = this.entities[i];
|
|
1051
|
-
const components = /* @__PURE__ */ new Map();
|
|
1052
|
-
for (const componentType of this.componentTypes) {
|
|
1053
|
-
const data = this.getComponentData(componentType)[i];
|
|
1054
|
-
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1055
|
-
}
|
|
1056
|
-
const sparseTuples = this.sparseRelations.getAllForEntity(entity);
|
|
1057
|
-
for (const [componentType, data] of sparseTuples) components.set(componentType, data);
|
|
1058
|
-
callback(entity, components);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
hasRelationWithComponentId(componentId) {
|
|
1062
|
-
for (const componentType of this.componentTypes) {
|
|
1063
|
-
const detailedType = getDetailedIdType(componentType);
|
|
1064
|
-
if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
|
|
1065
|
-
}
|
|
1066
|
-
for (const entityId of this.entities) if (this.sparseRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
};
|
|
1070
|
-
//#endregion
|
|
1071
|
-
//#region src/archetype/store.ts
|
|
1072
|
-
/**
|
|
1073
|
-
* Production implementation of SparseStore.
|
|
1074
|
-
*
|
|
1075
|
-
* Internal layout (optimized):
|
|
1076
|
-
* - byComponent: baseComponentId → (entityId → RelationEntry)
|
|
1077
|
-
* RelationEntry uses a single-value form for the common exclusive case (1 target),
|
|
1078
|
-
* avoiding Map allocation for the vast majority of usage.
|
|
1079
|
-
* - entityIndex: entityId → Set<baseComponentId>
|
|
1080
|
-
* Lightweight reverse index.
|
|
1081
|
-
*/
|
|
1082
|
-
var SparseStoreImpl = class {
|
|
1083
|
-
/**
|
|
1084
|
-
* Primary storage, keyed by the base relation component ID.
|
|
1085
|
-
*/
|
|
1086
|
-
byComponent = /* @__PURE__ */ new Map();
|
|
1087
|
-
/**
|
|
1088
|
-
* Reverse index: which base component kinds an entity participates in.
|
|
1089
|
-
* Only required to support getAllForEntity and deleteEntity efficiently.
|
|
1090
|
-
* The primary storage (byComponent) is deliberately not optimized for these operations.
|
|
1091
|
-
*/
|
|
1092
|
-
entityIndex = /* @__PURE__ */ new Map();
|
|
1093
|
-
getValue(entityId, relationType) {
|
|
1094
|
-
const componentId = getComponentIdFromRelationId(relationType);
|
|
1095
|
-
if (componentId === void 0) return void 0;
|
|
1096
|
-
const entities = this.byComponent.get(componentId);
|
|
1097
|
-
if (!entities) return void 0;
|
|
1098
|
-
const entry = entities.get(entityId);
|
|
1099
|
-
if (!entry) return void 0;
|
|
1100
|
-
const targetId = getTargetIdFromRelationId(relationType);
|
|
1101
|
-
if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
|
|
1102
|
-
else {
|
|
1103
|
-
const item = entry.targets.get(targetId);
|
|
1104
|
-
return item ? item.data : void 0;
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
setValue(entityId, relationType, data) {
|
|
1108
|
-
const componentId = getComponentIdFromRelationId(relationType);
|
|
1109
|
-
if (componentId === void 0) throw new Error("setValue called with a non-relation type on SparseStore");
|
|
1110
|
-
let entities = this.byComponent.get(componentId);
|
|
1111
|
-
if (!entities) {
|
|
1112
|
-
entities = /* @__PURE__ */ new Map();
|
|
1113
|
-
this.byComponent.set(componentId, entities);
|
|
1114
|
-
}
|
|
1115
|
-
const targetId = getTargetIdFromRelationId(relationType);
|
|
1116
|
-
let entry = entities.get(entityId);
|
|
1117
|
-
if (!entry) {
|
|
1118
|
-
entry = {
|
|
1119
|
-
type: "single",
|
|
1120
|
-
relationType,
|
|
1121
|
-
target: targetId,
|
|
1122
|
-
data
|
|
1123
|
-
};
|
|
1124
|
-
entities.set(entityId, entry);
|
|
1125
|
-
} else if (entry.type === "single") if (entry.target === targetId) {
|
|
1126
|
-
entry.data = data;
|
|
1127
|
-
entry.relationType = relationType;
|
|
1128
|
-
} else {
|
|
1129
|
-
const targets = /* @__PURE__ */ new Map();
|
|
1130
|
-
targets.set(entry.target, {
|
|
1131
|
-
relationType: entry.relationType,
|
|
1132
|
-
data: entry.data
|
|
1133
|
-
});
|
|
1134
|
-
targets.set(targetId, {
|
|
1135
|
-
relationType,
|
|
1136
|
-
data
|
|
1137
|
-
});
|
|
1138
|
-
entities.set(entityId, {
|
|
1139
|
-
type: "multi",
|
|
1140
|
-
targets
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
else entry.targets.set(targetId, {
|
|
1144
|
-
relationType,
|
|
1145
|
-
data
|
|
1146
|
-
});
|
|
1147
|
-
let components = this.entityIndex.get(entityId);
|
|
1148
|
-
if (!components) {
|
|
1149
|
-
components = /* @__PURE__ */ new Set();
|
|
1150
|
-
this.entityIndex.set(entityId, components);
|
|
1151
|
-
}
|
|
1152
|
-
components.add(componentId);
|
|
1153
|
-
}
|
|
1154
|
-
deleteValue(entityId, relationType) {
|
|
1155
|
-
const componentId = getComponentIdFromRelationId(relationType);
|
|
1156
|
-
if (componentId === void 0) return false;
|
|
1157
|
-
const entities = this.byComponent.get(componentId);
|
|
1158
|
-
if (!entities) return false;
|
|
1159
|
-
const entry = entities.get(entityId);
|
|
1160
|
-
if (!entry) return false;
|
|
1161
|
-
const targetId = getTargetIdFromRelationId(relationType);
|
|
1162
|
-
let existed = false;
|
|
1163
|
-
if (entry.type === "single") {
|
|
1164
|
-
if (entry.target === targetId) {
|
|
1165
|
-
existed = true;
|
|
1166
|
-
entities.delete(entityId);
|
|
1167
|
-
}
|
|
1168
|
-
} else {
|
|
1169
|
-
existed = entry.targets.delete(targetId);
|
|
1170
|
-
if (entry.targets.size === 0) entities.delete(entityId);
|
|
1171
|
-
else if (entry.targets.size === 1) {
|
|
1172
|
-
const [first] = entry.targets.entries();
|
|
1173
|
-
const [t, item] = first;
|
|
1174
|
-
entities.set(entityId, {
|
|
1175
|
-
type: "single",
|
|
1176
|
-
relationType: item.relationType,
|
|
1177
|
-
target: t,
|
|
1178
|
-
data: item.data
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1163
|
+
return data;
|
|
1181
1164
|
}
|
|
1182
|
-
if (
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1165
|
+
if (this.sparseRelations.getValue(entityId, componentType) !== void 0 || this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.sparseRelations.getValue(entityId, componentType);
|
|
1166
|
+
throw new Error(`Component type ${componentType} not found for entity ${entityId}`);
|
|
1167
|
+
}
|
|
1168
|
+
getOptional(entityId, componentType) {
|
|
1169
|
+
const index = this.entityToIndex.get(entityId);
|
|
1170
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1171
|
+
if (this.componentTypeSet.has(componentType)) {
|
|
1172
|
+
const data = this.getComponentData(componentType)[index];
|
|
1173
|
+
if (data === MISSING_COMPONENT) return void 0;
|
|
1174
|
+
return { value: data };
|
|
1187
1175
|
}
|
|
1188
|
-
|
|
1176
|
+
const value = this.sparseRelations.getValue(entityId, componentType);
|
|
1177
|
+
if (value !== void 0) return { value };
|
|
1178
|
+
if (this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.sparseRelations.getValue(entityId, componentType) };
|
|
1189
1179
|
}
|
|
1190
|
-
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1180
|
+
set(entityId, componentType, data) {
|
|
1181
|
+
const index = this.entityToIndex.get(entityId);
|
|
1182
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1183
|
+
if (this.componentData.has(componentType)) {
|
|
1184
|
+
this.getComponentData(componentType)[index] = data;
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1188
|
+
if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) {
|
|
1189
|
+
this.sparseRelations.setValue(entityId, componentType, data);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1193
1193
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const entities = this.byComponent.get(componentId);
|
|
1197
|
-
if (!entities) return result;
|
|
1198
|
-
const entry = entities.get(entityId);
|
|
1199
|
-
if (!entry) return result;
|
|
1200
|
-
if (entry.type === "single") result.push([entry.target, entry.data]);
|
|
1201
|
-
else for (const [target, item] of entry.targets) result.push([target, item.data]);
|
|
1202
|
-
return result;
|
|
1194
|
+
getEntities() {
|
|
1195
|
+
return this.entities;
|
|
1203
1196
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (!components || components.size === 0) return [];
|
|
1207
|
-
const result = [];
|
|
1208
|
-
for (const componentId of components) {
|
|
1209
|
-
const entry = this.byComponent.get(componentId)?.get(entityId);
|
|
1210
|
-
if (entry) if (entry.type === "single") result.push([entry.relationType, entry.data]);
|
|
1211
|
-
else for (const item of entry.targets.values()) result.push([item.relationType, item.data]);
|
|
1212
|
-
}
|
|
1213
|
-
return result;
|
|
1197
|
+
getEntityToIndexMap() {
|
|
1198
|
+
return this.entityToIndex;
|
|
1214
1199
|
}
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
if (!
|
|
1218
|
-
|
|
1219
|
-
const entities = this.byComponent.get(componentId);
|
|
1220
|
-
if (entities) {
|
|
1221
|
-
entities.delete(entityId);
|
|
1222
|
-
if (entities.size === 0) this.byComponent.delete(componentId);
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
this.entityIndex.delete(entityId);
|
|
1200
|
+
getComponentData(componentType) {
|
|
1201
|
+
const data = this.componentData.get(componentType);
|
|
1202
|
+
if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1203
|
+
return data;
|
|
1226
1204
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1205
|
+
getOptionalComponentData(componentType) {
|
|
1206
|
+
return this.componentData.get(componentType);
|
|
1207
|
+
}
|
|
1208
|
+
getCachedComponentDataSources(componentTypes) {
|
|
1209
|
+
const cacheKey = buildCacheKey(componentTypes);
|
|
1210
|
+
return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
|
|
1211
|
+
}
|
|
1212
|
+
getComponentDataSource(compType) {
|
|
1213
|
+
const optional = isOptionalEntityId(compType);
|
|
1214
|
+
const actualType = optional ? compType.optional : compType;
|
|
1215
|
+
if (getIdType(actualType) === "wildcard-relation") {
|
|
1216
|
+
const componentId = getComponentIdFromRelationId(actualType);
|
|
1217
|
+
return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
|
|
1232
1218
|
}
|
|
1233
|
-
return
|
|
1219
|
+
return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
1234
1220
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
//#region src/commands/buffer.ts
|
|
1238
|
-
/**
|
|
1239
|
-
* Maximum number of command buffer execution iterations to prevent infinite loops
|
|
1240
|
-
*/
|
|
1241
|
-
const MAX_COMMAND_ITERATIONS = 100;
|
|
1242
|
-
/**
|
|
1243
|
-
* Command buffer for deferred structural changes
|
|
1244
|
-
*/
|
|
1245
|
-
var CommandBuffer = class {
|
|
1246
|
-
commands = [];
|
|
1247
|
-
swapBuffer = [];
|
|
1248
|
-
/** Reusable map to group commands by entity, avoids per-sync allocations */
|
|
1249
|
-
entityCommands = /* @__PURE__ */ new Map();
|
|
1250
|
-
executeEntityCommands;
|
|
1251
|
-
/**
|
|
1252
|
-
* Create a command buffer with an executor function
|
|
1253
|
-
*/
|
|
1254
|
-
constructor(executeEntityCommands) {
|
|
1255
|
-
this.executeEntityCommands = executeEntityCommands;
|
|
1221
|
+
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
|
|
1222
|
+
return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.sparseRelations));
|
|
1256
1223
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
componentType,
|
|
1262
|
-
component
|
|
1263
|
-
});
|
|
1224
|
+
getEntitiesWithComponents(componentTypes) {
|
|
1225
|
+
const result = [];
|
|
1226
|
+
this.appendEntitiesWithComponents(componentTypes, result);
|
|
1227
|
+
return result;
|
|
1264
1228
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
entityId,
|
|
1272
|
-
componentType
|
|
1229
|
+
appendEntitiesWithComponents(componentTypes, result) {
|
|
1230
|
+
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
1231
|
+
result.push({
|
|
1232
|
+
entity,
|
|
1233
|
+
components
|
|
1234
|
+
});
|
|
1273
1235
|
});
|
|
1274
1236
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
entityId
|
|
1282
|
-
});
|
|
1237
|
+
*iterateWithComponents(componentTypes) {
|
|
1238
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1239
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1240
|
+
const entity = this.entities[entityIndex];
|
|
1241
|
+
yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
|
|
1242
|
+
}
|
|
1283
1243
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
let iterations = 0;
|
|
1290
|
-
while (this.commands.length > 0) {
|
|
1291
|
-
if (iterations >= MAX_COMMAND_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
1292
|
-
iterations++;
|
|
1293
|
-
const currentCommands = this.commands;
|
|
1294
|
-
this.commands = this.swapBuffer;
|
|
1295
|
-
const entityCommands = this.entityCommands;
|
|
1296
|
-
for (const cmd of currentCommands) {
|
|
1297
|
-
const existing = entityCommands.get(cmd.entityId);
|
|
1298
|
-
if (existing !== void 0) existing.push(cmd);
|
|
1299
|
-
else entityCommands.set(cmd.entityId, [cmd]);
|
|
1300
|
-
}
|
|
1301
|
-
currentCommands.length = 0;
|
|
1302
|
-
this.swapBuffer = currentCommands;
|
|
1303
|
-
for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
|
|
1304
|
-
entityCommands.clear();
|
|
1244
|
+
forEachWithComponents(componentTypes, callback) {
|
|
1245
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1246
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1247
|
+
const entity = this.entities[entityIndex];
|
|
1248
|
+
callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
|
|
1305
1249
|
}
|
|
1306
|
-
return iterations;
|
|
1307
1250
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1251
|
+
forEach(callback) {
|
|
1252
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
1253
|
+
const entity = this.entities[i];
|
|
1254
|
+
const components = /* @__PURE__ */ new Map();
|
|
1255
|
+
for (const componentType of this.componentTypes) {
|
|
1256
|
+
const data = this.getComponentData(componentType)[i];
|
|
1257
|
+
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1258
|
+
}
|
|
1259
|
+
const sparseTuples = this.sparseRelations.getAllForEntity(entity);
|
|
1260
|
+
for (const [componentType, data] of sparseTuples) components.set(componentType, data);
|
|
1261
|
+
callback(entity, components);
|
|
1262
|
+
}
|
|
1313
1263
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1264
|
+
hasRelationWithComponentId(componentId) {
|
|
1265
|
+
for (const componentType of this.componentTypes) {
|
|
1266
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1267
|
+
if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
|
|
1268
|
+
}
|
|
1269
|
+
for (const entityId of this.entities) if (this.sparseRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
|
|
1270
|
+
return false;
|
|
1319
1271
|
}
|
|
1320
1272
|
};
|
|
1321
1273
|
//#endregion
|
|
1322
|
-
//#region src/
|
|
1274
|
+
//#region src/archetype/helpers.ts
|
|
1323
1275
|
/**
|
|
1324
|
-
*
|
|
1276
|
+
* Check if a components map has any wildcard relations matching a component ID
|
|
1277
|
+
* @param components - Component entity's components map
|
|
1278
|
+
* @param wildcardComponentId - The component ID to match
|
|
1279
|
+
* @returns True if at least one matching relation exists
|
|
1325
1280
|
*/
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
/**
|
|
1330
|
-
* Add a component to the changeset
|
|
1331
|
-
*/
|
|
1332
|
-
set(componentType, component) {
|
|
1333
|
-
this.adds.set(componentType, component);
|
|
1334
|
-
this.removes.delete(componentType);
|
|
1335
|
-
}
|
|
1336
|
-
/**
|
|
1337
|
-
* Remove a component from the changeset
|
|
1338
|
-
*/
|
|
1339
|
-
delete(componentType) {
|
|
1340
|
-
this.removes.add(componentType);
|
|
1341
|
-
this.adds.delete(componentType);
|
|
1281
|
+
function hasWildcardRelation(components, wildcardComponentId) {
|
|
1282
|
+
for (const relId of components.keys()) if (isRelationId(relId)) {
|
|
1283
|
+
if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
|
|
1342
1284
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1285
|
+
return false;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Check if a detailed type represents a relation (entity or component)
|
|
1289
|
+
*/
|
|
1290
|
+
function isRelationType(detailedType) {
|
|
1291
|
+
return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Check if a component type matches a given component ID for relations
|
|
1295
|
+
*/
|
|
1296
|
+
function matchesRelationComponentId(componentType, componentId) {
|
|
1297
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1298
|
+
return isRelationType(detailedType) && detailedType.componentId === componentId;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Build cache key for component types
|
|
1302
|
+
*/
|
|
1303
|
+
function buildCacheKey(componentTypes) {
|
|
1304
|
+
return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Get data source for wildcard relations from component types
|
|
1308
|
+
*/
|
|
1309
|
+
function getWildcardRelationDataSource(componentTypes, componentId, optional) {
|
|
1310
|
+
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
1311
|
+
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Build wildcard relation value from matching relations.
|
|
1315
|
+
* Receives the SparseStore directly for efficient per-component lookups.
|
|
1316
|
+
*/
|
|
1317
|
+
function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, sparseStore, entityId, optional) {
|
|
1318
|
+
const relations = [];
|
|
1319
|
+
const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
1320
|
+
for (const relType of matchingRelations || []) {
|
|
1321
|
+
const data = getDataAtIndex(relType);
|
|
1322
|
+
const targetId = getTargetIdFromRelationId(relType);
|
|
1323
|
+
relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
1348
1324
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
clear() {
|
|
1353
|
-
this.adds.clear();
|
|
1354
|
-
this.removes.clear();
|
|
1325
|
+
if (targetComponentId !== void 0) {
|
|
1326
|
+
const dfMatches = sparseStore.getRelationsForComponent(entityId, targetComponentId);
|
|
1327
|
+
for (const m of dfMatches) relations.push(m);
|
|
1355
1328
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
for (const [componentType, component] of other.adds) {
|
|
1361
|
-
this.adds.set(componentType, component);
|
|
1362
|
-
this.removes.delete(componentType);
|
|
1363
|
-
}
|
|
1364
|
-
for (const componentType of other.removes) {
|
|
1365
|
-
this.removes.add(componentType);
|
|
1366
|
-
this.adds.delete(componentType);
|
|
1329
|
+
if (relations.length === 0) {
|
|
1330
|
+
if (!optional) {
|
|
1331
|
+
const componentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
1332
|
+
throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
|
|
1367
1333
|
}
|
|
1334
|
+
return;
|
|
1368
1335
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1336
|
+
return optional ? { value: relations } : relations;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Build regular component value from data source
|
|
1340
|
+
*/
|
|
1341
|
+
function buildRegularComponentValue(dataSource, entityIndex, optional) {
|
|
1342
|
+
if (dataSource === void 0) {
|
|
1343
|
+
if (optional) return void 0;
|
|
1344
|
+
throw new Error(`Component data not found for mandatory component type`);
|
|
1376
1345
|
}
|
|
1377
|
-
|
|
1346
|
+
const data = dataSource[entityIndex];
|
|
1347
|
+
const result = data === MISSING_COMPONENT ? void 0 : data;
|
|
1348
|
+
return optional ? { value: result } : result;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Build a single component value based on its type
|
|
1352
|
+
*/
|
|
1353
|
+
function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, sparseRelations) {
|
|
1354
|
+
const optional = isOptionalEntityId(compType);
|
|
1355
|
+
const actualType = optional ? compType.optional : compType;
|
|
1356
|
+
if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], sparseRelations, entityId, optional);
|
|
1357
|
+
else return buildRegularComponentValue(dataSource, entityIndex, optional);
|
|
1358
|
+
}
|
|
1378
1359
|
//#endregion
|
|
1379
1360
|
//#region src/component/entity-store.ts
|
|
1380
1361
|
/**
|
|
@@ -1934,78 +1915,299 @@ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, co
|
|
|
1934
1915
|
if (changeset.removes.has(otherComponentType)) continue;
|
|
1935
1916
|
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1936
1917
|
}
|
|
1937
|
-
for (const addedType of changeset.adds.keys()) {
|
|
1938
|
-
if (addedType === removedComponentType) continue;
|
|
1939
|
-
if (getComponentIdFromRelationId(addedType) === componentId) return;
|
|
1918
|
+
for (const addedType of changeset.adds.keys()) {
|
|
1919
|
+
if (addedType === removedComponentType) continue;
|
|
1920
|
+
if (getComponentIdFromRelationId(addedType) === componentId) return;
|
|
1921
|
+
}
|
|
1922
|
+
changeset.delete(wildcardMarker);
|
|
1923
|
+
}
|
|
1924
|
+
function hasEntityComponent(archetype, entityId, componentType) {
|
|
1925
|
+
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
1926
|
+
return archetype.getEntitySparseRelations(entityId)?.has(componentType) ?? false;
|
|
1927
|
+
}
|
|
1928
|
+
function pruneMissingRemovals(changeset, archetype, entityId) {
|
|
1929
|
+
let toPrune;
|
|
1930
|
+
for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
|
|
1931
|
+
if (toPrune === void 0) toPrune = [];
|
|
1932
|
+
toPrune.push(componentType);
|
|
1933
|
+
}
|
|
1934
|
+
if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
|
|
1935
|
+
}
|
|
1936
|
+
function hasArchetypeStructuralChange(changeset, currentArchetype) {
|
|
1937
|
+
for (const componentType of changeset.removes) if (!isSparseRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1938
|
+
for (const componentType of changeset.adds.keys()) if (!isSparseRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
function buildFinalRegularComponentTypes(currentArchetype, changeset) {
|
|
1942
|
+
const finalRegularTypes = new Set(currentArchetype.componentTypes);
|
|
1943
|
+
for (const componentType of changeset.removes) if (!isSparseRelation(componentType)) finalRegularTypes.delete(componentType);
|
|
1944
|
+
for (const [componentType] of changeset.adds) if (!isSparseRelation(componentType)) finalRegularTypes.add(componentType);
|
|
1945
|
+
return Array.from(finalRegularTypes);
|
|
1946
|
+
}
|
|
1947
|
+
function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
|
|
1948
|
+
pruneMissingRemovals(changeset, currentArchetype, entityId);
|
|
1949
|
+
if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
|
|
1950
|
+
const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
|
|
1951
|
+
const newArchetype = ctx.ensureArchetype(finalRegularTypes);
|
|
1952
|
+
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1953
|
+
if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
|
|
1954
|
+
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1955
|
+
entityToArchetype.set(entityId, newArchetype);
|
|
1956
|
+
return newArchetype;
|
|
1957
|
+
}
|
|
1958
|
+
if (removedComponents !== null) applySparseChanges(ctx.sparseStore, entityId, changeset, removedComponents);
|
|
1959
|
+
else applySparseChangesNoHooks(ctx.sparseStore, entityId, changeset);
|
|
1960
|
+
for (const [componentType, component] of changeset.adds) {
|
|
1961
|
+
if (isSparseRelation(componentType)) continue;
|
|
1962
|
+
currentArchetype.set(entityId, componentType, component);
|
|
1963
|
+
}
|
|
1964
|
+
return currentArchetype;
|
|
1965
|
+
}
|
|
1966
|
+
function applySparseChanges(sparseStore, entityId, changeset, removedComponents) {
|
|
1967
|
+
for (const componentType of changeset.removes) if (isSparseRelation(componentType)) {
|
|
1968
|
+
const removedValue = sparseStore.getValue(entityId, componentType);
|
|
1969
|
+
if (removedValue !== void 0 || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
|
|
1970
|
+
sparseStore.deleteValue(entityId, componentType);
|
|
1971
|
+
}
|
|
1972
|
+
for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
|
|
1973
|
+
}
|
|
1974
|
+
function applySparseChangesNoHooks(sparseStore, entityId, changeset) {
|
|
1975
|
+
for (const componentType of changeset.removes) if (isSparseRelation(componentType)) sparseStore.deleteValue(entityId, componentType);
|
|
1976
|
+
for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
|
|
1977
|
+
}
|
|
1978
|
+
function filterRegularComponentTypes(componentTypes) {
|
|
1979
|
+
const regularTypes = [];
|
|
1980
|
+
for (const componentType of componentTypes) {
|
|
1981
|
+
if (isSparseWildcard(componentType)) {
|
|
1982
|
+
regularTypes.push(componentType);
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
if (isSparseRelation(componentType)) continue;
|
|
1986
|
+
regularTypes.push(componentType);
|
|
1987
|
+
}
|
|
1988
|
+
return regularTypes;
|
|
1989
|
+
}
|
|
1990
|
+
//#endregion
|
|
1991
|
+
//#region src/world/archetype-manager.ts
|
|
1992
|
+
/**
|
|
1993
|
+
* Encapsulates all archetype storage, indexing, creation, removal, and reverse
|
|
1994
|
+
* referencing logic that was previously scattered as private methods + maps
|
|
1995
|
+
* directly on the World class.
|
|
1996
|
+
*
|
|
1997
|
+
* Responsibilities:
|
|
1998
|
+
* - Archetype memoization by signature
|
|
1999
|
+
* - Component-type reverse index (archetypesByComponent)
|
|
2000
|
+
* - Entity → current Archetype map
|
|
2001
|
+
* - Reverse "who references this entity via component/relation" index
|
|
2002
|
+
* - Creation + removal with proper notifications to QueryRegistry + hook matching
|
|
2003
|
+
* - Cleanup of empty archetypes after entity cascades
|
|
2004
|
+
*
|
|
2005
|
+
* This extraction shrinks World while keeping the same behavior and hot-path characteristics.
|
|
2006
|
+
*/
|
|
2007
|
+
var ArchetypeManager = class {
|
|
2008
|
+
archetypes = [];
|
|
2009
|
+
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
2010
|
+
entityToArchetype = /* @__PURE__ */ new Map();
|
|
2011
|
+
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
2012
|
+
entityToReferencingArchetypes = /* @__PURE__ */ new Map();
|
|
2013
|
+
sparseStore;
|
|
2014
|
+
ctx;
|
|
2015
|
+
constructor(ctx, sparseStore) {
|
|
2016
|
+
this.ctx = ctx;
|
|
2017
|
+
this.sparseStore = sparseStore;
|
|
2018
|
+
}
|
|
2019
|
+
/** Primary entry point — memoized archetype creation/lookup. */
|
|
2020
|
+
ensureArchetype(componentTypes) {
|
|
2021
|
+
const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
|
|
2022
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
2023
|
+
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
2024
|
+
}
|
|
2025
|
+
getArchetypeForEntity(entityId) {
|
|
2026
|
+
return this.entityToArchetype.get(entityId);
|
|
2027
|
+
}
|
|
2028
|
+
setEntityToArchetype(entityId, archetype) {
|
|
2029
|
+
this.entityToArchetype.set(entityId, archetype);
|
|
2030
|
+
}
|
|
2031
|
+
getMatchingArchetypes(componentTypes) {
|
|
2032
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2033
|
+
const regularComponents = [];
|
|
2034
|
+
const wildcardRelations = [];
|
|
2035
|
+
for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
|
|
2036
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
2037
|
+
if (componentId !== void 0) wildcardRelations.push({
|
|
2038
|
+
componentId,
|
|
2039
|
+
relationId: componentType
|
|
2040
|
+
});
|
|
2041
|
+
} else regularComponents.push(componentType);
|
|
2042
|
+
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
2043
|
+
for (const { componentId, relationId } of wildcardRelations) {
|
|
2044
|
+
const markerSet = this.archetypesByComponent.get(relationId);
|
|
2045
|
+
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
2046
|
+
matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
2047
|
+
}
|
|
2048
|
+
return matchingArchetypes;
|
|
2049
|
+
}
|
|
2050
|
+
getArchetypesWithComponents(componentTypes) {
|
|
2051
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2052
|
+
if (componentTypes.length === 1) {
|
|
2053
|
+
const set = this.archetypesByComponent.get(componentTypes[0]);
|
|
2054
|
+
return set ? Array.from(set) : [];
|
|
2055
|
+
}
|
|
2056
|
+
const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
|
|
2057
|
+
if (sets.length === 0) return [];
|
|
2058
|
+
if (sets.length < componentTypes.length) return [];
|
|
2059
|
+
const smallest = sets[0];
|
|
2060
|
+
if (sets.length === 2) {
|
|
2061
|
+
const other = sets[1];
|
|
2062
|
+
return Array.from(smallest).filter((a) => other.has(a));
|
|
2063
|
+
}
|
|
2064
|
+
let result = new Set(smallest);
|
|
2065
|
+
for (let i = 1; i < sets.length; i++) {
|
|
2066
|
+
for (const item of result) if (!sets[i].has(item)) result.delete(item);
|
|
2067
|
+
if (result.size === 0) return [];
|
|
2068
|
+
}
|
|
2069
|
+
return Array.from(result);
|
|
2070
|
+
}
|
|
2071
|
+
createArchetypeSignature(componentTypes) {
|
|
2072
|
+
return componentTypes.join(",");
|
|
2073
|
+
}
|
|
2074
|
+
/** Deduplicated version of the original pair of methods. */
|
|
2075
|
+
updateReferencingIndex(componentType, archetype, isAdd) {
|
|
2076
|
+
const detailedType = getDetailedIdType(componentType);
|
|
2077
|
+
let entityId;
|
|
2078
|
+
if (detailedType.type === "entity") entityId = componentType;
|
|
2079
|
+
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
2080
|
+
if (entityId !== void 0) {
|
|
2081
|
+
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2082
|
+
if (isAdd) {
|
|
2083
|
+
if (!refs) {
|
|
2084
|
+
refs = /* @__PURE__ */ new Set();
|
|
2085
|
+
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
2086
|
+
}
|
|
2087
|
+
refs.add(archetype);
|
|
2088
|
+
} else if (refs) {
|
|
2089
|
+
refs.delete(archetype);
|
|
2090
|
+
if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
createNewArchetype(componentTypes) {
|
|
2095
|
+
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
2096
|
+
this.archetypes.push(newArchetype);
|
|
2097
|
+
this.ctx.recordArchetypeCreated?.();
|
|
2098
|
+
for (const componentType of componentTypes) {
|
|
2099
|
+
let archetypes = this.archetypesByComponent.get(componentType);
|
|
2100
|
+
if (!archetypes) {
|
|
2101
|
+
archetypes = /* @__PURE__ */ new Set();
|
|
2102
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
2103
|
+
}
|
|
2104
|
+
archetypes.add(newArchetype);
|
|
2105
|
+
this.updateReferencingIndex(componentType, newArchetype, true);
|
|
2106
|
+
}
|
|
2107
|
+
this.ctx.queryRegistry.onNewArchetype(newArchetype);
|
|
2108
|
+
this.updateArchetypeHookMatches(newArchetype);
|
|
2109
|
+
return newArchetype;
|
|
2110
|
+
}
|
|
2111
|
+
updateArchetypeHookMatches(archetype) {
|
|
2112
|
+
for (const entry of this.ctx.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
|
|
2113
|
+
archetype.matchingMultiHooks.add(entry);
|
|
2114
|
+
if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
archetypeMatchesHook(archetype, entry) {
|
|
2118
|
+
return entry.requiredComponents.every((c) => {
|
|
2119
|
+
if (isWildcardRelationId(c)) {
|
|
2120
|
+
if (isSparseWildcard(c)) return true;
|
|
2121
|
+
const componentId = getComponentIdFromRelationId(c);
|
|
2122
|
+
return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
|
|
2123
|
+
}
|
|
2124
|
+
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
2125
|
+
}) && matchesFilter(archetype, entry.filter);
|
|
2126
|
+
}
|
|
2127
|
+
/** Called during cascade deletion cleanup. */
|
|
2128
|
+
cleanupArchetypesReferencingEntity(entityId) {
|
|
2129
|
+
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2130
|
+
if (!refs) return;
|
|
2131
|
+
for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
|
|
2132
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
1940
2133
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2134
|
+
removeArchetype(archetype) {
|
|
2135
|
+
const index = this.archetypes.indexOf(archetype);
|
|
2136
|
+
if (index !== -1) {
|
|
2137
|
+
const last = this.archetypes[this.archetypes.length - 1];
|
|
2138
|
+
this.archetypes[index] = last;
|
|
2139
|
+
this.archetypes.pop();
|
|
2140
|
+
}
|
|
2141
|
+
this.ctx.recordArchetypeRemoved?.();
|
|
2142
|
+
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
2143
|
+
for (const componentType of archetype.componentTypes) {
|
|
2144
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
2145
|
+
if (archetypes) {
|
|
2146
|
+
archetypes.delete(archetype);
|
|
2147
|
+
if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
|
|
2148
|
+
}
|
|
2149
|
+
this.updateReferencingIndex(componentType, archetype, false);
|
|
2150
|
+
}
|
|
2151
|
+
this.ctx.queryRegistry.onArchetypeRemoved(archetype);
|
|
1952
2152
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
|
|
1969
|
-
const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
|
|
1970
|
-
const newArchetype = ctx.ensureArchetype(finalRegularTypes);
|
|
1971
|
-
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1972
|
-
if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
|
|
1973
|
-
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1974
|
-
entityToArchetype.set(entityId, newArchetype);
|
|
1975
|
-
return newArchetype;
|
|
2153
|
+
};
|
|
2154
|
+
//#endregion
|
|
2155
|
+
//#region src/commands/changeset.ts
|
|
2156
|
+
/**
|
|
2157
|
+
* @internal Represents a set of component changes to be applied to an entity
|
|
2158
|
+
*/
|
|
2159
|
+
var ComponentChangeset = class {
|
|
2160
|
+
adds = /* @__PURE__ */ new Map();
|
|
2161
|
+
removes = /* @__PURE__ */ new Set();
|
|
2162
|
+
/**
|
|
2163
|
+
* Add a component to the changeset
|
|
2164
|
+
*/
|
|
2165
|
+
set(componentType, component) {
|
|
2166
|
+
this.adds.set(componentType, component);
|
|
2167
|
+
this.removes.delete(componentType);
|
|
1976
2168
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2169
|
+
/**
|
|
2170
|
+
* Remove a component from the changeset
|
|
2171
|
+
*/
|
|
2172
|
+
delete(componentType) {
|
|
2173
|
+
this.removes.add(componentType);
|
|
2174
|
+
this.adds.delete(componentType);
|
|
1982
2175
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
if (removedValue !== void 0 || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
|
|
1989
|
-
sparseStore.deleteValue(entityId, componentType);
|
|
2176
|
+
/**
|
|
2177
|
+
* Check if the changeset has any changes
|
|
2178
|
+
*/
|
|
2179
|
+
hasChanges() {
|
|
2180
|
+
return this.adds.size > 0 || this.removes.size > 0;
|
|
1990
2181
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2182
|
+
/**
|
|
2183
|
+
* Clear all changes
|
|
2184
|
+
*/
|
|
2185
|
+
clear() {
|
|
2186
|
+
this.adds.clear();
|
|
2187
|
+
this.removes.clear();
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Merge another changeset into this one
|
|
2191
|
+
*/
|
|
2192
|
+
merge(other) {
|
|
2193
|
+
for (const [componentType, component] of other.adds) {
|
|
2194
|
+
this.adds.set(componentType, component);
|
|
2195
|
+
this.removes.delete(componentType);
|
|
2196
|
+
}
|
|
2197
|
+
for (const componentType of other.removes) {
|
|
2198
|
+
this.removes.add(componentType);
|
|
2199
|
+
this.adds.delete(componentType);
|
|
2003
2200
|
}
|
|
2004
|
-
if (isSparseRelation(componentType)) continue;
|
|
2005
|
-
regularTypes.push(componentType);
|
|
2006
2201
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2202
|
+
/**
|
|
2203
|
+
* Apply the changeset to existing components and return the final state
|
|
2204
|
+
*/
|
|
2205
|
+
applyTo(existingComponents) {
|
|
2206
|
+
for (const componentType of this.removes) existingComponents.delete(componentType);
|
|
2207
|
+
for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
|
|
2208
|
+
return existingComponents;
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2009
2211
|
//#endregion
|
|
2010
2212
|
//#region src/world/hooks.ts
|
|
2011
2213
|
/**
|
|
@@ -2310,8 +2512,282 @@ function untrackEntityReference(entityReferences, sourceEntityId, componentType,
|
|
|
2310
2512
|
*
|
|
2311
2513
|
* @internal
|
|
2312
2514
|
*/
|
|
2313
|
-
function getEntityReferences(entityReferences, targetEntityId) {
|
|
2314
|
-
return entityReferences.get(targetEntityId) ?? new MultiMap();
|
|
2515
|
+
function getEntityReferences(entityReferences, targetEntityId) {
|
|
2516
|
+
return entityReferences.get(targetEntityId) ?? new MultiMap();
|
|
2517
|
+
}
|
|
2518
|
+
//#endregion
|
|
2519
|
+
//#region src/world/command-executor.ts
|
|
2520
|
+
/**
|
|
2521
|
+
* Encapsulates the command execution pipeline, reusable changesets,
|
|
2522
|
+
* and related orchestration that was previously private methods + fields on World.
|
|
2523
|
+
*
|
|
2524
|
+
* Responsibilities:
|
|
2525
|
+
* - executeEntityCommands (routing for singletons / destroy / structural changes)
|
|
2526
|
+
* - applyEntityCommands (changeset processing + exclusive relations + apply + refs + hooks)
|
|
2527
|
+
* - removeComponentImmediate (used by cascade deletion)
|
|
2528
|
+
* - updateEntityReferences (keeps the reverse index in sync)
|
|
2529
|
+
*
|
|
2530
|
+
* This extraction significantly reduces World line count while preserving
|
|
2531
|
+
* every fast-path branch and allocation-avoidance characteristic.
|
|
2532
|
+
*/
|
|
2533
|
+
var CommandExecutor = class {
|
|
2534
|
+
ctx;
|
|
2535
|
+
_changeset = new ComponentChangeset();
|
|
2536
|
+
_removeChangeset = new ComponentChangeset();
|
|
2537
|
+
_commandCtx;
|
|
2538
|
+
_hooksCtx;
|
|
2539
|
+
constructor(ctx) {
|
|
2540
|
+
this.ctx = ctx;
|
|
2541
|
+
this._commandCtx = {
|
|
2542
|
+
sparseStore: ctx.sparseStore,
|
|
2543
|
+
ensureArchetype: ctx.ensureArchetype
|
|
2544
|
+
};
|
|
2545
|
+
this._hooksCtx = {
|
|
2546
|
+
multiHooks: ctx.hooks,
|
|
2547
|
+
has: ctx.has,
|
|
2548
|
+
get: ctx.get,
|
|
2549
|
+
getOptional: ctx.getOptional
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Entry point used by the CommandBuffer.
|
|
2554
|
+
* Routes to singleton handling, destroy fast path, or structural apply.
|
|
2555
|
+
*/
|
|
2556
|
+
executeEntityCommands(entityId, commands) {
|
|
2557
|
+
this._changeset.clear();
|
|
2558
|
+
if (this.ctx.componentEntities.exists(entityId)) {
|
|
2559
|
+
this.ctx.componentEntities.executeCommands(entityId, commands);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
2563
|
+
this.ctx.destroyEntityImmediate(entityId);
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
this.applyEntityCommands(entityId, commands);
|
|
2567
|
+
}
|
|
2568
|
+
applyEntityCommands(entityId, commands) {
|
|
2569
|
+
const currentArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
2570
|
+
if (!currentArchetype) return;
|
|
2571
|
+
const changeset = this._changeset;
|
|
2572
|
+
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
2573
|
+
if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
|
|
2574
|
+
});
|
|
2575
|
+
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
2576
|
+
if (this.ctx.hooks.size === 0) {
|
|
2577
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, null);
|
|
2578
|
+
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2579
|
+
if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const removedComponents = /* @__PURE__ */ new Map();
|
|
2583
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, removedComponents);
|
|
2584
|
+
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2585
|
+
if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
|
|
2586
|
+
this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Immediate (non-buffered) component removal used during cascade deletion.
|
|
2590
|
+
* Called from destroy* paths (which remain in World).
|
|
2591
|
+
*/
|
|
2592
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
2593
|
+
const sourceArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
2594
|
+
if (!sourceArchetype) return;
|
|
2595
|
+
const changeset = this._removeChangeset;
|
|
2596
|
+
changeset.clear();
|
|
2597
|
+
changeset.delete(componentType);
|
|
2598
|
+
maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
|
|
2599
|
+
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
2600
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.ctx.entityToArchetype, null);
|
|
2601
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetEntityId);
|
|
2602
|
+
this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
|
|
2603
|
+
}
|
|
2604
|
+
/**
|
|
2605
|
+
* Keeps the entity reference reverse index in sync after structural changes.
|
|
2606
|
+
* Called from apply paths.
|
|
2607
|
+
*/
|
|
2608
|
+
updateEntityReferences(entityId, changeset) {
|
|
2609
|
+
for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
|
|
2610
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
2611
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
2612
|
+
} else if (componentType >= 1024) untrackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
2613
|
+
for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
|
|
2614
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
2615
|
+
trackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
2616
|
+
} else if (componentType >= 1024) trackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Exposed for any future direct needs (currently not required outside the executor).
|
|
2620
|
+
*/
|
|
2621
|
+
getHooksContext() {
|
|
2622
|
+
return this._hooksCtx;
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
//#endregion
|
|
2626
|
+
//#region src/world/debug-stats.ts
|
|
2627
|
+
/**
|
|
2628
|
+
* Manages debug stats collectors and transient activity counters for World#sync().
|
|
2629
|
+
*
|
|
2630
|
+
* Extracted from World to shrink the main class while keeping the entire debug/observability
|
|
2631
|
+
* path isolated, zero-cost when no collectors are active, and easy to test/maintain.
|
|
2632
|
+
*
|
|
2633
|
+
* Follows the same context/callback injection style as ArchetypeManager, CommandProcessorContext,
|
|
2634
|
+
* and HooksContext to avoid tight coupling.
|
|
2635
|
+
*
|
|
2636
|
+
* All collectors receive the *exact same* stats object for a given sync (as before).
|
|
2637
|
+
* Exceptions in user callbacks are swallowed (as before).
|
|
2638
|
+
*/
|
|
2639
|
+
var DebugStatsManager = class {
|
|
2640
|
+
collectors = /* @__PURE__ */ new Set();
|
|
2641
|
+
migrations = 0;
|
|
2642
|
+
archetypesCreated = 0;
|
|
2643
|
+
archetypesRemoved = 0;
|
|
2644
|
+
/** Fast check used to arm timing + reset + counting in hot paths. */
|
|
2645
|
+
hasActiveCollectors() {
|
|
2646
|
+
return this.collectors.size > 0;
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Registers a collector. Returns a disposable handle (supports `using`).
|
|
2650
|
+
* Collection stops when the handle is disposed.
|
|
2651
|
+
*/
|
|
2652
|
+
createCollector(callback) {
|
|
2653
|
+
this.collectors.add(callback);
|
|
2654
|
+
return { [Symbol.dispose]: () => {
|
|
2655
|
+
this.collectors.delete(callback);
|
|
2656
|
+
} };
|
|
2657
|
+
}
|
|
2658
|
+
recordArchetypeCreated() {
|
|
2659
|
+
if (this.hasActiveCollectors()) this.archetypesCreated++;
|
|
2660
|
+
}
|
|
2661
|
+
recordArchetypeRemoved() {
|
|
2662
|
+
if (this.hasActiveCollectors()) this.archetypesRemoved++;
|
|
2663
|
+
}
|
|
2664
|
+
incrementMigrations() {
|
|
2665
|
+
if (this.hasActiveCollectors()) this.migrations++;
|
|
2666
|
+
}
|
|
2667
|
+
/** Reset all activity counters + the shared hook execution counter. Called at start of an armed sync. */
|
|
2668
|
+
resetActivity() {
|
|
2669
|
+
this.migrations = 0;
|
|
2670
|
+
this.archetypesCreated = 0;
|
|
2671
|
+
this.archetypesRemoved = 0;
|
|
2672
|
+
debugHookExecutionCounter.value = 0;
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Build and deliver a SyncDebugStats payload to every active collector.
|
|
2676
|
+
* World supplies the pre-computed snapshot numbers (keeps debug manager decoupled from
|
|
2677
|
+
* internal World maps/registries while preserving exact original stats shape and values).
|
|
2678
|
+
*/
|
|
2679
|
+
deliver(timings, data) {
|
|
2680
|
+
const stats = {
|
|
2681
|
+
timestamps: {
|
|
2682
|
+
syncStart: timings.syncStart,
|
|
2683
|
+
syncEnd: timings.syncEnd,
|
|
2684
|
+
commandBufferStart: timings.commandBufferStart,
|
|
2685
|
+
commandBufferEnd: timings.commandBufferEnd
|
|
2686
|
+
},
|
|
2687
|
+
commandIterations: timings.commandIterations,
|
|
2688
|
+
entities: {
|
|
2689
|
+
total: data.entityCount,
|
|
2690
|
+
freelistSize: data.freelistSize,
|
|
2691
|
+
nextId: data.nextId
|
|
2692
|
+
},
|
|
2693
|
+
archetypes: {
|
|
2694
|
+
total: data.archetypeCount,
|
|
2695
|
+
empty: data.emptyArchetypes
|
|
2696
|
+
},
|
|
2697
|
+
queries: {
|
|
2698
|
+
cached: data.cachedQueryCount,
|
|
2699
|
+
registered: data.registeredQueryCount
|
|
2700
|
+
},
|
|
2701
|
+
hooks: { total: data.hookCount },
|
|
2702
|
+
indices: {
|
|
2703
|
+
entityReferences: data.entityReferencesSize,
|
|
2704
|
+
entityToReferencingArchetypes: data.entityToReferencingArchetypesSize,
|
|
2705
|
+
archetypesByComponent: data.archetypesByComponentSize
|
|
2706
|
+
},
|
|
2707
|
+
activity: {
|
|
2708
|
+
migrations: this.migrations,
|
|
2709
|
+
hooksExecuted: debugHookExecutionCounter.value,
|
|
2710
|
+
archetypesCreated: this.archetypesCreated,
|
|
2711
|
+
archetypesRemoved: this.archetypesRemoved
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
for (const cb of this.collectors) try {
|
|
2715
|
+
cb(stats);
|
|
2716
|
+
} catch {}
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
//#endregion
|
|
2720
|
+
//#region src/world/operations.ts
|
|
2721
|
+
/**
|
|
2722
|
+
* Validation and overload-resolution helpers extracted from World.
|
|
2723
|
+
*
|
|
2724
|
+
* These were previously private methods on World. Moving them reduces line count
|
|
2725
|
+
* in the core class with almost zero coupling (the only dep is a liveness predicate
|
|
2726
|
+
* for assertEntityExists, supplied by the caller).
|
|
2727
|
+
*
|
|
2728
|
+
* Pure type checks (assert*TypeValid) and the resolve* helpers for set/remove
|
|
2729
|
+
* overloads live here.
|
|
2730
|
+
*/
|
|
2731
|
+
/**
|
|
2732
|
+
* Assert that an entity (or component-entity) is alive in the world.
|
|
2733
|
+
* The caller supplies the liveness check (World.exists or equivalent) to keep
|
|
2734
|
+
* this module free of direct references to stores.
|
|
2735
|
+
*/
|
|
2736
|
+
function assertEntityExists(entityId, label, exists) {
|
|
2737
|
+
if (!exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
|
|
2738
|
+
}
|
|
2739
|
+
function assertComponentTypeValid(componentType) {
|
|
2740
|
+
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2741
|
+
}
|
|
2742
|
+
function assertSetComponentTypeValid(componentType) {
|
|
2743
|
+
const detailedType = getDetailedIdType(componentType);
|
|
2744
|
+
if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2745
|
+
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Resolve the (entity, componentType, value) for a set() call.
|
|
2749
|
+
*/
|
|
2750
|
+
function resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, argCount = 3, exists = () => true) {
|
|
2751
|
+
const targetEntityId = entityId;
|
|
2752
|
+
if (argCount === 2 && isComponentId(targetEntityId) && typeof componentTypeOrComponent !== "number") {
|
|
2753
|
+
assertEntityExists(targetEntityId, "Component entity", exists);
|
|
2754
|
+
return {
|
|
2755
|
+
entityId: targetEntityId,
|
|
2756
|
+
componentType: targetEntityId,
|
|
2757
|
+
component: componentTypeOrComponent,
|
|
2758
|
+
deprecatedSingletonShorthand: true
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
const componentType = componentTypeOrComponent;
|
|
2762
|
+
assertEntityExists(targetEntityId, "Entity", exists);
|
|
2763
|
+
assertSetComponentTypeValid(componentType);
|
|
2764
|
+
return {
|
|
2765
|
+
entityId: targetEntityId,
|
|
2766
|
+
componentType,
|
|
2767
|
+
component: maybeComponent,
|
|
2768
|
+
deprecatedSingletonShorthand: false
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Resolve the (entity, componentType) for a remove() call, handling the
|
|
2773
|
+
* singleton component overload (remove(componentId)).
|
|
2774
|
+
*/
|
|
2775
|
+
function resolveRemoveOperation(entityId, componentType, exists = () => true) {
|
|
2776
|
+
if (componentType === void 0) {
|
|
2777
|
+
const componentId = entityId;
|
|
2778
|
+
assertEntityExists(componentId, "Component entity", exists);
|
|
2779
|
+
return {
|
|
2780
|
+
entityId: componentId,
|
|
2781
|
+
componentType: componentId
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
const targetEntityId = entityId;
|
|
2785
|
+
assertEntityExists(targetEntityId, "Entity", exists);
|
|
2786
|
+
assertComponentTypeValid(componentType);
|
|
2787
|
+
return {
|
|
2788
|
+
entityId: targetEntityId,
|
|
2789
|
+
componentType
|
|
2790
|
+
};
|
|
2315
2791
|
}
|
|
2316
2792
|
//#endregion
|
|
2317
2793
|
//#region src/storage/serialization.ts
|
|
@@ -2479,51 +2955,63 @@ function deserializeWorld(ctx, snapshot) {
|
|
|
2479
2955
|
* World class for ECS architecture
|
|
2480
2956
|
* Manages entities and components
|
|
2481
2957
|
*/
|
|
2482
|
-
var World = class {
|
|
2958
|
+
var World = class World {
|
|
2959
|
+
static DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING = "world.set(componentId, value) for singleton components is deprecated; use world.singleton(componentId).set(value) or world.set(componentId, componentId, value) instead.";
|
|
2483
2960
|
entityIdManager = new EntityIdManager();
|
|
2484
|
-
archetypes = [];
|
|
2485
|
-
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
2486
|
-
entityToArchetype = /* @__PURE__ */ new Map();
|
|
2487
|
-
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
2488
2961
|
entityReferences = /* @__PURE__ */ new Map();
|
|
2489
|
-
/** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
|
|
2490
|
-
entityToReferencingArchetypes = /* @__PURE__ */ new Map();
|
|
2491
2962
|
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
2492
2963
|
sparseStore = new SparseStoreImpl();
|
|
2493
2964
|
/** Component entity (singleton) storage */
|
|
2494
2965
|
componentEntities = new ComponentEntityStore();
|
|
2966
|
+
archetypeManager;
|
|
2967
|
+
get archetypes() {
|
|
2968
|
+
return this.archetypeManager.archetypes;
|
|
2969
|
+
}
|
|
2970
|
+
get entityToArchetype() {
|
|
2971
|
+
return this.archetypeManager.entityToArchetype;
|
|
2972
|
+
}
|
|
2973
|
+
get archetypesByComponent() {
|
|
2974
|
+
return this.archetypeManager.archetypesByComponent;
|
|
2975
|
+
}
|
|
2976
|
+
get entityToReferencingArchetypes() {
|
|
2977
|
+
return this.archetypeManager.entityToReferencingArchetypes;
|
|
2978
|
+
}
|
|
2495
2979
|
queryRegistry = new QueryRegistry();
|
|
2496
2980
|
hooks = /* @__PURE__ */ new Set();
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
_debugArchetypesRemoved = 0;
|
|
2501
|
-
commandBuffer = new CommandBuffer((entityId, commands) => this.executeEntityCommands(entityId, commands));
|
|
2502
|
-
_changeset = new ComponentChangeset();
|
|
2503
|
-
_removeChangeset = new ComponentChangeset();
|
|
2504
|
-
/** Cached command processor context to avoid per-entity object allocation */
|
|
2505
|
-
_commandCtx = {
|
|
2506
|
-
sparseStore: this.sparseStore,
|
|
2507
|
-
ensureArchetype: (ct) => this.ensureArchetype(ct)
|
|
2508
|
-
};
|
|
2509
|
-
/** Cached hooks context to avoid per-entity object allocation */
|
|
2510
|
-
_hooksCtx = {
|
|
2511
|
-
multiHooks: this.hooks,
|
|
2512
|
-
has: (eid, ct) => this.has(eid, ct),
|
|
2513
|
-
get: (eid, ct) => this.get(eid, ct),
|
|
2514
|
-
getOptional: (eid, ct) => this.getOptional(eid, ct)
|
|
2515
|
-
};
|
|
2981
|
+
debugStats = new DebugStatsManager();
|
|
2982
|
+
commandBuffer;
|
|
2983
|
+
commandExecutor;
|
|
2516
2984
|
constructor(snapshot) {
|
|
2985
|
+
this.archetypeManager = new ArchetypeManager({
|
|
2986
|
+
queryRegistry: this.queryRegistry,
|
|
2987
|
+
hooks: this.hooks,
|
|
2988
|
+
recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
|
|
2989
|
+
recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved()
|
|
2990
|
+
}, this.sparseStore);
|
|
2517
2991
|
if (snapshot && typeof snapshot === "object") deserializeWorld({
|
|
2518
2992
|
entityIdManager: this.entityIdManager,
|
|
2519
2993
|
componentEntities: this.componentEntities,
|
|
2520
2994
|
entityReferences: this.entityReferences,
|
|
2521
2995
|
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
2522
|
-
setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch)
|
|
2996
|
+
setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch)
|
|
2523
2997
|
}, snapshot);
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2998
|
+
const execCtx = {
|
|
2999
|
+
componentEntities: this.componentEntities,
|
|
3000
|
+
entityReferences: this.entityReferences,
|
|
3001
|
+
hooks: this.hooks,
|
|
3002
|
+
entityToArchetype: this.entityToArchetype,
|
|
3003
|
+
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
3004
|
+
sparseStore: this.sparseStore,
|
|
3005
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
3006
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
3007
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
3008
|
+
destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
|
|
3009
|
+
incrementMigrations: () => this.debugStats.incrementMigrations(),
|
|
3010
|
+
triggerLifecycleHooks,
|
|
3011
|
+
triggerRemoveHooksForEntityDeletion
|
|
3012
|
+
};
|
|
3013
|
+
this.commandExecutor = new CommandExecutor(execCtx);
|
|
3014
|
+
this.commandBuffer = new CommandBuffer((entityId, commands) => this.commandExecutor.executeEntityCommands(entityId, commands));
|
|
2527
3015
|
}
|
|
2528
3016
|
/**
|
|
2529
3017
|
* Creates a new entity.
|
|
@@ -2619,64 +3107,13 @@ var World = class {
|
|
|
2619
3107
|
if (this.componentEntities.exists(entityId)) return true;
|
|
2620
3108
|
return this.entityToArchetype.has(entityId);
|
|
2621
3109
|
}
|
|
2622
|
-
assertEntityExists(entityId, label) {
|
|
2623
|
-
if (!this.exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
|
|
2624
|
-
}
|
|
2625
|
-
assertComponentTypeValid(componentType) {
|
|
2626
|
-
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2627
|
-
}
|
|
2628
|
-
assertSetComponentTypeValid(componentType) {
|
|
2629
|
-
const detailedType = getDetailedIdType(componentType);
|
|
2630
|
-
if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2631
|
-
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
2632
|
-
}
|
|
2633
|
-
resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent) {
|
|
2634
|
-
if (maybeComponent === void 0 && componentTypeOrComponent !== void 0) {
|
|
2635
|
-
const detailedType = getDetailedIdType(entityId);
|
|
2636
|
-
if (detailedType.type === "component" || detailedType.type === "component-relation") {
|
|
2637
|
-
const componentId = entityId;
|
|
2638
|
-
this.assertEntityExists(componentId, "Component entity");
|
|
2639
|
-
this.assertSetComponentTypeValid(componentId);
|
|
2640
|
-
return {
|
|
2641
|
-
entityId: componentId,
|
|
2642
|
-
componentType: componentId,
|
|
2643
|
-
component: componentTypeOrComponent
|
|
2644
|
-
};
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
const targetEntityId = entityId;
|
|
2648
|
-
const componentType = componentTypeOrComponent;
|
|
2649
|
-
this.assertEntityExists(targetEntityId, "Entity");
|
|
2650
|
-
this.assertSetComponentTypeValid(componentType);
|
|
2651
|
-
return {
|
|
2652
|
-
entityId: targetEntityId,
|
|
2653
|
-
componentType,
|
|
2654
|
-
component: maybeComponent
|
|
2655
|
-
};
|
|
2656
|
-
}
|
|
2657
|
-
resolveRemoveOperation(entityId, componentType) {
|
|
2658
|
-
if (componentType === void 0) {
|
|
2659
|
-
const componentId = entityId;
|
|
2660
|
-
this.assertEntityExists(componentId, "Component entity");
|
|
2661
|
-
return {
|
|
2662
|
-
entityId: componentId,
|
|
2663
|
-
componentType: componentId
|
|
2664
|
-
};
|
|
2665
|
-
}
|
|
2666
|
-
const targetEntityId = entityId;
|
|
2667
|
-
this.assertEntityExists(targetEntityId, "Entity");
|
|
2668
|
-
this.assertComponentTypeValid(componentType);
|
|
2669
|
-
return {
|
|
2670
|
-
entityId: targetEntityId,
|
|
2671
|
-
componentType
|
|
2672
|
-
};
|
|
2673
|
-
}
|
|
2674
3110
|
set(entityId, componentTypeOrComponent, maybeComponent) {
|
|
2675
|
-
const { entityId: targetEntityId, componentType, component } =
|
|
3111
|
+
const { entityId: targetEntityId, componentType, component, deprecatedSingletonShorthand } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, arguments.length, (id) => this.exists(id));
|
|
3112
|
+
if (deprecatedSingletonShorthand) console.warn(World.DEPRECATED_SINGLETON_SET_SHORTHAND_WARNING);
|
|
2676
3113
|
this.commandBuffer.set(targetEntityId, componentType, component);
|
|
2677
3114
|
}
|
|
2678
3115
|
remove(entityId, componentType) {
|
|
2679
|
-
const { entityId: targetEntityId, componentType: targetComponentType } =
|
|
3116
|
+
const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(entityId, componentType, (id) => this.exists(id));
|
|
2680
3117
|
this.commandBuffer.remove(targetEntityId, targetComponentType);
|
|
2681
3118
|
}
|
|
2682
3119
|
/**
|
|
@@ -2693,6 +3130,30 @@ var World = class {
|
|
|
2693
3130
|
delete(entityId) {
|
|
2694
3131
|
this.commandBuffer.delete(entityId);
|
|
2695
3132
|
}
|
|
3133
|
+
/**
|
|
3134
|
+
* Returns an explicit handle for a singleton component (component-as-entity).
|
|
3135
|
+
*
|
|
3136
|
+
* This is the preferred API for singleton components.
|
|
3137
|
+
*
|
|
3138
|
+
* @example
|
|
3139
|
+
* const config = world.singleton(GlobalConfig);
|
|
3140
|
+
* config.set({ debug: true });
|
|
3141
|
+
* world.sync();
|
|
3142
|
+
* console.log(config.get());
|
|
3143
|
+
*/
|
|
3144
|
+
singleton(componentId) {
|
|
3145
|
+
assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
|
|
3146
|
+
assertSetComponentTypeValid(componentId);
|
|
3147
|
+
return new SingletonHandle(componentId, {
|
|
3148
|
+
has: () => this.componentEntities.hasSingleton(componentId),
|
|
3149
|
+
get: () => this.get(componentId),
|
|
3150
|
+
getOptional: () => this.getOptional(componentId),
|
|
3151
|
+
remove: () => this.commandBuffer.remove(componentId, componentId),
|
|
3152
|
+
set: (value) => {
|
|
3153
|
+
this.commandBuffer.set(componentId, componentId, value);
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
2696
3157
|
has(entityId, componentType) {
|
|
2697
3158
|
if (componentType === void 0) {
|
|
2698
3159
|
const componentId = entityId;
|
|
@@ -2763,7 +3224,7 @@ var World = class {
|
|
|
2763
3224
|
* // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
|
|
2764
3225
|
*/
|
|
2765
3226
|
getRelationTargets(entityId, relationComp) {
|
|
2766
|
-
|
|
3227
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
2767
3228
|
const wildcard = relation(relationComp, "*");
|
|
2768
3229
|
if (this.componentEntities.exists(entityId)) return this.componentEntities.getWildcard(entityId, wildcard);
|
|
2769
3230
|
return this.get(entityId, wildcard);
|
|
@@ -2793,7 +3254,7 @@ var World = class {
|
|
|
2793
3254
|
* given base component.
|
|
2794
3255
|
*/
|
|
2795
3256
|
hasRelation(entityId, relationComp, targetId) {
|
|
2796
|
-
|
|
3257
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
2797
3258
|
if (targetId !== void 0) {
|
|
2798
3259
|
const specific = relation(relationComp, targetId);
|
|
2799
3260
|
return this.has(entityId, specific);
|
|
@@ -2804,7 +3265,7 @@ var World = class {
|
|
|
2804
3265
|
* Returns the number of relations of the given base component held by the entity.
|
|
2805
3266
|
*/
|
|
2806
3267
|
countRelations(entityId, relationComp) {
|
|
2807
|
-
|
|
3268
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
2808
3269
|
return this.getRelationTargets(entityId, relationComp).length;
|
|
2809
3270
|
}
|
|
2810
3271
|
/**
|
|
@@ -2951,60 +3412,7 @@ var World = class {
|
|
|
2951
3412
|
* This is intended for development/debugging and leak detection.
|
|
2952
3413
|
*/
|
|
2953
3414
|
createDebugStatsCollector(callback) {
|
|
2954
|
-
this.
|
|
2955
|
-
return { [Symbol.dispose]: () => {
|
|
2956
|
-
this._debugCollectors.delete(callback);
|
|
2957
|
-
} };
|
|
2958
|
-
}
|
|
2959
|
-
_resetDebugActivityCounters() {
|
|
2960
|
-
this._debugMigrations = 0;
|
|
2961
|
-
this._debugArchetypesCreated = 0;
|
|
2962
|
-
this._debugArchetypesRemoved = 0;
|
|
2963
|
-
debugHookExecutionCounter.value = 0;
|
|
2964
|
-
}
|
|
2965
|
-
_deliverDebugStats(timings) {
|
|
2966
|
-
const entityCount = this.entityToArchetype.size;
|
|
2967
|
-
let emptyArchetypes = 0;
|
|
2968
|
-
for (const arch of this.archetypes) if (arch.size === 0) emptyArchetypes++;
|
|
2969
|
-
let archetypesByComponentSize = 0;
|
|
2970
|
-
for (const set of this.archetypesByComponent.values()) archetypesByComponentSize += set.size;
|
|
2971
|
-
const stats = {
|
|
2972
|
-
timestamps: {
|
|
2973
|
-
syncStart: timings.syncStart,
|
|
2974
|
-
syncEnd: timings.syncEnd,
|
|
2975
|
-
commandBufferStart: timings.commandBufferStart,
|
|
2976
|
-
commandBufferEnd: timings.commandBufferEnd
|
|
2977
|
-
},
|
|
2978
|
-
commandIterations: timings.commandIterations,
|
|
2979
|
-
entities: {
|
|
2980
|
-
total: entityCount,
|
|
2981
|
-
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
2982
|
-
nextId: this.entityIdManager.getNextId()
|
|
2983
|
-
},
|
|
2984
|
-
archetypes: {
|
|
2985
|
-
total: this.archetypes.length,
|
|
2986
|
-
empty: emptyArchetypes
|
|
2987
|
-
},
|
|
2988
|
-
queries: {
|
|
2989
|
-
cached: this.queryRegistry.cache?.size ?? 0,
|
|
2990
|
-
registered: this.queryRegistry.queries?.size ?? 0
|
|
2991
|
-
},
|
|
2992
|
-
hooks: { total: this.hooks.size },
|
|
2993
|
-
indices: {
|
|
2994
|
-
entityReferences: this.entityReferences.size,
|
|
2995
|
-
entityToReferencingArchetypes: this.entityToReferencingArchetypes.size,
|
|
2996
|
-
archetypesByComponent: archetypesByComponentSize
|
|
2997
|
-
},
|
|
2998
|
-
activity: {
|
|
2999
|
-
migrations: this._debugMigrations,
|
|
3000
|
-
hooksExecuted: debugHookExecutionCounter.value,
|
|
3001
|
-
archetypesCreated: this._debugArchetypesCreated,
|
|
3002
|
-
archetypesRemoved: this._debugArchetypesRemoved
|
|
3003
|
-
}
|
|
3004
|
-
};
|
|
3005
|
-
for (const cb of this._debugCollectors) try {
|
|
3006
|
-
cb(stats);
|
|
3007
|
-
} catch {}
|
|
3415
|
+
return this.debugStats.createCollector(callback);
|
|
3008
3416
|
}
|
|
3009
3417
|
/**
|
|
3010
3418
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
@@ -3017,19 +3425,39 @@ var World = class {
|
|
|
3017
3425
|
* world.sync(); // Apply all buffered changes
|
|
3018
3426
|
*/
|
|
3019
3427
|
sync() {
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3428
|
+
if (!this.debugStats.hasActiveCollectors()) {
|
|
3429
|
+
this.commandBuffer.execute();
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
const syncStart = performance.now();
|
|
3433
|
+
this.debugStats.resetActivity();
|
|
3434
|
+
const commandBufferStart = performance.now();
|
|
3024
3435
|
const commandIterations = this.commandBuffer.execute();
|
|
3025
|
-
const commandBufferEnd =
|
|
3026
|
-
const syncEnd =
|
|
3027
|
-
|
|
3436
|
+
const commandBufferEnd = performance.now();
|
|
3437
|
+
const syncEnd = performance.now();
|
|
3438
|
+
const entityCount = this.entityToArchetype.size;
|
|
3439
|
+
let emptyArchetypes = 0;
|
|
3440
|
+
for (const arch of this.archetypes) if (arch.size === 0) emptyArchetypes++;
|
|
3441
|
+
let archetypesByComponentSize = 0;
|
|
3442
|
+
for (const set of this.archetypesByComponent.values()) archetypesByComponentSize += set.size;
|
|
3443
|
+
this.debugStats.deliver({
|
|
3028
3444
|
syncStart,
|
|
3029
3445
|
syncEnd,
|
|
3030
3446
|
commandBufferStart,
|
|
3031
3447
|
commandBufferEnd,
|
|
3032
3448
|
commandIterations
|
|
3449
|
+
}, {
|
|
3450
|
+
entityCount,
|
|
3451
|
+
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
3452
|
+
nextId: this.entityIdManager.getNextId(),
|
|
3453
|
+
archetypeCount: this.archetypes.length,
|
|
3454
|
+
emptyArchetypes,
|
|
3455
|
+
archetypesByComponentSize,
|
|
3456
|
+
cachedQueryCount: this.queryRegistry.cache?.size ?? 0,
|
|
3457
|
+
registeredQueryCount: this.queryRegistry.queries?.size ?? 0,
|
|
3458
|
+
hookCount: this.hooks.size,
|
|
3459
|
+
entityReferencesSize: this.entityReferences.size,
|
|
3460
|
+
entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size
|
|
3033
3461
|
});
|
|
3034
3462
|
}
|
|
3035
3463
|
/**
|
|
@@ -3066,7 +3494,7 @@ var World = class {
|
|
|
3066
3494
|
createQuery(componentTypes, filter = {}) {
|
|
3067
3495
|
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
3068
3496
|
const filterKey = serializeQueryFilter(filter);
|
|
3069
|
-
const key = `${
|
|
3497
|
+
const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
|
|
3070
3498
|
return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
|
|
3071
3499
|
}
|
|
3072
3500
|
/**
|
|
@@ -3132,44 +3560,7 @@ var World = class {
|
|
|
3132
3560
|
* @internal
|
|
3133
3561
|
*/
|
|
3134
3562
|
getMatchingArchetypes(componentTypes) {
|
|
3135
|
-
|
|
3136
|
-
const regularComponents = [];
|
|
3137
|
-
const wildcardRelations = [];
|
|
3138
|
-
for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
|
|
3139
|
-
const componentId = getComponentIdFromRelationId(componentType);
|
|
3140
|
-
if (componentId !== void 0) wildcardRelations.push({
|
|
3141
|
-
componentId,
|
|
3142
|
-
relationId: componentType
|
|
3143
|
-
});
|
|
3144
|
-
} else regularComponents.push(componentType);
|
|
3145
|
-
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
3146
|
-
for (const { componentId, relationId } of wildcardRelations) {
|
|
3147
|
-
const markerSet = this.archetypesByComponent.get(relationId);
|
|
3148
|
-
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
3149
|
-
matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
3150
|
-
}
|
|
3151
|
-
return matchingArchetypes;
|
|
3152
|
-
}
|
|
3153
|
-
getArchetypesWithComponents(componentTypes) {
|
|
3154
|
-
if (componentTypes.length === 0) return [...this.archetypes];
|
|
3155
|
-
if (componentTypes.length === 1) {
|
|
3156
|
-
const set = this.archetypesByComponent.get(componentTypes[0]);
|
|
3157
|
-
return set ? Array.from(set) : [];
|
|
3158
|
-
}
|
|
3159
|
-
const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
|
|
3160
|
-
if (sets.length === 0) return [];
|
|
3161
|
-
if (sets.length < componentTypes.length) return [];
|
|
3162
|
-
const smallest = sets[0];
|
|
3163
|
-
if (sets.length === 2) {
|
|
3164
|
-
const other = sets[1];
|
|
3165
|
-
return Array.from(smallest).filter((a) => other.has(a));
|
|
3166
|
-
}
|
|
3167
|
-
let result = new Set(smallest);
|
|
3168
|
-
for (let i = 1; i < sets.length; i++) {
|
|
3169
|
-
for (const item of result) if (!sets[i].has(item)) result.delete(item);
|
|
3170
|
-
if (result.size === 0) return [];
|
|
3171
|
-
}
|
|
3172
|
-
return Array.from(result);
|
|
3563
|
+
return this.archetypeManager.getMatchingArchetypes(componentTypes);
|
|
3173
3564
|
}
|
|
3174
3565
|
query(componentTypes, includeComponents) {
|
|
3175
3566
|
const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
|
|
@@ -3183,154 +3574,14 @@ var World = class {
|
|
|
3183
3574
|
return result;
|
|
3184
3575
|
}
|
|
3185
3576
|
}
|
|
3186
|
-
executeEntityCommands(entityId, commands) {
|
|
3187
|
-
this._changeset.clear();
|
|
3188
|
-
if (this.componentEntities.exists(entityId)) {
|
|
3189
|
-
this.componentEntities.executeCommands(entityId, commands);
|
|
3190
|
-
return;
|
|
3191
|
-
}
|
|
3192
|
-
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
3193
|
-
this.destroyEntityImmediate(entityId);
|
|
3194
|
-
return;
|
|
3195
|
-
}
|
|
3196
|
-
this.applyEntityCommands(entityId, commands);
|
|
3197
|
-
}
|
|
3198
|
-
applyEntityCommands(entityId, commands) {
|
|
3199
|
-
const currentArchetype = this.entityToArchetype.get(entityId);
|
|
3200
|
-
if (!currentArchetype) return;
|
|
3201
|
-
const changeset = this._changeset;
|
|
3202
|
-
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
3203
|
-
if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
|
|
3204
|
-
});
|
|
3205
|
-
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
3206
|
-
if (this.hooks.size === 0) {
|
|
3207
|
-
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, null);
|
|
3208
|
-
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
3209
|
-
if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) this._debugMigrations++;
|
|
3210
|
-
return;
|
|
3211
|
-
}
|
|
3212
|
-
const removedComponents = /* @__PURE__ */ new Map();
|
|
3213
|
-
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.entityToArchetype, removedComponents);
|
|
3214
|
-
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
3215
|
-
if (this._debugCollectors.size > 0 && newArchetype !== currentArchetype) this._debugMigrations++;
|
|
3216
|
-
triggerLifecycleHooks(this.createHooksContext(), entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
|
|
3217
|
-
}
|
|
3218
|
-
createHooksContext() {
|
|
3219
|
-
return this._hooksCtx;
|
|
3220
|
-
}
|
|
3221
|
-
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
3222
|
-
const sourceArchetype = this.entityToArchetype.get(entityId);
|
|
3223
|
-
if (!sourceArchetype) return;
|
|
3224
|
-
const changeset = this._removeChangeset;
|
|
3225
|
-
changeset.clear();
|
|
3226
|
-
changeset.delete(componentType);
|
|
3227
|
-
maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
|
|
3228
|
-
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
3229
|
-
const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.entityToArchetype, null);
|
|
3230
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, targetEntityId);
|
|
3231
|
-
triggerLifecycleHooks(this.createHooksContext(), entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
|
|
3232
|
-
}
|
|
3233
|
-
updateEntityReferences(entityId, changeset) {
|
|
3234
|
-
for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
|
|
3235
|
-
const targetId = getTargetIdFromRelationId(componentType);
|
|
3236
|
-
untrackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
3237
|
-
} else if (componentType >= 1024) untrackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
3238
|
-
for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
|
|
3239
|
-
const targetId = getTargetIdFromRelationId(componentType);
|
|
3240
|
-
trackEntityReference(this.entityReferences, entityId, componentType, targetId);
|
|
3241
|
-
} else if (componentType >= 1024) trackEntityReference(this.entityReferences, entityId, componentType, componentType);
|
|
3242
|
-
}
|
|
3243
3577
|
ensureArchetype(componentTypes) {
|
|
3244
|
-
|
|
3245
|
-
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
3246
|
-
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
3247
|
-
}
|
|
3248
|
-
/** Add componentType to the reverse index if it contains an entity ID */
|
|
3249
|
-
addToReferencingIndex(componentType, archetype) {
|
|
3250
|
-
const detailedType = getDetailedIdType(componentType);
|
|
3251
|
-
let entityId;
|
|
3252
|
-
if (detailedType.type === "entity") entityId = componentType;
|
|
3253
|
-
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
3254
|
-
if (entityId !== void 0) {
|
|
3255
|
-
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
3256
|
-
if (!refs) {
|
|
3257
|
-
refs = /* @__PURE__ */ new Set();
|
|
3258
|
-
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
3259
|
-
}
|
|
3260
|
-
refs.add(archetype);
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
/** Remove componentType from the reverse index */
|
|
3264
|
-
removeFromReferencingIndex(componentType, archetype) {
|
|
3265
|
-
const detailedType = getDetailedIdType(componentType);
|
|
3266
|
-
let entityId;
|
|
3267
|
-
if (detailedType.type === "entity") entityId = componentType;
|
|
3268
|
-
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
3269
|
-
if (entityId !== void 0) {
|
|
3270
|
-
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
3271
|
-
if (refs) {
|
|
3272
|
-
refs.delete(archetype);
|
|
3273
|
-
if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
|
|
3274
|
-
}
|
|
3275
|
-
}
|
|
3276
|
-
}
|
|
3277
|
-
createNewArchetype(componentTypes) {
|
|
3278
|
-
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
3279
|
-
this.archetypes.push(newArchetype);
|
|
3280
|
-
if (this._debugCollectors.size > 0) this._debugArchetypesCreated++;
|
|
3281
|
-
for (const componentType of componentTypes) {
|
|
3282
|
-
let archetypes = this.archetypesByComponent.get(componentType);
|
|
3283
|
-
if (!archetypes) {
|
|
3284
|
-
archetypes = /* @__PURE__ */ new Set();
|
|
3285
|
-
this.archetypesByComponent.set(componentType, archetypes);
|
|
3286
|
-
}
|
|
3287
|
-
archetypes.add(newArchetype);
|
|
3288
|
-
this.addToReferencingIndex(componentType, newArchetype);
|
|
3289
|
-
}
|
|
3290
|
-
this.queryRegistry.onNewArchetype(newArchetype);
|
|
3291
|
-
this.updateArchetypeHookMatches(newArchetype);
|
|
3292
|
-
return newArchetype;
|
|
3293
|
-
}
|
|
3294
|
-
updateArchetypeHookMatches(archetype) {
|
|
3295
|
-
for (const entry of this.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
|
|
3296
|
-
archetype.matchingMultiHooks.add(entry);
|
|
3297
|
-
if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
archetypeMatchesHook(archetype, entry) {
|
|
3301
|
-
return entry.requiredComponents.every((c) => {
|
|
3302
|
-
if (isWildcardRelationId(c)) {
|
|
3303
|
-
if (isSparseWildcard(c)) return true;
|
|
3304
|
-
const componentId = getComponentIdFromRelationId(c);
|
|
3305
|
-
return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
|
|
3306
|
-
}
|
|
3307
|
-
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
3308
|
-
}) && matchesFilter(archetype, entry.filter);
|
|
3578
|
+
return this.archetypeManager.ensureArchetype(componentTypes);
|
|
3309
3579
|
}
|
|
3310
3580
|
cleanupArchetypesReferencingEntity(entityId) {
|
|
3311
|
-
|
|
3312
|
-
if (!refs) return;
|
|
3313
|
-
for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
|
|
3314
|
-
this.entityToReferencingArchetypes.delete(entityId);
|
|
3581
|
+
this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
|
|
3315
3582
|
}
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
if (index !== -1) {
|
|
3319
|
-
const last = this.archetypes[this.archetypes.length - 1];
|
|
3320
|
-
this.archetypes[index] = last;
|
|
3321
|
-
this.archetypes.pop();
|
|
3322
|
-
}
|
|
3323
|
-
if (this._debugCollectors.size > 0) this._debugArchetypesRemoved++;
|
|
3324
|
-
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
3325
|
-
for (const componentType of archetype.componentTypes) {
|
|
3326
|
-
const archetypes = this.archetypesByComponent.get(componentType);
|
|
3327
|
-
if (archetypes) {
|
|
3328
|
-
archetypes.delete(archetype);
|
|
3329
|
-
if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
|
|
3330
|
-
}
|
|
3331
|
-
this.removeFromReferencingIndex(componentType, archetype);
|
|
3332
|
-
}
|
|
3333
|
-
this.queryRegistry.onArchetypeRemoved(archetype);
|
|
3583
|
+
archetypeMatchesHook(archetype, entry) {
|
|
3584
|
+
return this.archetypeManager.archetypeMatchesHook(archetype, entry);
|
|
3334
3585
|
}
|
|
3335
3586
|
/**
|
|
3336
3587
|
* Serializes the entire world state to a plain JavaScript object.
|
|
@@ -3353,11 +3604,22 @@ var World = class {
|
|
|
3353
3604
|
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
3354
3605
|
* const newWorld = new World(savedData);
|
|
3355
3606
|
*/
|
|
3607
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
3608
|
+
this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
|
|
3609
|
+
}
|
|
3610
|
+
createHooksContext() {
|
|
3611
|
+
return this.commandExecutor.getHooksContext?.() ?? {
|
|
3612
|
+
multiHooks: this.hooks,
|
|
3613
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
3614
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
3615
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct)
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3356
3618
|
serialize() {
|
|
3357
|
-
return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
|
|
3619
|
+
return serializeWorld(this.archetypeManager.archetypes, this.componentEntities, this.entityIdManager);
|
|
3358
3620
|
}
|
|
3359
3621
|
};
|
|
3360
3622
|
//#endregion
|
|
3361
|
-
export {
|
|
3623
|
+
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 };
|
|
3362
3624
|
|
|
3363
3625
|
//# sourceMappingURL=world.mjs.map
|