@codehz/ecs 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/dist/builder.d.mts +59 -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 +1136 -887
- 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 +76 -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 +88 -0
- package/src/world/singleton.ts +51 -0
- package/src/world/world.ts +178 -573
package/dist/world.mjs
CHANGED
|
@@ -622,124 +622,326 @@ var EntityBuilder = class {
|
|
|
622
622
|
}
|
|
623
623
|
};
|
|
624
624
|
//#endregion
|
|
625
|
-
//#region src/
|
|
625
|
+
//#region src/world/singleton.ts
|
|
626
626
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
//#endregion
|
|
639
|
-
//#region src/utils/utils.ts
|
|
640
|
-
/**
|
|
641
|
-
* Utility functions for ECS library
|
|
642
|
-
*/
|
|
643
|
-
/**
|
|
644
|
-
* Get a value from cache or compute and cache it if not present
|
|
645
|
-
* @param cache The cache map
|
|
646
|
-
* @param key The cache key
|
|
647
|
-
* @param compute Function to compute the value if not cached (may have side effects)
|
|
648
|
-
* @returns The cached or computed value
|
|
627
|
+
* Explicit handle for a singleton component (component-as-entity).
|
|
628
|
+
*
|
|
629
|
+
* This provides an explicit and concise API for singleton components without
|
|
630
|
+
* overloading `world.set()` semantics.
|
|
631
|
+
*
|
|
632
|
+
* @example
|
|
633
|
+
* const config = world.singleton(Config);
|
|
634
|
+
* config.set({ debug: true });
|
|
635
|
+
* world.sync();
|
|
636
|
+
* console.log(config.get());
|
|
649
637
|
*/
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
638
|
+
var SingletonHandle = class {
|
|
639
|
+
componentId;
|
|
640
|
+
ops;
|
|
641
|
+
constructor(componentId, ops) {
|
|
642
|
+
this.componentId = componentId;
|
|
643
|
+
this.ops = ops;
|
|
655
644
|
}
|
|
656
|
-
|
|
657
|
-
|
|
645
|
+
has() {
|
|
646
|
+
return this.ops.has();
|
|
647
|
+
}
|
|
648
|
+
get() {
|
|
649
|
+
return this.ops.get();
|
|
650
|
+
}
|
|
651
|
+
getOptional() {
|
|
652
|
+
return this.ops.getOptional();
|
|
653
|
+
}
|
|
654
|
+
remove() {
|
|
655
|
+
this.ops.remove();
|
|
656
|
+
}
|
|
657
|
+
set(...args) {
|
|
658
|
+
this.ops.set(args[0]);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
658
661
|
//#endregion
|
|
659
|
-
//#region src/archetype/
|
|
662
|
+
//#region src/archetype/store.ts
|
|
660
663
|
/**
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
664
|
+
* Production implementation of SparseStore.
|
|
665
|
+
*
|
|
666
|
+
* Internal layout (optimized):
|
|
667
|
+
* - byComponent: baseComponentId → (entityId → RelationEntry)
|
|
668
|
+
* RelationEntry uses a single-value form for the common exclusive case (1 target),
|
|
669
|
+
* avoiding Map allocation for the vast majority of usage.
|
|
670
|
+
* - entityIndex: entityId → Set<baseComponentId>
|
|
671
|
+
* Lightweight reverse index.
|
|
665
672
|
*/
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
673
|
+
var SparseStoreImpl = class {
|
|
674
|
+
/**
|
|
675
|
+
* Primary storage, keyed by the base relation component ID.
|
|
676
|
+
*/
|
|
677
|
+
byComponent = /* @__PURE__ */ new Map();
|
|
678
|
+
/**
|
|
679
|
+
* Reverse index: which base component kinds an entity participates in.
|
|
680
|
+
* Only required to support getAllForEntity and deleteEntity efficiently.
|
|
681
|
+
* The primary storage (byComponent) is deliberately not optimized for these operations.
|
|
682
|
+
*/
|
|
683
|
+
entityIndex = /* @__PURE__ */ new Map();
|
|
684
|
+
getValue(entityId, relationType) {
|
|
685
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
686
|
+
if (componentId === void 0) return void 0;
|
|
687
|
+
const entities = this.byComponent.get(componentId);
|
|
688
|
+
if (!entities) return void 0;
|
|
689
|
+
const entry = entities.get(entityId);
|
|
690
|
+
if (!entry) return void 0;
|
|
691
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
692
|
+
if (entry.type === "single") return entry.target === targetId ? entry.data : void 0;
|
|
693
|
+
else {
|
|
694
|
+
const item = entry.targets.get(targetId);
|
|
695
|
+
return item ? item.data : void 0;
|
|
696
|
+
}
|
|
669
697
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
698
|
+
setValue(entityId, relationType, data) {
|
|
699
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
700
|
+
if (componentId === void 0) throw new Error("setValue called with a non-relation type on SparseStore");
|
|
701
|
+
let entities = this.byComponent.get(componentId);
|
|
702
|
+
if (!entities) {
|
|
703
|
+
entities = /* @__PURE__ */ new Map();
|
|
704
|
+
this.byComponent.set(componentId, entities);
|
|
705
|
+
}
|
|
706
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
707
|
+
let entry = entities.get(entityId);
|
|
708
|
+
if (!entry) {
|
|
709
|
+
entry = {
|
|
710
|
+
type: "single",
|
|
711
|
+
relationType,
|
|
712
|
+
target: targetId,
|
|
713
|
+
data
|
|
714
|
+
};
|
|
715
|
+
entities.set(entityId, entry);
|
|
716
|
+
} else if (entry.type === "single") if (entry.target === targetId) {
|
|
717
|
+
entry.data = data;
|
|
718
|
+
entry.relationType = relationType;
|
|
719
|
+
} else {
|
|
720
|
+
const targets = /* @__PURE__ */ new Map();
|
|
721
|
+
targets.set(entry.target, {
|
|
722
|
+
relationType: entry.relationType,
|
|
723
|
+
data: entry.data
|
|
724
|
+
});
|
|
725
|
+
targets.set(targetId, {
|
|
726
|
+
relationType,
|
|
727
|
+
data
|
|
728
|
+
});
|
|
729
|
+
entities.set(entityId, {
|
|
730
|
+
type: "multi",
|
|
731
|
+
targets
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
else entry.targets.set(targetId, {
|
|
735
|
+
relationType,
|
|
736
|
+
data
|
|
737
|
+
});
|
|
738
|
+
let components = this.entityIndex.get(entityId);
|
|
739
|
+
if (!components) {
|
|
740
|
+
components = /* @__PURE__ */ new Set();
|
|
741
|
+
this.entityIndex.set(entityId, components);
|
|
742
|
+
}
|
|
743
|
+
components.add(componentId);
|
|
744
|
+
}
|
|
745
|
+
deleteValue(entityId, relationType) {
|
|
746
|
+
const componentId = getComponentIdFromRelationId(relationType);
|
|
747
|
+
if (componentId === void 0) return false;
|
|
748
|
+
const entities = this.byComponent.get(componentId);
|
|
749
|
+
if (!entities) return false;
|
|
750
|
+
const entry = entities.get(entityId);
|
|
751
|
+
if (!entry) return false;
|
|
752
|
+
const targetId = getTargetIdFromRelationId(relationType);
|
|
753
|
+
let existed = false;
|
|
754
|
+
if (entry.type === "single") {
|
|
755
|
+
if (entry.target === targetId) {
|
|
756
|
+
existed = true;
|
|
757
|
+
entities.delete(entityId);
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
existed = entry.targets.delete(targetId);
|
|
761
|
+
if (entry.targets.size === 0) entities.delete(entityId);
|
|
762
|
+
else if (entry.targets.size === 1) {
|
|
763
|
+
const [first] = entry.targets.entries();
|
|
764
|
+
const [t, item] = first;
|
|
765
|
+
entities.set(entityId, {
|
|
766
|
+
type: "single",
|
|
767
|
+
relationType: item.relationType,
|
|
768
|
+
target: t,
|
|
769
|
+
data: item.data
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (!entities.has(entityId) && entities.size === 0) this.byComponent.delete(componentId);
|
|
774
|
+
const components = this.entityIndex.get(entityId);
|
|
775
|
+
if (components && !entities.has(entityId)) {
|
|
776
|
+
components.delete(componentId);
|
|
777
|
+
if (components.size === 0) this.entityIndex.delete(entityId);
|
|
778
|
+
}
|
|
779
|
+
return existed;
|
|
780
|
+
}
|
|
781
|
+
hasAnyForComponent(componentId) {
|
|
782
|
+
const entities = this.byComponent.get(componentId);
|
|
783
|
+
return entities !== void 0 && entities.size > 0;
|
|
784
|
+
}
|
|
785
|
+
getRelationsForComponent(entityId, componentId) {
|
|
786
|
+
const result = [];
|
|
787
|
+
const entities = this.byComponent.get(componentId);
|
|
788
|
+
if (!entities) return result;
|
|
789
|
+
const entry = entities.get(entityId);
|
|
790
|
+
if (!entry) return result;
|
|
791
|
+
if (entry.type === "single") result.push([entry.target, entry.data]);
|
|
792
|
+
else for (const [target, item] of entry.targets) result.push([target, item.data]);
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
getAllForEntity(entityId) {
|
|
796
|
+
const components = this.entityIndex.get(entityId);
|
|
797
|
+
if (!components || components.size === 0) return [];
|
|
798
|
+
const result = [];
|
|
799
|
+
for (const componentId of components) {
|
|
800
|
+
const entry = this.byComponent.get(componentId)?.get(entityId);
|
|
801
|
+
if (entry) if (entry.type === "single") result.push([entry.relationType, entry.data]);
|
|
802
|
+
else for (const item of entry.targets.values()) result.push([item.relationType, item.data]);
|
|
803
|
+
}
|
|
804
|
+
return result;
|
|
805
|
+
}
|
|
806
|
+
deleteEntity(entityId) {
|
|
807
|
+
const components = this.entityIndex.get(entityId);
|
|
808
|
+
if (!components) return;
|
|
809
|
+
for (const componentId of components) {
|
|
810
|
+
const entities = this.byComponent.get(componentId);
|
|
811
|
+
if (entities) {
|
|
812
|
+
entities.delete(entityId);
|
|
813
|
+
if (entities.size === 0) this.byComponent.delete(componentId);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
this.entityIndex.delete(entityId);
|
|
817
|
+
}
|
|
818
|
+
getAllForEntities(entityIds) {
|
|
819
|
+
const result = /* @__PURE__ */ new Map();
|
|
820
|
+
for (const eid of entityIds) {
|
|
821
|
+
const data = this.getAllForEntity(eid);
|
|
822
|
+
if (data.length > 0) result.set(eid, data);
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/commands/buffer.ts
|
|
691
829
|
/**
|
|
692
|
-
*
|
|
830
|
+
* Maximum number of command buffer execution iterations to prevent infinite loops
|
|
693
831
|
*/
|
|
694
|
-
|
|
695
|
-
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
696
|
-
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
697
|
-
}
|
|
832
|
+
const MAX_COMMAND_ITERATIONS = 100;
|
|
698
833
|
/**
|
|
699
|
-
*
|
|
700
|
-
* Receives the SparseStore directly for efficient per-component lookups.
|
|
834
|
+
* Command buffer for deferred structural changes
|
|
701
835
|
*/
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
836
|
+
var CommandBuffer = class {
|
|
837
|
+
commands = [];
|
|
838
|
+
swapBuffer = [];
|
|
839
|
+
/** Reusable map to group commands by entity, avoids per-sync allocations */
|
|
840
|
+
entityCommands = /* @__PURE__ */ new Map();
|
|
841
|
+
executeEntityCommands;
|
|
842
|
+
/**
|
|
843
|
+
* Create a command buffer with an executor function
|
|
844
|
+
*/
|
|
845
|
+
constructor(executeEntityCommands) {
|
|
846
|
+
this.executeEntityCommands = executeEntityCommands;
|
|
709
847
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
848
|
+
set(entityId, componentType, component) {
|
|
849
|
+
this.commands.push({
|
|
850
|
+
type: "set",
|
|
851
|
+
entityId,
|
|
852
|
+
componentType,
|
|
853
|
+
component
|
|
854
|
+
});
|
|
713
855
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Remove a component from an entity (deferred)
|
|
858
|
+
*/
|
|
859
|
+
remove(entityId, componentType) {
|
|
860
|
+
this.commands.push({
|
|
861
|
+
type: "delete",
|
|
862
|
+
entityId,
|
|
863
|
+
componentType
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Destroy an entity (deferred)
|
|
868
|
+
*/
|
|
869
|
+
delete(entityId) {
|
|
870
|
+
this.commands.push({
|
|
871
|
+
type: "destroy",
|
|
872
|
+
entityId
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Execute all commands and clear the buffer.
|
|
877
|
+
* Returns the number of iterations performed (for debug stats).
|
|
878
|
+
*/
|
|
879
|
+
execute() {
|
|
880
|
+
let iterations = 0;
|
|
881
|
+
while (this.commands.length > 0) {
|
|
882
|
+
if (iterations >= MAX_COMMAND_ITERATIONS) throw new Error("Command execution exceeded maximum iterations, possible infinite loop");
|
|
883
|
+
iterations++;
|
|
884
|
+
const currentCommands = this.commands;
|
|
885
|
+
this.commands = this.swapBuffer;
|
|
886
|
+
const entityCommands = this.entityCommands;
|
|
887
|
+
for (const cmd of currentCommands) {
|
|
888
|
+
const existing = entityCommands.get(cmd.entityId);
|
|
889
|
+
if (existing !== void 0) existing.push(cmd);
|
|
890
|
+
else entityCommands.set(cmd.entityId, [cmd]);
|
|
891
|
+
}
|
|
892
|
+
currentCommands.length = 0;
|
|
893
|
+
this.swapBuffer = currentCommands;
|
|
894
|
+
for (const [entityId, commands] of entityCommands) this.executeEntityCommands(entityId, commands);
|
|
895
|
+
entityCommands.clear();
|
|
718
896
|
}
|
|
719
|
-
return;
|
|
897
|
+
return iterations;
|
|
720
898
|
}
|
|
721
|
-
|
|
899
|
+
/**
|
|
900
|
+
* Get current commands (for testing)
|
|
901
|
+
*/
|
|
902
|
+
getCommands() {
|
|
903
|
+
return [...this.commands];
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Clear all commands
|
|
907
|
+
*/
|
|
908
|
+
clear() {
|
|
909
|
+
this.commands = [];
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/types/index.ts
|
|
914
|
+
function isOptionalEntityId(type) {
|
|
915
|
+
return typeof type === "object" && type !== null && "optional" in type;
|
|
722
916
|
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/component/type-utils.ts
|
|
723
919
|
/**
|
|
724
|
-
*
|
|
920
|
+
* Normalize component type collections into a stable ascending order.
|
|
921
|
+
* This keeps cache keys and archetype signatures deterministic.
|
|
725
922
|
*/
|
|
726
|
-
function
|
|
727
|
-
|
|
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;
|
|
923
|
+
function normalizeComponentTypes(componentTypes) {
|
|
924
|
+
return [...componentTypes].sort((a, b) => a - b);
|
|
734
925
|
}
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/utils/utils.ts
|
|
735
928
|
/**
|
|
736
|
-
*
|
|
929
|
+
* Utility functions for ECS library
|
|
737
930
|
*/
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
931
|
+
/**
|
|
932
|
+
* Get a value from cache or compute and cache it if not present
|
|
933
|
+
* @param cache The cache map
|
|
934
|
+
* @param key The cache key
|
|
935
|
+
* @param compute Function to compute the value if not cached (may have side effects)
|
|
936
|
+
* @returns The cached or computed value
|
|
937
|
+
*/
|
|
938
|
+
function getOrCompute(cache, key, compute) {
|
|
939
|
+
let value = cache.get(key);
|
|
940
|
+
if (value === void 0) {
|
|
941
|
+
value = compute();
|
|
942
|
+
cache.set(key, value);
|
|
943
|
+
}
|
|
944
|
+
return value;
|
|
743
945
|
}
|
|
744
946
|
//#endregion
|
|
745
947
|
//#region src/archetype/archetype.ts
|
|
@@ -961,420 +1163,198 @@ var Archetype = class {
|
|
|
961
1163
|
}
|
|
962
1164
|
if (this.sparseRelations.getValue(entityId, componentType) !== void 0 || this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return this.sparseRelations.getValue(entityId, componentType);
|
|
963
1165
|
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
|
-
}
|
|
1166
|
+
}
|
|
1167
|
+
getOptional(entityId, componentType) {
|
|
1168
|
+
const index = this.entityToIndex.get(entityId);
|
|
1169
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1170
|
+
if (this.componentTypeSet.has(componentType)) {
|
|
1171
|
+
const data = this.getComponentData(componentType)[index];
|
|
1172
|
+
if (data === MISSING_COMPONENT) return void 0;
|
|
1173
|
+
return { value: data };
|
|
1181
1174
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1175
|
+
const value = this.sparseRelations.getValue(entityId, componentType);
|
|
1176
|
+
if (value !== void 0) return { value };
|
|
1177
|
+
if (this.sparseRelations.getAllForEntity(entityId).some(([t]) => t === componentType)) return { value: this.sparseRelations.getValue(entityId, componentType) };
|
|
1178
|
+
}
|
|
1179
|
+
set(entityId, componentType, data) {
|
|
1180
|
+
const index = this.entityToIndex.get(entityId);
|
|
1181
|
+
if (index === void 0) throw new Error(`Entity ${entityId} is not in this archetype`);
|
|
1182
|
+
if (this.componentData.has(componentType)) {
|
|
1183
|
+
this.getComponentData(componentType)[index] = data;
|
|
1184
|
+
return;
|
|
1187
1185
|
}
|
|
1188
|
-
|
|
1186
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1187
|
+
if (isRelationType(detailedType) && isSparseComponent(detailedType.componentId)) {
|
|
1188
|
+
this.sparseRelations.setValue(entityId, componentType, data);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1189
1192
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
return entities !== void 0 && entities.size > 0;
|
|
1193
|
+
getEntities() {
|
|
1194
|
+
return this.entities;
|
|
1193
1195
|
}
|
|
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;
|
|
1196
|
+
getEntityToIndexMap() {
|
|
1197
|
+
return this.entityToIndex;
|
|
1203
1198
|
}
|
|
1204
|
-
|
|
1205
|
-
const
|
|
1206
|
-
if (!
|
|
1207
|
-
|
|
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;
|
|
1199
|
+
getComponentData(componentType) {
|
|
1200
|
+
const data = this.componentData.get(componentType);
|
|
1201
|
+
if (!data) throw new Error(`Component type ${componentType} is not in this archetype`);
|
|
1202
|
+
return data;
|
|
1214
1203
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
if (!components) return;
|
|
1218
|
-
for (const componentId of components) {
|
|
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);
|
|
1204
|
+
getOptionalComponentData(componentType) {
|
|
1205
|
+
return this.componentData.get(componentType);
|
|
1226
1206
|
}
|
|
1227
|
-
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1207
|
+
getCachedComponentDataSources(componentTypes) {
|
|
1208
|
+
const cacheKey = buildCacheKey(componentTypes);
|
|
1209
|
+
return getOrCompute(this.componentDataSourcesCache, cacheKey, () => componentTypes.map((compType) => this.getComponentDataSource(compType)));
|
|
1210
|
+
}
|
|
1211
|
+
getComponentDataSource(compType) {
|
|
1212
|
+
const optional = isOptionalEntityId(compType);
|
|
1213
|
+
const actualType = optional ? compType.optional : compType;
|
|
1214
|
+
if (getIdType(actualType) === "wildcard-relation") {
|
|
1215
|
+
const componentId = getComponentIdFromRelationId(actualType);
|
|
1216
|
+
return getWildcardRelationDataSource(this.componentTypes, componentId, optional);
|
|
1232
1217
|
}
|
|
1233
|
-
return
|
|
1218
|
+
return optional ? this.getOptionalComponentData(actualType) : this.getComponentData(actualType);
|
|
1234
1219
|
}
|
|
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;
|
|
1220
|
+
buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entityId) {
|
|
1221
|
+
return componentDataSources.map((dataSource, i) => buildSingleComponent(componentTypes[i], dataSource, entityIndex, entityId, (type) => this.getComponentData(type), this.sparseRelations));
|
|
1256
1222
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
componentType,
|
|
1262
|
-
component
|
|
1263
|
-
});
|
|
1223
|
+
getEntitiesWithComponents(componentTypes) {
|
|
1224
|
+
const result = [];
|
|
1225
|
+
this.appendEntitiesWithComponents(componentTypes, result);
|
|
1226
|
+
return result;
|
|
1264
1227
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
entityId,
|
|
1272
|
-
componentType
|
|
1228
|
+
appendEntitiesWithComponents(componentTypes, result) {
|
|
1229
|
+
this.forEachWithComponents(componentTypes, (entity, ...components) => {
|
|
1230
|
+
result.push({
|
|
1231
|
+
entity,
|
|
1232
|
+
components
|
|
1233
|
+
});
|
|
1273
1234
|
});
|
|
1274
1235
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
entityId
|
|
1282
|
-
});
|
|
1236
|
+
*iterateWithComponents(componentTypes) {
|
|
1237
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1238
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1239
|
+
const entity = this.entities[entityIndex];
|
|
1240
|
+
yield [entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity)];
|
|
1241
|
+
}
|
|
1283
1242
|
}
|
|
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();
|
|
1243
|
+
forEachWithComponents(componentTypes, callback) {
|
|
1244
|
+
const componentDataSources = this.getCachedComponentDataSources(componentTypes);
|
|
1245
|
+
for (let entityIndex = 0; entityIndex < this.entities.length; entityIndex++) {
|
|
1246
|
+
const entity = this.entities[entityIndex];
|
|
1247
|
+
callback(entity, ...this.buildComponentsForIndex(componentTypes, componentDataSources, entityIndex, entity));
|
|
1305
1248
|
}
|
|
1306
|
-
return iterations;
|
|
1307
1249
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1250
|
+
forEach(callback) {
|
|
1251
|
+
for (let i = 0; i < this.entities.length; i++) {
|
|
1252
|
+
const entity = this.entities[i];
|
|
1253
|
+
const components = /* @__PURE__ */ new Map();
|
|
1254
|
+
for (const componentType of this.componentTypes) {
|
|
1255
|
+
const data = this.getComponentData(componentType)[i];
|
|
1256
|
+
components.set(componentType, data === MISSING_COMPONENT ? void 0 : data);
|
|
1257
|
+
}
|
|
1258
|
+
const sparseTuples = this.sparseRelations.getAllForEntity(entity);
|
|
1259
|
+
for (const [componentType, data] of sparseTuples) components.set(componentType, data);
|
|
1260
|
+
callback(entity, components);
|
|
1261
|
+
}
|
|
1313
1262
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1263
|
+
hasRelationWithComponentId(componentId) {
|
|
1264
|
+
for (const componentType of this.componentTypes) {
|
|
1265
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1266
|
+
if (isRelationType(detailedType) && detailedType.componentId === componentId) return true;
|
|
1267
|
+
}
|
|
1268
|
+
for (const entityId of this.entities) if (this.sparseRelations.getRelationsForComponent(entityId, componentId).length > 0) return true;
|
|
1269
|
+
return false;
|
|
1319
1270
|
}
|
|
1320
1271
|
};
|
|
1321
1272
|
//#endregion
|
|
1322
|
-
//#region src/
|
|
1273
|
+
//#region src/archetype/helpers.ts
|
|
1323
1274
|
/**
|
|
1324
|
-
*
|
|
1275
|
+
* Check if a components map has any wildcard relations matching a component ID
|
|
1276
|
+
* @param components - Component entity's components map
|
|
1277
|
+
* @param wildcardComponentId - The component ID to match
|
|
1278
|
+
* @returns True if at least one matching relation exists
|
|
1325
1279
|
*/
|
|
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);
|
|
1280
|
+
function hasWildcardRelation(components, wildcardComponentId) {
|
|
1281
|
+
for (const relId of components.keys()) if (isRelationId(relId)) {
|
|
1282
|
+
if (getComponentIdFromRelationId(relId) === wildcardComponentId) return true;
|
|
1342
1283
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Check if a detailed type represents a relation (entity or component)
|
|
1288
|
+
*/
|
|
1289
|
+
function isRelationType(detailedType) {
|
|
1290
|
+
return detailedType.type === "entity-relation" || detailedType.type === "component-relation";
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Check if a component type matches a given component ID for relations
|
|
1294
|
+
*/
|
|
1295
|
+
function matchesRelationComponentId(componentType, componentId) {
|
|
1296
|
+
const detailedType = getDetailedIdType(componentType);
|
|
1297
|
+
return isRelationType(detailedType) && detailedType.componentId === componentId;
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Build cache key for component types
|
|
1301
|
+
*/
|
|
1302
|
+
function buildCacheKey(componentTypes) {
|
|
1303
|
+
return componentTypes.map((id) => isOptionalEntityId(id) ? `opt(${id.optional})` : `${id}`).join(",");
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* Get data source for wildcard relations from component types
|
|
1307
|
+
*/
|
|
1308
|
+
function getWildcardRelationDataSource(componentTypes, componentId, optional) {
|
|
1309
|
+
const matchingRelations = componentTypes.filter((ct) => matchesRelationComponentId(ct, componentId));
|
|
1310
|
+
return optional ? matchingRelations.length > 0 ? matchingRelations : void 0 : matchingRelations;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Build wildcard relation value from matching relations.
|
|
1314
|
+
* Receives the SparseStore directly for efficient per-component lookups.
|
|
1315
|
+
*/
|
|
1316
|
+
function buildWildcardRelationValue(wildcardRelationType, matchingRelations, getDataAtIndex, sparseStore, entityId, optional) {
|
|
1317
|
+
const relations = [];
|
|
1318
|
+
const targetComponentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
1319
|
+
for (const relType of matchingRelations || []) {
|
|
1320
|
+
const data = getDataAtIndex(relType);
|
|
1321
|
+
const targetId = getTargetIdFromRelationId(relType);
|
|
1322
|
+
relations.push([targetId, data === MISSING_COMPONENT ? void 0 : data]);
|
|
1348
1323
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
clear() {
|
|
1353
|
-
this.adds.clear();
|
|
1354
|
-
this.removes.clear();
|
|
1324
|
+
if (targetComponentId !== void 0) {
|
|
1325
|
+
const dfMatches = sparseStore.getRelationsForComponent(entityId, targetComponentId);
|
|
1326
|
+
for (const m of dfMatches) relations.push(m);
|
|
1355
1327
|
}
|
|
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);
|
|
1328
|
+
if (relations.length === 0) {
|
|
1329
|
+
if (!optional) {
|
|
1330
|
+
const componentId = getComponentIdFromRelationId(wildcardRelationType);
|
|
1331
|
+
throw new Error(`No matching relations found for mandatory wildcard relation component ${componentId} on entity ${entityId}`);
|
|
1367
1332
|
}
|
|
1333
|
+
return;
|
|
1368
1334
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1335
|
+
return optional ? { value: relations } : relations;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Build regular component value from data source
|
|
1339
|
+
*/
|
|
1340
|
+
function buildRegularComponentValue(dataSource, entityIndex, optional) {
|
|
1341
|
+
if (dataSource === void 0) {
|
|
1342
|
+
if (optional) return void 0;
|
|
1343
|
+
throw new Error(`Component data not found for mandatory component type`);
|
|
1376
1344
|
}
|
|
1377
|
-
|
|
1345
|
+
const data = dataSource[entityIndex];
|
|
1346
|
+
const result = data === MISSING_COMPONENT ? void 0 : data;
|
|
1347
|
+
return optional ? { value: result } : result;
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* Build a single component value based on its type
|
|
1351
|
+
*/
|
|
1352
|
+
function buildSingleComponent(compType, dataSource, entityIndex, entityId, getComponentData, sparseRelations) {
|
|
1353
|
+
const optional = isOptionalEntityId(compType);
|
|
1354
|
+
const actualType = optional ? compType.optional : compType;
|
|
1355
|
+
if (getIdType(actualType) === "wildcard-relation") return buildWildcardRelationValue(actualType, dataSource, (relType) => getComponentData(relType)[entityIndex], sparseRelations, entityId, optional);
|
|
1356
|
+
else return buildRegularComponentValue(dataSource, entityIndex, optional);
|
|
1357
|
+
}
|
|
1378
1358
|
//#endregion
|
|
1379
1359
|
//#region src/component/entity-store.ts
|
|
1380
1360
|
/**
|
|
@@ -1928,84 +1908,305 @@ function maybeRemoveWildcardMarker(entityId, archetype, removedComponentType, co
|
|
|
1928
1908
|
if (changeset.removes.has(otherComponentType)) continue;
|
|
1929
1909
|
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1930
1910
|
}
|
|
1931
|
-
const sparseData = archetype.getEntitySparseRelations(entityId);
|
|
1932
|
-
if (sparseData) for (const otherComponentType of sparseData.keys()) {
|
|
1933
|
-
if (otherComponentType === removedComponentType) continue;
|
|
1934
|
-
if (changeset.removes.has(otherComponentType)) continue;
|
|
1935
|
-
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1911
|
+
const sparseData = archetype.getEntitySparseRelations(entityId);
|
|
1912
|
+
if (sparseData) for (const otherComponentType of sparseData.keys()) {
|
|
1913
|
+
if (otherComponentType === removedComponentType) continue;
|
|
1914
|
+
if (changeset.removes.has(otherComponentType)) continue;
|
|
1915
|
+
if (getComponentIdFromRelationId(otherComponentType) === componentId) return;
|
|
1916
|
+
}
|
|
1917
|
+
for (const addedType of changeset.adds.keys()) {
|
|
1918
|
+
if (addedType === removedComponentType) continue;
|
|
1919
|
+
if (getComponentIdFromRelationId(addedType) === componentId) return;
|
|
1920
|
+
}
|
|
1921
|
+
changeset.delete(wildcardMarker);
|
|
1922
|
+
}
|
|
1923
|
+
function hasEntityComponent(archetype, entityId, componentType) {
|
|
1924
|
+
if (archetype.componentTypeSet.has(componentType)) return true;
|
|
1925
|
+
return archetype.getEntitySparseRelations(entityId)?.has(componentType) ?? false;
|
|
1926
|
+
}
|
|
1927
|
+
function pruneMissingRemovals(changeset, archetype, entityId) {
|
|
1928
|
+
let toPrune;
|
|
1929
|
+
for (const componentType of changeset.removes) if (!hasEntityComponent(archetype, entityId, componentType)) {
|
|
1930
|
+
if (toPrune === void 0) toPrune = [];
|
|
1931
|
+
toPrune.push(componentType);
|
|
1932
|
+
}
|
|
1933
|
+
if (toPrune !== void 0) for (const componentType of toPrune) changeset.removes.delete(componentType);
|
|
1934
|
+
}
|
|
1935
|
+
function hasArchetypeStructuralChange(changeset, currentArchetype) {
|
|
1936
|
+
for (const componentType of changeset.removes) if (!isSparseRelation(componentType) && currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1937
|
+
for (const componentType of changeset.adds.keys()) if (!isSparseRelation(componentType) && !currentArchetype.componentTypeSet.has(componentType)) return true;
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
function buildFinalRegularComponentTypes(currentArchetype, changeset) {
|
|
1941
|
+
const finalRegularTypes = new Set(currentArchetype.componentTypes);
|
|
1942
|
+
for (const componentType of changeset.removes) if (!isSparseRelation(componentType)) finalRegularTypes.delete(componentType);
|
|
1943
|
+
for (const [componentType] of changeset.adds) if (!isSparseRelation(componentType)) finalRegularTypes.add(componentType);
|
|
1944
|
+
return Array.from(finalRegularTypes);
|
|
1945
|
+
}
|
|
1946
|
+
function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
|
|
1947
|
+
pruneMissingRemovals(changeset, currentArchetype, entityId);
|
|
1948
|
+
if (hasArchetypeStructuralChange(changeset, currentArchetype)) {
|
|
1949
|
+
const finalRegularTypes = buildFinalRegularComponentTypes(currentArchetype, changeset);
|
|
1950
|
+
const newArchetype = ctx.ensureArchetype(finalRegularTypes);
|
|
1951
|
+
const currentComponents = currentArchetype.removeEntity(entityId);
|
|
1952
|
+
if (removedComponents !== null) for (const componentType of changeset.removes) removedComponents.set(componentType, currentComponents.get(componentType));
|
|
1953
|
+
newArchetype.addEntity(entityId, changeset.applyTo(currentComponents));
|
|
1954
|
+
entityToArchetype.set(entityId, newArchetype);
|
|
1955
|
+
return newArchetype;
|
|
1956
|
+
}
|
|
1957
|
+
if (removedComponents !== null) applySparseChanges(ctx.sparseStore, entityId, changeset, removedComponents);
|
|
1958
|
+
else applySparseChangesNoHooks(ctx.sparseStore, entityId, changeset);
|
|
1959
|
+
for (const [componentType, component] of changeset.adds) {
|
|
1960
|
+
if (isSparseRelation(componentType)) continue;
|
|
1961
|
+
currentArchetype.set(entityId, componentType, component);
|
|
1962
|
+
}
|
|
1963
|
+
return currentArchetype;
|
|
1964
|
+
}
|
|
1965
|
+
function applySparseChanges(sparseStore, entityId, changeset, removedComponents) {
|
|
1966
|
+
for (const componentType of changeset.removes) if (isSparseRelation(componentType)) {
|
|
1967
|
+
const removedValue = sparseStore.getValue(entityId, componentType);
|
|
1968
|
+
if (removedValue !== void 0 || sparseStore.getAllForEntity(entityId).some(([t]) => t === componentType)) removedComponents.set(componentType, removedValue);
|
|
1969
|
+
sparseStore.deleteValue(entityId, componentType);
|
|
1970
|
+
}
|
|
1971
|
+
for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
|
|
1972
|
+
}
|
|
1973
|
+
function applySparseChangesNoHooks(sparseStore, entityId, changeset) {
|
|
1974
|
+
for (const componentType of changeset.removes) if (isSparseRelation(componentType)) sparseStore.deleteValue(entityId, componentType);
|
|
1975
|
+
for (const [componentType, component] of changeset.adds) if (isSparseRelation(componentType)) sparseStore.setValue(entityId, componentType, component);
|
|
1976
|
+
}
|
|
1977
|
+
function filterRegularComponentTypes(componentTypes) {
|
|
1978
|
+
const regularTypes = [];
|
|
1979
|
+
for (const componentType of componentTypes) {
|
|
1980
|
+
if (isSparseWildcard(componentType)) {
|
|
1981
|
+
regularTypes.push(componentType);
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
if (isSparseRelation(componentType)) continue;
|
|
1985
|
+
regularTypes.push(componentType);
|
|
1986
|
+
}
|
|
1987
|
+
return regularTypes;
|
|
1988
|
+
}
|
|
1989
|
+
//#endregion
|
|
1990
|
+
//#region src/world/archetype-manager.ts
|
|
1991
|
+
/**
|
|
1992
|
+
* Encapsulates all archetype storage, indexing, creation, removal, and reverse
|
|
1993
|
+
* referencing logic that was previously scattered as private methods + maps
|
|
1994
|
+
* directly on the World class.
|
|
1995
|
+
*
|
|
1996
|
+
* Responsibilities:
|
|
1997
|
+
* - Archetype memoization by signature
|
|
1998
|
+
* - Component-type reverse index (archetypesByComponent)
|
|
1999
|
+
* - Entity → current Archetype map
|
|
2000
|
+
* - Reverse "who references this entity via component/relation" index
|
|
2001
|
+
* - Creation + removal with proper notifications to QueryRegistry + hook matching
|
|
2002
|
+
* - Cleanup of empty archetypes after entity cascades
|
|
2003
|
+
*
|
|
2004
|
+
* This extraction shrinks World while keeping the same behavior and hot-path characteristics.
|
|
2005
|
+
*/
|
|
2006
|
+
var ArchetypeManager = class {
|
|
2007
|
+
archetypes = [];
|
|
2008
|
+
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
2009
|
+
entityToArchetype = /* @__PURE__ */ new Map();
|
|
2010
|
+
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
2011
|
+
entityToReferencingArchetypes = /* @__PURE__ */ new Map();
|
|
2012
|
+
sparseStore;
|
|
2013
|
+
ctx;
|
|
2014
|
+
constructor(ctx, sparseStore) {
|
|
2015
|
+
this.ctx = ctx;
|
|
2016
|
+
this.sparseStore = sparseStore;
|
|
2017
|
+
}
|
|
2018
|
+
/** Primary entry point — memoized archetype creation/lookup. */
|
|
2019
|
+
ensureArchetype(componentTypes) {
|
|
2020
|
+
const sortedTypes = normalizeComponentTypes(filterRegularComponentTypes(componentTypes));
|
|
2021
|
+
const hashKey = this.createArchetypeSignature(sortedTypes);
|
|
2022
|
+
return getOrCompute(this.archetypeBySignature, hashKey, () => this.createNewArchetype(sortedTypes));
|
|
2023
|
+
}
|
|
2024
|
+
getArchetypeForEntity(entityId) {
|
|
2025
|
+
return this.entityToArchetype.get(entityId);
|
|
2026
|
+
}
|
|
2027
|
+
setEntityToArchetype(entityId, archetype) {
|
|
2028
|
+
this.entityToArchetype.set(entityId, archetype);
|
|
2029
|
+
}
|
|
2030
|
+
getMatchingArchetypes(componentTypes) {
|
|
2031
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2032
|
+
const regularComponents = [];
|
|
2033
|
+
const wildcardRelations = [];
|
|
2034
|
+
for (const componentType of componentTypes) if (isWildcardRelationId(componentType)) {
|
|
2035
|
+
const componentId = getComponentIdFromRelationId(componentType);
|
|
2036
|
+
if (componentId !== void 0) wildcardRelations.push({
|
|
2037
|
+
componentId,
|
|
2038
|
+
relationId: componentType
|
|
2039
|
+
});
|
|
2040
|
+
} else regularComponents.push(componentType);
|
|
2041
|
+
let matchingArchetypes = this.getArchetypesWithComponents(regularComponents);
|
|
2042
|
+
for (const { componentId, relationId } of wildcardRelations) {
|
|
2043
|
+
const markerSet = this.archetypesByComponent.get(relationId);
|
|
2044
|
+
const archetypesWithMarker = markerSet ? Array.from(markerSet) : [];
|
|
2045
|
+
matchingArchetypes = matchingArchetypes.length === 0 ? archetypesWithMarker : matchingArchetypes.filter((a) => markerSet?.has(a) || a.hasRelationWithComponentId(componentId));
|
|
2046
|
+
}
|
|
2047
|
+
return matchingArchetypes;
|
|
2048
|
+
}
|
|
2049
|
+
getArchetypesWithComponents(componentTypes) {
|
|
2050
|
+
if (componentTypes.length === 0) return [...this.archetypes];
|
|
2051
|
+
if (componentTypes.length === 1) {
|
|
2052
|
+
const set = this.archetypesByComponent.get(componentTypes[0]);
|
|
2053
|
+
return set ? Array.from(set) : [];
|
|
2054
|
+
}
|
|
2055
|
+
const sets = componentTypes.map((type) => this.archetypesByComponent.get(type)).filter((s) => s !== void 0 && s.size > 0).sort((a, b) => a.size - b.size);
|
|
2056
|
+
if (sets.length === 0) return [];
|
|
2057
|
+
if (sets.length < componentTypes.length) return [];
|
|
2058
|
+
const smallest = sets[0];
|
|
2059
|
+
if (sets.length === 2) {
|
|
2060
|
+
const other = sets[1];
|
|
2061
|
+
return Array.from(smallest).filter((a) => other.has(a));
|
|
2062
|
+
}
|
|
2063
|
+
let result = new Set(smallest);
|
|
2064
|
+
for (let i = 1; i < sets.length; i++) {
|
|
2065
|
+
for (const item of result) if (!sets[i].has(item)) result.delete(item);
|
|
2066
|
+
if (result.size === 0) return [];
|
|
2067
|
+
}
|
|
2068
|
+
return Array.from(result);
|
|
2069
|
+
}
|
|
2070
|
+
createArchetypeSignature(componentTypes) {
|
|
2071
|
+
return componentTypes.join(",");
|
|
2072
|
+
}
|
|
2073
|
+
/** Deduplicated version of the original pair of methods. */
|
|
2074
|
+
updateReferencingIndex(componentType, archetype, isAdd) {
|
|
2075
|
+
const detailedType = getDetailedIdType(componentType);
|
|
2076
|
+
let entityId;
|
|
2077
|
+
if (detailedType.type === "entity") entityId = componentType;
|
|
2078
|
+
else if (detailedType.type === "entity-relation") entityId = detailedType.targetId;
|
|
2079
|
+
if (entityId !== void 0) {
|
|
2080
|
+
let refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2081
|
+
if (isAdd) {
|
|
2082
|
+
if (!refs) {
|
|
2083
|
+
refs = /* @__PURE__ */ new Set();
|
|
2084
|
+
this.entityToReferencingArchetypes.set(entityId, refs);
|
|
2085
|
+
}
|
|
2086
|
+
refs.add(archetype);
|
|
2087
|
+
} else if (refs) {
|
|
2088
|
+
refs.delete(archetype);
|
|
2089
|
+
if (refs.size === 0) this.entityToReferencingArchetypes.delete(entityId);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
createNewArchetype(componentTypes) {
|
|
2094
|
+
const newArchetype = new Archetype(componentTypes, this.sparseStore);
|
|
2095
|
+
this.archetypes.push(newArchetype);
|
|
2096
|
+
this.ctx.recordArchetypeCreated?.();
|
|
2097
|
+
for (const componentType of componentTypes) {
|
|
2098
|
+
let archetypes = this.archetypesByComponent.get(componentType);
|
|
2099
|
+
if (!archetypes) {
|
|
2100
|
+
archetypes = /* @__PURE__ */ new Set();
|
|
2101
|
+
this.archetypesByComponent.set(componentType, archetypes);
|
|
2102
|
+
}
|
|
2103
|
+
archetypes.add(newArchetype);
|
|
2104
|
+
this.updateReferencingIndex(componentType, newArchetype, true);
|
|
2105
|
+
}
|
|
2106
|
+
this.ctx.queryRegistry.onNewArchetype(newArchetype);
|
|
2107
|
+
this.updateArchetypeHookMatches(newArchetype);
|
|
2108
|
+
return newArchetype;
|
|
2109
|
+
}
|
|
2110
|
+
updateArchetypeHookMatches(archetype) {
|
|
2111
|
+
for (const entry of this.ctx.hooks) if (this.archetypeMatchesHook(archetype, entry)) {
|
|
2112
|
+
archetype.matchingMultiHooks.add(entry);
|
|
2113
|
+
if (entry.matchedArchetypes) entry.matchedArchetypes.add(archetype);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
archetypeMatchesHook(archetype, entry) {
|
|
2117
|
+
return entry.requiredComponents.every((c) => {
|
|
2118
|
+
if (isWildcardRelationId(c)) {
|
|
2119
|
+
if (isSparseWildcard(c)) return true;
|
|
2120
|
+
const componentId = getComponentIdFromRelationId(c);
|
|
2121
|
+
return componentId !== void 0 && archetype.hasRelationWithComponentId(componentId);
|
|
2122
|
+
}
|
|
2123
|
+
return archetype.componentTypeSet.has(c) || isSparseRelation(c);
|
|
2124
|
+
}) && matchesFilter(archetype, entry.filter);
|
|
2125
|
+
}
|
|
2126
|
+
/** Called during cascade deletion cleanup. */
|
|
2127
|
+
cleanupArchetypesReferencingEntity(entityId) {
|
|
2128
|
+
const refs = this.entityToReferencingArchetypes.get(entityId);
|
|
2129
|
+
if (!refs) return;
|
|
2130
|
+
for (const archetype of refs) if (archetype.getEntities().length === 0) this.removeArchetype(archetype);
|
|
2131
|
+
this.entityToReferencingArchetypes.delete(entityId);
|
|
1936
2132
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (
|
|
2133
|
+
removeArchetype(archetype) {
|
|
2134
|
+
const index = this.archetypes.indexOf(archetype);
|
|
2135
|
+
if (index !== -1) {
|
|
2136
|
+
const last = this.archetypes[this.archetypes.length - 1];
|
|
2137
|
+
this.archetypes[index] = last;
|
|
2138
|
+
this.archetypes.pop();
|
|
2139
|
+
}
|
|
2140
|
+
this.ctx.recordArchetypeRemoved?.();
|
|
2141
|
+
this.archetypeBySignature.delete(this.createArchetypeSignature(archetype.componentTypes));
|
|
2142
|
+
for (const componentType of archetype.componentTypes) {
|
|
2143
|
+
const archetypes = this.archetypesByComponent.get(componentType);
|
|
2144
|
+
if (archetypes) {
|
|
2145
|
+
archetypes.delete(archetype);
|
|
2146
|
+
if (archetypes.size === 0) this.archetypesByComponent.delete(componentType);
|
|
2147
|
+
}
|
|
2148
|
+
this.updateReferencingIndex(componentType, archetype, false);
|
|
2149
|
+
}
|
|
2150
|
+
this.ctx.queryRegistry.onArchetypeRemoved(archetype);
|
|
1940
2151
|
}
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2152
|
+
};
|
|
2153
|
+
//#endregion
|
|
2154
|
+
//#region src/commands/changeset.ts
|
|
2155
|
+
/**
|
|
2156
|
+
* @internal Represents a set of component changes to be applied to an entity
|
|
2157
|
+
*/
|
|
2158
|
+
var ComponentChangeset = class {
|
|
2159
|
+
adds = /* @__PURE__ */ new Map();
|
|
2160
|
+
removes = /* @__PURE__ */ new Set();
|
|
2161
|
+
/**
|
|
2162
|
+
* Add a component to the changeset
|
|
2163
|
+
*/
|
|
2164
|
+
set(componentType, component) {
|
|
2165
|
+
this.adds.set(componentType, component);
|
|
2166
|
+
this.removes.delete(componentType);
|
|
1952
2167
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
}
|
|
1960
|
-
function buildFinalRegularComponentTypes(currentArchetype, changeset) {
|
|
1961
|
-
const finalRegularTypes = new Set(currentArchetype.componentTypes);
|
|
1962
|
-
for (const componentType of changeset.removes) if (!isSparseRelation(componentType)) finalRegularTypes.delete(componentType);
|
|
1963
|
-
for (const [componentType] of changeset.adds) if (!isSparseRelation(componentType)) finalRegularTypes.add(componentType);
|
|
1964
|
-
return Array.from(finalRegularTypes);
|
|
1965
|
-
}
|
|
1966
|
-
function applyChangeset(ctx, entityId, currentArchetype, changeset, entityToArchetype, removedComponents) {
|
|
1967
|
-
pruneMissingRemovals(changeset, currentArchetype, entityId);
|
|
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;
|
|
2168
|
+
/**
|
|
2169
|
+
* Remove a component from the changeset
|
|
2170
|
+
*/
|
|
2171
|
+
delete(componentType) {
|
|
2172
|
+
this.removes.add(componentType);
|
|
2173
|
+
this.adds.delete(componentType);
|
|
1976
2174
|
}
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2175
|
+
/**
|
|
2176
|
+
* Check if the changeset has any changes
|
|
2177
|
+
*/
|
|
2178
|
+
hasChanges() {
|
|
2179
|
+
return this.adds.size > 0 || this.removes.size > 0;
|
|
1982
2180
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
sparseStore.deleteValue(entityId, componentType);
|
|
2181
|
+
/**
|
|
2182
|
+
* Clear all changes
|
|
2183
|
+
*/
|
|
2184
|
+
clear() {
|
|
2185
|
+
this.adds.clear();
|
|
2186
|
+
this.removes.clear();
|
|
1990
2187
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
continue;
|
|
2188
|
+
/**
|
|
2189
|
+
* Merge another changeset into this one
|
|
2190
|
+
*/
|
|
2191
|
+
merge(other) {
|
|
2192
|
+
for (const [componentType, component] of other.adds) {
|
|
2193
|
+
this.adds.set(componentType, component);
|
|
2194
|
+
this.removes.delete(componentType);
|
|
2195
|
+
}
|
|
2196
|
+
for (const componentType of other.removes) {
|
|
2197
|
+
this.removes.add(componentType);
|
|
2198
|
+
this.adds.delete(componentType);
|
|
2003
2199
|
}
|
|
2004
|
-
if (isSparseRelation(componentType)) continue;
|
|
2005
|
-
regularTypes.push(componentType);
|
|
2006
2200
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2201
|
+
/**
|
|
2202
|
+
* Apply the changeset to existing components and return the final state
|
|
2203
|
+
*/
|
|
2204
|
+
applyTo(existingComponents) {
|
|
2205
|
+
for (const componentType of this.removes) existingComponents.delete(componentType);
|
|
2206
|
+
for (const [componentType, component] of this.adds) existingComponents.set(componentType, component);
|
|
2207
|
+
return existingComponents;
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2009
2210
|
//#endregion
|
|
2010
2211
|
//#region src/world/hooks.ts
|
|
2011
2212
|
/**
|
|
@@ -2310,8 +2511,272 @@ function untrackEntityReference(entityReferences, sourceEntityId, componentType,
|
|
|
2310
2511
|
*
|
|
2311
2512
|
* @internal
|
|
2312
2513
|
*/
|
|
2313
|
-
function getEntityReferences(entityReferences, targetEntityId) {
|
|
2314
|
-
return entityReferences.get(targetEntityId) ?? new MultiMap();
|
|
2514
|
+
function getEntityReferences(entityReferences, targetEntityId) {
|
|
2515
|
+
return entityReferences.get(targetEntityId) ?? new MultiMap();
|
|
2516
|
+
}
|
|
2517
|
+
//#endregion
|
|
2518
|
+
//#region src/world/command-executor.ts
|
|
2519
|
+
/**
|
|
2520
|
+
* Encapsulates the command execution pipeline, reusable changesets,
|
|
2521
|
+
* and related orchestration that was previously private methods + fields on World.
|
|
2522
|
+
*
|
|
2523
|
+
* Responsibilities:
|
|
2524
|
+
* - executeEntityCommands (routing for singletons / destroy / structural changes)
|
|
2525
|
+
* - applyEntityCommands (changeset processing + exclusive relations + apply + refs + hooks)
|
|
2526
|
+
* - removeComponentImmediate (used by cascade deletion)
|
|
2527
|
+
* - updateEntityReferences (keeps the reverse index in sync)
|
|
2528
|
+
*
|
|
2529
|
+
* This extraction significantly reduces World line count while preserving
|
|
2530
|
+
* every fast-path branch and allocation-avoidance characteristic.
|
|
2531
|
+
*/
|
|
2532
|
+
var CommandExecutor = class {
|
|
2533
|
+
ctx;
|
|
2534
|
+
_changeset = new ComponentChangeset();
|
|
2535
|
+
_removeChangeset = new ComponentChangeset();
|
|
2536
|
+
_commandCtx;
|
|
2537
|
+
_hooksCtx;
|
|
2538
|
+
constructor(ctx) {
|
|
2539
|
+
this.ctx = ctx;
|
|
2540
|
+
this._commandCtx = {
|
|
2541
|
+
sparseStore: ctx.sparseStore,
|
|
2542
|
+
ensureArchetype: ctx.ensureArchetype
|
|
2543
|
+
};
|
|
2544
|
+
this._hooksCtx = {
|
|
2545
|
+
multiHooks: ctx.hooks,
|
|
2546
|
+
has: ctx.has,
|
|
2547
|
+
get: ctx.get,
|
|
2548
|
+
getOptional: ctx.getOptional
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Entry point used by the CommandBuffer.
|
|
2553
|
+
* Routes to singleton handling, destroy fast path, or structural apply.
|
|
2554
|
+
*/
|
|
2555
|
+
executeEntityCommands(entityId, commands) {
|
|
2556
|
+
this._changeset.clear();
|
|
2557
|
+
if (this.ctx.componentEntities.exists(entityId)) {
|
|
2558
|
+
this.ctx.componentEntities.executeCommands(entityId, commands);
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
if (commands.some((cmd) => cmd.type === "destroy")) {
|
|
2562
|
+
this.ctx.destroyEntityImmediate(entityId);
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
this.applyEntityCommands(entityId, commands);
|
|
2566
|
+
}
|
|
2567
|
+
applyEntityCommands(entityId, commands) {
|
|
2568
|
+
const currentArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
2569
|
+
if (!currentArchetype) return;
|
|
2570
|
+
const changeset = this._changeset;
|
|
2571
|
+
processCommands(entityId, currentArchetype, commands, changeset, (eid, arch, compId) => {
|
|
2572
|
+
if (isExclusiveComponent(compId)) removeMatchingRelations(eid, arch, compId, changeset);
|
|
2573
|
+
});
|
|
2574
|
+
const hasStructuralChange = changeset.removes.size > 0 || changeset.adds.size > 0;
|
|
2575
|
+
if (this.ctx.hooks.size === 0) {
|
|
2576
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, null);
|
|
2577
|
+
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2578
|
+
if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const removedComponents = /* @__PURE__ */ new Map();
|
|
2582
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, currentArchetype, changeset, this.ctx.entityToArchetype, removedComponents);
|
|
2583
|
+
if (hasStructuralChange) this.updateEntityReferences(entityId, changeset);
|
|
2584
|
+
if (newArchetype !== currentArchetype) this.ctx.incrementMigrations();
|
|
2585
|
+
this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, changeset.adds, removedComponents, currentArchetype, newArchetype);
|
|
2586
|
+
}
|
|
2587
|
+
/**
|
|
2588
|
+
* Immediate (non-buffered) component removal used during cascade deletion.
|
|
2589
|
+
* Called from destroy* paths (which remain in World).
|
|
2590
|
+
*/
|
|
2591
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
2592
|
+
const sourceArchetype = this.ctx.entityToArchetype.get(entityId);
|
|
2593
|
+
if (!sourceArchetype) return;
|
|
2594
|
+
const changeset = this._removeChangeset;
|
|
2595
|
+
changeset.clear();
|
|
2596
|
+
changeset.delete(componentType);
|
|
2597
|
+
maybeRemoveWildcardMarker(entityId, sourceArchetype, componentType, getComponentIdFromRelationId(componentType), changeset);
|
|
2598
|
+
const removedComponent = sourceArchetype.get(entityId, componentType);
|
|
2599
|
+
const newArchetype = applyChangeset(this._commandCtx, entityId, sourceArchetype, changeset, this.ctx.entityToArchetype, null);
|
|
2600
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetEntityId);
|
|
2601
|
+
this.ctx.triggerLifecycleHooks(this._hooksCtx, entityId, /* @__PURE__ */ new Map(), new Map([[componentType, removedComponent]]), sourceArchetype, newArchetype);
|
|
2602
|
+
}
|
|
2603
|
+
/**
|
|
2604
|
+
* Keeps the entity reference reverse index in sync after structural changes.
|
|
2605
|
+
* Called from apply paths.
|
|
2606
|
+
*/
|
|
2607
|
+
updateEntityReferences(entityId, changeset) {
|
|
2608
|
+
for (const componentType of changeset.removes) if (isEntityRelation(componentType)) {
|
|
2609
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
2610
|
+
untrackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
2611
|
+
} else if (componentType >= 1024) untrackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
2612
|
+
for (const [componentType] of changeset.adds) if (isEntityRelation(componentType)) {
|
|
2613
|
+
const targetId = getTargetIdFromRelationId(componentType);
|
|
2614
|
+
trackEntityReference(this.ctx.entityReferences, entityId, componentType, targetId);
|
|
2615
|
+
} else if (componentType >= 1024) trackEntityReference(this.ctx.entityReferences, entityId, componentType, componentType);
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Exposed for any future direct needs (currently not required outside the executor).
|
|
2619
|
+
*/
|
|
2620
|
+
getHooksContext() {
|
|
2621
|
+
return this._hooksCtx;
|
|
2622
|
+
}
|
|
2623
|
+
};
|
|
2624
|
+
//#endregion
|
|
2625
|
+
//#region src/world/debug-stats.ts
|
|
2626
|
+
/**
|
|
2627
|
+
* Manages debug stats collectors and transient activity counters for World#sync().
|
|
2628
|
+
*
|
|
2629
|
+
* Extracted from World to shrink the main class while keeping the entire debug/observability
|
|
2630
|
+
* path isolated, zero-cost when no collectors are active, and easy to test/maintain.
|
|
2631
|
+
*
|
|
2632
|
+
* Follows the same context/callback injection style as ArchetypeManager, CommandProcessorContext,
|
|
2633
|
+
* and HooksContext to avoid tight coupling.
|
|
2634
|
+
*
|
|
2635
|
+
* All collectors receive the *exact same* stats object for a given sync (as before).
|
|
2636
|
+
* Exceptions in user callbacks are swallowed (as before).
|
|
2637
|
+
*/
|
|
2638
|
+
var DebugStatsManager = class {
|
|
2639
|
+
collectors = /* @__PURE__ */ new Set();
|
|
2640
|
+
migrations = 0;
|
|
2641
|
+
archetypesCreated = 0;
|
|
2642
|
+
archetypesRemoved = 0;
|
|
2643
|
+
/** Fast check used to arm timing + reset + counting in hot paths. */
|
|
2644
|
+
hasActiveCollectors() {
|
|
2645
|
+
return this.collectors.size > 0;
|
|
2646
|
+
}
|
|
2647
|
+
/**
|
|
2648
|
+
* Registers a collector. Returns a disposable handle (supports `using`).
|
|
2649
|
+
* Collection stops when the handle is disposed.
|
|
2650
|
+
*/
|
|
2651
|
+
createCollector(callback) {
|
|
2652
|
+
this.collectors.add(callback);
|
|
2653
|
+
return { [Symbol.dispose]: () => {
|
|
2654
|
+
this.collectors.delete(callback);
|
|
2655
|
+
} };
|
|
2656
|
+
}
|
|
2657
|
+
recordArchetypeCreated() {
|
|
2658
|
+
if (this.hasActiveCollectors()) this.archetypesCreated++;
|
|
2659
|
+
}
|
|
2660
|
+
recordArchetypeRemoved() {
|
|
2661
|
+
if (this.hasActiveCollectors()) this.archetypesRemoved++;
|
|
2662
|
+
}
|
|
2663
|
+
incrementMigrations() {
|
|
2664
|
+
if (this.hasActiveCollectors()) this.migrations++;
|
|
2665
|
+
}
|
|
2666
|
+
/** Reset all activity counters + the shared hook execution counter. Called at start of an armed sync. */
|
|
2667
|
+
resetActivity() {
|
|
2668
|
+
this.migrations = 0;
|
|
2669
|
+
this.archetypesCreated = 0;
|
|
2670
|
+
this.archetypesRemoved = 0;
|
|
2671
|
+
debugHookExecutionCounter.value = 0;
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Build and deliver a SyncDebugStats payload to every active collector.
|
|
2675
|
+
* World supplies the pre-computed snapshot numbers (keeps debug manager decoupled from
|
|
2676
|
+
* internal World maps/registries while preserving exact original stats shape and values).
|
|
2677
|
+
*/
|
|
2678
|
+
deliver(timings, data) {
|
|
2679
|
+
const stats = {
|
|
2680
|
+
timestamps: {
|
|
2681
|
+
syncStart: timings.syncStart,
|
|
2682
|
+
syncEnd: timings.syncEnd,
|
|
2683
|
+
commandBufferStart: timings.commandBufferStart,
|
|
2684
|
+
commandBufferEnd: timings.commandBufferEnd
|
|
2685
|
+
},
|
|
2686
|
+
commandIterations: timings.commandIterations,
|
|
2687
|
+
entities: {
|
|
2688
|
+
total: data.entityCount,
|
|
2689
|
+
freelistSize: data.freelistSize,
|
|
2690
|
+
nextId: data.nextId
|
|
2691
|
+
},
|
|
2692
|
+
archetypes: {
|
|
2693
|
+
total: data.archetypeCount,
|
|
2694
|
+
empty: data.emptyArchetypes
|
|
2695
|
+
},
|
|
2696
|
+
queries: {
|
|
2697
|
+
cached: data.cachedQueryCount,
|
|
2698
|
+
registered: data.registeredQueryCount
|
|
2699
|
+
},
|
|
2700
|
+
hooks: { total: data.hookCount },
|
|
2701
|
+
indices: {
|
|
2702
|
+
entityReferences: data.entityReferencesSize,
|
|
2703
|
+
entityToReferencingArchetypes: data.entityToReferencingArchetypesSize,
|
|
2704
|
+
archetypesByComponent: data.archetypesByComponentSize
|
|
2705
|
+
},
|
|
2706
|
+
activity: {
|
|
2707
|
+
migrations: this.migrations,
|
|
2708
|
+
hooksExecuted: debugHookExecutionCounter.value,
|
|
2709
|
+
archetypesCreated: this.archetypesCreated,
|
|
2710
|
+
archetypesRemoved: this.archetypesRemoved
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
for (const cb of this.collectors) try {
|
|
2714
|
+
cb(stats);
|
|
2715
|
+
} catch {}
|
|
2716
|
+
}
|
|
2717
|
+
};
|
|
2718
|
+
//#endregion
|
|
2719
|
+
//#region src/world/operations.ts
|
|
2720
|
+
/**
|
|
2721
|
+
* Validation and overload-resolution helpers extracted from World.
|
|
2722
|
+
*
|
|
2723
|
+
* These were previously private methods on World. Moving them reduces line count
|
|
2724
|
+
* in the core class with almost zero coupling (the only dep is a liveness predicate
|
|
2725
|
+
* for assertEntityExists, supplied by the caller).
|
|
2726
|
+
*
|
|
2727
|
+
* Pure type checks (assert*TypeValid) and the resolve* helpers for set/remove
|
|
2728
|
+
* overloads live here.
|
|
2729
|
+
*/
|
|
2730
|
+
/**
|
|
2731
|
+
* Assert that an entity (or component-entity) is alive in the world.
|
|
2732
|
+
* The caller supplies the liveness check (World.exists or equivalent) to keep
|
|
2733
|
+
* this module free of direct references to stores.
|
|
2734
|
+
*/
|
|
2735
|
+
function assertEntityExists(entityId, label, exists) {
|
|
2736
|
+
if (!exists(entityId)) throw new Error(`${label} ${entityId} does not exist`);
|
|
2737
|
+
}
|
|
2738
|
+
function assertComponentTypeValid(componentType) {
|
|
2739
|
+
if (getDetailedIdType(componentType).type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2740
|
+
}
|
|
2741
|
+
function assertSetComponentTypeValid(componentType) {
|
|
2742
|
+
const detailedType = getDetailedIdType(componentType);
|
|
2743
|
+
if (detailedType.type === "invalid") throw new Error(`Invalid component type: ${componentType}`);
|
|
2744
|
+
if (detailedType.type === "wildcard-relation") throw new Error(`Cannot directly add wildcard relation components: ${componentType}`);
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Resolve the (entity, componentType, value) for a set() call.
|
|
2748
|
+
*/
|
|
2749
|
+
function resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, exists = () => true) {
|
|
2750
|
+
const targetEntityId = entityId;
|
|
2751
|
+
const componentType = componentTypeOrComponent;
|
|
2752
|
+
assertEntityExists(targetEntityId, "Entity", exists);
|
|
2753
|
+
assertSetComponentTypeValid(componentType);
|
|
2754
|
+
return {
|
|
2755
|
+
entityId: targetEntityId,
|
|
2756
|
+
componentType,
|
|
2757
|
+
component: maybeComponent
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* Resolve the (entity, componentType) for a remove() call, handling the
|
|
2762
|
+
* singleton component overload (remove(componentId)).
|
|
2763
|
+
*/
|
|
2764
|
+
function resolveRemoveOperation(entityId, componentType, exists = () => true) {
|
|
2765
|
+
if (componentType === void 0) {
|
|
2766
|
+
const componentId = entityId;
|
|
2767
|
+
assertEntityExists(componentId, "Component entity", exists);
|
|
2768
|
+
return {
|
|
2769
|
+
entityId: componentId,
|
|
2770
|
+
componentType: componentId
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
const targetEntityId = entityId;
|
|
2774
|
+
assertEntityExists(targetEntityId, "Entity", exists);
|
|
2775
|
+
assertComponentTypeValid(componentType);
|
|
2776
|
+
return {
|
|
2777
|
+
entityId: targetEntityId,
|
|
2778
|
+
componentType
|
|
2779
|
+
};
|
|
2315
2780
|
}
|
|
2316
2781
|
//#endregion
|
|
2317
2782
|
//#region src/storage/serialization.ts
|
|
@@ -2481,49 +2946,60 @@ function deserializeWorld(ctx, snapshot) {
|
|
|
2481
2946
|
*/
|
|
2482
2947
|
var World = class {
|
|
2483
2948
|
entityIdManager = new EntityIdManager();
|
|
2484
|
-
archetypes = [];
|
|
2485
|
-
archetypeBySignature = /* @__PURE__ */ new Map();
|
|
2486
|
-
entityToArchetype = /* @__PURE__ */ new Map();
|
|
2487
|
-
archetypesByComponent = /* @__PURE__ */ new Map();
|
|
2488
2949
|
entityReferences = /* @__PURE__ */ new Map();
|
|
2489
|
-
/** Reverse index: entity ID → set of archetypes whose componentTypes include that entity ID */
|
|
2490
|
-
entityToReferencingArchetypes = /* @__PURE__ */ new Map();
|
|
2491
2950
|
/** Sparse relation storage (for components created with `sparse: true`), shared with all Archetype instances */
|
|
2492
2951
|
sparseStore = new SparseStoreImpl();
|
|
2493
2952
|
/** Component entity (singleton) storage */
|
|
2494
2953
|
componentEntities = new ComponentEntityStore();
|
|
2954
|
+
archetypeManager;
|
|
2955
|
+
get archetypes() {
|
|
2956
|
+
return this.archetypeManager.archetypes;
|
|
2957
|
+
}
|
|
2958
|
+
get entityToArchetype() {
|
|
2959
|
+
return this.archetypeManager.entityToArchetype;
|
|
2960
|
+
}
|
|
2961
|
+
get archetypesByComponent() {
|
|
2962
|
+
return this.archetypeManager.archetypesByComponent;
|
|
2963
|
+
}
|
|
2964
|
+
get entityToReferencingArchetypes() {
|
|
2965
|
+
return this.archetypeManager.entityToReferencingArchetypes;
|
|
2966
|
+
}
|
|
2495
2967
|
queryRegistry = new QueryRegistry();
|
|
2496
2968
|
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
|
-
};
|
|
2969
|
+
debugStats = new DebugStatsManager();
|
|
2970
|
+
commandBuffer;
|
|
2971
|
+
commandExecutor;
|
|
2516
2972
|
constructor(snapshot) {
|
|
2973
|
+
this.archetypeManager = new ArchetypeManager({
|
|
2974
|
+
queryRegistry: this.queryRegistry,
|
|
2975
|
+
hooks: this.hooks,
|
|
2976
|
+
recordArchetypeCreated: () => this.debugStats.recordArchetypeCreated(),
|
|
2977
|
+
recordArchetypeRemoved: () => this.debugStats.recordArchetypeRemoved()
|
|
2978
|
+
}, this.sparseStore);
|
|
2517
2979
|
if (snapshot && typeof snapshot === "object") deserializeWorld({
|
|
2518
2980
|
entityIdManager: this.entityIdManager,
|
|
2519
2981
|
componentEntities: this.componentEntities,
|
|
2520
2982
|
entityReferences: this.entityReferences,
|
|
2521
2983
|
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
2522
|
-
setEntityToArchetype: (eid, arch) => this.entityToArchetype.set(eid, arch)
|
|
2984
|
+
setEntityToArchetype: (eid, arch) => this.archetypeManager.entityToArchetype.set(eid, arch)
|
|
2523
2985
|
}, snapshot);
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2986
|
+
const execCtx = {
|
|
2987
|
+
componentEntities: this.componentEntities,
|
|
2988
|
+
entityReferences: this.entityReferences,
|
|
2989
|
+
hooks: this.hooks,
|
|
2990
|
+
entityToArchetype: this.entityToArchetype,
|
|
2991
|
+
ensureArchetype: (ct) => this.ensureArchetype(ct),
|
|
2992
|
+
sparseStore: this.sparseStore,
|
|
2993
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
2994
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
2995
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct),
|
|
2996
|
+
destroyEntityImmediate: (eid) => this.destroyEntityImmediate(eid),
|
|
2997
|
+
incrementMigrations: () => this.debugStats.incrementMigrations(),
|
|
2998
|
+
triggerLifecycleHooks,
|
|
2999
|
+
triggerRemoveHooksForEntityDeletion
|
|
3000
|
+
};
|
|
3001
|
+
this.commandExecutor = new CommandExecutor(execCtx);
|
|
3002
|
+
this.commandBuffer = new CommandBuffer((entityId, commands) => this.commandExecutor.executeEntityCommands(entityId, commands));
|
|
2527
3003
|
}
|
|
2528
3004
|
/**
|
|
2529
3005
|
* Creates a new entity.
|
|
@@ -2619,64 +3095,12 @@ var World = class {
|
|
|
2619
3095
|
if (this.componentEntities.exists(entityId)) return true;
|
|
2620
3096
|
return this.entityToArchetype.has(entityId);
|
|
2621
3097
|
}
|
|
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
3098
|
set(entityId, componentTypeOrComponent, maybeComponent) {
|
|
2675
|
-
const { entityId: targetEntityId, componentType, component } =
|
|
3099
|
+
const { entityId: targetEntityId, componentType, component } = resolveSetOperation(entityId, componentTypeOrComponent, maybeComponent, (id) => this.exists(id));
|
|
2676
3100
|
this.commandBuffer.set(targetEntityId, componentType, component);
|
|
2677
3101
|
}
|
|
2678
3102
|
remove(entityId, componentType) {
|
|
2679
|
-
const { entityId: targetEntityId, componentType: targetComponentType } =
|
|
3103
|
+
const { entityId: targetEntityId, componentType: targetComponentType } = resolveRemoveOperation(entityId, componentType, (id) => this.exists(id));
|
|
2680
3104
|
this.commandBuffer.remove(targetEntityId, targetComponentType);
|
|
2681
3105
|
}
|
|
2682
3106
|
/**
|
|
@@ -2693,6 +3117,30 @@ var World = class {
|
|
|
2693
3117
|
delete(entityId) {
|
|
2694
3118
|
this.commandBuffer.delete(entityId);
|
|
2695
3119
|
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Returns an explicit handle for a singleton component (component-as-entity).
|
|
3122
|
+
*
|
|
3123
|
+
* This is the preferred API for singleton components.
|
|
3124
|
+
*
|
|
3125
|
+
* @example
|
|
3126
|
+
* const config = world.singleton(GlobalConfig);
|
|
3127
|
+
* config.set({ debug: true });
|
|
3128
|
+
* world.sync();
|
|
3129
|
+
* console.log(config.get());
|
|
3130
|
+
*/
|
|
3131
|
+
singleton(componentId) {
|
|
3132
|
+
assertEntityExists(componentId, "Component entity", (id) => this.exists(id));
|
|
3133
|
+
assertSetComponentTypeValid(componentId);
|
|
3134
|
+
return new SingletonHandle(componentId, {
|
|
3135
|
+
has: () => this.componentEntities.hasSingleton(componentId),
|
|
3136
|
+
get: () => this.get(componentId),
|
|
3137
|
+
getOptional: () => this.getOptional(componentId),
|
|
3138
|
+
remove: () => this.commandBuffer.remove(componentId, componentId),
|
|
3139
|
+
set: (value) => {
|
|
3140
|
+
this.commandBuffer.set(componentId, componentId, value);
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2696
3144
|
has(entityId, componentType) {
|
|
2697
3145
|
if (componentType === void 0) {
|
|
2698
3146
|
const componentId = entityId;
|
|
@@ -2763,7 +3211,7 @@ var World = class {
|
|
|
2763
3211
|
* // world.getChildren(parent, ChildOf), world.getParent(child, ChildOf)
|
|
2764
3212
|
*/
|
|
2765
3213
|
getRelationTargets(entityId, relationComp) {
|
|
2766
|
-
|
|
3214
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
2767
3215
|
const wildcard = relation(relationComp, "*");
|
|
2768
3216
|
if (this.componentEntities.exists(entityId)) return this.componentEntities.getWildcard(entityId, wildcard);
|
|
2769
3217
|
return this.get(entityId, wildcard);
|
|
@@ -2793,7 +3241,7 @@ var World = class {
|
|
|
2793
3241
|
* given base component.
|
|
2794
3242
|
*/
|
|
2795
3243
|
hasRelation(entityId, relationComp, targetId) {
|
|
2796
|
-
|
|
3244
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
2797
3245
|
if (targetId !== void 0) {
|
|
2798
3246
|
const specific = relation(relationComp, targetId);
|
|
2799
3247
|
return this.has(entityId, specific);
|
|
@@ -2804,7 +3252,7 @@ var World = class {
|
|
|
2804
3252
|
* Returns the number of relations of the given base component held by the entity.
|
|
2805
3253
|
*/
|
|
2806
3254
|
countRelations(entityId, relationComp) {
|
|
2807
|
-
|
|
3255
|
+
assertEntityExists(entityId, "Entity", (id) => this.exists(id));
|
|
2808
3256
|
return this.getRelationTargets(entityId, relationComp).length;
|
|
2809
3257
|
}
|
|
2810
3258
|
/**
|
|
@@ -2951,60 +3399,7 @@ var World = class {
|
|
|
2951
3399
|
* This is intended for development/debugging and leak detection.
|
|
2952
3400
|
*/
|
|
2953
3401
|
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 {}
|
|
3402
|
+
return this.debugStats.createCollector(callback);
|
|
3008
3403
|
}
|
|
3009
3404
|
/**
|
|
3010
3405
|
* Synchronizes all buffered commands (set/remove/delete) to the world.
|
|
@@ -3017,19 +3412,39 @@ var World = class {
|
|
|
3017
3412
|
* world.sync(); // Apply all buffered changes
|
|
3018
3413
|
*/
|
|
3019
3414
|
sync() {
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3415
|
+
if (!this.debugStats.hasActiveCollectors()) {
|
|
3416
|
+
this.commandBuffer.execute();
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
const syncStart = performance.now();
|
|
3420
|
+
this.debugStats.resetActivity();
|
|
3421
|
+
const commandBufferStart = performance.now();
|
|
3024
3422
|
const commandIterations = this.commandBuffer.execute();
|
|
3025
|
-
const commandBufferEnd =
|
|
3026
|
-
const syncEnd =
|
|
3027
|
-
|
|
3423
|
+
const commandBufferEnd = performance.now();
|
|
3424
|
+
const syncEnd = performance.now();
|
|
3425
|
+
const entityCount = this.entityToArchetype.size;
|
|
3426
|
+
let emptyArchetypes = 0;
|
|
3427
|
+
for (const arch of this.archetypes) if (arch.size === 0) emptyArchetypes++;
|
|
3428
|
+
let archetypesByComponentSize = 0;
|
|
3429
|
+
for (const set of this.archetypesByComponent.values()) archetypesByComponentSize += set.size;
|
|
3430
|
+
this.debugStats.deliver({
|
|
3028
3431
|
syncStart,
|
|
3029
3432
|
syncEnd,
|
|
3030
3433
|
commandBufferStart,
|
|
3031
3434
|
commandBufferEnd,
|
|
3032
3435
|
commandIterations
|
|
3436
|
+
}, {
|
|
3437
|
+
entityCount,
|
|
3438
|
+
freelistSize: this.entityIdManager.getFreelistSize(),
|
|
3439
|
+
nextId: this.entityIdManager.getNextId(),
|
|
3440
|
+
archetypeCount: this.archetypes.length,
|
|
3441
|
+
emptyArchetypes,
|
|
3442
|
+
archetypesByComponentSize,
|
|
3443
|
+
cachedQueryCount: this.queryRegistry.cache?.size ?? 0,
|
|
3444
|
+
registeredQueryCount: this.queryRegistry.queries?.size ?? 0,
|
|
3445
|
+
hookCount: this.hooks.size,
|
|
3446
|
+
entityReferencesSize: this.entityReferences.size,
|
|
3447
|
+
entityToReferencingArchetypesSize: this.entityToReferencingArchetypes.size
|
|
3033
3448
|
});
|
|
3034
3449
|
}
|
|
3035
3450
|
/**
|
|
@@ -3066,7 +3481,7 @@ var World = class {
|
|
|
3066
3481
|
createQuery(componentTypes, filter = {}) {
|
|
3067
3482
|
const sortedTypes = normalizeComponentTypes(componentTypes);
|
|
3068
3483
|
const filterKey = serializeQueryFilter(filter);
|
|
3069
|
-
const key = `${
|
|
3484
|
+
const key = `${sortedTypes.join(",")}${filterKey ? `|${filterKey}` : ""}`;
|
|
3070
3485
|
return this.queryRegistry.getOrCreate(this, sortedTypes, key, filter);
|
|
3071
3486
|
}
|
|
3072
3487
|
/**
|
|
@@ -3132,44 +3547,7 @@ var World = class {
|
|
|
3132
3547
|
* @internal
|
|
3133
3548
|
*/
|
|
3134
3549
|
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);
|
|
3550
|
+
return this.archetypeManager.getMatchingArchetypes(componentTypes);
|
|
3173
3551
|
}
|
|
3174
3552
|
query(componentTypes, includeComponents) {
|
|
3175
3553
|
const matchingArchetypes = this.getMatchingArchetypes(componentTypes);
|
|
@@ -3183,154 +3561,14 @@ var World = class {
|
|
|
3183
3561
|
return result;
|
|
3184
3562
|
}
|
|
3185
3563
|
}
|
|
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
3564
|
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);
|
|
3565
|
+
return this.archetypeManager.ensureArchetype(componentTypes);
|
|
3309
3566
|
}
|
|
3310
3567
|
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);
|
|
3568
|
+
this.archetypeManager.cleanupArchetypesReferencingEntity(entityId);
|
|
3315
3569
|
}
|
|
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);
|
|
3570
|
+
archetypeMatchesHook(archetype, entry) {
|
|
3571
|
+
return this.archetypeManager.archetypeMatchesHook(archetype, entry);
|
|
3334
3572
|
}
|
|
3335
3573
|
/**
|
|
3336
3574
|
* Serializes the entire world state to a plain JavaScript object.
|
|
@@ -3353,11 +3591,22 @@ var World = class {
|
|
|
3353
3591
|
* const savedData = JSON.parse(localStorage.getItem('save'));
|
|
3354
3592
|
* const newWorld = new World(savedData);
|
|
3355
3593
|
*/
|
|
3594
|
+
removeComponentImmediate(entityId, componentType, targetEntityId) {
|
|
3595
|
+
this.commandExecutor.removeComponentImmediate(entityId, componentType, targetEntityId);
|
|
3596
|
+
}
|
|
3597
|
+
createHooksContext() {
|
|
3598
|
+
return this.commandExecutor.getHooksContext?.() ?? {
|
|
3599
|
+
multiHooks: this.hooks,
|
|
3600
|
+
has: (eid, ct) => this.has(eid, ct),
|
|
3601
|
+
get: (eid, ct) => this.get(eid, ct),
|
|
3602
|
+
getOptional: (eid, ct) => this.getOptional(eid, ct)
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3356
3605
|
serialize() {
|
|
3357
|
-
return serializeWorld(this.archetypes, this.componentEntities, this.entityIdManager);
|
|
3606
|
+
return serializeWorld(this.archetypeManager.archetypes, this.componentEntities, this.entityIdManager);
|
|
3358
3607
|
}
|
|
3359
3608
|
};
|
|
3360
3609
|
//#endregion
|
|
3361
|
-
export {
|
|
3610
|
+
export { component as a, isSparseComponent as c, decodeRelationId as d, isWildcardRelationId as f, isRelationId as g, isEntityId as h, EntityBuilder as i, isSparseRelation as l, isComponentId as m, Query as n, getComponentIdByName as o, relation as p, SingletonHandle as r, getComponentNameById as s, World as t, isSparseWildcard as u };
|
|
3362
3611
|
|
|
3363
3612
|
//# sourceMappingURL=world.mjs.map
|