Merge branch 'fork/Partmedia/notafet/gas_reactions_lite' into engi-atmos/YAML-gas-reactions

This commit is contained in:
ArtisticRoomba
2025-12-20 11:26:09 -08:00
7 changed files with 166 additions and 83 deletions

View File

@@ -11,6 +11,7 @@ namespace Content.Server.Atmos.EntitySystems
public sealed partial class AtmosphereSystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly GenericGasReactionSystem _reaction = default!;
private GasReactionPrototype[] _gasReactions = Array.Empty<GasReactionPrototype>();
private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases];
@@ -443,6 +444,7 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder)
{
// First pass: run through the legacy (hard-coded) gas reactions
var reaction = ReactionResult.NoReaction;
var temperature = mixture.Temperature;
var energy = GetThermalEnergy(mixture);
@@ -474,7 +476,8 @@ namespace Content.Server.Atmos.EntitySystems
break;
}
return reaction;
// Second pass: Regardless of result, run YAML gas reactions
return _reaction.ReactAll(GasReactions, mixture, holder);
}
public enum GasCompareResult

View File

@@ -0,0 +1,124 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos.Reactions;
using Content.Shared.Atmos;
namespace Content.Server.Atmos.EntitySystems;
public sealed class GenericGasReactionSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
/// <summary>
/// Return a reaction rate (in units reactants per second) for a given reaction. Based on the
/// Arrhenius equation (https://en.wikipedia.org/wiki/Arrhenius_equation).
///
/// This means that most reactions scale exponentially above the MinimumTemperatureRequirement.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
private float ReactionRate(GasReactionPrototype reaction, GasMixture mix, float dE)
{
var temp = mix.Temperature;
// Gas reactions have a MinimumEnergyRequirement which is in spirit activation energy (Ea),
// but no reactions define it. So we have to calculate one to use. One way is to assume that
// Ea = 10 * R * MinimumTemperatureRequirement such that Ea >> RT.
const float TScaleFactor = 10;
var Ea = TScaleFactor * Atmospherics.R * reaction.MinimumTemperatureRequirement + dE;
// To compute initial rate coefficient A, assume that at temp = min temp we return 1/10.
const float RateScaleFactor = 10; // not necessarily the same as TScaleFactor! Don't get confused!
var A = MathF.Exp(TScaleFactor) / RateScaleFactor;
// Prevent divide by zero
if (temp < Atmospherics.TCMB)
return 0;
return A * MathF.Exp(-Ea / (Atmospherics.R * temp));
}
/// <summary>
/// Run all of the reactions given on the given gas mixture located in the given container.
/// </summary>
public ReactionResult ReactAll(IEnumerable<GasReactionPrototype> reactions,
GasMixture mix,
IGasMixtureHolder? holder)
{
var nTotal = mix.TotalMoles;
// Guard against very small amounts of gas in mixture
if (nTotal < Atmospherics.GasMinMoles)
return ReactionResult.NoReaction;
// Guard against volume div/0.
// Realistically, GasMixtures should never have this low of a volume.
Debug.Assert(mix.Volume > Atmospherics.GasMinVolumeForReactions);
foreach (var reaction in reactions)
{
// Check if this is a generic YAML reaction (has reactants)
if (reaction.Reactants.Count == 0)
continue;
// Add concentration-dependent reaction rate
// For 1A + 2B -> 3C, the concentration-dependence is [A]^1 * [B]^2
var rate = 1f; // rate of this reaction
foreach (var (reactant, num) in reaction.Reactants)
{
var concentration = mix.GetMoles(reactant) / mix.Volume;
rate *= MathF.Pow(concentration, num);
}
// Sum catalysts
float catalystEnergy = 0;
foreach (var (catalyst, dE) in reaction.Catalysts)
{
var concentration = mix.GetMoles(catalyst) / mix.Volume;
catalystEnergy += dE * concentration;
}
// Now apply temperature-dependent reaction rate scaling
rate *= ReactionRate(reaction, mix, catalystEnergy);
// Nothing to do
if (rate <= 0)
continue;
// Go through and remove all the reactants
// If any of the reactants were zero, then the code above would have already set
// rate to zero, so we don't have to check that again here.
foreach (var (reactant, num) in reaction.Reactants)
{
mix.AdjustMoles(reactant, -num * rate);
}
// Go through and add products
foreach (var (product, num) in reaction.Products)
{
mix.AdjustMoles(product, num * rate);
}
// Add heat from the reaction
if (reaction.Enthalpy != 0)
{
_atmosphere.AddHeat(mix, reaction.Enthalpy / _atmosphere.HeatScale * rate);
if (reaction.Enthalpy > 0)
{
mix.ReactionResults[(byte)GasReaction.Fire] += rate;
if (holder is TileAtmosphere location)
{
if (mix.Temperature > Atmospherics.FireMinimumTemperatureToExist)
{
_atmosphere.HotspotExpose(location.GridIndex,
location.GridIndices,
mix.Temperature,
mix.Volume);
}
}
}
}
}
return ReactionResult.Reacting;
}
}

View File

@@ -1,34 +0,0 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
using JetBrains.Annotations;
namespace Content.Server.Atmos.Reactions;
[UsedImplicitly]
public sealed partial class AmmoniaOxygenReaction : IGasReactionEffect
{
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem, float heatScale)
{
var nAmmonia = mixture.GetMoles(Gas.Ammonia);
var nOxygen = mixture.GetMoles(Gas.Oxygen);
var nTotal = mixture.TotalMoles;
// Concentration-dependent reaction rate
var fAmmonia = nAmmonia/nTotal;
var fOxygen = nOxygen/nTotal;
var rate = MathF.Pow(fAmmonia, 2) * MathF.Pow(fOxygen, 2);
var deltaMoles = nAmmonia / Atmospherics.AmmoniaOxygenReactionRate * 2 * rate;
if (deltaMoles <= 0 || nAmmonia - deltaMoles < 0)
return ReactionResult.NoReaction;
mixture.AdjustMoles(Gas.Ammonia, -deltaMoles);
mixture.AdjustMoles(Gas.Oxygen, -deltaMoles);
mixture.AdjustMoles(Gas.NitrousOxide, deltaMoles / 2);
mixture.AdjustMoles(Gas.WaterVapor, deltaMoles * 1.5f);
return ReactionResult.Reacting;
}
}

View File

@@ -48,6 +48,27 @@ namespace Content.Server.Atmos.Reactions
/// </summary>
[DataField("effects")] private List<IGasReactionEffect> _effects = new();
[DataField("enthalpy")]
public float Enthalpy;
/// <summary>
/// Integer gas IDs and integer ratios required in the reaction.
/// </summary>
[DataField("reactants")]
public Dictionary<Gas, int> Reactants = new();
/// <summary>
/// Integer gas IDs and integer ratios of reaction products.
/// </summary>
[DataField("products")]
public Dictionary<Gas, int> Products = new();
/// <summary>
/// Integer gas IDs and how much they modify the activation energy (J/mol).
/// </summary>
[DataField("catalysts")]
public Dictionary<Gas, int> Catalysts = new();
/// <summary>
/// Process all reaction effects.
/// </summary>

View File

@@ -1,29 +0,0 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
using JetBrains.Annotations;
namespace Content.Server.Atmos.Reactions;
/// <summary>
/// Decomposes Nitrous Oxide into Nitrogen and Oxygen.
/// </summary>
[UsedImplicitly]
public sealed partial class N2ODecompositionReaction : IGasReactionEffect
{
public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem, float heatScale)
{
var cacheN2O = mixture.GetMoles(Gas.NitrousOxide);
var burnedFuel = cacheN2O / Atmospherics.N2ODecompositionRate;
if (burnedFuel <= 0 || cacheN2O - burnedFuel < 0)
return ReactionResult.NoReaction;
mixture.AdjustMoles(Gas.NitrousOxide, -burnedFuel);
mixture.AdjustMoles(Gas.Nitrogen, burnedFuel);
mixture.AdjustMoles(Gas.Oxygen, burnedFuel / 2);
return ReactionResult.Reacting;
}
}

View File

@@ -111,6 +111,12 @@ namespace Content.Shared.Atmos
/// </summary>
public const float GasMinMoles = 0.00000005f;
/// <summary>
/// Minimum volume that a <see cref="GasMixture"/> must have to perform reactions.
/// Prevents div/0 issues.
/// </summary>
public const float GasMinVolumeForReactions = 0.0001f;
public const float OpenHeatTransferCoefficient = 0.4f;
/// <summary>
@@ -274,16 +280,6 @@ namespace Content.Shared.Atmos
/// </summary>
public const float FrezonProductionConversionRate = 50f;
/// <summary>
/// The maximum portion of the N2O that can decompose each reaction tick. (50%)
/// </summary>
public const float N2ODecompositionRate = 2f;
/// <summary>
/// Divisor for Ammonia Oxygen reaction so that it doesn't happen instantaneously.
/// </summary>
public const float AmmoniaOxygenReactionRate = 10f;
/// <summary>
/// Determines at what pressure the ultra-high pressure red icon is displayed.
/// </summary>

View File

@@ -43,20 +43,22 @@
id: AmmoniaOxygenReaction
priority: 2
minimumTemperature: 323.149
minimumRequirements:
Oxygen: 0.01
Ammonia: 0.01
effects:
- !type:AmmoniaOxygenReaction {}
reactants:
Ammonia: 2
Oxygen: 2
products:
NitrousOxide: 1
WaterVapor: 3
- type: gasReaction
id: N2ODecomposition
priority: 0
minimumTemperature: 850
minimumRequirements:
NitrousOxide: 0.01
effects:
- !type:N2ODecompositionReaction {}
reactants:
NitrousOxide: 2
products:
Nitrogen: 2
Oxygen: 1
#- type: gasReaction
# id: WaterVaporPuddle