ExplosionSystem.TileFill.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. using System.Linq;
  2. using System.Numerics;
  3. using Content.Shared.Administration;
  4. using Content.Shared.Explosion;
  5. using Content.Shared.Explosion.Components;
  6. using Robust.Shared.Map;
  7. using Robust.Shared.Map.Components;
  8. using Robust.Shared.Physics.Components;
  9. using Robust.Shared.Timing;
  10. namespace Content.Server.Explosion.EntitySystems;
  11. // This partial part of the explosion system has all of the functions used to create the actual explosion map.
  12. // I.e, to get the sets of tiles & intensity values that describe an explosion.
  13. public sealed partial class ExplosionSystem
  14. {
  15. /// <summary>
  16. /// This is the main explosion generating function.
  17. /// </summary>
  18. /// <param name="epicenter">The center of the explosion</param>
  19. /// <param name="typeID">The explosion type. this determines the explosion damage</param>
  20. /// <param name="totalIntensity">The final sum of the tile intensities. This governs the overall size of the
  21. /// explosion</param>
  22. /// <param name="slope">How quickly does the intensity decrease when moving away from the epicenter.</param>
  23. /// <param name="maxIntensity">The maximum intensity that the explosion can have at any given tile. This
  24. /// effectively caps the damage that this explosion can do.</param>
  25. /// <returns>A list of tile-sets and a list of intensity values which describe the explosion.</returns>
  26. private (int, List<float>, ExplosionSpaceTileFlood?, Dictionary<EntityUid, ExplosionGridTileFlood>, Matrix3x2)? GetExplosionTiles(
  27. MapCoordinates epicenter,
  28. string typeID,
  29. float totalIntensity,
  30. float slope,
  31. float maxIntensity)
  32. {
  33. if (totalIntensity <= 0 || slope <= 0)
  34. return null;
  35. if (!_explosionTypes.TryGetValue(typeID, out var typeIndex))
  36. {
  37. Log.Error("Attempted to spawn explosion using a prototype that was not defined during initialization. Explosion prototype hot-reload is not currently supported.");
  38. return null;
  39. }
  40. Vector2i initialTile;
  41. EntityUid? epicentreGrid = null;
  42. var (localGrids, referenceGrid, maxDistance) = GetLocalGrids(epicenter, totalIntensity, slope, maxIntensity);
  43. // get the epicenter tile indices
  44. if (_mapManager.TryFindGridAt(epicenter, out var gridUid, out var candidateGrid) &&
  45. candidateGrid.TryGetTileRef(candidateGrid.WorldToTile(epicenter.Position), out var tileRef) &&
  46. !tileRef.Tile.IsEmpty)
  47. {
  48. epicentreGrid = gridUid;
  49. initialTile = tileRef.GridIndices;
  50. }
  51. else if (referenceGrid != null)
  52. {
  53. // reference grid defines coordinate system that the explosion in space will use
  54. initialTile = Comp<MapGridComponent>(referenceGrid.Value).WorldToTile(epicenter.Position);
  55. }
  56. else
  57. {
  58. // this is a space-based explosion that (should) not touch any grids.
  59. initialTile = new Vector2i(
  60. (int) Math.Floor(epicenter.Position.X / DefaultTileSize),
  61. (int) Math.Floor(epicenter.Position.Y / DefaultTileSize));
  62. }
  63. // Main data for the exploding tiles in space and on various grids
  64. Dictionary<EntityUid, ExplosionGridTileFlood> gridData = new();
  65. ExplosionSpaceTileFlood? spaceData = null;
  66. // The intensity slope is how much the intensity drop over a one-tile distance. The actual algorithm step-size is half of thhat.
  67. var stepSize = slope / 2;
  68. // Hashsets used for when grid-based explosion propagate into space. Basically: used to move data between
  69. // `gridData` and `spaceData` in-between neighbor finding iterations.
  70. HashSet<Vector2i> spaceJump = new();
  71. HashSet<Vector2i> previousSpaceJump;
  72. // As above, but for space-based explosion propagating from space onto grids.
  73. HashSet<EntityUid> encounteredGrids = new();
  74. Dictionary<EntityUid, HashSet<Vector2i>>? previousGridJump;
  75. // variables for transforming between grid and space-coordinates
  76. var spaceMatrix = Matrix3x2.Identity;
  77. var spaceAngle = Angle.Zero;
  78. if (referenceGrid != null)
  79. {
  80. var xform = Transform(Comp<MapGridComponent>(referenceGrid.Value).Owner);
  81. (_, spaceAngle, spaceMatrix) = _transformSystem.GetWorldPositionRotationMatrix(xform);
  82. }
  83. // is the explosion starting on a grid?
  84. if (epicentreGrid != null)
  85. {
  86. // set up the initial `gridData` instance
  87. encounteredGrids.Add(epicentreGrid.Value);
  88. if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
  89. airtightMap = new();
  90. var initialGridData = new ExplosionGridTileFlood(
  91. Comp<MapGridComponent>(epicentreGrid.Value),
  92. airtightMap,
  93. maxIntensity,
  94. stepSize,
  95. typeIndex,
  96. _gridEdges[epicentreGrid.Value],
  97. referenceGrid,
  98. spaceMatrix,
  99. spaceAngle);
  100. gridData[epicentreGrid.Value] = initialGridData;
  101. initialGridData.InitTile(initialTile);
  102. }
  103. else
  104. {
  105. // set up the space explosion data
  106. spaceData = new ExplosionSpaceTileFlood(this, epicenter, referenceGrid, localGrids, maxDistance);
  107. spaceData.InitTile(initialTile);
  108. }
  109. // Is this even a multi-tile explosion?
  110. if (totalIntensity < stepSize)
  111. // Bit anticlimactic. All that set up for nothing....
  112. return (1, new List<float> { totalIntensity }, spaceData, gridData, spaceMatrix);
  113. // These variables keep track of the total intensity we have distributed
  114. List<int> tilesInIteration = new() { 1 };
  115. List<float> iterationIntensity = new() {stepSize};
  116. var totalTiles = 1;
  117. var remainingIntensity = totalIntensity - stepSize;
  118. var iteration = 1;
  119. var maxIntensityIndex = 0;
  120. // If an explosion is trapped in an indestructible room, we can end the neighbor finding steps early.
  121. // These variables are used to check if we can abort early.
  122. float previousIntensity;
  123. var intensityUnchangedLastLoop = false;
  124. // Main flood-fill / neighbor-finding loop
  125. while (remainingIntensity > 0 && iteration <= MaxIterations && totalTiles < MaxArea)
  126. {
  127. previousIntensity = remainingIntensity;
  128. // First, we increase the intensity of the tiles that were already discovered in previous iterations.
  129. for (var i = maxIntensityIndex; i < iteration; i++)
  130. {
  131. var intensityIncrease = MathF.Min(stepSize, maxIntensity - iterationIntensity[i]);
  132. if (tilesInIteration[i] * intensityIncrease >= remainingIntensity)
  133. {
  134. // there is not enough intensity left to distribute. add a fractional amount and break.
  135. iterationIntensity[i] += remainingIntensity / tilesInIteration[i];
  136. remainingIntensity = 0;
  137. break;
  138. }
  139. iterationIntensity[i] += intensityIncrease;
  140. remainingIntensity -= tilesInIteration[i] * intensityIncrease;
  141. // Has this tile-set has reached max intensity? If so, stop iterating over it in future
  142. if (intensityIncrease < stepSize)
  143. maxIntensityIndex++;
  144. }
  145. if (remainingIntensity <= 0) break;
  146. // Next, we will add a new iteration of tiles
  147. // In order to treat "cost" of moving off a grid on the same level as moving onto a grid, both space -> grid and grid -> space have to be delayed by one iteration.
  148. previousSpaceJump = spaceJump;
  149. previousGridJump = spaceData?.GridJump;
  150. spaceJump = new();
  151. var newTileCount = 0;
  152. if (previousGridJump != null)
  153. encounteredGrids.UnionWith(previousGridJump.Keys);
  154. foreach (var grid in encounteredGrids)
  155. {
  156. // is this a new grid, for which we must create a new explosion data set
  157. if (!gridData.TryGetValue(grid, out var data))
  158. {
  159. if (!_airtightMap.TryGetValue(grid, out var airtightMap))
  160. airtightMap = new();
  161. data = new ExplosionGridTileFlood(
  162. Comp<MapGridComponent>(grid),
  163. airtightMap,
  164. maxIntensity,
  165. stepSize,
  166. typeIndex,
  167. _gridEdges[grid],
  168. referenceGrid,
  169. spaceMatrix,
  170. spaceAngle);
  171. gridData[grid] = data;
  172. }
  173. // get the new neighbours, and populate gridToSpaceTiles in the process.
  174. newTileCount += data.AddNewTiles(iteration, previousGridJump?.GetValueOrDefault(grid));
  175. spaceJump.UnionWith(data.SpaceJump);
  176. }
  177. // if space-data is null, but some grid-based explosion reached space, we need to initialize it.
  178. if (spaceData == null && previousSpaceJump.Count != 0)
  179. spaceData = new ExplosionSpaceTileFlood(this, epicenter, referenceGrid, localGrids, maxDistance);
  180. // If the explosion has reached space, do that neighbors finding step as well.
  181. if (spaceData != null)
  182. newTileCount += spaceData.AddNewTiles(iteration, previousSpaceJump);
  183. // Does adding these tiles bring us above the total target intensity?
  184. tilesInIteration.Add(newTileCount);
  185. if (newTileCount * stepSize >= remainingIntensity)
  186. {
  187. iterationIntensity.Add(remainingIntensity / newTileCount);
  188. break;
  189. }
  190. // add the new tiles and decrement available intensity
  191. remainingIntensity -= newTileCount * stepSize;
  192. iterationIntensity.Add(stepSize);
  193. totalTiles += newTileCount;
  194. // It is possible that the explosion has some max intensity and is stuck in a container whose walls it
  195. // cannot break. if the remaining intensity remains unchanged TWO loops in a row, we know that this is the
  196. // case.
  197. if (intensityUnchangedLastLoop && remainingIntensity == previousIntensity)
  198. break;
  199. intensityUnchangedLastLoop = remainingIntensity == previousIntensity;
  200. iteration += 1;
  201. }
  202. // Neighbor finding is done. Perform final clean up and return.
  203. foreach (var grid in gridData.Values)
  204. {
  205. grid.CleanUp();
  206. }
  207. spaceData?.CleanUp();
  208. return (totalTiles, iterationIntensity, spaceData, gridData, spaceMatrix);
  209. }
  210. /// <summary>
  211. /// Look for grids in an area and returns them. Also selects a special grid that will be used to determine the
  212. /// orientation of an explosion in space.
  213. /// </summary>
  214. /// <remarks>
  215. /// Note that even though an explosion may start ON a grid, the explosion in space may still be orientated to
  216. /// match a separate grid. This is done so that if you have something like a tiny suicide-bomb shuttle exploding
  217. /// near a large station, the explosion will still orient to match the station, not the tiny shuttle.
  218. /// </remarks>
  219. public (List<EntityUid>, EntityUid?, float) GetLocalGrids(MapCoordinates epicenter, float totalIntensity, float slope, float maxIntensity)
  220. {
  221. // Get the explosion radius (approx radius if it were in open-space). Note that if the explosion is confined in
  222. // some directions but not in others, the actual explosion may reach further than this distance from the
  223. // epicenter. Conversely, it might go nowhere near as far.
  224. var radius = 0.5f + IntensityToRadius(totalIntensity, slope, maxIntensity);
  225. // to avoid a silly lookup for silly input numbers, cap the radius to half of the theoretical maximum (lookup area gets doubled later on).
  226. radius = Math.Min(radius, MaxIterations / 4);
  227. EntityUid? referenceGrid = null;
  228. float mass = 0;
  229. // First attempt to find a grid that is relatively close to the explosion's center. Instead of looking in a
  230. // diameter x diameter sized box, use a smaller box with radius sized sides:
  231. var box = Box2.CenteredAround(epicenter.Position, new Vector2(radius, radius));
  232. foreach (var grid in _mapManager.FindGridsIntersecting(epicenter.MapId, box))
  233. {
  234. if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
  235. {
  236. mass = physics.Mass;
  237. referenceGrid = grid.Owner;
  238. }
  239. }
  240. // Next, we use a much larger lookup to determine all grids relevant to the explosion. This is used to determine
  241. // what grids should be included during the grid-edge transformation steps. This means that if a grid is not in
  242. // this set, the explosion can never propagate from space onto this grid.
  243. // As mentioned before, the `diameter` is only indicative, as an explosion that is obstructed (e.g., in a
  244. // tunnel) may travel further away from the epicenter. But this should be very rare for space-traversing
  245. // explosions. So instead of using the largest possible distance that an explosion could theoretically travel
  246. // and using that for the grid look-up, we will just arbitrarily fudge the lookup size to be twice the diameter.
  247. radius *= 4;
  248. box = Box2.CenteredAround(epicenter.Position, new Vector2(radius, radius));
  249. var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
  250. var grids = mapGrids.Select(x => x.Owner).ToList();
  251. if (referenceGrid != null)
  252. return (grids, referenceGrid, radius);
  253. // We still don't have are reference grid. So lets also look in the enlarged region
  254. foreach (var grid in mapGrids)
  255. {
  256. if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
  257. {
  258. mass = physics.Mass;
  259. referenceGrid = grid.Owner;
  260. }
  261. }
  262. return (grids, referenceGrid, radius);
  263. }
  264. public ExplosionVisualsState? GenerateExplosionPreview(SpawnExplosionEuiMsg.PreviewRequest request)
  265. {
  266. var stopwatch = new Stopwatch();
  267. stopwatch.Start();
  268. var results = GetExplosionTiles(
  269. request.Epicenter,
  270. request.TypeId,
  271. request.TotalIntensity,
  272. request.IntensitySlope,
  273. request.MaxIntensity);
  274. if (results == null)
  275. return null;
  276. var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
  277. Log.Info($"Generated explosion preview with {area} tiles in {stopwatch.Elapsed.TotalMilliseconds}ms");
  278. Dictionary<NetEntity, Dictionary<int, List<Vector2i>>> tileLists = new();
  279. foreach (var (grid, data) in gridData)
  280. {
  281. tileLists.Add(GetNetEntity(grid), data.TileLists);
  282. }
  283. return new ExplosionVisualsState(
  284. request.Epicenter,
  285. request.TypeId,
  286. iterationIntensity,
  287. spaceData?.TileLists,
  288. tileLists, spaceMatrix,
  289. spaceData?.TileSize ?? DefaultTileSize
  290. );
  291. }
  292. }