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