-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Atmos Delta-Pressure Window Shattering #39238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 27 commits
2832218
a7203de
e72f6f2
4f7afe9
62ae561
83fc462
05720dd
4c27c53
f51cf22
743325f
71f4e3e
1859583
810f36a
a995848
0587ef8
fc643eb
e7f652f
e89c90e
6015999
bdeb5e2
4b4a167
5d70985
4ed6c94
1119fc3
cb1e886
71c4fd5
826d7d4
5d8a33e
a56ff49
a76ff69
574bfa5
89b17ce
56823f3
cbd9069
40570cf
7c150fe
08d5b85
bda1722
130096e
f51323a
b44953b
1b374b7
b898ada
c709356
6d35510
80bbe23
5fa7af1
4026a31
c04bc7a
76524f0
3229a24
3cc527b
bd4f067
3a7548e
e03820c
32d23af
f5e04cb
2cf5034
339ad47
cc9da16
25da8d3
d79a793
59a79c0
b6ec066
386a2de
8d43656
2261975
160ba07
8dc4c94
74b066c
0e14ad1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| { | ||
| /// <summary> | ||
| /// Whether the entity is allowed to take pressure damage or not. | ||
|
||
| /// </summary> | ||
| [DataField(readOnly: true)] | ||
| public bool Enabled; | ||
|
||
|
|
||
| /// <summary> | ||
| /// Whether this entity is currently taking damage. | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /// </summary> | ||
| [DataField(readOnly: true)] | ||
| public bool IsTakingDamage; | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// <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> | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| [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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// <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. | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// </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; | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /// <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; | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// <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. | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// </summary> | ||
| [Serializable] | ||
| public enum DeltaPressureDamageScalingType : byte | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| /// <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, | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /// <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, | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,6 +61,12 @@ public sealed partial class GridAtmosphereComponent : Component | |
| [ViewVariables] | ||
| public int HighPressureDeltaCount => HighPressureDelta.Count; | ||
|
|
||
| [ViewVariables] | ||
| public readonly HashSet<Entity<DeltaPressureComponent>> DeltaPressureEntities = new(); | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| [ViewVariables] | ||
| public readonly Dictionary<Vector2i, float> DeltaPressureCoords = new(1000); | ||
|
||
|
|
||
| [ViewVariables] | ||
| public readonly HashSet<IPipeNet> PipeNets = new(); | ||
|
|
||
|
|
@@ -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(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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); | ||
|
||
|
|
||
| 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); | ||
|
|
||
|
|
||
| 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; | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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) | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| 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)) | ||
|
||
| 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, | ||
ArtisticRoomba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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, ... | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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)) | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| 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) | ||
ArtisticRoomba marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| // Cache the pressure value for this tile index. | ||
| nf = tileA.Air.Pressure; | ||
| } | ||
|
|
||
| gridAtmosComp.DeltaPressureCoords[indices] = nf; | ||
| return nf; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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 withAirtightComponent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still unresolved.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
DeltaPressureComponentto exist without a matchingAirtightComponent, then they should be one component. It's all about making illegal states irrepresentable. But I won't make this a blocking issue.