diff --git a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
index 94c30349dc..52aa96776b 100644
--- a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
+++ b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml
@@ -7,7 +7,14 @@
-
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs
index 4264eda0b0..f5adecfd92 100644
--- a/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs
+++ b/Content.Client/Administration/UI/ManageSolutions/EditSolutionsWindow.xaml.cs
@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
+using Robust.Shared.Maths;
namespace Content.Client.Administration.UI.ManageSolutions
{
@@ -66,9 +67,8 @@ namespace Content.Client.Administration.UI.ManageSolutions
if (!_solutions.TryGetValue(_selectedSolution, out var solution))
return;
- TotalLabel.Text = Loc.GetString("admin-solutions-window-capacity-label",
- ("currentVolume", solution.TotalVolume),
- ("maxVolume",solution.MaxVolume));
+ UpdateVolumeBox(solution);
+ UpdateThermalBox(solution);
foreach (var reagent in solution)
{
@@ -76,6 +76,109 @@ namespace Content.Client.Administration.UI.ManageSolutions
}
}
+ ///
+ /// Updates the entry displaying the current and maximum volume of the selected solution.
+ ///
+ /// The selected solution.
+ private void UpdateVolumeBox(Solution solution)
+ {
+ VolumeBox.DisposeAllChildren();
+
+ var volumeLabel = new Label();
+ volumeLabel.HorizontalExpand = true;
+ volumeLabel.Margin = new Thickness(0, 4);
+ volumeLabel.Text = Loc.GetString("admin-solutions-window-volume-label",
+ ("currentVolume", solution.CurrentVolume),
+ ("maxVolume", solution.MaxVolume));
+
+ var capacityBox = new BoxContainer();
+ capacityBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
+ capacityBox.HorizontalExpand = true;
+ capacityBox.Margin = new Thickness(0, 4);
+
+ var capacityLabel = new Label();
+ capacityLabel.HorizontalExpand = true;
+ capacityLabel.Margin = new Thickness(0, 1);
+ capacityLabel.Text = Loc.GetString("admin-solutions-window-capacity-label");
+
+ var capacitySpin = new FloatSpinBox(1, 2);
+ capacitySpin.HorizontalExpand = true;
+ capacitySpin.Margin = new Thickness(0, 1);
+ capacitySpin.Value = (float) solution.MaxVolume;
+ capacitySpin.OnValueChanged += SetCapacity;
+
+ capacityBox.AddChild(capacityLabel);
+ capacityBox.AddChild(capacitySpin);
+
+ VolumeBox.AddChild(volumeLabel);
+ VolumeBox.AddChild(capacityBox);
+ }
+
+ ///
+ /// Updates the entry displaying the current specific heat, heat capacity, temperature, and thermal energy
+ /// of the selected solution.
+ ///
+ /// The selected solution.
+ private void UpdateThermalBox(Solution solution)
+ {
+ ThermalBox.DisposeAllChildren();
+
+ var specificHeatLabel = new Label();
+ specificHeatLabel.HorizontalExpand = true;
+ specificHeatLabel.Margin = new Thickness(0, 1);
+ specificHeatLabel.Text = Loc.GetString("admin-solutions-window-specific-heat-label", ("specificHeat", solution.SpecificHeat));
+
+ var heatCapacityLabel = new Label();
+ heatCapacityLabel.HorizontalExpand = true;
+ heatCapacityLabel.Margin = new Thickness(0, 1);
+ heatCapacityLabel.Text = Loc.GetString("admin-solutions-window-heat-capacity-label", ("heatCapacity", solution.HeatCapacity));
+
+ // Temperature entry:
+ var temperatureBox = new BoxContainer();
+ temperatureBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
+ temperatureBox.HorizontalExpand = true;
+ temperatureBox.Margin = new Thickness(0, 1);
+
+ var temperatureLabel = new Label();
+ temperatureLabel.HorizontalExpand = true;
+ temperatureLabel.Margin = new Thickness(0, 1);
+ temperatureLabel.Text = Loc.GetString("admin-solutions-window-temperature-label");
+
+ var temperatureSpin = new FloatSpinBox(1, 2);
+ temperatureSpin.HorizontalExpand = true;
+ temperatureSpin.Margin = new Thickness(0, 1);
+ temperatureSpin.Value = solution.Temperature;
+ temperatureSpin.OnValueChanged += SetTemperature;
+
+ temperatureBox.AddChild(temperatureLabel);
+ temperatureBox.AddChild(temperatureSpin);
+
+ // Thermal energy entry:
+ var thermalEnergyBox = new BoxContainer();
+ thermalEnergyBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
+ thermalEnergyBox.HorizontalExpand = true;
+ thermalEnergyBox.Margin = new Thickness(0, 1);
+
+ var thermalEnergyLabel = new Label();
+ thermalEnergyLabel.HorizontalExpand = true;
+ thermalEnergyLabel.Margin = new Thickness(0, 1);
+ thermalEnergyLabel.Text = Loc.GetString("admin-solutions-window-thermal-energy-label");
+
+ var thermalEnergySpin = new FloatSpinBox(1, 2);
+ thermalEnergySpin.HorizontalExpand = true;
+ thermalEnergySpin.Margin = new Thickness(0, 1);
+ thermalEnergySpin.Value = solution.ThermalEnergy;
+ thermalEnergySpin.OnValueChanged += SetThermalEnergy;
+
+ thermalEnergyBox.AddChild(thermalEnergyLabel);
+ thermalEnergyBox.AddChild(thermalEnergySpin);
+
+ ThermalBox.AddChild(specificHeatLabel);
+ ThermalBox.AddChild(heatCapacityLabel);
+ ThermalBox.AddChild(temperatureBox);
+ ThermalBox.AddChild(thermalEnergyBox);
+ }
+
///
/// Add a single reagent entry to the list
///
@@ -112,6 +215,41 @@ namespace Content.Client.Administration.UI.ManageSolutions
_consoleHost.ExecuteCommand(command);
}
+ private void SetCapacity(FloatSpinBox.FloatSpinBoxEventArgs args)
+ {
+ if (_solutions == null || _selectedSolution == null)
+ return;
+
+ var command = $"setsolutioncapacity {_target} {_selectedSolution} {args.Value}";
+ _consoleHost.ExecuteCommand(command);
+ }
+
+ ///
+ /// Sets the temperature of the selected solution to a value.
+ ///
+ /// An argument struct containing the value to set the temperature to.
+ private void SetTemperature(FloatSpinBox.FloatSpinBoxEventArgs args)
+ {
+ if (_solutions == null || _selectedSolution == null)
+ return;
+
+ var command = $"setsolutiontemperature {_target} {_selectedSolution} {args.Value}";
+ _consoleHost.ExecuteCommand(command);
+ }
+
+ ///
+ /// Sets the thermal energy of the selected solution to a value.
+ ///
+ /// An argument struct containing the value to set the thermal energy to.
+ private void SetThermalEnergy(FloatSpinBox.FloatSpinBoxEventArgs args)
+ {
+ if (_solutions == null || _selectedSolution == null)
+ return;
+
+ var command = $"setsolutionthermalenergy {_target} {_selectedSolution} {args.Value}";
+ _consoleHost.ExecuteCommand(command);
+ }
+
///
/// Open a new window that has options to add new reagents to the solution.
///
diff --git a/Content.Server/Administration/Commands/SetSolutionCapacity.cs b/Content.Server/Administration/Commands/SetSolutionCapacity.cs
new file mode 100644
index 0000000000..917b6a9002
--- /dev/null
+++ b/Content.Server/Administration/Commands/SetSolutionCapacity.cs
@@ -0,0 +1,62 @@
+using Content.Server.Chemistry.Components.SolutionManager;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Shared.Administration;
+using Content.Shared.FixedPoint;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Administration.Commands
+{
+ [AdminCommand(AdminFlags.Fun)]
+ public class SetSolutionCapacity : IConsoleCommand
+ {
+ public string Command => "setsolutioncapacity";
+ public string Description => "Set the capacity (maximum volume) of some solution.";
+ public string Help => $"Usage: {Command} ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length < 3)
+ {
+ shell.WriteLine($"Not enough arguments.\n{Help}");
+ return;
+ }
+
+ if (!EntityUid.TryParse(args[0], out var uid))
+ {
+ shell.WriteLine($"Invalid entity id.");
+ return;
+ }
+
+ if (!IoCManager.Resolve().TryGetComponent(uid, out SolutionContainerManagerComponent man))
+ {
+ shell.WriteLine($"Entity does not have any solutions.");
+ return;
+ }
+
+ if (!man.Solutions.ContainsKey(args[1]))
+ {
+ var validSolutions = string.Join(", ", man.Solutions.Keys);
+ shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
+ return;
+ }
+ var solution = man.Solutions[args[1]];
+
+ if (!float.TryParse(args[2], out var quantityFloat))
+ {
+ shell.WriteLine($"Failed to parse new capacity.");
+ return;
+ }
+
+ if(quantityFloat < 0.0f)
+ {
+ shell.WriteLine($"Cannot set the maximum volume of a solution to a negative number.");
+ return;
+ }
+
+ var quantity = FixedPoint2.New(quantityFloat);
+ EntitySystem.Get().SetCapacity(uid, solution, quantity);
+ }
+ }
+}
diff --git a/Content.Server/Administration/Commands/SetSolutionTemperature.cs b/Content.Server/Administration/Commands/SetSolutionTemperature.cs
new file mode 100644
index 0000000000..998b0dbd5a
--- /dev/null
+++ b/Content.Server/Administration/Commands/SetSolutionTemperature.cs
@@ -0,0 +1,60 @@
+using Content.Server.Chemistry.Components.SolutionManager;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Administration.Commands
+{
+ [AdminCommand(AdminFlags.Fun)]
+ public class SetSolutionTemperature : IConsoleCommand
+ {
+ public string Command => "setsolutiontemperature";
+ public string Description => "Set the temperature of some solution.";
+ public string Help => $"Usage: {Command} ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length < 3)
+ {
+ shell.WriteLine($"Not enough arguments.\n{Help}");
+ return;
+ }
+
+ if (!EntityUid.TryParse(args[0], out var uid))
+ {
+ shell.WriteLine($"Invalid entity id.");
+ return;
+ }
+
+ if (!IoCManager.Resolve().TryGetComponent(uid, out SolutionContainerManagerComponent man))
+ {
+ shell.WriteLine($"Entity does not have any solutions.");
+ return;
+ }
+
+ if (!man.Solutions.ContainsKey(args[1]))
+ {
+ var validSolutions = string.Join(", ", man.Solutions.Keys);
+ shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
+ return;
+ }
+ var solution = man.Solutions[args[1]];
+
+ if (!float.TryParse(args[2], out var quantity))
+ {
+ shell.WriteLine($"Failed to parse new temperature.");
+ return;
+ }
+
+ if (quantity <= 0.0f)
+ {
+ shell.WriteLine($"Cannot set the temperature of a solution to a non-positive number.");
+ return;
+ }
+
+ EntitySystem.Get().SetTemperature(uid, solution, quantity);
+ }
+ }
+}
diff --git a/Content.Server/Administration/Commands/SetSolutionThermalEnergy.cs b/Content.Server/Administration/Commands/SetSolutionThermalEnergy.cs
new file mode 100644
index 0000000000..5b8ac77053
--- /dev/null
+++ b/Content.Server/Administration/Commands/SetSolutionThermalEnergy.cs
@@ -0,0 +1,67 @@
+using Content.Server.Chemistry.Components.SolutionManager;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.Administration.Commands
+{
+ [AdminCommand(AdminFlags.Fun)]
+ public class SetSolutionThermalEnergy : IConsoleCommand
+ {
+ public string Command => "setsolutionthermalenergy";
+ public string Description => "Set the thermal energy of some solution.";
+ public string Help => $"Usage: {Command} ";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length < 3)
+ {
+ shell.WriteLine($"Not enough arguments.\n{Help}");
+ return;
+ }
+
+ if (!EntityUid.TryParse(args[0], out var uid))
+ {
+ shell.WriteLine($"Invalid entity id.");
+ return;
+ }
+
+ if (!IoCManager.Resolve().TryGetComponent(uid, out SolutionContainerManagerComponent man))
+ {
+ shell.WriteLine($"Entity does not have any solutions.");
+ return;
+ }
+
+ if (!man.Solutions.ContainsKey(args[1]))
+ {
+ var validSolutions = string.Join(", ", man.Solutions.Keys);
+ shell.WriteLine($"Entity does not have a \"{args[1]}\" solution. Valid solutions are:\n{validSolutions}");
+ return;
+ }
+ var solution = man.Solutions[args[1]];
+
+ if (!float.TryParse(args[2], out var quantity))
+ {
+ shell.WriteLine($"Failed to parse new thermal energy.");
+ return;
+ }
+
+ if (solution.HeatCapacity <= 0.0f)
+ {
+ if(quantity != 0.0f)
+ {
+ shell.WriteLine($"Cannot set the thermal energy of a solution with 0 heat capacity to a non-zero number.");
+ return;
+ }
+ } else if(quantity <= 0.0f)
+ {
+ shell.WriteLine($"Cannot set the thermal energy of a solution with heat capacity to a non-positive number.");
+ return;
+ }
+
+ EntitySystem.Get().SetThermalEnergy(uid, solution, quantity);
+ }
+ }
+}
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs
index a7b61f4bbc..408f7fd3b1 100644
--- a/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/SolutionContainerSystem.cs
@@ -153,6 +153,24 @@ namespace Content.Server.Chemistry.EntitySystems
}
}
+ ///
+ /// Sets the capacity (maximum volume) of a solution to a new value.
+ ///
+ /// The entity containing the solution.
+ /// The solution to set the capacity of.
+ /// The value to set the capacity of the solution to.
+ public void SetCapacity(EntityUid targetUid, Solution targetSolution, FixedPoint2 capacity)
+ {
+ if (targetSolution.MaxVolume == capacity)
+ return;
+
+ targetSolution.MaxVolume = capacity;
+ if (capacity < targetSolution.CurrentVolume)
+ targetSolution.RemoveSolution(targetSolution.CurrentVolume - capacity);
+
+ UpdateChemicals(targetUid, targetSolution);
+ }
+
///
/// Adds reagent of an Id to the container.
///
@@ -163,10 +181,10 @@ namespace Content.Server.Chemistry.EntitySystems
/// The amount of reagent successfully added.
/// If all the reagent could be added.
public bool TryAddReagent(EntityUid targetUid, Solution targetSolution, string reagentId, FixedPoint2 quantity,
- out FixedPoint2 acceptedQuantity)
+ out FixedPoint2 acceptedQuantity, float? temperature = null)
{
acceptedQuantity = targetSolution.AvailableVolume > quantity ? quantity : targetSolution.AvailableVolume;
- targetSolution.AddReagent(reagentId, acceptedQuantity);
+ targetSolution.AddReagent(reagentId, acceptedQuantity, temperature);
if (acceptedQuantity > 0)
UpdateChemicals(targetUid, targetSolution, true);
@@ -257,6 +275,8 @@ namespace Content.Server.Chemistry.EntitySystems
{
var (reagentId, curQuantity) = solution.Contents[i];
removedReagent[pos++] = reagentId;
+ if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype? proto))
+ proto = new ReagentPrototype();
var newQuantity = curQuantity - quantity;
if (newQuantity <= 0)
@@ -302,5 +322,56 @@ namespace Content.Server.Chemistry.EntitySystems
return reagentQuantity;
}
+
+
+ // Thermal energy and temperature management.
+ #region Thermal Energy and Temperature
+
+ ///
+ /// Sets the temperature of a solution to a new value and then checks for reaction processing.
+ ///
+ /// The entity in which the solution is located.
+ /// The solution to set the temperature of.
+ /// The new value to set the temperature to.
+ public void SetTemperature(EntityUid owner, Solution solution, float temperature)
+ {
+ if (temperature == solution.Temperature)
+ return;
+
+ solution.Temperature = temperature;
+ UpdateChemicals(owner, solution, true);
+ }
+
+ ///
+ /// Sets the thermal energy of a solution to a new value and then checks for reaction processing.
+ ///
+ /// The entity in which the solution is located.
+ /// The solution to set the thermal energy of.
+ /// The new value to set the thermal energy to.
+ public void SetThermalEnergy(EntityUid owner, Solution solution, float thermalEnergy)
+ {
+ if (thermalEnergy == solution.ThermalEnergy)
+ return;
+
+ solution.ThermalEnergy = thermalEnergy;
+ UpdateChemicals(owner, solution, true);
+ }
+
+ ///
+ /// Adds some thermal energy to a solution and then checks for reaction processing.
+ ///
+ /// The entity in which the solution is located.
+ /// The solution to set the thermal energy of.
+ /// The new value to set the thermal energy to.
+ public void AddThermalEnergy(EntityUid owner, Solution solution, float thermalEnergy)
+ {
+ if (thermalEnergy == 0.0f)
+ return;
+
+ solution.ThermalEnergy += thermalEnergy;
+ UpdateChemicals(owner, solution, true);
+ }
+
+ #endregion Thermal Energy and Temperature
}
}
diff --git a/Content.Server/Chemistry/ReactionEffects/SolutionTemperatureEffects.cs b/Content.Server/Chemistry/ReactionEffects/SolutionTemperatureEffects.cs
new file mode 100644
index 0000000000..b4bae814b7
--- /dev/null
+++ b/Content.Server/Chemistry/ReactionEffects/SolutionTemperatureEffects.cs
@@ -0,0 +1,98 @@
+using System;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Chemistry.ReactionEffects
+{
+ ///
+ /// Sets the temperature of the solution involved with the reaction to a new value.
+ ///
+ [DataDefinition]
+ public class SetSolutionTemperatureEffect : ReagentEffect
+ {
+ ///
+ /// The temperature to set the solution to.
+ ///
+ [DataField("temperature", required: true)] private float _temperature;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ var solution = args.Source;
+ if (solution == null)
+ return;
+
+ solution.Temperature = _temperature;
+ }
+ }
+
+ ///
+ /// Adjusts the temperature of the solution involved in the reaction.
+ ///
+ [DataDefinition]
+ public class AdjustSolutionTemperatureEffect : ReagentEffect
+ {
+ ///
+ /// The total change in the thermal energy of the solution.
+ ///
+ [DataField("delta", required: true)] protected float Delta;
+
+ ///
+ /// The minimum temperature this effect can reach.
+ ///
+ [DataField("minTemp")] private float _minTemp = 0.0f;
+
+ ///
+ /// The maximum temperature this effect can reach.
+ ///
+ [DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity;
+
+ ///
+ /// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount.
+ ///
+ [DataField("scaled")] private bool _scaled;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected virtual float GetDeltaT(Solution solution) => Delta;
+
+ public override void Effect(ReagentEffectArgs args)
+ {
+ var solution = args.Source;
+ if (solution == null)
+ return;
+
+ var deltaT = GetDeltaT(solution);
+ if (_scaled)
+ deltaT = deltaT * (float) args.Quantity;
+
+ if (deltaT == 0.0d)
+ return;
+ if (deltaT > 0.0d && solution.Temperature >= _maxTemp)
+ return;
+ if (deltaT < 0.0d && solution.Temperature <= _minTemp)
+ return;
+
+ solution.Temperature = MathF.Max(MathF.Min(solution.Temperature + deltaT, _minTemp), _maxTemp);
+ }
+ }
+
+ ///
+ /// Adjusts the thermal energy of the solution involved in the reaction.
+ ///
+ public class AdjustSolutionThermalEnergyEffect : AdjustSolutionTemperatureEffect
+ {
+ protected override float GetDeltaT(Solution solution)
+ {
+ var heatCapacity = solution.HeatCapacity;
+ if (heatCapacity == 0.0f)
+ return 0.0f;
+ return Delta / heatCapacity;
+ }
+ }
+}
+
+
diff --git a/Content.Server/Chemistry/ReagentEffectConditions/SolutionTemperature.cs b/Content.Server/Chemistry/ReagentEffectConditions/SolutionTemperature.cs
new file mode 100644
index 0000000000..82c778b1f0
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffectConditions/SolutionTemperature.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Chemistry.ReagentEffectConditions
+{
+ ///
+ /// Requires the solution to be above or below a certain temperature.
+ /// Used for things like explosives.
+ ///
+ public class SolutionTemperature : ReagentEffectCondition
+ {
+ [DataField("min")]
+ public float Min = 0.0f;
+
+ [DataField("max")]
+ public float Max = float.PositiveInfinity;
+ public override bool Condition(ReagentEffectArgs args)
+ {
+ if (args.Source == null)
+ return false;
+ if (args.Source.Temperature < Min)
+ return false;
+ if (args.Source.Temperature > Max)
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Server/Chemistry/ReagentEffectConditions/SolutionThermalEnergy.cs b/Content.Server/Chemistry/ReagentEffectConditions/SolutionThermalEnergy.cs
new file mode 100644
index 0000000000..17dcf01aeb
--- /dev/null
+++ b/Content.Server/Chemistry/ReagentEffectConditions/SolutionThermalEnergy.cs
@@ -0,0 +1,30 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Reagent;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.Server.Chemistry.ReagentEffectConditions
+{
+ ///
+ /// Requires the solution to be above or below a certain thermal energy.
+ /// Used for things like explosives.
+ ///
+ public class SolutionThermalEnergy : ReagentEffectCondition
+ {
+ [DataField("min")]
+ public float Min = 0.0f;
+
+ [DataField("max")]
+ public float Max = float.PositiveInfinity;
+ public override bool Condition(ReagentEffectArgs args)
+ {
+ if (args.Source == null)
+ return false;
+ if (args.Source.ThermalEnergy < Min)
+ return false;
+ if (args.Source.ThermalEnergy > Max)
+ return false;
+
+ return true;
+ }
+ }
+}
diff --git a/Content.Shared/Chemistry/Components/Solution.Managerial.cs b/Content.Shared/Chemistry/Components/Solution.Managerial.cs
index 493c38f9a4..805f083187 100644
--- a/Content.Shared/Chemistry/Components/Solution.Managerial.cs
+++ b/Content.Shared/Chemistry/Components/Solution.Managerial.cs
@@ -1,5 +1,7 @@
-using Content.Shared.Chemistry.Reagent;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -48,5 +50,45 @@ namespace Content.Shared.Chemistry.Components
[ViewVariables]
public FixedPoint2 CurrentVolume => TotalVolume;
+
+ ///
+ /// The total heat capacity of all reagents in the solution.
+ ///
+ [ViewVariables]
+ public float HeatCapacity => GetHeatCapacity();
+
+ ///
+ /// The average specific heat of all reagents in the solution.
+ ///
+ [ViewVariables]
+ public float SpecificHeat => HeatCapacity / (float) TotalVolume;
+
+ ///
+ /// The total thermal energy of the reagents in the solution.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float ThermalEnergy {
+ get { return Temperature * HeatCapacity; }
+ set { Temperature = ((HeatCapacity == 0.0f) ? 0.0f : (value / HeatCapacity)); }
+ }
+
+ ///
+ /// Returns the total heat capacity of the reagents in this solution.
+ ///
+ /// The total heat capacity of the reagents in this solution.
+ private float GetHeatCapacity()
+ {
+ var heatCapacity = 0.0f;
+ var prototypeManager = IoCManager.Resolve();
+ foreach(var reagent in Contents)
+ {
+ if (!prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
+ proto = new ReagentPrototype();
+
+ heatCapacity += (float) reagent.Quantity * proto.SpecificHeat;
+ }
+
+ return heatCapacity;
+ }
}
}
diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs
index 2a34320ac1..3911ce094f 100644
--- a/Content.Shared/Chemistry/Components/Solution.cs
+++ b/Content.Shared/Chemistry/Components/Solution.cs
@@ -35,6 +35,13 @@ namespace Content.Shared.Chemistry.Components
[ViewVariables]
public FixedPoint2 TotalVolume { get; set; }
+ ///
+ /// The temperature of the reagents in the solution.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("temperature")]
+ public float Temperature { get; set; } = 293.15f;
+
public Color Color => GetColor();
///
@@ -94,11 +101,18 @@ namespace Content.Shared.Chemistry.Components
///
/// The prototype ID of the reagent to add.
/// The quantity in milli-units.
- public void AddReagent(string reagentId, FixedPoint2 quantity)
+ public void AddReagent(string reagentId, FixedPoint2 quantity, float? temperature = null)
{
if (quantity <= 0)
return;
+ if (!IoCManager.Resolve().TryIndex(reagentId, out ReagentPrototype? proto))
+ proto = new ReagentPrototype();
+ if (temperature == null)
+ temperature = Temperature;
+
+ var oldThermalEnergy = Temperature * GetHeatCapacity();
+ var addedThermalEnergy = (float) ((float) quantity * proto.SpecificHeat * temperature);
for (var i = 0; i < Contents.Count; i++)
{
var reagent = Contents[i];
@@ -106,12 +120,16 @@ namespace Content.Shared.Chemistry.Components
continue;
Contents[i] = new ReagentQuantity(reagentId, reagent.Quantity + quantity);
+
TotalVolume += quantity;
+ ThermalEnergy = oldThermalEnergy + addedThermalEnergy;
return;
}
Contents.Add(new ReagentQuantity(reagentId, quantity));
+
TotalVolume += quantity;
+ ThermalEnergy = oldThermalEnergy + addedThermalEnergy;
}
///
@@ -161,9 +179,10 @@ namespace Content.Shared.Chemistry.Components
var reagent = Contents[i];
if(reagent.ReagentId != reagentId)
continue;
+ if (!IoCManager.Resolve().TryIndex(reagentId, out ReagentPrototype? proto))
+ proto = new ReagentPrototype();
var curQuantity = reagent.Quantity;
-
var newQuantity = curQuantity - quantity;
if (newQuantity <= 0)
{
@@ -234,7 +253,9 @@ namespace Content.Shared.Chemistry.Components
newSolution = new Solution();
var newTotalVolume = FixedPoint2.New(0);
+ var newHeatCapacity = 0.0d;
var remainingVolume = TotalVolume;
+ var prototypeManager = IoCManager.Resolve();
for (var i = Contents.Count - 1; i >= 0; i--)
{
@@ -244,6 +265,9 @@ namespace Content.Shared.Chemistry.Components
var reagent = Contents[i];
var ratio = (remainingVolume - quantity).Double() / remainingVolume.Double();
+ if(!prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
+ proto = new ReagentPrototype();
+
remainingVolume -= reagent.Quantity;
var newQuantity = reagent.Quantity * ratio;
@@ -258,10 +282,12 @@ namespace Content.Shared.Chemistry.Components
newSolution.Contents.Add(new ReagentQuantity(reagent.ReagentId, splitQuantity));
newTotalVolume += splitQuantity;
+ newHeatCapacity += (float) splitQuantity * proto.SpecificHeat;
quantity -= splitQuantity;
}
newSolution.TotalVolume = newTotalVolume;
+ newSolution.Temperature = Temperature;
TotalVolume -= newTotalVolume;
return newSolution;
@@ -269,6 +295,8 @@ namespace Content.Shared.Chemistry.Components
public void AddSolution(Solution otherSolution)
{
+ var oldThermalEnergy = Temperature * GetHeatCapacity();
+ var addedThermalEnergy = otherSolution.Temperature * otherSolution.GetHeatCapacity();
for (var i = 0; i < otherSolution.Contents.Count; i++)
{
var otherReagent = otherSolution.Contents[i];
@@ -292,6 +320,7 @@ namespace Content.Shared.Chemistry.Components
}
TotalVolume += otherSolution.TotalVolume;
+ ThermalEnergy = oldThermalEnergy + addedThermalEnergy;
}
private Color GetColor()
@@ -329,16 +358,23 @@ namespace Content.Shared.Chemistry.Components
public Solution Clone()
{
var volume = FixedPoint2.New(0);
+ var heatCapacity = 0.0d;
var newSolution = new Solution();
+ var prototypeManager = IoCManager.Resolve();
for (var i = 0; i < Contents.Count; i++)
{
var reagent = Contents[i];
+ if (!prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
+ proto = new ReagentPrototype();
+
newSolution.Contents.Add(reagent);
volume += reagent.Quantity;
+ heatCapacity += (float) reagent.Quantity * proto.SpecificHeat;
}
newSolution.TotalVolume = volume;
+ newSolution.Temperature = Temperature;
return newSolution;
}
diff --git a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs
index 3369207d2c..c5a50a494e 100644
--- a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs
+++ b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs
@@ -30,6 +30,18 @@ namespace Content.Shared.Chemistry.Reaction
[DataField("reactants", customTypeSerializer:typeof(PrototypeIdDictionarySerializer))]
public Dictionary Reactants = new();
+ ///
+ /// The minimum temperature the reaction can occur at.
+ ///
+ [DataField("minTemp")]
+ public float MinimumTemperature = 0.0f;
+
+ ///
+ /// The maximum temperature the reaction can occur at.
+ ///
+ [DataField("maxTemp")]
+ public float MaximumTemperature = float.PositiveInfinity;
+
///
/// Reagents created when the reaction occurs.
///
diff --git a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs
index 7ae48dd52b..cd5499167a 100644
--- a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs
+++ b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs
@@ -109,6 +109,15 @@ namespace Content.Shared.Chemistry.Reaction
private static bool CanReact(Solution solution, ReactionPrototype reaction, out FixedPoint2 lowestUnitReactions)
{
lowestUnitReactions = FixedPoint2.MaxValue;
+ if (solution.Temperature < reaction.MinimumTemperature)
+ {
+ lowestUnitReactions = FixedPoint2.Zero;
+ return false;
+ } else if(solution.Temperature > reaction.MaximumTemperature)
+ {
+ lowestUnitReactions = FixedPoint2.Zero;
+ return false;
+ }
foreach (var reactantData in reaction.Reactants)
{
@@ -202,7 +211,7 @@ namespace Content.Shared.Chemistry.Reaction
/// Removes the reactants from the solution, then returns a solution with all products.
/// WARNING: Does not trigger reactions between solution and new products.
///
- private bool ProcessReactions(Solution solution, EntityUid Owner, [MaybeNullWhen(false)] out Solution productSolution)
+ private bool ProcessReactions(Solution solution, EntityUid owner, [MaybeNullWhen(false)] out Solution productSolution)
{
foreach(var reactant in solution.Contents)
{
@@ -214,7 +223,7 @@ namespace Content.Shared.Chemistry.Reaction
if (!CanReact(solution, reaction, out var unitReactions))
continue;
- productSolution = PerformReaction(solution, Owner, reaction, unitReactions);
+ productSolution = PerformReaction(solution, owner, reaction, unitReactions);
return true;
}
}
diff --git a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs
index 5a008a3eaf..42be3db8e1 100644
--- a/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs
+++ b/Content.Shared/Chemistry/Reagent/ReagentPrototype.cs
@@ -46,6 +46,13 @@ namespace Content.Shared.Chemistry.Reagent
[DataField("color")]
public Color SubstanceColor { get; } = Color.White;
+ ///
+ /// The specific heat of the reagent.
+ /// How much energy it takes to heat one unit of this reagent by one Kelvin.
+ ///
+ [DataField("specificHeat")]
+ public float SpecificHeat { get; } = 1.0f;
+
[DataField("boilingPoint")]
public float? BoilingPoint { get; }
diff --git a/Content.Shared/Localizations/Units.cs b/Content.Shared/Localizations/Units.cs
index c998e4ecfe..e07e824a5e 100644
--- a/Content.Shared/Localizations/Units.cs
+++ b/Content.Shared/Localizations/Units.cs
@@ -112,12 +112,22 @@ namespace Content.Shared.Localizations
public static readonly TypeTable Energy = new TypeTable
(
- new TypeTable.Entry(range: (null, 1e-3), factor: 1e6, unit: "u--joule"),
- new TypeTable.Entry(range: (1e-3, 1), factor: 1e3, unit: "m--joule"),
- new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "joule"),
- new TypeTable.Entry(range: (1000, 1e6), factor: 1e-4, unit: "k-joule"),
- new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "m-joule"),
- new TypeTable.Entry(range: ( 1e9, null), factor: 1e-9, unit: "g-joule")
+ new TypeTable.Entry(range: ( null, 1e-3), factor: 1e6, unit: "u--joule"),
+ new TypeTable.Entry(range: ( 1e-3, 1), factor: 1e3, unit: "m--joule"),
+ new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "joule"),
+ new TypeTable.Entry(range: ( 1000, 1e6), factor: 1e-4, unit: "k-joule"),
+ new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "m-joule"),
+ new TypeTable.Entry(range: ( 1e9, null), factor: 1e-9, unit: "g-joule")
+ );
+
+ public static readonly TypeTable Temperature = new TypeTable
+ (
+ new TypeTable.Entry(range: ( null, 1e-3), factor: 1e6, unit: "u--kelvin"),
+ new TypeTable.Entry(range: ( 1e-3, 1), factor: 1e3, unit: "m--kelvin"),
+ new TypeTable.Entry(range: ( 1, 1e3), factor: 1, unit: "kelvin"),
+ new TypeTable.Entry(range: ( 1e3, 1e6), factor: 1e-3, unit: "k-kelvin"),
+ new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "m-kelvin"),
+ new TypeTable.Entry(range: ( 1e9, null), factor: 1e-9, unit: "g-kelvin")
);
public readonly static Dictionary Types = new Dictionary
@@ -125,7 +135,8 @@ namespace Content.Shared.Localizations
["generic"] = Generic!,
["pressure"] = Pressure!,
["power"] = Power!,
- ["energy"] = Energy!
+ ["energy"] = Energy!,
+ ["temperature"] = Temperature!
};
}
}
diff --git a/Content.Tests/Shared/Chemistry/Solution_Tests.cs b/Content.Tests/Shared/Chemistry/Solution_Tests.cs
index 9e8969d604..7caadfb6bc 100644
--- a/Content.Tests/Shared/Chemistry/Solution_Tests.cs
+++ b/Content.Tests/Shared/Chemistry/Solution_Tests.cs
@@ -1,13 +1,21 @@
-using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
using NUnit.Framework;
namespace Content.Tests.Shared.Chemistry
{
[TestFixture, Parallelizable, TestOf(typeof(Solution))]
- public class Solution_Tests
+ public class Solution_Tests : ContentUnitTest
{
+ [OneTimeSetUp]
+ public void Setup()
+ {
+ IoCManager.Resolve().Initialize();
+ }
+
[Test]
public void AddReagentAndGetSolution()
{
@@ -343,5 +351,134 @@ namespace Content.Tests.Shared.Chemistry
Assert.That(solutionOne.GetReagentQuantity("earth").Int(), Is.EqualTo(1000));
Assert.That(solutionOne.TotalVolume.Int(), Is.EqualTo(4500));
}
+
+ // Tests concerning thermal energy and temperature.
+ #region Thermal Energy and Temperature
+
+ [Test]
+ public void EmptySolutionHasNoHeatCapacity()
+ {
+ var solution = new Solution();
+ Assert.That(solution.HeatCapacity, Is.EqualTo(0.0f));
+ }
+
+ [Test]
+ public void EmptySolutionHasNoThermalEnergy()
+ {
+ var solution = new Solution();
+ Assert.That(solution.ThermalEnergy, Is.EqualTo(0.0f));
+ }
+
+ [Test]
+ public void AddReagentToEmptySolutionSetsTemperature()
+ {
+ const float testTemp = 100.0f;
+
+ var solution = new Solution();
+ solution.AddReagent("water", FixedPoint2.New(100), testTemp);
+ Assert.That(solution.Temperature, Is.EqualTo(testTemp));
+ }
+
+ [Test]
+ public void AddReagentWithNullTemperatureDoesNotEffectTemperature()
+ {
+ const float initialTemp = 100.0f;
+
+ var solution = new Solution();
+ solution.AddReagent("water", FixedPoint2.New(100), initialTemp);
+
+ solution.AddReagent("water", FixedPoint2.New(100));
+ Assert.That(solution.Temperature, Is.EqualTo(initialTemp));
+
+ solution.AddReagent("earth", FixedPoint2.New(100));
+ Assert.That(solution.Temperature, Is.EqualTo(initialTemp));
+ }
+
+ [Test]
+ public void AddSolutionWithEqualTemperatureDoesNotChangeTemperature()
+ {
+ const float initialTemp = 100.0f;
+
+ var solutionOne = new Solution();
+ solutionOne.AddReagent("water", FixedPoint2.New(100));
+ solutionOne.Temperature = initialTemp;
+
+ var solutionTwo = new Solution();
+ solutionTwo.AddReagent("water", FixedPoint2.New(100));
+ solutionTwo.AddReagent("earth", FixedPoint2.New(100));
+ solutionTwo.Temperature = initialTemp;
+
+ solutionOne.AddSolution(solutionTwo);
+ Assert.That(solutionOne.Temperature, Is.EqualTo(initialTemp));
+ }
+
+ [Test]
+ public void RemoveReagentDoesNotEffectTemperature()
+ {
+ const float initialTemp = 100.0f;
+
+ var solution = new Solution();
+ solution.AddReagent("water", FixedPoint2.New(100), initialTemp);
+ solution.RemoveReagent("water", FixedPoint2.New(50));
+ Assert.That(solution.Temperature, Is.EqualTo(initialTemp));
+ }
+
+ [Test]
+ public void RemoveSolutionDoesNotEffectTemperature()
+ {
+ const float initialTemp = 100.0f;
+
+ var solution = new Solution();
+ solution.AddReagent("water", FixedPoint2.New(100), initialTemp);
+ solution.RemoveSolution(FixedPoint2.New(50));
+ Assert.That(solution.Temperature, Is.EqualTo(initialTemp));
+ }
+
+ [Test]
+ public void SplitSolutionDoesNotEffectTemperature()
+ {
+ const float initialTemp = 100.0f;
+
+ var solution = new Solution();
+ solution.AddReagent("water", FixedPoint2.New(100), initialTemp);
+ solution.SplitSolution(FixedPoint2.New(50));
+ Assert.That(solution.Temperature, Is.EqualTo(initialTemp));
+ }
+
+ [Test]
+ public void AddReagentWithSetTemperatureAdjustsTemperature()
+ {
+ const float temp = 100.0f;
+
+ var solution = new Solution();
+ solution.AddReagent("water", FixedPoint2.New(100), temp * 1);
+ Assert.That(solution.Temperature, Is.EqualTo(temp * 1));
+
+ solution.AddReagent("water", FixedPoint2.New(100), temp * 3);
+ Assert.That(solution.Temperature, Is.EqualTo(temp * 2));
+
+ solution.AddReagent("earth", FixedPoint2.New(100), temp * 5);
+ Assert.That(solution.Temperature, Is.EqualTo(temp * 3));
+ }
+
+ [Test]
+ public void AddSolutionCombinesThermalEnergy()
+ {
+ const float initialTemp = 100.0f;
+
+ var solutionOne = new Solution();
+ solutionOne.AddReagent("water", FixedPoint2.New(100), initialTemp);
+
+ var solutionTwo = new Solution();
+ solutionTwo.AddReagent("water", FixedPoint2.New(100), initialTemp);
+ solutionTwo.AddReagent("earth", FixedPoint2.New(100));
+
+ var thermalEnergyOne = solutionOne.ThermalEnergy;
+ var thermalEnergyTwo = solutionTwo.ThermalEnergy;
+ solutionOne.AddSolution(solutionTwo);
+ Assert.That(solutionOne.ThermalEnergy, Is.EqualTo(thermalEnergyOne + thermalEnergyTwo));
+ }
+
+ #endregion Thermal Energy and Temperature
}
}
diff --git a/Resources/Locale/en-US/_units.ftl b/Resources/Locale/en-US/_units.ftl
index 1e687a05d4..c8a2d649ef 100644
--- a/Resources/Locale/en-US/_units.ftl
+++ b/Resources/Locale/en-US/_units.ftl
@@ -78,3 +78,18 @@ units-m--joule-long = Millijoule
units-joule-long = Joule
units-k-joule-long = Kilojoule
units-m-joule-long = Megajoule
+
+## Kelvin (Temperature)
+units-u--kelvin = µK
+units-m--kelvin = mK
+units-kelvin = K
+units-k-kelvin = kK
+units-m-kelvin = MK
+units-g-kelvin = GK
+
+units-u--kelvin-long = Microkelvin
+units-m--kelvin-long = Millikelvin
+units-kelvin-long = Kelvin
+units-k-kelvin-long = Kilokelvin
+units-m-kelvin-long = Megakelvin
+units-g-kelvin-long = Gigakelvin
diff --git a/Resources/Locale/en-US/administration/ui/manage-solutions/manage-solutions.ftl b/Resources/Locale/en-US/administration/ui/manage-solutions/manage-solutions.ftl
index 2fac32cf9c..d180babc5c 100644
--- a/Resources/Locale/en-US/administration/ui/manage-solutions/manage-solutions.ftl
+++ b/Resources/Locale/en-US/administration/ui/manage-solutions/manage-solutions.ftl
@@ -1,4 +1,9 @@
admin-solutions-window-title = Solution Editor - {$targetName}
admin-solutions-window-solution-label = Target solution:
admin-solutions-window-add-new-button = Add new reagent
-admin-solutions-window-capacity-label = Capacity {$currentVolume}/{$maxVolume}u
+admin-solutions-window-volume-label = Volume {$currentVolume}/{$maxVolume}u
+admin-solutions-window-capacity-label = Capacity (u):
+admin-solutions-window-specific-heat-label = Specific Heat: {$specificHeat} J/(K*u)
+admin-solutions-window-heat-capacity-label = Heat Capacity: {$heatCapacity} J/K
+admin-solutions-window-temperature-label = Temperature (K):
+admin-solutions-window-thermal-energy-label = Thermal Energy (J):