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