From 2b195fccb9a95cf74c31cd64af72c65bca64f3e7 Mon Sep 17 00:00:00 2001 From: py01 <60152240+collinlunn@users.noreply.github.com> Date: Thu, 7 Jan 2021 00:31:43 -0600 Subject: [PATCH] Chemical reaction refactor (#2936) * Moves ContainsReagent from SolutionContainer to Solution GetMajorReagentId from SOlutionContainer to Solution Makes capability checks use HasFlag Moves Solution Color calculation from SolutionContainer to Solution Replaces SolutionContainerCaps.NoExamine with CanExamine Misc SolutionContainer.Capabilities yaml cleanup * Moves IReactionEffect from server to shared * Moves ReactionPrototype from server to shared * Moves SolutionValidReaction from SolutionContainer to ChemicalReactionSystem * Moves PerformReaction from SolutionContainer to ChemicalReactionSystem * Moves CheckForReaction from SolutionContainer to ChemicalReactionSystem * Removes unused SolutionContainer methods * Removes now-unused GetMajorReagentId from SOlutionContainer * ChemicalReactionSystem comments * Replaces usage of SolutionContainer.ContainsReagent and replaces it with SolutionContainer.Solution.ContainsReagent * ChemicalReactionSystem ProcessReactions * Moves ExplosionReactionEffect to shared, comments out server code, TODO: figure out how to let ReactionEffects in shared do server stuff * Fixes SolutionContainer.CheckForReaction infinite recursion * Moves IReactionEffect and ExplosionReactionEffect back to server * Moves ChemicalReactionSystem and ReactionPrototype back to server * Uncomments out Explosion code * namespace fixes * Moves ReactionPrototype and IReactionEffect from Server to Shared * Moves ChemicalReactionSystem from Server to Shared * ChemicalReaction code partial rewrite * Moves CanReact and PerformReaction to Solution * Revert "Moves CanReact and PerformReaction to Solution" This reverts commit bab791c3ebd0ff39d22f2610e27ca04f0d46d6b8. * Moves ChemistrySystem from Server to Shared * diff fix * TODO warning Co-authored-by: py01 --- .../ExplosionReactionEffect.cs | 2 +- .../Chemistry/RehydratableComponent.cs | 2 +- .../Chemistry/SolutionContainerComponent.cs | 115 +------------- .../TransformableContainerComponent.cs | 6 +- .../Components/Kitchen/MicrowaveComponent.cs | 7 +- .../Chemistry/ReactionPrototype.cs | 17 +- .../EntitySystems/ChemicalReactionSystem.cs | 150 ++++++++++++++++++ .../EntitySystems/ChemistrySystem.cs | 4 +- .../Interfaces/Chemistry/IReactionEffect.cs | 2 +- 9 files changed, 179 insertions(+), 126 deletions(-) rename {Content.Server => Content.Shared}/Chemistry/ReactionPrototype.cs (79%) create mode 100644 Content.Shared/GameObjects/EntitySystems/ChemicalReactionSystem.cs rename {Content.Server => Content.Shared}/GameObjects/EntitySystems/ChemistrySystem.cs (94%) rename {Content.Server => Content.Shared}/Interfaces/Chemistry/IReactionEffect.cs (87%) diff --git a/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs index f552652678..d9cb1b4b3c 100644 --- a/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs +++ b/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs @@ -1,4 +1,4 @@ -using System; +using System; using Content.Server.Explosions; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.Interfaces.Chemistry; diff --git a/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs b/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs index a6194d8efb..e5cc33c2ff 100644 --- a/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/RehydratableComponent.cs @@ -1,7 +1,7 @@ #nullable enable -using Content.Server.GameObjects.EntitySystems; using Content.Server.Utility; using Content.Shared.Chemistry; +using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces.GameObjects.Components; using Robust.Shared.GameObjects; using Robust.Shared.Localization; diff --git a/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs b/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs index 0cfb06bcdc..48cf119f7a 100644 --- a/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/SolutionContainerComponent.cs @@ -16,6 +16,7 @@ using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -35,6 +36,7 @@ namespace Content.Server.GameObjects.Components.Chemistry [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; private IEnumerable _reactions; + private ChemicalReactionSystem _reactionSystem; private string _fillInitState; private int _fillInitSteps; private string _fillPathString = "Objects/Specific/Chemistry/fillings.rsi"; @@ -49,7 +51,6 @@ namespace Content.Server.GameObjects.Components.Chemistry /// [ViewVariables] public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume; - public IReadOnlyList ReagentList => Solution.Contents; public bool CanExamineContents => Capabilities.HasCap(SolutionContainerCaps.CanExamine); public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser); @@ -74,6 +75,7 @@ namespace Content.Server.GameObjects.Components.Chemistry _audioSystem = EntitySystem.Get(); _chemistrySystem = _entitySystemManager.GetEntitySystem(); _reactions = _prototypeManager.EnumeratePrototypes(); + _reactionSystem = _entitySystemManager.GetEntitySystem(); } protected override void Startup() @@ -98,7 +100,7 @@ namespace Content.Server.GameObjects.Components.Chemistry public override bool TryRemoveReagent(string reagentId, ReagentUnit quantity) { - if (!ContainsReagent(reagentId, out var currentQuantity)) + if (!Solution.ContainsReagent(reagentId, out var currentQuantity)) { return false; } @@ -289,29 +291,7 @@ namespace Content.Server.GameObjects.Components.Chemistry private void CheckForReaction() { - bool checkForNewReaction = false; - while (true) - { - //TODO: make a hashmap at startup and then look up reagents in the contents for a reaction - //Check the solution for every reaction - foreach (var reaction in _reactions) - { - if (SolutionValidReaction(reaction, out var unitReactions)) - { - PerformReaction(reaction, unitReactions); - checkForNewReaction = true; - break; - } - } - - //Check for a new reaction if a reaction occurs, run loop again. - if (checkForNewReaction) - { - checkForNewReaction = false; - continue; - } - return; - } + _reactionSystem.FullyReactSolution(Solution, Owner, MaxVolume); } public bool TryAddReagent(string reagentId, ReagentUnit quantity, out ReagentUnit acceptedQuantity, bool skipReactionCheck = false, bool skipColor = false) @@ -357,91 +337,6 @@ namespace Content.Server.GameObjects.Components.Chemistry return true; } - /// - /// Checks if a solution has the reactants required to cause a specified reaction. - /// - /// The solution to check for reaction conditions. - /// The reaction whose reactants will be checked for in the solution. - /// The number of times the reaction can occur with the given solution. - /// - private bool SolutionValidReaction(ReactionPrototype reaction, out ReagentUnit unitReactions) - { - unitReactions = ReagentUnit.MaxValue; //Set to some impossibly large number initially - foreach (var reactant in reaction.Reactants) - { - if (!ContainsReagent(reactant.Key, out ReagentUnit reagentQuantity)) - { - return false; - } - var currentUnitReactions = reagentQuantity / reactant.Value.Amount; - if (currentUnitReactions < unitReactions) - { - unitReactions = currentUnitReactions; - } - } - - if (unitReactions == 0) - { - return false; - } - else - { - return true; - } - } - - /// - /// Perform a reaction on a solution. This assumes all reaction criteria have already been checked and are met. - /// - /// Solution to be reacted. - /// Reaction to occur. - /// The number of times to cause this reaction. - private void PerformReaction(ReactionPrototype reaction, ReagentUnit unitReactions) - { - //Remove non-catalysts - foreach (var reactant in reaction.Reactants) - { - if (!reactant.Value.Catalyst) - { - var amountToRemove = unitReactions * reactant.Value.Amount; - TryRemoveReagent(reactant.Key, amountToRemove); - } - } - - // Add products - foreach (var product in reaction.Products) - { - TryAddReagent(product.Key, product.Value * unitReactions, out var acceptedQuantity, true); - } - - // Trigger reaction effects - foreach (var effect in reaction.Effects) - { - effect.React(Owner, unitReactions.Double()); - } - - // Play reaction sound client-side - _audioSystem.PlayAtCoords("/Audio/Effects/Chemistry/bubbles.ogg", Owner.Transform.Coordinates); - } - - /// - /// Check if the solution contains the specified reagent. - /// - /// The reagent to check for. - /// Output the quantity of the reagent if it is contained, 0 if it isn't. - /// Return true if the solution contains the reagent. - public bool ContainsReagent(string reagentId, out ReagentUnit quantity) - { - var containsReagent = Solution.ContainsReagent(reagentId, out var quantityFound); - quantity = quantityFound; - return containsReagent; - } - - public string GetMajorReagentId() - { - return Solution.GetPrimaryReagentId(); - } - protected void UpdateFillIcon() { if (string.IsNullOrEmpty(_fillInitState)) diff --git a/Content.Server/GameObjects/Components/Chemistry/TransformableContainerComponent.cs b/Content.Server/GameObjects/Components/Chemistry/TransformableContainerComponent.cs index 1eff6c49b2..5fad172db7 100644 --- a/Content.Server/GameObjects/Components/Chemistry/TransformableContainerComponent.cs +++ b/Content.Server/GameObjects/Components/Chemistry/TransformableContainerComponent.cs @@ -1,6 +1,6 @@ -#nullable enable -using Content.Server.GameObjects.EntitySystems; +#nullable enable using Content.Shared.Chemistry; +using Content.Shared.GameObjects.EntitySystems; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -71,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Chemistry } //the biggest reagent in the solution decides the appearance - var reagentId = solution.GetMajorReagentId(); + var reagentId = solution.Solution.GetPrimaryReagentId(); //If biggest reagent didn't changed - don't change anything at all if (_currentReagent != null && _currentReagent.ID == reagentId) diff --git a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs index 2baf98a183..60aca373e1 100644 --- a/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs +++ b/Content.Server/GameObjects/Components/Kitchen/MicrowaveComponent.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -7,7 +7,6 @@ using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Power.ApcNetComponents; -using Content.Server.GameObjects.EntitySystems; using Content.Server.Interfaces.Chat; using Content.Server.Interfaces.GameObjects; using Content.Server.Utility; @@ -15,6 +14,7 @@ using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Power; +using Content.Shared.GameObjects.EntitySystems; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Kitchen; @@ -32,7 +32,6 @@ using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Serialization; -using Robust.Shared.Timers; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Kitchen @@ -441,7 +440,7 @@ namespace Content.Server.GameObjects.Components.Kitchen foreach (var reagent in recipe.IngredientsReagents) { - if (!solution.ContainsReagent(reagent.Key, out var amount)) + if (!solution.Solution.ContainsReagent(reagent.Key, out var amount)) { return MicrowaveSuccessState.RecipeFail; } diff --git a/Content.Server/Chemistry/ReactionPrototype.cs b/Content.Shared/Chemistry/ReactionPrototype.cs similarity index 79% rename from Content.Server/Chemistry/ReactionPrototype.cs rename to Content.Shared/Chemistry/ReactionPrototype.cs index 3cfef8c4d9..1c0911da95 100644 --- a/Content.Server/Chemistry/ReactionPrototype.cs +++ b/Content.Shared/Chemistry/ReactionPrototype.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Content.Server.Interfaces.Chemistry; -using Content.Shared.Chemistry; +using Content.Shared.Interfaces; using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.IoC; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using YamlDotNet.RepresentationModel; -namespace Content.Server.Chemistry +namespace Content.Shared.Chemistry { /// /// Prototype for chemical reaction definitions @@ -35,6 +36,8 @@ namespace Content.Server.Chemistry /// public IReadOnlyList Effects => _effects; + [Dependency] private readonly IModuleManager _moduleManager = default!; + public void LoadFrom(YamlMappingNode mapping) { var serializer = YamlObjectSerializer.NewReader(mapping); @@ -43,7 +46,13 @@ namespace Content.Server.Chemistry serializer.DataField(ref _name, "name", string.Empty); serializer.DataField(ref _reactants, "reactants", new Dictionary()); serializer.DataField(ref _products, "products", new Dictionary()); - serializer.DataField(ref _effects, "effects", new List()); + + if (_moduleManager.IsServerModule) + { + //TODO: Don't have a check for if this is the server + //Some implementations of IReactionEffect can't currently be moved to shared, so this is here to prevent the client from breaking when reading server-only IReactionEffects. + serializer.DataField(ref _effects, "effects", new List()); + } } } diff --git a/Content.Shared/GameObjects/EntitySystems/ChemicalReactionSystem.cs b/Content.Shared/GameObjects/EntitySystems/ChemicalReactionSystem.cs new file mode 100644 index 0000000000..0c43fff0d9 --- /dev/null +++ b/Content.Shared/GameObjects/EntitySystems/ChemicalReactionSystem.cs @@ -0,0 +1,150 @@ +using Content.Shared.Chemistry; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using System.Collections.Generic; + +namespace Content.Shared.GameObjects.EntitySystems +{ + public class ChemicalReactionSystem : EntitySystem + { + private IEnumerable _reactions; + + private const int MaxReactionIterations = 20; + + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public override void Initialize() + { + base.Initialize(); + _reactions = _prototypeManager.EnumeratePrototypes(); + } + + /// + /// Checks if a solution can undergo a specified reaction. + /// + /// The solution to check. + /// The reaction to check. + /// How many times this reaction can occur. + /// + private static bool CanReact(Solution solution, ReactionPrototype reaction, out ReagentUnit lowestUnitReactions) + { + lowestUnitReactions = ReagentUnit.MaxValue; + + foreach (var reactantData in reaction.Reactants) + { + var reactantName = reactantData.Key; + var reactantCoefficient = reactantData.Value.Amount; + + if (!solution.ContainsReagent(reactantName, out var reactantQuantity)) + return false; + + var unitReactions = reactantQuantity / reactantCoefficient; + + if (unitReactions < lowestUnitReactions) + { + lowestUnitReactions = unitReactions; + } + } + return true; + } + + /// + /// Perform a reaction on a solution. This assumes all reaction criteria are met. + /// Removes the reactants from the solution, then returns a solution with all products. + /// + private static Solution PerformReaction(Solution solution, IEntity owner, ReactionPrototype reaction, ReagentUnit unitReactions) + { + //Remove reactants + foreach (var reactant in reaction.Reactants) + { + if (!reactant.Value.Catalyst) + { + var amountToRemove = unitReactions * reactant.Value.Amount; + solution.RemoveReagent(reactant.Key, amountToRemove); + } + } + + //Create products + var products = new Solution(); + foreach (var product in reaction.Products) + { + products.AddReagent(product.Key, product.Value * unitReactions); + } + + // Trigger reaction effects + foreach (var effect in reaction.Effects) + { + effect.React(owner, unitReactions.Double()); + } + + return products; + } + + /// + /// Performs all chemical reactions that can be run on a solution. + /// Removes the reactants from the solution, then returns a solution with all products. + /// WARNING: Does not trigger reactions between solution and new products. + /// + private Solution ProcessReactions(Solution solution, IEntity owner) + { + //TODO: make a hashmap at startup and then look up reagents in the contents for a reaction + var overallProducts = new Solution(); + foreach (var reaction in _reactions) + { + if (CanReact(solution, reaction, out var unitReactions)) + { + var reactionProducts = PerformReaction(solution, owner, reaction, unitReactions); + overallProducts.AddSolution(reactionProducts); + break; + } + } + return overallProducts; + } + + /// + /// Continually react a solution until no more reactions occur. + /// + public void FullyReactSolution(Solution solution, IEntity owner) + { + for (var i = 0; i < MaxReactionIterations; i++) + { + var products = ProcessReactions(solution, owner); + + if (products.TotalVolume <= 0) + return; + + solution.AddSolution(products); + } + Logger.Error($"{nameof(Solution)} on {owner} (Uid: {owner.Uid}) could not finish reacting in under {MaxReactionIterations} loops."); + } + + /// + /// Continually react a solution until no more reactions occur, with a volume constraint. + /// If a reaction's products would exceed the max volume, some product is deleted. + /// + public void FullyReactSolution(Solution solution, IEntity owner, ReagentUnit maxVolume) + { + for (var i = 0; i < MaxReactionIterations; i++) + { + var products = ProcessReactions(solution, owner); + + 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)} on {owner} (Uid: {owner.Uid}) could not finish reacting in under {MaxReactionIterations} loops."); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/ChemistrySystem.cs b/Content.Shared/GameObjects/EntitySystems/ChemistrySystem.cs similarity index 94% rename from Content.Server/GameObjects/EntitySystems/ChemistrySystem.cs rename to Content.Shared/GameObjects/EntitySystems/ChemistrySystem.cs index 69bd555460..9c414cd6f8 100644 --- a/Content.Server/GameObjects/EntitySystems/ChemistrySystem.cs +++ b/Content.Shared/GameObjects/EntitySystems/ChemistrySystem.cs @@ -1,10 +1,10 @@ -using System; +using System; using System.Linq; using JetBrains.Annotations; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; -namespace Content.Server.GameObjects.EntitySystems +namespace Content.Shared.GameObjects.EntitySystems { /// /// This interface gives components behavior on whether entities solution (implying SolutionComponent is in place) is changed diff --git a/Content.Server/Interfaces/Chemistry/IReactionEffect.cs b/Content.Shared/Interfaces/Chemistry/IReactionEffect.cs similarity index 87% rename from Content.Server/Interfaces/Chemistry/IReactionEffect.cs rename to Content.Shared/Interfaces/Chemistry/IReactionEffect.cs index 36209aef3c..21a4ea91f9 100644 --- a/Content.Server/Interfaces/Chemistry/IReactionEffect.cs +++ b/Content.Shared/Interfaces/Chemistry/IReactionEffect.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Serialization; namespace Content.Server.Interfaces.Chemistry