@@ -1,334 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Explosion;
|
||||
using Robust.Shared.Map;
|
||||
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 : EntitySystem
|
||||
{
|
||||
/// <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>, SpaceExplosion?, Dictionary<GridId, GridExplosion>, Matrix3)? GetExplosionTiles(
|
||||
MapCoordinates epicenter,
|
||||
string typeID,
|
||||
float totalIntensity,
|
||||
float slope,
|
||||
float maxIntensity)
|
||||
{
|
||||
if (totalIntensity <= 0 || slope <= 0)
|
||||
return null;
|
||||
|
||||
Vector2i initialTile;
|
||||
GridId? epicentreGrid = null;
|
||||
var (localGrids, referenceGrid) = GetLocalGrids(epicenter, totalIntensity, slope, maxIntensity);
|
||||
|
||||
// get the epicenter tile indices
|
||||
if (_mapManager.TryFindGridAt(epicenter, out var candidateGrid) &&
|
||||
candidateGrid.TryGetTileRef(candidateGrid.WorldToTile(epicenter.Position), out var tileRef) &&
|
||||
!tileRef.Tile.IsEmpty)
|
||||
{
|
||||
epicentreGrid = candidateGrid.Index;
|
||||
initialTile = tileRef.GridIndices;
|
||||
}
|
||||
else if (referenceGrid != null)
|
||||
{
|
||||
// reference grid defines coordinate system that the explosion in space will use
|
||||
initialTile = _mapManager.GetGrid(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<GridId, GridExplosion> gridData = new();
|
||||
SpaceExplosion? 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<GridId> encounteredGrids = new();
|
||||
Dictionary<GridId, HashSet<Vector2i>>? previousGridJump;
|
||||
|
||||
// variables for transforming between grid and space-coordiantes
|
||||
var spaceMatrix = Matrix3.Identity;
|
||||
var spaceAngle = Angle.Zero;
|
||||
if (referenceGrid != null)
|
||||
{
|
||||
var xform = Transform(_mapManager.GetGrid(referenceGrid.Value).GridEntityId);
|
||||
spaceMatrix = xform.WorldMatrix;
|
||||
spaceAngle = xform.WorldRotation;
|
||||
}
|
||||
|
||||
// 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 GridExplosion(
|
||||
_mapManager.GetGrid(epicentreGrid.Value),
|
||||
airtightMap,
|
||||
maxIntensity,
|
||||
stepSize,
|
||||
typeID,
|
||||
_gridEdges[epicentreGrid.Value],
|
||||
referenceGrid,
|
||||
spaceMatrix,
|
||||
spaceAngle);
|
||||
|
||||
gridData[epicentreGrid.Value] = initialGridData;
|
||||
|
||||
initialGridData.InitTile(initialTile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// set up the space explosion data
|
||||
spaceData = new SpaceExplosion(this, epicenter.MapId, referenceGrid, localGrids);
|
||||
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 = 0;
|
||||
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] += (float) 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 GridExplosion(
|
||||
_mapManager.GetGrid(grid),
|
||||
airtightMap,
|
||||
maxIntensity,
|
||||
stepSize,
|
||||
typeID,
|
||||
_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 SpaceExplosion(this, epicenter.MapId, referenceGrid, localGrids);
|
||||
|
||||
// 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((float) 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;
|
||||
}
|
||||
|
||||
if (totalTiles >= MaxArea)
|
||||
Logger.Info("Whooooo! MAXCAP!");
|
||||
|
||||
// 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<GridId>, GridId?) 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);
|
||||
|
||||
GridId? 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, (radius, radius));
|
||||
|
||||
foreach (var grid in _mapManager.FindGridsIntersecting(epicenter.MapId, box))
|
||||
{
|
||||
if (TryComp(grid.GridEntityId, out PhysicsComponent? physics) && physics.Mass > mass)
|
||||
{
|
||||
mass = physics.Mass;
|
||||
referenceGrid = grid.Index;
|
||||
}
|
||||
}
|
||||
|
||||
// Next, we use a much larger lookup to determine all grids relevant to the explosion. This is used to determine
|
||||
// what grids should be includes 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.
|
||||
|
||||
box = box.Scale(4); // box with width and height of 4*radius.
|
||||
var mapGrids = _mapManager.FindGridsIntersecting(epicenter.MapId, box).ToList();
|
||||
var grids = mapGrids.Select(x => x.Index).ToList();
|
||||
|
||||
if (referenceGrid != null)
|
||||
return (grids, referenceGrid);
|
||||
|
||||
// We still don't have are reference grid. So lets also look in the enlarged region
|
||||
foreach (var grid in mapGrids)
|
||||
{
|
||||
if (TryComp(grid.GridEntityId, out PhysicsComponent? physics) && physics.Mass > mass)
|
||||
{
|
||||
mass = physics.Mass;
|
||||
referenceGrid = grid.Index;
|
||||
}
|
||||
}
|
||||
|
||||
return (grids, referenceGrid);
|
||||
}
|
||||
|
||||
public ExplosionEvent? 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;
|
||||
|
||||
Logger.Info($"Generated explosion preview with {area} tiles in {stopwatch.Elapsed.TotalMilliseconds}ms");
|
||||
|
||||
// the explosion event that **would** be sent to all clients, if it were a real explosion.
|
||||
return GetExplosionEvent(request.Epicenter, request.TypeId, spaceMatrix, spaceData, gridData.Values, iterationIntensity);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user