Atmos Delta-Pressure Window Shattering (#39238)

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.
This commit is contained in:
ArtisticRoomba
2025-09-03 16:58:48 -07:00
committed by GitHub
parent f63eb2e97a
commit 20f2cb920b
22 changed files with 1685 additions and 1 deletions

View File

@@ -0,0 +1,174 @@
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnosers;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
/// <summary>
/// Spawns N number of entities with a <see cref="DeltaPressureComponent"/> and
/// simulates them for a number of ticks M.
/// </summary>
[Virtual]
[GcServer(true)]
//[MemoryDiagnoser]
//[ThreadingDiagnoser]
public class DeltaPressureBenchmark
{
/// <summary>
/// Number of entities (windows, really) to spawn with a <see cref="DeltaPressureComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
public int EntityCount;
/// <summary>
/// Number of entities that each parallel processing job will handle.
/// </summary>
// [Params(1, 10, 100, 1000, 5000, 10000)] For testing how multithreading parameters affect performance (THESE TESTS TAKE 16+ HOURS TO RUN)
[Params(10)]
public int BatchSize;
/// <summary>
/// Number of entities to process per iteration in the DeltaPressure
/// processing loop.
/// </summary>
// [Params(100, 1000, 5000, 10000, 50000)]
[Params(1000)]
public int EntitiesPerIteration;
private readonly EntProtoId _windowProtoId = "Window";
private readonly EntProtoId _wallProtoId = "WallPlastitaniumIndestructible";
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private SharedMapSystem _map = default!;
private IRobustRandom _random = default!;
private IConfigurationManager _cvar = default!;
private ITileDefinitionManager _tileDefMan = default!;
private AtmosphereSystem _atmospereSystem = default!;
private Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>
_testEnt;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_map = _entMan.System<SharedMapSystem>();
_random = server.ResolveDependency<IRobustRandom>();
_cvar = server.ResolveDependency<IConfigurationManager>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_atmospereSystem = _entMan.System<AtmosphereSystem>();
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
_cvar.SetCVar(CCVars.DeltaPressureParallelToProcessPerIteration, EntitiesPerIteration);
_cvar.SetCVar(CCVars.DeltaPressureParallelBatchSize, BatchSize);
var plating = _tileDefMan["Plating"].TileId;
/*
Basically, we want to have a 5-wide grid of tiles.
Edges are walled, and the length of the grid is determined by N + 2.
Windows should only touch the top and bottom walls, and each other.
*/
var length = EntityCount + 2; // ensures we can spawn exactly N windows between side walls
const int height = 5;
await server.WaitPost(() =>
{
// Fill required tiles (extend grid) with plating
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
}
}
// Spawn perimeter walls and windows row in the middle (y = 2)
const int midY = height / 2;
for (var x = 0; x < length; x++)
{
for (var y = 0; y < height; y++)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
var isPerimeter = x == 0 || x == length - 1 || y == 0 || y == height - 1;
if (isPerimeter)
{
_entMan.SpawnEntity(_wallProtoId, coords);
continue;
}
// Spawn windows only on the middle row, spanning interior (excluding side walls)
if (y == midY)
{
_entMan.SpawnEntity(_windowProtoId, coords);
}
}
}
});
// Next we run the fixgridatmos command to ensure that we have some air on our grid.
// Wait a little bit as well.
// TODO: Unhardcode command magic string when fixgridatmos is an actual command we can ref and not just
// a stamp-on in AtmosphereSystem.
await _pair.WaitCommand("fixgridatmos " + mapdata.Grid.Owner, 1);
var uid = mapdata.Grid.Owner;
_testEnt = new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(
uid,
_entMan.GetComponent<GridAtmosphereComponent>(uid),
_entMan.GetComponent<GasTileOverlayComponent>(uid),
_entMan.GetComponent<MapGridComponent>(uid),
_entMan.GetComponent<TransformComponent>(uid));
}
[Benchmark]
public async Task PerformFullProcess()
{
await _pair.Server.WaitPost(() =>
{
while (!_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure)) { }
});
}
[Benchmark]
public async Task PerformSingleRunProcess()
{
await _pair.Server.WaitPost(() =>
{
_atmospereSystem.RunProcessingStage(_testEnt, AtmosphereProcessingState.DeltaPressure);
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -0,0 +1,417 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
/// <summary>
/// Tests for AtmosphereSystem.DeltaPressure and surrounding systems
/// handling the DeltaPressureComponent.
/// </summary>
[TestFixture]
[TestOf(typeof(DeltaPressureSystem))]
public sealed class DeltaPressureTest
{
#region Prototypes
[TestPrototypes]
private const string Prototypes = @"
- type: entity
parent: BaseStructure
id: DeltaPressureSolidTest
placement:
mode: SnapgridCenter
snap:
- Wall
components:
- type: Physics
bodyType: Static
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: ""-0.5,-0.5,0.5,0.5""
mask:
- FullTileMask
layer:
- WallLayer
density: 1000
- type: Airtight
- type: DeltaPressure
minPressure: 15000
minPressureDelta: 10000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
- type: Damageable
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 300
behaviors:
- !type:SpawnEntitiesBehavior
spawn:
Girder:
min: 1
max: 1
- !type:DoActsBehavior
acts: [ ""Destruction"" ]
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestNoAutoJoin
components:
- type: DeltaPressure
autoJoinProcessingList: false
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestAbsolute
components:
- type: DeltaPressure
minPressure: 10000
minPressureDelta: 15000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
";
#endregion
private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
/// <summary>
/// Asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
/// set to true is automatically added to the DeltaPressure processing list
/// on the grid's GridAtmosphereComponent.
///
/// Also asserts that an entity with a DeltaPressureComponent with autoJoinProcessingList
/// set to false is not automatically added to the DeltaPressure processing list.
/// </summary>
[Test]
public async Task ProcessingListAutoJoinTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
await server.WaitAssertion(() =>
{
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
entMan.DeleteEntity(uid);
Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that doesn't need to be damaged by DeltaPressure
/// is not damaged by DeltaPressure.
/// </summary>
[Test]
public async Task ProcessingDeltaStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressureDelta - 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that needs to be damaged by DeltaPressure
/// is damaged by DeltaPressure when the pressure is above the threshold.
/// </summary>
[Test]
public async Task ProcessingDeltaDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
// Need to spawn an entity each run to ensure it works for all directions.
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressureDelta + 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that doesn't need to be damaged by DeltaPressure
/// is not damaged by DeltaPressure when using absolute pressure thresholds.
/// </summary>
[Test]
public async Task ProcessingAbsoluteStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
var toPressurize = dpEnt.Comp!.MinPressure - 10; // just below absolute threshold
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that an entity that needs to be damaged by DeltaPressure
/// is damaged by DeltaPressure when the pressure is above the absolute threshold.
/// </summary>
[Test]
public async Task ProcessingAbsoluteDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
{
// Spawn fresh entity each iteration to verify all directions work
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
// Above absolute threshold but below delta threshold to ensure absolute alone causes damage
var toPressurize = dpEnt.Comp!.MinPressure + 10;
var moles = (toPressurize * tile.Air.Volume) / (Atmospherics.R * Atmospherics.T20C);
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
}

View File

@@ -0,0 +1,139 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
namespace Content.Server.Atmos.Components;
/// <summary>
/// Entities that have this component will have damage done to them depending on the local pressure
/// environment that they reside in.
///
/// Atmospherics.DeltaPressure batch-processes entities with this component in a list on
/// the grid's <see cref="GridAtmosphereComponent"/>.
/// The entities are automatically added and removed from this list, and automatically
/// added on initialization.
/// </summary>
/// <remarks> Note that the entity should have an <see cref="AirtightComponent"/> and be a grid structure.</remarks>
[RegisterComponent]
public sealed partial class DeltaPressureComponent : Component
{
/// <summary>
/// Whether the entity is currently in the processing list of the grid's <see cref="GridAtmosphereComponent"/>.
/// </summary>
[DataField(readOnly: true)]
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
public bool InProcessingList;
/// <summary>
/// Whether this entity is currently taking damage from pressure.
/// </summary>
[DataField(readOnly: true)]
[ViewVariables(VVAccess.ReadOnly)]
[Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
public bool IsTakingDamage;
/// <summary>
/// The current cached position of this entity on the grid.
/// Updated via MoveEvent.
/// </summary>
[DataField(readOnly: true)]
public Vector2i CurrentPosition = Vector2i.Zero;
/// <summary>
/// The grid this entity is currently joined to for processing.
/// Required for proper deletion, as we cannot reference the grid
/// for removal while the entity is being deleted.
/// </summary>
[DataField]
public EntityUid? GridUid;
/// <summary>
/// The percent chance that the entity will take damage each atmos tick,
/// when the entity is above the damage threshold.
/// Makes it so that windows don't all break in one go.
/// Float is from 0 to 1, where 1 means 100% chance.
/// If this is set to 0, the entity will never take damage.
/// </summary>
[DataField]
public float RandomDamageChance = 1f;
/// <summary>
/// The base damage applied to the entity per atmos tick when it is above the damage threshold.
/// This damage will be scaled as defined by the <see cref="DeltaPressureDamageScalingType"/> enum
/// depending on the current effective pressure this entity is experiencing.
/// Note that this damage will scale depending on the pressure above the minimum pressure,
/// not at the current pressure.
/// </summary>
[DataField]
public DamageSpecifier BaseDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Structural", 10 },
},
};
/// <summary>
/// The minimum pressure in kPa at which the entity will start taking damage.
/// This doesn't depend on the difference in pressure.
/// The entity will start to take damage if it is exposed to this pressure.
/// This is needed because we don't correctly handle 2-layer windows yet.
/// </summary>
[DataField]
public float MinPressure = 10000;
/// <summary>
/// The minimum difference in pressure between any side required for the entity to start taking damage.
/// </summary>
[DataField]
public float MinPressureDelta = 7500;
/// <summary>
/// The maximum pressure at which damage will no longer scale.
/// If the effective pressure goes beyond this, the damage will be considered at this pressure.
/// </summary>
[DataField]
public float MaxEffectivePressure = 10000;
/// <summary>
/// Simple constant to affect the scaling behavior.
/// See comments in the <see cref="DeltaPressureDamageScalingType"/> types to see how this affects scaling.
/// </summary>
[DataField]
public float ScalingPower = 1;
/// <summary>
/// Defines the scaling behavior for the damage.
/// </summary>
[DataField]
public DeltaPressureDamageScalingType ScalingType = DeltaPressureDamageScalingType.Threshold;
}
/// <summary>
/// An enum that defines how the damage dealt by the <see cref="DeltaPressureComponent"/> scales
/// depending on the pressure experienced by the entity.
/// The scaling is done on the effective pressure, which is the pressure above the minimum pressure.
/// See https://www.desmos.com/calculator/9ctlq3zpnt for a visual representation of the scaling types.
/// </summary>
[Serializable]
public enum DeltaPressureDamageScalingType : byte
{
/// <summary>
/// Damage dealt will be constant as long as the minimum values are met.
/// Scaling power is ignored.
/// </summary>
Threshold,
/// <summary>
/// Damage dealt will be a linear function.
/// Scaling power determines the slope of the function.
/// </summary>
Linear,
/// <summary>
/// Damage dealt will be a logarithmic function.
/// Scaling power determines the base of the log.
/// </summary>
Log,
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Serialization; using Content.Server.Atmos.Serialization;
@@ -61,6 +62,39 @@ namespace Content.Server.Atmos.Components
[ViewVariables] [ViewVariables]
public int HighPressureDeltaCount => HighPressureDelta.Count; 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] [ViewVariables]
public readonly HashSet<IPipeNet> PipeNets = new(); public readonly HashSet<IPipeNet> PipeNets = new();

View File

@@ -1,3 +1,4 @@
using System.Diagnostics;
using System.Linq; using System.Linq;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Atmos.Piping.Components; using Content.Server.Atmos.Piping.Components;
@@ -5,6 +6,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Reactions; using Content.Shared.Atmos.Reactions;
using JetBrains.Annotations;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -319,6 +321,107 @@ public partial class AtmosphereSystem
return true; 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 [ByRefEvent] private record struct SetSimulatedGridMethodEvent
(EntityUid Grid, bool Simulated, bool Handled = false); (EntityUid Grid, bool Simulated, bool Handled = false);

View File

@@ -0,0 +1,49 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos.Components;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
/*
Helper methods to assist in getting very low overhead profiling of individual stages of the atmospherics simulation.
Ideal for benchmarking and performance testing.
These methods obviously aren't to be used in production code. Don't call them. They know my voice.
*/
/// <summary>
/// Runs the grid entity through a single processing stage of the atmosphere simulation.
/// Ideal for benchmarking single stages of the simulation.
/// </summary>
/// <param name="ent">The entity to profile Atmospherics with.</param>
/// <param name="state">The state to profile on the entity.</param>
/// <param name="mapEnt">The optional mapEntity to provide when benchmarking ProcessAtmosDevices.</param>
/// <returns>True if the processing stage completed, false if the processing stage had to pause processing due to time constraints.</returns>
public bool RunProcessingStage(
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ent,
AtmosphereProcessingState state,
Entity<MapAtmosphereComponent?>? mapEnt = null)
{
var processingPaused = state switch
{
AtmosphereProcessingState.Revalidate => ProcessRevalidate(ent),
AtmosphereProcessingState.TileEqualize => ProcessTileEqualize(ent),
AtmosphereProcessingState.ActiveTiles => ProcessActiveTiles(ent),
AtmosphereProcessingState.ExcitedGroups => ProcessExcitedGroups(ent),
AtmosphereProcessingState.HighPressureDelta => ProcessHighPressureDelta(ent),
AtmosphereProcessingState.DeltaPressure => ProcessDeltaPressure(ent),
AtmosphereProcessingState.Hotspots => ProcessHotspots(ent),
AtmosphereProcessingState.Superconductivity => ProcessSuperconductivity(ent),
AtmosphereProcessingState.PipeNet => ProcessPipeNets(ent),
AtmosphereProcessingState.AtmosDevices => mapEnt is not null
? ProcessAtmosDevices(ent, mapEnt.Value)
: throw new ArgumentException(
"An Entity<MapAtmosphereComponent> must be provided when benchmarking ProcessAtmosDevices."),
_ => throw new ArgumentOutOfRangeException(),
};
ent.Comp1.ProcessingPaused = !processingPaused;
return processingPaused;
}
}

View File

@@ -26,6 +26,9 @@ namespace Content.Server.Atmos.EntitySystems
public float AtmosTickRate { get; private set; } public float AtmosTickRate { get; private set; }
public float Speedup { get; private set; } public float Speedup { get; private set; }
public float HeatScale { 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> /// <summary>
/// Time between each atmos sub-update. If you are writing an atmos device, use AtmosDeviceUpdateEvent.dt /// 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.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true);
Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true); Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true);
Subs.CVar(_cfg, CCVars.ExcitedGroupsSpaceIsAllConsuming, value => ExcitedGroupsSpaceIsAllConsuming = 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);
} }
} }
} }

View File

@@ -0,0 +1,259 @@
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Damage;
using Robust.Shared.Random;
using Robust.Shared.Threading;
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
/// <summary>
/// The number of pairs of opposing directions we can have.
/// This is Atmospherics.Directions / 2, since we always compare opposing directions
/// (e.g. North vs South, East vs West, etc.).
/// Used to determine the size of the opposing groups when processing delta pressure entities.
/// </summary>
private const int DeltaPressurePairCount = Atmospherics.Directions / 2;
/// <summary>
/// The length to pre-allocate list/dicts of delta pressure entities on a <see cref="GridAtmosphereComponent"/>.
/// </summary>
public const int DeltaPressurePreAllocateLength = 1000;
/// <summary>
/// Processes a singular entity, determining the pressures it's experiencing and applying damage based on that.
/// </summary>
/// <param name="ent">The entity to process.</param>
/// <param name="gridAtmosComp">The <see cref="GridAtmosphereComponent"/> that belongs to the entity's GridUid.</param>
private void ProcessDeltaPressureEntity(Entity<DeltaPressureComponent> ent, GridAtmosphereComponent gridAtmosComp)
{
if (!_random.Prob(ent.Comp.RandomDamageChance))
return;
/*
To make our comparisons a little bit faster, we take advantage of SIMD-accelerated methods
in the NumericsHelpers class.
This involves loading our values into a span in the form of opposing pairs,
so simple vector operations like min/max/abs can be performed on them.
*/
var tiles = new TileAtmosphere?[Atmospherics.Directions];
for (var i = 0; i < Atmospherics.Directions; i++)
{
var direction = (AtmosDirection)(1 << i);
var offset = ent.Comp.CurrentPosition.Offset(direction);
tiles[i] = gridAtmosComp.Tiles.GetValueOrDefault(offset);
}
Span<float> pressures = stackalloc float[Atmospherics.Directions];
GetBulkTileAtmospherePressures(tiles, pressures);
Span<float> opposingGroupA = stackalloc float[DeltaPressurePairCount];
Span<float> opposingGroupB = stackalloc float[DeltaPressurePairCount];
Span<float> opposingGroupMax = stackalloc float[DeltaPressurePairCount];
// Directions are always in pairs: the number of directions is always even
// (we must consider the future where Multi-Z is real)
// Load values into opposing pairs.
for (var i = 0; i < DeltaPressurePairCount; i++)
{
opposingGroupA[i] = pressures[i];
opposingGroupB[i] = pressures[i + DeltaPressurePairCount];
}
// TODO ATMOS: Needs to be changed to batch operations so that more operations can actually be done in parallel.
// Need to determine max pressure in opposing directions for absolute pressure calcs.
NumericsHelpers.Max(opposingGroupA, opposingGroupB, opposingGroupMax);
// Calculate pressure differences between opposing directions.
NumericsHelpers.Sub(opposingGroupA, opposingGroupB);
NumericsHelpers.Abs(opposingGroupA);
var maxPressure = 0f;
var maxDelta = 0f;
for (var i = 0; i < DeltaPressurePairCount; i++)
{
maxPressure = MathF.Max(maxPressure, opposingGroupMax[i]);
maxDelta = MathF.Max(maxDelta, opposingGroupA[i]);
}
EnqueueDeltaPressureDamage(ent,
gridAtmosComp,
maxPressure,
maxDelta);
}
/// <summary>
/// A DeltaPressure helper method that retrieves the pressures of all gas mixtures
/// in the given array of <see cref="TileAtmosphere"/>s, and stores the results in the
/// provided <paramref name="pressures"/> span.
/// The tiles array length is limited to Atmosphereics.Directions.
/// </summary>
/// <param name="tiles">The tiles array to find the pressures of.</param>
/// <param name="pressures">The span to store the pressures to - this should be the same length
/// as the tile array.</param>
/// <remarks>This is for internal use of the DeltaPressure system -
/// it may not be a good idea to use this generically.</remarks>
private static void GetBulkTileAtmospherePressures(TileAtmosphere?[] tiles, Span<float> pressures)
{
#if DEBUG
// Just in case someone tries to use this method incorrectly.
if (tiles.Length != pressures.Length || tiles.Length != Atmospherics.Directions)
throw new ArgumentException("Length of arrays must be the same and of Atmospherics.Directions length.");
#endif
// This hardcoded direction limit is stopping goobers from
// overflowing the stack with massive arrays.
// If this method is pulled into a more generic place,
// it should be replaced with method params.
Span<float> mixtVol = stackalloc float[Atmospherics.Directions];
Span<float> mixtTemp = stackalloc float[Atmospherics.Directions];
Span<float> mixtMoles = stackalloc float[Atmospherics.Directions];
Span<float> atmosR = stackalloc float[Atmospherics.Directions];
for (var i = 0; i < tiles.Length; i++)
{
if (tiles[i] is not { Air: { } mixture })
{
pressures[i] = 0f;
// To prevent any NaN/Div/0 errors, we just bite the bullet
// and set everything to the lowest possible value.
mixtVol[i] = 1;
mixtTemp[i] = 1;
mixtMoles[i] = float.Epsilon;
atmosR[i] = 1;
continue;
}
mixtVol[i] = mixture.Volume;
mixtTemp[i] = mixture.Temperature;
mixtMoles[i] = mixture.TotalMoles;
atmosR[i] = Atmospherics.R;
}
/*
Retrieval of single tile pressures requires calling a get method for each tile,
which does a bunch of scalar operations.
So we go ahead and batch-retrieve the pressures of all tiles
and process them in bulk.
*/
NumericsHelpers.Multiply(mixtMoles, atmosR);
NumericsHelpers.Multiply(mixtMoles, mixtTemp);
NumericsHelpers.Divide(mixtMoles, mixtVol, pressures);
}
/// <summary>
/// Packs data into a <see cref="DeltaPressureDamageResult"/> data struct and enqueues it
/// into the <see cref="GridAtmosphereComponent.DeltaPressureDamageResults"/> queue for
/// later processing.
/// </summary>
/// <param name="ent">The entity to enqueue if necessary.</param>
/// <param name="gridAtmosComp">The <see cref="GridAtmosphereComponent"/>
/// containing the queue.</param>
/// <param name="pressure">The current absolute pressure being experienced by the entity.</param>
/// <param name="delta">The current delta pressure being experienced by the entity.</param>
private static void EnqueueDeltaPressureDamage(Entity<DeltaPressureComponent> ent,
GridAtmosphereComponent gridAtmosComp,
float pressure,
float delta)
{
var aboveMinPressure = pressure > ent.Comp.MinPressure;
var aboveMinDeltaPressure = delta > ent.Comp.MinPressureDelta;
if (!aboveMinPressure && !aboveMinDeltaPressure)
{
ent.Comp.IsTakingDamage = false;
return;
}
gridAtmosComp.DeltaPressureDamageResults.Enqueue(new DeltaPressureDamageResult(ent,
pressure,
delta));
}
/// <summary>
/// Job for solving DeltaPressure entities in parallel.
/// Batches are given some index to start from, so each thread can simply just start at that index
/// and process the next n entities in the list.
/// </summary>
/// <param name="system">The AtmosphereSystem instance.</param>
/// <param name="atmosphere">The GridAtmosphereComponent to work with.</param>
/// <param name="startIndex">The index in the DeltaPressureEntities list to start from.</param>
/// <param name="cvarBatchSize">The batch size to use for this job.</param>
private sealed class DeltaPressureParallelJob(
AtmosphereSystem system,
GridAtmosphereComponent atmosphere,
int startIndex,
int cvarBatchSize)
: IParallelRobustJob
{
public int BatchSize => cvarBatchSize;
public void Execute(int index)
{
// The index is relative to the startIndex (because we can pause and resume computation),
// so we need to add it to the startIndex.
var actualIndex = startIndex + index;
if (actualIndex >= atmosphere.DeltaPressureEntities.Count)
return;
var ent = atmosphere.DeltaPressureEntities[actualIndex];
system.ProcessDeltaPressureEntity(ent, atmosphere);
}
}
/// <summary>
/// Struct that holds the result of delta pressure damage processing for an entity.
/// This is only created and enqueued when the entity needs to take damage.
/// </summary>
/// <param name="Ent">The entity to deal damage to.</param>
/// <param name="Pressure">The current absolute pressure the entity is experiencing.</param>
/// <param name="DeltaPressure">The current delta pressure the entity is experiencing.</param>
public readonly record struct DeltaPressureDamageResult(
Entity<DeltaPressureComponent> Ent,
float Pressure,
float DeltaPressure);
/// <summary>
/// Does damage to an entity depending on the pressure experienced by it, based on the
/// entity's <see cref="DeltaPressureComponent"/>.
/// </summary>
/// <param name="ent">The entity to apply damage to.</param>
/// <param name="pressure">The absolute pressure being exerted on the entity.</param>
/// <param name="deltaPressure">The delta pressure being exerted on the entity.</param>
private void PerformDamage(Entity<DeltaPressureComponent> ent, float pressure, float deltaPressure)
{
var maxPressure = Math.Max(pressure - ent.Comp.MinPressure, deltaPressure - ent.Comp.MinPressureDelta);
var appliedDamage = ScaleDamage(ent, ent.Comp.BaseDamage, maxPressure);
_damage.TryChangeDamage(ent, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
ent.Comp.IsTakingDamage = true;
}
/// <summary>
/// Returns a new DamageSpecifier scaled based on values on an entity with a DeltaPressureComponent.
/// </summary>
/// <param name="ent">The entity to base the manipulations off of (pull scaling type)</param>
/// <param name="damage">The base damage specifier to scale.</param>
/// <param name="pressure">The pressure being exerted on the entity.</param>
/// <returns>A scaled DamageSpecifier.</returns>
private static DamageSpecifier ScaleDamage(Entity<DeltaPressureComponent> ent, DamageSpecifier damage, float pressure)
{
var factor = ent.Comp.ScalingType switch
{
DeltaPressureDamageScalingType.Threshold => 1f,
DeltaPressureDamageScalingType.Linear => pressure * ent.Comp.ScalingPower,
DeltaPressureDamageScalingType.Log =>
(float) Math.Log(pressure, ent.Comp.ScalingPower),
_ => throw new ArgumentOutOfRangeException(nameof(ent), "Invalid damage scaling type!"),
};
return damage * factor;
}
}

View File

@@ -467,6 +467,66 @@ namespace Content.Server.Atmos.EntitySystems
return true; 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) private bool ProcessPipeNets(GridAtmosphereComponent atmosphere)
{ {
if (!atmosphere.ProcessingPaused) if (!atmosphere.ProcessingPaused)
@@ -510,6 +570,8 @@ namespace Content.Server.Atmos.EntitySystems
num--; num--;
if (!ExcitedGroups) if (!ExcitedGroups)
num--; num--;
if (!DeltaPressureDamage)
num--;
if (!Superconduction) if (!Superconduction)
num--; num--;
return num * AtmosTime; return num * AtmosTime;
@@ -653,6 +715,18 @@ namespace Content.Server.Atmos.EntitySystems
return; 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.ProcessingPaused = false;
atmosphere.State = AtmosphereProcessingState.Hotspots; atmosphere.State = AtmosphereProcessingState.Hotspots;
continue; continue;
@@ -721,6 +795,7 @@ namespace Content.Server.Atmos.EntitySystems
ActiveTiles, ActiveTiles,
ExcitedGroups, ExcitedGroups,
HighPressureDelta, HighPressureDelta,
DeltaPressure,
Hotspots, Hotspots,
Superconductivity, Superconductivity,
PipeNet, PipeNet,

View File

@@ -1,6 +1,5 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.EntitySystems;
@@ -15,6 +14,8 @@ using Robust.Shared.Map;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using System.Linq; using System.Linq;
using Content.Shared.Damage;
using Robust.Shared.Threading;
namespace Content.Server.Atmos.EntitySystems; namespace Content.Server.Atmos.EntitySystems;
@@ -27,6 +28,7 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IAdminLogManager _adminLog = default!; [Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!; [Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = 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 TileSystem _tile = default!;
[Dependency] private readonly MapSystem _map = default!; [Dependency] private readonly MapSystem _map = default!;
[Dependency] public readonly PuddleSystem Puddle = default!; [Dependency] public readonly PuddleSystem Puddle = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
private const float ExposedUpdateDelay = 1f; private const float ExposedUpdateDelay = 1f;
private float _exposedTimer = 0f; private float _exposedTimer = 0f;

View File

@@ -0,0 +1,82 @@
using Content.Server.Atmos.Components;
using Content.Shared.Examine;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
/// <summary>
/// <para>System that handles <see cref="DeltaPressureComponent"/>.</para>
///
/// <para>Entities with a <see cref="DeltaPressureComponent"/> will take damage per atmostick
/// depending on the pressure they experience.</para>
///
/// <para>DeltaPressure logic is mostly handled in a partial class in Atmospherics.
/// This system handles the adding and removing of entities to a processing list,
/// as well as any field changes via the API.</para>
/// </summary>
public sealed class DeltaPressureSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeltaPressureComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DeltaPressureComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<DeltaPressureComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DeltaPressureComponent, MoveEvent>(OnMoveEvent);
SubscribeLocalEvent<DeltaPressureComponent, GridUidChangedEvent>(OnGridChanged);
}
private void OnMoveEvent(Entity<DeltaPressureComponent> ent, ref MoveEvent args)
{
var xform = Transform(ent);
// May move off-grid, so, might as well protect against that.
if (!TryComp<MapGridComponent>(xform.GridUid, out var mapGridComponent))
{
return;
}
ent.Comp.CurrentPosition = _map.CoordinatesToTile(xform.GridUid.Value, mapGridComponent, args.NewPosition);
}
private void OnComponentInit(Entity<DeltaPressureComponent> ent, ref ComponentInit args)
{
var xform = Transform(ent);
if (xform.GridUid == null)
return;
_atmosphereSystem.TryAddDeltaPressureEntity(xform.GridUid.Value, ent);
}
private void OnComponentShutdown(Entity<DeltaPressureComponent> ent, ref ComponentShutdown args)
{
// Wasn't part of a list, so nothing to clean up.
if (ent.Comp.GridUid == null)
return;
_atmosphereSystem.TryRemoveDeltaPressureEntity(ent.Comp.GridUid.Value, ent);
}
private void OnExamined(Entity<DeltaPressureComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.IsTakingDamage)
args.PushMarkup(Loc.GetString("window-taking-damage"));
}
private void OnGridChanged(Entity<DeltaPressureComponent> ent, ref GridUidChangedEvent args)
{
if (args.OldGrid != null)
{
_atmosphereSystem.TryRemoveDeltaPressureEntity(args.OldGrid.Value, ent);
}
if (args.NewGrid != null)
{
_atmosphereSystem.TryAddDeltaPressureEntity(args.NewGrid.Value, ent);
}
}
}

View File

@@ -150,4 +150,31 @@ public sealed partial class CCVars
/// </summary> /// </summary>
public static readonly CVarDef<float> AtmosTankFragment = public static readonly CVarDef<float> AtmosTankFragment =
CVarDef.Create("atmos.max_explosion_range", 26f, CVar.SERVERONLY); 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);
} }

View File

@@ -0,0 +1 @@
window-taking-damage = [color=orange]It's straining under pressure![/color]

View File

@@ -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
...

View File

@@ -163,6 +163,10 @@
noAirWhenFullyAirBlocked: false noAirWhenFullyAirBlocked: false
airBlockedDirection: airBlockedDirection:
- South - South
- type: DeltaPressure
minPressure: 250
minPressureDelta: 187.5
scalingType: Threshold
- type: Construction - type: Construction
graph: Windoor graph: Windoor
node: windoor node: windoor
@@ -235,6 +239,10 @@
- type: Construction - type: Construction
graph: Windoor graph: Windoor
node: windoorSecure node: windoorSecure
- type: DeltaPressure
minPressure: 3750
minPressureDelta: 2500
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 350 price: 350
- type: Tag - type: Tag
@@ -304,6 +312,10 @@
- type: Construction - type: Construction
graph: Windoor graph: Windoor
node: pwindoor node: pwindoor
- type: DeltaPressure
minPressure: 18750
minPressureDelta: 12500
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: RadiationBlocker - type: RadiationBlocker
@@ -370,6 +382,10 @@
- type: Construction - type: Construction
graph: Windoor graph: Windoor
node: pwindoorSecure node: pwindoorSecure
- type: DeltaPressure
minPressure: 37500
minPressureDelta: 25000
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: RadiationBlocker - type: RadiationBlocker
@@ -438,6 +454,10 @@
max: 2 max: 2
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 18750
minPressureDelta: 12500
scalingType: Threshold
- type: Construction - type: Construction
graph: Windoor graph: Windoor
node: uwindoor node: uwindoor
@@ -504,6 +524,10 @@
max: 2 max: 2
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 37500
minPressureDelta: 25000
scalingType: Threshold
- type: Construction - type: Construction
graph: Windoor graph: Windoor
node: uwindoorSecure node: uwindoorSecure

View File

@@ -48,6 +48,11 @@
trackAllDamage: true trackAllDamage: true
damageOverlay: damageOverlay:
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: DeltaPressure
minPressure: 75000
minPressureDelta: 50000
scalingType: Linear
scalingPower: 0.0005
- type: StaticPrice - type: StaticPrice
price: 100 price: 100
- type: RadiationBlocker - type: RadiationBlocker
@@ -104,6 +109,10 @@
max: 1 max: 1
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 18750
minPressureDelta: 12500
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 50 price: 50
- type: RadiationBlocker - type: RadiationBlocker

View File

@@ -55,6 +55,10 @@
trackAllDamage: true trackAllDamage: true
damageOverlay: damageOverlay:
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: DeltaPressure
minPressure: 15000
minPressureDelta: 10000
scalingType: Threshold
- type: entity - type: entity
id: WindowReinforcedDirectional id: WindowReinforcedDirectional
@@ -113,6 +117,9 @@
max: 1 max: 1
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 3750
minPressureDelta: 2500
- type: StaticPrice - type: StaticPrice
price: 22.5 price: 22.5

View File

@@ -53,6 +53,11 @@
trackAllDamage: true trackAllDamage: true
damageOverlay: damageOverlay:
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: DeltaPressure
minPressure: 150000
minPressureDelta: 100000
scalingType: Linear
scalingPower: 0.0001
- type: StaticPrice - type: StaticPrice
price: 132 price: 132
@@ -111,6 +116,10 @@
max: 1 max: 1
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 37500
minPressureDelta: 25000
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 66 price: 66

View File

@@ -48,6 +48,11 @@
trackAllDamage: true trackAllDamage: true
damageOverlay: damageOverlay:
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: DeltaPressure
minPressure: 150000
minPressureDelta: 100000
scalingType: Linear
scalingPower: 0.0001
- type: StaticPrice - type: StaticPrice
price: 215 price: 215
- type: RadiationBlocker - type: RadiationBlocker
@@ -106,6 +111,10 @@
max: 2 max: 2
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 37500
minPressureDelta: 25000
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 110 price: 110
- type: RadiationBlocker - type: RadiationBlocker

View File

@@ -51,6 +51,11 @@
trackAllDamage: true trackAllDamage: true
damageOverlay: damageOverlay:
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: DeltaPressure
minPressure: 15000
minPressureDelta: 10000
scalingType: Linear
scalingPower: 0.0005
- type: StaticPrice - type: StaticPrice
price: 150 price: 150

View File

@@ -46,6 +46,11 @@
trackAllDamage: true trackAllDamage: true
damageOverlay: damageOverlay:
sprite: Structures/Windows/cracks.rsi sprite: Structures/Windows/cracks.rsi
- type: DeltaPressure
minPressure: 75000
minPressureDelta: 50000
scalingType: Linear
scalingPower: 0.0005
- type: StaticPrice - type: StaticPrice
price: 200 price: 200
- type: RadiationBlocker - type: RadiationBlocker
@@ -99,6 +104,10 @@
max: 1 max: 1
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: DeltaPressure
minPressure: 18750
minPressureDelta: 12500
scalingType: Threshold
- type: StaticPrice - type: StaticPrice
price: 100 price: 100
- type: RadiationBlocker - type: RadiationBlocker

View File

@@ -75,6 +75,11 @@
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: Airtight - type: Airtight
- type: DeltaPressure
minPressure: 1000
minPressureDelta: 750
scalingType: Linear
scalingPower: 0.0005
- type: IconSmooth - type: IconSmooth
key: windows key: windows
base: window base: window
@@ -206,6 +211,10 @@
noAirWhenFullyAirBlocked: false noAirWhenFullyAirBlocked: false
airBlockedDirection: airBlockedDirection:
- South - South
- type: DeltaPressure
minPressure: 250
minPressureDelta: 187.5
scalingType: Threshold
- type: Construction - type: Construction
graph: WindowDirectional graph: WindowDirectional
node: windowDirectional node: windowDirectional