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.
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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,
+}
+using System.Collections.Concurrent;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Serialization;
[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();
+using System.Diagnostics;
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components;
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;
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);
--- /dev/null
+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;
+ }
+}
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
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);
}
}
}
--- /dev/null
+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;
+ }
+}
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)
num--;
if (!ExcitedGroups)
num--;
+ if (!DeltaPressureDamage)
+ num--;
if (!Superconduction)
num--;
return num * AtmosTime;
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;
ActiveTiles,
ExcitedGroups,
HighPressureDelta,
+ DeltaPressure,
Hotspots,
Superconductivity,
PipeNet,
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;
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;
[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!;
[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;
--- /dev/null
+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);
+ }
+ }
+}
/// </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);
}
--- /dev/null
+window-taking-damage = [color=orange]It's straining under pressure![/color]
--- /dev/null
+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
+...
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
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
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
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
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
trackAllDamage: true
damageOverlay:
sprite: Structures/Windows/cracks.rsi
+ - type: DeltaPressure
+ minPressure: 15000
+ minPressureDelta: 10000
+ scalingType: Linear
+ scalingPower: 0.0005
- type: StaticPrice
price: 150
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
- !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