diff --git a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs index 048c91e6cb..6ecbade184 100644 --- a/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs +++ b/Content.Shared/Chemistry/Reaction/ReactionPrototype.cs @@ -15,7 +15,7 @@ namespace Content.Shared.Chemistry.Reaction /// Prototype for chemical reaction definitions /// [Prototype("reaction")] - public sealed class ReactionPrototype : IPrototype + public sealed class ReactionPrototype : IPrototype, IComparable { [ViewVariables] [DataField("id", required: true)] @@ -67,6 +67,33 @@ namespace Content.Shared.Chemistry.Reaction /// enough reactants, the reaction does not occur. Useful for spawn-entity reactions (e.g. creating cheese). /// [DataField("quantized")] public bool Quantized = false; + + /// + /// Determines the order in which reactions occur. This should used to ensure that (in general) descriptive / + /// pop-up generating and explosive reactions occur before things like foam/area effects. + /// + [DataField("priority")] + public int Priority; + + /// + /// Comparison for creating a sorted set of reactions. Determines the order in which reactions occur. + /// + public int CompareTo(ReactionPrototype? other) + { + if (other == null) + return -1; + + if (Priority != other.Priority) + return other.Priority - Priority; + + // Prioritize reagents that don't generate products. This should reduce instances where a solution + // temporarily overflows and discards products simply due to the order in which the reactions occurred. + // Basically: Make space in the beaker before adding new products. + if (Products.Count != other.Products.Count) + return Products.Count - other.Products.Count; + + return ID.CompareTo(other.ID); + } } /// diff --git a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs index 10251271a8..4b2dc7ecca 100644 --- a/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs +++ b/Content.Shared/Chemistry/Reaction/SharedChemicalReactionSystem.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Content.Shared.Administration; using Content.Shared.Administration.Logs; using Content.Shared.Chemistry.Components; @@ -211,44 +212,56 @@ namespace Content.Shared.Chemistry.Reaction /// Removes the reactants from the solution, then returns a solution with all products. /// WARNING: Does not trigger reactions between solution and new products. /// - private bool ProcessReactions(Solution solution, EntityUid owner, [MaybeNullWhen(false)] out Solution productSolution) + private bool ProcessReactions(Solution solution, EntityUid owner, FixedPoint2 maxVolume, SortedSet reactions) { - foreach(var reactant in solution.Contents) + HashSet toRemove = new(); + Solution? products = null; + + // attempt to perform any applicable reaction + foreach (var reaction in reactions) { - if (!_reactions.TryGetValue(reactant.ReagentId, out var reactions)) - continue; - - foreach(var reaction in reactions) + if (!CanReact(solution, reaction, out var unitReactions)) { - if (!CanReact(solution, reaction, out var unitReactions)) - continue; - - productSolution = PerformReaction(solution, owner, reaction, unitReactions); - return true; + toRemove.Add(reaction); + continue; } + + products = PerformReaction(solution, owner, reaction, unitReactions); + break; } - productSolution = null; - return false; + // did any reaction occur? + if (products == null) + return false; ; + + // Remove any reactions that were not applicable. Avoids re-iterating over them in future. + reactions.Except(toRemove); + + if (products.TotalVolume <= 0) + return true; + + // remove excess product + // TODO spill excess? + var excessVolume = solution.TotalVolume + products.TotalVolume - maxVolume; + if (excessVolume > 0) + products.RemoveSolution(excessVolume); + + // Add any reactions associated with the new products. This may re-add reactions that were already iterated + // over previously. The new product may mean the reactions are applicable again and need to be processed. + foreach (var reactant in products.Contents) + { + if (_reactions.TryGetValue(reactant.ReagentId, out var reactantReactions)) + reactions.UnionWith(reactantReactions); + } + + solution.AddSolution(products); + return true; } /// /// Continually react a solution until no more reactions occur. /// - public void FullyReactSolution(Solution solution, EntityUid owner) - { - for (var i = 0; i < MaxReactionIterations; i++) - { - if (!ProcessReactions(solution, owner, out var products)) - return; - - if (products.TotalVolume <= 0) - return; - - solution.AddSolution(products); - } - Logger.Error($"{nameof(Solution)} {owner} could not finish reacting in under {MaxReactionIterations} loops."); - } + public void FullyReactSolution(Solution solution, EntityUid owner) => FullyReactSolution(solution, owner, FixedPoint2.MaxValue); /// /// Continually react a solution until no more reactions occur, with a volume constraint. @@ -256,24 +269,22 @@ namespace Content.Shared.Chemistry.Reaction /// public void FullyReactSolution(Solution solution, EntityUid owner, FixedPoint2 maxVolume) { + // construct the initial set of reactions to check. + SortedSet reactions = new(); + foreach (var reactant in solution.Contents) + { + if (_reactions.TryGetValue(reactant.ReagentId, out var reactantReactions)) + reactions.UnionWith(reactantReactions); + } + + // Repeatedly attempt to perform reactions, ending when there are no more applicable reactions, or when we + // exceed the iteration limit. for (var i = 0; i < MaxReactionIterations; i++) { - if (!ProcessReactions(solution, owner, out var products)) + if (!ProcessReactions(solution, owner, maxVolume, reactions)) return; - - if (products.TotalVolume <= 0) - return; - - var totalVolume = solution.TotalVolume + products.TotalVolume; - var excessVolume = totalVolume - maxVolume; - - if (excessVolume > 0) - { - products.RemoveSolution(excessVolume); //excess product is deleted to fit under volume limit - } - - solution.AddSolution(products); } + Logger.Error($"{nameof(Solution)} {owner} could not finish reacting in under {MaxReactionIterations} loops."); } } diff --git a/Resources/Prototypes/Recipes/Reactions/chemicals.yml b/Resources/Prototypes/Recipes/Reactions/chemicals.yml index 0a0c663e71..a871719d15 100644 --- a/Resources/Prototypes/Recipes/Reactions/chemicals.yml +++ b/Resources/Prototypes/Recipes/Reactions/chemicals.yml @@ -81,6 +81,7 @@ - type: reaction id: PotassiumExplosion impact: High + priority: 20 reactants: Water: amount: 1 @@ -98,6 +99,7 @@ - type: reaction id: Smoke + priority: 10 impact: High reactants: Phosphorus: @@ -121,6 +123,7 @@ - type: reaction id: Foam + priority: 10 impact: High reactants: Fluorosurfactant: @@ -146,6 +149,7 @@ - type: reaction id: IronMetalFoam impact: High + priority: 10 reactants: Iron: amount: 3 @@ -172,6 +176,7 @@ - type: reaction id: AluminiumMetalFoam impact: High + priority: 10 reactants: Aluminium: amount: 3 diff --git a/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml b/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml index 62731c9062..22affe6422 100644 --- a/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml +++ b/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml @@ -30,6 +30,7 @@ - type: reaction id: ChlorineTrifluoride + priority: 20 reactants: Chlorine: amount: 1