From 3ab803636375c4c60bafb3bf75c207cb5aa2155c Mon Sep 17 00:00:00 2001 From: moneyl <8206401+Moneyl@users.noreply.github.com> Date: Thu, 21 Nov 2019 17:24:19 -0500 Subject: [PATCH] De-hardcode chemical metabolism of StomachComponent (#437) * Move chemical reaction effects into Chemistry/ReactionEffects/ subfolder * Replace hardcoded StomachComponent metabolism with IMetabolizable The benefits of this approach are that reagent metabolism effects are not hardcoded into StomachComponent, and metabolism effects can be more easily chained together in yaml prototypes, and reagents can have different metabolism rates. One problem with this approach is that getting metabolism rates slower than 1u / second is impossible. Implementing #377 should resolve that problem. * Fix DefaultFood and DefaultDrink so they remove reagent regardless of Hunger/ThirstComponent presence Previously if neither of those were present the reagents wouldn't be removed from the stomach. This fixes that. * Additional comment on function * Make metabolizer interface implementations explicit Also removed some unused using statements * Make StomachComponent._reagentDeltas readonly * Fix misleading variable names and docs for metabolizables Changes one of the arguments for `IMetabolizable.Metabolize()` to be called `tickTime` instead of `frameTime` to more accurately reflect it's purpose. It's not really the frametime, but the time since the last metabolism tick. Also updated and expanded the docs to reflect this and to be more clear. --- .../Chemistry/Metabolism/DefaultDrink.cs | 41 +++++++ .../Chemistry/Metabolism/DefaultFood.cs | 41 +++++++ .../ExplosionReactionEffect.cs | 2 +- .../Components/Nutrition/FoodComponent.cs | 9 +- .../Components/Nutrition/StomachComponent.cs | 103 ++++++------------ .../Interfaces/Chemistry/IReactionEffect.cs | 2 +- .../Chemistry/DefaultMetabolizable.cs | 27 +++++ Content.Shared/Chemistry/ReagentPrototype.cs | 32 ++++-- .../Interfaces/Chemistry/IMetabolizable.cs | 24 ++++ Resources/Prototypes/Reagents/chemicals.yml | 6 + Resources/Prototypes/Reagents/drinks.yml | 11 +- 11 files changed, 213 insertions(+), 85 deletions(-) create mode 100644 Content.Server/Chemistry/Metabolism/DefaultDrink.cs create mode 100644 Content.Server/Chemistry/Metabolism/DefaultFood.cs rename Content.Server/Chemistry/{ => ReactionEffects}/ExplosionReactionEffect.cs (98%) create mode 100644 Content.Shared/Chemistry/DefaultMetabolizable.cs create mode 100644 Content.Shared/Interfaces/Chemistry/IMetabolizable.cs diff --git a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs new file mode 100644 index 0000000000..e43a1d7dd3 --- /dev/null +++ b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs @@ -0,0 +1,41 @@ +using System; +using Content.Server.GameObjects.Components.Nutrition; +using Content.Shared.Interfaces.Chemistry; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Serialization; + +namespace Content.Server.Chemistry.Metabolism +{ + /// + /// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, + /// and to update it's thirst values. + /// + class DefaultDrink : IMetabolizable + { + //Rate of metabolism in units / second + private int _metabolismRate; + public int MetabolismRate => _metabolismRate; + + //How much thirst is satiated when 1u of the reagent is metabolized + private float _hydrationFactor; + public float HydrationFactor => _hydrationFactor; + + void IExposeData.ExposeData(ObjectSerializer serializer) + { + serializer.DataField(ref _metabolismRate, "rate", 1); + serializer.DataField(ref _hydrationFactor, "nutrimentFactor", 30.0f); + } + + //Remove reagent at set rate, satiate thirst if a ThirstComponent can be found + int IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + { + int metabolismAmount = (int)Math.Round(MetabolismRate * tickTime); + if (solutionEntity.TryGetComponent(out ThirstComponent thirst)) + thirst.UpdateThirst(metabolismAmount * HydrationFactor); + + //Return amount of reagent to be removed, remove reagent regardless of ThirstComponent presence + return metabolismAmount; + } + } +} diff --git a/Content.Server/Chemistry/Metabolism/DefaultFood.cs b/Content.Server/Chemistry/Metabolism/DefaultFood.cs new file mode 100644 index 0000000000..824721acfd --- /dev/null +++ b/Content.Server/Chemistry/Metabolism/DefaultFood.cs @@ -0,0 +1,41 @@ +using System; +using Content.Server.GameObjects.Components.Nutrition; +using Content.Shared.Interfaces.Chemistry; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Serialization; + +namespace Content.Server.Chemistry.Metabolism +{ + /// + /// Default metabolism for food reagents. Attempts to find a HungerComponent on the target, + /// and to update it's hunger values. + /// + class DefaultFood : IMetabolizable + { + //Rate of metabolism in units / second + private int _metabolismRate; + public int MetabolismRate => _metabolismRate; + + //How much hunger is satiated when 1u of the reagent is metabolized + private float _nutritionFactor; + public float NutritionFactor => _nutritionFactor; + + void IExposeData.ExposeData(ObjectSerializer serializer) + { + serializer.DataField(ref _metabolismRate, "rate", 1); + serializer.DataField(ref _nutritionFactor, "nutrimentFactor", 30.0f); + } + + //Remove reagent at set rate, satiate hunger if a HungerComponent can be found + int IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + { + int metabolismAmount = (int)Math.Round(MetabolismRate * tickTime); + if (solutionEntity.TryGetComponent(out HungerComponent hunger)) + hunger.UpdateFood(metabolismAmount * NutritionFactor); + + //Return amount of reagent to be removed, remove reagent regardless of HungerComponent presence + return metabolismAmount; + } + } +} diff --git a/Content.Server/Chemistry/ExplosionReactionEffect.cs b/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs similarity index 98% rename from Content.Server/Chemistry/ExplosionReactionEffect.cs rename to Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs index 4ab6f5e282..7382a7bdc2 100644 --- a/Content.Server/Chemistry/ExplosionReactionEffect.cs +++ b/Content.Server/Chemistry/ReactionEffects/ExplosionReactionEffect.cs @@ -5,7 +5,7 @@ using Content.Shared.Interfaces; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Serialization; -namespace Content.Server.Chemistry +namespace Content.Server.Chemistry.ReactionEffects { class ExplosionReactionEffect : IReactionEffect { diff --git a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs index 25e104688d..fb53626e59 100644 --- a/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/FoodComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using Content.Server.GameObjects.Components.Chemistry; using Content.Server.GameObjects.Components.Sound; using Content.Server.GameObjects.EntitySystems; @@ -44,9 +44,7 @@ namespace Content.Server.GameObjects.Components.Nutrition serializer.DataField(ref _initialContents, "contents", null); serializer.DataField(ref _useSound, "use_sound", "/Audio/items/eatfood.ogg"); // Default is transfer 30 units - serializer.DataField(ref _transferAmount, - "transfer_amount", - 30 / StomachComponent.NutrimentFactor); + serializer.DataField(ref _transferAmount, "transfer_amount", 5); // E.g. empty chip packet when done serializer.DataField(ref _finishPrototype, "spawn_on_finish", null); } @@ -81,8 +79,7 @@ namespace Content.Server.GameObjects.Components.Nutrition _initialContents = null; if (_contents.CurrentVolume == 0) { - _contents.TryAddReagent("chem.Nutriment", 30 / StomachComponent.NutrimentFactor, - out _); + _contents.TryAddReagent("chem.Nutriment", 5, out _); } Owner.TryGetComponent(out AppearanceComponent appearance); _appearanceComponent = appearance; diff --git a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs b/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs index 2f4e1933fc..7478d6c038 100644 --- a/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs +++ b/Content.Server/GameObjects/Components/Nutrition/StomachComponent.cs @@ -1,7 +1,11 @@ +using System.Collections.Generic; using Content.Server.GameObjects.Components.Chemistry; +using Content.Server.GameObjects.EntitySystems; using Content.Shared.Chemistry; using Content.Shared.GameObjects.Components.Nutrition; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -10,43 +14,35 @@ namespace Content.Server.GameObjects.Components.Nutrition [RegisterComponent] public class StomachComponent : SharedStomachComponent { - // Essentially every time it ticks it'll pull out the MetabolisationAmount of reagents and process them. - // Generic food goes under "nutriment" like SS13 - // There's also separate hunger and thirst components which means you can have a stomach - // but not require food / water. - public static readonly int NutrimentFactor = 30; - public static readonly int HydrationFactor = 30; - public static readonly int MetabolisationAmount = 5; +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; +#pragma warning restore 649 + [ViewVariables(VVAccess.ReadOnly)] private SolutionComponent _stomachContents; - public float MetaboliseDelay => _metaboliseDelay; - [ViewVariables] - private float _metaboliseDelay; // How long between metabolisation for 5 units - public int MaxVolume { get => _stomachContents.MaxVolume; set => _stomachContents.MaxVolume = value; } - - private float _metabolisationCounter = 0.0f; - private int _initialMaxVolume; + //Used to track changes to reagent amounts during metabolism + private readonly Dictionary _reagentDeltas = new Dictionary(); public override void ExposeData(ObjectSerializer serializer) { base.ExposeData(serializer); - serializer.DataField(ref _metaboliseDelay, "metabolise_delay", 6.0f); serializer.DataField(ref _initialMaxVolume, "max_volume", 20); } public override void Initialize() { base.Initialize(); - // Shouldn't add to Owner to avoid cross-contamination (e.g. with blood or whatever they made hold other solutions) + //Doesn't use Owner.AddComponent<>() to avoid cross-contamination (e.g. with blood or whatever they holds other solutions) _stomachContents = new SolutionComponent(); _stomachContents.InitializeFromPrototype(); _stomachContents.MaxVolume = _initialMaxVolume; + _stomachContents.Owner = Owner; //Manually set owner to avoid crash when VV'ing this } public bool TryTransferSolution(Solution solution) @@ -61,69 +57,42 @@ namespace Content.Server.GameObjects.Components.Nutrition } /// - /// This is where the magic happens. Make people throw up, increase nutrition, whatever + /// Loops through each reagent in _stomachContents, and calls the IMetabolizable for each of them./> /// - /// - public void React(Solution solution) + /// The time since the last metabolism tick in seconds. + public void Metabolize(float tickTime) { - // TODO: Implement metabolism post from here - // https://github.com/space-wizards/space-station-14/issues/170#issuecomment-481835623 as raised by moneyl - var hungerUpdate = 0; - var thirstUpdate = 0; - foreach (var reagent in solution.Contents) + if (_stomachContents.CurrentVolume == 0) + return; + + //Run metabolism for each reagent, track quantity changes + _reagentDeltas.Clear(); + foreach (var reagent in _stomachContents.ReagentList) { - switch (reagent.ReagentId) + if(!_prototypeManager.TryIndex(reagent.ReagentId, out ReagentPrototype proto)) + continue; + + foreach (var metabolizable in proto.Metabolism) { - case "chem.Nutriment": - hungerUpdate++; - break; - case "chem.H2O": - thirstUpdate++; - break; - case "chem.Alcohol": - thirstUpdate++; - break; - default: - continue; + _reagentDeltas[reagent.ReagentId] = metabolizable.Metabolize(Owner, reagent.ReagentId, tickTime); } } - // Quantity x restore amount per unit - if (hungerUpdate > 0 && Owner.TryGetComponent(out HungerComponent hungerComponent)) + //Apply changes to quantity afterwards. Can't change the reagent quantities while the iterating the + //list of reagents, because that would invalidate the iterator and throw an exception. + foreach (var reagentDelta in _reagentDeltas) { - hungerComponent.UpdateFood(hungerUpdate * NutrimentFactor); + _stomachContents.TryRemoveReagent(reagentDelta.Key, reagentDelta.Value); } - - if (thirstUpdate > 0 && Owner.TryGetComponent(out ThirstComponent thirstComponent)) - { - thirstComponent.UpdateThirst(thirstUpdate * HydrationFactor); - } - - // TODO: Dispose solution? } - public void Metabolise() + /// + /// Triggers metabolism of the reagents inside _stomachContents. Called by + /// + /// The time since the last metabolism tick in seconds. + public void OnUpdate(float tickTime) { - if (_stomachContents.CurrentVolume == 0) - { - return; - } - - var metabolisation = _stomachContents.SplitSolution(MetabolisationAmount); - - React(metabolisation); - } - - public void OnUpdate(float frameTime) - { - _metabolisationCounter += frameTime; - if (_metabolisationCounter >= MetaboliseDelay) - { - // Going to be rounding issues with frametime but no easy way to avoid it with int reagents. - // It is a long-term mechanic so shouldn't be a big deal. - Metabolise(); - _metabolisationCounter -= MetaboliseDelay; - } + Metabolize(tickTime); } } } diff --git a/Content.Server/Interfaces/Chemistry/IReactionEffect.cs b/Content.Server/Interfaces/Chemistry/IReactionEffect.cs index b3e388bb8a..bd01799557 100644 --- a/Content.Server/Interfaces/Chemistry/IReactionEffect.cs +++ b/Content.Server/Interfaces/Chemistry/IReactionEffect.cs @@ -8,6 +8,6 @@ namespace Content.Shared.Interfaces /// public interface IReactionEffect : IExposeData { - void React(IEntity solutionEntity, int intensity ); + void React(IEntity solutionEntity, int intensity); } } diff --git a/Content.Shared/Chemistry/DefaultMetabolizable.cs b/Content.Shared/Chemistry/DefaultMetabolizable.cs new file mode 100644 index 0000000000..ab5816d9f0 --- /dev/null +++ b/Content.Shared/Chemistry/DefaultMetabolizable.cs @@ -0,0 +1,27 @@ +using System; +using Content.Shared.Interfaces.Chemistry; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry +{ + //Default metabolism for reagents. Metabolizes the reagent with no effects + class DefaultMetabolizable : IMetabolizable + { + //Rate of metabolism in units / second + private int _metabolismRate = 1; + public int MetabolismRate => _metabolismRate; + + void IExposeData.ExposeData(ObjectSerializer serializer) + { + serializer.DataField(ref _metabolismRate, "rate", 1); + } + + int IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + { + int metabolismAmount = (int)Math.Round(MetabolismRate * tickTime); + return metabolismAmount; + } + } +} diff --git a/Content.Shared/Chemistry/ReagentPrototype.cs b/Content.Shared/Chemistry/ReagentPrototype.cs index b123489a2b..e4cbfab8f1 100644 --- a/Content.Shared/Chemistry/ReagentPrototype.cs +++ b/Content.Shared/Chemistry/ReagentPrototype.cs @@ -1,5 +1,9 @@ -using Robust.Shared.Maths; +using System; +using System.Collections.Generic; +using Content.Shared.Interfaces.Chemistry; +using Robust.Shared.Maths; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; @@ -8,18 +12,28 @@ namespace Content.Shared.Chemistry [Prototype("reagent")] public class ReagentPrototype : IPrototype, IIndexedPrototype { - public string ID { get; private set; } - public string Name { get; private set; } - public string Description { get; private set; } - public Color SubstanceColor { get; private set; } + private string _id; + private string _name; + private string _description; + private Color _substanceColor; + private List _metabolism; + + public string ID => _id; + public string Name => _name; + public string Description => _description; + public Color SubstanceColor => _substanceColor; + //List of metabolism effects this reagent has, should really only be used server-side. + public List Metabolism => _metabolism; public void LoadFrom(YamlMappingNode mapping) { - ID = mapping.GetNode("id").AsString(); - Name = mapping.GetNode("name").ToString(); - Description = mapping.GetNode("desc").ToString(); + var serializer = YamlObjectSerializer.NewReader(mapping); - SubstanceColor = mapping.TryGetNode("color", out var colorNode) ? colorNode.AsHexColor(Color.White) : Color.White; + serializer.DataField(ref _id, "id", string.Empty); + serializer.DataField(ref _name, "name", string.Empty); + serializer.DataField(ref _description, "desc", string.Empty); + serializer.DataField(ref _substanceColor, "color", Color.White); + serializer.DataField(ref _metabolism, "metabolism", new List{new DefaultMetabolizable()}); } } } diff --git a/Content.Shared/Interfaces/Chemistry/IMetabolizable.cs b/Content.Shared/Interfaces/Chemistry/IMetabolizable.cs new file mode 100644 index 0000000000..4b03ef51e2 --- /dev/null +++ b/Content.Shared/Interfaces/Chemistry/IMetabolizable.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Content.Shared.Chemistry; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Serialization; + +namespace Content.Shared.Interfaces.Chemistry +{ + /// + /// Metabolism behavior for a reagent. + /// + public interface IMetabolizable : IExposeData + { + /// + /// Metabolize the attached reagent. Return the amount of reagent to be removed from the solution. + /// You shouldn't remove the reagent yourself to avoid invalidating the iterator of the metabolism + /// organ that is processing it's reagents. + /// + /// The entity containing the solution. + /// The reagent id + /// The time since the last metabolism tick in seconds. + /// The amount of reagent to be removed. The metabolizing organ should handle removing the reagent. + int Metabolize(IEntity solutionEntity, string reagentId, float tickTime); + } +} diff --git a/Resources/Prototypes/Reagents/chemicals.yml b/Resources/Prototypes/Reagents/chemicals.yml index f12ac1cdaa..a5e29b8487 100644 --- a/Resources/Prototypes/Reagents/chemicals.yml +++ b/Resources/Prototypes/Reagents/chemicals.yml @@ -2,6 +2,9 @@ id: chem.Nutriment name: Nutriment desc: Generic nutrition + metabolism: + - !type:DefaultFood + rate: 1 - type: reagent id: chem.H2SO4 @@ -12,6 +15,9 @@ id: chem.H2O name: Water desc: A tasty colorless liquid. + metabolism: + - !type:DefaultDrink + rate: 1 - type: reagent id: chem.Ice diff --git a/Resources/Prototypes/Reagents/drinks.yml b/Resources/Prototypes/Reagents/drinks.yml index 7f5217b21c..eb804d8d8a 100644 --- a/Resources/Prototypes/Reagents/drinks.yml +++ b/Resources/Prototypes/Reagents/drinks.yml @@ -17,13 +17,22 @@ id: chem.Cola name: Cola desc: A sweet, carbonated soft drink. Caffeine free. + metabolism: + - !type:DefaultDrink + rate: 1 - type: reagent id: chem.Coffee name: Coffee desc: A drink made from brewed coffee beans. Contains a moderate amount of caffeine. + metabolism: + - !type:DefaultDrink + rate: 1 - type: reagent id: chem.Tea name: Tea - desc: A made by boiling leaves of the tea tree, Camellia sinensis. \ No newline at end of file + desc: A made by boiling leaves of the tea tree, Camellia sinensis. + metabolism: + - !type:DefaultDrink + rate: 1