Mirror of Svelto.ECS because we're a fan of it
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

518 lines
24KB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Runtime.CompilerServices;
  4. using System.Threading;
  5. using Svelto.DataStructures;
  6. namespace Svelto.ECS
  7. {
  8. /// <summary>
  9. /// This mechanism is not for thread-safety but to be sure that all the permutations of group tags always
  10. /// point to the same group ID.
  11. /// A group compound can generate several permutation of tags, so that the order of the tag doesn't matter,
  12. /// but for this to work, each permutation must be identified by the same ID (generated by the unique combination)
  13. /// each permutation of the same groups of tag is actually a different class with a different static constructor and we
  14. /// don't want to call it more than once for the same set of tags
  15. /// it's thread local because since it's not linked to a specific group, it must work synchronously
  16. /// </summary>
  17. static class GroupCompoundInitializer
  18. {
  19. internal static readonly ThreadLocal<bool> skipStaticCompoundConstructorsWith4Tags = new ThreadLocal<bool>();
  20. internal static readonly ThreadLocal<bool> skipStaticCompoundConstructorsWith3Tags = new ThreadLocal<bool>();
  21. internal static readonly ThreadLocal<bool> skipStaticCompoundConstructorsWith2Tags = new ThreadLocal<bool>();
  22. }
  23. interface ITouchedByReflection { }
  24. public abstract class GroupCompound<G1, G2, G3, G4>: ITouchedByReflection
  25. where G1 : GroupTag<G1>
  26. where G2 : GroupTag<G2>
  27. where G3 : GroupTag<G3>
  28. where G4 : GroupTag<G4>
  29. {
  30. static GroupCompound()
  31. {
  32. /// c# Static constructors are guaranteed to be thread safe and not called more than once
  33. if (Interlocked.CompareExchange(ref isInitialised, 1, 0) != 0)
  34. throw new Exception("GroupCompound static constructor called twice - impossible");
  35. if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value == false)
  36. {
  37. var group = new ExclusiveGroup(GroupTag<G1>.bitmask | GroupTag<G2>.bitmask | GroupTag<G3>.bitmask | GroupTag<G4>.bitmask);
  38. _Groups = new FasterList<ExclusiveGroupStruct>(1);
  39. _Groups.Add(group);
  40. #if DEBUG
  41. var name =
  42. $"Compound: {typeof(G1).Name}-{typeof(G2).Name}-{typeof(G3).Name}-{typeof(G4).Name} ID {(uint)group.id}";
  43. GroupNamesMap.idToName[group] = name;
  44. #endif
  45. //The hashname is independent from the actual group ID. this is fundamental because it is want
  46. //guarantees the hash to be the same across different machines
  47. GroupHashMap.RegisterGroup(group, typeof(GroupCompound<G1, G2, G3, G4>).FullName);
  48. //ToArrayFast is theoretically not correct, but since multiple 0s are ignored and we don't care if we
  49. //add one, we avoid an allocation
  50. var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
  51. _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
  52. for (var index = 0; index < count; ++index)
  53. {
  54. var exclusiveGroupStruct = exclusiveGroupStructs[index];
  55. _GroupsHashSet.Add(exclusiveGroupStruct);
  56. }
  57. GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value = true;
  58. //all the permutations must share the same group and group hashset. Warm them up, avoid call the
  59. //constructors again, set the desired value
  60. GroupCompound<G1, G2, G4, G3>._Groups = _Groups;
  61. GroupCompound<G1, G3, G2, G4>._Groups = _Groups;
  62. GroupCompound<G1, G3, G4, G2>._Groups = _Groups;
  63. GroupCompound<G1, G4, G2, G3>._Groups = _Groups;
  64. GroupCompound<G2, G1, G3, G4>._Groups = _Groups;
  65. GroupCompound<G2, G3, G4, G1>._Groups = _Groups;
  66. GroupCompound<G3, G1, G2, G4>._Groups = _Groups;
  67. GroupCompound<G4, G1, G2, G3>._Groups = _Groups;
  68. GroupCompound<G1, G4, G3, G2>._Groups = _Groups;
  69. GroupCompound<G2, G1, G4, G3>._Groups = _Groups;
  70. GroupCompound<G2, G4, G3, G1>._Groups = _Groups;
  71. GroupCompound<G3, G1, G4, G2>._Groups = _Groups;
  72. GroupCompound<G4, G1, G3, G2>._Groups = _Groups;
  73. GroupCompound<G2, G3, G1, G4>._Groups = _Groups;
  74. GroupCompound<G3, G4, G1, G2>._Groups = _Groups;
  75. GroupCompound<G2, G4, G1, G3>._Groups = _Groups;
  76. GroupCompound<G3, G2, G1, G4>._Groups = _Groups;
  77. GroupCompound<G3, G2, G4, G1>._Groups = _Groups;
  78. GroupCompound<G3, G4, G2, G1>._Groups = _Groups;
  79. GroupCompound<G4, G2, G1, G3>._Groups = _Groups;
  80. GroupCompound<G4, G2, G3, G1>._Groups = _Groups;
  81. GroupCompound<G4, G3, G1, G2>._Groups = _Groups;
  82. GroupCompound<G4, G3, G2, G1>._Groups = _Groups;
  83. //all the constructor have been called now
  84. GroupCompoundInitializer.skipStaticCompoundConstructorsWith4Tags.Value = false;
  85. GroupCompound<G1, G2, G4, G3>._GroupsHashSet = _GroupsHashSet;
  86. GroupCompound<G1, G3, G2, G4>._GroupsHashSet = _GroupsHashSet;
  87. GroupCompound<G1, G3, G4, G2>._GroupsHashSet = _GroupsHashSet;
  88. GroupCompound<G1, G4, G2, G3>._GroupsHashSet = _GroupsHashSet;
  89. GroupCompound<G2, G1, G3, G4>._GroupsHashSet = _GroupsHashSet;
  90. GroupCompound<G2, G3, G4, G1>._GroupsHashSet = _GroupsHashSet;
  91. GroupCompound<G3, G1, G2, G4>._GroupsHashSet = _GroupsHashSet;
  92. GroupCompound<G4, G1, G2, G3>._GroupsHashSet = _GroupsHashSet;
  93. GroupCompound<G1, G4, G3, G2>._GroupsHashSet = _GroupsHashSet;
  94. GroupCompound<G2, G1, G4, G3>._GroupsHashSet = _GroupsHashSet;
  95. GroupCompound<G2, G4, G3, G1>._GroupsHashSet = _GroupsHashSet;
  96. GroupCompound<G3, G1, G4, G2>._GroupsHashSet = _GroupsHashSet;
  97. GroupCompound<G4, G1, G3, G2>._GroupsHashSet = _GroupsHashSet;
  98. GroupCompound<G2, G3, G1, G4>._GroupsHashSet = _GroupsHashSet;
  99. GroupCompound<G3, G4, G1, G2>._GroupsHashSet = _GroupsHashSet;
  100. GroupCompound<G2, G4, G1, G3>._GroupsHashSet = _GroupsHashSet;
  101. GroupCompound<G3, G2, G1, G4>._GroupsHashSet = _GroupsHashSet;
  102. GroupCompound<G3, G2, G4, G1>._GroupsHashSet = _GroupsHashSet;
  103. GroupCompound<G3, G4, G2, G1>._GroupsHashSet = _GroupsHashSet;
  104. GroupCompound<G4, G2, G1, G3>._GroupsHashSet = _GroupsHashSet;
  105. GroupCompound<G4, G2, G3, G1>._GroupsHashSet = _GroupsHashSet;
  106. GroupCompound<G4, G3, G1, G2>._GroupsHashSet = _GroupsHashSet;
  107. GroupCompound<G4, G3, G2, G1>._GroupsHashSet = _GroupsHashSet;
  108. GroupCompound<G1, G2, G3>.Add(group);
  109. GroupCompound<G1, G2, G4>.Add(group);
  110. GroupCompound<G1, G3, G4>.Add(group);
  111. GroupCompound<G2, G3, G4>.Add(group);
  112. GroupCompound<G1, G2>.Add(group); //<G1/G2> and <G2/G1> must share the same array
  113. GroupCompound<G1, G3>.Add(group);
  114. GroupCompound<G1, G4>.Add(group);
  115. GroupCompound<G2, G3>.Add(group);
  116. GroupCompound<G2, G4>.Add(group);
  117. GroupCompound<G3, G4>.Add(group);
  118. //This is done here to be sure that the group is added once per group tag
  119. //(if done inside the previous group compound it would be added multiple times)
  120. GroupTag<G1>.Add(group);
  121. GroupTag<G2>.Add(group);
  122. GroupTag<G3>.Add(group);
  123. GroupTag<G4>.Add(group);
  124. }
  125. }
  126. public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
  127. {
  128. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  129. get => new(_Groups);
  130. }
  131. public static ExclusiveBuildGroup BuildGroup
  132. {
  133. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  134. get => new(_Groups[0], 1);
  135. }
  136. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  137. public static bool Includes(ExclusiveGroupStruct group)
  138. {
  139. DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");
  140. return _GroupsHashSet.Contains(group);
  141. }
  142. internal static void Add(ExclusiveGroupStruct group)
  143. {
  144. #if DEBUG && !PROFILE_SVELTO
  145. for (var i = 0; i < _Groups.count; ++i)
  146. if (_Groups[i] == group)
  147. throw new System.Exception("this test must be transformed in unit test");
  148. #endif
  149. _Groups.Add(group);
  150. _GroupsHashSet.Add(group);
  151. }
  152. static readonly FasterList<ExclusiveGroupStruct> _Groups;
  153. static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;
  154. //we are changing this with Interlocked, so it cannot be readonly
  155. static int isInitialised;
  156. }
  157. public abstract class GroupCompound<G1, G2, G3>: ITouchedByReflection
  158. where G1 : GroupTag<G1>
  159. where G2 : GroupTag<G2>
  160. where G3 : GroupTag<G3>
  161. {
  162. static GroupCompound()
  163. {
  164. /// c# Static constructors are guaranteed to be thread safe and not called more than once
  165. if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0)
  166. throw new Exception("GroupCompound static constructor called twice - impossible");
  167. if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value == false)
  168. {
  169. var group = new ExclusiveGroup(GroupTag<G1>.bitmask | GroupTag<G2>.bitmask | GroupTag<G3>.bitmask);
  170. _Groups = new FasterList<ExclusiveGroupStruct>(1);
  171. _Groups.Add(group);
  172. #if DEBUG
  173. var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name}-{typeof(G3).Name} ID {(uint)group.id}";
  174. GroupNamesMap.idToName[group] = name;
  175. #endif
  176. //The hashname is independent from the actual group ID. this is fundamental because it is want
  177. //guarantees the hash to be the same across different machines
  178. GroupHashMap.RegisterGroup(group, typeof(GroupCompound<G1, G2, G3>).FullName);
  179. var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
  180. _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
  181. for (var index = 0; index < count; ++index)
  182. {
  183. var exclusiveGroupStruct = exclusiveGroupStructs[index];
  184. _GroupsHashSet.Add(exclusiveGroupStruct);
  185. }
  186. GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value = true;
  187. //all the combinations must share the same group and group hashset
  188. GroupCompound<G3, G1, G2>._Groups = _Groups;
  189. GroupCompound<G2, G3, G1>._Groups = _Groups;
  190. GroupCompound<G3, G2, G1>._Groups = _Groups;
  191. GroupCompound<G1, G3, G2>._Groups = _Groups;
  192. GroupCompound<G2, G1, G3>._Groups = _Groups;
  193. //all the constructor have been called now
  194. GroupCompoundInitializer.skipStaticCompoundConstructorsWith3Tags.Value = false;
  195. GroupCompound<G3, G1, G2>._GroupsHashSet = _GroupsHashSet;
  196. GroupCompound<G2, G3, G1>._GroupsHashSet = _GroupsHashSet;
  197. GroupCompound<G3, G2, G1>._GroupsHashSet = _GroupsHashSet;
  198. GroupCompound<G1, G3, G2>._GroupsHashSet = _GroupsHashSet;
  199. GroupCompound<G2, G1, G3>._GroupsHashSet = _GroupsHashSet;
  200. GroupCompound<G1, G2>.Add(group); //<G1/G2> and <G2/G1> must share the same array
  201. GroupCompound<G1, G3>.Add(group);
  202. GroupCompound<G2, G3>.Add(group);
  203. //This is done here to be sure that the group is added once per group tag
  204. //(if done inside the previous group compound it would be added multiple times)
  205. GroupTag<G1>.Add(group);
  206. GroupTag<G2>.Add(group);
  207. GroupTag<G3>.Add(group);
  208. }
  209. }
  210. public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
  211. {
  212. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  213. get => new(_Groups);
  214. }
  215. public static ExclusiveBuildGroup BuildGroup
  216. {
  217. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  218. get => new(_Groups[0], 1);
  219. }
  220. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  221. public static bool Includes(ExclusiveGroupStruct group)
  222. {
  223. DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");
  224. return _GroupsHashSet.Contains(group);
  225. }
  226. internal static void Add(ExclusiveGroupStruct group)
  227. {
  228. #if DEBUG && !PROFILE_SVELTO
  229. for (var i = 0; i < _Groups.count; ++i)
  230. if (_Groups[i] == group)
  231. throw new System.Exception("this test must be transformed in unit test");
  232. #endif
  233. _Groups.Add(group);
  234. _GroupsHashSet.Add(group);
  235. }
  236. static readonly FasterList<ExclusiveGroupStruct> _Groups;
  237. static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;
  238. //we are changing this with Interlocked, so it cannot be readonly
  239. static int isInitializing;
  240. }
  241. public abstract class GroupCompound<G1, G2>: ITouchedByReflection
  242. where G1 : GroupTag<G1>
  243. where G2 : GroupTag<G2>
  244. {
  245. static GroupCompound()
  246. {
  247. /// c# Static constructors are guaranteed to be thread safe and not called more than once
  248. if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0)
  249. throw new Exception($"{typeof(GroupCompound<G1, G2>).FullName} GroupCompound static constructor called twice - impossible");
  250. if (GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value == false)
  251. {
  252. range = GroupTag<G1>.range > GroupTag<G2>.range ? GroupTag<G1>.range : GroupTag<G2>.range;
  253. var initialSize = range;
  254. _Groups = new FasterList<ExclusiveGroupStruct>(initialSize);
  255. var group = new ExclusiveGroup(initialSize, GroupTag<G1>.bitmask | GroupTag<G2>.bitmask);
  256. #if DEBUG
  257. for (uint i = 0; i < initialSize; ++i)
  258. {
  259. var groupID = group.id + i;
  260. var name = $"Compound: {typeof(G1).Name}-{typeof(G2).Name} ID {groupID}";
  261. GroupNamesMap.idToName[group + i] = name;
  262. }
  263. #endif
  264. for (uint i = 0; i < initialSize; ++i)
  265. {
  266. var exclusiveGroupStruct = group + i;
  267. //The hashname is independent from the actual group ID. this is fundamental because it is want
  268. //guarantees the hash to be the same across different machines
  269. GroupHashMap.RegisterGroup(exclusiveGroupStruct, typeof(GroupCompound<G1, G2>).FullName + i);
  270. _Groups.Add(exclusiveGroupStruct);
  271. //every abstract group preemptively adds this group, it may or may not be empty in future
  272. GroupTag<G1>.Add(exclusiveGroupStruct);
  273. GroupTag<G2>.Add(exclusiveGroupStruct);
  274. }
  275. var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
  276. _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
  277. for (var index = 0; index < count; index++)
  278. {
  279. var exclusiveGroupStruct = exclusiveGroupStructs[index];
  280. _GroupsHashSet.Add(exclusiveGroupStruct);
  281. }
  282. GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value = true;
  283. GroupCompound<G2, G1>._Groups = _Groups;
  284. //all the constructor have been called now
  285. GroupCompoundInitializer.skipStaticCompoundConstructorsWith2Tags.Value = false;
  286. GroupCompound<G2, G1>.range = initialSize;
  287. GroupCompound<G2, G1>._GroupsHashSet = _GroupsHashSet;
  288. }
  289. }
  290. public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
  291. {
  292. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  293. get => new(_Groups);
  294. }
  295. public static ExclusiveBuildGroup BuildGroup
  296. {
  297. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  298. get => new(_Groups[0], range);
  299. }
  300. //TODO there is an overlap between this method and ExclusiveGroupExtensions FoundIn
  301. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  302. public static bool Includes(ExclusiveGroupStruct group)
  303. {
  304. DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");
  305. return _GroupsHashSet.Contains(group);
  306. }
  307. internal static void Add(ExclusiveGroupStruct group)
  308. {
  309. #if DEBUG && !PROFILE_SVELTO
  310. for (var i = 0; i < _Groups.count; ++i)
  311. if (_Groups[i] == group)
  312. throw new System.Exception("this test must be transformed in unit test");
  313. #endif
  314. _Groups.Add(group);
  315. _GroupsHashSet.Add(group);
  316. }
  317. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  318. public static uint Offset(ExclusiveGroupStruct group)
  319. {
  320. return BuildGroup.Offset(group);
  321. }
  322. static readonly FasterList<ExclusiveGroupStruct> _Groups;
  323. static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;
  324. static ushort range;
  325. static int isInitializing;
  326. }
  327. /// <summary>
  328. /// A Group Tag holds initially just a group, itself. However the number of groups can grow with the number of
  329. /// combinations of GroupTags including this one. This because a GroupTag is an adjective and different entities
  330. /// can use the same adjective together with other ones. Albeit since I need to be able to iterate over all the
  331. /// groups with the same adjective, a group tag needs to hold all the group compounds using it.
  332. /// </summary>
  333. /// <typeparam name="T"></typeparam>
  334. public abstract class GroupTag<T>: ITouchedByReflection where T : GroupTag<T>
  335. {
  336. static GroupTag()
  337. {
  338. if (Interlocked.CompareExchange(ref isInitializing, 1, 0) != 0)
  339. throw new Exception("GroupTag static constructor called twice - impossible");
  340. //GroupTag can set values for ranges and bitmasks in their static constructors so they must be called first
  341. //there is no other way around this, as the base static constructor will be called once base fields are touched
  342. //RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);
  343. 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)
  344. var initialRange = range; //range may be overriden by the constructor previously called
  345. if (initialRange == 0) //means never initialised by a inherited static constructor
  346. {
  347. initialRange = 1;
  348. range = 1;
  349. }
  350. var group = new ExclusiveGroup(initialRange, bitmask);
  351. for (uint i = 0; i < initialRange; ++i)
  352. {
  353. _Groups.Add(group + i);
  354. //The hashname is independent from the actual group ID. this is fundamental because it is want
  355. //guarantees the hash to be the same across different machines
  356. GroupHashMap.RegisterGroup(group + i, typeof(GroupTag<T>).FullName + i);
  357. }
  358. #if DEBUG
  359. var typeInfo = typeof(T);
  360. for (uint i = 0; i < initialRange; ++i)
  361. {
  362. var groupID = group.id + i;
  363. var name = $"Compound: {typeInfo.Name} ID {groupID}";
  364. var typeInfoBaseType = typeInfo.BaseType;
  365. //todo: this should shield from using a pattern different than public class GROUP_NAME : GroupTag<GROUP_NAME> {} however I am not sure it's working
  366. if (typeInfoBaseType.GenericTypeArguments[0] != typeInfo)
  367. throw new ECSException("Invalid Group Tag declared");
  368. GroupNamesMap.idToName[group + i] = name;
  369. }
  370. #endif
  371. var exclusiveGroupStructs = _Groups.ToArrayFast(out var count);
  372. _GroupsHashSet = new HashSet<ExclusiveGroupStruct>(count);
  373. for (var index = 0; index < count; index++)
  374. {
  375. var exclusiveGroupStruct = exclusiveGroupStructs[index];
  376. _GroupsHashSet.Add(exclusiveGroupStruct);
  377. }
  378. }
  379. public static FasterReadOnlyList<ExclusiveGroupStruct> Groups
  380. {
  381. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  382. get => new(_Groups);
  383. }
  384. public static ExclusiveBuildGroup BuildGroup
  385. {
  386. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  387. get => new(_Groups[0], range);
  388. }
  389. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  390. public static bool Includes(ExclusiveGroupStruct group)
  391. {
  392. DBC.ECS.Check.Require(group != ExclusiveGroupStruct.Invalid, "invalid group passed");
  393. return _GroupsHashSet.Contains(group);
  394. }
  395. //Each time a new combination of group tags is found a new group is added.
  396. internal static void Add(ExclusiveGroupStruct group)
  397. {
  398. #if DEBUG && !PROFILE_SVELTO
  399. for (var i = 0; i < _Groups.count; ++i)
  400. if (_Groups[i] == group)
  401. throw new System.Exception("this test must be transformed in unit test");
  402. #endif
  403. _Groups.Add(group);
  404. _GroupsHashSet.Add(group);
  405. }
  406. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  407. public static uint Offset(ExclusiveGroupStruct group)
  408. {
  409. return BuildGroup.Offset(group);
  410. }
  411. static readonly FasterList<ExclusiveGroupStruct> _Groups = new FasterList<ExclusiveGroupStruct>(1);
  412. static readonly HashSet<ExclusiveGroupStruct> _GroupsHashSet;
  413. //we are changing this with Interlocked, so it cannot be readonly
  414. static int isInitializing;
  415. //special group attributes, at the moment of writing this comment, only the disabled group has a special attribute
  416. //Allow to call GroupTag static constructors like
  417. // public class Dead: GroupTag<Dead>
  418. // {
  419. // static Dead()
  420. // {
  421. // bitmask = ExclusiveGroupBitmask.DISABLED_BIT;
  422. // }
  423. // };
  424. protected internal static ExclusiveGroupBitmask bitmask;
  425. //set a number different than 0 to create a range of groups instead of a single group
  426. //example of usage:
  427. // public class VehicleGroup:GroupTag<VehicleGroup>
  428. // {
  429. // static VehicleGroup()
  430. // {
  431. // range = (ushort)Data.MaxTeamCount;
  432. // }
  433. // }
  434. protected internal static ushort range;
  435. }
  436. }