]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Atmos GasSpecificHeats in shared (#42136)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Wed, 14 Jan 2026 23:21:04 +0000 (15:21 -0800)
committerGitHub <noreply@github.com>
Wed, 14 Jan 2026 23:21:04 +0000 (15:21 -0800)
Content.Benchmarks/HeatCapacityBenchmark.cs [new file with mode: 0644]
Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs [new file with mode: 0644]
Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs
Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs [new file with mode: 0644]
Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs
Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs [new file with mode: 0644]
Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs [new file with mode: 0644]
Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs
Content.Shared/CCVar/CCVars.Atmos.cs

diff --git a/Content.Benchmarks/HeatCapacityBenchmark.cs b/Content.Benchmarks/HeatCapacityBenchmark.cs
new file mode 100644 (file)
index 0000000..cef5bc1
--- /dev/null
@@ -0,0 +1,83 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+
+namespace Content.Benchmarks;
+
+[Virtual]
+[GcServer(true)]
+[MemoryDiagnoser]
+public class HeatCapacityBenchmark
+{
+    private TestPair _pair = default!;
+    private IEntityManager _sEntMan = default!;
+    private IEntityManager _cEntMan = default!;
+    private Client.Atmos.EntitySystems.AtmosphereSystem _cAtmos = default!;
+    private AtmosphereSystem _sAtmos = default!;
+    private GasMixture _mix;
+
+    [GlobalSetup]
+    public async Task SetupAsync()
+    {
+        ProgramShared.PathOffset = "../../../../";
+        PoolManager.Startup();
+        _pair = await PoolManager.GetServerClient();
+        await _pair.Connect();
+        _cEntMan = _pair.Client.ResolveDependency<IEntityManager>();
+        _sEntMan = _pair.Server.ResolveDependency<IEntityManager>();
+        _cAtmos = _cEntMan.System<Client.Atmos.EntitySystems.AtmosphereSystem>();
+        _sAtmos = _sEntMan.System<AtmosphereSystem>();
+
+        const float volume = 2500f;
+        const float temperature = 293.15f;
+
+        const float o2 = 12.3f;
+        const float n2 = 45.6f;
+        const float co2 = 0.42f;
+        const float plasma = 0.05f;
+
+        _mix = new GasMixture(volume) { Temperature = temperature };
+
+        _mix.AdjustMoles(Gas.Oxygen, o2);
+        _mix.AdjustMoles(Gas.Nitrogen, n2);
+        _mix.AdjustMoles(Gas.CarbonDioxide, co2);
+        _mix.AdjustMoles(Gas.Plasma, plasma);
+    }
+
+    [Benchmark]
+    public async Task ClientHeatCapacityBenchmark()
+    {
+        await _pair.Client.WaitPost(delegate
+        {
+            for (var i = 0; i < 10000; i++)
+            {
+                _cAtmos.GetHeatCapacity(_mix, applyScaling: true);
+            }
+        });
+    }
+
+    [Benchmark]
+    public async Task ServerHeatCapacityBenchmark()
+    {
+        await _pair.Server.WaitPost(delegate
+        {
+            for (var i = 0; i < 10000; i++)
+            {
+                _sAtmos.GetHeatCapacity(_mix, applyScaling: true);
+            }
+        });
+    }
+
+    [GlobalCleanup]
+    public async Task CleanupAsync()
+    {
+        await _pair.DisposeAsync();
+        PoolManager.Shutdown();
+    }
+}
diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs
new file mode 100644 (file)
index 0000000..17b994e
--- /dev/null
@@ -0,0 +1,35 @@
+using System.Runtime.CompilerServices;
+using Content.Shared.Atmos;
+
+namespace Content.Client.Atmos.EntitySystems;
+
+public sealed partial class AtmosphereSystem
+{
+    /*
+     Partial class for operations involving GasMixtures.
+
+     Any method that is overridden here is usually because the server-sided implementation contains
+     code that would escape sandbox. As such these methods are overridden here with a safe
+     implementation.
+     */
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    protected override 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;
+        }
+
+        // explicit stackalloc call is banned on client tragically.
+        // the JIT does not stackalloc this during runtime,
+        // though this isnt the hottest code path so it should be fine
+        // the gc can eat a little as a treat
+        var tmp = new 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);
+    }
+}
index 44759372f4e4b58b24b78148f617e0c95d708e69..30567abbf790ec6e639c3786b9512cd106b59d74 100644 (file)
@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
 
 namespace Content.Client.Atmos.EntitySystems;
 
-public sealed class AtmosphereSystem : SharedAtmosphereSystem
+public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
 {
     public override void Initialize()
     {
index d3bdc91cda44defc62190ef24968243d28672580..a956d0cbab5960aed694610c82e590005484cc88 100644 (file)
@@ -18,6 +18,7 @@ namespace Content.IntegrationTests.Tests.Atmos;
 public abstract class AtmosTest : InteractionTest
 {
     protected AtmosphereSystem SAtmos = default!;
+    protected Content.Client.Atmos.EntitySystems.AtmosphereSystem CAtmos = default!;
     protected EntityLookupSystem LookupSystem = default!;
 
     protected Entity<GridAtmosphereComponent> RelevantAtmos;
@@ -38,6 +39,7 @@ public abstract class AtmosTest : InteractionTest
         await base.Setup();
 
         SAtmos = SEntMan.System<AtmosphereSystem>();
+        CAtmos = CEntMan.System<Content.Client.Atmos.EntitySystems.AtmosphereSystem>();
         LookupSystem = SEntMan.System<EntityLookupSystem>();
 
         SEntMan.TryGetComponent<GridAtmosphereComponent>(MapData.Grid, out var gridAtmosComp);
diff --git a/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs
new file mode 100644 (file)
index 0000000..6c3bcbe
--- /dev/null
@@ -0,0 +1,275 @@
+using Content.Client.Atmos.EntitySystems;
+using Content.IntegrationTests.Pair;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.EntitySystems;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameObjects;
+using Robust.UnitTesting;
+
+namespace Content.IntegrationTests.Tests.Atmos;
+
+/// <summary>
+/// Tests for asserting that various gas specific heat operations agree with each other and do not deviate
+/// across client and server.
+/// </summary>
+[TestOf(nameof(SharedAtmosphereSystem))]
+public sealed class SharedGasSpecificHeatsTest
+{
+    private IConfigurationManager _sConfig;
+    private IConfigurationManager _cConfig;
+
+    private TestPair _pair = default!;
+
+    private RobustIntegrationTest.ServerIntegrationInstance Server => _pair.Server;
+    private RobustIntegrationTest.ClientIntegrationInstance Client => _pair.Client;
+
+    private IEntityManager _sEntMan = default!;
+    private Content.Server.Atmos.EntitySystems.AtmosphereSystem _sAtmos = default!;
+
+    private IEntityManager _cEntMan = default!;
+    private AtmosphereSystem _cAtmos = default!;
+
+    [SetUp]
+    public async Task SetUp()
+    {
+        var poolSettings = new PoolSettings
+        {
+            Connected = true,
+        };
+        _pair = await PoolManager.GetServerClient(poolSettings);
+
+        _sEntMan = Server.ResolveDependency<IEntityManager>();
+        _cEntMan = Client.ResolveDependency<IEntityManager>();
+
+        _sAtmos = _sEntMan.System<Content.Server.Atmos.EntitySystems.AtmosphereSystem>();
+        _cAtmos = _cEntMan.System<AtmosphereSystem>();
+    }
+
+    /// <summary>
+    /// Asserts that the cached gas specific heat arrays agree with each other.
+    /// </summary>
+    [Test]
+    public async Task GasSpecificHeats_Agree()
+    {
+        var serverSpecificHeats = Array.Empty<float>();
+        var clientSpecificHeats = Array.Empty<float>();
+        await Server.WaitPost(delegate
+        {
+            serverSpecificHeats = _sAtmos.GasSpecificHeats;
+        });
+
+        await Client.WaitPost(delegate
+        {
+            clientSpecificHeats = _cAtmos.GasSpecificHeats;
+        });
+
+        Assert.That(serverSpecificHeats,
+            Is.EqualTo(clientSpecificHeats),
+            "Server and client gas specific heat arrays do not agree.");
+    }
+
+    /// <summary>
+    /// Asserts that heat capacity calculations agree for the same gas mixture.
+    /// </summary>
+    [Test]
+    public async Task HeatCapacity_Agree()
+    {
+        const float volume = 2500f;
+        const float temperature = 293.15f;
+
+        const float o2 = 12.3f;
+        const float n2 = 45.6f;
+        const float co2 = 0.42f;
+        const float plasma = 0.05f;
+
+        var serverScaled = 0f;
+        var serverUnscaled = 0f;
+        var clientScaled = 0f;
+        var clientUnscaled = 0f;
+
+        await Server.WaitPost(delegate
+        {
+            var mix = new GasMixture(volume) { Temperature = temperature };
+            mix.AdjustMoles(Gas.Oxygen, o2);
+            mix.AdjustMoles(Gas.Nitrogen, n2);
+            mix.AdjustMoles(Gas.CarbonDioxide, co2);
+            mix.AdjustMoles(Gas.Plasma, plasma);
+
+            serverScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true);
+            serverUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false);
+        });
+
+        await Client.WaitPost(delegate
+        {
+            var mix = new GasMixture(volume) { Temperature = temperature };
+            mix.AdjustMoles(Gas.Oxygen, o2);
+            mix.AdjustMoles(Gas.Nitrogen, n2);
+            mix.AdjustMoles(Gas.CarbonDioxide, co2);
+            mix.AdjustMoles(Gas.Plasma, plasma);
+
+            clientScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true);
+            clientUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false);
+        });
+
+        // none of these should be exploding or nonzero.
+        // they could potentially agree at insane values and pass the test
+        // so check for if they're sane.
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(serverScaled,
+                Is.GreaterThan(0f),
+                "Heat capacity calculated on server with scaling is not greater than zero.");
+            Assert.That(serverUnscaled,
+                Is.GreaterThan(0f),
+                "Heat capacity calculated on server without scaling is not greater than zero.");
+            Assert.That(clientScaled,
+                Is.GreaterThan(0f),
+                "Heat capacity calculated on client with scaling is not greater than zero.");
+            Assert.That(clientUnscaled,
+                Is.GreaterThan(0f),
+                "Heat capacity calculated on client without scaling is not greater than zero.");
+
+            Assert.That(float.IsFinite(serverScaled),
+                Is.True,
+                "Heat capacity calculated on server with scaling is not finite.");
+            Assert.That(float.IsFinite(serverUnscaled),
+                Is.True,
+                "Heat capacity calculated on server without scaling is not finite.");
+            Assert.That(float.IsFinite(clientScaled),
+                Is.True,
+                "Heat capacity calculated on client with scaling is not finite.");
+            Assert.That(float.IsFinite(clientUnscaled),
+                Is.True,
+                "Heat capacity calculated on client without scaling is not finite.");
+        }
+
+        const float epsilon = 1e-4f;
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(serverScaled,
+                Is.EqualTo(clientScaled).Within(epsilon),
+                "Heat capacity calculated with scaling does not agree between client and server.");
+            Assert.That(serverUnscaled,
+                Is.EqualTo(clientUnscaled).Within(epsilon),
+                "Heat capacity calculated without scaling does not agree between client and server.");
+
+            Assert.That(serverUnscaled,
+                Is.EqualTo(serverScaled * _sAtmos.HeatScale).Within(epsilon),
+                "Heat capacity calculated on server without scaling does not equal scaled value multiplied by HeatScale.");
+            Assert.That(clientUnscaled,
+                Is.EqualTo(clientScaled * _cAtmos.HeatScale).Within(epsilon),
+                "Heat capacity calculated on client without scaling does not equal scaled value multiplied by HeatScale.");
+        }
+    }
+
+    /// <summary>
+    /// HeatScale CVAR is required for specific heat calculations.
+    /// Assert that they agree across client and server, and that changing the CVAR
+    /// replicates properly and updates the cached value.
+    /// Also assert that calculations using the updated HeatScale agree properly.
+    /// </summary>
+    [Test]
+    public async Task HeatScaleCVar_Replicates_Agree()
+    {
+        // ensure that replicated value changes by testing a new value
+        const float newHeatScale = 13f;
+
+        _sConfig = Server.ResolveDependency<IConfigurationManager>();
+        _cConfig = Client.ResolveDependency<IConfigurationManager>();
+
+        await Server.WaitPost(delegate
+        {
+            _sConfig.SetCVar(CCVars.AtmosHeatScale, newHeatScale);
+        });
+
+        await Server.WaitRunTicks(5);
+        await Client.WaitRunTicks(5);
+
+        // assert agreement between client and server
+        float serverCVar = 0;
+        float clientCVar = 0;
+        float serverHeatScale = 0;
+        float clientHeatScale = 0;
+
+        await Server.WaitPost(delegate
+        {
+            serverCVar = _sConfig.GetCVar(CCVars.AtmosHeatScale);
+            serverHeatScale = _sAtmos.HeatScale;
+        });
+
+        await Client.WaitPost(delegate
+        {
+            clientCVar = _cConfig.GetCVar(CCVars.AtmosHeatScale);
+            clientHeatScale = _cAtmos.HeatScale;
+        });
+
+        const float epsilon = 1e-4f;
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(serverCVar,
+                Is.EqualTo(newHeatScale).Within(epsilon),
+                "Server CVAR value for AtmosHeatScale does not equal the set value.");
+            Assert.That(clientCVar,
+                Is.EqualTo(newHeatScale).Within(epsilon),
+                "Client CVAR value for AtmosHeatScale does not equal the set value.");
+
+            Assert.That(serverHeatScale,
+                Is.EqualTo(newHeatScale).Within(epsilon),
+                "Server cached HeatScale does not equal the set CVAR value.");
+            Assert.That(clientHeatScale,
+                Is.EqualTo(newHeatScale).Within(epsilon),
+                "Client cached HeatScale does not equal the set CVAR value.");
+
+            Assert.That(serverHeatScale,
+                Is.EqualTo(clientHeatScale).Within(epsilon),
+                "Client and server cached HeatScale values do not agree.");
+        }
+
+        // verify that anything calculated using the shared HeatScale agrees properly
+        const float volume = 2500f;
+        const float temperature = 293.15f;
+
+        var sScaled = 0f;
+        var sUnscaled = 0f;
+        var cScaled = 0f;
+        var cUnscaled = 0f;
+
+        await Server.WaitPost(delegate
+        {
+            var mix = new GasMixture(volume) { Temperature = temperature };
+            mix.AdjustMoles(Gas.Oxygen, 10f);
+            mix.AdjustMoles(Gas.Nitrogen, 20f);
+
+            sScaled = _sAtmos.GetHeatCapacity(mix, applyScaling: true);
+            sUnscaled = _sAtmos.GetHeatCapacity(mix, applyScaling: false);
+        });
+
+        await Client.WaitPost(delegate
+        {
+            var mix = new GasMixture(volume) { Temperature = temperature };
+            mix.AdjustMoles(Gas.Oxygen, 10f);
+            mix.AdjustMoles(Gas.Nitrogen, 20f);
+
+            cScaled = _cAtmos.GetHeatCapacity(mix, applyScaling: true);
+            cUnscaled = _cAtmos.GetHeatCapacity(mix, applyScaling: false);
+        });
+
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(sScaled,
+                Is.GreaterThan(0f),
+                "Heat capacity calculated on server with scaling is not greater than zero after CVAR change.");
+            Assert.That(cScaled,
+                Is.GreaterThan(0f),
+                "Heat capacity calculated on client with scaling is not greater than zero after CVAR change.");
+
+            Assert.That(sUnscaled,
+                Is.EqualTo(sScaled * serverHeatScale).Within(epsilon),
+                "Heat capacity calculated on server without scaling does not equal scaled value multiplied by updated HeatScale.");
+            Assert.That(cUnscaled,
+                Is.EqualTo(cScaled * clientHeatScale).Within(epsilon),
+                "Heat capacity calculated on client without scaling does not equal scaled value multiplied by updated HeatScale.");
+        }
+    }
+}
index f24f0ae171fcd9c8128d352b766502b6d4d5dee6..11e7cde25483c1f9dd6b5bcb5d636dc4ee9ed15d 100644 (file)
@@ -25,7 +25,6 @@ namespace Content.Server.Atmos.EntitySystems
         public float AtmosMaxProcessTime { get; private set; }
         public float AtmosTickRate { get; private set; }
         public float Speedup { get; private set; }
-        public float HeatScale { get; private set; }
         public bool DeltaPressureDamage { get; private set; }
         public int DeltaPressureParallelProcessPerIteration { get; private set; }
         public int DeltaPressureParallelBatchSize { get; private set; }
@@ -55,7 +54,6 @@ namespace Content.Server.Atmos.EntitySystems
             Subs.CVar(_cfg, CCVars.AtmosMaxProcessTime, value => AtmosMaxProcessTime = value, true);
             Subs.CVar(_cfg, CCVars.AtmosTickRate, value => AtmosTickRate = value, true);
             Subs.CVar(_cfg, CCVars.AtmosSpeedup, value => Speedup = value, true);
-            Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true);
             Subs.CVar(_cfg, CCVars.ExcitedGroups, value => ExcitedGroups = value, true);
             Subs.CVar(_cfg, CCVars.ExcitedGroupsSpaceIsAllConsuming, value => ExcitedGroupsSpaceIsAllConsuming = value, true);
             Subs.CVar(_cfg, CCVars.DeltaPressureDamage, value => DeltaPressureDamage = value, true);
index 95d56c9ca63205725d73f9385a883cba1f1a3597..4d5bdb3f80b39d35c6a9981d0bd8f3729e998480 100644 (file)
@@ -13,53 +13,23 @@ namespace Content.Server.Atmos.EntitySystems
     {
         [Dependency] private readonly IPrototypeManager _protoMan = default!;
 
-        private GasReactionPrototype[] _gasReactions = Array.Empty<GasReactionPrototype>();
-        private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases];
+        private GasReactionPrototype[] _gasReactions = [];
 
         /// <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()
+        public override void InitializeGases()
         {
+            base.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)
+        protected override 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))
diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs
new file mode 100644 (file)
index 0000000..852af70
--- /dev/null
@@ -0,0 +1,20 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.CCVar;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract partial class SharedAtmosphereSystem
+{
+    /*
+     Partial class for storing shared configuration values.
+     */
+
+    public float HeatScale { get; private set; }
+
+    [SuppressMessage("ReSharper", "BadExpressionBracesLineBreaks")]
+    [SuppressMessage("ReSharper", "MultipleStatementsOnOneLine")]
+    private void InitializeCVars()
+    {
+        Subs.CVar(_cfg, CCVars.AtmosHeatScale, value => { HeatScale = value; InitializeGases(); }, true);
+    }
+}
diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs
new file mode 100644 (file)
index 0000000..956c1aa
--- /dev/null
@@ -0,0 +1,77 @@
+using System.Runtime.CompilerServices;
+using Content.Shared.Atmos.Prototypes;
+
+namespace Content.Shared.Atmos.EntitySystems;
+
+public abstract partial class SharedAtmosphereSystem
+{
+    /*
+     Partial class for operations involving GasMixtures.
+
+     Sometimes methods here are abstract because they need different client/server implementations
+     due to sandboxing.
+     */
+
+    /// <summary>
+    /// Cached array of gas specific heats.
+    /// </summary>
+    public float[] GasSpecificHeats => _gasSpecificHeats;
+    private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases];
+
+    public string?[] GasReagents = new string[Atmospherics.TotalNumberOfGases];
+    protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases];
+
+    public virtual void InitializeGases()
+    {
+        foreach (var gas in Enum.GetValues<Gas>())
+        {
+            var idx = (int)gas;
+            // Log an error if the corresponding prototype isn't found
+            if (!_prototypeManager.TryIndex<GasPrototype>(gas.ToString(), out var gasPrototype))
+            {
+                Log.Error($"Failed to find corresponding {nameof(GasPrototype)} for gas ID {(int)gas} ({gas}) with expected ID \"{gas.ToString()}\". Is your prototype named correctly?");
+                continue;
+            }
+            GasPrototypes[idx] = gasPrototype;
+            GasReagents[idx] = gasPrototype.Reagent;
+        }
+
+        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;
+    }
+
+    protected float GetHeatCapacity(GasMixture mixture)
+    {
+        return GetHeatCapacityCalculation(mixture.Moles, mixture.Immutable);
+    }
+
+    /// <summary>
+    /// Gets the heat capacity for a <see cref="GasMixture"/>.
+    /// </summary>
+    /// <param name="moles">The moles array of the <see cref="GasMixture"/></param>
+    /// <param name="space">Whether this <see cref="GasMixture"/> represents space,
+    /// and thus experiences space-specific mechanics (we cheat and make it a bit cooler).</param>
+    /// <returns></returns>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    protected abstract float GetHeatCapacityCalculation(float[] moles, bool space);
+}
index 67d6dec8af572a139d83b4fd1c16a9e367bbbfd6..593c7728ded2ffa054a7a9fa0cf8a3f51065b5b4 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Shared.Atmos.Prototypes;
 using Content.Shared.Body.Components;
 using Content.Shared.Body.Systems;
+using Robust.Shared.Configuration;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Atmos.EntitySystems
@@ -9,13 +10,10 @@ namespace Content.Shared.Atmos.EntitySystems
     {
         [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
         [Dependency] private readonly SharedInternalsSystem _internals = default!;
+        [Dependency] private readonly IConfigurationManager _cfg = default!;
 
         private EntityQuery<InternalsComponent> _internalsQuery;
 
-        public string?[] GasReagents = new string[Atmospherics.TotalNumberOfGases];
-
-        protected readonly GasPrototype[] GasPrototypes = new GasPrototype[Atmospherics.TotalNumberOfGases];
-
         public override void Initialize()
         {
             base.Initialize();
@@ -23,19 +21,8 @@ namespace Content.Shared.Atmos.EntitySystems
             _internalsQuery = GetEntityQuery<InternalsComponent>();
 
             InitializeBreathTool();
-
-            foreach (var gas in Enum.GetValues<Gas>())
-            {
-                var idx = (int)gas;
-                // Log an error if the corresponding prototype isn't found
-                if (!_prototypeManager.TryIndex<GasPrototype>(gas.ToString(), out var gasPrototype))
-                {
-                    Log.Error($"Failed to find corresponding {nameof(GasPrototype)} for gas ID {(int)gas} ({gas}) with expected ID \"{gas.ToString()}\". Is your prototype named correctly?");
-                    continue;
-                }
-                GasPrototypes[idx] = gasPrototype;
-                GasReagents[idx] = gasPrototype.Reagent;
-            }
+            InitializeGases();
+            InitializeCVars();
         }
 
         public GasPrototype GetGas(int gasId) => GasPrototypes[gasId];
index 7ef40b7911b8fa01a3dd031adddd6a4b2d4489fe..9b3ef5388f2ad2660a39d19d35a5e0c23dbeb06d 100644 (file)
@@ -142,7 +142,7 @@ public sealed partial class CCVars
     ///     gases heat up and cool down 64x faster than real life.
     /// </summary>
     public static readonly CVarDef<float> AtmosHeatScale =
-        CVarDef.Create("atmos.heat_scale", 8f, CVar.SERVERONLY);
+        CVarDef.Create("atmos.heat_scale", 8f, CVar.REPLICATED | CVar.SERVER);
 
     /// <summary>
     ///     Maximum explosion radius for explosions caused by bursting a gas tank ("max caps").