]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Atmos Delta-Pressure Window Shattering (#39238)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Wed, 3 Sep 2025 23:58:48 +0000 (16:58 -0700)
committerGitHub <noreply@github.com>
Wed, 3 Sep 2025 23:58:48 +0000 (16:58 -0700)
This PR adds delta-pressure damage. In short, airtight structures can now take damage proportional to the difference in pressures between the sides of the structure.

22 files changed:
Content.Benchmarks/DeltaPressureBenchmark.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Atmos/DeltaPressureTest.cs [new file with mode: 0644]
Content.Server/Atmos/Components/DeltaPressureComponent.cs [new file with mode: 0644]
Content.Server/Atmos/Components/GridAtmosphereComponent.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.BenchmarkHelpers.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Processing.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.cs
Content.Server/Atmos/EntitySystems/DeltaPressureSystem.cs [new file with mode: 0644]
Content.Shared/CCVar/CCVars.Atmos.cs
Resources/Locale/en-US/atmos/delta-pressure-component.ftl [new file with mode: 0644]
Resources/Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
Resources/Prototypes/Entities/Structures/Windows/plasma.yml
Resources/Prototypes/Entities/Structures/Windows/reinforced.yml
Resources/Prototypes/Entities/Structures/Windows/rplasma.yml
Resources/Prototypes/Entities/Structures/Windows/ruranium.yml
Resources/Prototypes/Entities/Structures/Windows/shuttle.yml
Resources/Prototypes/Entities/Structures/Windows/uranium.yml
Resources/Prototypes/Entities/Structures/Windows/window.yml

diff --git a/Content.Benchmarks/DeltaPressureBenchmark.cs b/Content.Benchmarks/DeltaPressureBenchmark.cs
new file mode 100644 (file)
index 0000000..b31b3ed
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Spawns N number of entities with a <see cref="DeltaPressureComponent"/> and
+/// simulates them for a number of ticks M.
+/// </summary>
+[Virtual]
+[GcServer(true)]
+//[MemoryDiagnoser]
+//[ThreadingDiagnoser]
+public class DeltaPressureBenchmark
+{
+    /// <summary>
+    /// Number of entities (windows, really) to spawn with a <see cref="DeltaPressureComponent"/>.
+    /// </summary>
+    [Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
+    public int EntityCount;
+
+    /// <summary>
+    /// Number of entities that each parallel processing job will handle.
+    /// </summary>
+    // [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;
+
+    /// <summary>
+    /// Number of entities to process per iteration in the DeltaPressure
+    /// processing loop.
+    /// </summary>
+    // [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<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>
+        _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<IEntityManager>();
+        _map = _entMan.System<SharedMapSystem>();
+        _random = server.ResolveDependency<IRobustRandom>();
+        _cvar = server.ResolveDependency<IConfigurationManager>();
+        _tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
+        _atmospereSystem = _entMan.System<AtmosphereSystem>();
+
+        _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<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(
+            uid,
+            _entMan.GetComponent<GridAtmosphereComponent>(uid),
+            _entMan.GetComponent<GasTileOverlayComponent>(uid),
+            _entMan.GetComponent<MapGridComponent>(uid),
+            _entMan.GetComponent<TransformComponent>(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 (file)
index 0000000..9dda130
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
+/// handling the DeltaPressureComponent.
+/// </summary>
+[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");
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [Test]
+    public async Task ProcessingListAutoJoinTest()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.EntMan;
+        var mapLoader = entMan.System<MapLoaderSystem>();
+        var atmosphereSystem = entMan.System<AtmosphereSystem>();
+        var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+        Entity<MapGridComponent> grid = default;
+        Entity<DeltaPressureComponent> 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<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(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();
+    }
+
+    /// <summary>
+    /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+    /// is not damaged by DeltaPressure.
+    /// </summary>
+    [Test]
+    public async Task ProcessingDeltaStandbyTest()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.EntMan;
+        var mapLoader = entMan.System<MapLoaderSystem>();
+        var atmosphereSystem = entMan.System<AtmosphereSystem>();
+        var transformSystem = entMan.System<SharedTransformSystem>();
+        var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+        Entity<MapGridComponent> grid = default;
+        Entity<DeltaPressureComponent> 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<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(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<GridAtmosphereComponent>(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();
+    }
+
+    /// <summary>
+    /// Asserts that an entity that needs to be damaged by DeltaPressure
+    /// is damaged by DeltaPressure when the pressure is above the threshold.
+    /// </summary>
+    [Test]
+    public async Task ProcessingDeltaDamageTest()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.EntMan;
+        var mapLoader = entMan.System<MapLoaderSystem>();
+        var atmosphereSystem = entMan.System<AtmosphereSystem>();
+        var transformSystem = entMan.System<SharedTransformSystem>();
+        var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+        Entity<MapGridComponent> grid = default;
+        Entity<DeltaPressureComponent> 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<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(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<GridAtmosphereComponent>(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();
+    }
+
+    /// <summary>
+    /// Asserts that an entity that doesn't need to be damaged by DeltaPressure
+    /// is not damaged by DeltaPressure when using absolute pressure thresholds.
+    /// </summary>
+    [Test]
+    public async Task ProcessingAbsoluteStandbyTest()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.EntMan;
+        var mapLoader = entMan.System<MapLoaderSystem>();
+        var atmosphereSystem = entMan.System<AtmosphereSystem>();
+        var transformSystem = entMan.System<SharedTransformSystem>();
+        var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+        Entity<MapGridComponent> grid = default;
+        Entity<DeltaPressureComponent> 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<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(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<GridAtmosphereComponent>(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();
+    }
+
+    /// <summary>
+    /// Asserts that an entity that needs to be damaged by DeltaPressure
+    /// is damaged by DeltaPressure when the pressure is above the absolute threshold.
+    /// </summary>
+    [Test]
+    public async Task ProcessingAbsoluteDamageTest()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.EntMan;
+        var mapLoader = entMan.System<MapLoaderSystem>();
+        var atmosphereSystem = entMan.System<AtmosphereSystem>();
+        var transformSystem = entMan.System<SharedTransformSystem>();
+        var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
+
+        Entity<MapGridComponent> grid = default;
+        Entity<DeltaPressureComponent> 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<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(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<GridAtmosphereComponent>(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 (file)
index 0000000..f90c133
--- /dev/null
@@ -0,0 +1,139 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+
+namespace Content.Server.Atmos.Components;
+
+/// <summary>
+/// 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 <see cref="GridAtmosphereComponent"/>.
+/// The entities are automatically added and removed from this list, and automatically
+/// added on initialization.
+/// </summary>
+/// <remarks> Note that the entity should have an <see cref="AirtightComponent"/> and be a grid structure.</remarks>
+[RegisterComponent]
+public sealed partial class DeltaPressureComponent : Component
+{
+    /// <summary>
+    /// Whether the entity is currently in the processing list of the grid's <see cref="GridAtmosphereComponent"/>.
+    /// </summary>
+    [DataField(readOnly: true)]
+    [ViewVariables(VVAccess.ReadOnly)]
+    [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+    public bool InProcessingList;
+
+    /// <summary>
+    /// Whether this entity is currently taking damage from pressure.
+    /// </summary>
+    [DataField(readOnly: true)]
+    [ViewVariables(VVAccess.ReadOnly)]
+    [Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
+    public bool IsTakingDamage;
+
+    /// <summary>
+    /// The current cached position of this entity on the grid.
+    /// Updated via MoveEvent.
+    /// </summary>
+    [DataField(readOnly: true)]
+    public Vector2i CurrentPosition = Vector2i.Zero;
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [DataField]
+    public EntityUid? GridUid;
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [DataField]
+    public float RandomDamageChance = 1f;
+
+    /// <summary>
+    /// 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 <see cref="DeltaPressureDamageScalingType"/> 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.
+    /// </summary>
+    [DataField]
+    public DamageSpecifier BaseDamage = new()
+    {
+        DamageDict = new Dictionary<string, FixedPoint2>
+        {
+            { "Structural", 10 },
+        },
+    };
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [DataField]
+    public float MinPressure = 10000;
+
+    /// <summary>
+    /// The minimum difference in pressure between any side required for the entity to start taking damage.
+    /// </summary>
+    [DataField]
+    public float MinPressureDelta = 7500;
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    [DataField]
+    public float MaxEffectivePressure = 10000;
+
+    /// <summary>
+    /// Simple constant to affect the scaling behavior.
+    /// See comments in the <see cref="DeltaPressureDamageScalingType"/> types to see how this affects scaling.
+    /// </summary>
+    [DataField]
+    public float ScalingPower = 1;
+
+    /// <summary>
+    /// Defines the scaling behavior for the damage.
+    /// </summary>
+    [DataField]
+    public DeltaPressureDamageScalingType ScalingType = DeltaPressureDamageScalingType.Threshold;
+}
+
+/// <summary>
+/// An enum that defines how the damage dealt by the <see cref="DeltaPressureComponent"/> 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.
+/// </summary>
+[Serializable]
+public enum DeltaPressureDamageScalingType : byte
+{
+    /// <summary>
+    /// Damage dealt will be constant as long as the minimum values are met.
+    /// Scaling power is ignored.
+    /// </summary>
+    Threshold,
+
+    /// <summary>
+    /// Damage dealt will be a linear function.
+    /// Scaling power determines the slope of the function.
+    /// </summary>
+    Linear,
+
+    /// <summary>
+    /// Damage dealt will be a logarithmic function.
+    /// Scaling power determines the base of the log.
+    /// </summary>
+    Log,
+}
index e682fd09644efb0c52743f56caaeab552fba1865..2d36d2bd148ca967316f9e4869cc428287fec528 100644 (file)
@@ -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;
 
+        /// <summary>
+        /// A list of entities that have a <see cref="DeltaPressureComponent"/> and are to
+        /// be processed by the <see cref="DeltaPressureSystem"/>, if enabled.
+        ///
+        /// To prevent massive bookkeeping overhead, this list is processed in-place,
+        /// with add/remove/find operations helped via a dict.
+        /// </summary>
+        /// <remarks>If you want to add/remove/find entities in this list,
+        /// use the API methods in the Atmospherics API.</remarks>
+        [ViewVariables]
+        public readonly List<Entity<DeltaPressureComponent>> DeltaPressureEntities =
+            new(AtmosphereSystem.DeltaPressurePreAllocateLength);
+
+        /// <summary>
+        /// An index lookup for the <see cref="DeltaPressureEntities"/> list.
+        /// Used for add/remove/find operations to speed up processing.
+        /// </summary>
+        public readonly Dictionary<EntityUid, int> DeltaPressureEntityLookup =
+            new(AtmosphereSystem.DeltaPressurePreAllocateLength);
+
+        /// <summary>
+        /// Integer that indicates the current position in the
+        /// <see cref="DeltaPressureEntities"/> list that is being processed.
+        /// </summary>
+        [ViewVariables(VVAccess.ReadOnly)]
+        public int DeltaPressureCursor;
+
+        /// <summary>
+        /// Queue of entities that need to have damage applied to them.
+        /// </summary>
+        [ViewVariables]
+        public readonly ConcurrentQueue<AtmosphereSystem.DeltaPressureDamageResult> DeltaPressureDamageResults = new();
+
         [ViewVariables]
         public readonly HashSet<IPipeNet> PipeNets = new();
 
index 67f3a207794a404ef149cf758a0ea47642f98829..87cfce135d7a5e94db33da259c1844221646b005 100644 (file)
@@ -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;
     }
 
+    /// <summary>
+    /// Adds an entity with a DeltaPressureComponent to the DeltaPressure processing list.
+    /// Also fills in important information on the component itself.
+    /// </summary>
+    /// <param name="grid">The grid to add the entity to.</param>
+    /// <param name="ent">The entity to add.</param>
+    /// <returns>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.</returns>
+    [PublicAPI]
+    public bool TryAddDeltaPressureEntity(Entity<GridAtmosphereComponent?> grid, Entity<DeltaPressureComponent> 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<MapGridComponent>(grid),
+            xform.Coordinates);
+
+        ent.Comp.GridUid = grid.Owner;
+        ent.Comp.InProcessingList = true;
+
+        return true;
+    }
+
+    /// <summary>
+    /// Removes an entity with a DeltaPressureComponent from the DeltaPressure processing list.
+    /// </summary>
+    /// <param name="grid">The grid to remove the entity from.</param>
+    /// <param name="ent">The entity to remove.</param>
+    /// <returns>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.</returns>
+    [PublicAPI]
+    public bool TryRemoveDeltaPressureEntity(Entity<GridAtmosphereComponent?> grid, Entity<DeltaPressureComponent> 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;
+    }
+
+    /// <summary>
+    /// Checks if a DeltaPressureComponent is currently considered for processing on a grid.
+    /// </summary>
+    /// <param name="grid">The grid that the entity may belong to.</param>
+    /// <param name="ent">The entity to check.</param>
+    /// <returns>True if the entity is part of the processing list, false otherwise.</returns>
+    [PublicAPI]
+    public bool IsDeltaPressureEntityInList(Entity<GridAtmosphereComponent?> grid, Entity<DeltaPressureComponent> 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 (file)
index 0000000..f86ebce
--- /dev/null
@@ -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.
+     */
+
+    /// <summary>
+    /// Runs the grid entity through a single processing stage of the atmosphere simulation.
+    /// Ideal for benchmarking single stages of the simulation.
+    /// </summary>
+    /// <param name="ent">The entity to profile Atmospherics with.</param>
+    /// <param name="state">The state to profile on the entity.</param>
+    /// <param name="mapEnt">The optional mapEntity to provide when benchmarking ProcessAtmosDevices.</param>
+    /// <returns>True if the processing stage completed, false if the processing stage had to pause processing due to time constraints.</returns>
+    public bool RunProcessingStage(
+        Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
+        AtmosphereProcessingState state,
+        Entity<MapAtmosphereComponent?>? 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<MapAtmosphereComponent> must be provided when benchmarking ProcessAtmosDevices."),
+            _ => throw new ArgumentOutOfRangeException(),
+        };
+        ent.Comp1.ProcessingPaused = !processingPaused;
+
+        return processingPaused;
+    }
+}
index 3aaa5429fb0344424da48bbfdf2b45177e2d1512..f24f0ae171fcd9c8128d352b766502b6d4d5dee6 100644 (file)
@@ -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; }
 
         /// <summary>
         /// 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 (file)
index 0000000..207589e
--- /dev/null
@@ -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
+{
+    /// <summary>
+    /// 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.
+    /// </summary>
+    private const int DeltaPressurePairCount = Atmospherics.Directions / 2;
+
+    /// <summary>
+    /// The length to pre-allocate list/dicts of delta pressure entities on a <see cref="GridAtmosphereComponent"/>.
+    /// </summary>
+    public const int DeltaPressurePreAllocateLength = 1000;
+
+    /// <summary>
+    /// Processes a singular entity, determining the pressures it's experiencing and applying damage based on that.
+    /// </summary>
+    /// <param name="ent">The entity to process.</param>
+    /// <param name="gridAtmosComp">The <see cref="GridAtmosphereComponent"/> that belongs to the entity's GridUid.</param>
+    private void ProcessDeltaPressureEntity(Entity<DeltaPressureComponent> 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<float> pressures = stackalloc float[Atmospherics.Directions];
+
+        GetBulkTileAtmospherePressures(tiles, pressures);
+
+        Span<float> opposingGroupA = stackalloc float[DeltaPressurePairCount];
+        Span<float> opposingGroupB = stackalloc float[DeltaPressurePairCount];
+        Span<float> 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);
+    }
+
+    /// <summary>
+    /// A DeltaPressure helper method that retrieves the pressures of all gas mixtures
+    /// in the given array of <see cref="TileAtmosphere"/>s, and stores the results in the
+    /// provided <paramref name="pressures"/> span.
+    /// The tiles array length is limited to Atmosphereics.Directions.
+    /// </summary>
+    /// <param name="tiles">The tiles array to find the pressures of.</param>
+    /// <param name="pressures">The span to store the pressures to - this should be the same length
+    /// as the tile array.</param>
+    /// <remarks>This is for internal use of the DeltaPressure system -
+    /// it may not be a good idea to use this generically.</remarks>
+    private static void GetBulkTileAtmospherePressures(TileAtmosphere?[] tiles, Span<float> 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<float> mixtVol = stackalloc float[Atmospherics.Directions];
+        Span<float> mixtTemp = stackalloc float[Atmospherics.Directions];
+        Span<float> mixtMoles = stackalloc float[Atmospherics.Directions];
+        Span<float> 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);
+    }
+
+    /// <summary>
+    /// Packs data into a <see cref="DeltaPressureDamageResult"/> data struct and enqueues it
+    /// into the <see cref="GridAtmosphereComponent.DeltaPressureDamageResults"/> queue for
+    /// later processing.
+    /// </summary>
+    /// <param name="ent">The entity to enqueue if necessary.</param>
+    /// <param name="gridAtmosComp">The <see cref="GridAtmosphereComponent"/>
+    /// containing the queue.</param>
+    /// <param name="pressure">The current absolute pressure being experienced by the entity.</param>
+    /// <param name="delta">The current delta pressure being experienced by the entity.</param>
+    private static void EnqueueDeltaPressureDamage(Entity<DeltaPressureComponent> 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));
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    /// <param name="system">The AtmosphereSystem instance.</param>
+    /// <param name="atmosphere">The GridAtmosphereComponent to work with.</param>
+    /// <param name="startIndex">The index in the DeltaPressureEntities list to start from.</param>
+    /// <param name="cvarBatchSize">The batch size to use for this job.</param>
+    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);
+        }
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    /// <param name="Ent">The entity to deal damage to.</param>
+    /// <param name="Pressure">The current absolute pressure the entity is experiencing.</param>
+    /// <param name="DeltaPressure">The current delta pressure the entity is experiencing.</param>
+    public readonly record struct DeltaPressureDamageResult(
+        Entity<DeltaPressureComponent> Ent,
+        float Pressure,
+        float DeltaPressure);
+
+    /// <summary>
+    /// Does damage to an entity depending on the pressure experienced by it, based on the
+    /// entity's <see cref="DeltaPressureComponent"/>.
+    /// </summary>
+    /// <param name="ent">The entity to apply damage to.</param>
+    /// <param name="pressure">The absolute pressure being exerted on the entity.</param>
+    /// <param name="deltaPressure">The delta pressure being exerted on the entity.</param>
+    private void PerformDamage(Entity<DeltaPressureComponent> 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;
+    }
+
+    /// <summary>
+    /// Returns a new DamageSpecifier scaled based on values on an entity with a DeltaPressureComponent.
+    /// </summary>
+    /// <param name="ent">The entity to base the manipulations off of (pull scaling type)</param>
+    /// <param name="damage">The base damage specifier to scale.</param>
+    /// <param name="pressure">The pressure being exerted on the entity.</param>
+    /// <returns>A scaled DamageSpecifier.</returns>
+    private static DamageSpecifier ScaleDamage(Entity<DeltaPressureComponent> 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;
+    }
+}
index 02d389b2153766873c4110bd41af276476cc3953..9b8654af6dcee452f7e2453ad08e537321d7031b 100644 (file)
@@ -467,6 +467,66 @@ namespace Content.Server.Atmos.EntitySystems
             return true;
         }
 
+        /// <summary>
+        /// Processes all entities with a <see cref="DeltaPressureComponent"/>, doing damage to them
+        /// depending on certain pressure differential conditions.
+        /// </summary>
+        /// <returns>True if we've finished processing all entities that required processing this run,
+        /// otherwise, false.</returns>
+        private bool ProcessDeltaPressure(Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> 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,
index e9383f3a23a7dedda69cf78ce100c3c27312b705..00b7e169134c8308a3ecb630136b7d91cf9b0a40 100644 (file)
@@ -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 (file)
index 0000000..a6cbec0
--- /dev/null
@@ -0,0 +1,82 @@
+using Content.Server.Atmos.Components;
+using Content.Shared.Examine;
+using Robust.Shared.Map.Components;
+
+namespace Content.Server.Atmos.EntitySystems;
+
+/// <summary>
+/// <para>System that handles <see cref="DeltaPressureComponent"/>.</para>
+///
+/// <para>Entities with a <see cref="DeltaPressureComponent"/> will take damage per atmostick
+/// depending on the pressure they experience.</para>
+///
+/// <para>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.</para>
+/// </summary>
+public sealed class DeltaPressureSystem : EntitySystem
+{
+    [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+    [Dependency] private readonly SharedMapSystem _map = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DeltaPressureComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<DeltaPressureComponent, ComponentShutdown>(OnComponentShutdown);
+        SubscribeLocalEvent<DeltaPressureComponent, ExaminedEvent>(OnExamined);
+        SubscribeLocalEvent<DeltaPressureComponent, MoveEvent>(OnMoveEvent);
+
+        SubscribeLocalEvent<DeltaPressureComponent, GridUidChangedEvent>(OnGridChanged);
+    }
+
+    private void OnMoveEvent(Entity<DeltaPressureComponent> ent, ref MoveEvent args)
+    {
+        var xform = Transform(ent);
+        // May move off-grid, so, might as well protect against that.
+        if (!TryComp<MapGridComponent>(xform.GridUid, out var mapGridComponent))
+        {
+            return;
+        }
+
+        ent.Comp.CurrentPosition = _map.CoordinatesToTile(xform.GridUid.Value, mapGridComponent, args.NewPosition);
+    }
+
+    private void OnComponentInit(Entity<DeltaPressureComponent> ent, ref ComponentInit args)
+    {
+        var xform = Transform(ent);
+        if (xform.GridUid == null)
+            return;
+
+        _atmosphereSystem.TryAddDeltaPressureEntity(xform.GridUid.Value, ent);
+    }
+
+    private void OnComponentShutdown(Entity<DeltaPressureComponent> 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<DeltaPressureComponent> ent, ref ExaminedEvent args)
+    {
+        if (ent.Comp.IsTakingDamage)
+            args.PushMarkup(Loc.GetString("window-taking-damage"));
+    }
+
+    private void OnGridChanged(Entity<DeltaPressureComponent> ent, ref GridUidChangedEvent args)
+    {
+        if (args.OldGrid != null)
+        {
+            _atmosphereSystem.TryRemoveDeltaPressureEntity(args.OldGrid.Value, ent);
+        }
+
+        if (args.NewGrid != null)
+        {
+            _atmosphereSystem.TryAddDeltaPressureEntity(args.NewGrid.Value, ent);
+        }
+    }
+}
index cc1069b4fc87522e1244d81296f19b925717e7a2..7ef40b7911b8fa01a3dd031adddd6a4b2d4489fe 100644 (file)
@@ -150,4 +150,31 @@ public sealed partial class CCVars
     /// </summary>
     public static readonly CVarDef<float> AtmosTankFragment =
         CVarDef.Create("atmos.max_explosion_range", 26f, CVar.SERVERONLY);
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    // TODO: Needs CVARs for global configuration, like min pressure, max damage, etc.
+    public static readonly CVarDef<bool> DeltaPressureDamage =
+        CVarDef.Create("atmos.delta_pressure_damage", true, CVar.SERVERONLY);
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public static readonly CVarDef<int> DeltaPressureParallelToProcessPerIteration =
+        CVarDef.Create("atmos.delta_pressure_parallel_process_per_iteration", 1000, CVar.SERVERONLY);
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public static readonly CVarDef<int> 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 (file)
index 0000000..f8ffd6d
--- /dev/null
@@ -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 (file)
index 0000000..cedbfb1
--- /dev/null
@@ -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
+...
index 811385645c83eed585099261968fc85cd5d6a343..07618243d45c0345060245f66a00b7e4e53b7379 100644 (file)
     noAirWhenFullyAirBlocked: false
     airBlockedDirection:
       - South
+  - type: DeltaPressure
+    minPressure: 250
+    minPressureDelta: 187.5
+    scalingType: Threshold
   - type: Construction
     graph: Windoor
     node: windoor
   - type: Construction
     graph: Windoor
     node: windoorSecure
+  - type: DeltaPressure
+    minPressure: 3750
+    minPressureDelta: 2500
+    scalingType: Threshold
   - type: StaticPrice
     price: 350
   - type: Tag
   - type: Construction
     graph: Windoor
     node: pwindoor
+  - type: DeltaPressure
+    minPressure: 18750
+    minPressureDelta: 12500
+    scalingType: Threshold
   - type: StaticPrice
     price: 500
   - type: RadiationBlocker
   - type: Construction
     graph: Windoor
     node: pwindoorSecure
+  - type: DeltaPressure
+    minPressure: 37500
+    minPressureDelta: 25000
+    scalingType: Threshold
   - type: StaticPrice
     price: 500
   - type: RadiationBlocker
             max: 2
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 18750
+    minPressureDelta: 12500
+    scalingType: Threshold
   - type: Construction
     graph: Windoor
     node: uwindoor
             max: 2
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 37500
+    minPressureDelta: 25000
+    scalingType: Threshold
   - type: Construction
     graph: Windoor
     node: uwindoorSecure
index 9e73dce7a1b0e122b08c8951972104c20b1b4bf7..d6761239a8aa1bbd93f8da5d0c166639b0b4847a 100644 (file)
     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
             max: 1
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 18750
+    minPressureDelta: 12500
+    scalingType: Threshold
   - type: StaticPrice
     price: 50
   - type: RadiationBlocker
index b9d4e6fd6394ae0758236b66f864991dbd9735e0..912313c13d7c89cb8c5e89e890ac8da68b1852ed 100644 (file)
     trackAllDamage: true
     damageOverlay:
       sprite: Structures/Windows/cracks.rsi
+  - type: DeltaPressure
+    minPressure: 15000
+    minPressureDelta: 10000
+    scalingType: Threshold
 
 - type: entity
   id: WindowReinforcedDirectional
             max: 1
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 3750
+    minPressureDelta: 2500
   - type: StaticPrice
     price: 22.5
 
index 520c85c8bb020a5c7642ab0e5e0c4a1d95690b36..2506560572da43bb69aef51ee27203579c5ad3d7 100644 (file)
     trackAllDamage: true
     damageOverlay:
       sprite: Structures/Windows/cracks.rsi
+  - type: DeltaPressure
+    minPressure: 150000
+    minPressureDelta: 100000
+    scalingType: Linear
+    scalingPower: 0.0001
   - type: StaticPrice
     price: 132
 
             max: 1
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 37500
+    minPressureDelta: 25000
+    scalingType: Threshold
   - type: StaticPrice
     price: 66
 
index 003051759345849ebf16150e57eeb2590d3c5f5d..943c9c66b75ecfbf7d3b90d22bf9200f6d232d4f 100644 (file)
     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
             max: 2
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 37500
+    minPressureDelta: 25000
+    scalingType: Threshold
   - type: StaticPrice
     price: 110
   - type: RadiationBlocker
index 6250f2d1942f79b4b90e4705cf772b7bf7395da6..fde110faf821a150f1ed8f65288f702754646335 100644 (file)
     trackAllDamage: true
     damageOverlay:
       sprite: Structures/Windows/cracks.rsi
+  - type: DeltaPressure
+    minPressure: 15000
+    minPressureDelta: 10000
+    scalingType: Linear
+    scalingPower: 0.0005
   - type: StaticPrice
     price: 150
 
index 7f7ec168c4a091c9b6eaace6aa6d33d634498a25..00645ca1f04e7e44bdeffa3aad5db6bcf4d91c7f 100644 (file)
     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
             max: 1
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: DeltaPressure
+    minPressure: 18750
+    minPressureDelta: 12500
+    scalingType: Threshold
   - type: StaticPrice
     price: 100
   - type: RadiationBlocker
index ad36a583623f750e63711c0bd47cadf3a4c0c041..99c19c1a7081b8a442746d0691969f03c91031b4 100644 (file)
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
   - type: Airtight
+  - type: DeltaPressure
+    minPressure: 1000
+    minPressureDelta: 750
+    scalingType: Linear
+    scalingPower: 0.0005
   - type: IconSmooth
     key: windows
     base: window
     noAirWhenFullyAirBlocked: false
     airBlockedDirection:
     - South
+  - type: DeltaPressure
+    minPressure: 250
+    minPressureDelta: 187.5
+    scalingType: Threshold
   - type: Construction
     graph: WindowDirectional
     node: windowDirectional