]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Atmospherics/Temperature HeatContainers (#39997)
authorArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
Sat, 27 Dec 2025 02:05:10 +0000 (18:05 -0800)
committerGitHub <noreply@github.com>
Sat, 27 Dec 2025 02:05:10 +0000 (02:05 +0000)
* Initial HeatContainer logic

* comment fixes

* Comment changes + ChangeHeatCapacity

* highly intelligent specimen

* n-body full heat exchange methods

* extract to partials

* highly intelligent specimen

* fixes + ChangeHeatCapacityKeepTemperature

* Divide and merge methods

* even divide

* different merge signature

* forgot one little thing

* address review

* missing docs

* addr review

* oops

* review

---------

Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Content.Shared/Temperature/HeatContainer/HeatContainer.cs [new file with mode: 0644]
Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Conduct.cs [new file with mode: 0644]
Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Divide.cs [new file with mode: 0644]
Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Exchange.cs [new file with mode: 0644]
Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Merge.cs [new file with mode: 0644]
Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.cs [new file with mode: 0644]

diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainer.cs b/Content.Shared/Temperature/HeatContainer/HeatContainer.cs
new file mode 100644 (file)
index 0000000..c88d11b
--- /dev/null
@@ -0,0 +1,66 @@
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.EntitySystems;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Temperature.HeatContainer;
+
+/// <summary>
+/// A general-purpose container for heat energy.
+/// Any object that contains, stores, or transfers heat should use a <see cref="HeatContainer"/>
+/// instead of implementing its own system.
+/// This allows for consistent heat transfer mechanics across different objects and systems.
+/// </summary>
+[Serializable, NetSerializable, DataDefinition]
+[Access(typeof(HeatContainerHelpers), typeof(SharedAtmosphereSystem))]
+public partial struct HeatContainer : IRobustCloneable<HeatContainer>
+{
+    /// <summary>
+    /// The heat capacity of this container in Joules per Kelvin.
+    /// This determines how much energy is required to change the temperature of the container.
+    /// Higher values mean the container can absorb or release more heat energy
+    /// without a significant change in temperature.
+    /// </summary>
+    [DataField]
+    public float HeatCapacity = 4000f; // about 1kg of water
+
+    /// <summary>
+    /// The current temperature of the container in Kelvin.
+    /// </summary>
+    [DataField]
+    public float Temperature = Atmospherics.T20C; // room temperature
+
+    /// <summary>
+    /// The current temperature of the container in Celsius.
+    /// Ideal if you just need to read the temperature for UI.
+    /// Do not perform computations in Celsius/set this value, use Kelvin instead.
+    /// </summary>
+    [ViewVariables]
+    public float TemperatureC => TemperatureHelpers.KelvinToCelsius(Temperature);
+
+    /// <summary>
+    /// The current thermal energy of the container in Joules.
+    /// </summary>
+    [ViewVariables]
+    public float InternalEnergy => Temperature * HeatCapacity;
+
+    public HeatContainer(float heatCapacity, float temperature)
+    {
+        HeatCapacity = heatCapacity;
+        Temperature = temperature;
+    }
+
+    /// <summary>
+    /// Copy constructor for implementing ICloneable.
+    /// </summary>
+    /// <param name="c">The HeatContainer to copy.</param>
+    private HeatContainer(HeatContainer c)
+    {
+        HeatCapacity = c.HeatCapacity;
+        Temperature = c.Temperature;
+    }
+
+    public HeatContainer Clone()
+    {
+        return new HeatContainer(this);
+    }
+}
diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Conduct.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Conduct.cs
new file mode 100644 (file)
index 0000000..deb705b
--- /dev/null
@@ -0,0 +1,155 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Temperature.HeatContainer;
+
+public static partial class HeatContainerHelpers
+{
+    /// <summary>
+    /// Conducts heat between a <see cref="HeatContainer"/> and some body with a different temperature,
+    /// given some constant thermal conductance g and a small time delta.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to conduct heat to.</param>
+    /// <param name="temp">The temperature of the second object that we are conducting heat with, in kelvin.</param>
+    /// <param name="deltaTime">
+    /// The amount of time that the heat is allowed to conduct, in seconds.
+    /// This value should be small such that deltaTime &lt;&lt; C / g where C is the heat capacity of the container.
+    /// If you need to simulate a larger time step split it into several smaller ones.
+    /// </param>
+    /// <param name="g">The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies.</param>
+    /// <returns>The amount of heat in joules that was added to the heat container.</returns>
+    /// <example>A positive value indicates heat transfer from a hot body to a cold heat container c.</example>
+    /// <remarks>
+    /// This performs a single step using the Euler method for solving the Fourier heat equation
+    /// \frac{dQ}{dt} = g \Delta T.
+    /// If we need more precision in the future consider using a higher order integration scheme.
+    /// If we need support for larger time steps in the future consider adding a method to split the time delta into several
+    /// integration steps with adaptive step size.
+    /// </remarks>
+    [PublicAPI]
+    public static float ConductHeat(this HeatContainer c, float temp, float deltaTime, float g)
+    {
+        var dQ = c.ConductHeatQuery(temp, deltaTime, g);
+        c.AddHeat(dQ);
+        return dQ;
+    }
+
+    /// <summary>
+    /// Conducts heat between two <see cref="HeatContainer"/>s,
+    /// given some constant thermal conductance g and a small time delta.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to conduct heat to.</param>
+    /// <param name="cB">The second <see cref="HeatContainer"/> to conduct heat to.</param>
+    /// <param name="deltaTime">
+    /// The amount of time that the heat is allowed to conduct, in seconds.
+    /// This value should be small such that deltaTime &lt;&lt; C / g where C is the heat capacity of the containers.
+    /// If you need to simulate a larger time step split it into several smaller ones.
+    /// </param>
+    /// <param name="g">The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies.</param>
+    /// <returns>The amount of heat in joules that is exchanged between the bodies.</returns>
+    /// <example>A positive value indicates heat transfer from a hot cB to a cold cA.</example>
+    /// <remarks>
+    /// This performs a single step using the Euler method for solving the Fourier heat equation
+    /// \frac{dQ}{dt} = g \Delta T.
+    /// If we need more precision in the future consider using a higher order integration scheme.
+    /// If we need support for larger time steps in the future consider adding a method to split the time delta into several
+    /// integration steps with adaptive step size.
+    /// </remarks>
+    [PublicAPI]
+    public static float ConductHeat(this HeatContainer cA, HeatContainer cB, float deltaTime, float g)
+    {
+        var dQ = ConductHeatQuery(cA, cB.Temperature, deltaTime, g);
+        cA.AddHeat(dQ);
+        cB.AddHeat(-dQ);
+        return dQ;
+    }
+
+    /// <summary>
+    /// Calculates the amount of heat that would be conducted between a <see cref="HeatContainer"/> and some body with a different temperature,
+    /// given some constant thermal conductance g and a small time delta.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to conduct heat to.</param>
+    /// <param name="temp">The temperature of the second object that we are conducting heat with, in kelvin.</param>
+    /// <param name="deltaTime">
+    /// The amount of time that the heat is allowed to conduct, in seconds.
+    /// This value should be small such that deltaTime &lt;&lt; C / g where C is the heat capacity of the container.
+    /// If you need to simulate a larger time step split it into several smaller ones.
+    /// </param>
+    /// <param name="g">The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies.</param>
+    /// <returns>The amount of heat in joules that would be exchanged between the bodies.</returns>
+    /// <example>A positive value indicates heat transfer from a hot body to a cold heat container c.</example>
+    /// <remarks>
+    /// This performs a single step using the Euler method for solving the Fourier heat equation
+    /// \frac{dQ}{dt} = g \Delta T.
+    /// If we need more precision in the future consider using a higher order integration scheme.
+    /// If we need support for larger time steps in the future consider adding a method to split the time delta into several
+    /// integration steps with adaptive step size.
+    /// </remarks>
+    [PublicAPI]
+    public static float ConductHeatQuery(this HeatContainer c, float temp, float deltaTime, float g)
+    {
+        var dQ = g * (temp - c.Temperature) * deltaTime;
+        var dQMax = Math.Abs(ConductHeatToTempQuery(c, temp));
+
+        // Clamp the transferred heat amount in case we are overshooting the equilibrium temperature because our time step was too large.
+        return Math.Clamp(dQ, -dQMax, dQMax);
+    }
+
+    /// <summary>
+    /// Calculates the amount of heat that would be conducted between two <see cref="HeatContainer"/>s,
+    /// given some conductivity constant k and a time delta. Does not modify the containers.
+    /// </summary>
+    /// <param name="c1">The first <see cref="HeatContainer"/> to conduct heat to.</param>
+    /// <param name="c2">The second <see cref="HeatContainer"/> to conduct heat to.</param>
+    /// <param name="deltaTime">
+    /// The amount of time that the heat is allowed to conduct, in seconds.
+    /// This value should be small such that deltaTime &lt;&lt; C / g where C is the heat capacity of the container.
+    /// If you need to simulate a larger time step split it into several smaller ones.
+    /// </param>
+    /// <param name="g">The thermal conductance in watt per kelvin. This describes how well heat flows between the bodies.</param>
+    /// <returns>The amount of heat in joules that would be exchanged between the bodies.</returns>
+    /// <example>A positive value indicates heat transfer from a hot c2 to a cold c1.</example>
+    /// <remarks>
+    /// This performs a single step using the Euler method for solving the Fourier heat equation
+    /// \frac{dQ}{dt} = g \Delta T.
+    /// If we need more precision in the future consider using a higher order integration scheme.
+    /// If we need support for larger time steps in the future consider adding a method to split the time delta into several
+    /// integration steps with adaptive step size.
+    /// </remarks>
+    [PublicAPI]
+    public static float ConductHeatQuery(this HeatContainer c1, HeatContainer c2, float deltaTime, float g)
+    {
+        return ConductHeatQuery(c1, c2.Temperature, deltaTime, g);
+    }
+
+    /// <summary>
+    /// Changes the temperature of a <see cref="HeatContainer"/> to a target temperature by
+    /// adding or removing the necessary amount of heat.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to change the temperature of.</param>
+    /// <param name="targetTemp">The desired temperature to reach.</param>
+    /// <returns>The amount of heat in joules that was transferred to or from the <see cref="HeatContainer"/>
+    /// to reach the target temperature.</returns>
+    /// <example>A positive value indicates heat must be added to the container to reach the target temperature.</example>
+    [PublicAPI]
+    public static float ConductHeatToTemp(this HeatContainer c, float targetTemp)
+    {
+        var dQ = ConductHeatToTempQuery(c, targetTemp);
+        c.Temperature = targetTemp;
+        return dQ;
+    }
+
+    /// <summary>
+    /// Determines the amount of heat that must be transferred to or from a <see cref="HeatContainer"/>
+    /// to reach a target temperature. Does not modify the heat container.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to query.</param>
+    /// <param name="targetTemp">The desired temperature to reach.</param>
+    /// <returns>The amount of heat in joules that must be transferred to or from the <see cref="HeatContainer"/>
+    /// to reach the target temperature.</returns>
+    /// <example>A positive value indicates heat must be added to the container to reach the target temperature.</example>
+    [PublicAPI]
+    public static float ConductHeatToTempQuery(this HeatContainer c, float targetTemp)
+    {
+        return (targetTemp - c.Temperature) * c.HeatCapacity;
+    }
+}
diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Divide.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Divide.cs
new file mode 100644 (file)
index 0000000..f1e7206
--- /dev/null
@@ -0,0 +1,55 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Temperature.HeatContainer;
+
+public static partial class HeatContainerHelpers
+{
+    /// <summary>
+    /// Splits a <see cref="HeatContainer"/> into two.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to split. This will be modified to contain the remaining heat capacity.</param>
+    /// <param name="fraction">The fraction of the heat capacity to move to the new container. Clamped between 0 and 1.</param>
+    /// <returns>A new <see cref="HeatContainer"/> containing the specified fraction of the original container's heat capacity and the same temperature.</returns>
+    [PublicAPI]
+    public static HeatContainer Split(this ref HeatContainer c, float fraction = 0.5f)
+    {
+        fraction = Math.Clamp(fraction, 0f, 1f);
+        var newHeatCapacity = c.HeatCapacity * fraction;
+
+        var newContainer = new HeatContainer
+        {
+            HeatCapacity = newHeatCapacity,
+            Temperature = c.Temperature,
+        };
+
+        c.HeatCapacity -= newHeatCapacity;
+
+        return newContainer;
+    }
+
+    /// <summary>
+    /// Divides a source <see cref="HeatContainer"/> into a specified number of equal parts.
+    /// </summary>
+    /// <param name="c">The input <see cref="HeatContainer"/> to split.</param>
+    /// <param name="num">The number of <see cref="HeatContainer"/>s
+    /// to split the source <see cref="HeatContainer"/> into.</param>
+    /// <exception cref="ArgumentException">Thrown when attempting to divide the source container by zero.</exception>
+    /// <returns>An array of <see cref="HeatContainer"/>s equally split from the source <see cref="HeatContainer"/>.</returns>
+    [PublicAPI]
+    public static HeatContainer[] Divide(this HeatContainer c, uint num)
+    {
+        if (num == 0)
+            throw new ArgumentException("Cannot divide by zero.", nameof(num));
+
+        var fraction = 1f / num;
+        var cFrac = c.Split(fraction);
+        var containers = new HeatContainer[num];
+
+        for (var i = 0; i < num; i++)
+        {
+            containers[i] = cFrac;
+        }
+
+        return containers;
+    }
+}
diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Exchange.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Exchange.cs
new file mode 100644 (file)
index 0000000..6b7eb12
--- /dev/null
@@ -0,0 +1,214 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Temperature.HeatContainer;
+
+public static partial class HeatContainerHelpers
+{
+    #region 2-Body Exchange
+
+    /// <summary>
+    /// Determines the amount of heat energy that must be transferred between two heat containers
+    /// to bring them into thermal equilibrium.
+    /// Does not modify the containers.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to exchange heat.</param>
+    /// <param name="cB">The second <see cref="HeatContainer"/> to exchange heat with.</param>
+    /// <returns>The amount of heat in joules that is needed
+    /// to bring the containers to thermal equilibrium.</returns>
+    /// <example>A positive value indicates heat transfer from a hot cA to a cold cB.</example>
+    [PublicAPI]
+    public static float EquilibriumHeatQuery(this HeatContainer cA, HeatContainer cB)
+    {
+        /*
+        The solution is derived from the following facts:
+        1. Let Q be the amount of heat energy transferred from cA to cB.
+        2. T_A > T_B, so heat will flow from cA to cB.
+        3. The energy lost by T_A is equal to Q = C_A * (T_A_initial - T_A_final)
+        4. The energy gained by T_B is equal to Q = C_B * (T_B_final - T_B_initial)
+        5. Energy is conserved. So T_A_final and T_B_final can be expressed as:
+        T_A_final = T_A_initial - Q / C_A
+        T_B_final = T_B_initial + Q / C_B
+        6. At thermal equilibrium, T_A_final = T_B_final.
+        7. Solve for Q.
+        */
+        return (cA.Temperature - cB.Temperature) *
+               (cA.HeatCapacity * cB.HeatCapacity / (cA.HeatCapacity + cB.HeatCapacity));
+    }
+
+    /// <summary>
+    /// Determines the resulting temperature if two heat containers are brought into thermal equilibrium.
+    /// Does not modify the containers.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to exchange heat.</param>
+    /// <param name="cB">The second <see cref="HeatContainer"/> to exchange heat with.</param>
+    /// <returns>The resulting equilibrium temperature both containers will be at.</returns>
+    [PublicAPI]
+    public static float EquilibriumTemperatureQuery(this HeatContainer cA, HeatContainer cB)
+    {
+        // Insert the above solution for Q into T_A_final = T_A_initial - Q / C_A and rearrange the result.
+        return (cA.HeatCapacity * cA.Temperature - cB.HeatCapacity * cB.Temperature) / (cA.HeatCapacity + cB.HeatCapacity);
+    }
+
+    /// <summary>
+    /// Brings two <see cref="HeatContainer"/>s into thermal equilibrium by exchanging heat.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to exchange heat.</param>
+    /// <param name="cB">The second <see cref="HeatContainer"/> to exchange heat with.</param>
+    [PublicAPI]
+    public static void Equilibrate(this HeatContainer cA, HeatContainer cB)
+    {
+        var tFinal = EquilibriumTemperatureQuery(cA, cB);
+        cA.Temperature = tFinal;
+        cB.Temperature = tFinal;
+    }
+
+    /// <summary>
+    /// Brings two <see cref="HeatContainer"/>s into thermal equilibrium by exchanging heat.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to exchange heat.</param>
+    /// <param name="cB">The second <see cref="HeatContainer"/> to exchange heat with.</param>
+    /// <param name="dQ">The amount of heat in joules that was transferred from container A to B.</param>
+    [PublicAPI]
+    public static void Equilibrate(this HeatContainer cA, HeatContainer cB, out float dQ)
+    {
+        var tInitialA = cA.Temperature;
+        var tFinal = EquilibriumTemperatureQuery(cA, cB);
+        cA.Temperature = tFinal;
+        cB.Temperature = tFinal;
+        dQ = (tInitialA - tFinal) / cA.HeatCapacity;
+    }
+
+    #endregion
+
+    #region N-Body Exchange
+
+    /// <summary>
+    /// Brings an array of <see cref="HeatContainer"/>s into thermal equilibrium by exchanging heat.
+    /// </summary>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to bring into thermal equilibrium.</param>
+    [PublicAPI]
+    public static void Equilibrate(this HeatContainer[] cN)
+    {
+        var tF = cN.EquilibriumTemperatureQuery();
+        for (var i = 0; i < cN.Length; i++)
+        {
+            cN[i].Temperature = tF;
+        }
+    }
+
+    /// <summary>
+    /// Brings a <see cref="HeatContainer"/> into thermal equilibrium
+    /// with an array of other <see cref="HeatContainer"/>s by exchanging heat.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to bring into thermal equilibrium.</param>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to bring into thermal equilibrium.</param>
+    [PublicAPI]
+    public static void Equilibrate(this HeatContainer cA, HeatContainer[] cN)
+    {
+        var tF = cA.EquilibriumTemperatureQuery(cN);
+
+        cA.Temperature = tF;
+        for (var i = 0; i < cN.Length; i++)
+        {
+            cN[i].Temperature = tF;
+        }
+    }
+
+    /// <summary>
+    /// Determines the final temperature of an array of <see cref="HeatContainer"/>s
+    /// when they are brought into thermal equilibrium. Does not modify the containers.
+    /// </summary>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to bring into thermal equilibrium.</param>
+    /// <returns>The temperature of all <see cref="HeatContainer"/>s involved after reaching thermal equilibrium.</returns>
+    [PublicAPI]
+    public static float EquilibriumTemperatureQuery(this HeatContainer[] cN)
+    {
+        /*
+        The solution is derived via the following:
+
+        1. In thermal equilibrium all bodies have the same temperature T_f.
+
+        2. Heat exchange for each body is defined by the equation \Delta Q_n = C_n \Delta T_n = C_n (T_f - T_n)
+        where C_n is the heat capacity and \Delta T_n the change in temperature of the n-th body.
+
+        3. Heat energy must be conserved, so the sum of all heat changes must equal zero.
+        Therefore, \sum_{n=1}^{N} Q_n = 0.
+
+        4. Substitute and expand.
+        \sum_{n=1}^{N} C_n (T_f - T_n) = 0.
+
+        5. Unroll and expand.
+        C_1(T_f - T_1) + C_2(T_f - T_2) + ... + C_n(T_f - T_n) = 0
+        C_1 T_f - C_1 T_1 + C_2 T_f - C_2 T_2 + ... + C_n T_f - C_n T_n = 0
+
+        6. Group like terms.
+        T_f(C_1 + C_2 + ... + C_n) - (C_1 T_1 + C_2 T_2 + ... + C_n T_n) = 0
+
+        7. Solve.
+        T_f(C_1 + C_2 + ... + C_n) = (C_1 T_1 + C_2 T_2 + ... + C_n T_n)
+        T_f = \frac{C_1 T_1 + C_2 T_2 + ... + C_n T_n}{C_1 + C_2 + ... + C_n}
+
+        8. Summation.
+        T_f = \frac{\sum(C_n T_n)}{\sum(C_n)}
+        */
+
+        var numerator = 0f;
+        var denominator = 0f;
+
+        foreach (var c in cN)
+        {
+            numerator += c.HeatCapacity * c.Temperature;
+            denominator += c.HeatCapacity;
+        }
+
+        return numerator / denominator;
+    }
+
+    /// <summary>
+    /// Determines the final temperature of an array of <see cref="HeatContainer"/>s
+    /// when they are brought into thermal equilibrium. Does not modify the containers.
+    /// </summary>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to bring into thermal equilibrium.</param>
+    /// <param name="dQ">The amount of heat in joules that was added to each container
+    /// to reach thermal equilibrium.</param>
+    /// <returns>The temperature of all <see cref="HeatContainer"/>s involved after reaching thermal equilibrium.</returns>
+    [PublicAPI]
+    public static float EquilibriumTemperatureQuery(this HeatContainer[] cN, out float[] dQ)
+    {
+        /*
+        For finding the total heat exchanged during the equalization between a group of bodies
+        take the difference of the internal energy before and after the exchange.
+
+        dQ = C * (T_f - T_i) for each container
+        */
+
+        var tF = cN.EquilibriumTemperatureQuery();
+        dQ = new float[cN.Length];
+
+        for (var i = 0; i < cN.Length; i++)
+        {
+            dQ[i] = cN[i].HeatCapacity * (tF - cN[i].Temperature);
+        }
+
+        return tF;
+    }
+
+    /// <summary>
+    /// Determines the final temperature of a <see cref="HeatContainer"/> when it is brought into thermal equilibrium
+    /// with an array of other <see cref="HeatContainer"/>s. Does not modify the containers.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to bring into thermal equilibrium.</param>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to bring into thermal equilibrium.</param>
+    /// <returns>The temperature of all <see cref="HeatContainer"/>s involved after reaching thermal equilibrium.</returns>
+    [PublicAPI]
+    public static float EquilibriumTemperatureQuery(this HeatContainer cA, HeatContainer[] cN)
+    {
+        var cAll = new HeatContainer[cN.Length + 1];
+        cAll[0] = cA;
+        cN.CopyTo(cAll, 1);
+
+        return cAll.EquilibriumTemperatureQuery();
+    }
+
+    #endregion
+}
diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Merge.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.Merge.cs
new file mode 100644 (file)
index 0000000..d9f2cdd
--- /dev/null
@@ -0,0 +1,66 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Temperature.HeatContainer;
+
+public static partial class HeatContainerHelpers
+{
+    /// <summary>
+    /// Merges two heat containers into one, conserving total internal energy.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to merge. This will be modified to contain the merged result.</param>
+    /// <param name="cB">The second <see cref="HeatContainer"/> to merge.</param>
+    [PublicAPI]
+    public static void Merge(this ref HeatContainer cA, HeatContainer cB)
+    {
+        var merged = new HeatContainer
+        {
+            HeatCapacity = cA.HeatCapacity + cB.HeatCapacity,
+            Temperature = (cA.InternalEnergy + cB.InternalEnergy) / (cA.HeatCapacity + cB.HeatCapacity)
+        };
+
+        cA = merged;
+    }
+
+
+    /// <summary>
+    /// Merges an array of <see cref="HeatContainer"/>s into a single heat container, conserving total internal energy.
+    /// </summary>
+    /// <param name="cA">The first <see cref="HeatContainer"/> to merge.
+    /// This will be modified to contain the merged result.</param>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to merge.</param>
+    [PublicAPI]
+    public static void Merge(this ref HeatContainer cA, HeatContainer[] cN)
+    {
+        var cAcN = new HeatContainer[cN.Length + 1];
+        cAcN[0] = cA;
+        cN.CopyTo(cAcN, 1);
+
+        cA = cAcN.Merge();
+    }
+
+    /// <summary>
+    /// Merges an array of <see cref="HeatContainer"/>s into a single heat container, conserving total internal energy.
+    /// </summary>
+    /// <param name="cN">The array of <see cref="HeatContainer"/>s to merge.</param>
+    /// <returns>A new <see cref="HeatContainer"/> representing the merged result.</returns>
+    [PublicAPI]
+    public static HeatContainer Merge(this HeatContainer[] cN)
+    {
+        var totalHeatCapacity = 0f;
+        var totalEnergy = 0f;
+
+        foreach (var c in cN)
+        {
+            totalHeatCapacity += c.HeatCapacity;
+            totalEnergy += c.InternalEnergy;
+        }
+
+        var result = new HeatContainer
+        {
+            HeatCapacity = totalHeatCapacity,
+            Temperature = totalEnergy / totalHeatCapacity,
+        };
+
+        return result;
+    }
+}
diff --git a/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.cs b/Content.Shared/Temperature/HeatContainer/HeatContainerHelpers.cs
new file mode 100644 (file)
index 0000000..39078a8
--- /dev/null
@@ -0,0 +1,52 @@
+using JetBrains.Annotations;
+
+namespace Content.Shared.Temperature.HeatContainer;
+
+/// <summary>
+/// Class containing helper methods for working with <see cref="HeatContainer"/>s.
+/// Use these classes instead of implementing your own heat transfer logic.
+/// </summary>
+public static partial class HeatContainerHelpers
+{
+    /// <summary>
+    /// Adds or removes heat energy from the container.
+    /// Positive values add heat, negative values remove heat.
+    /// The temperature can never become lower than 0K even if more heat is removed.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to add or remove energy.</param>
+    /// <param name="dQ">The energy in joules to add or remove.</param>
+    [PublicAPI]
+    public static void AddHeat(this HeatContainer c, float dQ)
+    {
+        c.Temperature = c.AddHeatQuery(dQ);
+    }
+
+    /// <summary>
+    /// Calculates the resulting temperature of the container after adding or removing heat energy.
+    /// Positive values add heat, negative values remove heat. This method doesn't change the container's state.
+    /// The temperature can never become lower than 0K even if more heat is removed.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to query.</param>
+    /// <param name="dQ">The energy in joules to add or remove.</param>
+    /// <returns>The resulting temperature in kelvin after the heat change.</returns>
+    [PublicAPI]
+    public static float AddHeatQuery(this HeatContainer c, float dQ)
+    {
+        // Don't allow the temperature to go below the absolute minimum.
+        return Math.Max(0f, c.Temperature + dQ / c.HeatCapacity);
+    }
+
+    /// <summary>
+    /// Sets the heat capacity of a <see cref="HeatContainer"/> without altering its thermal energy.
+    /// Adjusts the temperature accordingly to maintain the same internal energy.
+    /// </summary>
+    /// <param name="c">The <see cref="HeatContainer"/> to modify.</param>
+    /// <param name="newHeatCapacity">The new heat capacity to set.</param>
+    [PublicAPI]
+    public static void SetHeatCapacity(this HeatContainer c, float newHeatCapacity)
+    {
+        var currentEnergy = c.InternalEnergy;
+        c.HeatCapacity = newHeatCapacity;
+        c.Temperature = currentEnergy / c.HeatCapacity;
+    }
+}