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 ;
2023-11-27 22:12:34 +11:00
using Robust.Shared.Audio.Systems ;
2021-01-07 00:31:43 -06:00
using Robust.Shared.Prototypes ;
2023-12-22 09:13:45 -05:00
using Robust.Shared.Utility ;
2023-12-29 04:47:43 -08:00
using System.Collections.Frozen ;
using System.Linq ;
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
{
2023-05-13 15:10:32 +12:00
public sealed class ChemicalReactionSystem : 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 ! ;
2023-05-13 15:10:32 +12:00
[Dependency] private readonly SharedAudioSystem _audio = default ! ;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default ! ;
2021-01-07 00:31:43 -06:00
2021-11-28 07:32:33 -08:00
/// <summary>
2023-12-22 09:13:45 -05:00
/// A cache of all reactions indexed by at most ONE of their required reactants.
/// I.e., even if a reaction has more than one reagent, it will only ever appear once in this dictionary.
2021-11-28 07:32:33 -08:00
/// </summary>
2023-12-22 09:13:45 -05:00
private FrozenDictionary < string , List < ReactionPrototype > > _reactionsSingle = default ! ;
/// <summary>
/// A cache of all reactions indexed by one of their required reactants.
/// </summary>
private FrozenDictionary < string , List < ReactionPrototype > > _reactions = default ! ;
2021-11-28 07:32:33 -08:00
2021-01-07 00:31:43 -06:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2021-11-28 07:32:33 -08:00
InitializeReactionCache ( ) ;
2023-12-22 09:13:45 -05:00
SubscribeLocalEvent < PrototypesReloadedEventArgs > ( OnPrototypesReloaded ) ;
2021-11-28 07:32:33 -08:00
}
/// <summary>
/// Handles building the reaction cache.
/// </summary>
private void InitializeReactionCache ( )
{
2023-12-22 09:13:45 -05:00
// Construct single-reaction dictionary.
var dict = new Dictionary < string , List < ReactionPrototype > > ( ) ;
2023-12-29 04:47:43 -08:00
foreach ( var reaction in _prototypeManager . EnumeratePrototypes < ReactionPrototype > ( ) )
2021-11-28 07:32:33 -08:00
{
2023-12-22 09:13:45 -05:00
// For this dictionary we only need to cache based on the first reagent.
var reagent = reaction . Reactants . Keys . First ( ) ;
var list = dict . GetOrNew ( reagent ) ;
list . Add ( reaction ) ;
2021-11-28 07:32:33 -08:00
}
2023-12-22 09:13:45 -05:00
_reactionsSingle = dict . ToFrozenDictionary ( ) ;
2021-11-28 07:32:33 -08:00
2023-12-22 09:13:45 -05:00
dict . Clear ( ) ;
2023-12-29 04:47:43 -08:00
foreach ( var reaction in _prototypeManager . EnumeratePrototypes < ReactionPrototype > ( ) )
2021-11-28 07:32:33 -08:00
{
2023-12-22 09:13:45 -05:00
foreach ( var reagent in reaction . Reactants . Keys )
2021-11-28 07:32:33 -08:00
{
2023-12-22 09:13:45 -05:00
var list = dict . GetOrNew ( reagent ) ;
list . Add ( reaction ) ;
2021-11-28 07:32:33 -08:00
}
}
2023-12-22 09:13:45 -05:00
_reactions = dict . ToFrozenDictionary ( ) ;
2021-11-28 07:32:33 -08:00
}
/// <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 )
{
2023-12-22 09:13:45 -05:00
if ( eventArgs . WasModified < ReactionPrototype > ( ) )
InitializeReactionCache ( ) ;
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>
2023-12-29 04:47:43 -08:00
private bool CanReact ( Entity < SolutionComponent > soln , ReactionPrototype reaction , ReactionMixerComponent ? mixerComponent , out FixedPoint2 lowestUnitReactions )
2021-01-07 00:31:43 -06:00
{
2023-12-29 04:47:43 -08:00
var solution = soln . Comp . Solution ;
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 ;
2023-12-15 04:52:46 -05:00
}
if ( solution . Temperature > reaction . MaximumTemperature )
2021-12-24 01:22:34 -08:00
{
lowestUnitReactions = FixedPoint2 . Zero ;
return false ;
}
2021-01-07 00:31:43 -06:00
2023-12-29 04:47:43 -08:00
if ( ( mixerComponent = = null & & reaction . MixingCategories ! = null ) | |
2022-12-20 04:05:02 +00:00
mixerComponent ! = null & & reaction . MixingCategories ! = null & & reaction . MixingCategories . Except ( mixerComponent . ReactionTypes ) . Any ( ) )
{
lowestUnitReactions = FixedPoint2 . Zero ;
return false ;
}
2023-12-29 04:47:43 -08:00
var attempt = new ReactionAttemptEvent ( reaction , soln ) ;
RaiseLocalEvent ( soln , ref attempt ) ;
2022-04-05 04:02:33 +12:00
if ( attempt . Cancelled )
{
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 ;
2023-09-05 09:55:10 +12:00
var reactantQuantity = solution . GetTotalPrototypeQuantity ( reactantName ) ;
if ( reactantQuantity < = FixedPoint2 . Zero )
2021-01-07 00:31:43 -06:00
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.
2023-05-13 15:10:32 +12:00
/// Removes the reactants from the solution, adds products, and returns a list of products.
2021-01-07 00:31:43 -06:00
/// </summary>
2023-12-29 04:47:43 -08:00
private List < string > PerformReaction ( Entity < SolutionComponent > soln , ReactionPrototype reaction , FixedPoint2 unitReactions )
2021-01-07 00:31:43 -06:00
{
2023-12-29 04:47:43 -08:00
var ( uid , comp ) = soln ;
var solution = comp . Solution ;
2023-05-13 15:10:32 +12:00
var energy = reaction . ConserveEnergy ? solution . GetThermalEnergy ( _prototypeManager ) : 0 ;
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
2023-05-13 15:10:32 +12:00
var products = new List < string > ( ) ;
2021-01-07 00:31:43 -06:00
foreach ( var product in reaction . Products )
{
2023-05-13 15:10:32 +12:00
products . Add ( product . Key ) ;
solution . AddReagent ( product . Key , product . Value * unitReactions ) ;
}
if ( reaction . ConserveEnergy )
{
var newCap = solution . GetHeatCapacity ( _prototypeManager ) ;
if ( newCap > 0 )
solution . Temperature = energy / newCap ;
2021-01-07 00:31:43 -06:00
}
2023-12-29 04:47:43 -08:00
OnReaction ( soln , reaction , null , unitReactions ) ;
2021-01-14 01:06:23 -06:00
return products ;
}
2023-12-29 04:47:43 -08:00
private void OnReaction ( Entity < SolutionComponent > soln , ReactionPrototype reaction , ReagentPrototype ? reagent , FixedPoint2 unitReactions )
2021-01-14 01:06:23 -06:00
{
2023-12-29 04:47:43 -08:00
var args = new ReagentEffectArgs ( soln , null , soln . Comp . Solution ,
2023-05-13 15:10:32 +12:00
reagent ,
2022-12-21 09:51:49 -05:00
unitReactions , EntityManager , null , 1f ) ;
2021-11-21 00:35:02 -07:00
2023-12-29 04:47:43 -08:00
var coordinates = Transform ( soln ) . Coordinates ;
2023-05-13 15:10:32 +12:00
_adminLogger . Add ( LogType . ChemicalReaction , reaction . Impact ,
2023-12-29 04:47:43 -08:00
$"Chemical reaction {reaction.ID:reaction} occurred with strength {unitReactions:strength} on entity {ToPrettyString(soln):metabolizer} at {coordinates}" ) ;
2023-05-13 15:10:32 +12:00
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 ;
2023-05-13 15:10:32 +12:00
_adminLogger . 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
}
2023-05-13 15:10:32 +12:00
2023-12-29 04:47:43 -08:00
_audio . PlayPvs ( reaction . Sound , soln ) ;
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>
2023-12-29 04:47:43 -08:00
private bool ProcessReactions ( Entity < SolutionComponent > soln , SortedSet < ReactionPrototype > reactions , ReactionMixerComponent ? mixerComponent )
2021-01-07 00:31:43 -06:00
{
2022-03-31 02:29:01 +13:00
HashSet < ReactionPrototype > toRemove = new ( ) ;
2023-05-13 15:10:32 +12:00
List < string > ? products = null ;
2022-03-31 02:29:01 +13:00
// attempt to perform any applicable reaction
foreach ( var reaction in reactions )
2021-01-07 00:31:43 -06:00
{
2023-12-29 04:47:43 -08:00
if ( ! CanReact ( soln , reaction , mixerComponent , out var unitReactions ) )
2022-03-31 02:29:01 +13:00
{
toRemove . Add ( reaction ) ;
2021-11-28 07:32:33 -08:00
continue ;
2022-03-31 02:29:01 +13:00
}
2021-11-28 07:32:33 -08:00
2023-12-29 04:47:43 -08:00
products = PerformReaction ( soln , reaction , unitReactions ) ;
2022-03-31 02:29:01 +13:00
break ;
}
2021-11-28 07:32:33 -08:00
2022-03-31 02:29:01 +13:00
// did any reaction occur?
if ( products = = null )
2023-01-19 03:56:45 +01:00
return false ;
2022-03-31 02:29:01 +13:00
2023-05-13 15:10:32 +12:00
if ( products . Count = = 0 )
2022-03-31 02:29:01 +13:00
return true ;
// Add any reactions associated with the new products. This may re-add reactions that were already iterated
// over previously. The new product may mean the reactions are applicable again and need to be processed.
2023-05-13 15:10:32 +12:00
foreach ( var product in products )
2022-03-31 02:29:01 +13:00
{
2023-05-13 15:10:32 +12:00
if ( _reactions . TryGetValue ( product , out var reactantReactions ) )
2022-03-31 02:29:01 +13:00
reactions . UnionWith ( reactantReactions ) ;
2021-01-07 00:31:43 -06:00
}
2021-11-28 07:32:33 -08:00
2022-03-31 02:29:01 +13:00
return true ;
2021-01-07 00:31:43 -06:00
}
/// <summary>
/// Continually react a solution until no more reactions occur, with a volume constraint.
/// </summary>
2023-12-29 04:47:43 -08:00
public void FullyReactSolution ( Entity < SolutionComponent > soln , ReactionMixerComponent ? mixerComponent = null )
2021-01-07 00:31:43 -06:00
{
2022-03-31 02:29:01 +13:00
// construct the initial set of reactions to check.
SortedSet < ReactionPrototype > reactions = new ( ) ;
2023-12-29 04:47:43 -08:00
foreach ( var reactant in soln . Comp . Solution . Contents )
2021-01-07 00:31:43 -06:00
{
2023-12-22 09:13:45 -05:00
if ( _reactionsSingle . TryGetValue ( reactant . Reagent . Prototype , out var reactantReactions ) )
2022-03-31 02:29:01 +13:00
reactions . UnionWith ( reactantReactions ) ;
}
2021-01-07 00:31:43 -06:00
2022-03-31 02:29:01 +13:00
// Repeatedly attempt to perform reactions, ending when there are no more applicable reactions, or when we
// exceed the iteration limit.
for ( var i = 0 ; i < MaxReactionIterations ; i + + )
{
2023-12-29 04:47:43 -08:00
if ( ! ProcessReactions ( soln , reactions , mixerComponent ) )
2021-01-07 00:31:43 -06:00
return ;
}
2022-03-31 02:29:01 +13:00
2023-12-29 04:47:43 -08:00
Log . Error ( $"{nameof(Solution)} {soln.Owner} could not finish reacting in under {MaxReactionIterations} loops." ) ;
2021-01-07 00:31:43 -06:00
}
}
2022-04-05 04:02:33 +12:00
/// <summary>
/// Raised directed at the owner of a solution to determine whether the reaction should be allowed to occur.
/// </summary>
/// <reamrks>
/// Some solution containers (e.g., bloodstream, smoke, foam) use this to block certain reactions from occurring.
/// </reamrks>
2023-12-29 04:47:43 -08:00
[ByRefEvent]
public record struct ReactionAttemptEvent ( ReactionPrototype Reaction , Entity < SolutionComponent > Solution )
2022-04-05 04:02:33 +12:00
{
2023-12-29 04:47:43 -08:00
public readonly ReactionPrototype Reaction = Reaction ;
public readonly Entity < SolutionComponent > Solution = Solution ;
public bool Cancelled = false ;
2022-04-05 04:02:33 +12:00
}
2021-01-07 00:31:43 -06:00
}