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;
}
}