using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using Svelto.DataStructures; namespace Svelto.ECS { /// /// This mechanism is not for thread-safety but to be sure that all the permutations of group tags always /// point to the same group ID. /// A group compound can generate several permutation of tags, so that the order of the tag doesn't matter, /// but for this to work, each permutation must be identified by the same ID (generated by the unique combination) /// each permutation of the same groups of tag is actually a different class with a different static constructor and we /// don't want to call it more than once for the same set of tags /// it's thread local because since it's not linked to a specific group, it must work synchronously /// static class GroupCompoundInitializer { internal static readonly ThreadLocal skipStaticCompoundConstructorsWith4Tags = new ThreadLocal(); internal static readonly ThreadLocal skipStaticCompoundConstructorsWith3Tags = new ThreadLocal(); internal static readonly ThreadLocal skipStaticCompoundConstructorsWith2Tags = new ThreadLocal(); } interface ITouchedByReflection { } public abstract class GroupCompound: ITouchedByReflection where G1 : GroupTag where G2 : GroupTag where G3 : GroupTag where G4 : GroupTag { static GroupCompound() { /// c# Static constructors are guaranteed to be thread safe and not called more than once if (Interlocked.CompareExchange(ref isInitialised, 1, 0) != 0) throw new Exception("GroupCompound static constructor called twice - impossible"); if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value == false) { var group = new ExclusiveGroup(GroupTag.bitmask | GroupTag.bitmask | GroupTag.bitmask | GroupTag.bitmask); _Groups = new FasterList(1); _Groups.Add(group); #if DEBUG var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name}-{typeof(G3).Name}-{typeof(G4).Name} ID {(uint)group.id}"; GroupNamesMap.idToName[group] = name; #endif //The hashname is independent from the actual group ID. this is fundamental because it is want //guarantees the hash to be the same across different machines GroupHashMap.RegisterGroup(group, typeof(GroupCompound).FullName); //ToArrayFast is theoretically not correct, but since multiple 0s are ignored and we don't care if we //add one, we avoid an allocation var exclusiveGroupStructs = _Groups.ToArrayFast(out var count); _GroupsHashSet = new HashSet(count); for (var index = 0; index < count; ++index) { var exclusiveGroupStruct = exclusiveGroupStructs[index]; _GroupsHashSet.Add(exclusiveGroupStruct); } GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value = true; //all the permutations must share the same group and group hashset. Warm them up, avoid call the //constructors again, set the desired value GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; //all the constructor have been called now GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value = false; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound.Add(group); GroupCompound.Add(group); GroupCompound.Add(group); GroupCompound.Add(group); GroupCompound.Add(group); // and must share the same array GroupCompound.Add(group); GroupCompound.Add(group); GroupCompound.Add(group); GroupCompound.Add(group); GroupCompound.Add(group); //This is done here to be sure that the group is added once per group tag //(if done inside the previous group compound it would be added multiple times) GroupTag.Add(group); GroupTag.Add(group); GroupTag.Add(group); GroupTag.Add(group); } } public static FasterReadOnlyList Groups { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups); } public static ExclusiveBuildGroup BuildGroup { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups[0], 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Includes(ExclusiveGroupStruct group) { DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed"); return _GroupsHashSet.Contains(group); } internal static void Add(ExclusiveGroupStruct group) { #if DEBUG && !PROFILE_SVELTO for (var i = 0; i < _Groups.count; ++i) if (_Groups[i] == group) throw new System.Exception("this test must be transformed in unit test"); #endif _Groups.Add(group); _GroupsHashSet.Add(group); } static readonly FasterList _Groups; static readonly HashSet _GroupsHashSet; //we are changing this with Interlocked, so it cannot be readonly static int isInitialised; } public abstract class GroupCompound: ITouchedByReflection where G1 : GroupTag where G2 : GroupTag where G3 : GroupTag { static GroupCompound() { /// c# Static constructors are guaranteed to be thread safe and not called more than once if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0) throw new Exception("GroupCompound static constructor called twice - impossible"); if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value == false) { var group = new ExclusiveGroup(GroupTag.bitmask | GroupTag.bitmask | GroupTag.bitmask); _Groups = new FasterList(1); _Groups.Add(group); #if DEBUG var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name}-{typeof(G3).Name} ID {(uint)group.id}"; GroupNamesMap.idToName[group] = name; #endif //The hashname is independent from the actual group ID. this is fundamental because it is want //guarantees the hash to be the same across different machines GroupHashMap.RegisterGroup(group, typeof(GroupCompound).FullName); var exclusiveGroupStructs = _Groups.ToArrayFast(out var count); _GroupsHashSet = new HashSet(count); for (var index = 0; index < count; ++index) { var exclusiveGroupStruct = exclusiveGroupStructs[index]; _GroupsHashSet.Add(exclusiveGroupStruct); } GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value = true; //all the combinations must share the same group and group hashset GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; GroupCompound._Groups = _Groups; //all the constructor have been called now GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value = false; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound._GroupsHashSet = _GroupsHashSet; GroupCompound.Add(group); // and must share the same array GroupCompound.Add(group); GroupCompound.Add(group); //This is done here to be sure that the group is added once per group tag //(if done inside the previous group compound it would be added multiple times) GroupTag.Add(group); GroupTag.Add(group); GroupTag.Add(group); } } public static FasterReadOnlyList Groups { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups); } public static ExclusiveBuildGroup BuildGroup { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups[0], 1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Includes(ExclusiveGroupStruct group) { DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed"); return _GroupsHashSet.Contains(group); } internal static void Add(ExclusiveGroupStruct group) { #if DEBUG && !PROFILE_SVELTO for (var i = 0; i < _Groups.count; ++i) if (_Groups[i] == group) throw new System.Exception("this test must be transformed in unit test"); #endif _Groups.Add(group); _GroupsHashSet.Add(group); } static readonly FasterList _Groups; static readonly HashSet _GroupsHashSet; //we are changing this with Interlocked, so it cannot be readonly static int isInitializing; } public abstract class GroupCompound: ITouchedByReflection where G1 : GroupTag where G2 : GroupTag { static GroupCompound() { /// c# Static constructors are guaranteed to be thread safe and not called more than once if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0) throw new Exception($"{typeof(GroupCompound).FullName} GroupCompound static constructor called twice - impossible"); if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value == false) { range = GroupTag.range > GroupTag.range ? GroupTag.range : GroupTag.range; var initialSize = range; _Groups = new FasterList(initialSize); var group = new ExclusiveGroup(initialSize, GroupTag.bitmask | GroupTag.bitmask); #if DEBUG for (uint i = 0; i < initialSize; ++i) { var groupID = group.id + i; var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name} ID {groupID}"; GroupNamesMap.idToName[group + i] = name; } #endif for (uint i = 0; i < initialSize; ++i) { var exclusiveGroupStruct = group + i; //The hashname is independent from the actual group ID. this is fundamental because it is want //guarantees the hash to be the same across different machines GroupHashMap.RegisterGroup(exclusiveGroupStruct, typeof(GroupCompound).FullName + i); _Groups.Add(exclusiveGroupStruct); //every abstract group preemptively adds this group, it may or may not be empty in future GroupTag.Add(exclusiveGroupStruct); GroupTag.Add(exclusiveGroupStruct); } var exclusiveGroupStructs = _Groups.ToArrayFast(out var count); _GroupsHashSet = new HashSet(count); for (var index = 0; index < count; index++) { var exclusiveGroupStruct = exclusiveGroupStructs[index]; _GroupsHashSet.Add(exclusiveGroupStruct); } GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value = true; GroupCompound._Groups = _Groups; //all the constructor have been called now GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value = false; GroupCompound.range = initialSize; GroupCompound._GroupsHashSet = _GroupsHashSet; } } public static FasterReadOnlyList Groups { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups); } public static ExclusiveBuildGroup BuildGroup { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups[0], range); } //TODO there is an overlap between this method and ExclusiveGroupExtensions FoundIn [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Includes(ExclusiveGroupStruct group) { DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed"); return _GroupsHashSet.Contains(group); } internal static void Add(ExclusiveGroupStruct group) { #if DEBUG && !PROFILE_SVELTO for (var i = 0; i < _Groups.count; ++i) if (_Groups[i] == group) throw new System.Exception("this test must be transformed in unit test"); #endif _Groups.Add(group); _GroupsHashSet.Add(group); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Offset(ExclusiveGroupStruct group) { return BuildGroup.Offset(group); } static readonly FasterList _Groups; static readonly HashSet _GroupsHashSet; static ushort range; static int isInitializing; } /// /// A Group Tag holds initially just a group, itself. However the number of groups can grow with the number of /// combinations of GroupTags including this one. This because a GroupTag is an adjective and different entities /// can use the same adjective together with other ones. Albeit since I need to be able to iterate over all the /// groups with the same adjective, a group tag needs to hold all the group compounds using it. /// /// public abstract class GroupTag: ITouchedByReflection where T : GroupTag { static GroupTag() { if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0) throw new Exception("GroupTag static constructor called twice - impossible"); //GroupTag can set values for ranges and bitmasks in their static constructors so they must be called first //there is no other way around this, as the base static constructor will be called once base fields are touched //RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle); typeof(T).TypeInitializer?.Invoke(null, null); //must use this because if a specialised GroupTag is called first will call the this constructor before having the chance to initialise the protected values. This will force to initialise the values no matter what (and won't call the base constructor again because already executing) var initialRange = range; //range may be overriden by the constructor previously called if (initialRange == 0) //means never initialised by a inherited static constructor { initialRange = 1; range = 1; } var group = new ExclusiveGroup(initialRange, bitmask); for (uint i = 0; i < initialRange; ++i) { _Groups.Add(group + i); //The hashname is independent from the actual group ID. this is fundamental because it is want //guarantees the hash to be the same across different machines GroupHashMap.RegisterGroup(group + i, typeof(GroupTag).FullName + i); } #if DEBUG var typeInfo = typeof(T); for (uint i = 0; i < initialRange; ++i) { var groupID = group.id + i; var name = $"Compound: {typeInfo.Name} ID {groupID}"; var typeInfoBaseType = typeInfo.BaseType; //todo: this should shield from using a pattern different than public class GROUP_NAME : GroupTag {} however I am not sure it's working if (typeInfoBaseType.GenericTypeArguments[0] != typeInfo) throw new ECSException("Invalid Group Tag declared"); GroupNamesMap.idToName[group + i] = name; } #endif var exclusiveGroupStructs = _Groups.ToArrayFast(out var count); _GroupsHashSet = new HashSet(count); for (var index = 0; index < count; index++) { var exclusiveGroupStruct = exclusiveGroupStructs[index]; _GroupsHashSet.Add(exclusiveGroupStruct); } } public static FasterReadOnlyList Groups { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups); } public static ExclusiveBuildGroup BuildGroup { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(_Groups[0], range); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool Includes(ExclusiveGroupStruct group) { DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed"); return _GroupsHashSet.Contains(group); } //Each time a new combination of group tags is found a new group is added. internal static void Add(ExclusiveGroupStruct group) { #if DEBUG && !PROFILE_SVELTO for (var i = 0; i < _Groups.count; ++i) if (_Groups[i] == group) throw new System.Exception("this test must be transformed in unit test"); #endif _Groups.Add(group); _GroupsHashSet.Add(group); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Offset(ExclusiveGroupStruct group) { return BuildGroup.Offset(group); } static readonly FasterList _Groups = new FasterList(1); static readonly HashSet _GroupsHashSet; //we are changing this with Interlocked, so it cannot be readonly static int isInitializing; //special group attributes, at the moment of writing this comment, only the disabled group has a special attribute //Allow to call GroupTag static constructors like // public class Dead: GroupTag // { // static Dead() // { // bitmask = ExclusiveGroupBitmask.DISABLED_BIT; // } // }; protected internal static ExclusiveGroupBitmask bitmask; //set a number different than 0 to create a range of groups instead of a single group //example of usage: // public class VehicleGroup:GroupTag // { // static VehicleGroup() // { // range = (ushort)Data.MaxTeamCount; // } // } protected internal static ushort range; } }