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];
break;
}
- return reaction;
+ return _reaction.ReactAll(GasReactions, mixture, holder);
}
public enum GasCompareResult
--- /dev/null
+using Content.Server.Atmos.Reactions;
+using Content.Shared.Atmos;
+using JetBrains.Annotations;
+using System.Collections;
+using System.Linq;
+
+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>
+ private float ReactionRate(GasReactionPrototype reaction, GasMixture mix, float dE)
+ {
+ float temp = mix.Temperature;
+
+ // Gas reactions have a MinimumEnergyRequirement which is in spirit activiation 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.
+ float TScaleFactor = 10;
+ float Ea = TScaleFactor*Atmospherics.R*reaction.MinimumTemperatureRequirement + dE;
+
+ // To compute initial rate coefficient A, assume that at temp = min temp we return 1/10.
+ float RateScaleFactor = 10; // not necessarily the same as TScaleFactor! Don't get confused!
+ float A = MathF.Exp(TScaleFactor) / RateScaleFactor;
+
+ return reaction.RateMultiplier*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)
+ {
+ // It is possible for reactions to change the specific heat capacity, so we need to save initial
+ // internal energy so that we can conserve energy at the end
+ float initialE = _atmosphere.GetThermalEnergy(mix);
+ float reactionE = 0; // heat added by reaction enthalpy
+ foreach (var reaction in reactions)
+ {
+ float rate = 1f; // rate of this reaction
+ int reactants = 0;
+
+ // Reactions that have a maximum temperature really don't make physical sense since increasing
+ // kinetic energy always increases reaction rate. But begrudgingly implement this anyway.
+ if (mix.Temperature > reaction.MaximumTemperatureRequirement)
+ continue;
+
+ // Add concentration-dependent reaction rate
+ // For 1A + 2B -> 3C, the concentration-dependence is [A]^1 * [B]^2
+ float nTotal = mix.TotalMoles;
+ if (nTotal < Atmospherics.GasMinMoles)
+ continue;
+
+ foreach (var (reactant, num) in reaction.Reactants)
+ {
+ rate *= MathF.Pow(mix.GetMoles(reactant)/nTotal, num);
+ reactants++;
+ }
+
+ // No reactants; this is not a generic reaction.
+ if (reactants == 0)
+ continue;
+
+ // Sum catalysts
+ float catalystEnergy = 0;
+ foreach (var (catalyst, dE) in reaction.Catalysts)
+ {
+ catalystEnergy += dE;
+ }
+
+ // Now apply temperature-dependent reaction rate scaling
+ rate *= ReactionRate(reaction, mix, catalystEnergy);
+
+ // Nothing to do
+ if (rate <= 0)
+ continue;
+
+ // Pass to check the maximum rate, limited by the minimum available
+ // reactant to avoid going negative
+ float rateLim = rate;
+ foreach (var (reactant, num) in reaction.Reactants)
+ {
+ rateLim = MathF.Min(mix.GetMoles(reactant)/num, rateLim);
+ }
+ rate = rateLim;
+
+ // Go through and remove all the reactants
+ 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)
+ {
+ reactionE += reaction.Enthalpy/_atmosphere.HeatScale * rate;
+ if (reaction.Enthalpy > 0)
+ mix.ReactionResults[GasReaction.Fire] += rate;
+ }
+ }
+
+ float newHeatCapacity = _atmosphere.GetHeatCapacity(mix, true);
+ mix.Temperature = (initialE + reactionE)/newHeatCapacity;
+ if (reactionE > 0)
+ {
+ var location = holder as TileAtmosphere;
+ if (location != null)
+ {
+ if (mix.Temperature > Atmospherics.FireMinimumTemperatureToExist)
+ {
+ _atmosphere.HotspotExpose(location.GridIndex, location.GridIndices, mix.Temperature, mix.Volume);
+ }
+ }
+ }
+ return ReactionResult.Reacting;
+ }
+}
+++ /dev/null
-using Content.Server.Atmos.EntitySystems;
-using Content.Shared.Atmos;
-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;
- }
-}
+++ /dev/null
-using Content.Server.Atmos.EntitySystems;
-using Content.Shared.Atmos;
-using JetBrains.Annotations;
-
-namespace Content.Server.Atmos.Reactions;
-
-/// <summary>
-/// Takes in nitrogen and frezon and cools down the surrounding area.
-/// </summary>
-[UsedImplicitly]
-public sealed partial class FrezonCoolantReaction : IGasReactionEffect
-{
- public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem, float heatScale)
- {
- var oldHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true);
- var temperature = mixture.Temperature;
-
- var energyModifier = 1f;
- var scale = (temperature - Atmospherics.FrezonCoolLowerTemperature) /
- (Atmospherics.FrezonCoolMidTemperature - Atmospherics.FrezonCoolLowerTemperature);
-
- if (scale > 1f)
- {
- // Scale energy but not frezon usage if we're in a very, very hot place
- energyModifier = Math.Min(scale, Atmospherics.FrezonCoolMaximumEnergyModifier);
- scale = 1f;
- }
-
- if (scale <= 0)
- return ReactionResult.NoReaction;
-
- var initialNit = mixture.GetMoles(Gas.Nitrogen);
- var initialFrezon = mixture.GetMoles(Gas.Frezon);
-
- var burnRate = initialFrezon * scale / Atmospherics.FrezonCoolRateModifier;
-
- var energyReleased = 0f;
- if (burnRate > Atmospherics.MinimumHeatCapacity)
- {
- var nitAmt = Math.Min(burnRate * Atmospherics.FrezonNitrogenCoolRatio, initialNit);
- var frezonAmt = Math.Min(burnRate, initialFrezon);
- mixture.AdjustMoles(Gas.Nitrogen, -nitAmt);
- mixture.AdjustMoles(Gas.Frezon, -frezonAmt);
- mixture.AdjustMoles(Gas.NitrousOxide, nitAmt + frezonAmt);
- energyReleased = burnRate * Atmospherics.FrezonCoolEnergyReleased * energyModifier;
- }
-
- energyReleased /= heatScale; // adjust energy to make sure speedup doesn't cause mega temperature rise
- if (energyReleased >= 0f)
- return ReactionResult.NoReaction;
-
- var newHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true);
- if (newHeatCapacity > Atmospherics.MinimumHeatCapacity)
- mixture.Temperature = (temperature * oldHeatCapacity + energyReleased) / newHeatCapacity;
-
- return ReactionResult.Reacting;
- }
-}
public string ID { get; private set; } = default!;
/// <summary>
- /// Minimum gas amount requirements.
+ /// Minimum gas amount requirements. Reactions that meet these minimum mole requirements
+ /// have their reaction effects run. Generic gas reactions do not have minimum requirements.
/// </summary>
[DataField("minimumRequirements")]
public float[] MinimumRequirements { get; private set; } = new float[Atmospherics.TotalNumberOfGases];
[DataField("minimumTemperature")]
public float MinimumTemperatureRequirement { get; private set; } = Atmospherics.TCMB;
+ /// <summary>
+ /// If this is a generic gas reaction, multiply the initial rate by this. The default is reasonable for
+ /// synthesis reactions. Consider raising this for fires.
+ /// </summary>
+ [DataField("rateMultiplier")]
+ public float RateMultiplier = 1f;
+
/// <summary>
/// Minimum energy requirement.
/// </summary>
/// </summary>
[DataField("effects")] private List<IGasReactionEffect> _effects = new();
+ /// <summary>
+ /// Energy released by the reaction.
+ /// </summary>
+ [DataField("enthalpy")]
+ public float Enthalpy;
+
+ /// <summary>
+ /// Integer gas IDs and integer ratios required in the reaction. If this is defined, the
+ /// generic gas reaction will run.
+ /// </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>
+++ /dev/null
-using Content.Server.Atmos.EntitySystems;
-using Content.Shared.Atmos;
-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;
- }
-}
+++ /dev/null
-using Content.Server.Atmos.EntitySystems;
-using Content.Shared.Atmos;
-using JetBrains.Annotations;
-
-namespace Content.Server.Atmos.Reactions
-{
- [UsedImplicitly]
- [DataDefinition]
- public sealed partial class TritiumFireReaction : IGasReactionEffect
- {
- public ReactionResult React(GasMixture mixture, IGasMixtureHolder? holder, AtmosphereSystem atmosphereSystem, float heatScale)
- {
- var energyReleased = 0f;
- var oldHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true);
- var temperature = mixture.Temperature;
- var location = holder as TileAtmosphere;
- mixture.ReactionResults[GasReaction.Fire] = 0f;
- var burnedFuel = 0f;
- var initialTrit = mixture.GetMoles(Gas.Tritium);
-
- if (mixture.GetMoles(Gas.Oxygen) < initialTrit ||
- Atmospherics.MinimumTritiumOxyburnEnergy > (temperature * oldHeatCapacity))
- {
- burnedFuel = mixture.GetMoles(Gas.Oxygen) / Atmospherics.TritiumBurnOxyFactor;
- if (burnedFuel > initialTrit)
- burnedFuel = initialTrit;
-
- mixture.AdjustMoles(Gas.Tritium, -burnedFuel);
- }
- else
- {
- burnedFuel = initialTrit;
- mixture.SetMoles(Gas.Tritium, mixture.GetMoles(Gas.Tritium ) * (1 - 1 / Atmospherics.TritiumBurnTritFactor));
- mixture.AdjustMoles(Gas.Oxygen, -mixture.GetMoles(Gas.Tritium));
- energyReleased += (Atmospherics.FireHydrogenEnergyReleased * burnedFuel * (Atmospherics.TritiumBurnTritFactor - 1));
- }
-
- if (burnedFuel > 0)
- {
- energyReleased += (Atmospherics.FireHydrogenEnergyReleased * burnedFuel);
-
- // TODO ATMOS Radiation pulse here!
-
- // Conservation of mass is important.
- mixture.AdjustMoles(Gas.WaterVapor, burnedFuel);
-
- mixture.ReactionResults[GasReaction.Fire] += burnedFuel;
- }
-
- energyReleased /= heatScale; // adjust energy to make sure speedup doesn't cause mega temperature rise
- if (energyReleased > 0)
- {
- var newHeatCapacity = atmosphereSystem.GetHeatCapacity(mixture, true);
- if (newHeatCapacity > Atmospherics.MinimumHeatCapacity)
- mixture.Temperature = ((temperature * oldHeatCapacity + energyReleased) / newHeatCapacity);
- }
-
- if (location != null)
- {
- temperature = mixture.Temperature;
- if (temperature > Atmospherics.FireMinimumTemperatureToExist)
- {
- atmosphereSystem.HotspotExpose(location.GridIndex, location.GridIndices, temperature, mixture.Volume);
- }
- }
-
- return mixture.ReactionResults[GasReaction.Fire] != 0 ? ReactionResult.Reacting : ReactionResult.NoReaction;
- }
- }
-}
public const float PlasmaOxygenFullburn = 10f;
public const float PlasmaBurnRateDelta = 9f;
- /// <summary>
- /// This is calculated to help prevent singlecap bombs (Overpowered tritium/oxygen single tank bombs)
- /// </summary>
- public const float MinimumTritiumOxyburnEnergy = 143000f;
-
- public const float TritiumBurnOxyFactor = 100f;
- public const float TritiumBurnTritFactor = 10f;
-
- public const float FrezonCoolLowerTemperature = 23.15f;
-
- /// <summary>
- /// Frezon cools better at higher temperatures.
- /// </summary>
- public const float FrezonCoolMidTemperature = 373.15f;
-
- public const float FrezonCoolMaximumEnergyModifier = 10f;
-
- /// <summary>
- /// Remove X mol of nitrogen for each mol of frezon.
- /// </summary>
- public const float FrezonNitrogenCoolRatio = 5;
- public const float FrezonCoolEnergyReleased = -600e3f;
- public const float FrezonCoolRateModifier = 20f;
-
public const float FrezonProductionMaxEfficiencyTemperature = 73.15f;
/// <summary>
/// </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>
id: TritiumFire
priority: -1
minimumTemperature: 373.149 # Same as Atmospherics.FireMinimumTemperatureToExist
- minimumRequirements: # In this case, same as minimum mole count.
- - 0.01 # oxygen
- - 0 # nitrogen
- - 0 # carbon dioxide
- - 0 # plasma
- - 0.01 # tritium
- effects:
- - !type:TritiumFireReaction {}
+ enthalpy: 284000
+ reactants:
+ Tritium: 2
+ Oxygen: 1
+ products:
+ WaterVapor: 2
- type: gasReaction
id: FrezonCoolant
priority: 1
minimumTemperature: 23.15
- minimumRequirements:
- - 0 # oxygen
- - 0.01 # nitrogen
- - 0 # carbon dioxide
- - 0 # plasma
- - 0 # tritium
- - 0 # vapor
- - 0 # ammonia
- - 0 # n2o
- - 0.01 # frezon
- effects:
- - !type:FrezonCoolantReaction {}
+ enthalpy: -600000
+ reactants:
+ Frezon: 1
+ Nitrogen: 5
+ products:
+ NitrousOxide: 6
- type: gasReaction
id: FrezonProduction
id: AmmoniaOxygenReaction
priority: 2
minimumTemperature: 323.149
- minimumRequirements:
- - 0.01 # oxygen
- - 0 # nitrogen
- - 0 # carbon dioxide
- - 0 # plasma
- - 0 # tritium
- - 0 # vapor
- - 0.01 # ammonia
- - 0 # n2o
- - 0 # frezon
- effects:
- - !type:AmmoniaOxygenReaction {}
+ reactants:
+ Ammonia: 2
+ Oxygen: 2
+ products:
+ NitrousOxide: 1
+ WaterVapor: 3
- type: gasReaction
id: N2ODecomposition
priority: 0
minimumTemperature: 850
- minimumRequirements:
- - 0 # oxygen
- - 0 # nitrogen
- - 0 # carbon dioxide
- - 0 # plasma
- - 0 # tritium
- - 0 # vapor
- - 0 # ammonia
- - 0.01 # n2o
- - 0 # frezon
- effects:
- - !type:N2ODecompositionReaction {}
+ reactants:
+ NitrousOxide: 2
+ products:
+ Nitrogen: 2
+ Oxygen: 1
#- type: gasReaction
# id: WaterVaporPuddle