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
{
///
/// A solution of reagents.
///
[Serializable, NetSerializable]
[DataDefinition]
public sealed partial class Solution : IEnumerable, 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 Contents;
///
/// The calculated total volume of all reagents in the solution (ex. Total volume of liquid in beaker).
///
[ViewVariables]
public FixedPoint2 Volume { get; set; }
///
/// Maximum volume this solution supports.
///
///
/// 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.
///
[DataField("maxVol")]
[ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 MaxVolume { get; set; } = FixedPoint2.Zero;
public float FillFraction => MaxVolume == 0 ? 1 : Volume.Float() / MaxVolume.Float();
///
/// If reactions will be checked for when adding reagents to the container.
///
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canReact")]
public bool CanReact { get; set; } = true;
///
/// If reactions can occur via mixing.
///
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canMix")]
public bool CanMix { get; set; } = false;
///
/// Volume needed to fill this container.
///
[ViewVariables]
public FixedPoint2 AvailableVolume => MaxVolume - Volume;
///
/// The temperature of the reagents in the solution.
///
[ViewVariables(VVAccess.ReadWrite)]
[DataField("temperature")]
public float Temperature { get; set; } = 293.15f;
///
/// The name of this solution, if it is contained in some
///
[DataField]
public string? Name;
///
/// Checks if a solution can fit into the container.
///
public bool CanAddSolution(Solution solution)
{
return solution.Volume <= AvailableVolume;
}
///
/// The total heat capacity of all reagents in the solution.
///
[ViewVariables] private float _heatCapacity;
///
/// If true, then needs to be recomputed.
///
[ViewVariables] private bool _heatCapacityDirty = true;
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(reagent.Prototype).SpecificHeat;
}
}
public float GetHeatCapacity(IPrototypeManager? protoMan)
{
if (_heatCapacityDirty)
UpdateHeatCapacity(protoMan);
return _heatCapacity;
}
public float GetThermalEnergy(IPrototypeManager? protoMan)
{
return GetHeatCapacity(protoMan) * Temperature;
}
///
/// Constructs an empty solution (ex. an empty beaker).
///
public Solution() : this(2) // Most objects on the station hold only 1 or 2 reagents.
{
}
///
/// Constructs an empty solution (ex. an empty beaker).
///
public Solution(int capacity)
{
Contents = new(capacity);
}
///
/// Constructs a solution containing 100% of a reagent (ex. A beaker of pure water).
///
/// The prototype ID of the reagent to add.
/// The quantity in milli-units.
public Solution(string prototype, FixedPoint2 quantity, ReagentData? data = null) : this()
{
AddReagent(new ReagentId(prototype, data), quantity);
}
public Solution(IEnumerable 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;
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));
}
#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;
}
}
///
/// Get the volume/quantity of a single reagent in the solution.
///
[Pure]
public FixedPoint2 GetReagentQuantity(ReagentId id)
{
return GetReagent(id).Quantity;
}
///
/// 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
///
[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;
}
///
/// Adds a given quantity of a reagent directly into the solution.
///
/// The prototype ID of the reagent to add.
/// The quantity in milli-units.
public void AddReagent(string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true)
=> AddReagent(new ReagentId(prototype, null), quantity, dirtyHeatCap);
///
/// Adds a given quantity of a reagent directly into the solution.
///
/// The reagent to add.
/// The quantity in milli-units.
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();
}
///
/// Adds a given quantity of a reagent directly into the solution.
///
/// The reagent to add.
/// The quantity in milli-units.
public void AddReagent(ReagentPrototype proto, ReagentId reagentId, FixedPoint2 quantity)
{
AddReagent(reagentId, quantity, false);
_heatCapacity += quantity.Float() * proto.SpecificHeat;
}
public void AddReagent(ReagentQuantity reagentQuantity)
=> AddReagent(reagentQuantity.Reagent, reagentQuantity.Quantity);
///
/// Adds a given quantity of a reagent directly into the solution.
///
/// The prototype of the reagent to add.
/// The quantity in milli-units.
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;
}
///
/// Scales the amount of solution by some integer quantity.
///
/// The scalar to modify the solution by.
public void ScaleSolution(int scale)
{
if (scale == 1)
return;
if (scale <= 0)
{
RemoveAllSolution();
return;
}
_heatCapacity *= scale;
Volume *= scale;
for (int i = 0; i < Contents.Count; i++)
{
var old = Contents[i];
Contents[i] = new ReagentQuantity(old.Reagent, old.Quantity * scale);
}
ValidateSolution();
}
///
/// Scales the amount of solution.
///
/// The scalar to modify the solution by.
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();
}
///
/// Attempts to remove an amount of reagent from the solution.
///
/// The reagent to be removed.
/// How much reagent was actually removed. Zero if the reagent is not present on the solution.
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;
}
///
/// Attempts to remove an amount of reagent from the solution.
///
/// The prototype of the reagent to be removed.
/// The amount of reagent to remove.
/// How much reagent was actually removed. Zero if the reagent is not present on the solution.
public FixedPoint2 RemoveReagent(string prototype, FixedPoint2 quantity, ReagentData? data = null)
{
return RemoveReagent(new ReagentQuantity(prototype, quantity, data));
}
///
/// Attempts to remove an amount of reagent from the solution.
///
/// The reagent to be removed.
/// The amount of reagent to remove.
/// How much reagent was actually removed. Zero if the reagent is not present on the solution.
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;
}
///
/// Splits a solution without the specified reagent prototypes.
///
public Solution SplitSolutionWithout(FixedPoint2 toTake, params string[] excludedPrototypes)
{
// First remove the blacklisted prototypes
List 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;
}
///
/// Splits a solution without the specified reagent prototypes.
///
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes)
{
// First remove the non-included prototypes
List 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;
}
///
/// Variant of that doesn't return a new solution containing the removed reagents.
///
/// The quantity of this solution to remove
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;
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 GetEnumerator()
{
return Contents.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
public void SetContents(IEnumerable 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 GetReagentPrototypes(IPrototypeManager protoMan)
{
var dict = new Dictionary(Contents.Count);
foreach (var (reagent, quantity) in Contents)
{
var proto = protoMan.Index(reagent.Prototype);
dict[proto] = quantity + dict.GetValueOrDefault(proto);
}
return dict;
}
}
}