* Add test for two chemistry issues 1. rounding issue with reaction processing when making chloral hydrate 2. reliable assert trip due to the ValidateSolution() heat capacity issue. * Fix FixedPoint2 arithmetic Fix internal floating point arithmetic in places where it could be avoided. Fix incorrect rounding mode used in other places (it should always floor, like regular int arithmetic). I had to add an explicit epsilon value for float -> FixedPoint2 because something like 1.05 is actually like 1.04999 and that'd cause it to be rounded down to 1.04. This fixes reaction reagent processing in cases where the reagent inputs can't cleanly divide. Previously, when making 30u chloral hydrate by adding the chlorine in 10u increments you'd end up with 0.04 chlorine left over. This was caused by division in the reaction code rounding up in some cases. Changing division here to always round down fixes it. * Attempt to fix heat capacity precision assert issues. Fixes #22126 First, we just increase the tolerance of the assert. It was way too low. Second, actually put a cap on float drift from one-off _heatCapacity changes. * Fix float -> FixedPoint2 epsilon for negative number, fix tests. * Fix DamageableTest * Oh yeah I need to call CleanReturnAsync
908 lines
30 KiB
C#
908 lines
30 KiB
C#
using Content.Shared.Chemistry.Reagent;
|
|
using Content.Shared.FixedPoint;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Utility;
|
|
using System.Collections;
|
|
using System.Linq;
|
|
|
|
namespace Content.Shared.Chemistry.Components
|
|
{
|
|
/// <summary>
|
|
/// A solution of reagents.
|
|
/// </summary>
|
|
[Serializable, NetSerializable]
|
|
[DataDefinition]
|
|
public sealed partial class Solution : IEnumerable<ReagentQuantity>, ISerializationHooks
|
|
{
|
|
// This is a list because it is actually faster to add and remove reagents from
|
|
// a list than a dictionary, though contains-reagent checks are slightly slower,
|
|
[DataField("reagents")]
|
|
public List<ReagentQuantity> Contents;
|
|
|
|
/// <summary>
|
|
/// The calculated total volume of all reagents in the solution (ex. Total volume of liquid in beaker).
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public FixedPoint2 Volume { get; set; }
|
|
|
|
/// <summary>
|
|
/// Maximum volume this solution supports.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A value of zero means the maximum will automatically be set equal to the current volume during
|
|
/// initialization. Note that most solution methods ignore max volume altogether, but various solution
|
|
/// systems use this.
|
|
/// </remarks>
|
|
[DataField("maxVol")]
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public FixedPoint2 MaxVolume { get; set; } = FixedPoint2.Zero;
|
|
|
|
public float FillFraction => MaxVolume == 0 ? 1 : Volume.Float() / MaxVolume.Float();
|
|
|
|
/// <summary>
|
|
/// If reactions will be checked for when adding reagents to the container.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
[DataField("canReact")]
|
|
public bool CanReact { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// If reactions can occur via mixing.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
[DataField("canMix")]
|
|
public bool CanMix { get; set; } = false;
|
|
|
|
/// <summary>
|
|
/// Volume needed to fill this container.
|
|
/// </summary>
|
|
[ViewVariables]
|
|
public FixedPoint2 AvailableVolume => MaxVolume - Volume;
|
|
|
|
/// <summary>
|
|
/// The temperature of the reagents in the solution.
|
|
/// </summary>
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
[DataField("temperature")]
|
|
public float Temperature { get; set; } = 293.15f;
|
|
|
|
/// <summary>
|
|
/// The name of this solution, if it is contained in some <see cref="SolutionContainerManagerComponent"/>
|
|
/// </summary>
|
|
[DataField]
|
|
public string? Name;
|
|
|
|
/// <summary>
|
|
/// Checks if a solution can fit into the container.
|
|
/// </summary>
|
|
public bool CanAddSolution(Solution solution)
|
|
{
|
|
return solution.Volume <= AvailableVolume;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The total heat capacity of all reagents in the solution.
|
|
/// </summary>
|
|
[ViewVariables] private float _heatCapacity;
|
|
|
|
/// <summary>
|
|
/// If true, then <see cref="_heatCapacity"/> needs to be recomputed.
|
|
/// </summary>
|
|
[ViewVariables] private bool _heatCapacityDirty = true;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
private int _heatCapacityUpdateCounter;
|
|
|
|
// This value is arbitrary btw.
|
|
private const int HeatCapacityUpdateInterval = 15;
|
|
|
|
public void UpdateHeatCapacity(IPrototypeManager? protoMan)
|
|
{
|
|
IoCManager.Resolve(ref protoMan);
|
|
DebugTools.Assert(_heatCapacityDirty);
|
|
_heatCapacityDirty = false;
|
|
_heatCapacity = 0;
|
|
foreach (var (reagent, quantity) in Contents)
|
|
{
|
|
_heatCapacity += (float) quantity *
|
|
protoMan.Index<ReagentPrototype>(reagent.Prototype).SpecificHeat;
|
|
}
|
|
|
|
_heatCapacityUpdateCounter = 0;
|
|
}
|
|
|
|
public float GetHeatCapacity(IPrototypeManager? protoMan)
|
|
{
|
|
if (_heatCapacityDirty)
|
|
UpdateHeatCapacity(protoMan);
|
|
return _heatCapacity;
|
|
}
|
|
|
|
public void CheckRecalculateHeatCapacity()
|
|
{
|
|
// For performance, we have a few ways for heat capacity to get modified without a full recalculation.
|
|
// To avoid these drifting too much due to float error, we mark it as dirty after N such operations,
|
|
// so it will be recalculated.
|
|
if (++_heatCapacityUpdateCounter >= HeatCapacityUpdateInterval)
|
|
_heatCapacityDirty = true;
|
|
}
|
|
|
|
public float GetThermalEnergy(IPrototypeManager? protoMan)
|
|
{
|
|
return GetHeatCapacity(protoMan) * Temperature;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs an empty solution (ex. an empty beaker).
|
|
/// </summary>
|
|
public Solution() : this(2) // Most objects on the station hold only 1 or 2 reagents.
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs an empty solution (ex. an empty beaker).
|
|
/// </summary>
|
|
public Solution(int capacity)
|
|
{
|
|
Contents = new(capacity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs a solution containing 100% of a reagent (ex. A beaker of pure water).
|
|
/// </summary>
|
|
/// <param name="prototype">The prototype ID of the reagent to add.</param>
|
|
/// <param name="quantity">The quantity in milli-units.</param>
|
|
public Solution(string prototype, FixedPoint2 quantity, ReagentData? data = null) : this()
|
|
{
|
|
AddReagent(new ReagentId(prototype, data), quantity);
|
|
}
|
|
|
|
public Solution(IEnumerable<ReagentQuantity> reagents, bool setMaxVol = true)
|
|
{
|
|
Contents = new(reagents);
|
|
Volume = FixedPoint2.Zero;
|
|
foreach (var reagent in Contents)
|
|
{
|
|
Volume += reagent.Quantity;
|
|
}
|
|
|
|
if (setMaxVol)
|
|
MaxVolume = Volume;
|
|
|
|
ValidateSolution();
|
|
}
|
|
|
|
public Solution(Solution solution)
|
|
{
|
|
Contents = solution.Contents.ShallowClone();
|
|
Volume = solution.Volume;
|
|
MaxVolume = solution.MaxVolume;
|
|
Temperature = solution.Temperature;
|
|
_heatCapacity = solution._heatCapacity;
|
|
_heatCapacityDirty = solution._heatCapacityDirty;
|
|
_heatCapacityUpdateCounter = solution._heatCapacityUpdateCounter;
|
|
ValidateSolution();
|
|
}
|
|
|
|
public Solution Clone()
|
|
{
|
|
return new Solution(this);
|
|
}
|
|
|
|
[AssertionMethod]
|
|
public void ValidateSolution()
|
|
{
|
|
// sandbox forbids: [Conditional("DEBUG")]
|
|
#if DEBUG
|
|
// Correct volume
|
|
DebugTools.Assert(Contents.Select(x => x.Quantity).Sum() == Volume);
|
|
|
|
// All reagents have at least some reagent present.
|
|
DebugTools.Assert(!Contents.Any(x => x.Quantity <= FixedPoint2.Zero));
|
|
|
|
// No duplicate reagents iDs
|
|
DebugTools.Assert(Contents.Select(x => x.Reagent).ToHashSet().Count == Contents.Count);
|
|
|
|
// If it isn't flagged as dirty, check heat capacity is correct.
|
|
if (!_heatCapacityDirty)
|
|
{
|
|
var cur = _heatCapacity;
|
|
_heatCapacityDirty = true;
|
|
UpdateHeatCapacity(null);
|
|
DebugTools.Assert(MathHelper.CloseTo(_heatCapacity, cur, tolerance: 0.01));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ISerializationHooks.AfterDeserialization()
|
|
{
|
|
Volume = FixedPoint2.Zero;
|
|
foreach (var reagent in Contents)
|
|
{
|
|
Volume += reagent.Quantity;
|
|
}
|
|
|
|
if (MaxVolume == FixedPoint2.Zero)
|
|
MaxVolume = Volume;
|
|
}
|
|
|
|
public bool ContainsPrototype(string prototype)
|
|
{
|
|
foreach (var (reagent, _) in Contents)
|
|
{
|
|
if (reagent.Prototype == prototype)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool ContainsReagent(ReagentId id)
|
|
{
|
|
foreach (var (reagent, _) in Contents)
|
|
{
|
|
if (reagent == id)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool ContainsReagent(string reagentId, ReagentData? data)
|
|
=> ContainsReagent(new(reagentId, data));
|
|
|
|
public bool TryGetReagent(ReagentId id, out ReagentQuantity quantity)
|
|
{
|
|
foreach (var tuple in Contents)
|
|
{
|
|
if (tuple.Reagent != id)
|
|
continue;
|
|
|
|
DebugTools.Assert(tuple.Quantity > FixedPoint2.Zero);
|
|
quantity = tuple;
|
|
return true;
|
|
}
|
|
|
|
quantity = new ReagentQuantity(id, FixedPoint2.Zero);
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetReagentQuantity(ReagentId id, out FixedPoint2 volume)
|
|
{
|
|
volume = FixedPoint2.Zero;
|
|
if (!TryGetReagent(id, out var quant))
|
|
return false;
|
|
|
|
volume = quant.Quantity;
|
|
return true;
|
|
}
|
|
|
|
[Pure]
|
|
public ReagentQuantity GetReagent(ReagentId id)
|
|
{
|
|
TryGetReagent(id, out var quantity);
|
|
return quantity;
|
|
}
|
|
|
|
public ReagentQuantity this[ReagentId id]
|
|
{
|
|
get
|
|
{
|
|
if (!TryGetReagent(id, out var quantity))
|
|
throw new KeyNotFoundException(id.ToString());
|
|
return quantity;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the volume/quantity of a single reagent in the solution.
|
|
/// </summary>
|
|
[Pure]
|
|
public FixedPoint2 GetReagentQuantity(ReagentId id)
|
|
{
|
|
return GetReagent(id).Quantity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the total volume of all reagents in the solution with the given prototype Id.
|
|
/// If you only want the volume of a single reagent, use <see cref="GetReagentQuantity"/>
|
|
/// </summary>
|
|
[Pure]
|
|
public FixedPoint2 GetTotalPrototypeQuantity(params string[] prototypes)
|
|
{
|
|
var total = FixedPoint2.Zero;
|
|
foreach (var (reagent, quantity) in Contents)
|
|
{
|
|
if (prototypes.Contains(reagent.Prototype))
|
|
total += quantity;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
public FixedPoint2 GetTotalPrototypeQuantity(string id)
|
|
{
|
|
var total = FixedPoint2.Zero;
|
|
foreach (var (reagent, quantity) in Contents)
|
|
{
|
|
if (id == reagent.Prototype)
|
|
total += quantity;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
public ReagentId? GetPrimaryReagentId()
|
|
{
|
|
if (Contents.Count == 0)
|
|
return null;
|
|
|
|
ReagentQuantity max = default;
|
|
|
|
foreach (var reagent in Contents)
|
|
{
|
|
if (reagent.Quantity >= max.Quantity)
|
|
{
|
|
max = reagent;
|
|
}
|
|
}
|
|
|
|
return max.Reagent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a given quantity of a reagent directly into the solution.
|
|
/// </summary>
|
|
/// <param name="prototype">The prototype ID of the reagent to add.</param>
|
|
/// <param name="quantity">The quantity in milli-units.</param>
|
|
public void AddReagent(string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true)
|
|
=> AddReagent(new ReagentId(prototype, null), quantity, dirtyHeatCap);
|
|
|
|
/// <summary>
|
|
/// Adds a given quantity of a reagent directly into the solution.
|
|
/// </summary>
|
|
/// <param name="id">The reagent to add.</param>
|
|
/// <param name="quantity">The quantity in milli-units.</param>
|
|
public void AddReagent(ReagentId id, FixedPoint2 quantity, bool dirtyHeatCap = true)
|
|
{
|
|
if (quantity <= 0)
|
|
{
|
|
DebugTools.Assert(quantity == 0, "Attempted to add negative reagent quantity");
|
|
return;
|
|
}
|
|
|
|
Volume += quantity;
|
|
_heatCapacityDirty |= dirtyHeatCap;
|
|
for (var i = 0; i < Contents.Count; i++)
|
|
{
|
|
var (reagent, existingQuantity) = Contents[i];
|
|
if (reagent != id)
|
|
continue;
|
|
|
|
Contents[i] = new ReagentQuantity(id, existingQuantity + quantity);
|
|
ValidateSolution();
|
|
return;
|
|
}
|
|
|
|
Contents.Add(new ReagentQuantity(id, quantity));
|
|
ValidateSolution();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a given quantity of a reagent directly into the solution.
|
|
/// </summary>
|
|
/// <param name="reagentId">The reagent to add.</param>
|
|
/// <param name="quantity">The quantity in milli-units.</param>
|
|
public void AddReagent(ReagentPrototype proto, ReagentId reagentId, FixedPoint2 quantity)
|
|
{
|
|
AddReagent(reagentId, quantity, false);
|
|
|
|
_heatCapacity += quantity.Float() * proto.SpecificHeat;
|
|
CheckRecalculateHeatCapacity();
|
|
}
|
|
|
|
public void AddReagent(ReagentQuantity reagentQuantity)
|
|
=> AddReagent(reagentQuantity.Reagent, reagentQuantity.Quantity);
|
|
|
|
/// <summary>
|
|
/// Adds a given quantity of a reagent directly into the solution.
|
|
/// </summary>
|
|
/// <param name="proto">The prototype of the reagent to add.</param>
|
|
/// <param name="quantity">The quantity in milli-units.</param>
|
|
public void AddReagent(ReagentPrototype proto, FixedPoint2 quantity, float temperature, IPrototypeManager? protoMan, ReagentData? data = null)
|
|
{
|
|
if (_heatCapacityDirty)
|
|
UpdateHeatCapacity(protoMan);
|
|
|
|
var totalThermalEnergy = Temperature * _heatCapacity + temperature * proto.SpecificHeat;
|
|
AddReagent(new ReagentId(proto.ID, data), quantity);
|
|
Temperature = _heatCapacity == 0 ? 0 : totalThermalEnergy / _heatCapacity;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Scales the amount of solution by some integer quantity.
|
|
/// </summary>
|
|
/// <param name="scale">The scalar to modify the solution by.</param>
|
|
public void ScaleSolution(int scale)
|
|
{
|
|
if (scale == 1)
|
|
return;
|
|
|
|
if (scale <= 0)
|
|
{
|
|
RemoveAllSolution();
|
|
return;
|
|
}
|
|
|
|
_heatCapacity *= scale;
|
|
Volume *= scale;
|
|
CheckRecalculateHeatCapacity();
|
|
|
|
for (int i = 0; i < Contents.Count; i++)
|
|
{
|
|
var old = Contents[i];
|
|
Contents[i] = new ReagentQuantity(old.Reagent, old.Quantity * scale);
|
|
}
|
|
ValidateSolution();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales the amount of solution.
|
|
/// </summary>
|
|
/// <param name="scale">The scalar to modify the solution by.</param>
|
|
public void ScaleSolution(float scale)
|
|
{
|
|
if (scale == 1)
|
|
return;
|
|
|
|
if (scale == 0)
|
|
{
|
|
RemoveAllSolution();
|
|
return;
|
|
}
|
|
|
|
Volume = FixedPoint2.Zero;
|
|
for (int i = Contents.Count - 1; i >= 0; i--)
|
|
{
|
|
var old = Contents[i];
|
|
var newQuantity = old.Quantity * scale;
|
|
if (newQuantity == FixedPoint2.Zero)
|
|
Contents.RemoveSwap(i);
|
|
else
|
|
{
|
|
Contents[i] = new ReagentQuantity(old.Reagent, newQuantity);
|
|
Volume += newQuantity;
|
|
}
|
|
}
|
|
|
|
_heatCapacityDirty = true;
|
|
ValidateSolution();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove an amount of reagent from the solution.
|
|
/// </summary>
|
|
/// <param name="toRemove">The reagent to be removed.</param>
|
|
/// <returns>How much reagent was actually removed. Zero if the reagent is not present on the solution.</returns>
|
|
public FixedPoint2 RemoveReagent(ReagentQuantity toRemove, bool preserveOrder = false)
|
|
{
|
|
if (toRemove.Quantity <= FixedPoint2.Zero)
|
|
return FixedPoint2.Zero;
|
|
|
|
for (var i = 0; i < Contents.Count; i++)
|
|
{
|
|
var (reagent, curQuantity) = Contents[i];
|
|
|
|
if(reagent != toRemove.Reagent)
|
|
continue;
|
|
|
|
var newQuantity = curQuantity - toRemove.Quantity;
|
|
_heatCapacityDirty = true;
|
|
|
|
if (newQuantity <= 0)
|
|
{
|
|
if (!preserveOrder)
|
|
Contents.RemoveSwap(i);
|
|
else
|
|
Contents.RemoveAt(i);
|
|
|
|
Volume -= curQuantity;
|
|
ValidateSolution();
|
|
return curQuantity;
|
|
}
|
|
|
|
Contents[i] = new ReagentQuantity(reagent, newQuantity);
|
|
Volume -= toRemove.Quantity;
|
|
ValidateSolution();
|
|
return toRemove.Quantity;
|
|
}
|
|
|
|
// Reagent is not on the solution...
|
|
return FixedPoint2.Zero;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove an amount of reagent from the solution.
|
|
/// </summary>
|
|
/// <param name="prototype">The prototype of the reagent to be removed.</param>
|
|
/// <param name="quantity">The amount of reagent to remove.</param>
|
|
/// <returns>How much reagent was actually removed. Zero if the reagent is not present on the solution.</returns>
|
|
public FixedPoint2 RemoveReagent(string prototype, FixedPoint2 quantity, ReagentData? data = null)
|
|
{
|
|
return RemoveReagent(new ReagentQuantity(prototype, quantity, data));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove an amount of reagent from the solution.
|
|
/// </summary>
|
|
/// <param name="reagentId">The reagent to be removed.</param>
|
|
/// <param name="quantity">The amount of reagent to remove.</param>
|
|
/// <returns>How much reagent was actually removed. Zero if the reagent is not present on the solution.</returns>
|
|
public FixedPoint2 RemoveReagent(ReagentId reagentId, FixedPoint2 quantity, bool preserveOrder = false)
|
|
{
|
|
return RemoveReagent(new ReagentQuantity(reagentId, quantity), preserveOrder);
|
|
}
|
|
|
|
public void RemoveAllSolution()
|
|
{
|
|
Contents.Clear();
|
|
Volume = FixedPoint2.Zero;
|
|
_heatCapacityDirty = false;
|
|
_heatCapacity = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a solution without the specified reagent prototypes.
|
|
/// </summary>
|
|
public Solution SplitSolutionWithout(FixedPoint2 toTake, params string[] excludedPrototypes)
|
|
{
|
|
// First remove the blacklisted prototypes
|
|
List<ReagentQuantity> excluded = new();
|
|
foreach (var id in excludedPrototypes)
|
|
{
|
|
foreach (var tuple in Contents)
|
|
{
|
|
if (tuple.Reagent.Prototype != id)
|
|
continue;
|
|
|
|
excluded.Add(tuple);
|
|
RemoveReagent(tuple);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Then split the solution
|
|
var sol = SplitSolution(toTake);
|
|
|
|
// Then re-add the excluded reagents to the original solution.
|
|
foreach (var reagent in excluded)
|
|
{
|
|
AddReagent(reagent);
|
|
}
|
|
|
|
return sol;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Splits a solution without the specified reagent prototypes.
|
|
/// </summary>
|
|
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes)
|
|
{
|
|
// First remove the non-included prototypes
|
|
List<ReagentQuantity> excluded = new();
|
|
for (var i = Contents.Count - 1; i >= 0; i--)
|
|
{
|
|
if (includedPrototypes.Contains(Contents[i].Reagent.Prototype))
|
|
continue;
|
|
|
|
excluded.Add(Contents[i]);
|
|
RemoveReagent(Contents[i]);
|
|
}
|
|
|
|
// Then split the solution
|
|
var sol = SplitSolution(toTake);
|
|
|
|
// Then re-add the excluded reagents to the original solution.
|
|
foreach (var reagent in excluded)
|
|
{
|
|
AddReagent(reagent);
|
|
}
|
|
|
|
return sol;
|
|
}
|
|
|
|
public Solution SplitSolution(FixedPoint2 toTake)
|
|
{
|
|
if (toTake <= FixedPoint2.Zero)
|
|
return new Solution();
|
|
|
|
Solution newSolution;
|
|
|
|
if (toTake >= Volume)
|
|
{
|
|
newSolution = Clone();
|
|
RemoveAllSolution();
|
|
return newSolution;
|
|
}
|
|
|
|
var origVol = Volume;
|
|
var effVol = Volume.Value;
|
|
newSolution = new Solution(Contents.Count) { Temperature = Temperature };
|
|
var remaining = (long) toTake.Value;
|
|
|
|
for (var i = Contents.Count - 1; i >= 0; i--) // iterate backwards because of remove swap.
|
|
{
|
|
var (reagent, quantity) = Contents[i];
|
|
|
|
// This is set up such that integer rounding will tend to take more reagents.
|
|
var split = remaining * quantity.Value / effVol;
|
|
|
|
if (split <= 0)
|
|
{
|
|
effVol -= quantity.Value;
|
|
DebugTools.Assert(split == 0, "Negative solution quantity while splitting? Long/int overflow?");
|
|
continue;
|
|
}
|
|
|
|
var splitQuantity = FixedPoint2.FromCents((int) split);
|
|
var newQuantity = quantity - splitQuantity;
|
|
|
|
DebugTools.Assert(newQuantity >= 0);
|
|
|
|
if (newQuantity > FixedPoint2.Zero)
|
|
Contents[i] = new ReagentQuantity(reagent, newQuantity);
|
|
else
|
|
Contents.RemoveSwap(i);
|
|
|
|
newSolution.Contents.Add(new ReagentQuantity(reagent, splitQuantity));
|
|
Volume -= splitQuantity;
|
|
remaining -= split;
|
|
effVol -= quantity.Value;
|
|
}
|
|
|
|
newSolution.Volume = origVol - Volume;
|
|
|
|
DebugTools.Assert(remaining >= 0);
|
|
DebugTools.Assert(remaining == 0 || Volume == FixedPoint2.Zero);
|
|
|
|
_heatCapacityDirty = true;
|
|
newSolution._heatCapacityDirty = true;
|
|
|
|
ValidateSolution();
|
|
newSolution.ValidateSolution();
|
|
|
|
return newSolution;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Variant of <see cref="SplitSolution(FixedPoint2)"/> that doesn't return a new solution containing the removed reagents.
|
|
/// </summary>
|
|
/// <param name="toTake">The quantity of this solution to remove</param>
|
|
public void RemoveSolution(FixedPoint2 toTake)
|
|
{
|
|
if (toTake <= FixedPoint2.Zero)
|
|
return;
|
|
|
|
if (toTake >= Volume)
|
|
{
|
|
RemoveAllSolution();
|
|
return;
|
|
}
|
|
|
|
var effVol = Volume.Value;
|
|
Volume -= toTake;
|
|
var remaining = (long) toTake.Value;
|
|
for (var i = Contents.Count - 1; i >= 0; i--)// iterate backwards because of remove swap.
|
|
{
|
|
var (reagent, quantity) = Contents[i];
|
|
|
|
// This is set up such that integer rounding will tend to take more reagents.
|
|
var split = remaining * quantity.Value / effVol;
|
|
|
|
if (split <= 0)
|
|
{
|
|
effVol -= quantity.Value;
|
|
DebugTools.Assert(split == 0, "Negative solution quantity while splitting? Long/int overflow?");
|
|
continue;
|
|
}
|
|
|
|
var splitQuantity = FixedPoint2.FromCents((int) split);
|
|
var newQuantity = quantity - splitQuantity;
|
|
|
|
if (newQuantity > FixedPoint2.Zero)
|
|
Contents[i] = new ReagentQuantity(reagent, newQuantity);
|
|
else
|
|
Contents.RemoveSwap(i);
|
|
|
|
remaining -= split;
|
|
effVol -= quantity.Value;
|
|
}
|
|
|
|
DebugTools.Assert(remaining >= 0);
|
|
DebugTools.Assert(remaining == 0 || Volume == FixedPoint2.Zero);
|
|
|
|
_heatCapacityDirty = true;
|
|
ValidateSolution();
|
|
}
|
|
|
|
public void AddSolution(Solution otherSolution, IPrototypeManager? protoMan)
|
|
{
|
|
if (otherSolution.Volume <= FixedPoint2.Zero)
|
|
return;
|
|
|
|
Volume += otherSolution.Volume;
|
|
|
|
var closeTemps = MathHelper.CloseTo(otherSolution.Temperature, Temperature);
|
|
float totalThermalEnergy = 0;
|
|
if (!closeTemps)
|
|
{
|
|
IoCManager.Resolve(ref protoMan);
|
|
|
|
if (_heatCapacityDirty)
|
|
UpdateHeatCapacity(protoMan);
|
|
|
|
if (otherSolution._heatCapacityDirty)
|
|
otherSolution.UpdateHeatCapacity(protoMan);
|
|
|
|
totalThermalEnergy = _heatCapacity * Temperature + otherSolution._heatCapacity * otherSolution.Temperature;
|
|
}
|
|
|
|
for (var i = 0; i < otherSolution.Contents.Count; i++)
|
|
{
|
|
var (otherReagent, otherQuantity) = otherSolution.Contents[i];
|
|
|
|
var found = false;
|
|
for (var j = 0; j < Contents.Count; j++)
|
|
{
|
|
var (reagent, quantity) = Contents[j];
|
|
if (reagent == otherReagent)
|
|
{
|
|
found = true;
|
|
Contents[j] = new ReagentQuantity(reagent, quantity + otherQuantity);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
Contents.Add(new ReagentQuantity(otherReagent, otherQuantity));
|
|
}
|
|
}
|
|
|
|
_heatCapacity += otherSolution._heatCapacity;
|
|
CheckRecalculateHeatCapacity();
|
|
if (closeTemps)
|
|
_heatCapacityDirty |= otherSolution._heatCapacityDirty;
|
|
else
|
|
Temperature = _heatCapacity == 0 ? 0 : totalThermalEnergy / _heatCapacity;
|
|
|
|
ValidateSolution();
|
|
}
|
|
|
|
public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without)
|
|
{
|
|
if (Volume == FixedPoint2.Zero)
|
|
{
|
|
return Color.Transparent;
|
|
}
|
|
|
|
IoCManager.Resolve(ref protoMan);
|
|
|
|
Color mixColor = default;
|
|
var runningTotalQuantity = FixedPoint2.New(0);
|
|
bool first = true;
|
|
|
|
foreach (var (reagent, quantity) in Contents)
|
|
{
|
|
if (without.Contains(reagent.Prototype))
|
|
continue;
|
|
|
|
runningTotalQuantity += quantity;
|
|
|
|
if (!protoMan.TryIndex(reagent.Prototype, out ReagentPrototype? proto))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (first)
|
|
{
|
|
first = false;
|
|
mixColor = proto.SubstanceColor;
|
|
continue;
|
|
}
|
|
|
|
var interpolateValue = quantity.Float() / runningTotalQuantity.Float();
|
|
mixColor = Color.InterpolateBetween(mixColor, proto.SubstanceColor, interpolateValue);
|
|
}
|
|
return mixColor;
|
|
}
|
|
|
|
public Color GetColor(IPrototypeManager? protoMan)
|
|
{
|
|
return GetColorWithout(protoMan);
|
|
}
|
|
|
|
public Color GetColorWithOnly(IPrototypeManager? protoMan, params string[] included)
|
|
{
|
|
if (Volume == FixedPoint2.Zero)
|
|
{
|
|
return Color.Transparent;
|
|
}
|
|
|
|
IoCManager.Resolve(ref protoMan);
|
|
|
|
Color mixColor = default;
|
|
var runningTotalQuantity = FixedPoint2.New(0);
|
|
bool first = true;
|
|
|
|
foreach (var (reagent, quantity) in Contents)
|
|
{
|
|
if (!included.Contains(reagent.Prototype))
|
|
continue;
|
|
|
|
runningTotalQuantity += quantity;
|
|
|
|
if (!protoMan.TryIndex(reagent.Prototype, out ReagentPrototype? proto))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (first)
|
|
{
|
|
first = false;
|
|
mixColor = proto.SubstanceColor;
|
|
continue;
|
|
}
|
|
|
|
var interpolateValue = quantity.Float() / runningTotalQuantity.Float();
|
|
mixColor = Color.InterpolateBetween(mixColor, proto.SubstanceColor, interpolateValue);
|
|
}
|
|
return mixColor;
|
|
}
|
|
|
|
#region Enumeration
|
|
|
|
public IEnumerator<ReagentQuantity> GetEnumerator()
|
|
{
|
|
return Contents.GetEnumerator();
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return GetEnumerator();
|
|
}
|
|
#endregion
|
|
|
|
public void SetContents(IEnumerable<ReagentQuantity> reagents, bool setMaxVol = false)
|
|
{
|
|
Volume = 0;
|
|
RemoveAllSolution();
|
|
_heatCapacityDirty = true;
|
|
Contents = new(reagents);
|
|
foreach (var reagent in Contents)
|
|
{
|
|
Volume += reagent.Quantity;
|
|
}
|
|
|
|
if (setMaxVol)
|
|
MaxVolume = Volume;
|
|
|
|
ValidateSolution();
|
|
}
|
|
|
|
public Dictionary<ReagentPrototype, FixedPoint2> GetReagentPrototypes(IPrototypeManager protoMan)
|
|
{
|
|
var dict = new Dictionary<ReagentPrototype, FixedPoint2>(Contents.Count);
|
|
foreach (var (reagent, quantity) in Contents)
|
|
{
|
|
var proto = protoMan.Index<ReagentPrototype>(reagent.Prototype);
|
|
dict[proto] = quantity + dict.GetValueOrDefault(proto);
|
|
}
|
|
return dict;
|
|
}
|
|
}
|
|
}
|