using System.Numerics;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem
{
///
/// Used to limit explosion processing time. See .
///
internal readonly Stopwatch Stopwatch = new();
///
/// How many tiles to explode before checking the stopwatch timer
///
internal static int TileCheckIteration = 1;
///
/// Queue for delayed processing of explosions. If there is an explosion that covers more than tiles, other explosions will actually be delayed slightly. Unless it's a station
/// nuke, this delay should never really be noticeable.
/// This is also used to combine explosion intensities of the same kind.
///
private Queue _explosionQueue = new();
///
/// All queued explosions that will be processed in .
/// These always have the same contents.
///
private HashSet _queuedExplosions = new();
///
/// The explosion currently being processed.
///
private Explosion? _activeExplosion;
///
/// This list is used when raising to avoid allocating a new list per event.
///
private readonly List _containedEntities = new();
private readonly List<(EntityUid, DamageSpecifier)> _toDamage = new();
private List _anchored = new();
private void OnMapRemoved(MapRemovedEvent ev)
{
// If a map was deleted, check the explosion currently being processed belongs to that map.
if (_activeExplosion?.Epicenter.MapId != ev.MapId)
return;
QueueDel(_activeExplosion.VisualEnt);
_activeExplosion = null;
_nodeGroupSystem.PauseUpdating = false;
_pathfindingSystem.PauseUpdating = false;
}
///
/// Process the explosion queue.
///
public override void Update(float frameTime)
{
if (_activeExplosion == null && _explosionQueue.Count == 0)
// nothing to do
return;
Stopwatch.Restart();
var x = Stopwatch.Elapsed.TotalMilliseconds;
var tilesRemaining = TilesPerTick;
while (tilesRemaining > 0 && MaxProcessingTime > Stopwatch.Elapsed.TotalMilliseconds)
{
// if there is no active explosion, get a new one to process
if (_activeExplosion == null)
{
// EXPLOSION TODO allow explosion spawning to be interrupted by time limit. In the meantime, ensure that
// there is at-least 1ms of time left before creating a new explosion
if (MathF.Max(MaxProcessingTime - 1, 0.1f) < Stopwatch.Elapsed.TotalMilliseconds)
break;
if (!_explosionQueue.TryDequeue(out var queued))
break;
_queuedExplosions.Remove(queued);
_activeExplosion = SpawnExplosion(queued);
// explosion spawning can be null if something somewhere went wrong. (e.g., negative explosion
// intensity).
if (_activeExplosion == null)
continue;
// just a lil nap
if (SleepNodeSys)
{
_nodeGroupSystem.PauseUpdating = true;
_pathfindingSystem.PauseUpdating = true;
// snooze grid-chunk regeneration?
// snooze power network (recipients look for new suppliers as wires get destroyed).
}
if (_activeExplosion.Area > SingleTickAreaLimit)
break; // start processing next turn.
}
// TODO EXPLOSION check if active explosion is on a paused map. If it is... I guess support swapping out &
// storing the "currently active" explosion?
#if EXCEPTION_TOLERANCE
try
{
#endif
var processed = _activeExplosion.Process(tilesRemaining);
tilesRemaining -= processed;
// has the explosion finished processing?
if (_activeExplosion.FinishedProcessing)
{
var comp = EnsureComp(_activeExplosion.VisualEnt);
comp.Lifetime = _cfg.GetCVar(CCVars.ExplosionPersistence);
_appearance.SetData(_activeExplosion.VisualEnt, ExplosionAppearanceData.Progress, int.MaxValue);
_activeExplosion = null;
}
#if EXCEPTION_TOLERANCE
}
catch (Exception)
{
// Ensure the system does not get stuck in an error-loop.
if (_activeExplosion != null)
QueueDel(_activeExplosion.VisualEnt);
_activeExplosion = null;
_nodeGroupSystem.PauseUpdating = false;
_pathfindingSystem.PauseUpdating = false;
throw;
}
#endif
}
Log.Info($"Processed {TilesPerTick - tilesRemaining} tiles in {Stopwatch.Elapsed.TotalMilliseconds}ms");
// we have finished processing our tiles. Is there still an ongoing explosion?
if (_activeExplosion != null)
{
_appearance.SetData(_activeExplosion.VisualEnt, ExplosionAppearanceData.Progress, _activeExplosion.CurrentIteration + 1);
return;
}
if (_explosionQueue.Count > 0)
return;
//wakey wakey
_nodeGroupSystem.PauseUpdating = false;
_pathfindingSystem.PauseUpdating = false;
}
///
/// Determines whether an entity is blocking a tile or not. (whether it can prevent the tile from being uprooted
/// by an explosion).
///
///
/// Used for a variation of that makes use of the fact that we have
/// already done an entity lookup on a tile, and don't need to do so again.
///
public bool IsBlockingTurf(EntityUid uid)
{
if (EntityManager.IsQueuedForDeletion(uid))
return false;
if (!_physicsQuery.TryGetComponent(uid, out var physics))
return false;
return physics.CanCollide && physics.Hard && (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0;
}
///
/// Find entities on a grid tile using the EntityLookupComponent and apply explosion effects.
///
/// True if the underlying tile can be uprooted, false if the tile is blocked by a dense entity
internal bool ExplodeTile(BroadphaseComponent lookup,
Entity grid,
Vector2i tile,
float throwForce,
DamageSpecifier damage,
MapCoordinates epicenter,
HashSet processed,
string id,
float? fireStacks,
EntityUid? cause)
{
var size = grid.Comp.TileSize;
var gridBox = new Box2(tile * size, (tile + 1) * size);
// get the entities on a tile. Note that we cannot process them directly, or we get
// enumerator-changed-while-enumerating errors.
List<(EntityUid, TransformComponent)> list = new();
var state = (list, processed, EntityManager.TransformQuery);
// get entities:
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
lookup.StaticTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
lookup.StaticSundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
// process those entities
foreach (var (uid, xform) in list)
{
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks, cause);
}
// process anchored entities
var tileBlocked = false;
_anchored.Clear();
_map.GetAnchoredEntities(grid, tile, _anchored);
foreach (var entity in _anchored)
{
processed.Add(entity);
ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks, cause);
}
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
// the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return
// information about the entities that were spawned as a result, but without that information we just have to
// re-check for new anchored entities. Compared to entity spawning & deleting, this should still be relatively minor.
if (_anchored.Count > 0)
{
_anchored.Clear();
_map.GetAnchoredEntities(grid, tile, _anchored);
foreach (var entity in _anchored)
{
tileBlocked |= IsBlockingTurf(entity);
}
}
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
// windows will be flung outwards, and not stay where they spawned. This is however somewhat unnecessary, and a
// prime candidate for computational cost-cutting. Alternatively, it would be nice if there was just some sort
// of spawned-on-destruction event that could be used to automatically assemble a list of new entities that need
// to be thrown.
//
// All things considered, until entity spawning & destruction is sped up, this isn't all that time consuming.
// And throwing is disabled for nukes anyways.
if (throwForce <= 0)
return !tileBlocked;
list.Clear();
lookup.DynamicTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
lookup.SundriesTree.QueryAabb(ref state, GridQueryCallback, gridBox, true);
foreach (var (uid, xform) in list)
{
// Here we only throw, no dealing damage. Containers n such might drop their entities after being destroyed, but
// they should handle their own damage pass-through, with their own damage reduction calculation.
ProcessEntity(uid, epicenter, null, throwForce, id, xform, null, cause);
}
return !tileBlocked;
}
private static bool GridQueryCallback(
ref (List<(EntityUid, TransformComponent)> List, HashSet Processed, EntityQuery XformQuery) state,
in EntityUid uid)
{
if (state.Processed.Add(uid) && state.XformQuery.TryGetComponent(uid, out var xform))
state.List.Add((uid, xform));
return true;
}
private static bool GridQueryCallback(
ref (List<(EntityUid, TransformComponent)> List, HashSet Processed, EntityQuery XformQuery) state,
in FixtureProxy proxy)
{
var owner = proxy.Entity;
return GridQueryCallback(ref state, in owner);
}
///
/// Same as , but for SPAAAAAAACE.
///
internal void ExplodeSpace(Entity lookup,
Matrix3x2 spaceMatrix,
Matrix3x2 invSpaceMatrix,
Vector2i tile,
float throwForce,
DamageSpecifier damage,
MapCoordinates epicenter,
HashSet processed,
string id,
float? fireStacks,
EntityUid? cause)
{
var gridBox = Box2.FromDimensions(tile * DefaultTileSize, new Vector2(DefaultTileSize, DefaultTileSize));
var worldBox = spaceMatrix.TransformBox(gridBox);
var list = new List<(EntityUid, TransformComponent)>();
var state = (list, processed, invSpaceMatrix, lookup.Owner, EntityManager.TransformQuery, gridBox, _transformSystem);
// get entities:
lookup.Comp.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.Comp.StaticTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.Comp.SundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.Comp.StaticSundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
foreach (var (uid, xform) in state.Item1)
{
processed.Add(uid);
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks, cause);
}
if (throwForce <= 0)
return;
// Also, throw any entities that were spawned as shrapnel. Compared to entity spawning & destruction, this extra
// lookup is relatively minor computational cost, and throwing is disabled for nukes anyways.
list.Clear();
lookup.Comp.DynamicTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
lookup.Comp.SundriesTree.QueryAabb(ref state, SpaceQueryCallback, worldBox, true);
foreach (var (uid, xform) in list)
{
ProcessEntity(uid, epicenter, null, throwForce, id, xform, fireStacks, cause);
}
}
private static bool SpaceQueryCallback(
ref (List<(EntityUid, TransformComponent)> List, HashSet Processed, Matrix3x2 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery XformQuery, Box2 GridBox, SharedTransformSystem System) state,
in EntityUid uid)
{
if (state.Processed.Contains(uid))
return true;
var xform = state.XformQuery.GetComponent(uid);
if (xform.ParentUid == state.LookupOwner)
{
// parented directly to the map, use local position
if (state.GridBox.Contains(Vector2.Transform(xform.LocalPosition, state.InvSpaceMatrix)))
state.List.Add((uid, xform));
return true;
}
// finally check if it intersects our tile
var wpos = state.System.GetWorldPosition(xform);
if (state.GridBox.Contains(Vector2.Transform(wpos, state.InvSpaceMatrix)))
state.List.Add((uid, xform));
return true;
}
private static bool SpaceQueryCallback(
ref (List<(EntityUid, TransformComponent)> List, HashSet Processed, Matrix3x2 InvSpaceMatrix, EntityUid LookupOwner, EntityQuery XformQuery, Box2 GridBox, SharedTransformSystem System) state,
in FixtureProxy proxy)
{
var uid = proxy.Entity;
return SpaceQueryCallback(ref state, in uid);
}
private DamageSpecifier GetDamage(EntityUid uid,
string id, DamageSpecifier damage)
{
// TODO Explosion Performance
// Cache this? I.e., instead of raising an event, check for a component?
var resistanceEv = new GetExplosionResistanceEvent(id);
RaiseLocalEvent(uid, ref resistanceEv);
resistanceEv.DamageCoefficient = Math.Max(0, resistanceEv.DamageCoefficient);
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (resistanceEv.DamageCoefficient != 1)
damage *= resistanceEv.DamageCoefficient;
return damage;
}
private void GetEntitiesToDamage(EntityUid uid, DamageSpecifier originalDamage, string prototype)
{
_toDamage.Clear();
// don't raise BeforeExplodeEvent if the entity is completely immune to explosions
var thisDamage = GetDamage(uid, prototype, originalDamage);
if (thisDamage.Empty)
return;
_toDamage.Add((uid, thisDamage));
for (var i = 0; i < _toDamage.Count; i++)
{
var (ent, damage) = _toDamage[i];
_containedEntities.Clear();
var ev = new BeforeExplodeEvent(damage, prototype, _containedEntities);
RaiseLocalEvent(ent, ref ev);
if (_containedEntities.Count == 0)
continue;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (ev.DamageCoefficient != 1)
damage *= ev.DamageCoefficient;
_toDamage.EnsureCapacity(_toDamage.Count + _containedEntities.Count);
foreach (var contained in _containedEntities)
{
var newDamage = GetDamage(contained, prototype, damage);
_toDamage.Add((contained, newDamage));
}
}
}
///
/// This function actually applies the explosion affects to an entity.
///
private void ProcessEntity(
EntityUid uid,
MapCoordinates epicenter,
DamageSpecifier? originalDamage,
float throwForce,
string id,
TransformComponent? xform,
float? fireStacksOnIgnite,
EntityUid? cause)
{
if (originalDamage is not null)
{
GetEntitiesToDamage(uid, originalDamage, id);
foreach (var (entity, damage) in _toDamage)
{
if (!_damageableQuery.TryComp(entity, out var damageable))
continue;
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage((entity, damageable), damage, ignoreResistances: true, ignoreGlobalModifiers: true);
if (_actorQuery.HasComp(entity))
{
// Log damage to player entities only; this will create a massive amount of log spam otherwise.
if (cause is not null)
_adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion of {ToPrettyString(cause):actor} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
else
_adminLogger.Add(LogType.ExplosionHit, LogImpact.Medium, $"Explosion at {epicenter:epicenter} dealt {damage.GetTotal()} damage to {ToPrettyString(entity):subject}");
}
}
}
// ignite
if (fireStacksOnIgnite != null)
{
if (_flammableQuery.TryGetComponent(uid, out var flammable))
{
flammable.FireStacks += fireStacksOnIgnite.Value;
_flammableSystem.Ignite(uid, uid, flammable);
}
}
// throw
if (xform != null // null implies anchored or in a container
&& !xform.Anchored
&& throwForce > 0
&& !EntityManager.IsQueuedForDeletion(uid)
&& _physicsQuery.TryGetComponent(uid, out var physics)
&& physics.BodyType == BodyType.Dynamic)
{
var pos = _transformSystem.GetWorldPosition(xform);
var dir = pos - epicenter.Position;
if (dir.IsLengthZero())
dir = _robustRandom.NextVector2().Normalized();
_throwingSystem.TryThrow(
uid,
dir,
physics,
xform,
_projectileQuery,
throwForce);
}
}
///
/// Tries to damage floor tiles. Not to be confused with the function that damages entities intersecting the
/// grid tile.
///
public void DamageFloorTile(TileRef tileRef,
float effectiveIntensity,
int maxTileBreak,
bool canCreateVacuum,
List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
ExplosionPrototype type)
{
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef
|| tileDef.Indestructible)
return;
if (!CanCreateVacuum)
canCreateVacuum = false;
else if (tileDef.MapAtmosphere)
canCreateVacuum = true; // is already a vacuum.
int tileBreakages = 0;
while (maxTileBreak > tileBreakages && _robustRandom.Prob(type.TileBreakChance(effectiveIntensity)))
{
tileBreakages++;
effectiveIntensity -= type.TileBreakRerollReduction;
// does this have a base-turf that we can break it down to?
if (string.IsNullOrEmpty(tileDef.BaseTurf))
break;
if (_tileDefinitionManager[tileDef.BaseTurf] is not ContentTileDefinition newDef)
break;
if (newDef.MapAtmosphere && !canCreateVacuum)
break;
tileDef = newDef;
}
if (tileDef.TileId == tileRef.Tile.TypeId)
return;
damagedTiles.Add((tileRef.GridIndices, new Tile(tileDef.TileId)));
}
}
///
/// This is a data class that stores information about the area affected by an explosion, for processing by .
///
///
/// This is basically the output of , but with some utility functions for
/// iterating over the tiles, along with the ability to keep track of what entities have already been damaged by
/// this explosion.
///
sealed class Explosion
{
///
/// For every grid (+ space) that the explosion reached, this data struct stores information about the tiles and
/// caches the entity-lookup component so that it doesn't have to be re-fetched for every tile.
///
struct ExplosionData
{
///
/// The tiles that the explosion damaged, grouped by the iteration (can be thought of as the distance from the epicenter)
///
public Dictionary> TileLists;
///
/// Lookup component for this grid (or space/map).
///
public Entity Lookup;
///
/// The actual grid that this corresponds to. If null, this implies space.
///
public Entity? MapGrid;
}
private readonly List _explosionData = new();
///
/// The explosion intensity associated with each tile iteration.
///
private readonly List _tileSetIntensity;
///
/// Used to avoid applying explosion effects repeatedly to the same entity. Particularly important if the
/// explosion throws this entity, as then it will be moving while the explosion is happening.
///
public readonly HashSet ProcessedEntities = new();
///
/// This integer tracks how much of this explosion has been processed.
///
public int CurrentIteration { get; private set; } = 0;
///
/// The prototype for this explosion. Determines tile break chance, damage, etc.
///
public readonly ExplosionPrototype ExplosionType;
///
/// The center of the explosion. Used for physics throwing. Also used to identify the map on which the explosion is happening.
///
public readonly MapCoordinates Epicenter;
///
/// The matrix that defines the reference frame for the explosion in space.
///
private readonly Matrix3x2 _spaceMatrix;
///
/// Inverse of
///
private readonly Matrix3x2 _invSpaceMatrix;
///
/// Have all the tiles on all the grids been processed?
///
public bool FinishedProcessing;
// Variables used for enumerating over tiles, grids, etc
private DamageSpecifier _currentDamage = default!;
#if DEBUG
private DamageSpecifier? _expectedDamage;
#endif
private Entity _currentLookup = default!;
private Entity? _currentGrid;
private float _currentIntensity;
private float _currentThrowForce;
private List.Enumerator _currentEnumerator;
private int _currentDataIndex;
///
/// The set of tiles that need to be updated when the explosion has finished processing. Used to avoid having
/// the explosion trigger chunk regeneration & shuttle-system processing every tick.
///
private readonly Dictionary, List<(Vector2i, Tile)>> _tileUpdateDict = new();
// Entity Queries
private readonly EntityQuery _xformQuery;
private readonly EntityQuery _physicsQuery;
private readonly EntityQuery _damageQuery;
private readonly EntityQuery _projectileQuery;
private readonly EntityQuery _tagQuery;
///
/// Total area that the explosion covers.
///
public readonly int Area;
///
/// factor used to scale the tile break chances.
///
private readonly float _tileBreakScale;
///
/// Maximum number of times that an explosion will break a single tile.
///
private readonly int _maxTileBreak;
///
/// Whether this explosion can turn non-vacuum tiles into vacuum-tiles.
///
private readonly bool _canCreateVacuum;
private readonly IEntityManager _entMan;
private readonly ExplosionSystem _system;
private readonly SharedMapSystem _mapSystem;
private readonly Shared.Damage.Systems.DamageableSystem _damageable;
public readonly EntityUid VisualEnt;
public readonly EntityUid? Cause;
///
/// Initialize a new instance for processing
///
public Explosion(ExplosionSystem system,
ExplosionPrototype explosionType,
ExplosionSpaceTileFlood? spaceData,
List gridData,
List tileSetIntensity,
MapCoordinates epicenter,
Matrix3x2 spaceMatrix,
int area,
float tileBreakScale,
int maxTileBreak,
bool canCreateVacuum,
IEntityManager entMan,
EntityUid visualEnt,
EntityUid? cause,
SharedMapSystem mapSystem,
Shared.Damage.Systems.DamageableSystem damageable)
{
VisualEnt = visualEnt;
Cause = cause;
_system = system;
_mapSystem = mapSystem;
ExplosionType = explosionType;
_tileSetIntensity = tileSetIntensity;
Epicenter = epicenter;
Area = area;
_tileBreakScale = tileBreakScale;
_maxTileBreak = maxTileBreak;
_canCreateVacuum = canCreateVacuum;
_entMan = entMan;
_damageable = damageable;
_xformQuery = entMan.GetEntityQuery();
_physicsQuery = entMan.GetEntityQuery();
_damageQuery = entMan.GetEntityQuery();
_tagQuery = entMan.GetEntityQuery();
_projectileQuery = entMan.GetEntityQuery();
if (spaceData != null)
{
var mapUid = mapSystem.GetMap(epicenter.MapId);
_explosionData.Add(new()
{
TileLists = spaceData.TileLists,
Lookup = (mapUid, entMan.GetComponent(mapUid)),
MapGrid = null
});
_spaceMatrix = spaceMatrix;
Matrix3x2.Invert(spaceMatrix, out _invSpaceMatrix);
}
foreach (var grid in gridData)
{
_explosionData.Add(new ExplosionData
{
TileLists = grid.TileLists,
Lookup = (grid.Grid, entMan.GetComponent(grid.Grid)),
MapGrid = grid.Grid,
});
}
if (TryGetNextTileEnumerator())
MoveNext();
}
///
/// Find the next tile-enumerator. This either means retrieving a set of tiles on the next grid, or incrementing
/// the tile iteration by one and moving back to the first grid. This will also update the current damage, current entity-lookup, etc.
///
private bool TryGetNextTileEnumerator()
{
while (CurrentIteration < _tileSetIntensity.Count)
{
_currentIntensity = _tileSetIntensity[CurrentIteration];
#if DEBUG
if (_expectedDamage != null)
{
// Check that explosion processing hasn't somehow accidentally mutated the damage set.
DebugTools.Assert(_expectedDamage.Equals(_currentDamage));
_expectedDamage = ExplosionType.DamagePerIntensity * _currentIntensity;
}
#endif
var modifier = _currentIntensity
* _damageable.UniversalExplosionDamageModifier
* _damageable.UniversalAllDamageModifier;
_currentDamage = ExplosionType.DamagePerIntensity * modifier;
// only throw if either the explosion is small, or if this is the outer ring of a large explosion.
var doThrow = Area < _system.ThrowLimit || CurrentIteration > _tileSetIntensity.Count - 6;
_currentThrowForce = doThrow ? 10 * MathF.Sqrt(_currentIntensity) : 0;
// for each grid/space tile set
while (_currentDataIndex < _explosionData.Count)
{
// try get any tile hash-set corresponding to this intensity
var tileSets = _explosionData[_currentDataIndex].TileLists;
if (!tileSets.TryGetValue(CurrentIteration, out var tileList))
{
_currentDataIndex++;
continue;
}
_currentEnumerator = tileList.GetEnumerator();
_currentLookup = _explosionData[_currentDataIndex].Lookup;
_currentGrid = _explosionData[_currentDataIndex].MapGrid;
_currentDataIndex++;
// sanity checks, in case something changed while the explosion was being processed over several ticks.
if (_currentLookup.Comp.Deleted || _currentGrid != null && !_entMan.EntityExists(_currentGrid.Value))
continue;
return true;
}
// All the tiles belonging to this explosion iteration have been processed. Move onto the next iteration and
// reset the grid counter.
CurrentIteration++;
_currentDataIndex = 0;
}
// No more explosion tiles to process
FinishedProcessing = true;
return false;
}
///
/// Get the next tile that needs processing
///
private bool MoveNext()
{
if (FinishedProcessing)
return false;
while (!FinishedProcessing)
{
if (_currentEnumerator.MoveNext())
return true;
else
TryGetNextTileEnumerator();
}
return false;
}
///
/// Attempt to process (i.e., damage entities) some number of grid tiles.
///
public int Process(int processingTarget)
{
// In case the explosion terminated early last tick due to exceeding the allocated processing time, use this
// time to update the tiles.
SetTiles();
int processed;
for (processed = 0; processed < processingTarget; processed++)
{
if (processed % ExplosionSystem.TileCheckIteration == 0 &&
_system.Stopwatch.Elapsed.TotalMilliseconds > _system.MaxProcessingTime)
{
break;
}
// Is the current tile on a grid (instead of in space)?
if (_currentGrid is { } currentGrid &&
_mapSystem.TryGetTileRef(currentGrid, currentGrid.Comp, _currentEnumerator.Current, out var tileRef) &&
!tileRef.Tile.IsEmpty)
{
if (!_tileUpdateDict.TryGetValue(currentGrid, out var tileUpdateList))
{
tileUpdateList = new();
_tileUpdateDict[currentGrid] = tileUpdateList;
}
// damage entities on the tile. Also figures out whether there are any solid entities blocking the floor
// from being destroyed.
var canDamageFloor = _system.ExplodeTile(_currentLookup,
currentGrid,
_currentEnumerator.Current,
_currentThrowForce,
_currentDamage,
Epicenter,
ProcessedEntities,
ExplosionType.ID,
ExplosionType.FireStacks,
Cause);
// If the floor is not blocked by some dense object, damage the floor tiles.
if (canDamageFloor)
_system.DamageFloorTile(tileRef, _currentIntensity * _tileBreakScale, _maxTileBreak, _canCreateVacuum, tileUpdateList, ExplosionType);
}
else
{
// The current "tile" is in space. Damage any entities in that region
_system.ExplodeSpace(_currentLookup,
_spaceMatrix,
_invSpaceMatrix,
_currentEnumerator.Current,
_currentThrowForce,
_currentDamage,
Epicenter,
ProcessedEntities,
ExplosionType.ID,
ExplosionType.FireStacks,
Cause);
}
if (!MoveNext())
break;
}
// Update damaged/broken tiles on the grid.
SetTiles();
return processed;
}
private void SetTiles()
{
// Updating the grid can result in chunk collision regeneration & slow processing by the shuttle system.
// Therefore, tile breaking may be configure to only happen at the end of an explosion, rather than during every
// tick.
if (!_system.IncrementalTileBreaking && !FinishedProcessing)
return;
foreach (var (grid, list) in _tileUpdateDict)
{
if (list.Count > 0 && _entMan.EntityExists(grid.Owner))
{
_mapSystem.SetTiles(grid.Owner, grid, list);
}
}
_tileUpdateDict.Clear();
}
}
///
/// Data needed to spawn an explosion with .
///
public sealed class QueuedExplosion(ExplosionPrototype proto)
{
public MapCoordinates Epicenter;
public ExplosionPrototype Proto = proto;
public float TotalIntensity, Slope, MaxTileIntensity, TileBreakScale;
public int MaxTileBreak;
public bool CanCreateVacuum;
public EntityUid? Cause; // The entity that exploded, for logging purposes.
}