Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
2832218
Initial system groundwork
ArtisticRoomba Jul 24, 2025
a7203de
Basic AtmosphereSystem and DeltaPressureSystem API
ArtisticRoomba Jul 24, 2025
e72f6f2
Initial MVP
ArtisticRoomba Jul 25, 2025
4f7afe9
First round of optimizations
ArtisticRoomba Jul 26, 2025
62ae561
Second round of optimizations
ArtisticRoomba Jul 26, 2025
83fc462
Third round of optimizations
ArtisticRoomba Jul 27, 2025
05720dd
Microfix
ArtisticRoomba Jul 27, 2025
4c27c53
Misc cleanup
ArtisticRoomba Jul 27, 2025
f51cf22
lol
ArtisticRoomba Jul 28, 2025
743325f
Cache recently checked tiles
ArtisticRoomba Jul 28, 2025
71f4e3e
Fix tile caching invalidation
ArtisticRoomba Jul 28, 2025
1859583
Implement basic multithreading
ArtisticRoomba Jul 29, 2025
810f36a
Revert "Implement basic multithreading"
ArtisticRoomba Jul 29, 2025
a995848
Cleanup, CVAR toggle
ArtisticRoomba Jul 29, 2025
0587ef8
More cleanup and damage logic refac
ArtisticRoomba Jul 29, 2025
fc643eb
Move methods in AtmosphereSystem.DeltaPressure.cs over to DeltaPressu…
ArtisticRoomba Jul 30, 2025
e7f652f
Expand DeltaPressureSystem.cs API
ArtisticRoomba Jul 31, 2025
e89c90e
Add locale
ArtisticRoomba Jul 31, 2025
6015999
Adjust default components
ArtisticRoomba Aug 1, 2025
bdeb5e2
Add damage values to windows
ArtisticRoomba Aug 1, 2025
4b4a167
Add damage values to windoors
ArtisticRoomba Aug 1, 2025
5d70985
Add damage values to uranium windows
ArtisticRoomba Aug 1, 2025
4ed6c94
Add damage values to shuttle windows
ArtisticRoomba Aug 1, 2025
1119fc3
Actually enable small damage numbers to be applied
ArtisticRoomba Aug 1, 2025
cb1e886
Make most API methods static
ArtisticRoomba Aug 1, 2025
71c4fd5
Add WIP option for window damage randomness
ArtisticRoomba Aug 2, 2025
826d7d4
Do randomness properly this time
ArtisticRoomba Aug 2, 2025
5d8a33e
Address review
ArtisticRoomba Aug 11, 2025
a56ff49
base
ArtisticRoomba Aug 12, 2025
a76ff69
initial commit
ArtisticRoomba Aug 13, 2025
574bfa5
misc
ArtisticRoomba Aug 13, 2025
89b17ce
misc
ArtisticRoomba Aug 14, 2025
56823f3
fix params
ArtisticRoomba Aug 14, 2025
cbd9069
Revert "base"
ArtisticRoomba Aug 14, 2025
40570cf
Parallel solve initial commit
ArtisticRoomba Aug 14, 2025
7c150fe
Merge branch 'atmos/delta-pressure-benchmark' into atmos/delta-pressu…
ArtisticRoomba Aug 14, 2025
08d5b85
misc optimizations
ArtisticRoomba Aug 14, 2025
bda1722
tune default vars
ArtisticRoomba Aug 15, 2025
130096e
misc cleanup
ArtisticRoomba Aug 15, 2025
f51323a
get rid of DeltaPressureBenchmark.cs
ArtisticRoomba Aug 15, 2025
b44953b
Cleanup using directives
ArtisticRoomba Aug 15, 2025
1b374b7
Revert "get rid of DeltaPressureBenchmark.cs"
ArtisticRoomba Aug 15, 2025
b898ada
More parallel solve CVARs
ArtisticRoomba Aug 15, 2025
c709356
Fix mistake
ArtisticRoomba Aug 15, 2025
6d35510
Merge branch 'atmos/delta-pressure' into atmos/delta-pressure-benchmark
ArtisticRoomba Aug 15, 2025
80bbe23
Modify params
ArtisticRoomba Aug 16, 2025
5fa7af1
Modify API
ArtisticRoomba Aug 17, 2025
4026a31
Fix entity leak
ArtisticRoomba Aug 17, 2025
c04bc7a
Fix API for grid ref
ArtisticRoomba Aug 17, 2025
76524f0
Two tests
ArtisticRoomba Aug 17, 2025
3229a24
Merge branch 'atmos/delta-pressure' into atmos/delta-pressure-tests
ArtisticRoomba Aug 17, 2025
3cc527b
directional pressure tests
ArtisticRoomba Aug 19, 2025
bd4f067
shitcoded static pressure tests
ArtisticRoomba Aug 19, 2025
3a7548e
cleanup
ArtisticRoomba Aug 19, 2025
e03820c
SIMD accel bulk pressure retrieval plus cleanup
ArtisticRoomba Aug 20, 2025
32d23af
Assert on dict/list check for sync
ArtisticRoomba Aug 20, 2025
f5e04cb
Fix API and add TODO
ArtisticRoomba Aug 21, 2025
2cf5034
Docs
ArtisticRoomba Aug 22, 2025
339ad47
fix out of bounds issues
ArtisticRoomba Aug 22, 2025
cc9da16
improve comments
ArtisticRoomba Aug 23, 2025
25da8d3
fix NaN in bulk processing
ArtisticRoomba Aug 26, 2025
d79a793
csgrad
ArtisticRoomba Aug 26, 2025
59a79c0
Merge branch 'atmos/delta-pressure-tests' into atmos/delta-pressure
ArtisticRoomba Sep 1, 2025
b6ec066
Merge branch 'atmos/delta-pressure-benchmark' into atmos/delta-pressure
ArtisticRoomba Sep 1, 2025
386a2de
Major benchmarking changes to reduce measurement overhead, microoptim…
ArtisticRoomba Sep 2, 2025
8d43656
Cache current ent pos to prevent relatively expensive indices lookup
ArtisticRoomba Sep 2, 2025
2261975
Address review
ArtisticRoomba Sep 2, 2025
160ba07
Misc fixes
ArtisticRoomba Sep 2, 2025
8dc4c94
Comment fixe
ArtisticRoomba Sep 2, 2025
74b066c
cleanup using directives
ArtisticRoomba Sep 2, 2025
0e14ad1
Adjust default config values
ArtisticRoomba Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions Content.Server/Atmos/Components/DeltaPressureComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 if <see cref="AutoJoin"/> is set to true.
/// </summary>
/// <remarks><para>Systems wanting to change these values should go through the <see cref="DeltaPressureSystem"/> API.</para>
/// <para>Note that the entity should have an <see cref="AirtightComponent"/> and be a grid structure.</para></remarks>
[RegisterComponent]
[Access(typeof(DeltaPressureSystem), typeof(AtmosphereSystem))]
public sealed partial class DeltaPressureComponent : Component
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the above, it seems to only make sense to define this component for entities with AirtightComponent. As such, maybe we should consider merging this with AirtightComponent.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still unresolved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it is true that DeltaPressureComponent entities basically require AirtightComponent, I feel that it's good to define this behavior as a seperate component that can be simply added or removed if you want the behavior to continue or not, with that being seperate from AirtightComponent. That way, whatever system could play with DeltaPressure entities just like any other marker component (though we usually use tags for this type of thing).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think that if it doesn't make sense for a DeltaPressureComponent to exist without a matching AirtightComponent, then they should be one component. It's all about making illegal states irrepresentable. But I won't make this a blocking issue.

{
/// <summary>
/// Whether the entity is allowed to take pressure damage or not.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the existence of this component determine whether this entity is allowed to take pressure damage or not?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this is incorrect documentation. This describes if the entity is currently included in the processing list. Could have sworn I changed bit, but alas.

/// </summary>
[DataField(readOnly: true)]
public bool Enabled;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like Enabled isn't really being used? It's only ever written to, and it can also be derived by checking if it's in the parent grid's GridAtmosComp.DeltaPressureEntities.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an easy way for any system to check if an entity is currently being included for processing (as well as debugging so I know if it is actually being processed).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case the doc comment should reflect that (setting enabled: false in a prototype won't actually do anything if it's set to autojoin The List™), though I'm still not sure I can think of a use case for it. Currently, everything's going to be marked for processing if it has the component and is on a grid, no?


/// <summary>
/// Whether this entity is currently taking damage.
/// </summary>
[DataField(readOnly: true)]
public bool IsTakingDamage;

/// <summary>
/// Whether the entity should automatically join the processing list on the grid's <see cref="GridAtmosphereComponent"/>
/// for delta pressure processing.
/// If this is set to false, the entity will not be automatically added to the list.
/// </summary>
[DataField]
public bool AutoJoin = true;

/// <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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the default should be reasonable, and not be 1. Otherwise, everything breaks at the same time.

Uploading out-2025-08-31_12.47.20.mp4…


/// <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>
/// If the entity is fulfilling both minimum pressure requirements, then this entity will stack damage.
/// </summary>
[DataField]
public bool StackDamage;

/// <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.
/// </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 MaxPressure = 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,

/// <summary>
/// Damage dealt will be an exponential function.
/// Scaling power determines the power of the exponent.
/// </summary>
Exponential,
}
9 changes: 9 additions & 0 deletions Content.Server/Atmos/Components/GridAtmosphereComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public sealed partial class GridAtmosphereComponent : Component
[ViewVariables]
public int HighPressureDeltaCount => HighPressureDelta.Count;

[ViewVariables]
public readonly HashSet<Entity<DeltaPressureComponent>> DeltaPressureEntities = new();

[ViewVariables]
public readonly Dictionary<Vector2i, float> DeltaPressureCoords = new(1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use a better field name. Also it'd be nice if you put docs on all the fields you added since they are public.


[ViewVariables]
public readonly HashSet<IPipeNet> PipeNets = new();

Expand All @@ -73,6 +79,9 @@ public sealed partial class GridAtmosphereComponent : Component
[ViewVariables]
public readonly Queue<ExcitedGroup> CurrentRunExcitedGroups = new();

[ViewVariables]
public readonly Queue<Entity<DeltaPressureComponent>> CurrentRunDeltaPressureEntities = new();

[ViewVariables]
public readonly Queue<IPipeNet> CurrentRunPipeNet = new();

Expand Down
56 changes: 56 additions & 0 deletions Content.Server/Atmos/EntitySystems/AtmosphereSystem.API.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;

Expand Down Expand Up @@ -319,6 +320,61 @@ public bool RemoveAtmosDevice(Entity<GridAtmosphereComponent?> grid, Entity<Atmo
return true;
}

/// <summary>
/// Adds an entity with a DeltaPressureComponent to the DeltaPressure processing list.
/// </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 :)
DebugTools.Assert(Transform(ent).GridUid == grid);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this really be a debug assert? Or is it so you can avoid the transform resolve out of DEBUG? If it's the latter the doc should mention that it's the callers job to check that the passed in grid is the grid of the ent.

If you do end up keeping the DeltaPressureComponents inside of DeltaPressureEntities, it might also be worth including the Transform of each one, since you resolve that again in ProcessDeltaPressureEntity.


if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
return false;

if (!grid.Comp.DeltaPressureEntities.Add(ent))
return false;

ent.Comp.Enabled = 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.DeltaPressureEntities.Remove(ent))
return false;

ent.Comp.Enabled = false;
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)
{
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.DeltaPressureEntities.Contains(ent);
}

[ByRefEvent] private record struct SetSimulatedGridMethodEvent
(EntityUid Grid, bool Simulated, bool Handled = false);

Expand Down
2 changes: 2 additions & 0 deletions Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public sealed partial class AtmosphereSystem
public float AtmosTickRate { get; private set; }
public float Speedup { get; private set; }
public float HeatScale { get; private set; }
public bool DeltaPressureDamage { get; private set; }

/// <summary>
/// Time between each atmos sub-update. If you are writing an atmos device, use AtmosDeviceUpdateEvent.dt
Expand Down Expand Up @@ -55,6 +56,7 @@ private void InitializeCVars()
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);
}
}
}
113 changes: 113 additions & 0 deletions Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Runtime.InteropServices;
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Damage;

namespace Content.Server.Atmos.EntitySystems;

public sealed partial class AtmosphereSystem
{
/// <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 (ent.Comp.RandomDamageChance is not 1f &&
Random.Shared.NextSingle() >= ent.Comp.RandomDamageChance)
{
return;
}

// Retrieve the current tile coords of this ent, use cached lookup.
// This ent could also just not exist anymore when we finally got around to processing it
// (as atmos spans processing across multiple ticks), so this is a good check for that.
if (!TryComp(ent, out TransformComponent? xform))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the best way to check for this? Why not the Deleted accessor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We (or rather GetGridOrMapTilePosition) need to retrieve the TransformComponent anyways. This logic can actually be simplified to forego this, as the DeltaPressure system should ensure that no deleted entities persist in the list. It would also log an error, which is good. So I will make those changes.

return;

var indices = _transformSystem.GetGridOrMapTilePosition(ent, xform);

/*
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.
*/

// Directions are always in pairs: the number of directions is always even
// (we must consider the future where Multi-Z is real)
const int pairCount = Atmospherics.Directions / 2;

Span<float> opposingGroupA = stackalloc float[pairCount]; // Will hold North, East, ...
Span<float> opposingGroupB = stackalloc float[pairCount]; // Will hold South, West, ...
Span<float> opposingGroupMax = stackalloc float[pairCount];

// First, we null check data and prep it for comparison
for (var i = 0; i < pairCount; i++)
{
// First direction in the pair (North, East, ...)
var dirA = (AtmosDirection)(1 << i);

// Second direction in the pair (South, West, ...)
var dirB = (AtmosDirection)(1 << (i + pairCount));

opposingGroupA[i] = GetTilePressure(gridAtmosComp, indices.Offset(dirA));
opposingGroupB[i] = GetTilePressure(gridAtmosComp, indices.Offset(dirB));
}

// Need to determine max pressure in opposing directions.
NumericsHelpers.Max(opposingGroupA, opposingGroupB, opposingGroupMax);

// Calculate pressure differences between opposing directions.
NumericsHelpers.Sub(opposingGroupA, opposingGroupB);
NumericsHelpers.Abs(opposingGroupA);

var maxPressure = 0f;
for (var i = 0; i < pairCount; i++)
{
maxPressure = Math.Max(maxPressure, opposingGroupMax[i]);
}

// Find maximum pressure difference
var maxDelta = 0f;
for (var i = 0; i < pairCount; i++)
{
maxDelta = Math.Max(maxDelta, opposingGroupA[i]);
}

_deltaPressure.PerformDamage(ent, maxPressure, maxDelta);
}

/// <summary>
/// Retrieves a cached lookup of the pressure at a specific tile index on a grid.
/// If not found, caches the pressure value for that tile index.
/// </summary>
/// <param name="gridAtmosComp">The grid to check.</param>
/// <param name="indices">The indices to check.</param>
private static float GetTilePressure(GridAtmosphereComponent gridAtmosComp, Vector2i indices)
{
// First try and retrieve the tile atmosphere for the given indices from our cache.
// Use a safe lookup method because we're going to be writing to the dictionary.
if (gridAtmosComp.DeltaPressureCoords.TryGetValue(indices, out var cf))
{
return cf;
}

// Didn't hit the cache.
// Since we're not writing to this dict, we can use an unsafe lookup method.
// Supposed to be a bit faster, though we need to check for null refs.
ref var tileA = ref CollectionsMarshal.GetValueRefOrNullRef(gridAtmosComp.Tiles, indices);
var nf = 0f;

if (!System.Runtime.CompilerServices.Unsafe.IsNullRef(ref tileA) && tileA.Air != null)
{
// Cache the pressure value for this tile index.
nf = tileA.Air.Pressure;
}

gridAtmosComp.DeltaPressureCoords[indices] = nf;
return nf;
}
}
Loading
Loading