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 /// 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) { Volume = solution.Volume; _heatCapacity = solution._heatCapacity; _heatCapacityDirty = solution._heatCapacityDirty; Contents = solution.Contents.ShallowClone(); 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) { 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) { Contents.RemoveSwap(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) { return RemoveReagent(new ReagentQuantity(reagentId, quantity)); } 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; } } }