using System.Collections.Generic; using Content.Shared.Administration.Logs; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.FixedPoint; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Shared.Chemistry.Reaction { public abstract class SharedChemicalReactionSystem : EntitySystem { private IEnumerable _reactions = default!; private const int MaxReactionIterations = 20; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] protected readonly SharedAdminLogSystem _logSystem = 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 FixedPoint2 lowestUnitReactions) { lowestUnitReactions = FixedPoint2.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 Solution PerformReaction(Solution solution, EntityUid ownerUid, ReactionPrototype reaction, FixedPoint2 unitReactions) { // We do this so that ReagentEffect can have something to work with, even if it's // a little meaningless. var randomReagent = _prototypeManager.Index(_random.Pick(reaction.Reactants).Key); //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 OnReaction(solution, reaction, randomReagent, ownerUid, unitReactions); return products; } protected virtual void OnReaction(Solution solution, ReactionPrototype reaction, ReagentPrototype randomReagent, EntityUid ownerUid, FixedPoint2 unitReactions) { var args = new ReagentEffectArgs(ownerUid, null, solution, randomReagent, unitReactions, EntityManager, null); foreach (var effect in reaction.Effects) { if (!effect.ShouldApply(args)) continue; var entity = EntityManager.GetEntity(args.SolutionEntity); _logSystem.Add(LogType.ReagentEffect, LogImpact.Low, $"Reaction effect {effect.GetType().Name} of reaction ${reaction.ID:reaction} applied on entity {entity} at {entity.Transform.Coordinates}"); effect.Effect(args); } } /// /// 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, EntityUid ownerUid) { //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, ownerUid, reaction, unitReactions); overallProducts.AddSolution(reactionProducts); break; } } return overallProducts; } /// /// Continually react a solution until no more reactions occur. /// public void FullyReactSolution(Solution solution, EntityUid ownerUid) { for (var i = 0; i < MaxReactionIterations; i++) { var products = ProcessReactions(solution, ownerUid); if (products.TotalVolume <= 0) return; solution.AddSolution(products); } Logger.Error($"{nameof(Solution)} {ownerUid} 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, EntityUid ownerUid, FixedPoint2 maxVolume) { for (var i = 0; i < MaxReactionIterations; i++) { var products = ProcessReactions(solution, ownerUid); 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)} {ownerUid} could not finish reacting in under {MaxReactionIterations} loops."); } } }