From f702dc8f2d8e24feb30199d49d0c0b5cf7133043 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Wed, 14 Jan 2026 15:21:04 -0800 Subject: [PATCH] Atmos GasSpecificHeats in shared (#42136) --- Content.Benchmarks/HeatCapacityBenchmark.cs | 83 ++++++ .../EntitySystems/AtmosphereSystem.Gases.cs | 35 +++ .../Atmos/EntitySystems/AtmosphereSystem.cs | 2 +- .../Tests/Atmos/AtmosTest.cs | 2 + .../Tests/Atmos/SharedGasSpecificHeatsTest.cs | 275 ++++++++++++++++++ .../EntitySystems/AtmosphereSystem.CVars.cs | 2 - .../EntitySystems/AtmosphereSystem.Gases.cs | 40 +-- .../SharedAtmosphereSystem.CVars.cs | 20 ++ .../SharedAtmosphereSystem.Gases.cs | 77 +++++ .../EntitySystems/SharedAtmosphereSystem.cs | 21 +- Content.Shared/CCVar/CCVars.Atmos.cs | 2 +- 11 files changed, 503 insertions(+), 56 deletions(-) create mode 100644 Content.Benchmarks/HeatCapacityBenchmark.cs create mode 100644 Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs create mode 100644 Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs create mode 100644 Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs create mode 100644 Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs diff --git a/Content.Benchmarks/HeatCapacityBenchmark.cs b/Content.Benchmarks/HeatCapacityBenchmark.cs new file mode 100644 index 0000000000..cef5bc10c7 --- /dev/null +++ b/Content.Benchmarks/HeatCapacityBenchmark.cs @@ -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(); + _sEntMan = _pair.Server.ResolveDependency(); + _cAtmos = _cEntMan.System(); + _sAtmos = _sEntMan.System(); + + 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 index 0000000000..17b994e64f --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -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); + } +} diff --git a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs index 44759372f4..30567abbf7 100644 --- a/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs +++ b/Content.Client/Atmos/EntitySystems/AtmosphereSystem.cs @@ -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() { diff --git a/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs b/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs index d3bdc91cda..a956d0cbab 100644 --- a/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs +++ b/Content.IntegrationTests/Tests/Atmos/AtmosTest.cs @@ -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 RelevantAtmos; @@ -38,6 +39,7 @@ public abstract class AtmosTest : InteractionTest await base.Setup(); SAtmos = SEntMan.System(); + CAtmos = CEntMan.System(); LookupSystem = SEntMan.System(); SEntMan.TryGetComponent(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 index 0000000000..6c3bcbe6db --- /dev/null +++ b/Content.IntegrationTests/Tests/Atmos/SharedGasSpecificHeatsTest.cs @@ -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; + +/// +/// Tests for asserting that various gas specific heat operations agree with each other and do not deviate +/// across client and server. +/// +[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(); + _cEntMan = Client.ResolveDependency(); + + _sAtmos = _sEntMan.System(); + _cAtmos = _cEntMan.System(); + } + + /// + /// Asserts that the cached gas specific heat arrays agree with each other. + /// + [Test] + public async Task GasSpecificHeats_Agree() + { + var serverSpecificHeats = Array.Empty(); + var clientSpecificHeats = Array.Empty(); + 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."); + } + + /// + /// Asserts that heat capacity calculations agree for the same gas mixture. + /// + [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."); + } + } + + /// + /// 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. + /// + [Test] + public async Task HeatScaleCVar_Replicates_Agree() + { + // ensure that replicated value changes by testing a new value + const float newHeatScale = 13f; + + _sConfig = Server.ResolveDependency(); + _cConfig = Client.ResolveDependency(); + + 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."); + } + } +} diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs index f24f0ae171..11e7cde254 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.CVars.cs @@ -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); diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs index 95d56c9ca6..4d5bdb3f80 100644 --- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs +++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.Gases.cs @@ -13,53 +13,23 @@ namespace Content.Server.Atmos.EntitySystems { [Dependency] private readonly IPrototypeManager _protoMan = default!; - private GasReactionPrototype[] _gasReactions = Array.Empty(); - private float[] _gasSpecificHeats = new float[Atmospherics.TotalNumberOfGases]; + private GasReactionPrototype[] _gasReactions = []; /// /// List of gas reactions ordered by priority. /// public IEnumerable GasReactions => _gasReactions; - /// - /// Cached array of gas specific heats. - /// - public float[] GasSpecificHeats => _gasSpecificHeats; - - private void InitializeGases() + public override void InitializeGases() { + base.InitializeGases(); + _gasReactions = _protoMan.EnumeratePrototypes().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; - } } - /// - /// Calculates the heat capacity for a gas mixture. - /// - /// The mixture whose heat capacity should be calculated - /// Whether the internal heat capacity scaling should be applied. This should not be - /// used outside of atmospheric related heat transfer. - /// - 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 index 0000000000..852af70219 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.CVars.cs @@ -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 index 0000000000..956c1aa827 --- /dev/null +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.Gases.cs @@ -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. + */ + + /// + /// Cached array of gas specific heats. + /// + 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()) + { + var idx = (int)gas; + // Log an error if the corresponding prototype isn't found + if (!_prototypeManager.TryIndex(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; + } + } + + /// + /// Calculates the heat capacity for a gas mixture. + /// + /// The mixture whose heat capacity should be calculated + /// Whether the internal heat capacity scaling should be applied. This should not be + /// used outside of atmospheric related heat transfer. + /// + 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); + } + + /// + /// Gets the heat capacity for a . + /// + /// The moles array of the + /// Whether this represents space, + /// and thus experiences space-specific mechanics (we cheat and make it a bit cooler). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected abstract float GetHeatCapacityCalculation(float[] moles, bool space); +} diff --git a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs index 67d6dec8af..593c7728de 100644 --- a/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs +++ b/Content.Shared/Atmos/EntitySystems/SharedAtmosphereSystem.cs @@ -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 _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(); InitializeBreathTool(); - - foreach (var gas in Enum.GetValues()) - { - var idx = (int)gas; - // Log an error if the corresponding prototype isn't found - if (!_prototypeManager.TryIndex(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]; diff --git a/Content.Shared/CCVar/CCVars.Atmos.cs b/Content.Shared/CCVar/CCVars.Atmos.cs index 7ef40b7911..9b3ef5388f 100644 --- a/Content.Shared/CCVar/CCVars.Atmos.cs +++ b/Content.Shared/CCVar/CCVars.Atmos.cs @@ -142,7 +142,7 @@ public sealed partial class CCVars /// gases heat up and cool down 64x faster than real life. /// public static readonly CVarDef AtmosHeatScale = - CVarDef.Create("atmos.heat_scale", 8f, CVar.SERVERONLY); + CVarDef.Create("atmos.heat_scale", 8f, CVar.REPLICATED | CVar.SERVER); /// /// Maximum explosion radius for explosions caused by bursting a gas tank ("max caps"). -- 2.52.0