diff --git a/Content.Server/Explosion/Components/ExplosionAirtightGridComponent.cs b/Content.Server/Explosion/Components/ExplosionAirtightGridComponent.cs
new file mode 100644
index 0000000000..68f576dcb9
--- /dev/null
+++ b/Content.Server/Explosion/Components/ExplosionAirtightGridComponent.cs
@@ -0,0 +1,100 @@
+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;
+
+///
+/// Stores data for airtight explosion traversal on a entity.
+///
+///
+[RegisterComponent]
+[Access(typeof(ExplosionSystem), Other = AccessPermissions.None)]
+public sealed partial class ExplosionAirtightGridComponent : Component
+{
+ ///
+ /// Data for every tile on the current grid.
+ ///
+ ///
+ /// Intentionally not saved.
+ ///
+ [ViewVariables]
+ public readonly Dictionary Tiles = new();
+
+ ///
+ /// Data struct that describes the explosion-blocking airtight entities on a tile.
+ ///
+ public struct TileData
+ {
+ ///
+ /// Which index into the tolerance cache of this tile is using.
+ ///
+ public required int ToleranceCacheIndex;
+
+ ///
+ /// Which directions this tile is blocking explosions in. Bitflag field.
+ ///
+ public required AtmosDirection BlockedDirections;
+ }
+
+ ///
+ /// A set of tolerance values
+ ///
+ public struct ToleranceValues : IEquatable
+ {
+ ///
+ /// Special value that indicates the entity is "invulnerable" against a specific explosion type.
+ ///
+ ///
+ /// Here to deal with the limited range of over typical floats.
+ ///
+ public static readonly FixedPoint2 Invulnerable = FixedPoint2.MaxValue;
+
+ ///
+ /// The intensities at which explosions of each type can instantly break through an entity.
+ ///
+ ///
+ ///
+ /// This is an array, with the index of each value corresponding to the "explosion type ID" cached by
+ /// .
+ ///
+ ///
+ /// Values are stored as to avoid possible precision issues resulting in
+ /// different-but-almost-identical tolerance values wasting memory.
+ ///
+ ///
+ /// If a value is , that indicates the tile is invulnerable.
+ ///
+ ///
+ 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);
+ }
+ }
+}
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs b/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs
index da3ce635af..0274979c55 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionGridTileFlood.cs
@@ -1,7 +1,8 @@
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;
@@ -11,6 +12,8 @@ namespace Content.Server.Explosion.EntitySystems;
///
public sealed class ExplosionGridTileFlood : ExplosionTileFlood
{
+ private readonly ExplosionSystem _explosionSystem;
+
public Entity Grid;
private bool _needToTransform = false;
@@ -45,7 +48,8 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
Dictionary edgeTiles,
EntityUid? referenceGrid,
Matrix3x2 spaceMatrix,
- Angle spaceAngle)
+ Angle spaceAngle,
+ ExplosionSystem explosionSystem)
{
Grid = grid;
_airtightMap = airtightMap;
@@ -53,6 +57,7 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
_intensityStepSize = intensityStepSize;
_typeIndex = typeIndex;
_edgeTiles = edgeTiles;
+ _explosionSystem = explosionSystem;
// initialise SpaceTiles
foreach (var (tile, spaceNeighbors) in _edgeTiles)
@@ -193,11 +198,11 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
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
@@ -261,13 +266,13 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
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
@@ -290,7 +295,7 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
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))
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
index 303c4e8cab..da2a571900 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.Airtight.cs
@@ -1,44 +1,59 @@
+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 _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 with just a FixedPoint2[].
+ private readonly Dictionary, 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 _toleranceIndex = new();
+ // Storage for tolerance values. Entries form a free linked list when not occupied by a set of real values.
+ private ValueList _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 with just a float[].
int index = 0;
foreach (var prototype in _prototypeManager.EnumeratePrototypes())
{
- // 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
- //
- // Hence, each tile has a tuple (Dictionary, 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> _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)
{
@@ -46,6 +61,12 @@ public sealed partial class ExplosionSystem
UpdateAirtightMap(gridId, grid, tile);
}
+ [Access(typeof(ExplosionGridTileFlood))]
+ public ToleranceValues GetToleranceValues(int idx)
+ {
+ return _toleranceData[idx].Values;
+ }
+
///
/// Update the map of explosion blockers.
///
@@ -58,11 +79,12 @@ public sealed partial class ExplosionSystem
///
public void UpdateAirtightMap(EntityUid gridId, MapGridComponent grid, Vector2i tile)
{
- var tolerance = new float[_explosionTypes.Count];
- var blockedDirections = AtmosDirection.Invalid;
+ var airtightGrid = EnsureComp(gridId);
- if (!_airtightMap.ContainsKey(gridId))
- _airtightMap[gridId] = new();
+ // Calculate tile new airtight state.
+
+ var tolerance = new FixedPoint2[_explosionTypes.Count];
+ var blockedDirections = AtmosDirection.Invalid;
var anchoredEnumerator = _map.GetAnchoredEntitiesEnumerator(gridId, grid, tile);
@@ -72,17 +94,97 @@ public sealed partial class ExplosionSystem
continue;
blockedDirections |= airtight.AirBlockedDirection;
- var entityTolerances = GetExplosionTolerance(uid.Value);
- for (var i = 0; i < tolerance.Length; i++)
- {
- tolerance[i] = Math.Max(tolerance[i], entityTolerances[i]);
- }
+ GetExplosionTolerance(uid.Value, tolerance);
}
- if (blockedDirections != AtmosDirection.Invalid)
- _airtightMap[gridId][tile] = new(tolerance, blockedDirections);
+ // Log.Info($"UPDATE {gridId}/{tile}: {blockedDirections}");
+
+ if (blockedDirections == AtmosDirection.Invalid)
+ {
+ // No longer airtight
+
+ if (!airtightGrid.Tiles.Remove(tile, out var tileData))
+ {
+ // Did not have this tile before and after, nothing to do.
+ return;
+ }
+
+ // Removing tile data.
+ DecrementRefCount(tileData.ToleranceCacheIndex);
+ return;
+ }
+
+ 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!");
+ }
}
///
@@ -106,7 +208,7 @@ public sealed partial class ExplosionSystem
///
/// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity.
///
- public float[] GetExplosionTolerance(EntityUid uid)
+ private void GetExplosionTolerance(EntityUid uid, Span 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
@@ -117,14 +219,14 @@ public sealed partial class ExplosionSystem
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
@@ -157,38 +259,43 @@ public sealed partial class ExplosionSystem
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;
+ }
}
- ///
- /// Data struct that describes the explosion-blocking airtight entities on a tile.
- ///
- 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(entity);
}
public override void ReloadMap()
{
- foreach (var(grid, dict) in _airtightMap)
+ var enumerator = EntityQueryEnumerator();
+ while (enumerator.MoveNext(out var uid, out var airtightComp, out var mapGrid))
{
- var comp = Comp(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
+ }
+
}
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs
index 5c032d5c82..3767d0c238 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.GridMap.cs
@@ -38,7 +38,7 @@ public sealed partial class ExplosionSystem
private void OnGridRemoved(GridRemovalEvent ev)
{
- _airtightMap.Remove(ev.EntityUid);
+ OnAirtightGridRemoved(ev.EntityUid);
_gridEdges.Remove(ev.EntityUid);
// this should be a small enough set that iterating all of them is fine
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs
index ac539da213..a274fa8660 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.TileFill.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
+using Content.Server.Explosion.Components;
using Content.Shared.Administration;
using Content.Shared.Explosion.Components;
using Robust.Shared.Map;
@@ -40,11 +41,7 @@ public sealed partial class ExplosionSystem
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;
- }
+ var typeIndex = _explosionTypes[typeID];
Vector2i initialTile;
EntityUid? epicentreGrid = null;
@@ -103,8 +100,7 @@ public sealed partial class ExplosionSystem
// set up the initial `gridData` instance
encounteredGrids.Add(epicentreGrid.Value);
- if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
- airtightMap = new();
+ var airtightMap = CompOrNull(epicentreGrid)?.Tiles ?? new();
var initialGridData = new ExplosionGridTileFlood(
(epicentreGrid.Value, Comp(epicentreGrid.Value)),
@@ -115,7 +111,8 @@ public sealed partial class ExplosionSystem
_gridEdges[epicentreGrid.Value],
referenceGrid,
spaceMatrix,
- spaceAngle);
+ spaceAngle,
+ this);
gridData[epicentreGrid.Value] = initialGridData;
@@ -192,8 +189,7 @@ public sealed partial class ExplosionSystem
// 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();
+ var airtightMap = CompOrNull(grid)?.Tiles ?? new();
data = new ExplosionGridTileFlood(
(grid, Comp(grid)),
@@ -204,7 +200,8 @@ public sealed partial class ExplosionSystem
_gridEdges[grid],
referenceGrid,
spaceMatrix,
- spaceAngle);
+ spaceAngle,
+ this);
gridData[grid] = data;
}
diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
index 934e4b40c6..223d1dbd01 100644
--- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
+++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs
@@ -106,6 +106,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
_destructibleQuery = GetEntityQuery();
_damageableQuery = GetEntityQuery();
_airtightQuery = GetEntityQuery();
+
+ _prototypeManager.PrototypesReloaded += ReloadExplosionPrototypes;
}
private void OnReset(RoundRestartCleanupEvent ev)
@@ -124,6 +126,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
base.Shutdown();
_nodeGroupSystem.PauseUpdating = false;
_pathfindingSystem.PauseUpdating = false;
+ _prototypeManager.PrototypesReloaded -= ReloadExplosionPrototypes;
}
private void RelayedResistance(EntityUid uid, ExplosionResistanceComponent component,
diff --git a/Resources/Prototypes/explosion.yml b/Resources/Prototypes/explosion.yml
index ad00333892..4d3febeda7 100644
--- a/Resources/Prototypes/explosion.yml
+++ b/Resources/Prototypes/explosion.yml
@@ -1,11 +1,3 @@
-# Does not currently support prototype hot-reloading. See comments in c# file.
-
-# Note that for every explosion type you define, explosions & nukes will start performing worse
-# You should only define a new explopsion type if you really need to
-#
-# If you just want to modify properties other than `damagePerIntensity`, it'd be better to
-# split off explosion damage & explosion visuals/effects into their own separate prototypes.
-
- type: explosion
id: Default
damagePerIntensity:
@@ -135,7 +127,3 @@
texturePath: /Textures/Effects/fire.rsi
fireStates: 3
fireStacks: 2
-
-# STOP
-# BEFORE YOU ADD MORE EXPLOSION TYPES CONSIDER IF AN EXISTING ONE IS SUITABLE
-# ADDING NEW ONES IS PROHIBITIVELY EXPENSIVE