Solution.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  1. using Content.Shared.Chemistry.Reagent;
  2. using Content.Shared.FixedPoint;
  3. using JetBrains.Annotations;
  4. using Robust.Shared.Prototypes;
  5. using Robust.Shared.Serialization;
  6. using Robust.Shared.Utility;
  7. using System.Collections;
  8. using System.Linq;
  9. using Content.Shared.Chemistry.Components.SolutionManager;
  10. namespace Content.Shared.Chemistry.Components
  11. {
  12. /// <summary>
  13. /// A solution of reagents.
  14. /// </summary>
  15. [Serializable, NetSerializable]
  16. [DataDefinition]
  17. public sealed partial class Solution : IEnumerable<ReagentQuantity>, ISerializationHooks
  18. {
  19. // This is a list because it is actually faster to add and remove reagents from
  20. // a list than a dictionary, though contains-reagent checks are slightly slower,
  21. [DataField("reagents")]
  22. public List<ReagentQuantity> Contents;
  23. /// <summary>
  24. /// The calculated total volume of all reagents in the solution (ex. Total volume of liquid in beaker).
  25. /// </summary>
  26. [ViewVariables]
  27. public FixedPoint2 Volume { get; set; }
  28. /// <summary>
  29. /// Maximum volume this solution supports.
  30. /// </summary>
  31. /// <remarks>
  32. /// A value of zero means the maximum will automatically be set equal to the current volume during
  33. /// initialization. Note that most solution methods ignore max volume altogether, but various solution
  34. /// systems use this.
  35. /// </remarks>
  36. [DataField("maxVol")]
  37. [ViewVariables(VVAccess.ReadWrite)]
  38. public FixedPoint2 MaxVolume { get; set; } = FixedPoint2.Zero;
  39. public float FillFraction => MaxVolume == 0 ? 1 : Volume.Float() / MaxVolume.Float();
  40. /// <summary>
  41. /// If reactions will be checked for when adding reagents to the container.
  42. /// </summary>
  43. [ViewVariables(VVAccess.ReadWrite)]
  44. [DataField("canReact")]
  45. public bool CanReact { get; set; } = true;
  46. /// <summary>
  47. /// Volume needed to fill this container.
  48. /// </summary>
  49. [ViewVariables]
  50. public FixedPoint2 AvailableVolume => MaxVolume - Volume;
  51. /// <summary>
  52. /// The temperature of the reagents in the solution.
  53. /// </summary>
  54. [ViewVariables(VVAccess.ReadWrite)]
  55. [DataField("temperature")]
  56. public float Temperature { get; set; } = 293.15f;
  57. /// <summary>
  58. /// The name of this solution, if it is contained in some <see cref="SolutionContainerManagerComponent"/>
  59. /// </summary>
  60. [DataField]
  61. public string? Name;
  62. /// <summary>
  63. /// Checks if a solution can fit into the container.
  64. /// </summary>
  65. public bool CanAddSolution(Solution solution)
  66. {
  67. return solution.Volume <= AvailableVolume;
  68. }
  69. /// <summary>
  70. /// The total heat capacity of all reagents in the solution.
  71. /// </summary>
  72. [ViewVariables] private float _heatCapacity;
  73. /// <summary>
  74. /// If true, then <see cref="_heatCapacity"/> needs to be recomputed.
  75. /// </summary>
  76. [ViewVariables] private bool _heatCapacityDirty = true;
  77. [ViewVariables(VVAccess.ReadWrite)]
  78. private int _heatCapacityUpdateCounter;
  79. // This value is arbitrary btw.
  80. private const int HeatCapacityUpdateInterval = 15;
  81. public void UpdateHeatCapacity(IPrototypeManager? protoMan)
  82. {
  83. IoCManager.Resolve(ref protoMan);
  84. DebugTools.Assert(_heatCapacityDirty);
  85. _heatCapacityDirty = false;
  86. _heatCapacity = 0;
  87. foreach (var (reagent, quantity) in Contents)
  88. {
  89. _heatCapacity += (float) quantity *
  90. protoMan.Index<ReagentPrototype>(reagent.Prototype).SpecificHeat;
  91. }
  92. _heatCapacityUpdateCounter = 0;
  93. }
  94. public float GetHeatCapacity(IPrototypeManager? protoMan)
  95. {
  96. if (_heatCapacityDirty)
  97. UpdateHeatCapacity(protoMan);
  98. return _heatCapacity;
  99. }
  100. public void CheckRecalculateHeatCapacity()
  101. {
  102. // For performance, we have a few ways for heat capacity to get modified without a full recalculation.
  103. // To avoid these drifting too much due to float error, we mark it as dirty after N such operations,
  104. // so it will be recalculated.
  105. if (++_heatCapacityUpdateCounter >= HeatCapacityUpdateInterval)
  106. _heatCapacityDirty = true;
  107. }
  108. public float GetThermalEnergy(IPrototypeManager? protoMan)
  109. {
  110. return GetHeatCapacity(protoMan) * Temperature;
  111. }
  112. /// <summary>
  113. /// Constructs an empty solution (ex. an empty beaker).
  114. /// </summary>
  115. public Solution() : this(2) // Most objects on the station hold only 1 or 2 reagents.
  116. {
  117. }
  118. /// <summary>
  119. /// Constructs an empty solution (ex. an empty beaker).
  120. /// </summary>
  121. public Solution(int capacity)
  122. {
  123. Contents = new(capacity);
  124. }
  125. /// <summary>
  126. /// Constructs a solution containing 100% of a reagent (ex. A beaker of pure water).
  127. /// </summary>
  128. /// <param name="prototype">The prototype ID of the reagent to add.</param>
  129. /// <param name="quantity">The quantity in milli-units.</param>
  130. public Solution(string prototype, FixedPoint2 quantity, List<ReagentData>? data = null) : this()
  131. {
  132. AddReagent(new ReagentId(prototype, data), quantity);
  133. }
  134. public Solution(IEnumerable<ReagentQuantity> reagents, bool setMaxVol = true)
  135. {
  136. Contents = new(reagents);
  137. Volume = FixedPoint2.Zero;
  138. foreach (var reagent in Contents)
  139. {
  140. Volume += reagent.Quantity;
  141. }
  142. if (setMaxVol)
  143. MaxVolume = Volume;
  144. ValidateSolution();
  145. }
  146. public Solution(Solution solution)
  147. {
  148. Contents = solution.Contents.ShallowClone();
  149. Volume = solution.Volume;
  150. MaxVolume = solution.MaxVolume;
  151. Temperature = solution.Temperature;
  152. _heatCapacity = solution._heatCapacity;
  153. _heatCapacityDirty = solution._heatCapacityDirty;
  154. _heatCapacityUpdateCounter = solution._heatCapacityUpdateCounter;
  155. ValidateSolution();
  156. }
  157. public Solution Clone()
  158. {
  159. return new Solution(this);
  160. }
  161. [AssertionMethod]
  162. public void ValidateSolution()
  163. {
  164. // sandbox forbids: [Conditional("DEBUG")]
  165. #if DEBUG
  166. // Correct volume
  167. DebugTools.Assert(Contents.Select(x => x.Quantity).Sum() == Volume);
  168. // All reagents have at least some reagent present.
  169. DebugTools.Assert(!Contents.Any(x => x.Quantity <= FixedPoint2.Zero));
  170. // No duplicate reagents iDs
  171. DebugTools.Assert(Contents.Select(x => x.Reagent).ToHashSet().Count == Contents.Count);
  172. // If it isn't flagged as dirty, check heat capacity is correct.
  173. if (!_heatCapacityDirty)
  174. {
  175. var cur = _heatCapacity;
  176. _heatCapacityDirty = true;
  177. UpdateHeatCapacity(null);
  178. DebugTools.Assert(MathHelper.CloseTo(_heatCapacity, cur, tolerance: 0.01));
  179. }
  180. #endif
  181. }
  182. void ISerializationHooks.AfterDeserialization()
  183. {
  184. Volume = FixedPoint2.Zero;
  185. foreach (var reagent in Contents)
  186. {
  187. Volume += reagent.Quantity;
  188. }
  189. if (MaxVolume == FixedPoint2.Zero)
  190. MaxVolume = Volume;
  191. }
  192. public bool ContainsPrototype(string prototype)
  193. {
  194. foreach (var (reagent, _) in Contents)
  195. {
  196. if (reagent.Prototype == prototype)
  197. return true;
  198. }
  199. return false;
  200. }
  201. public bool ContainsReagent(ReagentId id)
  202. {
  203. foreach (var (reagent, _) in Contents)
  204. {
  205. if (reagent == id)
  206. return true;
  207. }
  208. return false;
  209. }
  210. public bool ContainsReagent(string reagentId, List<ReagentData>? data)
  211. => ContainsReagent(new(reagentId, data));
  212. public bool TryGetReagent(ReagentId id, out ReagentQuantity quantity)
  213. {
  214. foreach (var tuple in Contents)
  215. {
  216. if (tuple.Reagent != id)
  217. continue;
  218. DebugTools.Assert(tuple.Quantity > FixedPoint2.Zero);
  219. quantity = tuple;
  220. return true;
  221. }
  222. quantity = new ReagentQuantity(id, FixedPoint2.Zero);
  223. return false;
  224. }
  225. public bool TryGetReagentQuantity(ReagentId id, out FixedPoint2 volume)
  226. {
  227. volume = FixedPoint2.Zero;
  228. if (!TryGetReagent(id, out var quant))
  229. return false;
  230. volume = quant.Quantity;
  231. return true;
  232. }
  233. [Pure]
  234. public ReagentQuantity GetReagent(ReagentId id)
  235. {
  236. TryGetReagent(id, out var quantity);
  237. return quantity;
  238. }
  239. public ReagentQuantity this[ReagentId id]
  240. {
  241. get
  242. {
  243. if (!TryGetReagent(id, out var quantity))
  244. throw new KeyNotFoundException(id.ToString());
  245. return quantity;
  246. }
  247. }
  248. /// <summary>
  249. /// Get the volume/quantity of a single reagent in the solution.
  250. /// </summary>
  251. [Pure]
  252. public FixedPoint2 GetReagentQuantity(ReagentId id)
  253. {
  254. return GetReagent(id).Quantity;
  255. }
  256. /// <summary>
  257. /// Gets the total volume of all reagents in the solution with the given prototype Id.
  258. /// If you only want the volume of a single reagent, use <see cref="GetReagentQuantity"/>
  259. /// </summary>
  260. [Pure]
  261. public FixedPoint2 GetTotalPrototypeQuantity(params string[] prototypes)
  262. {
  263. var total = FixedPoint2.Zero;
  264. foreach (var (reagent, quantity) in Contents)
  265. {
  266. if (prototypes.Contains(reagent.Prototype))
  267. total += quantity;
  268. }
  269. return total;
  270. }
  271. public FixedPoint2 GetTotalPrototypeQuantity(string id)
  272. {
  273. var total = FixedPoint2.Zero;
  274. foreach (var (reagent, quantity) in Contents)
  275. {
  276. if (id == reagent.Prototype)
  277. total += quantity;
  278. }
  279. return total;
  280. }
  281. public ReagentId? GetPrimaryReagentId()
  282. {
  283. if (Contents.Count == 0)
  284. return null;
  285. ReagentQuantity max = default;
  286. foreach (var reagent in Contents)
  287. {
  288. if (reagent.Quantity >= max.Quantity)
  289. {
  290. max = reagent;
  291. }
  292. }
  293. return max.Reagent;
  294. }
  295. /// <summary>
  296. /// Adds a given quantity of a reagent directly into the solution.
  297. /// </summary>
  298. /// <param name="prototype">The prototype ID of the reagent to add.</param>
  299. /// <param name="quantity">The quantity in milli-units.</param>
  300. public void AddReagent(string prototype, FixedPoint2 quantity, bool dirtyHeatCap = true)
  301. => AddReagent(new ReagentId(prototype, null), quantity, dirtyHeatCap);
  302. /// <summary>
  303. /// Adds a given quantity of a reagent directly into the solution.
  304. /// </summary>
  305. /// <param name="id">The reagent to add.</param>
  306. /// <param name="quantity">The quantity in milli-units.</param>
  307. public void AddReagent(ReagentId id, FixedPoint2 quantity, bool dirtyHeatCap = true)
  308. {
  309. if (quantity <= 0)
  310. {
  311. DebugTools.Assert(quantity == 0, "Attempted to add negative reagent quantity");
  312. return;
  313. }
  314. Volume += quantity;
  315. _heatCapacityDirty |= dirtyHeatCap;
  316. for (var i = 0; i < Contents.Count; i++)
  317. {
  318. var (reagent, existingQuantity) = Contents[i];
  319. if (reagent != id)
  320. continue;
  321. Contents[i] = new ReagentQuantity(id, existingQuantity + quantity);
  322. ValidateSolution();
  323. return;
  324. }
  325. Contents.Add(new ReagentQuantity(id, quantity));
  326. ValidateSolution();
  327. }
  328. /// <summary>
  329. /// Adds a given quantity of a reagent directly into the solution.
  330. /// </summary>
  331. /// <param name="reagentId">The reagent to add.</param>
  332. /// <param name="quantity">The quantity in milli-units.</param>
  333. public void AddReagent(ReagentPrototype proto, ReagentId reagentId, FixedPoint2 quantity)
  334. {
  335. AddReagent(reagentId, quantity, false);
  336. _heatCapacity += quantity.Float() * proto.SpecificHeat;
  337. CheckRecalculateHeatCapacity();
  338. }
  339. public void AddReagent(ReagentQuantity reagentQuantity)
  340. => AddReagent(reagentQuantity.Reagent, reagentQuantity.Quantity);
  341. /// <summary>
  342. /// Adds a given quantity of a reagent directly into the solution.
  343. /// </summary>
  344. /// <param name="proto">The prototype of the reagent to add.</param>
  345. /// <param name="quantity">The quantity in milli-units.</param>
  346. public void AddReagent(ReagentPrototype proto, FixedPoint2 quantity, float temperature, IPrototypeManager? protoMan, List<ReagentData>? data = null)
  347. {
  348. if (_heatCapacityDirty)
  349. UpdateHeatCapacity(protoMan);
  350. var totalThermalEnergy = Temperature * _heatCapacity + temperature * proto.SpecificHeat;
  351. AddReagent(new ReagentId(proto.ID, data), quantity);
  352. Temperature = _heatCapacity == 0 ? 0 : totalThermalEnergy / _heatCapacity;
  353. }
  354. /// <summary>
  355. /// Scales the amount of solution by some integer quantity.
  356. /// </summary>
  357. /// <param name="scale">The scalar to modify the solution by.</param>
  358. public void ScaleSolution(int scale)
  359. {
  360. if (scale == 1)
  361. return;
  362. if (scale <= 0)
  363. {
  364. RemoveAllSolution();
  365. return;
  366. }
  367. _heatCapacity *= scale;
  368. Volume *= scale;
  369. CheckRecalculateHeatCapacity();
  370. for (int i = 0; i < Contents.Count; i++)
  371. {
  372. var old = Contents[i];
  373. Contents[i] = new ReagentQuantity(old.Reagent, old.Quantity * scale);
  374. }
  375. ValidateSolution();
  376. }
  377. /// <summary>
  378. /// Scales the amount of solution.
  379. /// </summary>
  380. /// <param name="scale">The scalar to modify the solution by.</param>
  381. public void ScaleSolution(float scale)
  382. {
  383. if (scale == 1)
  384. return;
  385. if (scale == 0)
  386. {
  387. RemoveAllSolution();
  388. return;
  389. }
  390. Volume = FixedPoint2.Zero;
  391. for (int i = Contents.Count - 1; i >= 0; i--)
  392. {
  393. var old = Contents[i];
  394. var newQuantity = old.Quantity * scale;
  395. if (newQuantity == FixedPoint2.Zero)
  396. Contents.RemoveSwap(i);
  397. else
  398. {
  399. Contents[i] = new ReagentQuantity(old.Reagent, newQuantity);
  400. Volume += newQuantity;
  401. }
  402. }
  403. _heatCapacityDirty = true;
  404. ValidateSolution();
  405. }
  406. /// <summary>
  407. /// Attempts to remove an amount of reagent from the solution.
  408. /// </summary>
  409. /// <param name="toRemove">The reagent to be removed.</param>
  410. /// <returns>How much reagent was actually removed. Zero if the reagent is not present on the solution.</returns>
  411. public FixedPoint2 RemoveReagent(ReagentQuantity toRemove, bool preserveOrder = false, bool ignoreReagentData = false)
  412. {
  413. if (toRemove.Quantity <= FixedPoint2.Zero)
  414. return FixedPoint2.Zero;
  415. List<int> reagentIndices = new List<int>();
  416. int totalRemoveVolume = 0;
  417. for (var i = 0; i < Contents.Count; i++)
  418. {
  419. var (reagent, quantity) = Contents[i];
  420. if (ignoreReagentData)
  421. {
  422. if (reagent.Prototype != toRemove.Reagent.Prototype)
  423. continue;
  424. }
  425. else
  426. {
  427. if (reagent != toRemove.Reagent)
  428. continue;
  429. }
  430. //We prepend instead of add to handle the Contents list back-to-front later down.
  431. //It makes RemoveSwap safe to use.
  432. totalRemoveVolume += quantity.Value;
  433. reagentIndices.Insert(0, i);
  434. }
  435. if (totalRemoveVolume <= 0)
  436. {
  437. // Reagent is not on the solution...
  438. return FixedPoint2.Zero;
  439. }
  440. FixedPoint2 removedQuantity = 0;
  441. for (var i = 0; i < reagentIndices.Count; i++)
  442. {
  443. var (reagent, curQuantity) = Contents[reagentIndices[i]];
  444. // This is set up such that integer rounding will tend to take more reagents.
  445. var split = ((long)toRemove.Quantity.Value) * curQuantity.Value / totalRemoveVolume;
  446. var splitQuantity = FixedPoint2.FromCents((int)split);
  447. var newQuantity = curQuantity - splitQuantity;
  448. _heatCapacityDirty = true;
  449. if (newQuantity <= 0)
  450. {
  451. if (!preserveOrder)
  452. Contents.RemoveSwap(reagentIndices[i]);
  453. else
  454. Contents.RemoveAt(reagentIndices[i]);
  455. Volume -= curQuantity;
  456. removedQuantity += curQuantity;
  457. continue;
  458. }
  459. Contents[reagentIndices[i]] = new ReagentQuantity(reagent, newQuantity);
  460. Volume -= splitQuantity;
  461. removedQuantity += splitQuantity;
  462. }
  463. ValidateSolution();
  464. return removedQuantity;
  465. }
  466. /// <summary>
  467. /// Attempts to remove an amount of reagent from the solution.
  468. /// </summary>
  469. /// <param name="prototype">The prototype of the reagent to be removed.</param>
  470. /// <param name="quantity">The amount of reagent to remove.</param>
  471. /// <returns>How much reagent was actually removed. Zero if the reagent is not present on the solution.</returns>
  472. public FixedPoint2 RemoveReagent(string prototype, FixedPoint2 quantity, List<ReagentData>? data = null, bool ignoreReagentData = false)
  473. {
  474. return RemoveReagent(new ReagentQuantity(prototype, quantity, data), ignoreReagentData: ignoreReagentData);
  475. }
  476. /// <summary>
  477. /// Attempts to remove an amount of reagent from the solution.
  478. /// </summary>
  479. /// <param name="reagentId">The reagent to be removed.</param>
  480. /// <param name="quantity">The amount of reagent to remove.</param>
  481. /// <returns>How much reagent was actually removed. Zero if the reagent is not present on the solution.</returns>
  482. public FixedPoint2 RemoveReagent(ReagentId reagentId, FixedPoint2 quantity, bool preserveOrder = false, bool ignoreReagentData = false)
  483. {
  484. return RemoveReagent(new ReagentQuantity(reagentId, quantity), preserveOrder, ignoreReagentData);
  485. }
  486. public void RemoveAllSolution()
  487. {
  488. Contents.Clear();
  489. Volume = FixedPoint2.Zero;
  490. _heatCapacityDirty = false;
  491. _heatCapacity = 0;
  492. }
  493. /// <summary>
  494. /// Splits a solution without the specified reagent prototypes.
  495. /// </summary>
  496. public Solution SplitSolutionWithout(FixedPoint2 toTake, params string[] excludedPrototypes)
  497. {
  498. // First remove the blacklisted prototypes
  499. List<ReagentQuantity> excluded = new();
  500. foreach (var id in excludedPrototypes)
  501. {
  502. foreach (var tuple in Contents)
  503. {
  504. if (tuple.Reagent.Prototype != id)
  505. continue;
  506. excluded.Add(tuple);
  507. RemoveReagent(tuple);
  508. break;
  509. }
  510. }
  511. // Then split the solution
  512. var sol = SplitSolution(toTake);
  513. // Then re-add the excluded reagents to the original solution.
  514. foreach (var reagent in excluded)
  515. {
  516. AddReagent(reagent);
  517. }
  518. return sol;
  519. }
  520. /// <summary>
  521. /// Splits a solution with only the specified reagent prototypes.
  522. /// </summary>
  523. public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes)
  524. {
  525. // First remove the non-included prototypes
  526. List<ReagentQuantity> excluded = new();
  527. for (var i = Contents.Count - 1; i >= 0; i--)
  528. {
  529. if (includedPrototypes.Contains(Contents[i].Reagent.Prototype))
  530. continue;
  531. excluded.Add(Contents[i]);
  532. RemoveReagent(Contents[i]);
  533. }
  534. // Then split the solution
  535. var sol = SplitSolution(toTake);
  536. // Then re-add the excluded reagents to the original solution.
  537. foreach (var reagent in excluded)
  538. {
  539. AddReagent(reagent);
  540. }
  541. return sol;
  542. }
  543. public Solution SplitSolution(FixedPoint2 toTake)
  544. {
  545. if (toTake <= FixedPoint2.Zero)
  546. return new Solution();
  547. Solution newSolution;
  548. if (toTake >= Volume)
  549. {
  550. newSolution = Clone();
  551. RemoveAllSolution();
  552. return newSolution;
  553. }
  554. var origVol = Volume;
  555. var effVol = Volume.Value;
  556. newSolution = new Solution(Contents.Count) { Temperature = Temperature };
  557. var remaining = (long) toTake.Value;
  558. for (var i = Contents.Count - 1; i >= 0; i--) // iterate backwards because of remove swap.
  559. {
  560. var (reagent, quantity) = Contents[i];
  561. // This is set up such that integer rounding will tend to take more reagents.
  562. var split = remaining * quantity.Value / effVol;
  563. if (split <= 0)
  564. {
  565. effVol -= quantity.Value;
  566. DebugTools.Assert(split == 0, "Negative solution quantity while splitting? Long/int overflow?");
  567. continue;
  568. }
  569. var splitQuantity = FixedPoint2.FromCents((int) split);
  570. var newQuantity = quantity - splitQuantity;
  571. DebugTools.Assert(newQuantity >= 0);
  572. if (newQuantity > FixedPoint2.Zero)
  573. Contents[i] = new ReagentQuantity(reagent, newQuantity);
  574. else
  575. Contents.RemoveSwap(i);
  576. newSolution.Contents.Add(new ReagentQuantity(reagent, splitQuantity));
  577. Volume -= splitQuantity;
  578. remaining -= split;
  579. effVol -= quantity.Value;
  580. }
  581. newSolution.Volume = origVol - Volume;
  582. DebugTools.Assert(remaining >= 0);
  583. DebugTools.Assert(remaining == 0 || Volume == FixedPoint2.Zero);
  584. _heatCapacityDirty = true;
  585. newSolution._heatCapacityDirty = true;
  586. ValidateSolution();
  587. newSolution.ValidateSolution();
  588. return newSolution;
  589. }
  590. /// <summary>
  591. /// Variant of <see cref="SplitSolution(FixedPoint2)"/> that doesn't return a new solution containing the removed reagents.
  592. /// </summary>
  593. /// <param name="toTake">The quantity of this solution to remove</param>
  594. public void RemoveSolution(FixedPoint2 toTake)
  595. {
  596. if (toTake <= FixedPoint2.Zero)
  597. return;
  598. if (toTake >= Volume)
  599. {
  600. RemoveAllSolution();
  601. return;
  602. }
  603. var effVol = Volume.Value;
  604. Volume -= toTake;
  605. var remaining = (long) toTake.Value;
  606. for (var i = Contents.Count - 1; i >= 0; i--)// iterate backwards because of remove swap.
  607. {
  608. var (reagent, quantity) = Contents[i];
  609. // This is set up such that integer rounding will tend to take more reagents.
  610. var split = remaining * quantity.Value / effVol;
  611. if (split <= 0)
  612. {
  613. effVol -= quantity.Value;
  614. DebugTools.Assert(split == 0, "Negative solution quantity while splitting? Long/int overflow?");
  615. continue;
  616. }
  617. var splitQuantity = FixedPoint2.FromCents((int) split);
  618. var newQuantity = quantity - splitQuantity;
  619. if (newQuantity > FixedPoint2.Zero)
  620. Contents[i] = new ReagentQuantity(reagent, newQuantity);
  621. else
  622. Contents.RemoveSwap(i);
  623. remaining -= split;
  624. effVol -= quantity.Value;
  625. }
  626. DebugTools.Assert(remaining >= 0);
  627. DebugTools.Assert(remaining == 0 || Volume == FixedPoint2.Zero);
  628. _heatCapacityDirty = true;
  629. ValidateSolution();
  630. }
  631. public void AddSolution(Solution otherSolution, IPrototypeManager? protoMan)
  632. {
  633. if (otherSolution.Volume <= FixedPoint2.Zero)
  634. return;
  635. Volume += otherSolution.Volume;
  636. var closeTemps = MathHelper.CloseTo(otherSolution.Temperature, Temperature);
  637. float totalThermalEnergy = 0;
  638. if (!closeTemps)
  639. {
  640. IoCManager.Resolve(ref protoMan);
  641. if (_heatCapacityDirty)
  642. UpdateHeatCapacity(protoMan);
  643. if (otherSolution._heatCapacityDirty)
  644. otherSolution.UpdateHeatCapacity(protoMan);
  645. totalThermalEnergy = _heatCapacity * Temperature + otherSolution._heatCapacity * otherSolution.Temperature;
  646. }
  647. for (var i = 0; i < otherSolution.Contents.Count; i++)
  648. {
  649. var (otherReagent, otherQuantity) = otherSolution.Contents[i];
  650. var found = false;
  651. for (var j = 0; j < Contents.Count; j++)
  652. {
  653. var (reagent, quantity) = Contents[j];
  654. if (reagent == otherReagent)
  655. {
  656. found = true;
  657. Contents[j] = new ReagentQuantity(reagent, quantity + otherQuantity);
  658. break;
  659. }
  660. }
  661. if (!found)
  662. {
  663. Contents.Add(new ReagentQuantity(otherReagent, otherQuantity));
  664. }
  665. }
  666. _heatCapacity += otherSolution._heatCapacity;
  667. CheckRecalculateHeatCapacity();
  668. if (closeTemps)
  669. _heatCapacityDirty |= otherSolution._heatCapacityDirty;
  670. else
  671. Temperature = _heatCapacity == 0 ? 0 : totalThermalEnergy / _heatCapacity;
  672. ValidateSolution();
  673. }
  674. public Color GetColorWithout(IPrototypeManager? protoMan, params string[] without)
  675. {
  676. if (Volume == FixedPoint2.Zero)
  677. {
  678. return Color.Transparent;
  679. }
  680. IoCManager.Resolve(ref protoMan);
  681. Color mixColor = default;
  682. var runningTotalQuantity = FixedPoint2.New(0);
  683. bool first = true;
  684. foreach (var (reagent, quantity) in Contents)
  685. {
  686. if (without.Contains(reagent.Prototype))
  687. continue;
  688. runningTotalQuantity += quantity;
  689. if (!protoMan.TryIndex(reagent.Prototype, out ReagentPrototype? proto))
  690. {
  691. continue;
  692. }
  693. if (first)
  694. {
  695. first = false;
  696. mixColor = proto.SubstanceColor;
  697. continue;
  698. }
  699. var interpolateValue = quantity.Float() / runningTotalQuantity.Float();
  700. mixColor = Color.InterpolateBetween(mixColor, proto.SubstanceColor, interpolateValue);
  701. }
  702. return mixColor;
  703. }
  704. public Color GetColor(IPrototypeManager? protoMan)
  705. {
  706. return GetColorWithout(protoMan);
  707. }
  708. public Color GetColorWithOnly(IPrototypeManager? protoMan, params string[] included)
  709. {
  710. if (Volume == FixedPoint2.Zero)
  711. {
  712. return Color.Transparent;
  713. }
  714. IoCManager.Resolve(ref protoMan);
  715. Color mixColor = default;
  716. var runningTotalQuantity = FixedPoint2.New(0);
  717. bool first = true;
  718. foreach (var (reagent, quantity) in Contents)
  719. {
  720. if (!included.Contains(reagent.Prototype))
  721. continue;
  722. runningTotalQuantity += quantity;
  723. if (!protoMan.TryIndex(reagent.Prototype, out ReagentPrototype? proto))
  724. {
  725. continue;
  726. }
  727. if (first)
  728. {
  729. first = false;
  730. mixColor = proto.SubstanceColor;
  731. continue;
  732. }
  733. var interpolateValue = quantity.Float() / runningTotalQuantity.Float();
  734. mixColor = Color.InterpolateBetween(mixColor, proto.SubstanceColor, interpolateValue);
  735. }
  736. return mixColor;
  737. }
  738. #region Enumeration
  739. public IEnumerator<ReagentQuantity> GetEnumerator()
  740. {
  741. return Contents.GetEnumerator();
  742. }
  743. IEnumerator IEnumerable.GetEnumerator()
  744. {
  745. return GetEnumerator();
  746. }
  747. #endregion
  748. public void SetContents(IEnumerable<ReagentQuantity> reagents, bool setMaxVol = false)
  749. {
  750. Volume = 0;
  751. RemoveAllSolution();
  752. _heatCapacityDirty = true;
  753. Contents = new(reagents);
  754. foreach (var reagent in Contents)
  755. {
  756. Volume += reagent.Quantity;
  757. }
  758. if (setMaxVol)
  759. MaxVolume = Volume;
  760. ValidateSolution();
  761. }
  762. public Dictionary<ReagentPrototype, FixedPoint2> GetReagentPrototypes(IPrototypeManager protoMan)
  763. {
  764. var dict = new Dictionary<ReagentPrototype, FixedPoint2>(Contents.Count);
  765. foreach (var (reagent, quantity) in Contents)
  766. {
  767. var proto = protoMan.Index<ReagentPrototype>(reagent.Prototype);
  768. dict[proto] = quantity + dict.GetValueOrDefault(proto);
  769. }
  770. return dict;
  771. }
  772. }
  773. }