diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs
new file mode 100644
index 0000000000..b31b3ed1a2
--- /dev/null
+++ b/Content.Benchmarks/DeltaPressureBenchmark.cs
@@ -0,0 +1,174 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Diagnosers;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.Components;
+using Content.Shared.CCVar;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Benchmarks;
+
+///
+/// Spawns N number of entities with a and
+/// simulates them for a number of ticks M.
+///
+[Virtual]
+[GcServer(true)]
+//[MemoryDiagnoser]
+//[ThreadingDiagnoser]
+public class DeltaPressureBenchmark
+{
+ ///
+ /// Number of entities (windows, really) to spawn with a .
+ ///
+ [Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
+ public int EntityCount;
+
+ ///
+ /// Number of entities that each parallel processing job will handle.
+ ///
+ // [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
+ [Params(10)]
+ public int BatchSize;
+
+ ///
+ /// Number of entities to process per iteration in the DeltaPressure
+ /// processing loop.
+ ///
+ // [Params(100, 1000, 5000, 10000, 50000)]
+ [Params(1000)]
+ public int EntitiesPerIteration;
+
+ private readonly EntProtoId _windowProtoId = "Window";
+ private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
+
+ private TestPair _pair = default!;
+ private IEntityManager _entMan = default!;
+ private SharedMapSystem _map = default!;
+ private IRobustRandom _random = default!;
+ private IConfigurationManager _cvar = default!;
+ private ITileDefinitionManager _tileDefMan = default!;
+ private AtmosphereSystem _atmospereSystem = default!;
+
+ private Entity
+ _testEnt;
+
+ [GlobalSetup]
+ public async Task SetupAsync()
+ {
+ ProgramShared.PathOffset = "../../../../";
+ PoolManager.Startup();
+ _pair = await PoolManager.GetServerClient();
+ var server = _pair.Server;
+
+ var mapdata = await _pair.CreateTestMap();
+
+ _entMan = server.ResolveDependency();
+ _map = _entMan.System();
+ _random = server.ResolveDependency();
+ _cvar = server.ResolveDependency();
+ _tileDefMan = server.ResolveDependency();
+ _atmospereSystem = _entMan.System();
+
+ _random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
+
+ _cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
+ _cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
+
+ var plating = _tileDefMan["Plating"].TileId;
+
+ /*
+ Basically, we want to have a 5-wide grid of tiles.
+ Edges are walled, and the length of the grid is determined by N + 2.
+ Windows should only touch the top and bottom walls, and each other.
+ */
+
+ var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
+ const int height = 5;
+
+ await server.WaitPost(() =>
+ {
+ // Fill required tiles (extend grid) with plating
+ for (var x = 0; x < length; x++)
+ {
+ for (var y = 0; y < height; y++)
+ {
+ _map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
+ }
+ }
+
+ // Spawn perimeter walls and windows row in the middle (y = 2)
+ const int midY = height / 2;
+ for (var x = 0; x < length; x++)
+ {
+ for (var y = 0; y < height; y++)
+ {
+ var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
+
+ var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
+ if (isPerimeter)
+ {
+ _entMan.SpawnEntity(_wallProtoId, coords);
+ continue;
+ }
+
+ // Spawn windows only on the middle row, spanning interior (excluding side walls)
+ if (y == midY)
+ {
+ _entMan.SpawnEntity(_windowProtoId, coords);
+ }
+ }
+ }
+ });
+
+ // Next we run the fixgridatmos command to ensure that we have some air on our grid.
+ // Wait a little bit as well.
+ // TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
+ // a stamp-on in AtmosphereSystem.
+ await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
+
+ var uid = mapdata.Grid.Owner;
+ _testEnt = new Entity(
+ uid,
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid),
+ _entMan.GetComponent(uid));
+ }
+
+ [Benchmark]
+ public async Task PerformFullProcess()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
+ });
+ }
+
+ [Benchmark]
+ public async Task PerformSingleRunProcess()
+ {
+ await _pair.Server.WaitPost(() =>
+ {
+ _atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
+ });
+ }
+
+ [GlobalCleanup]
+ public async Task CleanupAsync()
+ {
+ await _pair.DisposeAsync();
+ PoolManager.Shutdown();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
new file mode 100644
index 0000000000..9dda130847
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs
@@ -0,0 +1,417 @@
+using System.Linq;
+using System.Numerics;
+using Content.Server.Atmos;
+using Content.Server.Atmos.Components;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Robust.Shared.EntitySerialization;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Utility;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+///
+/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
+/// handling the DeltaPressureComponent.
+///
+[TestFixture]
+[TestOf(typeof(DeltaPressureSystem))]
+public sealed class DeltaPressureTest
+{
+ #region Prototypes
+
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ parent: BaseStructure
+ id: DeltaPressureSolidTest
+ placement:
+ mode: SnapgridCenter
+ snap:
+ - Wall
+ components:
+ - type: Physics
+ bodyType: Static
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: ""-0.5,-0.5,0.5,0.5""
+ mask:
+ - FullTileMask
+ layer:
+ - WallLayer
+ density: 1000
+ - type: Airtight
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+ - type: Damageable
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 300
+ behaviors:
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ Girder:
+ min: 1
+ max: 1
+ - !type:DoActsBehavior
+ acts: [ ""Destruction"" ]
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestNoAutoJoin
+ components:
+ - type: DeltaPressure
+ autoJoinProcessingList: false
+
+- type: entity
+ parent: DeltaPressureSolidTest
+ id: DeltaPressureSolidTestAbsolute
+ components:
+ - type: DeltaPressure
+ minPressure: 10000
+ minPressureDelta: 15000
+ scalingType: Threshold
+ baseDamage:
+ types:
+ Structural: 1000
+";
+
+ #endregion
+
+ private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
+
+ ///
+ /// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to true is automatically added to the DeltaPressure processing list
+ /// on the grid's GridAtmosphereComponent.
+ ///
+ /// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
+ /// set to false is not automatically added to the DeltaPressure processing list.
+ ///
+ [Test]
+ public async Task ProcessingListAutoJoinTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ });
+
+ await server.WaitAssertion(() =>
+ {
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
+ entMan.DeleteEntity(uid);
+ Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
+ });
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure.
+ ///
+ [Test]
+ public async Task ProcessingDeltaStandbyTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the threshold.
+ ///
+ [Test]
+ public async Task ProcessingDeltaDamageTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ // Load our test map in and assert that it exists.
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+
+ grid = gridSet.First();
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ // Need to spawn an entity each run to ensure it works for all directions.
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressureDelta + 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ // Entity should exist, if it took one tick of damage then it should be instantly destroyed.
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+ /// is not damaged by DeltaPressure when using absolute pressure thresholds.
+ ///
+ [Test]
+ public async Task ProcessingAbsoluteStandbyTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+ grid = gridSet.First();
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ var toPressurize = dpEnt.Comp!.MinPressure - 10; // just below absolute threshold
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+
+ ///
+ /// Asserts that an entity that needs to be damaged by DeltaPressure
+ /// is damaged by DeltaPressure when the pressure is above the absolute threshold.
+ ///
+ [Test]
+ public async Task ProcessingAbsoluteDamageTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var entMan = server.EntMan;
+ var mapLoader = entMan.System();
+ var atmosphereSystem = entMan.System();
+ var transformSystem = entMan.System();
+ var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+ Entity grid = default;
+ Entity dpEnt = default;
+ TileAtmosphere tile = null!;
+ AtmosDirection direction = default;
+
+ await server.WaitPost(() =>
+ {
+#pragma warning disable NUnit2045
+ Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
+ $"Failed to load map {_testMap}.");
+ Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
+#pragma warning restore NUnit2045
+ grid = gridSet.First();
+ });
+
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ // Spawn fresh entity each iteration to verify all directions work
+ var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
+ dpEnt = new Entity(uid, entMan.GetComponent(uid));
+ Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
+
+ var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
+ var gridAtmosComp = entMan.GetComponent(grid);
+
+ direction = (AtmosDirection)(1 << i);
+ var offsetIndices = indices.Offset(direction);
+ tile = gridAtmosComp.Tiles[offsetIndices];
+ Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
+
+ // Above absolute threshold but below delta threshold to ensure absolute alone causes damage
+ var toPressurize = dpEnt.Comp!.MinPressure + 10;
+ var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
+ tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
+ });
+
+ await server.WaitRunTicks(30);
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
+ tile.Air!.Clear();
+ });
+
+ await server.WaitRunTicks(30);
+ }
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.Server/Atmos/Components/DeltaPressureComponent.cs b/Content.Server/Atmos/Components/DeltaPressureComponent.cs
new file mode 100644
index 0000000000..f90c133dea
--- /dev/null
+++ b/Content.Server/Atmos/Components/DeltaPressureComponent.cs
@@ -0,0 +1,139 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+
+namespace Content.Server.Atmos.Components;
+
+///
+/// Entities that have this component will have damage done to them depending on the local pressure
+/// environment that they reside in.
+///
+/// Atmospherics.DeltaPressure batch-processes entities with this component in a list on
+/// the grid's .
+/// The entities are automatically added and removed from this list, and automatically
+/// added on initialization.
+///
+/// Note that the entity should have an and be a grid structure.
+[RegisterComponent]
+public sealed partial class DeltaPressureComponent : Component
+{
+ ///
+ /// Whether the entity is currently in the processing list of the grid's .
+ ///
+ [DataField(readOnly: true)]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+ public bool InProcessingList;
+
+ ///
+ /// Whether this entity is currently taking damage from pressure.
+ ///
+ [DataField(readOnly: true)]
+ [ViewVariables(VVAccess.ReadOnly)]
+ [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+ public bool IsTakingDamage;
+
+ ///
+ /// The current cached position of this entity on the grid.
+ /// Updated via MoveEvent.
+ ///
+ [DataField(readOnly: true)]
+ public Vector2i CurrentPosition = Vector2i.Zero;
+
+ ///
+ /// The grid this entity is currently joined to for processing.
+ /// Required for proper deletion, as we cannot reference the grid
+ /// for removal while the entity is being deleted.
+ ///
+ [DataField]
+ public EntityUid? GridUid;
+
+ ///
+ /// The percent chance that the entity will take damage each atmos tick,
+ /// when the entity is above the damage threshold.
+ /// Makes it so that windows don't all break in one go.
+ /// Float is from 0 to 1, where 1 means 100% chance.
+ /// If this is set to 0, the entity will never take damage.
+ ///
+ [DataField]
+ public float RandomDamageChance = 1f;
+
+ ///
+ /// The base damage applied to the entity per atmos tick when it is above the damage threshold.
+ /// This damage will be scaled as defined by the enum
+ /// depending on the current effective pressure this entity is experiencing.
+ /// Note that this damage will scale depending on the pressure above the minimum pressure,
+ /// not at the current pressure.
+ ///
+ [DataField]
+ public DamageSpecifier BaseDamage = new()
+ {
+ DamageDict = new Dictionary
+ {
+ { "Structural", 10 },
+ },
+ };
+
+ ///
+ /// The minimum pressure in kPa at which the entity will start taking damage.
+ /// This doesn't depend on the difference in pressure.
+ /// The entity will start to take damage if it is exposed to this pressure.
+ /// This is needed because we don't correctly handle 2-layer windows yet.
+ ///
+ [DataField]
+ public float MinPressure = 10000;
+
+ ///
+ /// The minimum difference in pressure between any side required for the entity to start taking damage.
+ ///
+ [DataField]
+ public float MinPressureDelta = 7500;
+
+ ///
+ /// The maximum pressure at which damage will no longer scale.
+ /// If the effective pressure goes beyond this, the damage will be considered at this pressure.
+ ///
+ [DataField]
+ public float MaxEffectivePressure = 10000;
+
+ ///
+ /// Simple constant to affect the scaling behavior.
+ /// See comments in the types to see how this affects scaling.
+ ///
+ [DataField]
+ public float ScalingPower = 1;
+
+ ///
+ /// Defines the scaling behavior for the damage.
+ ///
+ [DataField]
+ public DeltaPressureDamageScalingType ScalingType = DeltaPressureDamageScalingType.Threshold;
+}
+
+///
+/// An enum that defines how the damage dealt by the scales
+/// depending on the pressure experienced by the entity.
+/// The scaling is done on the effective pressure, which is the pressure above the minimum pressure.
+/// See https://www.desmos.com/calculator/9ctlq3zpnt for a visual representation of the scaling types.
+///
+[Serializable]
+public enum DeltaPressureDamageScalingType : byte
+{
+ ///
+ /// Damage dealt will be constant as long as the minimum values are met.
+ /// Scaling power is ignored.
+ ///
+ Threshold,
+
+ ///
+ /// Damage dealt will be a linear function.
+ /// Scaling power determines the slope of the function.
+ ///
+ Linear,
+
+ ///
+ /// Damage dealt will be a logarithmic function.
+ /// Scaling power determines the base of the log.
+ ///
+ Log,
+}
diff --git a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
index e682fd0964..2d36d2bd14 100644
--- a/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
+++ b/Content.Server/Atmos/Components/GridAtmosphereComponent.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Serialization;
@@ -61,6 +62,39 @@ namespace Content.Server.Atmos.Components
[ViewVariables]
public int HighPressureDeltaCount => HighPressureDelta.Count;
+ ///
+ /// A list of entities that have a and are to
+ /// be processed by the , if enabled.
+ ///
+ /// To prevent massive bookkeeping overhead, this list is processed in-place,
+ /// with add/remove/find operations helped via a dict.
+ ///
+ /// If you want to add/remove/find entities in this list,
+ /// use the API methods in the Atmospherics API.
+ [ViewVariables]
+ public readonly List> DeltaPressureEntities =
+ new(AtmosphereSystem.DeltaPressurePreAllocateLength);
+
+ ///
+ /// An index lookup for the list.
+ /// Used for add/remove/find operations to speed up processing.
+ ///
+ public readonly Dictionary DeltaPressureEntityLookup =
+ new(AtmosphereSystem.DeltaPressurePreAllocateLength);
+
+ ///
+ /// Integer that indicates the current position in the
+ /// list that is being processed.
+ ///
+ [ViewVariables(VVAccess.ReadOnly)]
+ public int DeltaPressureCursor;
+
+ ///
+ /// Queue of entities that need to have damage applied to them.
+ ///
+ [ViewVariables]
+ public readonly ConcurrentQueue DeltaPressureDamageResults = new();
+
[ViewVariables]
public readonly HashSet PipeNets = new();
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
index 67f3a20779..87cfce135d 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components;
@@ -5,6 +6,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Reactions;
+using JetBrains.Annotations;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
@@ -319,6 +321,107 @@ public partial class AtmosphereSystem
return true;
}
+ ///
+ /// Adds an entity with a DeltaPressureComponent to the DeltaPressure processing list.
+ /// Also fills in important information on the component itself.
+ ///
+ /// The grid to add the entity to.
+ /// The entity to add.
+ /// True if the entity was added to the list, false if it could not be added or
+ /// if the entity was already present in the list.
+ [PublicAPI]
+ public bool TryAddDeltaPressureEntity(Entity grid, Entity ent)
+ {
+ // The entity needs to be part of a grid, and it should be the right one :)
+ var xform = Transform(ent);
+
+ // The entity is not on a grid, so it cannot possibly have an atmosphere that affects it.
+ if (xform.GridUid == null)
+ {
+ return false;
+ }
+
+ // Entity should be on the grid it's being added to.
+ Debug.Assert(xform.GridUid == grid.Owner);
+
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ if (grid.Comp.DeltaPressureEntityLookup.ContainsKey(ent.Owner))
+ {
+ return false;
+ }
+
+ grid.Comp.DeltaPressureEntityLookup[ent.Owner] = grid.Comp.DeltaPressureEntities.Count;
+ grid.Comp.DeltaPressureEntities.Add(ent);
+
+ ent.Comp.CurrentPosition = _map.CoordinatesToTile(grid,
+ Comp(grid),
+ xform.Coordinates);
+
+ ent.Comp.GridUid = grid.Owner;
+ ent.Comp.InProcessingList = true;
+
+ return true;
+ }
+
+ ///
+ /// Removes an entity with a DeltaPressureComponent from the DeltaPressure processing list.
+ ///
+ /// The grid to remove the entity from.
+ /// The entity to remove.
+ /// True if the entity was removed from the list, false if it could not be removed or
+ /// if the entity was not present in the list.
+ [PublicAPI]
+ public bool TryRemoveDeltaPressureEntity(Entity grid, Entity ent)
+ {
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ if (!grid.Comp.DeltaPressureEntityLookup.TryGetValue(ent.Owner, out var index))
+ return false;
+
+ var lastIndex = grid.Comp.DeltaPressureEntities.Count - 1;
+ if (lastIndex < 0)
+ return false;
+
+ if (index != lastIndex)
+ {
+ var lastEnt = grid.Comp.DeltaPressureEntities[lastIndex];
+ grid.Comp.DeltaPressureEntities[index] = lastEnt;
+ grid.Comp.DeltaPressureEntityLookup[lastEnt.Owner] = index;
+ }
+
+ grid.Comp.DeltaPressureEntities.RemoveAt(lastIndex);
+ grid.Comp.DeltaPressureEntityLookup.Remove(ent.Owner);
+
+ if (grid.Comp.DeltaPressureCursor > grid.Comp.DeltaPressureEntities.Count)
+ grid.Comp.DeltaPressureCursor = grid.Comp.DeltaPressureEntities.Count;
+
+ ent.Comp.InProcessingList = false;
+ ent.Comp.GridUid = null;
+ return true;
+ }
+
+ ///
+ /// Checks if a DeltaPressureComponent is currently considered for processing on a grid.
+ ///
+ /// The grid that the entity may belong to.
+ /// The entity to check.
+ /// True if the entity is part of the processing list, false otherwise.
+ [PublicAPI]
+ public bool IsDeltaPressureEntityInList(Entity grid, Entity ent)
+ {
+ // Dict and list must be in sync - deep-fried if we aren't.
+ if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
+ return false;
+
+ var contains = grid.Comp.DeltaPressureEntityLookup.ContainsKey(ent.Owner);
+ Debug.Assert(contains == grid.Comp.DeltaPressureEntities.Contains(ent));
+
+ return contains;
+ }
+
[ByRefEvent] private record struct SetSimulatedGridMethodEvent
(EntityUid Grid, bool Simulated, bool Handled = false);
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
new file mode 100644
index 0000000000..f86ebcee73
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs
@@ -0,0 +1,49 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Atmos.Components;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+ /*
+ Helper methods to assist in getting very low overhead profiling of individual stages of the atmospherics simulation.
+ Ideal for benchmarking and performance testing.
+ These methods obviously aren't to be used in production code. Don't call them. They know my voice.
+ */
+
+ ///
+ /// Runs the grid entity through a single processing stage of the atmosphere simulation.
+ /// Ideal for benchmarking single stages of the simulation.
+ ///
+ /// The entity to profile Atmospherics with.
+ /// The state to profile on the entity.
+ /// The optional mapEntity to provide when benchmarking ProcessAtmosDevices.
+ /// True if the processing stage completed, false if the processing stage had to pause processing due to time constraints.
+ public bool RunProcessingStage(
+ Entity ent,
+ AtmosphereProcessingState state,
+ Entity? mapEnt = null)
+ {
+ var processingPaused = state switch
+ {
+ AtmosphereProcessingState.Revalidate => ProcessRevalidate(ent),
+ AtmosphereProcessingState.TileEqualize => ProcessTileEqualize(ent),
+ AtmosphereProcessingState.ActiveTiles => ProcessActiveTiles(ent),
+ AtmosphereProcessingState.ExcitedGroups => ProcessExcitedGroups(ent),
+ AtmosphereProcessingState.HighPressureDelta => ProcessHighPressureDelta(ent),
+ AtmosphereProcessingState.DeltaPressure => ProcessDeltaPressure(ent),
+ AtmosphereProcessingState.Hotspots => ProcessHotspots(ent),
+ AtmosphereProcessingState.Superconductivity => ProcessSuperconductivity(ent),
+ AtmosphereProcessingState.PipeNet => ProcessPipeNets(ent),
+ AtmosphereProcessingState.AtmosDevices => mapEnt is not null
+ ? ProcessAtmosDevices(ent, mapEnt.Value)
+ : throw new ArgumentException(
+ "An Entity must be provided when benchmarking ProcessAtmosDevices."),
+ _ => throw new ArgumentOutOfRangeException(),
+ };
+ ent.Comp1.ProcessingPaused = !processingPaused;
+
+ return processingPaused;
+ }
+}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
index 3aaa5429fb..f24f0ae171 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
@@ -26,6 +26,9 @@ namespace Content.Server.Atmos.EntitySystems
public float AtmosTickRate { get; private set; }
public float Speedup { get; private set; }
public float HeatScale { get; private set; }
+ public bool DeltaPressureDamage { get; private set; }
+ public int DeltaPressureParallelProcessPerIteration { get; private set; }
+ public int DeltaPressureParallelBatchSize { get; private set; }
///
/// Time between each atmos sub-update. If you are writing an atmos device, use AtmosDeviceUpdateEvent.dt
@@ -55,6 +58,9 @@ namespace Content.Server.Atmos.EntitySystems
Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true);
Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true);
Subs.CVar(_cfg, CCVars.ExcitedGroupsSpaceIsAllConsuming, value => ExcitedGroupsSpaceIsAllConsuming = value, true);
+ Subs.CVar(_cfg, CCVars.DeltaPressureDamage, value => DeltaPressureDamage = value, true);
+ Subs.CVar(_cfg, CCVars.DeltaPressureParallelToProcessPerIteration, value => DeltaPressureParallelProcessPerIteration = value, true);
+ Subs.CVar(_cfg, CCVars.DeltaPressureParallelBatchSize, value => DeltaPressureParallelBatchSize = value, true);
}
}
}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
new file mode 100644
index 0000000000..207589e554
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
@@ -0,0 +1,259 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Damage;
+using Robust.Shared.Random;
+using Robust.Shared.Threading;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+ ///
+ /// The number of pairs of opposing directions we can have.
+ /// This is Atmospherics.Directions / 2, since we always compare opposing directions
+ /// (e.g. North vs South, East vs West, etc.).
+ /// Used to determine the size of the opposing groups when processing delta pressure entities.
+ ///
+ private const int DeltaPressurePairCount = Atmospherics.Directions / 2;
+
+ ///
+ /// The length to pre-allocate list/dicts of delta pressure entities on a .
+ ///
+ public const int DeltaPressurePreAllocateLength = 1000;
+
+ ///
+ /// Processes a singular entity, determining the pressures it's experiencing and applying damage based on that.
+ ///
+ /// The entity to process.
+ /// The that belongs to the entity's GridUid.
+ private void ProcessDeltaPressureEntity(Entity ent, GridAtmosphereComponent gridAtmosComp)
+ {
+ if (!_random.Prob(ent.Comp.RandomDamageChance))
+ return;
+
+ /*
+ To make our comparisons a little bit faster, we take advantage of SIMD-accelerated methods
+ in the NumericsHelpers class.
+
+ This involves loading our values into a span in the form of opposing pairs,
+ so simple vector operations like min/max/abs can be performed on them.
+ */
+
+ var tiles = new TileAtmosphere?[Atmospherics.Directions];
+ for (var i = 0; i < Atmospherics.Directions; i++)
+ {
+ var direction = (AtmosDirection)(1 << i);
+ var offset = ent.Comp.CurrentPosition.Offset(direction);
+ tiles[i] = gridAtmosComp.Tiles.GetValueOrDefault(offset);
+ }
+
+ Span pressures = stackalloc float[Atmospherics.Directions];
+
+ GetBulkTileAtmospherePressures(tiles, pressures);
+
+ Span opposingGroupA = stackalloc float[DeltaPressurePairCount];
+ Span opposingGroupB = stackalloc float[DeltaPressurePairCount];
+ Span opposingGroupMax = stackalloc float[DeltaPressurePairCount];
+
+ // Directions are always in pairs: the number of directions is always even
+ // (we must consider the future where Multi-Z is real)
+ // Load values into opposing pairs.
+ for (var i = 0; i < DeltaPressurePairCount; i++)
+ {
+ opposingGroupA[i] = pressures[i];
+ opposingGroupB[i] = pressures[i + DeltaPressurePairCount];
+ }
+
+ // TODO ATMOS: Needs to be changed to batch operations so that more operations can actually be done in parallel.
+
+ // Need to determine max pressure in opposing directions for absolute pressure calcs.
+ NumericsHelpers.Max(opposingGroupA, opposingGroupB, opposingGroupMax);
+
+ // Calculate pressure differences between opposing directions.
+ NumericsHelpers.Sub(opposingGroupA, opposingGroupB);
+ NumericsHelpers.Abs(opposingGroupA);
+
+ var maxPressure = 0f;
+ var maxDelta = 0f;
+ for (var i = 0; i < DeltaPressurePairCount; i++)
+ {
+ maxPressure = MathF.Max(maxPressure, opposingGroupMax[i]);
+ maxDelta = MathF.Max(maxDelta, opposingGroupA[i]);
+ }
+
+ EnqueueDeltaPressureDamage(ent,
+ gridAtmosComp,
+ maxPressure,
+ maxDelta);
+ }
+
+ ///
+ /// A DeltaPressure helper method that retrieves the pressures of all gas mixtures
+ /// in the given array of s, and stores the results in the
+ /// provided span.
+ /// The tiles array length is limited to Atmosphereics.Directions.
+ ///
+ /// The tiles array to find the pressures of.
+ /// The span to store the pressures to - this should be the same length
+ /// as the tile array.
+ /// This is for internal use of the DeltaPressure system -
+ /// it may not be a good idea to use this generically.
+ private static void GetBulkTileAtmospherePressures(TileAtmosphere?[] tiles, Span pressures)
+ {
+ #if DEBUG
+ // Just in case someone tries to use this method incorrectly.
+ if (tiles.Length != pressures.Length || tiles.Length != Atmospherics.Directions)
+ throw new ArgumentException("Length of arrays must be the same and of Atmospherics.Directions length.");
+ #endif
+
+ // This hardcoded direction limit is stopping goobers from
+ // overflowing the stack with massive arrays.
+ // If this method is pulled into a more generic place,
+ // it should be replaced with method params.
+ Span mixtVol = stackalloc float[Atmospherics.Directions];
+ Span mixtTemp = stackalloc float[Atmospherics.Directions];
+ Span mixtMoles = stackalloc float[Atmospherics.Directions];
+ Span atmosR = stackalloc float[Atmospherics.Directions];
+
+ for (var i = 0; i < tiles.Length; i++)
+ {
+ if (tiles[i] is not { Air: { } mixture })
+ {
+ pressures[i] = 0f;
+
+ // To prevent any NaN/Div/0 errors, we just bite the bullet
+ // and set everything to the lowest possible value.
+ mixtVol[i] = 1;
+ mixtTemp[i] = 1;
+ mixtMoles[i] = float.Epsilon;
+ atmosR[i] = 1;
+ continue;
+ }
+
+ mixtVol[i] = mixture.Volume;
+ mixtTemp[i] = mixture.Temperature;
+ mixtMoles[i] = mixture.TotalMoles;
+ atmosR[i] = Atmospherics.R;
+ }
+
+ /*
+ Retrieval of single tile pressures requires calling a get method for each tile,
+ which does a bunch of scalar operations.
+
+ So we go ahead and batch-retrieve the pressures of all tiles
+ and process them in bulk.
+ */
+ NumericsHelpers.Multiply(mixtMoles, atmosR);
+ NumericsHelpers.Multiply(mixtMoles, mixtTemp);
+ NumericsHelpers.Divide(mixtMoles, mixtVol, pressures);
+ }
+
+ ///
+ /// Packs data into a data struct and enqueues it
+ /// into the queue for
+ /// later processing.
+ ///
+ /// The entity to enqueue if necessary.
+ /// The
+ /// containing the queue.
+ /// The current absolute pressure being experienced by the entity.
+ /// The current delta pressure being experienced by the entity.
+ private static void EnqueueDeltaPressureDamage(Entity ent,
+ GridAtmosphereComponent gridAtmosComp,
+ float pressure,
+ float delta)
+ {
+ var aboveMinPressure = pressure > ent.Comp.MinPressure;
+ var aboveMinDeltaPressure = delta > ent.Comp.MinPressureDelta;
+ if (!aboveMinPressure && !aboveMinDeltaPressure)
+ {
+ ent.Comp.IsTakingDamage = false;
+ return;
+ }
+
+ gridAtmosComp.DeltaPressureDamageResults.Enqueue(new DeltaPressureDamageResult(ent,
+ pressure,
+ delta));
+ }
+
+ ///
+ /// Job for solving DeltaPressure entities in parallel.
+ /// Batches are given some index to start from, so each thread can simply just start at that index
+ /// and process the next n entities in the list.
+ ///
+ /// The AtmosphereSystem instance.
+ /// The GridAtmosphereComponent to work with.
+ /// The index in the DeltaPressureEntities list to start from.
+ /// The batch size to use for this job.
+ private sealed class DeltaPressureParallelJob(
+ AtmosphereSystem system,
+ GridAtmosphereComponent atmosphere,
+ int startIndex,
+ int cvarBatchSize)
+ : IParallelRobustJob
+ {
+ public int BatchSize => cvarBatchSize;
+
+ public void Execute(int index)
+ {
+ // The index is relative to the startIndex (because we can pause and resume computation),
+ // so we need to add it to the startIndex.
+ var actualIndex = startIndex + index;
+
+ if (actualIndex >= atmosphere.DeltaPressureEntities.Count)
+ return;
+
+ var ent = atmosphere.DeltaPressureEntities[actualIndex];
+ system.ProcessDeltaPressureEntity(ent, atmosphere);
+ }
+ }
+
+ ///
+ /// Struct that holds the result of delta pressure damage processing for an entity.
+ /// This is only created and enqueued when the entity needs to take damage.
+ ///
+ /// The entity to deal damage to.
+ /// The current absolute pressure the entity is experiencing.
+ /// The current delta pressure the entity is experiencing.
+ public readonly record struct DeltaPressureDamageResult(
+ Entity Ent,
+ float Pressure,
+ float DeltaPressure);
+
+ ///
+ /// Does damage to an entity depending on the pressure experienced by it, based on the
+ /// entity's .
+ ///
+ /// The entity to apply damage to.
+ /// The absolute pressure being exerted on the entity.
+ /// The delta pressure being exerted on the entity.
+ private void PerformDamage(Entity ent, float pressure, float deltaPressure)
+ {
+ var maxPressure = Math.Max(pressure - ent.Comp.MinPressure, deltaPressure - ent.Comp.MinPressureDelta);
+ var appliedDamage = ScaleDamage(ent, ent.Comp.BaseDamage, maxPressure);
+
+ _damage.TryChangeDamage(ent, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
+ ent.Comp.IsTakingDamage = true;
+ }
+
+ ///
+ /// Returns a new DamageSpecifier scaled based on values on an entity with a DeltaPressureComponent.
+ ///
+ /// The entity to base the manipulations off of (pull scaling type)
+ /// The base damage specifier to scale.
+ /// The pressure being exerted on the entity.
+ /// A scaled DamageSpecifier.
+ private static DamageSpecifier ScaleDamage(Entity ent, DamageSpecifier damage, float pressure)
+ {
+ var factor = ent.Comp.ScalingType switch
+ {
+ DeltaPressureDamageScalingType.Threshold => 1f,
+ DeltaPressureDamageScalingType.Linear => pressure * ent.Comp.ScalingPower,
+ DeltaPressureDamageScalingType.Log =>
+ (float) Math.Log(pressure, ent.Comp.ScalingPower),
+ _ => throw new ArgumentOutOfRangeException(nameof(ent), "Invalid damage scaling type!"),
+ };
+
+ return damage * factor;
+ }
+}
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
index 02d389b215..9b8654af6d 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
@@ -467,6 +467,66 @@ namespace Content.Server.Atmos.EntitySystems
return true;
}
+ ///
+ /// Processes all entities with a , doing damage to them
+ /// depending on certain pressure differential conditions.
+ ///
+ /// True if we've finished processing all entities that required processing this run,
+ /// otherwise, false.
+ private bool ProcessDeltaPressure(Entity ent)
+ {
+ var atmosphere = ent.Comp1;
+ var count = atmosphere.DeltaPressureEntities.Count;
+ if (!atmosphere.ProcessingPaused)
+ {
+ atmosphere.DeltaPressureCursor = 0;
+ atmosphere.DeltaPressureDamageResults.Clear();
+ }
+
+ var remaining = count - atmosphere.DeltaPressureCursor;
+ var batchSize = Math.Max(50, DeltaPressureParallelProcessPerIteration);
+ var toProcess = Math.Min(batchSize, remaining);
+
+ var timeCheck1 = 0;
+ while (atmosphere.DeltaPressureCursor < count)
+ {
+ var job = new DeltaPressureParallelJob(this,
+ atmosphere,
+ atmosphere.DeltaPressureCursor,
+ DeltaPressureParallelBatchSize);
+ _parallel.ProcessNow(job, toProcess);
+
+ atmosphere.DeltaPressureCursor += toProcess;
+
+ if (timeCheck1++ < LagCheckIterations)
+ continue;
+
+ timeCheck1 = 0;
+ if (_simulationStopwatch.Elapsed.TotalMilliseconds >= AtmosMaxProcessTime)
+ return false;
+ }
+
+ var timeCheck2 = 0;
+ while (atmosphere.DeltaPressureDamageResults.TryDequeue(out var result))
+ {
+ PerformDamage(result.Ent,
+ result.Pressure,
+ result.DeltaPressure);
+
+ if (timeCheck2++ < LagCheckIterations)
+ continue;
+
+ timeCheck2 = 0;
+ // Process the rest next time.
+ if (_simulationStopwatch.Elapsed.TotalMilliseconds >= AtmosMaxProcessTime)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private bool ProcessPipeNets(GridAtmosphereComponent atmosphere)
{
if (!atmosphere.ProcessingPaused)
@@ -510,6 +570,8 @@ namespace Content.Server.Atmos.EntitySystems
num--;
if (!ExcitedGroups)
num--;
+ if (!DeltaPressureDamage)
+ num--;
if (!Superconduction)
num--;
return num * AtmosTime;
@@ -653,6 +715,18 @@ namespace Content.Server.Atmos.EntitySystems
return;
}
+ atmosphere.ProcessingPaused = false;
+ atmosphere.State = DeltaPressureDamage
+ ? AtmosphereProcessingState.DeltaPressure
+ : AtmosphereProcessingState.Hotspots;
+ continue;
+ case AtmosphereProcessingState.DeltaPressure:
+ if (!ProcessDeltaPressure(ent))
+ {
+ atmosphere.ProcessingPaused = true;
+ return;
+ }
+
atmosphere.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.Hotspots;
continue;
@@ -721,6 +795,7 @@ namespace Content.Server.Atmos.EntitySystems
ActiveTiles,
ExcitedGroups,
HighPressureDelta,
+ DeltaPressure,
Hotspots,
Superconductivity,
PipeNet,
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
index e9383f3a23..00b7e16913 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
@@ -1,6 +1,5 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
-using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
@@ -15,6 +14,8 @@ using Robust.Shared.Map;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using System.Linq;
+using Content.Shared.Damage;
+using Robust.Shared.Threading;
namespace Content.Server.Atmos.EntitySystems;
@@ -27,6 +28,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!;
+ [Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
@@ -37,6 +39,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly TileSystem _tile = default!;
[Dependency] private readonly MapSystem _map = default!;
[Dependency] public readonly PuddleSystem Puddle = default!;
+ [Dependency] private readonly DamageableSystem _damage = default!;
private const float ExposedUpdateDelay = 1f;
private float _exposedTimer = 0f;
diff --git a/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs b/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs
new file mode 100644
index 0000000000..a6cbec0d0c
--- /dev/null
+++ b/Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs
@@ -0,0 +1,82 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Examine;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+///
+/// System that handles .
+///
+/// Entities with a will take damage per atmostick
+/// depending on the pressure they experience.
+///
+/// DeltaPressure logic is mostly handled in a partial class in Atmospherics.
+/// This system handles the adding and removing of entities to a processing list,
+/// as well as any field changes via the API.
+///
+public sealed class DeltaPressureSystem : EntitySystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnComponentShutdown);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnMoveEvent);
+
+ SubscribeLocalEvent(OnGridChanged);
+ }
+
+ private void OnMoveEvent(Entity ent, ref MoveEvent args)
+ {
+ var xform = Transform(ent);
+ // May move off-grid, so, might as well protect against that.
+ if (!TryComp(xform.GridUid, out var mapGridComponent))
+ {
+ return;
+ }
+
+ ent.Comp.CurrentPosition = _map.CoordinatesToTile(xform.GridUid.Value, mapGridComponent, args.NewPosition);
+ }
+
+ private void OnComponentInit(Entity ent, ref ComponentInit args)
+ {
+ var xform = Transform(ent);
+ if (xform.GridUid == null)
+ return;
+
+ _atmosphereSystem.TryAddDeltaPressureEntity(xform.GridUid.Value, ent);
+ }
+
+ private void OnComponentShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ // Wasn't part of a list, so nothing to clean up.
+ if (ent.Comp.GridUid == null)
+ return;
+
+ _atmosphereSystem.TryRemoveDeltaPressureEntity(ent.Comp.GridUid.Value, ent);
+ }
+
+ private void OnExamined(Entity ent, ref ExaminedEvent args)
+ {
+ if (ent.Comp.IsTakingDamage)
+ args.PushMarkup(Loc.GetString("window-taking-damage"));
+ }
+
+ private void OnGridChanged(Entity ent, ref GridUidChangedEvent args)
+ {
+ if (args.OldGrid != null)
+ {
+ _atmosphereSystem.TryRemoveDeltaPressureEntity(args.OldGrid.Value, ent);
+ }
+
+ if (args.NewGrid != null)
+ {
+ _atmosphereSystem.TryAddDeltaPressureEntity(args.NewGrid.Value, ent);
+ }
+ }
+}
diff --git a/Content.Shared/CCVar/CCVars.Atmos.cs b/Content.Shared/CCVar/CCVars.Atmos.cs
index cc1069b4fc..7ef40b7911 100644
--- a/Content.Shared/CCVar/CCVars.Atmos.cs
+++ b/Content.Shared/CCVar/CCVars.Atmos.cs
@@ -150,4 +150,31 @@ public sealed partial class CCVars
///
public static readonly CVarDef AtmosTankFragment =
CVarDef.Create("atmos.max_explosion_range", 26f, CVar.SERVERONLY);
+
+ ///
+ /// Whether atmospherics will process delta-pressure damage on entities with a DeltaPressureComponent.
+ /// Entities with this component will take damage if they are exposed to a pressure difference
+ /// above the minimum pressure threshold defined in the component.
+ ///
+ // TODO: Needs CVARs for global configuration, like min pressure, max damage, etc.
+ public static readonly CVarDef DeltaPressureDamage =
+ CVarDef.Create("atmos.delta_pressure_damage", true, CVar.SERVERONLY);
+
+ ///
+ /// Number of entities to submit for parallel processing per processing run.
+ /// Low numbers may suffer from thinning out the work per job and leading to threads waiting,
+ /// or seeing a lot of threading overhead.
+ /// High numbers may cause Atmospherics to exceed its time budget per tick, as it will not
+ /// check its time often enough to know if it's exceeding it.
+ ///
+ public static readonly CVarDef DeltaPressureParallelToProcessPerIteration =
+ CVarDef.Create("atmos.delta_pressure_parallel_process_per_iteration", 1000, CVar.SERVERONLY);
+
+ ///
+ /// Number of entities to process per processing job.
+ /// Low numbers may cause Atmospherics to see high threading overhead,
+ /// high numbers may cause Atmospherics to distribute the work unevenly.
+ ///
+ public static readonly CVarDef DeltaPressureParallelBatchSize =
+ CVarDef.Create("atmos.delta_pressure_parallel_batch_size", 10, CVar.SERVERONLY);
}
diff --git a/Resources/Locale/en-US/atmos/delta-pressure-component.ftl b/Resources/Locale/en-US/atmos/delta-pressure-component.ftl
new file mode 100644
index 0000000000..f8ffd6d6ab
--- /dev/null
+++ b/Resources/Locale/en-US/atmos/delta-pressure-component.ftl
@@ -0,0 +1 @@
+window-taking-damage = [color=orange]It's straining under pressure![/color]
diff --git a/Resources/Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml b/Resources/Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml
new file mode 100644
index 0000000000..cedbfb1cff
--- /dev/null
+++ b/Resources/Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml
@@ -0,0 +1,234 @@
+meta:
+ format: 7
+ category: Map
+ engineVersion: 265.0.0
+ forkId: ""
+ forkVersion: ""
+ time: 08/16/2025 22:09:01
+ entityCount: 27
+maps:
+- 1
+grids:
+- 2
+orphans: []
+nullspace: []
+tilemap:
+ 1: Space
+ 0: Plating
+entities:
+- proto: ""
+ entities:
+ - uid: 1
+ components:
+ - type: MetaData
+ name: Map Entity
+ - type: Transform
+ - type: Map
+ mapPaused: True
+ - type: GridTree
+ - type: Broadphase
+ - type: OccluderTree
+ - uid: 2
+ components:
+ - type: MetaData
+ name: grid
+ - type: Transform
+ pos: -0.33581543,-0.640625
+ parent: 1
+ - type: MapGrid
+ chunks:
+ 0,0:
+ ind: 0,0
+ tiles: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAA==
+ version: 7
+ 0,-1:
+ ind: 0,-1
+ tiles: AQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAA==
+ version: 7
+ -1,-1:
+ ind: -1,-1
+ tiles: AQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAA==
+ version: 7
+ -1,0:
+ ind: -1,0
+ tiles: AQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAAEAAAAAAAABAAAAAAAAAQAAAAAAAA==
+ version: 7
+ - type: Broadphase
+ - type: Physics
+ bodyStatus: InAir
+ fixedRotation: False
+ bodyType: Dynamic
+ - type: Fixtures
+ fixtures: {}
+ - type: OccluderTree
+ - type: SpreaderGrid
+ - type: Shuttle
+ dampingModifier: 0.25
+ - type: ImplicitRoof
+ - type: GridPathfinding
+ - type: Gravity
+ gravityShakeSound: !type:SoundPathSpecifier
+ path: /Audio/Effects/alert.ogg
+ - type: DecalGrid
+ chunkCollection:
+ version: 2
+ nodes: []
+ - type: GridAtmosphere
+ version: 2
+ data:
+ tiles:
+ 0,0:
+ 0: 19
+ 0,-1:
+ 0: 4096
+ -1,0:
+ 0: 8
+ uniqueMixes:
+ - volume: 2500
+ temperature: 293.15
+ moles:
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ - 0
+ chunkSize: 4
+ - type: GasTileOverlay
+ - type: RadiationGridResistance
+- proto: AtmosFixBlockerMarker
+ entities:
+ - uid: 23
+ components:
+ - type: Transform
+ pos: 0.5,1.5
+ parent: 2
+ - uid: 24
+ components:
+ - type: Transform
+ pos: 0.5,0.5
+ parent: 2
+ - uid: 25
+ components:
+ - type: Transform
+ pos: 0.5,-0.5
+ parent: 2
+ - uid: 26
+ components:
+ - type: Transform
+ pos: -0.5,0.5
+ parent: 2
+ - uid: 27
+ components:
+ - type: Transform
+ pos: 1.5,0.5
+ parent: 2
+- proto: WallPlastitaniumIndestructible
+ entities:
+ - uid: 3
+ components:
+ - type: Transform
+ pos: -1.5,2.5
+ parent: 2
+ - uid: 4
+ components:
+ - type: Transform
+ pos: -0.5,2.5
+ parent: 2
+ - uid: 5
+ components:
+ - type: Transform
+ pos: 0.5,2.5
+ parent: 2
+ - uid: 6
+ components:
+ - type: Transform
+ pos: 1.5,2.5
+ parent: 2
+ - uid: 7
+ components:
+ - type: Transform
+ pos: 2.5,2.5
+ parent: 2
+ - uid: 8
+ components:
+ - type: Transform
+ pos: 2.5,1.5
+ parent: 2
+ - uid: 9
+ components:
+ - type: Transform
+ pos: 2.5,0.5
+ parent: 2
+ - uid: 10
+ components:
+ - type: Transform
+ pos: 2.5,-0.5
+ parent: 2
+ - uid: 11
+ components:
+ - type: Transform
+ pos: 2.5,-1.5
+ parent: 2
+ - uid: 12
+ components:
+ - type: Transform
+ pos: 1.5,-1.5
+ parent: 2
+ - uid: 13
+ components:
+ - type: Transform
+ pos: 0.5,-1.5
+ parent: 2
+ - uid: 14
+ components:
+ - type: Transform
+ pos: -0.5,-1.5
+ parent: 2
+ - uid: 15
+ components:
+ - type: Transform
+ pos: -1.5,-1.5
+ parent: 2
+ - uid: 16
+ components:
+ - type: Transform
+ pos: -1.5,-0.5
+ parent: 2
+ - uid: 17
+ components:
+ - type: Transform
+ pos: -1.5,0.5
+ parent: 2
+ - uid: 18
+ components:
+ - type: Transform
+ pos: -1.5,1.5
+ parent: 2
+ - uid: 19
+ components:
+ - type: Transform
+ pos: -0.5,1.5
+ parent: 2
+ - uid: 20
+ components:
+ - type: Transform
+ pos: 1.5,1.5
+ parent: 2
+ - uid: 21
+ components:
+ - type: Transform
+ pos: 1.5,-0.5
+ parent: 2
+ - uid: 22
+ components:
+ - type: Transform
+ pos: -0.5,-0.5
+ parent: 2
+...
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
index 811385645c..07618243d4 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
@@ -163,6 +163,10 @@
noAirWhenFullyAirBlocked: false
airBlockedDirection:
- South
+ - type: DeltaPressure
+ minPressure: 250
+ minPressureDelta: 187.5
+ scalingType: Threshold
- type: Construction
graph: Windoor
node: windoor
@@ -235,6 +239,10 @@
- type: Construction
graph: Windoor
node: windoorSecure
+ - type: DeltaPressure
+ minPressure: 3750
+ minPressureDelta: 2500
+ scalingType: Threshold
- type: StaticPrice
price: 350
- type: Tag
@@ -304,6 +312,10 @@
- type: Construction
graph: Windoor
node: pwindoor
+ - type: DeltaPressure
+ minPressure: 18750
+ minPressureDelta: 12500
+ scalingType: Threshold
- type: StaticPrice
price: 500
- type: RadiationBlocker
@@ -370,6 +382,10 @@
- type: Construction
graph: Windoor
node: pwindoorSecure
+ - type: DeltaPressure
+ minPressure: 37500
+ minPressureDelta: 25000
+ scalingType: Threshold
- type: StaticPrice
price: 500
- type: RadiationBlocker
@@ -438,6 +454,10 @@
max: 2
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 18750
+ minPressureDelta: 12500
+ scalingType: Threshold
- type: Construction
graph: Windoor
node: uwindoor
@@ -504,6 +524,10 @@
max: 2
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 37500
+ minPressureDelta: 25000
+ scalingType: Threshold
- type: Construction
graph: Windoor
node: uwindoorSecure
diff --git a/Resources/Prototypes/Entities/Structures/Windows/plasma.yml b/Resources/Prototypes/Entities/Structures/Windows/plasma.yml
index 9e73dce7a1..d6761239a8 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/plasma.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/plasma.yml
@@ -48,6 +48,11 @@
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 75000
+ minPressureDelta: 50000
+ scalingType: Linear
+ scalingPower: 0.0005
- type: StaticPrice
price: 100
- type: RadiationBlocker
@@ -104,6 +109,10 @@
max: 1
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 18750
+ minPressureDelta: 12500
+ scalingType: Threshold
- type: StaticPrice
price: 50
- type: RadiationBlocker
diff --git a/Resources/Prototypes/Entities/Structures/Windows/reinforced.yml b/Resources/Prototypes/Entities/Structures/Windows/reinforced.yml
index b9d4e6fd63..912313c13d 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/reinforced.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/reinforced.yml
@@ -55,6 +55,10 @@
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Threshold
- type: entity
id: WindowReinforcedDirectional
@@ -113,6 +117,9 @@
max: 1
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 3750
+ minPressureDelta: 2500
- type: StaticPrice
price: 22.5
diff --git a/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml b/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml
index 520c85c8bb..2506560572 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/rplasma.yml
@@ -53,6 +53,11 @@
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 150000
+ minPressureDelta: 100000
+ scalingType: Linear
+ scalingPower: 0.0001
- type: StaticPrice
price: 132
@@ -111,6 +116,10 @@
max: 1
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 37500
+ minPressureDelta: 25000
+ scalingType: Threshold
- type: StaticPrice
price: 66
diff --git a/Resources/Prototypes/Entities/Structures/Windows/ruranium.yml b/Resources/Prototypes/Entities/Structures/Windows/ruranium.yml
index 0030517593..943c9c66b7 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/ruranium.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/ruranium.yml
@@ -48,6 +48,11 @@
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 150000
+ minPressureDelta: 100000
+ scalingType: Linear
+ scalingPower: 0.0001
- type: StaticPrice
price: 215
- type: RadiationBlocker
@@ -106,6 +111,10 @@
max: 2
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 37500
+ minPressureDelta: 25000
+ scalingType: Threshold
- type: StaticPrice
price: 110
- type: RadiationBlocker
diff --git a/Resources/Prototypes/Entities/Structures/Windows/shuttle.yml b/Resources/Prototypes/Entities/Structures/Windows/shuttle.yml
index 6250f2d194..fde110faf8 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/shuttle.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/shuttle.yml
@@ -51,6 +51,11 @@
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Linear
+ scalingPower: 0.0005
- type: StaticPrice
price: 150
diff --git a/Resources/Prototypes/Entities/Structures/Windows/uranium.yml b/Resources/Prototypes/Entities/Structures/Windows/uranium.yml
index 7f7ec168c4..00645ca1f0 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/uranium.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/uranium.yml
@@ -46,6 +46,11 @@
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 75000
+ minPressureDelta: 50000
+ scalingType: Linear
+ scalingPower: 0.0005
- type: StaticPrice
price: 200
- type: RadiationBlocker
@@ -99,6 +104,10 @@
max: 1
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: DeltaPressure
+ minPressure: 18750
+ minPressureDelta: 12500
+ scalingType: Threshold
- type: StaticPrice
price: 100
- type: RadiationBlocker
diff --git a/Resources/Prototypes/Entities/Structures/Windows/window.yml b/Resources/Prototypes/Entities/Structures/Windows/window.yml
index ad36a58362..99c19c1a70 100644
--- a/Resources/Prototypes/Entities/Structures/Windows/window.yml
+++ b/Resources/Prototypes/Entities/Structures/Windows/window.yml
@@ -75,6 +75,11 @@
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: Airtight
+ - type: DeltaPressure
+ minPressure: 1000
+ minPressureDelta: 750
+ scalingType: Linear
+ scalingPower: 0.0005
- type: IconSmooth
key: windows
base: window
@@ -206,6 +211,10 @@
noAirWhenFullyAirBlocked: false
airBlockedDirection:
- South
+ - type: DeltaPressure
+ minPressure: 250
+ minPressureDelta: 187.5
+ scalingType: Threshold
- type: Construction
graph: WindowDirectional
node: windowDirectional