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