2021-06-09 22:19:39 +02:00
using System.Collections.Generic ;
2021-11-28 07:32:33 -08:00
using System.Diagnostics.CodeAnalysis ;
2021-12-11 17:28:16 -06:00
using Content.Shared.Administration ;
2021-11-22 23:51:43 -07:00
using Content.Shared.Administration.Logs ;
2021-09-06 15:49:44 +02:00
using Content.Shared.Chemistry.Components ;
2021-06-09 22:19:39 +02:00
using Content.Shared.Chemistry.Reagent ;
2021-11-28 14:56:53 +01:00
using Content.Shared.Database ;
2021-11-03 16:48:03 -07:00
using Content.Shared.FixedPoint ;
2021-06-09 22:19:39 +02:00
using Robust.Shared.GameObjects ;
2021-01-07 00:31:43 -06:00
using Robust.Shared.IoC ;
using Robust.Shared.Log ;
using Robust.Shared.Prototypes ;
2021-11-21 00:35:02 -07:00
using Robust.Shared.Random ;
2021-01-07 00:31:43 -06:00
2021-06-09 22:19:39 +02:00
namespace Content.Shared.Chemistry.Reaction
2021-01-07 00:31:43 -06:00
{
2021-01-14 01:06:23 -06:00
public abstract class SharedChemicalReactionSystem : EntitySystem
2021-01-07 00:31:43 -06:00
{
2021-11-28 07:32:33 -08:00
/// <summary>
/// The maximum number of reactions that may occur when a solution is changed.
/// </summary>
2021-01-07 00:31:43 -06:00
private const int MaxReactionIterations = 20 ;
[Dependency] private readonly IPrototypeManager _prototypeManager = default ! ;
2021-11-21 00:35:02 -07:00
[Dependency] private readonly IRobustRandom _random = default ! ;
2021-11-22 23:51:43 -07:00
[Dependency] protected readonly SharedAdminLogSystem _logSystem = default ! ;
2021-12-11 17:28:16 -06:00
[Dependency] private readonly IGamePrototypeLoadManager _gamePrototypeLoadManager = default ! ;
2021-01-07 00:31:43 -06:00
2021-11-28 07:32:33 -08:00
/// <summary>
/// A cache of all existant chemical reactions indexed by one of their
/// required reactants.
/// </summary>
private IDictionary < string , List < ReactionPrototype > > _reactions = default ! ;
2021-01-07 00:31:43 -06:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2021-11-28 07:32:33 -08:00
InitializeReactionCache ( ) ;
_prototypeManager . PrototypesReloaded + = OnPrototypesReloaded ;
2021-12-11 17:28:16 -06:00
_gamePrototypeLoadManager . GamePrototypeLoaded + = InitializeReactionCache ;
2021-11-28 07:32:33 -08:00
}
/// <summary>
/// Handles building the reaction cache.
/// </summary>
private void InitializeReactionCache ( )
{
_reactions = new Dictionary < string , List < ReactionPrototype > > ( ) ;
var reactions = _prototypeManager . EnumeratePrototypes < ReactionPrototype > ( ) ;
foreach ( var reaction in reactions )
{
CacheReaction ( reaction ) ;
}
}
/// <summary>
/// Caches a reaction by its first required reagent.
/// Used to build the reaction cache.
/// </summary>
/// <param name="reaction">A reaction prototype to cache.</param>
private void CacheReaction ( ReactionPrototype reaction )
{
var reagents = reaction . Reactants . Keys ;
foreach ( var reagent in reagents )
{
if ( ! _reactions . TryGetValue ( reagent , out var cache ) )
{
cache = new List < ReactionPrototype > ( ) ;
_reactions . Add ( reagent , cache ) ;
}
cache . Add ( reaction ) ;
return ; // Only need to cache based on the first reagent.
}
}
/// <summary>
/// Updates the reaction cache when the prototypes are reloaded.
/// </summary>
/// <param name="eventArgs">The set of modified prototypes.</param>
private void OnPrototypesReloaded ( PrototypesReloadedEventArgs eventArgs )
{
if ( ! eventArgs . ByType . TryGetValue ( typeof ( ReactionPrototype ) , out var set ) )
return ;
foreach ( var ( reactant , cache ) in _reactions )
{
cache . RemoveAll ( ( reaction ) = > set . Modified . ContainsKey ( reaction . ID ) ) ;
if ( cache . Count = = 0 )
_reactions . Remove ( reactant ) ;
}
foreach ( var prototype in set . Modified . Values )
{
CacheReaction ( ( ReactionPrototype ) prototype ) ;
}
2021-01-07 00:31:43 -06:00
}
/// <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>
2021-11-03 16:48:03 -07:00
private static bool CanReact ( Solution solution , ReactionPrototype reaction , out FixedPoint2 lowestUnitReactions )
2021-01-07 00:31:43 -06:00
{
2021-11-03 16:48:03 -07:00
lowestUnitReactions = FixedPoint2 . MaxValue ;
2021-12-24 01:22:34 -08:00
if ( solution . Temperature < reaction . MinimumTemperature )
{
lowestUnitReactions = FixedPoint2 . Zero ;
return false ;
} else if ( solution . Temperature > reaction . MaximumTemperature )
{
lowestUnitReactions = FixedPoint2 . Zero ;
return false ;
}
2021-01-07 00:31:43 -06:00
foreach ( var reactantData in reaction . Reactants )
{
var reactantName = reactantData . Key ;
var reactantCoefficient = reactantData . Value . Amount ;
if ( ! solution . ContainsReagent ( reactantName , out var reactantQuantity ) )
return false ;
2021-11-27 11:50:14 +13:00
if ( reactantData . Value . Catalyst )
{
// catalyst is not consumed, so will not limit the reaction. But it still needs to be present, and
// for quantized reactions we need to have a minimum amount
if ( reactantQuantity = = FixedPoint2 . Zero | | reaction . Quantized & & reactantQuantity < reactantCoefficient )
return false ;
continue ;
}
2021-01-07 00:31:43 -06:00
var unitReactions = reactantQuantity / reactantCoefficient ;
if ( unitReactions < lowestUnitReactions )
{
lowestUnitReactions = unitReactions ;
}
}
2021-11-27 11:50:14 +13:00
if ( reaction . Quantized )
lowestUnitReactions = ( int ) lowestUnitReactions ;
return lowestUnitReactions > 0 ;
2021-01-07 00:31:43 -06:00
}
/// <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>
2022-01-21 01:38:35 -08:00
private Solution PerformReaction ( Solution solution , EntityUid owner , ReactionPrototype reaction , FixedPoint2 unitReactions )
2021-01-07 00:31:43 -06:00
{
2021-11-21 00:35:02 -07:00
// We do this so that ReagentEffect can have something to work with, even if it's
// a little meaningless.
var randomReagent = _prototypeManager . Index < ReagentPrototype > ( _random . Pick ( reaction . Reactants ) . Key ) ;
2021-01-07 00:31:43 -06:00
//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
2021-09-06 15:49:44 +02:00
var products = new Solution ( ) ;
2021-01-07 00:31:43 -06:00
foreach ( var product in reaction . Products )
{
products . AddReagent ( product . Key , product . Value * unitReactions ) ;
}
// Trigger reaction effects
2022-01-21 01:38:35 -08:00
OnReaction ( solution , reaction , randomReagent , owner , unitReactions ) ;
2021-01-14 01:06:23 -06:00
return products ;
}
2022-01-21 01:38:35 -08:00
protected virtual void OnReaction ( Solution solution , ReactionPrototype reaction , ReagentPrototype randomReagent , EntityUid owner , FixedPoint2 unitReactions )
2021-01-14 01:06:23 -06:00
{
2022-01-21 01:38:35 -08:00
var args = new ReagentEffectArgs ( owner , null , solution ,
2021-11-21 00:35:02 -07:00
randomReagent ,
unitReactions , EntityManager , null ) ;
2021-01-07 00:31:43 -06:00
foreach ( var effect in reaction . Effects )
{
2021-11-21 00:35:02 -07:00
if ( ! effect . ShouldApply ( args ) )
continue ;
2021-11-27 00:31:56 -07:00
if ( effect . ShouldLog )
{
2021-12-05 18:09:01 +01:00
var entity = args . SolutionEntity ;
2021-11-27 00:31:56 -07:00
_logSystem . Add ( LogType . ReagentEffect , effect . LogImpact ,
2021-12-14 00:22:58 +13:00
$"Reaction effect {effect.GetType().Name:effect} of reaction ${reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}" ) ;
2021-11-27 00:31:56 -07:00
}
2021-11-27 01:27:47 -07:00
effect . Effect ( args ) ;
2021-01-07 00:31:43 -06:00
}
}
/// <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>
2021-12-24 01:22:34 -08:00
private bool ProcessReactions ( Solution solution , EntityUid owner , [ MaybeNullWhen ( false ) ] out Solution productSolution )
2021-01-07 00:31:43 -06:00
{
2021-11-28 07:32:33 -08:00
foreach ( var reactant in solution . Contents )
2021-01-07 00:31:43 -06:00
{
2021-11-28 07:32:33 -08:00
if ( ! _reactions . TryGetValue ( reactant . ReagentId , out var reactions ) )
continue ;
foreach ( var reaction in reactions )
2021-01-07 00:31:43 -06:00
{
2021-11-28 07:32:33 -08:00
if ( ! CanReact ( solution , reaction , out var unitReactions ) )
continue ;
2021-12-24 01:22:34 -08:00
productSolution = PerformReaction ( solution , owner , reaction , unitReactions ) ;
2021-11-28 07:32:33 -08:00
return true ;
2021-01-07 00:31:43 -06:00
}
}
2021-11-28 07:32:33 -08:00
productSolution = null ;
return false ;
2021-01-07 00:31:43 -06:00
}
/// <summary>
/// Continually react a solution until no more reactions occur.
/// </summary>
2022-01-21 01:38:35 -08:00
public void FullyReactSolution ( Solution solution , EntityUid owner )
2021-01-07 00:31:43 -06:00
{
for ( var i = 0 ; i < MaxReactionIterations ; i + + )
{
2022-01-21 01:38:35 -08:00
if ( ! ProcessReactions ( solution , owner , out var products ) )
2021-11-28 07:32:33 -08:00
return ;
2021-01-07 00:31:43 -06:00
if ( products . TotalVolume < = 0 )
return ;
solution . AddSolution ( products ) ;
}
2022-01-21 01:38:35 -08:00
Logger . Error ( $"{nameof(Solution)} {owner} could not finish reacting in under {MaxReactionIterations} loops." ) ;
2021-01-07 00:31:43 -06:00
}
/// <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>
2022-01-21 01:38:35 -08:00
public void FullyReactSolution ( Solution solution , EntityUid owner , FixedPoint2 maxVolume )
2021-01-07 00:31:43 -06:00
{
for ( var i = 0 ; i < MaxReactionIterations ; i + + )
{
2022-01-21 01:38:35 -08:00
if ( ! ProcessReactions ( solution , owner , out var products ) )
2021-11-28 07:32:33 -08:00
return ;
2021-01-07 00:31:43 -06:00
if ( products . TotalVolume < = 0 )
return ;
var totalVolume = solution . TotalVolume + products . TotalVolume ;
2021-02-27 04:12:09 +01:00
var excessVolume = totalVolume - maxVolume ;
2021-01-07 00:31:43 -06:00
if ( excessVolume > 0 )
{
products . RemoveSolution ( excessVolume ) ; //excess product is deleted to fit under volume limit
}
solution . AddSolution ( products ) ;
}
2022-01-21 01:38:35 -08:00
Logger . Error ( $"{nameof(Solution)} {owner} could not finish reacting in under {MaxReactionIterations} loops." ) ;
2021-01-07 00:31:43 -06:00
}
}
}