NavMapSystem.Regions.cs 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. using Content.Shared.Atmos;
  2. using Content.Shared.Pinpointer;
  3. using System.Linq;
  4. namespace Content.Client.Pinpointer;
  5. public sealed partial class NavMapSystem
  6. {
  7. private (AtmosDirection, Vector2i, AtmosDirection)[] _regionPropagationTable =
  8. {
  9. (AtmosDirection.East, new Vector2i(1, 0), AtmosDirection.West),
  10. (AtmosDirection.West, new Vector2i(-1, 0), AtmosDirection.East),
  11. (AtmosDirection.North, new Vector2i(0, 1), AtmosDirection.South),
  12. (AtmosDirection.South, new Vector2i(0, -1), AtmosDirection.North),
  13. };
  14. public override void Update(float frameTime)
  15. {
  16. // To prevent compute spikes, only one region is flood filled per frame
  17. var query = AllEntityQuery<NavMapComponent>();
  18. while (query.MoveNext(out var ent, out var entNavMapRegions))
  19. FloodFillNextEnqueuedRegion(ent, entNavMapRegions);
  20. }
  21. private void FloodFillNextEnqueuedRegion(EntityUid uid, NavMapComponent component)
  22. {
  23. if (!component.QueuedRegionsToFlood.Any())
  24. return;
  25. var regionOwner = component.QueuedRegionsToFlood.Dequeue();
  26. // If the region is no longer valid, flood the next one in the queue
  27. if (!component.RegionProperties.TryGetValue(regionOwner, out var regionProperties) ||
  28. !regionProperties.Seeds.Any())
  29. {
  30. FloodFillNextEnqueuedRegion(uid, component);
  31. return;
  32. }
  33. // Flood fill the region, using the region seeds as starting points
  34. var (floodedTiles, floodedChunks) = FloodFillRegion(uid, component, regionProperties);
  35. // Combine the flooded tiles into larger rectangles
  36. var gridCoords = GetMergedRegionTiles(floodedTiles);
  37. // Create and assign the new region overlay
  38. var regionOverlay = new NavMapRegionOverlay(regionProperties.UiKey, gridCoords)
  39. {
  40. Color = regionProperties.Color
  41. };
  42. component.RegionOverlays[regionOwner] = regionOverlay;
  43. // To reduce unnecessary future flood fills, we will track which chunks have been flooded by a region owner
  44. // First remove an old assignments
  45. if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var oldChunks))
  46. {
  47. foreach (var chunk in oldChunks)
  48. {
  49. if (component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var oldOwners))
  50. {
  51. oldOwners.Remove(regionOwner);
  52. component.ChunkToRegionOwnerTable[chunk] = oldOwners;
  53. }
  54. }
  55. }
  56. // Now update with the new assignments
  57. component.RegionOwnerToChunkTable[regionOwner] = floodedChunks;
  58. foreach (var chunk in floodedChunks)
  59. {
  60. if (!component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var owners))
  61. owners = new();
  62. owners.Add(regionOwner);
  63. component.ChunkToRegionOwnerTable[chunk] = owners;
  64. }
  65. }
  66. private (HashSet<Vector2i>, HashSet<Vector2i>) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties)
  67. {
  68. if (!regionProperties.Seeds.Any())
  69. return (new(), new());
  70. var visitedChunks = new HashSet<Vector2i>();
  71. var visitedTiles = new HashSet<Vector2i>();
  72. var tilesToVisit = new Stack<Vector2i>();
  73. foreach (var regionSeed in regionProperties.Seeds)
  74. {
  75. tilesToVisit.Push(regionSeed);
  76. while (tilesToVisit.Count > 0)
  77. {
  78. // If the max region area is hit, exit
  79. if (visitedTiles.Count > regionProperties.MaxArea)
  80. return (new(), new());
  81. // Pop the top tile from the stack
  82. var current = tilesToVisit.Pop();
  83. // If the current tile position has already been visited,
  84. // or is too far away from the seed, continue
  85. if ((regionSeed - current).Length > regionProperties.MaxRadius)
  86. continue;
  87. if (visitedTiles.Contains(current))
  88. continue;
  89. // Determine the tile's chunk index
  90. var chunkOrigin = SharedMapSystem.GetChunkIndices(current, ChunkSize);
  91. var relative = SharedMapSystem.GetChunkRelative(current, ChunkSize);
  92. var idx = GetTileIndex(relative);
  93. // Extract the tile data
  94. if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
  95. continue;
  96. var flag = chunk.TileData[idx];
  97. // If the current tile is entirely occupied, continue
  98. if ((FloorMask & flag) == 0)
  99. continue;
  100. if ((WallMask & flag) == WallMask)
  101. continue;
  102. if ((AirlockMask & flag) == AirlockMask)
  103. continue;
  104. // Otherwise the tile can be added to this region
  105. visitedTiles.Add(current);
  106. visitedChunks.Add(chunkOrigin);
  107. // Determine if we can propagate the region into its cardinally adjacent neighbors
  108. // To propagate to a neighbor, movement into the neighbors closest edge must not be
  109. // blocked, and vice versa
  110. foreach (var (direction, tileOffset, reverseDirection) in _regionPropagationTable)
  111. {
  112. if (!RegionCanPropagateInDirection(chunk, current, direction))
  113. continue;
  114. var neighbor = current + tileOffset;
  115. var neighborOrigin = SharedMapSystem.GetChunkIndices(neighbor, ChunkSize);
  116. if (!component.Chunks.TryGetValue(neighborOrigin, out var neighborChunk))
  117. continue;
  118. visitedChunks.Add(neighborOrigin);
  119. if (!RegionCanPropagateInDirection(neighborChunk, neighbor, reverseDirection))
  120. continue;
  121. tilesToVisit.Push(neighbor);
  122. }
  123. }
  124. }
  125. return (visitedTiles, visitedChunks);
  126. }
  127. private bool RegionCanPropagateInDirection(NavMapChunk chunk, Vector2i tile, AtmosDirection direction)
  128. {
  129. var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
  130. var idx = GetTileIndex(relative);
  131. var flag = chunk.TileData[idx];
  132. if ((FloorMask & flag) == 0)
  133. return false;
  134. var directionMask = 1 << (int)direction;
  135. var wallMask = (int)direction << (int)NavMapChunkType.Wall;
  136. var airlockMask = (int)direction << (int)NavMapChunkType.Airlock;
  137. if ((wallMask & flag) > 0)
  138. return false;
  139. if ((airlockMask & flag) > 0)
  140. return false;
  141. return true;
  142. }
  143. private List<(Vector2i, Vector2i)> GetMergedRegionTiles(HashSet<Vector2i> tiles)
  144. {
  145. if (!tiles.Any())
  146. return new();
  147. var x = tiles.Select(t => t.X);
  148. var minX = x.Min();
  149. var maxX = x.Max();
  150. var y = tiles.Select(t => t.Y);
  151. var minY = y.Min();
  152. var maxY = y.Max();
  153. var matrix = new int[maxX - minX + 1, maxY - minY + 1];
  154. foreach (var tile in tiles)
  155. {
  156. var a = tile.X - minX;
  157. var b = tile.Y - minY;
  158. matrix[a, b] = 1;
  159. }
  160. return GetMergedRegionTiles(matrix, new Vector2i(minX, minY));
  161. }
  162. private List<(Vector2i, Vector2i)> GetMergedRegionTiles(int[,] matrix, Vector2i offset)
  163. {
  164. var output = new List<(Vector2i, Vector2i)>();
  165. var rows = matrix.GetLength(0);
  166. var cols = matrix.GetLength(1);
  167. var dp = new int[rows, cols];
  168. var coords = (new Vector2i(), new Vector2i());
  169. var maxArea = 0;
  170. var count = 0;
  171. while (!IsArrayEmpty(matrix))
  172. {
  173. count++;
  174. if (count > rows * cols)
  175. break;
  176. // Clear old values
  177. dp = new int[rows, cols];
  178. coords = (new Vector2i(), new Vector2i());
  179. maxArea = 0;
  180. // Initialize the first row of dp
  181. for (int j = 0; j < cols; j++)
  182. {
  183. dp[0, j] = matrix[0, j];
  184. }
  185. // Calculate dp values for remaining rows
  186. for (int i = 1; i < rows; i++)
  187. {
  188. for (int j = 0; j < cols; j++)
  189. dp[i, j] = matrix[i, j] == 1 ? dp[i - 1, j] + 1 : 0;
  190. }
  191. // Find the largest rectangular area seeded for each position in the matrix
  192. for (int i = 0; i < rows; i++)
  193. {
  194. for (int j = 0; j < cols; j++)
  195. {
  196. int minWidth = dp[i, j];
  197. for (int k = j; k >= 0; k--)
  198. {
  199. if (dp[i, k] <= 0)
  200. break;
  201. minWidth = Math.Min(minWidth, dp[i, k]);
  202. var currArea = Math.Max(maxArea, minWidth * (j - k + 1));
  203. if (currArea > maxArea)
  204. {
  205. maxArea = currArea;
  206. coords = (new Vector2i(i - minWidth + 1, k), new Vector2i(i, j));
  207. }
  208. }
  209. }
  210. }
  211. // Save the recorded rectangle vertices
  212. output.Add((coords.Item1 + offset, coords.Item2 + offset));
  213. // Removed the tiles covered by the rectangle from matrix
  214. for (int i = coords.Item1.X; i <= coords.Item2.X; i++)
  215. {
  216. for (int j = coords.Item1.Y; j <= coords.Item2.Y; j++)
  217. matrix[i, j] = 0;
  218. }
  219. }
  220. return output;
  221. }
  222. private bool IsArrayEmpty(int[,] matrix)
  223. {
  224. for (int i = 0; i < matrix.GetLength(0); i++)
  225. {
  226. for (int j = 0; j < matrix.GetLength(1); j++)
  227. {
  228. if (matrix[i, j] == 1)
  229. return false;
  230. }
  231. }
  232. return true;
  233. }
  234. }