Turn ReagentEffects into generic EntityEffects (#28168)

* Oh the possibilities

* Merge fixes

* Forgot to remote LavaSystem oops

* Changed EntityEffectArgs to EntityEffectBaseArgs and EntityEffectReagentArgs

* Throw exception for unimplemented effectargs

* Remove Json and overrideable datafields

* Fix test issues

* Actually fix the compiling issue

* Fix comments and remove EntityEffectArgs (no longer used, replaced with EntityEffectBaseArgs)
This commit is contained in:
SlamBamActionman
2024-06-30 05:43:43 +02:00
committed by GitHub
parent 250109f0b4
commit b9fa941ca6
127 changed files with 2199 additions and 1978 deletions

View File

@@ -2,6 +2,7 @@ using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
@@ -197,9 +198,7 @@ namespace Content.Shared.Chemistry.Reaction
private void OnReaction(Entity<SolutionComponent> soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions)
{
var args = new ReagentEffectArgs(soln, null, soln.Comp.Solution,
reagent,
unitReactions, EntityManager, null, 1f);
var args = new EntityEffectReagentArgs(soln, EntityManager, null, soln.Comp.Solution, unitReactions, reagent, null, 1f);
var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos);
@@ -213,7 +212,7 @@ namespace Content.Shared.Chemistry.Reaction
if (effect.ShouldLog)
{
var entity = args.SolutionEntity;
var entity = args.TargetEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reaction effect {effect.GetType().Name:effect} of reaction {reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}");
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -59,7 +60,7 @@ namespace Content.Shared.Chemistry.Reaction
/// <summary>
/// Effects to be triggered when the reaction occurs.
/// </summary>
[DataField("effects", serverOnly: true)] public List<ReagentEffect> Effects = new();
[DataField("effects", serverOnly: true)] public List<EntityEffect> Effects = new();
/// <summary>
/// How dangerous is this effect? Stuff like bicaridine should be low, while things like methamphetamine

View File

@@ -1,4 +1,5 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.EntityEffects;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
@@ -33,7 +34,7 @@ public sealed partial class ReactiveReagentEffectEntry
public HashSet<string>? Reagents = null;
[DataField("effects", required: true)]
public List<ReagentEffect> Effects = default!;
public List<EntityEffect> Effects = default!;
[DataField("groups", readOnly: true, serverOnly: true,
customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<ReactionMethod>, ReactiveGroupPrototype>))]

View File

@@ -3,102 +3,107 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared.Chemistry
namespace Content.Shared.Chemistry;
[UsedImplicitly]
public sealed class ReactiveSystem : EntitySystem
{
[UsedImplicitly]
public sealed class ReactiveSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method)
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method)
foreach (var reagent in solution.Contents.ToArray())
{
foreach (var reagent in solution.Contents.ToArray())
{
ReactionEntity(uid, method, reagent, solution);
}
ReactionEntity(uid, method, reagent, solution);
}
}
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source)
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source)
{
// We throw if the reagent specified doesn't exist.
var proto = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
ReactionEntity(uid, method, proto, reagentQuantity, source);
}
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto,
ReagentQuantity reagentQuantity, Solution? source)
{
if (!TryComp(uid, out ReactiveComponent? reactive))
return;
// If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified.
var args = new EntityEffectReagentArgs(uid, EntityManager, null, source, source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, proto, method, 1f);
// First, check if the reagent wants to apply any effects.
if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null)
{
// We throw if the reagent specified doesn't exist.
var proto = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
ReactionEntity(uid, method, proto, reagentQuantity, source);
}
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto,
ReagentQuantity reagentQuantity, Solution? source)
{
if (!TryComp(uid, out ReactiveComponent? reactive))
return;
// If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified.
var args = new ReagentEffectArgs(uid, null, source, proto,
source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, EntityManager, method, 1f);
// First, check if the reagent wants to apply any effects.
if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null)
foreach (var (key, val) in proto.ReactiveEffects)
{
foreach (var (key, val) in proto.ReactiveEffects)
if (!val.Methods.Contains(method))
continue;
if (!reactive.ReactiveGroups.ContainsKey(key))
continue;
if (!reactive.ReactiveGroups[key].Contains(method))
continue;
foreach (var effect in val.Effects)
{
if (!val.Methods.Contains(method))
if (!effect.ShouldApply(args, _robustRandom))
continue;
if (!reactive.ReactiveGroups.ContainsKey(key))
continue;
if (!reactive.ReactiveGroups[key].Contains(method))
continue;
foreach (var effect in val.Effects)
if (effect.ShouldLog)
{
if (!effect.ShouldApply(args, _robustRandom))
continue;
if (effect.ShouldLog)
{
var entity = args.SolutionEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}");
}
effect.Effect(args);
var entity = args.TargetEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}");
}
effect.Effect(args);
}
}
}
// Then, check if the prototype has any effects it can apply as well.
if (reactive.Reactions != null)
// Then, check if the prototype has any effects it can apply as well.
if (reactive.Reactions != null)
{
foreach (var entry in reactive.Reactions)
{
foreach (var entry in reactive.Reactions)
if (!entry.Methods.Contains(method))
continue;
if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID))
continue;
foreach (var effect in entry.Effects)
{
if (!entry.Methods.Contains(method))
if (!effect.ShouldApply(args, _robustRandom))
continue;
if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID))
continue;
foreach (var effect in entry.Effects)
if (effect.ShouldLog)
{
if (!effect.ShouldApply(args, _robustRandom))
continue;
if (effect.ShouldLog)
{
var entity = args.SolutionEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}");
}
effect.Effect(args);
var entity = args.TargetEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}");
}
effect.Effect(args);
}
}
}
}
}
public enum ReactionMethod
{
Touch,
Injection,
Ingestion,
}

View File

@@ -1,112 +0,0 @@
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared.Chemistry.Reagent
{
/// <summary>
/// Reagent effects describe behavior that occurs when a reagent is ingested and metabolized by some
/// organ. They only trigger when all of <see cref="Conditions"/> are satisfied.
/// </summary>
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class ReagentEffect
{
[JsonPropertyName("id")] private protected string _id => this.GetType().Name;
/// <summary>
/// The list of conditions required for the effect to activate. Not required.
/// </summary>
[JsonPropertyName("conditions")]
[DataField("conditions")]
public ReagentEffectCondition[]? Conditions;
public virtual string ReagentEffectFormat => "guidebook-reagent-effect-description";
protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys);
/// <summary>
/// What's the chance, from 0 to 1, that this effect will occur?
/// </summary>
[JsonPropertyName("probability")]
[DataField("probability")]
public float Probability = 1.0f;
[JsonIgnore]
[DataField("logImpact")]
public virtual LogImpact LogImpact { get; private set; } = LogImpact.Low;
/// <summary>
/// Should this reagent effect log at all?
/// </summary>
[JsonIgnore]
[DataField("shouldLog")]
public virtual bool ShouldLog { get; private set; } = false;
public abstract void Effect(ReagentEffectArgs args);
/// <summary>
/// Produces a localized, bbcode'd guidebook description for this effect.
/// </summary>
/// <returns></returns>
public string? GuidebookEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys)
{
var effect = ReagentEffectGuidebookText(prototype, entSys);
if (effect is null)
return null;
return Loc.GetString(ReagentEffectFormat, ("effect", effect), ("chance", Probability),
("conditionCount", Conditions?.Length ?? 0),
("conditions",
ContentLocalizationManager.FormatList(Conditions?.Select(x => x.GuidebookExplanation(prototype)).ToList() ??
new List<string>())));
}
}
public static class ReagentEffectExt
{
public static bool ShouldApply(this ReagentEffect effect, ReagentEffectArgs args,
IRobustRandom? random = null)
{
if (random == null)
random = IoCManager.Resolve<IRobustRandom>();
if (effect.Probability < 1.0f && !random.Prob(effect.Probability))
return false;
if (effect.Conditions != null)
{
foreach (var cond in effect.Conditions)
{
if (!cond.Condition(args))
return false;
}
}
return true;
}
}
public enum ReactionMethod
{
Touch,
Injection,
Ingestion,
}
public readonly record struct ReagentEffectArgs(
EntityUid SolutionEntity,
EntityUid? OrganEntity,
Solution? Source,
ReagentPrototype? Reagent,
FixedPoint2 Quantity,
IEntityManager EntityManager,
ReactionMethod? Method,
float Scale
);
}

View File

@@ -1,22 +0,0 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared.Chemistry.Reagent
{
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class ReagentEffectCondition
{
[JsonPropertyName("id")] private protected string _id => this.GetType().Name;
public abstract bool Condition(ReagentEffectArgs args);
/// <summary>
/// Effect explanations are of the form "[chance to] [action] when [condition] and [condition]"
/// </summary>
/// <param name="prototype"></param>
/// <returns></returns>
public abstract string GuidebookExplanation(IPrototypeManager prototype);
}
}

View File

@@ -1,10 +1,11 @@
using System.Collections.Frozen;
using System.Collections.Frozen;
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.EntityEffects;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition;
@@ -134,7 +135,7 @@ namespace Content.Shared.Chemistry.Reagent
public List<ITileReaction> TileReactions = new(0);
[DataField("plantMetabolism", serverOnly: true)]
public List<ReagentEffect> PlantMetabolisms = new(0);
public List<EntityEffect> PlantMetabolisms = new(0);
[DataField]
public float PricePerUnit;
@@ -170,7 +171,7 @@ namespace Content.Shared.Chemistry.Reagent
var entMan = IoCManager.Resolve<IEntityManager>();
var random = IoCManager.Resolve<IRobustRandom>();
var args = new ReagentEffectArgs(plantHolder.Value, null, solution, this, amount.Quantity, entMan, null, 1f);
var args = new EntityEffectReagentArgs(plantHolder.Value, entMan, null, solution, amount.Quantity, this, null, 1f);
foreach (var plantMetabolizable in PlantMetabolisms)
{
if (!plantMetabolizable.ShouldApply(args, random))
@@ -178,7 +179,7 @@ namespace Content.Shared.Chemistry.Reagent
if (plantMetabolizable.ShouldLog)
{
var entity = args.SolutionEntity;
var entity = args.TargetEntity;
entMan.System<SharedAdminLogSystem>().Add(LogType.ReagentEffect, plantMetabolizable.LogImpact,
$"Plant metabolism effect {plantMetabolizable.GetType().Name:effect} of reagent {ID:reagent} applied on entity {entMan.ToPrettyString(entity):entity} at {entMan.GetComponent<TransformComponent>(entity).Coordinates:coordinates}");
}
@@ -230,7 +231,7 @@ namespace Content.Shared.Chemistry.Reagent
/// </summary>
[JsonPropertyName("effects")]
[DataField("effects", required: true)]
public ReagentEffect[] Effects = default!;
public EntityEffect[] Effects = default!;
public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys)
{
@@ -264,6 +265,6 @@ namespace Content.Shared.Chemistry.Reagent
public HashSet<ReactionMethod> Methods = default!;
[DataField("effects", required: true)]
public ReagentEffect[] Effects = default!;
public EntityEffect[] Effects = default!;
}
}

View File

@@ -0,0 +1,132 @@
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Toolshed.TypeParsers;
namespace Content.Shared.EntityEffects;
/// <summary>
/// Entity effects describe behavior that occurs on different kinds of triggers, e.g. when a reagent is ingested and metabolized by some
/// organ. They only trigger when all of <see cref="Conditions"/> are satisfied.
/// </summary>
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class EntityEffect
{
private protected string _id => this.GetType().Name;
/// <summary>
/// The list of conditions required for the effect to activate. Not required.
/// </summary>
[DataField("conditions")]
public EntityEffectCondition[]? Conditions;
public virtual string ReagentEffectFormat => "guidebook-reagent-effect-description";
protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys);
/// <summary>
/// What's the chance, from 0 to 1, that this effect will occur?
/// </summary>
[DataField("probability")]
public float Probability = 1.0f;
public virtual LogImpact LogImpact { get; private set; } = LogImpact.Low;
/// <summary>
/// Should this entity effect log at all?
/// </summary>
public virtual bool ShouldLog { get; private set; } = false;
public abstract void Effect(EntityEffectBaseArgs args);
/// <summary>
/// Produces a localized, bbcode'd guidebook description for this effect.
/// </summary>
/// <returns></returns>
public string? GuidebookEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys)
{
var effect = ReagentEffectGuidebookText(prototype, entSys);
if (effect is null)
return null;
return Loc.GetString(ReagentEffectFormat, ("effect", effect), ("chance", Probability),
("conditionCount", Conditions?.Length ?? 0),
("conditions",
ContentLocalizationManager.FormatList(Conditions?.Select(x => x.GuidebookExplanation(prototype)).ToList() ??
new List<string>())));
}
}
public static class EntityEffectExt
{
public static bool ShouldApply(this EntityEffect effect, EntityEffectBaseArgs args,
IRobustRandom? random = null)
{
if (random == null)
random = IoCManager.Resolve<IRobustRandom>();
if (effect.Probability < 1.0f && !random.Prob(effect.Probability))
return false;
if (effect.Conditions != null)
{
foreach (var cond in effect.Conditions)
{
if (!cond.Condition(args))
return false;
}
}
return true;
}
}
/// <summary>
/// EntityEffectBaseArgs only contains the target of an effect.
/// If a trigger wants to include more info (e.g. the quantity of the chemical triggering the effect), it can be extended (see EntityEffectReagentArgs).
/// </summary>
public record class EntityEffectBaseArgs
{
public EntityUid TargetEntity;
public IEntityManager EntityManager = default!;
public EntityEffectBaseArgs(EntityUid targetEntity, IEntityManager entityManager)
{
TargetEntity = targetEntity;
EntityManager = entityManager;
}
}
public record class EntityEffectReagentArgs : EntityEffectBaseArgs
{
public EntityUid? OrganEntity;
public Solution? Source;
public FixedPoint2 Quantity;
public ReagentPrototype? Reagent;
public ReactionMethod? Method;
public FixedPoint2 Scale;
public EntityEffectReagentArgs(EntityUid targetEntity, IEntityManager entityManager, EntityUid? organEntity, Solution? source, FixedPoint2 quantity, ReagentPrototype? reagent, ReactionMethod? method, FixedPoint2 scale) : base(targetEntity, entityManager)
{
OrganEntity = organEntity;
Source = source;
Quantity = quantity;
Reagent = reagent;
Method = method;
Scale = scale;
}
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects;
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract partial class EntityEffectCondition
{
[JsonPropertyName("id")] private protected string _id => this.GetType().Name;
public abstract bool Condition(EntityEffectBaseArgs args);
/// <summary>
/// Effect explanations are of the form "[chance to] [action] when [condition] and [condition]"
/// </summary>
/// <param name="prototype"></param>
/// <returns></returns>
public abstract string GuidebookExplanation(IPrototypeManager prototype);
}