* move component to shared * add fake systems * update server explosion system and remove duplicate transform query --------- Co-authored-by: deltanedas <@deltanedas:kde.org>
354 lines
15 KiB
C#
354 lines
15 KiB
C#
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);
|
|
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 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
|
|
);
|
|
}
|
|
}
|