--- /dev/null
+using Content.Server.Explosion.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Explosion.Components;
+
+/// <summary>
+/// Stores data for airtight explosion traversal on a <see cref="MapGridComponent"/> entity.
+/// </summary>
+/// <seealso cref="ExplosionSystem"/>
+[RegisterComponent]
+[Access(typeof(ExplosionSystem), Other = AccessPermissions.None)]
+public sealed partial class ExplosionAirtightGridComponent : Component
+{
+ /// <summary>
+ /// Data for every tile on the current grid.
+ /// </summary>
+ /// <remarks>
+ /// Intentionally not saved.
+ /// </remarks>
+ [ViewVariables]
+ public readonly Dictionary<Vector2i, TileData> Tiles = new();
+
+ /// <summary>
+ /// Data struct that describes the explosion-blocking airtight entities on a tile.
+ /// </summary>
+ public struct TileData
+ {
+ /// <summary>
+ /// Which index into the tolerance cache of <see cref="ExplosionSystem"/> this tile is using.
+ /// </summary>
+ public required int ToleranceCacheIndex;
+
+ /// <summary>
+ /// Which directions this tile is blocking explosions in. Bitflag field.
+ /// </summary>
+ public required AtmosDirection BlockedDirections;
+ }
+
+ /// <summary>
+ /// A set of tolerance values
+ /// </summary>
+ public struct ToleranceValues : IEquatable<ToleranceValues>
+ {
+ /// <summary>
+ /// Special value that indicates the entity is "invulnerable" against a specific explosion type.
+ /// </summary>
+ /// <remarks>
+ /// Here to deal with the limited range of <see cref="FixedPoint2"/> over typical floats.
+ /// </remarks>
+ public static readonly FixedPoint2 Invulnerable = FixedPoint2.MaxValue;
+
+ /// <summary>
+ /// The intensities at which explosions of each type can instantly break through an entity.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is an array, with the index of each value corresponding to the "explosion type ID" cached by
+ /// <see cref="ExplosionSystem"/>.
+ /// </para>
+ /// <para>
+ /// Values are stored as <see cref="FixedPoint2"/> to avoid possible precision issues resulting in
+ /// different-but-almost-identical tolerance values wasting memory.
+ /// </para>
+ /// <para>
+ /// If a value is <see cref="Invulnerable"/>, that indicates the tile is invulnerable.
+ /// </para>
+ /// </remarks>
+ public required FixedPoint2[] Values;
+
+ public bool Equals(ToleranceValues other)
+ {
+ return Values.AsSpan().SequenceEqual(other.Values);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is ToleranceValues other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ var hc = new HashCode();
+ hc.AddArray(Values);
+ return hc.ToHashCode();
+ }
+
+ public static bool operator ==(ToleranceValues left, ToleranceValues right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(ToleranceValues left, ToleranceValues right)
+ {
+ return !left.Equals(right);
+ }
+ }
+}
using System.Numerics;
using Content.Shared.Atmos;
-using Robust.Shared.Map;
+using Content.Shared.FixedPoint;
using Robust.Shared.Map.Components;
+using static Content.Server.Explosion.Components.ExplosionAirtightGridComponent;
using static Content.Server.Explosion.EntitySystems.ExplosionSystem;
namespace Content.Server.Explosion.EntitySystems;
/// </summary>
public sealed class ExplosionGridTileFlood : ExplosionTileFlood
{
+ private readonly ExplosionSystem _explosionSystem;
+
public Entity<MapGridComponent> Grid;
private bool _needToTransform = false;
Dictionary<Vector2i, NeighborFlag> edgeTiles,
EntityUid? referenceGrid,
Matrix3x2 spaceMatrix,
- Angle spaceAngle)
+ Angle spaceAngle,
+ ExplosionSystem explosionSystem)
{
Grid = grid;
_airtightMap = airtightMap;
_intensityStepSize = intensityStepSize;
_typeIndex = typeIndex;
_edgeTiles = edgeTiles;
+ _explosionSystem = explosionSystem;
// initialise SpaceTiles
foreach (var (tile, spaceNeighbors) in _edgeTiles)
NewBlockedTiles.Add(tile);
// At what explosion iteration would this blocker be destroyed?
- var required = tileData.ExplosionTolerance[_typeIndex];
+ var required = _explosionSystem.GetToleranceValues(tileData.ToleranceCacheIndex).Values[_typeIndex];
if (required > _maxIntensity)
return; // blocker is never destroyed.
- var clearIteration = iteration + (int) MathF.Ceiling(required / _intensityStepSize);
+ var clearIteration = iteration + (int) MathF.Ceiling((float)required / _intensityStepSize);
if (FreedTileLists.TryGetValue(clearIteration, out var list))
list.Add(tile);
else
foreach (var tile in tiles)
{
var blockedDirections = AtmosDirection.Invalid;
- float sealIntegrity = 0;
+ FixedPoint2 sealIntegrity = 0;
// Note that if (grid, tile) is not a valid key, then airtight.BlockedDirections will default to 0 (no blocked directions)
if (_airtightMap.TryGetValue(tile, out var tileData))
{
blockedDirections = tileData.BlockedDirections;
- sealIntegrity = tileData.ExplosionTolerance[_typeIndex];
+ sealIntegrity = _explosionSystem.GetToleranceValues(tileData.ToleranceCacheIndex).Values[_typeIndex];
}
// First, yield any neighboring tiles that are not blocked by airtight entities on this tile
continue;
// At what explosion iteration would this blocker be destroyed?
- var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize);
+ var clearIteration = iteration + (int) MathF.Ceiling((float) sealIntegrity / _intensityStepSize);
// Get the delayed neighbours list
if (!_delayedNeighbors.TryGetValue(clearIteration, out var list))
+using System.Linq;
+using System.Runtime.InteropServices;
using Content.Server.Atmos.Components;
+using Content.Server.Explosion.Components;
using Content.Shared.Atmos;
using Content.Shared.Damage.Systems;
using Content.Shared.Explosion;
using Content.Shared.FixedPoint;
+using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using static Content.Server.Explosion.Components.ExplosionAirtightGridComponent;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class ExplosionSystem
{
- private readonly Dictionary<string, int> _explosionTypes = new();
+ // We keep track of which tiles are airtight, and how much damage from explosions those airtight blockers can take.
+ // This is quite complicated, as the data effectively needs to be tracked *per tile*, *per explosion type*.
+ // To avoid wasting significant memory, we calculate the values and share the actual backing storage of it.
+ // Stored values are reference counted so they can be evicted when no longer needed.
+ // At the time of writing, this compacts the storage for Box Station from ~5500 tolerance value sets to 13,
+ // at round start.
+
+ // Use integers instead of prototype IDs for storage of explosion data.
+ // This allows us to replace a Dictionary<string, FixedPoint2> with just a FixedPoint2[].
+ private readonly Dictionary<ProtoId<ExplosionPrototype>, int> _explosionTypes = new();
+ // Index to look up if we already have an existing set of tolerance values stored, so the data can be shared.
+ private readonly Dictionary<ToleranceValues, int> _toleranceIndex = new();
+ // Storage for tolerance values. Entries form a free linked list when not occupied by a set of real values.
+ private ValueList<CacheEntry> _toleranceData;
+ // First free position in _toleranceData.
+ // -1 indicates there are no free slots left and the storage must be expanded.
+ private int _freeListHead = -1;
private void InitAirtightMap()
{
- // Currently explosion prototype hot-reload isn't supported, as it would involve completely re-computing the
- // airtight map. Could be done, just not yet implemented.
+ _explosionTypes.Clear();
- // for storing airtight entity damage thresholds for all anchored airtight entities, we will use integers in
- // place of id-strings. This initializes the string <--> id association.
- // This allows us to replace a Dictionary<string, float> with just a float[].
int index = 0;
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
{
- // TODO EXPLOSION
- // just make this a field on the prototype
_explosionTypes.Add(prototype.ID, index);
index++;
}
}
- // The explosion intensity required to break an entity depends on the explosion type. So it is stored in a
- // Dictionary<string, float>
- //
- // Hence, each tile has a tuple (Dictionary<string, float>, AtmosDirection). This specifies what directions are
- // blocked, and how intense a given explosion type needs to be in order to destroy ALL airtight entities on that
- // tile. This is the TileData struct.
- //
- // We then need this data for every tile on a grid. So this mess of a variable maps the Grid ID and Vector2i grid
- // indices to this tile-data struct.
- private Dictionary<EntityUid, Dictionary<Vector2i, TileData>> _airtightMap = new();
+ private void ReloadExplosionPrototypes(PrototypesReloadedEventArgs prototypesReloadedEventArgs)
+ {
+ if (!prototypesReloadedEventArgs.Modified.Contains(typeof(ExplosionPrototype)))
+ return;
+
+ InitAirtightMap();
+ ReloadMap();
+ }
public void UpdateAirtightMap(EntityUid gridId, Vector2i tile, MapGridComponent? grid = null)
{
UpdateAirtightMap(gridId, grid, tile);
}
+ [Access(typeof(ExplosionGridTileFlood))]
+ public ToleranceValues GetToleranceValues(int idx)
+ {
+ return _toleranceData[idx].Values;
+ }
+
/// <summary>
/// Update the map of explosion blockers.
/// </summary>
/// </remarks>
public void UpdateAirtightMap(EntityUid gridId, MapGridComponent grid, Vector2i tile)
{
- var tolerance = new float[_explosionTypes.Count];
- var blockedDirections = AtmosDirection.Invalid;
+ var airtightGrid = EnsureComp<ExplosionAirtightGridComponent>(gridId);
+
+ // Calculate tile new airtight state.
- if (!_airtightMap.ContainsKey(gridId))
- _airtightMap[gridId] = new();
+ var tolerance = new FixedPoint2[_explosionTypes.Count];
+ var blockedDirections = AtmosDirection.Invalid;
var anchoredEnumerator = _map.GetAnchoredEntitiesEnumerator(gridId, grid, tile);
continue;
blockedDirections |= airtight.AirBlockedDirection;
- var entityTolerances = GetExplosionTolerance(uid.Value);
- for (var i = 0; i < tolerance.Length; i++)
+ GetExplosionTolerance(uid.Value, tolerance);
+ }
+
+ // Log.Info($"UPDATE {gridId}/{tile}: {blockedDirections}");
+
+ if (blockedDirections == AtmosDirection.Invalid)
+ {
+ // No longer airtight
+
+ if (!airtightGrid.Tiles.Remove(tile, out var tileData))
{
- tolerance[i] = Math.Max(tolerance[i], entityTolerances[i]);
+ // Did not have this tile before and after, nothing to do.
+ return;
}
+
+ // Removing tile data.
+ DecrementRefCount(tileData.ToleranceCacheIndex);
+ return;
}
- if (blockedDirections != AtmosDirection.Invalid)
- _airtightMap[gridId][tile] = new(tolerance, blockedDirections);
+ ref var tileEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(airtightGrid.Tiles, tile, out var existed);
+ var cacheKey = new ToleranceValues { Values = tolerance };
+
+ // Remove previous tolerance reference if necessary.
+ if (existed)
+ {
+ ref var prevEntry = ref _toleranceData[tileEntry.ToleranceCacheIndex];
+ if (prevEntry.Values == cacheKey)
+ {
+ // No change.
+ return;
+ }
+
+ DecrementRefCount(tileEntry.ToleranceCacheIndex);
+ }
+
+ ref var newCacheIndex = ref CollectionsMarshal.GetValueRefOrAddDefault(_toleranceIndex, cacheKey, out existed);
+ if (existed)
+ {
+ _toleranceData[newCacheIndex].RefCount += 1;
+ }
else
- _airtightMap[gridId].Remove(tile);
+ {
+ if (_freeListHead < 0)
+ ExpandCache();
+
+ newCacheIndex = _freeListHead;
+ ref var newCacheEntry = ref _toleranceData[newCacheIndex];
+ _freeListHead = newCacheEntry.RefCount;
+
+ newCacheEntry.Values = cacheKey;
+ newCacheEntry.RefCount = 1;
+ }
+
+ tileEntry = new TileData
+ {
+ BlockedDirections = blockedDirections,
+ ToleranceCacheIndex = newCacheIndex,
+ };
+ }
+
+ private void ExpandCache()
+ {
+ var newCacheSize = Math.Max(8, _toleranceData.Count * 2);
+ var curSize = _toleranceData.Count;
+
+ _toleranceData.EnsureLength(newCacheSize);
+ for (var i = curSize; i < newCacheSize; i++)
+ {
+ _toleranceData[i].RefCount = _freeListHead;
+ _freeListHead = i;
+ }
+ }
+
+ private void DecrementRefCount(int index)
+ {
+ ref var cacheEntry = ref _toleranceData[index];
+
+ DebugTools.Assert(cacheEntry.RefCount > 0);
+ cacheEntry.RefCount -= 1;
+
+ if (cacheEntry.RefCount == 0)
+ {
+ var prevValue = cacheEntry.Values;
+ cacheEntry.Values = default;
+ cacheEntry.RefCount = _freeListHead;
+ _freeListHead = index;
+
+ var result = _toleranceIndex.Remove(prevValue);
+ DebugTools.Assert(result, "Failed to removed 0 refcounted index!");
+ }
}
/// <summary>
/// <summary>
/// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity.
/// </summary>
- public float[] GetExplosionTolerance(EntityUid uid)
+ private void GetExplosionTolerance(EntityUid uid, Span<FixedPoint2> explosionTolerance)
{
// How much total damage is needed to destroy this entity? This also includes "break" behaviors. This ASSUMES
// that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
}
- var explosionTolerance = new float[_explosionTypes.Count];
if (totalDamageTarget == FixedPoint2.MaxValue || !_damageableQuery.TryGetComponent(uid, out var damageable))
{
for (var i = 0; i < explosionTolerance.Length; i++)
{
- explosionTolerance[i] = float.MaxValue;
+ explosionTolerance[i] = ToleranceValues.Invulnerable;
}
- return explosionTolerance;
+
+ return;
}
// What multiple of each explosion type damage set will result in the damage exceeding the required amount? This
damagePerIntensity += value * mod * Math.Max(0, ev.DamageCoefficient);
}
- explosionTolerance[index] = damagePerIntensity > 0
+ var toleranceValue = damagePerIntensity > 0
? (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity)
- : float.MaxValue;
- }
+ : ToleranceValues.Invulnerable;
- return explosionTolerance;
+ explosionTolerance[index] = toleranceValue;
+ }
}
- /// <summary>
- /// Data struct that describes the explosion-blocking airtight entities on a tile.
- /// </summary>
- public struct TileData
+ private void OnAirtightGridRemoved(EntityUid entity)
{
- public TileData(float[] explosionTolerance, AtmosDirection blockedDirections)
+ if (!TryComp(entity, out ExplosionAirtightGridComponent? airtightGrid))
+ return;
+
+ foreach (var tile in airtightGrid.Tiles.Values)
{
- ExplosionTolerance = explosionTolerance;
- BlockedDirections = blockedDirections;
+ DecrementRefCount(tile.ToleranceCacheIndex);
}
- public float[] ExplosionTolerance;
- public AtmosDirection BlockedDirections = AtmosDirection.Invalid;
+ RemComp<ExplosionAirtightGridComponent>(entity);
}
public override void ReloadMap()
{
- foreach (var(grid, dict) in _airtightMap)
+ var enumerator = EntityQueryEnumerator<ExplosionAirtightGridComponent, MapGridComponent>();
+ while (enumerator.MoveNext(out var uid, out var airtightComp, out var mapGrid))
{
- var comp = Comp<MapGridComponent>(grid);
- foreach (var index in dict.Keys)
+ foreach (var pos in airtightComp.Tiles.Keys)
{
- UpdateAirtightMap(grid, comp, index);
+ UpdateAirtightMap(uid, pos, mapGrid);
}
}
}
+
+ private struct CacheEntry
+ {
+ public ToleranceValues Values;
+ public int RefCount; // Doubles as freelist chain
+ }
+
}