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 <pyronetics01@gmail.com>
This commit is contained in:
py01
2021-01-07 00:31:43 -06:00
committed by GitHub
parent 429851140a
commit 2b195fccb9
9 changed files with 179 additions and 126 deletions

View File

@@ -1,4 +1,4 @@
using System; using System;
using Content.Server.Explosions; using Content.Server.Explosions;
using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.Interfaces.Chemistry; using Content.Server.Interfaces.Chemistry;

View File

@@ -1,7 +1,7 @@
#nullable enable #nullable enable
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Utility; using Content.Server.Utility;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.Localization; using Robust.Shared.Localization;

View File

@@ -16,6 +16,7 @@ using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Maths; using Robust.Shared.Maths;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
@@ -35,6 +36,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private IEnumerable<ReactionPrototype> _reactions; private IEnumerable<ReactionPrototype> _reactions;
private ChemicalReactionSystem _reactionSystem;
private string _fillInitState; private string _fillInitState;
private int _fillInitSteps; private int _fillInitSteps;
private string _fillPathString = "Objects/Specific/Chemistry/fillings.rsi"; private string _fillPathString = "Objects/Specific/Chemistry/fillings.rsi";
@@ -49,7 +51,6 @@ namespace Content.Server.GameObjects.Components.Chemistry
/// </summary> /// </summary>
[ViewVariables] [ViewVariables]
public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume; public ReagentUnit EmptyVolume => MaxVolume - CurrentVolume;
public IReadOnlyList<Solution.ReagentQuantity> ReagentList => Solution.Contents; public IReadOnlyList<Solution.ReagentQuantity> ReagentList => Solution.Contents;
public bool CanExamineContents => Capabilities.HasCap(SolutionContainerCaps.CanExamine); public bool CanExamineContents => Capabilities.HasCap(SolutionContainerCaps.CanExamine);
public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser); public bool CanUseWithChemDispenser => Capabilities.HasCap(SolutionContainerCaps.FitsInDispenser);
@@ -74,6 +75,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
_audioSystem = EntitySystem.Get<AudioSystem>(); _audioSystem = EntitySystem.Get<AudioSystem>();
_chemistrySystem = _entitySystemManager.GetEntitySystem<ChemistrySystem>(); _chemistrySystem = _entitySystemManager.GetEntitySystem<ChemistrySystem>();
_reactions = _prototypeManager.EnumeratePrototypes<ReactionPrototype>(); _reactions = _prototypeManager.EnumeratePrototypes<ReactionPrototype>();
_reactionSystem = _entitySystemManager.GetEntitySystem<ChemicalReactionSystem>();
} }
protected override void Startup() protected override void Startup()
@@ -98,7 +100,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
public override bool TryRemoveReagent(string reagentId, ReagentUnit quantity) public override bool TryRemoveReagent(string reagentId, ReagentUnit quantity)
{ {
if (!ContainsReagent(reagentId, out var currentQuantity)) if (!Solution.ContainsReagent(reagentId, out var currentQuantity))
{ {
return false; return false;
} }
@@ -289,29 +291,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
private void CheckForReaction() private void CheckForReaction()
{ {
bool checkForNewReaction = false; _reactionSystem.FullyReactSolution(Solution, Owner, MaxVolume);
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;
}
} }
public bool TryAddReagent(string reagentId, ReagentUnit quantity, out ReagentUnit acceptedQuantity, bool skipReactionCheck = false, bool skipColor = false) 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; return true;
} }
/// <summary>
/// Checks if a solution has the reactants required to cause a specified reaction.
/// </summary>
/// <param name="solution">The solution to check for reaction conditions.</param>
/// <param name="reaction">The reaction whose reactants will be checked for in the solution.</param>
/// <param name="unitReactions">The number of times the reaction can occur with the given solution.</param>
/// <returns></returns>
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;
}
}
/// <summary>
/// Perform a reaction on a solution. This assumes all reaction criteria have already been checked and are met.
/// </summary>
/// <param name="solution">Solution to be reacted.</param>
/// <param name="reaction">Reaction to occur.</param>
/// <param name="unitReactions">The number of times to cause this reaction.</param>
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);
}
/// <summary>
/// Check if the solution contains the specified reagent.
/// </summary>
/// <param name="reagentId">The reagent to check for.</param>
/// <param name="quantity">Output the quantity of the reagent if it is contained, 0 if it isn't.</param>
/// <returns>Return true if the solution contains the reagent.</returns>
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() protected void UpdateFillIcon()
{ {
if (string.IsNullOrEmpty(_fillInitState)) if (string.IsNullOrEmpty(_fillInitState))

View File

@@ -1,6 +1,6 @@
#nullable enable #nullable enable
using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Chemistry; using Content.Shared.Chemistry;
using Content.Shared.GameObjects.EntitySystems;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -71,7 +71,7 @@ namespace Content.Server.GameObjects.Components.Chemistry
} }
//the biggest reagent in the solution decides the appearance //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 biggest reagent didn't changed - don't change anything at all
if (_currentReagent != null && _currentReagent.ID == reagentId) if (_currentReagent != null && _currentReagent.ID == reagentId)

View File

@@ -1,4 +1,4 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -7,7 +7,6 @@ using Content.Server.GameObjects.Components.Chemistry;
using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.GUI;
using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Items.Storage;
using Content.Server.GameObjects.Components.Power.ApcNetComponents; using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces.Chat; using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameObjects; using Content.Server.Interfaces.GameObjects;
using Content.Server.Utility; using Content.Server.Utility;
@@ -15,6 +14,7 @@ using Content.Shared.Chemistry;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Content.Shared.GameObjects.Components.Body.Part; using Content.Shared.GameObjects.Components.Body.Part;
using Content.Shared.GameObjects.Components.Power; using Content.Shared.GameObjects.Components.Power;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.Interfaces; using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components; using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Kitchen; using Content.Shared.Kitchen;
@@ -32,7 +32,6 @@ using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Timers;
using Robust.Shared.ViewVariables; using Robust.Shared.ViewVariables;
namespace Content.Server.GameObjects.Components.Kitchen namespace Content.Server.GameObjects.Components.Kitchen
@@ -441,7 +440,7 @@ namespace Content.Server.GameObjects.Components.Kitchen
foreach (var reagent in recipe.IngredientsReagents) 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; return MicrowaveSuccessState.RecipeFail;
} }

View File

@@ -1,12 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using Content.Server.Interfaces.Chemistry; using Content.Server.Interfaces.Chemistry;
using Content.Shared.Chemistry; using Content.Shared.Interfaces;
using Robust.Shared.Interfaces.Serialization; using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.IoC;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using YamlDotNet.RepresentationModel; using YamlDotNet.RepresentationModel;
namespace Content.Server.Chemistry namespace Content.Shared.Chemistry
{ {
/// <summary> /// <summary>
/// Prototype for chemical reaction definitions /// Prototype for chemical reaction definitions
@@ -35,6 +36,8 @@ namespace Content.Server.Chemistry
/// </summary> /// </summary>
public IReadOnlyList<IReactionEffect> Effects => _effects; public IReadOnlyList<IReactionEffect> Effects => _effects;
[Dependency] private readonly IModuleManager _moduleManager = default!;
public void LoadFrom(YamlMappingNode mapping) public void LoadFrom(YamlMappingNode mapping)
{ {
var serializer = YamlObjectSerializer.NewReader(mapping); var serializer = YamlObjectSerializer.NewReader(mapping);
@@ -43,7 +46,13 @@ namespace Content.Server.Chemistry
serializer.DataField(ref _name, "name", string.Empty); serializer.DataField(ref _name, "name", string.Empty);
serializer.DataField(ref _reactants, "reactants", new Dictionary<string, ReactantPrototype>()); serializer.DataField(ref _reactants, "reactants", new Dictionary<string, ReactantPrototype>());
serializer.DataField(ref _products, "products", new Dictionary<string, ReagentUnit>()); serializer.DataField(ref _products, "products", new Dictionary<string, ReagentUnit>());
serializer.DataField(ref _effects, "effects", new List<IReactionEffect>());
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<IReactionEffect>());
}
} }
} }

View File

@@ -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<ReactionPrototype> _reactions;
private const int MaxReactionIterations = 20;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
_reactions = _prototypeManager.EnumeratePrototypes<ReactionPrototype>();
}
/// <summary>
/// Checks if a solution can undergo a specified reaction.
/// </summary>
/// <param name="solution">The solution to check.</param>
/// <param name="reaction">The reaction to check.</param>
/// <param name="lowestUnitReactions">How many times this reaction can occur.</param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Continually react a solution until no more reactions occur.
/// </summary>
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.");
}
/// <summary>
/// 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.
/// </summary>
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.");
}
}
}

View File

@@ -1,10 +1,10 @@
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.GameObjects.EntitySystems namespace Content.Shared.GameObjects.EntitySystems
{ {
/// <summary> /// <summary>
/// This interface gives components behavior on whether entities solution (implying SolutionComponent is in place) is changed /// This interface gives components behavior on whether entities solution (implying SolutionComponent is in place) is changed

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Serialization; using Robust.Shared.Interfaces.Serialization;
namespace Content.Server.Interfaces.Chemistry namespace Content.Server.Interfaces.Chemistry