| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- using System.Linq;
- using System.Numerics;
- using Content.Shared.Administration;
- using Content.Shared.Explosion;
- using Content.Shared.Explosion.Components;
- using Robust.Shared.Map;
- using Robust.Shared.Map.Components;
- using Robust.Shared.Physics.Components;
- using Robust.Shared.Timing;
- namespace Content.Server.Explosion.EntitySystems;
- // This partial part of the explosion system has all of the functions used to create the actual explosion map.
- // I.e, to get the sets of tiles & intensity values that describe an explosion.
- public sealed partial class ExplosionSystem
- {
- /// <summary>
- /// This is the main explosion generating function.
- /// </summary>
- /// <param name="epicenter">The center of the explosion</param>
- /// <param name="typeID">The explosion type. this determines the explosion damage</param>
- /// <param name="totalIntensity">The final sum of the tile intensities. This governs the overall size of the
- /// explosion</param>
- /// <param name="slope">How quickly does the intensity decrease when moving away from the epicenter.</param>
- /// <param name="maxIntensity">The maximum intensity that the explosion can have at any given tile. This
- /// effectively caps the damage that this explosion can do.</param>
- /// <returns>A list of tile-sets and a list of intensity values which describe the explosion.</returns>
- private (int, List<float>, ExplosionSpaceTileFlood?, Dictionary<EntityUid, ExplosionGridTileFlood>, Matrix3x2)? GetExplosionTiles(
- MapCoordinates epicenter,
- string typeID,
- float totalIntensity,
- float slope,
- float maxIntensity)
- {
- if (totalIntensity <= 0 || slope <= 0)
- return null;
- if (!_explosionTypes.TryGetValue(typeID, out var typeIndex))
- {
- Log.Error("Attempted to spawn explosion using a prototype that was not defined during initialization. Explosion prototype hot-reload is not currently supported.");
- return null;
- }
- Vector2i initialTile;
- EntityUid? epicentreGrid = null;
- var (localGrids, referenceGrid, maxDistance) = GetLocalGrids(epicenter, totalIntensity, slope, maxIntensity);
- // get the epicenter tile indices
- if (_mapManager.TryFindGridAt(epicenter, out var gridUid, out var candidateGrid) &&
- candidateGrid.TryGetTileRef(candidateGrid.WorldToTile(epicenter.Position), out var tileRef) &&
- !tileRef.Tile.IsEmpty)
- {
- epicentreGrid = gridUid;
- initialTile = tileRef.GridIndices;
- }
- else if (referenceGrid != null)
- {
- // reference grid defines coordinate system that the explosion in space will use
- initialTile = Comp<MapGridComponent>(referenceGrid.Value).WorldToTile(epicenter.Position);
- }
- else
- {
- // this is a space-based explosion that (should) not touch any grids.
- initialTile = new Vector2i(
- (int) Math.Floor(epicenter.Position.X / DefaultTileSize),
- (int) Math.Floor(epicenter.Position.Y / DefaultTileSize));
- }
- // Main data for the exploding tiles in space and on various grids
- Dictionary<EntityUid, ExplosionGridTileFlood> gridData = new();
- ExplosionSpaceTileFlood? spaceData = null;
- // The intensity slope is how much the intensity drop over a one-tile distance. The actual algorithm step-size is half of thhat.
- var stepSize = slope / 2;
- // Hashsets used for when grid-based explosion propagate into space. Basically: used to move data between
- // `gridData` and `spaceData` in-between neighbor finding iterations.
- HashSet<Vector2i> spaceJump = new();
- HashSet<Vector2i> previousSpaceJump;
- // As above, but for space-based explosion propagating from space onto grids.
- HashSet<EntityUid> encounteredGrids = new();
- Dictionary<EntityUid, HashSet<Vector2i>>? previousGridJump;
- // variables for transforming between grid and space-coordinates
- var spaceMatrix = Matrix3x2.Identity;
- var spaceAngle = Angle.Zero;
- if (referenceGrid != null)
- {
- var xform = Transform(Comp<MapGridComponent>(referenceGrid.Value).Owner);
- (_, spaceAngle, spaceMatrix) = _transformSystem.GetWorldPositionRotationMatrix(xform);
- }
- // is the explosion starting on a grid?
- if (epicentreGrid != null)
- {
- // set up the initial `gridData` instance
- encounteredGrids.Add(epicentreGrid.Value);
- if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
- airtightMap = new();
- var initialGridData = new ExplosionGridTileFlood(
- Comp<MapGridComponent>(epicentreGrid.Value),
- airtightMap,
- maxIntensity,
- stepSize,
- typeIndex,
- _gridEdges[epicentreGrid.Value],
- referenceGrid,
- spaceMatrix,
- spaceAngle);
- gridData[epicentreGrid.Value] = initialGridData;
- initialGridData.InitTile(initialTile);
- }
- else
- {
- // set up the space explosion data
- spaceData = new ExplosionSpaceTileFlood(this, epicenter, referenceGrid, localGrids, maxDistance);
- spaceData.InitTile(initialTile);
- }
- // Is this even a multi-tile explosion?
- if (totalIntensity < stepSize)
- // Bit anticlimactic. All that set up for nothing....
- return (1, new List<float> { totalIntensity }, spaceData, gridData, spaceMatrix);
- // These variables keep track of the total intensity we have distributed
- List<int> tilesInIteration = new() { 1 };
- List<float> iterationIntensity = new() {stepSize};
- var totalTiles = 1;
- var remainingIntensity = totalIntensity - stepSize;
- var iteration = 1;
- var maxIntensityIndex = 0;
- // If an explosion is trapped in an indestructible room, we can end the neighbor finding steps early.
- // These variables are used to check if we can abort early.
- float previousIntensity;
- var intensityUnchangedLastLoop = false;
- // Main flood-fill / neighbor-finding loop
- while (remainingIntensity > 0 && iteration <= MaxIterations && totalTiles < MaxArea)
- {
- previousIntensity = remainingIntensity;
- // First, we increase the intensity of the tiles that were already discovered in previous iterations.
- for (var i = maxIntensityIndex; i < iteration; i++)
- {
- var intensityIncrease = MathF.Min(stepSize, maxIntensity - iterationIntensity[i]);
- if (tilesInIteration[i] * intensityIncrease >= remainingIntensity)
- {
- // there is not enough intensity left to distribute. add a fractional amount and break.
- iterationIntensity[i] += remainingIntensity / tilesInIteration[i];
- remainingIntensity = 0;
- break;
- }
- iterationIntensity[i] += intensityIncrease;
- remainingIntensity -= tilesInIteration[i] * intensityIncrease;
- // Has this tile-set has reached max intensity? If so, stop iterating over it in future
- if (intensityIncrease < stepSize)
- maxIntensityIndex++;
- }
- if (remainingIntensity <= 0) break;
- // Next, we will add a new iteration of tiles
- // 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.
- previousSpaceJump = spaceJump;
- previousGridJump = spaceData?.GridJump;
- spaceJump = new();
- var newTileCount = 0;
- if (previousGridJump != null)
- encounteredGrids.UnionWith(previousGridJump.Keys);
- foreach (var grid in encounteredGrids)
- {
- // is this a new grid, for which we must create a new explosion data set
- if (!gridData.TryGetValue(grid, out var data))
- {
- if (!_airtightMap.TryGetValue(grid, out var airtightMap))
- airtightMap = new();
- data = new ExplosionGridTileFlood(
- Comp<MapGridComponent>(grid),
- airtightMap,
- maxIntensity,
- stepSize,
- typeIndex,
- _gridEdges[grid],
- referenceGrid,
- spaceMatrix,
- spaceAngle);
- gridData[grid] = data;
- }
- // get the new neighbours, and populate gridToSpaceTiles in the process.
- newTileCount += data.AddNewTiles(iteration, previousGridJump?.GetValueOrDefault(grid));
- spaceJump.UnionWith(data.SpaceJump);
- }
- // if space-data is null, but some grid-based explosion reached space, we need to initialize it.
- if (spaceData == null && previousSpaceJump.Count != 0)
- spaceData = new ExplosionSpaceTileFlood(this, epicenter, referenceGrid, localGrids, maxDistance);
- // If the explosion has reached space, do that neighbors finding step as well.
- if (spaceData != null)
- newTileCount += spaceData.AddNewTiles(iteration, previousSpaceJump);
- // Does adding these tiles bring us above the total target intensity?
- tilesInIteration.Add(newTileCount);
- if (newTileCount * stepSize >= remainingIntensity)
- {
- iterationIntensity.Add(remainingIntensity / newTileCount);
- break;
- }
- // add the new tiles and decrement available intensity
- remainingIntensity -= newTileCount * stepSize;
- iterationIntensity.Add(stepSize);
- totalTiles += newTileCount;
- // It is possible that the explosion has some max intensity and is stuck in a container whose walls it
- // cannot break. if the remaining intensity remains unchanged TWO loops in a row, we know that this is the
- // case.
- if (intensityUnchangedLastLoop && remainingIntensity == previousIntensity)
- break;
- intensityUnchangedLastLoop = remainingIntensity == previousIntensity;
- iteration += 1;
- }
- // Neighbor finding is done. Perform final clean up and return.
- foreach (var grid in gridData.Values)
- {
- grid.CleanUp();
- }
- spaceData?.CleanUp();
- return (totalTiles, iterationIntensity, spaceData, gridData, spaceMatrix);
- }
- /// <summary>
- /// Look for grids in an area and returns them. Also selects a special grid that will be used to determine the
- /// orientation of an explosion in space.
- /// </summary>
- /// <remarks>
- /// Note that even though an explosion may start ON a grid, the explosion in space may still be orientated to
- /// match a separate grid. This is done so that if you have something like a tiny suicide-bomb shuttle exploding
- /// near a large station, the explosion will still orient to match the station, not the tiny shuttle.
- /// </remarks>
- public (List<EntityUid>, EntityUid?, float) GetLocalGrids(MapCoordinates epicenter, float totalIntensity, float slope, float maxIntensity)
- {
- // Get the explosion radius (approx radius if it were in open-space). Note that if the explosion is confined in
- // some directions but not in others, the actual explosion may reach further than this distance from the
- // epicenter. Conversely, it might go nowhere near as far.
- var radius = 0.5f + IntensityToRadius(totalIntensity, slope, maxIntensity);
- // to avoid a silly lookup for silly input numbers, cap the radius to half of the theoretical maximum (lookup area gets doubled later on).
- radius = Math.Min(radius, MaxIterations / 4);
- EntityUid? referenceGrid = null;
- float mass = 0;
- // First attempt to find a grid that is relatively close to the explosion's center. Instead of looking in a
- // diameter x diameter sized box, use a smaller box with radius sized sides:
- var box = Box2.CenteredAround(epicenter.Position, new Vector2(radius, radius));
- foreach (var grid in _mapManager.FindGridsIntersecting(epicenter.MapId, box))
- {
- if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
- {
- mass = physics.Mass;
- referenceGrid = grid.Owner;
- }
- }
- // Next, we use a much larger lookup to determine all grids relevant to the explosion. This is used to determine
- // what grids should be included during the grid-edge transformation steps. This means that if a grid is not in
- // this set, the explosion can never propagate from space onto this grid.
- // As mentioned before, the `diameter` is only indicative, as an explosion that is obstructed (e.g., in a
- // tunnel) may travel further away from the epicenter. But this should be very rare for space-traversing
- // explosions. So instead of using the largest possible distance that an explosion could theoretically travel
- // and using that for the grid look-up, we will just arbitrarily fudge the lookup size to be twice the diameter.
- radius *= 4;
- box = Box2.CenteredAround(epicenter.Position, new Vector2(radius, radius));
- var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
- var grids = mapGrids.Select(x => x.Owner).ToList();
- if (referenceGrid != null)
- return (grids, referenceGrid, radius);
- // We still don't have are reference grid. So lets also look in the enlarged region
- foreach (var grid in mapGrids)
- {
- if (TryComp(grid.Owner, out PhysicsComponent? physics) && physics.Mass > mass)
- {
- mass = physics.Mass;
- referenceGrid = grid.Owner;
- }
- }
- return (grids, referenceGrid, radius);
- }
- public ExplosionVisualsState? GenerateExplosionPreview(SpawnExplosionEuiMsg.PreviewRequest request)
- {
- var stopwatch = new Stopwatch();
- stopwatch.Start();
- var results = GetExplosionTiles(
- request.Epicenter,
- request.TypeId,
- request.TotalIntensity,
- request.IntensitySlope,
- request.MaxIntensity);
- if (results == null)
- return null;
- var (area, iterationIntensity, spaceData, gridData, spaceMatrix) = results.Value;
- Log.Info($"Generated explosion preview with {area} tiles in {stopwatch.Elapsed.TotalMilliseconds}ms");
- Dictionary<NetEntity, Dictionary<int, List<Vector2i>>> tileLists = new();
- foreach (var (grid, data) in gridData)
- {
- tileLists.Add(GetNetEntity(grid), data.TileLists);
- }
- return new ExplosionVisualsState(
- request.Epicenter,
- request.TypeId,
- iterationIntensity,
- spaceData?.TileLists,
- tileLists, spaceMatrix,
- spaceData?.TileSize ?? DefaultTileSize
- );
- }
- }
|