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