Files
tbd-station-14/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
thetuerk bb3fa43f1f Predict LungSystem (#40729)
* Initial edits of files
Untested yet. I would like to make sure all is accounted for before moving the files.

* trying my best

* Revert "trying my best"

This reverts commit 9aeece466df0169adec97e3947b061b54fd9b388.

* Revert "Initial edits of files"

This reverts commit 45c6e2343844b5fcafadbf2e5115fb2f241086a1.

* an actual meal

* Added networking to LungComponent.cs

* removed duplicate using

* moving GasRagents to SharedAtmosphereSystem.cs
2025-10-09 10:26:21 +00:00

487 lines
21 KiB
C#

using System.Linq;
using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
using Robust.Shared.Prototypes;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Server.Atmos.EntitySystems
{
public sealed partial class AtmosphereSystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
private GasReactionPrototype[] _gasReactions = Array.Empty<GasReactionPrototype>();
private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases];
/// <summary>
/// List of gas reactions ordered by priority.
/// </summary>
public IEnumerable<GasReactionPrototype> GasReactions => _gasReactions;
/// <summary>
/// Cached array of gas specific heats.
/// </summary>
public float[] GasSpecificHeats => _gasSpecificHeats;
private void InitializeGases()
{
_gasReactions = _protoMan.EnumeratePrototypes<GasReactionPrototype>().ToArray();
Array.Sort(_gasReactions, (a, b) => b.Priority.CompareTo(a.Priority));
Array.Resize(ref _gasSpecificHeats, MathHelper.NextMultipleOf(Atmospherics.TotalNumberOfGases, 4));
for (var i = 0; i < GasPrototypes.Length; i++)
{
_gasSpecificHeats[i] = GasPrototypes[i].SpecificHeat / HeatScale;
}
}
/// <summary>
/// Calculates the heat capacity for a gas mixture.
/// </summary>
/// <param name="mixture">The mixture whose heat capacity should be calculated</param>
/// <param name="applyScaling"> Whether the internal heat capacity scaling should be applied. This should not be
/// used outside of atmospheric related heat transfer.</param>
/// <returns></returns>
public float GetHeatCapacity(GasMixture mixture, bool applyScaling)
{
var scale = GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable);
// By default GetHeatCapacityCalculation() has the heat-scale divisor pre-applied.
// So if we want the un-scaled heat capacity, we have to multiply by the scale.
return applyScaling ? scale : scale * HeatScale;
}
private float GetHeatCapacity(GasMixture mixture)
=> GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private float GetHeatCapacityCalculation(float[] moles, bool space)
{
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
{
return Atmospherics.SpaceHeatCapacity;
}
Span<float> tmp = stackalloc float[moles.Length];
NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp);
// Adjust heat capacity by speedup, because this is primarily what
// determines how quickly gases heat up/cool.
return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
}
/// <summary>
/// Return speedup factor for pumped or flow-based devices that depend on MaxTransferRate.
/// </summary>
public float PumpSpeedup()
{
return Speedup;
}
/// <summary>
/// Calculates the thermal energy for a gas mixture.
/// </summary>
public float GetThermalEnergy(GasMixture mixture)
{
return mixture.Temperature * GetHeatCapacity(mixture);
}
/// <summary>
/// Calculates the thermal energy for a gas mixture, using a cached heat capacity value.
/// </summary>
public float GetThermalEnergy(GasMixture mixture, float cachedHeatCapacity)
{
return mixture.Temperature * cachedHeatCapacity;
}
/// <summary>
/// Add 'dQ' Joules of energy into 'mixture'.
/// </summary>
public void AddHeat(GasMixture mixture, float dQ)
{
var c = GetHeatCapacity(mixture);
float dT = dQ / c;
mixture.Temperature += dT;
}
/// <summary>
/// Merges the <see cref="giver"/> gas mixture into the <see cref="receiver"/> gas mixture.
/// The <see cref="giver"/> gas mixture is not modified by this method.
/// </summary>
public void Merge(GasMixture receiver, GasMixture giver)
{
if (receiver.Immutable) return;
if (MathF.Abs(receiver.Temperature - giver.Temperature) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
var receiverHeatCapacity = GetHeatCapacity(receiver);
var giverHeatCapacity = GetHeatCapacity(giver);
var combinedHeatCapacity = receiverHeatCapacity + giverHeatCapacity;
if (combinedHeatCapacity > Atmospherics.MinimumHeatCapacity)
{
receiver.Temperature = (GetThermalEnergy(giver, giverHeatCapacity) + GetThermalEnergy(receiver, receiverHeatCapacity)) / combinedHeatCapacity;
}
}
NumericsHelpers.Add(receiver.Moles, giver.Moles);
}
/// <summary>
/// Divides a source gas mixture into several recipient mixtures, scaled by their relative volumes. Does not
/// modify the source gas mixture. Used for pipe network splitting. Note that the total destination volume
/// may be larger or smaller than the source mixture.
/// </summary>
public void DivideInto(GasMixture source, List<GasMixture> receivers)
{
var totalVolume = 0f;
foreach (var receiver in receivers)
{
if (!receiver.Immutable)
totalVolume += receiver.Volume;
}
float? sourceHeatCapacity = null;
var buffer = new float[Atmospherics.AdjustedNumberOfGases];
foreach (var receiver in receivers)
{
if (receiver.Immutable)
continue;
var fraction = receiver.Volume / totalVolume;
// Set temperature, if necessary.
if (MathF.Abs(receiver.Temperature - source.Temperature) > Atmospherics.MinimumTemperatureDeltaToConsider)
{
// Often this divides a pipe net into new and completely empty pipe nets
if (receiver.TotalMoles == 0)
receiver.Temperature = source.Temperature;
else
{
sourceHeatCapacity ??= GetHeatCapacity(source);
var receiverHeatCapacity = GetHeatCapacity(receiver);
var combinedHeatCapacity = receiverHeatCapacity + sourceHeatCapacity.Value * fraction;
if (combinedHeatCapacity > Atmospherics.MinimumHeatCapacity)
receiver.Temperature = (GetThermalEnergy(source, sourceHeatCapacity.Value * fraction) + GetThermalEnergy(receiver, receiverHeatCapacity)) / combinedHeatCapacity;
}
}
// transfer moles
NumericsHelpers.Multiply(source.Moles, fraction, buffer);
NumericsHelpers.Add(receiver.Moles, buffer);
}
}
/// <summary>
/// Releases gas from this mixture to the output mixture.
/// If the output mixture is null, then this is being released into space.
/// It can't transfer air to a mixture with higher pressure.
/// </summary>
public bool ReleaseGasTo(GasMixture mixture, GasMixture? output, float targetPressure)
{
var outputStartingPressure = output?.Pressure ?? 0;
var inputStartingPressure = mixture.Pressure;
if (outputStartingPressure >= MathF.Min(targetPressure, inputStartingPressure - 10))
// No need to pump gas if the target is already reached or input pressure is too low.
// Need at least 10 kPa difference to overcome friction in the mechanism.
return false;
if (!(mixture.TotalMoles > 0) || !(mixture.Temperature > 0)) return false;
// We calculate the necessary moles to transfer with the ideal gas law.
var pressureDelta = MathF.Min(targetPressure - outputStartingPressure, (inputStartingPressure - outputStartingPressure) / 2f);
var transferMoles = pressureDelta * (output?.Volume ?? Atmospherics.CellVolume) / (mixture.Temperature * Atmospherics.R);
// And now we transfer the gas.
var removed = mixture.Remove(transferMoles);
if(output != null)
Merge(output, removed);
return true;
}
/// <summary>
/// Pump gas from this mixture to the output mixture.
/// Amount depends on target pressure.
/// </summary>
/// <param name="mixture">The mixture to pump the gas from</param>
/// <param name="output">The mixture to pump the gas to</param>
/// <param name="targetPressure">The target pressure to reach</param>
/// <returns>Whether we could pump air to the output or not</returns>
public bool PumpGasTo(GasMixture mixture, GasMixture output, float targetPressure)
{
var outputStartingPressure = output.Pressure;
var pressureDelta = targetPressure - outputStartingPressure;
if (pressureDelta < 0.01)
// No need to pump gas, we've reached the target.
return false;
if (!(mixture.TotalMoles > 0) || !(mixture.Temperature > 0)) return false;
// We calculate the necessary moles to transfer with the ideal gas law.
var transferMoles = pressureDelta * output.Volume / (mixture.Temperature * Atmospherics.R);
// And now we transfer the gas.
var removed = mixture.Remove(transferMoles);
Merge(output, removed);
return true;
}
/// <summary>
/// Scrubs specified gases from a gas mixture into a <see cref="destination"/> gas mixture.
/// </summary>
public void ScrubInto(GasMixture mixture, GasMixture destination, IReadOnlyCollection<Gas> filterGases)
{
var buffer = new GasMixture(mixture.Volume){Temperature = mixture.Temperature};
foreach (var gas in filterGases)
{
buffer.AdjustMoles(gas, mixture.GetMoles(gas));
mixture.SetMoles(gas, 0f);
}
Merge(destination, buffer);
}
/// <summary>
/// Calculates the dimensionless fraction of gas required to equalize pressure between two gas mixtures.
/// </summary>
/// <param name="gasMixture1">The first gas mixture involved in the pressure equalization.
/// This mixture should be the one you always expect to be the highest pressure.</param>
/// <param name="gasMixture2">The second gas mixture involved in the pressure equalization.</param>
/// <returns>A float (from 0 to 1) representing the dimensionless fraction of gas that needs to be transferred from the
/// mixture of higher pressure to the mixture of lower pressure.</returns>
/// <remarks>
/// <para>
/// This properly takes into account the effect
/// of gas merging from inlet to outlet affecting the temperature
/// (and possibly increasing the pressure) in the outlet.
/// </para>
/// <para>
/// The gas is assumed to expand freely,
/// so the temperature of the gas with the greater pressure is not changing.
/// </para>
/// </remarks>
/// <example>
/// If you want to calculate the moles required to equalize pressure between an inlet and an outlet,
/// multiply the fraction returned by the source moles.
/// </example>
public float FractionToEqualizePressure(GasMixture gasMixture1, GasMixture gasMixture2)
{
/*
Problem: the gas being merged from the inlet to the outlet could affect the
temp. of the gas and cause a pressure rise.
We want the pressure to be equalized, so we have to account for this.
For clarity, let's assume that gasMixture1 is the inlet and gasMixture2 is the outlet.
We require mechanical equilibrium, so \( P_1' = P_2' \)
Before the transfer, we have:
\( P_1 = \frac{n_1 R T_1}{V_1} \)
\( P_2 = \frac{n_2 R T_2}{V_2} \)
After removing fraction \( x \) moles from the inlet, we have:
\( P_1' = \frac{(1 - x) n_1 R T_1}{V_1} \)
The outlet will gain the same \( x n_1 \) moles of gas.
So \( n_2' = n_2 + x n_1 \)
After mixing, the outlet temperature will be changed.
Denote the new mixture temperature as \( T_2' \).
Volume is constant.
So we have:
\( P_2' = \frac{(n_2 + x n_1) R T_2}{V_2} \)
The total energy of the incoming inlet to outlet gas at \( T_1 \) plus the existing energy of the outlet gas at \( T_2 \)
will be equal to the energy of the new outlet gas at \( T_2' \).
This leads to the following derivation:
\( x n_1 C_1 T_1 + n_2 C_2 T_2 = (x n_1 C_1 + n_2 C_2) T_2' \)
Where \( C_1 \) and \( C_2 \) are the heat capacities of the inlet and outlet gases, respectively.
Solving for \( T_2' \) gives us:
\( T_2' = \frac{x n_1 C_1 T_1 + n_2 C_2 T_2}{x n_1 C_1 + n_2 C_2} \)
Once again, we require mechanical equilibrium (\( P_1' = P_2' \)),
so we can substitute \( T_2' \) into the pressure equation:
\( \frac{(1 - x) n_1 R T_1}{V_1} =
\frac{(n_2 + x n_1) R}{V_2} \cdot
\frac{x n_1 C_1 T_1 + n_2 C_2 T_2}
{x n_1 C_1 + n_2 C_2} \)
Now it's a matter of solving for \( x \).
Not going to show the full derivation here, just steps.
1. Cancel common factor \( R \).
2. Multiply both sides by \( x n_1 C_1 + n_2 C_2 \), so that everything
becomes a polynomial in terms of \( x \).
3. Expand both sides.
4. Collect like powers of \( x \).
5. After collecting, you should end up with a polynomial of the form:
\( (-n_1 C_1 T_1 (1 + \frac{V_2}{V_1})) x^2 +
(n_1 T_1 \frac{V_2}{V_1} (C_1 - C_2) - n_2 C_1 T_1 - n_1 C_2 T_2) x +
(n_1 T_1 \frac{V_2}{V_1} C_2 - n_2 C_2 T_2) = 0 \)
Divide through by \( n_1 C_1 T_1 \) and replace each ratio with a symbol for clarity:
\( k_V = \frac{V_2}{V_1} \)
\( k_n = \frac{n_2}{n_1} \)
\( k_T = \frac{T_2}{T_1} \)
\( k_C = \frac{C_2}{C_1} \)
*/
// Ensure that P_1 > P_2 so the quadratic works out.
if (gasMixture1.Pressure < gasMixture2.Pressure)
{
(gasMixture1, gasMixture2) = (gasMixture2, gasMixture1);
}
// Establish the dimensionless ratios.
var volumeRatio = gasMixture2.Volume / gasMixture1.Volume;
var molesRatio = gasMixture2.TotalMoles / gasMixture1.TotalMoles;
var temperatureRatio = gasMixture2.Temperature / gasMixture1.Temperature;
var heatCapacityRatio = GetHeatCapacity(gasMixture2) / GetHeatCapacity(gasMixture1);
// The quadratic equation is solved for the transfer fraction.
var quadraticA = 1 + volumeRatio;
var quadraticB = molesRatio - volumeRatio + heatCapacityRatio * (temperatureRatio + volumeRatio);
var quadraticC = heatCapacityRatio * (molesRatio * temperatureRatio - volumeRatio);
return (-quadraticB + MathF.Sqrt(quadraticB * quadraticB - 4 * quadraticA * quadraticC)) / (2 * quadraticA);
}
/// <summary>
/// Determines the number of moles that need to be removed from a <see cref="GasMixture"/> to reach a target pressure threshold.
/// </summary>
/// <param name="gasMixture">The gas mixture whose moles and properties will be used in the calculation.</param>
/// <param name="targetPressure">The target pressure threshold to calculate against.</param>
/// <returns>The difference in moles required to reach the target pressure threshold.</returns>
/// <remarks>The temperature of the gas is assumed to be not changing due to a free expansion.</remarks>
public static float MolesToPressureThreshold(GasMixture gasMixture, float targetPressure)
{
// Kid named PV = nRT.
return gasMixture.TotalMoles -
targetPressure * gasMixture.Volume / (Atmospherics.R * gasMixture.Temperature);
}
/// <summary>
/// Checks whether a gas mixture is probably safe.
/// This only checks temperature and pressure, not gas composition.
/// </summary>
/// <param name="air">Mixture to be checked.</param>
/// <returns>Whether the mixture is probably safe.</returns>
public bool IsMixtureProbablySafe(GasMixture? air)
{
// Note that oxygen mix isn't checked, but survival boxes make that not necessary.
if (air == null)
return false;
switch (air.Pressure)
{
case <= Atmospherics.WarningLowPressure:
case >= Atmospherics.WarningHighPressure:
return false;
}
switch (air.Temperature)
{
case <= 260:
case >= 360:
return false;
}
return true;
}
/// <summary>
/// Compares two TileAtmospheres to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>
public GasCompareResult CompareExchange(TileAtmosphere sample, TileAtmosphere otherSample)
{
if (sample.AirArchived == null || otherSample.AirArchived == null)
return GasCompareResult.NoExchange;
return CompareExchange(sample.AirArchived, otherSample.AirArchived);
}
/// <summary>
/// Compares two gas mixtures to see if they are within acceptable ranges for group processing to be enabled.
/// </summary>
public GasCompareResult CompareExchange(GasMixture sample, GasMixture otherSample)
{
var moles = 0f;
for(var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var gasMoles = sample.Moles[i];
var delta = MathF.Abs(gasMoles - otherSample.Moles[i]);
if (delta > Atmospherics.MinimumMolesDeltaToMove && (delta > gasMoles * Atmospherics.MinimumAirRatioToMove))
return (GasCompareResult)i; // We can move gases!
moles += gasMoles;
}
if (moles > Atmospherics.MinimumMolesDeltaToMove)
{
var tempDelta = MathF.Abs(sample.Temperature - otherSample.Temperature);
if (tempDelta > Atmospherics.MinimumTemperatureDeltaToSuspend)
return GasCompareResult.TemperatureExchange; // There can be temperature exchange.
}
// No exchange at all!
return GasCompareResult.NoExchange;
}
/// <summary>
/// Performs reactions for a given gas mixture on an optional holder.
/// </summary>
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder)
{
var reaction = ReactionResult.NoReaction;
var temperature = mixture.Temperature;
var energy = GetThermalEnergy(mixture);
foreach (var prototype in GasReactions)
{
if (energy < prototype.MinimumEnergyRequirement ||
temperature < prototype.MinimumTemperatureRequirement ||
temperature > prototype.MaximumTemperatureRequirement)
continue;
var doReaction = true;
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
var req = prototype.MinimumRequirements[i];
if (!(mixture.GetMoles(i) < req))
continue;
doReaction = false;
break;
}
if (!doReaction)
continue;
reaction = prototype.React(mixture, holder, this, HeatScale);
if(reaction.HasFlag(ReactionResult.StopReactions))
break;
}
return reaction;
}
public enum GasCompareResult
{
NoExchange = -2,
TemperatureExchange = -1,
}
}
}