--- /dev/null
+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);
+ }
+}
--- /dev/null
+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 << 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 << 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 << 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 << 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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}