Adds temperature to solutions (#5834)

This commit is contained in:
TemporalOroboros
2021-12-24 01:22:34 -08:00
committed by GitHub
parent c94f93732b
commit 201952e618
18 changed files with 858 additions and 21 deletions

View File

@@ -7,7 +7,14 @@
<Label Text ="{Loc 'admin-solutions-window-solution-label'}" Margin="0 0 10 0"/> <Label Text ="{Loc 'admin-solutions-window-solution-label'}" Margin="0 0 10 0"/>
<OptionButton Name="SolutionOption" HorizontalExpand="True"/> <OptionButton Name="SolutionOption" HorizontalExpand="True"/>
</BoxContainer> </BoxContainer>
<Label Name="TotalLabel" HorizontalExpand="True" Margin="0 4"/>
<!-- The total volume / capacity of the solution -->
<BoxContainer Name="VolumeBox" Orientation="Vertical" HorizontalExpand="True" Margin="0 4"/>
<!-- The temperature / heat capacity / thermal energy of the solution -->
<BoxContainer Name="ThermalBox" Orientation="Vertical" HorizontalExpand="True" Margin="0 4"/>
<!-- The reagents in the solution -->
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="0 4"> <ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="0 4">
<BoxContainer Name="ReagentList" Orientation="Vertical"/> <BoxContainer Name="ReagentList" Orientation="Vertical"/>
</ScrollContainer> </ScrollContainer>

View File

@@ -9,6 +9,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Maths;
namespace Content.Client.Administration.UI.ManageSolutions namespace Content.Client.Administration.UI.ManageSolutions
{ {
@@ -66,9 +67,8 @@ namespace Content.Client.Administration.UI.ManageSolutions
if (!_solutions.TryGetValue(_selectedSolution, out var solution)) if (!_solutions.TryGetValue(_selectedSolution, out var solution))
return; return;
TotalLabel.Text = Loc.GetString("admin-solutions-window-capacity-label", UpdateVolumeBox(solution);
("currentVolume", solution.TotalVolume), UpdateThermalBox(solution);
("maxVolume",solution.MaxVolume));
foreach (var reagent in solution) foreach (var reagent in solution)
{ {
@@ -76,6 +76,109 @@ namespace Content.Client.Administration.UI.ManageSolutions
} }
} }
/// <summary>
/// Updates the entry displaying the current and maximum volume of the selected solution.
/// </summary>
/// <param name="solution">The selected solution.</param>
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);
}
/// <summary>
/// Updates the entry displaying the current specific heat, heat capacity, temperature, and thermal energy
/// of the selected solution.
/// </summary>
/// <param name="solution">The selected solution.</param>
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);
}
/// <summary> /// <summary>
/// Add a single reagent entry to the list /// Add a single reagent entry to the list
/// </summary> /// </summary>
@@ -112,6 +215,41 @@ namespace Content.Client.Administration.UI.ManageSolutions
_consoleHost.ExecuteCommand(command); _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);
}
/// <summary>
/// Sets the temperature of the selected solution to a value.
/// </summary>
/// <param name="args">An argument struct containing the value to set the temperature to.</param>
private void SetTemperature(FloatSpinBox.FloatSpinBoxEventArgs args)
{
if (_solutions == null || _selectedSolution == null)
return;
var command = $"setsolutiontemperature {_target} {_selectedSolution} {args.Value}";
_consoleHost.ExecuteCommand(command);
}
/// <summary>
/// Sets the thermal energy of the selected solution to a value.
/// </summary>
/// <param name="args">An argument struct containing the value to set the thermal energy to.</param>
private void SetThermalEnergy(FloatSpinBox.FloatSpinBoxEventArgs args)
{
if (_solutions == null || _selectedSolution == null)
return;
var command = $"setsolutionthermalenergy {_target} {_selectedSolution} {args.Value}";
_consoleHost.ExecuteCommand(command);
}
/// <summary> /// <summary>
/// Open a new window that has options to add new reagents to the solution. /// Open a new window that has options to add new reagents to the solution.
/// </summary> /// </summary>

View File

@@ -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} <target> <solution> <new capacity>";
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<IEntityManager>().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<SolutionContainerSystem>().SetCapacity(uid, solution, quantity);
}
}
}

View File

@@ -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} <target> <solution> <new temperature>";
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<IEntityManager>().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<SolutionContainerSystem>().SetTemperature(uid, solution, quantity);
}
}
}

View File

@@ -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} <target> <solution> <new thermal energy>";
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<IEntityManager>().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<SolutionContainerSystem>().SetThermalEnergy(uid, solution, quantity);
}
}
}

View File

@@ -153,6 +153,24 @@ namespace Content.Server.Chemistry.EntitySystems
} }
} }
/// <summary>
/// Sets the capacity (maximum volume) of a solution to a new value.
/// </summary>
/// <param name="targetUid">The entity containing the solution.</param>
/// <param name="targetSolution">The solution to set the capacity of.</param>
/// <param name="capacity">The value to set the capacity of the solution to.</param>
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);
}
/// <summary> /// <summary>
/// Adds reagent of an Id to the container. /// Adds reagent of an Id to the container.
/// </summary> /// </summary>
@@ -163,10 +181,10 @@ namespace Content.Server.Chemistry.EntitySystems
/// <param name="acceptedQuantity">The amount of reagent successfully added.</param> /// <param name="acceptedQuantity">The amount of reagent successfully added.</param>
/// <returns>If all the reagent could be added.</returns> /// <returns>If all the reagent could be added.</returns>
public bool TryAddReagent(EntityUid targetUid, Solution targetSolution, string reagentId, FixedPoint2 quantity, 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; acceptedQuantity = targetSolution.AvailableVolume > quantity ? quantity : targetSolution.AvailableVolume;
targetSolution.AddReagent(reagentId, acceptedQuantity); targetSolution.AddReagent(reagentId, acceptedQuantity, temperature);
if (acceptedQuantity > 0) if (acceptedQuantity > 0)
UpdateChemicals(targetUid, targetSolution, true); UpdateChemicals(targetUid, targetSolution, true);
@@ -257,6 +275,8 @@ namespace Content.Server.Chemistry.EntitySystems
{ {
var (reagentId, curQuantity) = solution.Contents[i]; var (reagentId, curQuantity) = solution.Contents[i];
removedReagent[pos++] = reagentId; removedReagent[pos++] = reagentId;
if (!_prototypeManager.TryIndex(reagentId, out ReagentPrototype? proto))
proto = new ReagentPrototype();
var newQuantity = curQuantity - quantity; var newQuantity = curQuantity - quantity;
if (newQuantity <= 0) if (newQuantity <= 0)
@@ -302,5 +322,56 @@ namespace Content.Server.Chemistry.EntitySystems
return reagentQuantity; return reagentQuantity;
} }
// Thermal energy and temperature management.
#region Thermal Energy and Temperature
/// <summary>
/// Sets the temperature of a solution to a new value and then checks for reaction processing.
/// </summary>
/// <param name="owner">The entity in which the solution is located.</param>
/// <param name="solution">The solution to set the temperature of.</param>
/// <param name="temperature">The new value to set the temperature to.</param>
public void SetTemperature(EntityUid owner, Solution solution, float temperature)
{
if (temperature == solution.Temperature)
return;
solution.Temperature = temperature;
UpdateChemicals(owner, solution, true);
}
/// <summary>
/// Sets the thermal energy of a solution to a new value and then checks for reaction processing.
/// </summary>
/// <param name="owner">The entity in which the solution is located.</param>
/// <param name="solution">The solution to set the thermal energy of.</param>
/// <param name="thermalEnergy">The new value to set the thermal energy to.</param>
public void SetThermalEnergy(EntityUid owner, Solution solution, float thermalEnergy)
{
if (thermalEnergy == solution.ThermalEnergy)
return;
solution.ThermalEnergy = thermalEnergy;
UpdateChemicals(owner, solution, true);
}
/// <summary>
/// Adds some thermal energy to a solution and then checks for reaction processing.
/// </summary>
/// <param name="owner">The entity in which the solution is located.</param>
/// <param name="solution">The solution to set the thermal energy of.</param>
/// <param name="thermalEnergy">The new value to set the thermal energy to.</param>
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
} }
} }

View File

@@ -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
{
/// <summary>
/// Sets the temperature of the solution involved with the reaction to a new value.
/// </summary>
[DataDefinition]
public class SetSolutionTemperatureEffect : ReagentEffect
{
/// <summary>
/// The temperature to set the solution to.
/// </summary>
[DataField("temperature", required: true)] private float _temperature;
public override void Effect(ReagentEffectArgs args)
{
var solution = args.Source;
if (solution == null)
return;
solution.Temperature = _temperature;
}
}
/// <summary>
/// Adjusts the temperature of the solution involved in the reaction.
/// </summary>
[DataDefinition]
public class AdjustSolutionTemperatureEffect : ReagentEffect
{
/// <summary>
/// The total change in the thermal energy of the solution.
/// </summary>
[DataField("delta", required: true)] protected float Delta;
/// <summary>
/// The minimum temperature this effect can reach.
/// </summary>
[DataField("minTemp")] private float _minTemp = 0.0f;
/// <summary>
/// The maximum temperature this effect can reach.
/// </summary>
[DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity;
/// <summary>
/// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount.
/// </summary>
[DataField("scaled")] private bool _scaled;
/// <summary>
///
/// </summary>
/// <param name="solution"></param>
/// <returns></returns>
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);
}
}
/// <summary>
/// Adjusts the thermal energy of the solution involved in the reaction.
/// </summary>
public class AdjustSolutionThermalEnergyEffect : AdjustSolutionTemperatureEffect
{
protected override float GetDeltaT(Solution solution)
{
var heatCapacity = solution.HeatCapacity;
if (heatCapacity == 0.0f)
return 0.0f;
return Delta / heatCapacity;
}
}
}

View File

@@ -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
{
/// <summary>
/// Requires the solution to be above or below a certain temperature.
/// Used for things like explosives.
/// </summary>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Requires the solution to be above or below a certain thermal energy.
/// Used for things like explosives.
/// </summary>
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;
}
}
}

View File

@@ -1,5 +1,7 @@
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
@@ -48,5 +50,45 @@ namespace Content.Shared.Chemistry.Components
[ViewVariables] [ViewVariables]
public FixedPoint2 CurrentVolume => TotalVolume; public FixedPoint2 CurrentVolume => TotalVolume;
/// <summary>
/// The total heat capacity of all reagents in the solution.
/// </summary>
[ViewVariables]
public float HeatCapacity => GetHeatCapacity();
/// <summary>
/// The average specific heat of all reagents in the solution.
/// </summary>
[ViewVariables]
public float SpecificHeat => HeatCapacity / (float) TotalVolume;
/// <summary>
/// The total thermal energy of the reagents in the solution.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float ThermalEnergy {
get { return Temperature * HeatCapacity; }
set { Temperature = ((HeatCapacity == 0.0f) ? 0.0f : (value / HeatCapacity)); }
}
/// <summary>
/// Returns the total heat capacity of the reagents in this solution.
/// </summary>
/// <returns>The total heat capacity of the reagents in this solution.</returns>
private float GetHeatCapacity()
{
var heatCapacity = 0.0f;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
foreach(var reagent in Contents)
{
if (!prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
proto = new ReagentPrototype();
heatCapacity += (float) reagent.Quantity * proto.SpecificHeat;
}
return heatCapacity;
}
} }
} }

View File

@@ -35,6 +35,13 @@ namespace Content.Shared.Chemistry.Components
[ViewVariables] [ViewVariables]
public FixedPoint2 TotalVolume { get; set; } public FixedPoint2 TotalVolume { get; set; }
/// <summary>
/// The temperature of the reagents in the solution.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("temperature")]
public float Temperature { get; set; } = 293.15f;
public Color Color => GetColor(); public Color Color => GetColor();
/// <summary> /// <summary>
@@ -94,11 +101,18 @@ namespace Content.Shared.Chemistry.Components
/// </summary> /// </summary>
/// <param name="reagentId">The prototype ID of the reagent to add.</param> /// <param name="reagentId">The prototype ID of the reagent to add.</param>
/// <param name="quantity">The quantity in milli-units.</param> /// <param name="quantity">The quantity in milli-units.</param>
public void AddReagent(string reagentId, FixedPoint2 quantity) public void AddReagent(string reagentId, FixedPoint2 quantity, float? temperature = null)
{ {
if (quantity <= 0) if (quantity <= 0)
return; return;
if (!IoCManager.Resolve<IPrototypeManager>().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++) for (var i = 0; i < Contents.Count; i++)
{ {
var reagent = Contents[i]; var reagent = Contents[i];
@@ -106,12 +120,16 @@ namespace Content.Shared.Chemistry.Components
continue; continue;
Contents[i] = new ReagentQuantity(reagentId, reagent.Quantity + quantity); Contents[i] = new ReagentQuantity(reagentId, reagent.Quantity + quantity);
TotalVolume += quantity; TotalVolume += quantity;
ThermalEnergy = oldThermalEnergy + addedThermalEnergy;
return; return;
} }
Contents.Add(new ReagentQuantity(reagentId, quantity)); Contents.Add(new ReagentQuantity(reagentId, quantity));
TotalVolume += quantity; TotalVolume += quantity;
ThermalEnergy = oldThermalEnergy + addedThermalEnergy;
} }
/// <summary> /// <summary>
@@ -161,9 +179,10 @@ namespace Content.Shared.Chemistry.Components
var reagent = Contents[i]; var reagent = Contents[i];
if(reagent.ReagentId != reagentId) if(reagent.ReagentId != reagentId)
continue; continue;
if (!IoCManager.Resolve<IPrototypeManager>().TryIndex(reagentId, out ReagentPrototype? proto))
proto = new ReagentPrototype();
var curQuantity = reagent.Quantity; var curQuantity = reagent.Quantity;
var newQuantity = curQuantity - quantity; var newQuantity = curQuantity - quantity;
if (newQuantity <= 0) if (newQuantity <= 0)
{ {
@@ -234,7 +253,9 @@ namespace Content.Shared.Chemistry.Components
newSolution = new Solution(); newSolution = new Solution();
var newTotalVolume = FixedPoint2.New(0); var newTotalVolume = FixedPoint2.New(0);
var newHeatCapacity = 0.0d;
var remainingVolume = TotalVolume; var remainingVolume = TotalVolume;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
for (var i = Contents.Count - 1; i >= 0; i--) for (var i = Contents.Count - 1; i >= 0; i--)
{ {
@@ -244,6 +265,9 @@ namespace Content.Shared.Chemistry.Components
var reagent = Contents[i]; var reagent = Contents[i];
var ratio = (remainingVolume - quantity).Double() / remainingVolume.Double(); var ratio = (remainingVolume - quantity).Double() / remainingVolume.Double();
if(!prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
proto = new ReagentPrototype();
remainingVolume -= reagent.Quantity; remainingVolume -= reagent.Quantity;
var newQuantity = reagent.Quantity * ratio; var newQuantity = reagent.Quantity * ratio;
@@ -258,10 +282,12 @@ namespace Content.Shared.Chemistry.Components
newSolution.Contents.Add(new ReagentQuantity(reagent.ReagentId, splitQuantity)); newSolution.Contents.Add(new ReagentQuantity(reagent.ReagentId, splitQuantity));
newTotalVolume += splitQuantity; newTotalVolume += splitQuantity;
newHeatCapacity += (float) splitQuantity * proto.SpecificHeat;
quantity -= splitQuantity; quantity -= splitQuantity;
} }
newSolution.TotalVolume = newTotalVolume; newSolution.TotalVolume = newTotalVolume;
newSolution.Temperature = Temperature;
TotalVolume -= newTotalVolume; TotalVolume -= newTotalVolume;
return newSolution; return newSolution;
@@ -269,6 +295,8 @@ namespace Content.Shared.Chemistry.Components
public void AddSolution(Solution otherSolution) public void AddSolution(Solution otherSolution)
{ {
var oldThermalEnergy = Temperature * GetHeatCapacity();
var addedThermalEnergy = otherSolution.Temperature * otherSolution.GetHeatCapacity();
for (var i = 0; i < otherSolution.Contents.Count; i++) for (var i = 0; i < otherSolution.Contents.Count; i++)
{ {
var otherReagent = otherSolution.Contents[i]; var otherReagent = otherSolution.Contents[i];
@@ -292,6 +320,7 @@ namespace Content.Shared.Chemistry.Components
} }
TotalVolume += otherSolution.TotalVolume; TotalVolume += otherSolution.TotalVolume;
ThermalEnergy = oldThermalEnergy + addedThermalEnergy;
} }
private Color GetColor() private Color GetColor()
@@ -329,16 +358,23 @@ namespace Content.Shared.Chemistry.Components
public Solution Clone() public Solution Clone()
{ {
var volume = FixedPoint2.New(0); var volume = FixedPoint2.New(0);
var heatCapacity = 0.0d;
var newSolution = new Solution(); var newSolution = new Solution();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
for (var i = 0; i < Contents.Count; i++) for (var i = 0; i < Contents.Count; i++)
{ {
var reagent = Contents[i]; var reagent = Contents[i];
if (!prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype? proto))
proto = new ReagentPrototype();
newSolution.Contents.Add(reagent); newSolution.Contents.Add(reagent);
volume += reagent.Quantity; volume += reagent.Quantity;
heatCapacity += (float) reagent.Quantity * proto.SpecificHeat;
} }
newSolution.TotalVolume = volume; newSolution.TotalVolume = volume;
newSolution.Temperature = Temperature;
return newSolution; return newSolution;
} }

View File

@@ -30,6 +30,18 @@ namespace Content.Shared.Chemistry.Reaction
[DataField("reactants", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<ReactantPrototype, ReagentPrototype>))] [DataField("reactants", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<ReactantPrototype, ReagentPrototype>))]
public Dictionary<string, ReactantPrototype> Reactants = new(); public Dictionary<string, ReactantPrototype> Reactants = new();
/// <summary>
/// The minimum temperature the reaction can occur at.
/// </summary>
[DataField("minTemp")]
public float MinimumTemperature = 0.0f;
/// <summary>
/// The maximum temperature the reaction can occur at.
/// </summary>
[DataField("maxTemp")]
public float MaximumTemperature = float.PositiveInfinity;
/// <summary> /// <summary>
/// Reagents created when the reaction occurs. /// Reagents created when the reaction occurs.
/// </summary> /// </summary>

View File

@@ -109,6 +109,15 @@ namespace Content.Shared.Chemistry.Reaction
private static bool CanReact(Solution solution, ReactionPrototype reaction, out FixedPoint2 lowestUnitReactions) private static bool CanReact(Solution solution, ReactionPrototype reaction, out FixedPoint2 lowestUnitReactions)
{ {
lowestUnitReactions = FixedPoint2.MaxValue; 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) 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. /// Removes the reactants from the solution, then returns a solution with all products.
/// WARNING: Does not trigger reactions between solution and new products. /// WARNING: Does not trigger reactions between solution and new products.
/// </summary> /// </summary>
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) foreach(var reactant in solution.Contents)
{ {
@@ -214,7 +223,7 @@ namespace Content.Shared.Chemistry.Reaction
if (!CanReact(solution, reaction, out var unitReactions)) if (!CanReact(solution, reaction, out var unitReactions))
continue; continue;
productSolution = PerformReaction(solution, Owner, reaction, unitReactions); productSolution = PerformReaction(solution, owner, reaction, unitReactions);
return true; return true;
} }
} }

View File

@@ -46,6 +46,13 @@ namespace Content.Shared.Chemistry.Reagent
[DataField("color")] [DataField("color")]
public Color SubstanceColor { get; } = Color.White; public Color SubstanceColor { get; } = Color.White;
/// <summary>
/// The specific heat of the reagent.
/// How much energy it takes to heat one unit of this reagent by one Kelvin.
/// </summary>
[DataField("specificHeat")]
public float SpecificHeat { get; } = 1.0f;
[DataField("boilingPoint")] [DataField("boilingPoint")]
public float? BoilingPoint { get; } public float? BoilingPoint { get; }

View File

@@ -112,12 +112,22 @@ namespace Content.Shared.Localizations
public static readonly TypeTable Energy = new TypeTable public static readonly TypeTable Energy = new TypeTable
( (
new TypeTable.Entry(range: (null, 1e-3), factor: 1e6, unit: "u--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: ( 1e-3, 1), factor: 1e3, unit: "m--joule"),
new TypeTable.Entry(range: ( 1, 1000), factor: 1, unit: "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: ( 1000, 1e6), factor: 1e-4, unit: "k-joule"),
new TypeTable.Entry(range: ( 1e6, 1e9), factor: 1e-6, unit: "m-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: ( 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<string, TypeTable> Types = new Dictionary<string, TypeTable> public readonly static Dictionary<string, TypeTable> Types = new Dictionary<string, TypeTable>
@@ -125,7 +135,8 @@ namespace Content.Shared.Localizations
["generic"] = Generic!, ["generic"] = Generic!,
["pressure"] = Pressure!, ["pressure"] = Pressure!,
["power"] = Power!, ["power"] = Power!,
["energy"] = Energy! ["energy"] = Energy!,
["temperature"] = Temperature!
}; };
} }
} }

View File

@@ -1,13 +1,21 @@
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes;
using NUnit.Framework; using NUnit.Framework;
namespace Content.Tests.Shared.Chemistry namespace Content.Tests.Shared.Chemistry
{ {
[TestFixture, Parallelizable, TestOf(typeof(Solution))] [TestFixture, Parallelizable, TestOf(typeof(Solution))]
public class Solution_Tests public class Solution_Tests : ContentUnitTest
{ {
[OneTimeSetUp]
public void Setup()
{
IoCManager.Resolve<IPrototypeManager>().Initialize();
}
[Test] [Test]
public void AddReagentAndGetSolution() public void AddReagentAndGetSolution()
{ {
@@ -343,5 +351,134 @@ namespace Content.Tests.Shared.Chemistry
Assert.That(solutionOne.GetReagentQuantity("earth").Int(), Is.EqualTo(1000)); Assert.That(solutionOne.GetReagentQuantity("earth").Int(), Is.EqualTo(1000));
Assert.That(solutionOne.TotalVolume.Int(), Is.EqualTo(4500)); 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
} }
} }

View File

@@ -78,3 +78,18 @@ units-m--joule-long = Millijoule
units-joule-long = Joule units-joule-long = Joule
units-k-joule-long = Kilojoule units-k-joule-long = Kilojoule
units-m-joule-long = Megajoule 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

View File

@@ -1,4 +1,9 @@
admin-solutions-window-title = Solution Editor - {$targetName} admin-solutions-window-title = Solution Editor - {$targetName}
admin-solutions-window-solution-label = Target solution: admin-solutions-window-solution-label = Target solution:
admin-solutions-window-add-new-button = Add new reagent 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):