1
0

ExplosionTileFlood.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. using Content.Shared.Atmos;
  2. using System.Runtime.CompilerServices;
  3. namespace Content.Server.Explosion.EntitySystems;
  4. /// <summary>
  5. /// This class exists to facilitate the iterative neighbor-finding / flooding algorithm used by explosions in <see
  6. /// cref="ExplosionSystem.GetExplosionTiles"/>. This is the base class for <see cref="ExplosionSpaceTileFlood"/> and
  7. /// <see cref="ExplosionGridTileFlood"/>, each of which contains additional code fro logic specific to grids or space.
  8. /// </summary>
  9. /// <remarks>
  10. /// The class stores information about the tiles that the explosion has currently reached, and provides functions to
  11. /// perform a neighbor-finding iteration to expand the explosion area. It also has some functionality that allows
  12. /// tiles to move between grids/space.
  13. /// </remarks>
  14. public abstract class ExplosionTileFlood
  15. {
  16. // Main tile data sets, mapping iterations onto tile lists
  17. public Dictionary<int, List<Vector2i>> TileLists = new();
  18. protected Dictionary<int, List<Vector2i>> BlockedTileLists = new();
  19. protected Dictionary<int, HashSet<Vector2i>> FreedTileLists = new();
  20. // The new tile lists added each iteration. I **could** just pass these along to every function, but IMO it is more
  21. // readable if they are just private variables.
  22. protected List<Vector2i> NewTiles = default!;
  23. protected List<Vector2i> NewBlockedTiles = default!;
  24. protected HashSet<Vector2i> NewFreedTiles = default!;
  25. // HashSets used to ensure uniqueness of tiles. Prevents the explosion from looping back in on itself.
  26. protected UniqueVector2iSet ProcessedTiles = new();
  27. protected UniqueVector2iSet UnenteredBlockedTiles = new();
  28. protected UniqueVector2iSet EnteredBlockedTiles = new();
  29. public abstract void InitTile(Vector2i initialTile);
  30. protected abstract void ProcessNewTile(int iteration, Vector2i tile, AtmosDirection entryDirections);
  31. protected abstract AtmosDirection GetUnblockedDirectionOrAll(Vector2i tile);
  32. protected void AddNewDiagonalTiles(int iteration, IEnumerable<Vector2i> tiles, bool ignoreLocalBlocker = false)
  33. {
  34. AtmosDirection entryDirection = AtmosDirection.Invalid;
  35. foreach (var tile in tiles)
  36. {
  37. var freeDirections = ignoreLocalBlocker ? AtmosDirection.All : GetUnblockedDirectionOrAll(tile);
  38. // Get the free directions of the directly adjacent tiles
  39. var freeDirectionsN = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.North));
  40. var freeDirectionsE = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.East));
  41. var freeDirectionsS = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.South));
  42. var freeDirectionsW = GetUnblockedDirectionOrAll(tile.Offset(AtmosDirection.West));
  43. // North East
  44. if (freeDirections.IsFlagSet(AtmosDirection.North) && freeDirectionsN.IsFlagSet(AtmosDirection.SouthEast))
  45. entryDirection |= AtmosDirection.West;
  46. if (freeDirections.IsFlagSet(AtmosDirection.East) && freeDirectionsE.IsFlagSet(AtmosDirection.NorthWest))
  47. entryDirection |= AtmosDirection.South;
  48. if (entryDirection != AtmosDirection.Invalid)
  49. {
  50. ProcessNewTile(iteration, tile + (1, 1), entryDirection);
  51. entryDirection = AtmosDirection.Invalid;
  52. }
  53. // North West
  54. if (freeDirections.IsFlagSet(AtmosDirection.North) && freeDirectionsN.IsFlagSet(AtmosDirection.SouthWest))
  55. entryDirection |= AtmosDirection.East;
  56. if (freeDirections.IsFlagSet(AtmosDirection.West) && freeDirectionsW.IsFlagSet(AtmosDirection.NorthEast))
  57. entryDirection |= AtmosDirection.West;
  58. if (entryDirection != AtmosDirection.Invalid)
  59. {
  60. ProcessNewTile(iteration, tile + (-1, 1), entryDirection);
  61. entryDirection = AtmosDirection.Invalid;
  62. }
  63. // South East
  64. if (freeDirections.IsFlagSet(AtmosDirection.South) && freeDirectionsS.IsFlagSet(AtmosDirection.NorthEast))
  65. entryDirection |= AtmosDirection.West;
  66. if (freeDirections.IsFlagSet(AtmosDirection.East) && freeDirectionsE.IsFlagSet(AtmosDirection.SouthWest))
  67. entryDirection |= AtmosDirection.North;
  68. if (entryDirection != AtmosDirection.Invalid)
  69. {
  70. ProcessNewTile(iteration, tile + (1, -1), entryDirection);
  71. entryDirection = AtmosDirection.Invalid;
  72. }
  73. // South West
  74. if (freeDirections.IsFlagSet(AtmosDirection.South) && freeDirectionsS.IsFlagSet(AtmosDirection.NorthWest))
  75. entryDirection |= AtmosDirection.West;
  76. if (freeDirections.IsFlagSet(AtmosDirection.West) && freeDirectionsW.IsFlagSet(AtmosDirection.SouthEast))
  77. entryDirection |= AtmosDirection.North;
  78. if (entryDirection != AtmosDirection.Invalid)
  79. {
  80. ProcessNewTile(iteration, tile + (-1, -1), entryDirection);
  81. entryDirection = AtmosDirection.Invalid;
  82. }
  83. }
  84. }
  85. /// <summary>
  86. /// Merge all tile lists into a single output tile list.
  87. /// </summary>
  88. public void CleanUp()
  89. {
  90. foreach (var (iteration, blocked) in BlockedTileLists)
  91. {
  92. if (TileLists.TryGetValue(iteration, out var tiles))
  93. tiles.AddRange(blocked);
  94. else
  95. TileLists[iteration] = blocked;
  96. }
  97. }
  98. }
  99. /// <summary>
  100. /// This is a data structure can be used to ensure the uniqueness of Vector2i indices.
  101. /// </summary>
  102. /// <remarks>
  103. /// This basically exists to replace the use of HashSet&lt;Vector2i&gt; if all you need is the the functions Contains()
  104. /// and Add(). This is both faster and apparently allocates less. Does not support iterating over contents
  105. /// </remarks>
  106. public sealed class UniqueVector2iSet
  107. {
  108. private const int ChunkSize = 32; // # of bits in an integer.
  109. private Dictionary<Vector2i, VectorChunk> _chunks = new();
  110. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  111. public Vector2i ToChunkIndices(Vector2i indices)
  112. {
  113. var x = (int) Math.Floor(indices.X / (float) ChunkSize);
  114. var y = (int) Math.Floor(indices.Y / (float) ChunkSize);
  115. return new Vector2i(x, y);
  116. }
  117. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  118. public bool Add(Vector2i index)
  119. {
  120. var chunkIndex = ToChunkIndices(index);
  121. if (_chunks.TryGetValue(chunkIndex, out var chunk))
  122. {
  123. return chunk.Add(index);
  124. }
  125. chunk = new();
  126. chunk.Add(index);
  127. _chunks[chunkIndex] = chunk;
  128. return true;
  129. }
  130. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  131. public bool Contains(Vector2i index)
  132. {
  133. if (!_chunks.TryGetValue(ToChunkIndices(index), out var chunk))
  134. return false;
  135. return chunk.Contains(index);
  136. }
  137. private sealed class VectorChunk
  138. {
  139. // 32*32 chunk represented via 32 ints with 32 bits each. Basic testing showed that this was faster than using
  140. // 16-sized chunks with ushorts, a bool[,], or just having each chunk be a HashSet.
  141. private readonly int[] _tiles = new int[ChunkSize];
  142. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  143. public bool Add(Vector2i index)
  144. {
  145. var x = MathHelper.Mod(index.X, ChunkSize);
  146. var y = MathHelper.Mod(index.Y, ChunkSize);
  147. var oldFlags = _tiles[x];
  148. var newFlags = oldFlags | (1 << y);
  149. if (newFlags == oldFlags)
  150. return false;
  151. _tiles[x] = newFlags;
  152. return true;
  153. }
  154. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  155. public bool Contains(Vector2i index)
  156. {
  157. var x = MathHelper.Mod(index.X, ChunkSize);
  158. var y = MathHelper.Mod(index.Y, ChunkSize);
  159. return (_tiles[x] & (1 << y)) != 0;
  160. }
  161. }
  162. }