From b7cff815870f323766da63f0ad93b2532b794dfa Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Sat, 23 Sep 2023 03:10:04 +0100 Subject: [PATCH] Openable refactor (#19750) --- .../EntitySystems/PuddleSystem.Spillable.cs | 18 +- Content.Server/Glue/GlueSystem.cs | 9 +- Content.Server/Lube/LubeSystem.cs | 9 +- .../Materials/MaterialReclaimerSystem.cs | 6 +- .../NPC/Systems/NPCUtilitySystem.cs | 11 +- .../Nutrition/Components/DrinkComponent.cs | 71 +- .../Nutrition/Components/FoodComponent.cs | 124 ++- .../Nutrition/Components/OpenableComponent.cs | 46 + .../Components/PressurizedDrinkComponent.cs | 27 + .../Nutrition/EntitySystems/CreamPieSystem.cs | 2 +- .../Nutrition/EntitySystems/DrinkSystem.cs | 169 ++-- .../Nutrition/EntitySystems/FoodSystem.cs | 816 +++++++++--------- .../Nutrition/EntitySystems/OpenableSystem.cs | 140 +++ .../EntitySystems/SliceableFoodSystem.cs | 6 +- .../Components/SharedFoodComponent.cs | 6 +- .../nutrition/components/drink-component.ftl | 5 +- .../Prototypes/Entities/Effects/puddle.yml | 1 - .../Objects/Consumable/Drinks/drinks.yml | 2 - .../Consumable/Drinks/drinks_bottles.yml | 9 +- .../Objects/Consumable/Drinks/drinks_cans.yml | 11 +- .../Objects/Consumable/Drinks/drinks_cups.yml | 1 - .../Objects/Consumable/Drinks/drinks_fun.yml | 12 +- .../Consumable/Drinks/drinks_special.yml | 1 - .../Consumable/Drinks/trash_drinks.yml | 1 - .../Consumable/Food/Containers/bowl.yml | 1 - .../Consumable/Food/Containers/condiments.yml | 7 +- .../Objects/Consumable/Food/ingredients.yml | 5 +- .../Prototypes/Entities/Objects/Fun/toys.yml | 5 +- .../Objects/Specific/Janitorial/janitor.yml | 2 - .../Objects/Specific/chemical-containers.yml | 1 - .../Objects/Specific/chemistry-bottles.yml | 1 - .../Entities/Objects/Specific/chemistry.yml | 2 - .../Entities/Objects/Tools/bucket.yml | 2 +- 33 files changed, 842 insertions(+), 687 deletions(-) create mode 100644 Content.Server/Nutrition/Components/OpenableComponent.cs create mode 100644 Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs create mode 100644 Content.Server/Nutrition/EntitySystems/OpenableSystem.cs diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index ff12aed729..825c79e2ca 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -1,6 +1,6 @@ using Content.Server.Chemistry.EntitySystems; using Content.Server.Fluids.Components; -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reagent; @@ -23,11 +23,14 @@ namespace Content.Server.Fluids.EntitySystems; public sealed partial class PuddleSystem { + [Dependency] private readonly OpenableSystem _openable = default!; + private void InitializeSpillable() { SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(SpillOnLand); - SubscribeLocalEvent(SplashOnMeleeHit); + // openable handles the event if its closed + SubscribeLocalEvent(SplashOnMeleeHit, after: new[] { typeof(OpenableSystem) }); SubscribeLocalEvent>(AddSpillVerb); SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnSpikeOverflow); @@ -54,6 +57,9 @@ public sealed partial class PuddleSystem private void SplashOnMeleeHit(EntityUid uid, SpillableComponent component, MeleeHitEvent args) { + if (args.Handled) + return; + // When attacking someone reactive with a spillable entity, // splash a little on them (touch react) // If this also has solution transfer, then assume the transfer amount is how much we want to spill. @@ -62,9 +68,6 @@ public sealed partial class PuddleSystem if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var solution)) return; - if (TryComp(uid, out var drink) && !drink.Opened) - return; - var hitCount = args.HitEntities.Count; var totalSplit = FixedPoint2.Min(solution.MaxVolume * 0.25, solution.Volume); @@ -80,6 +83,7 @@ public sealed partial class PuddleSystem if (totalSplit == 0) return; + args.Handled = true; foreach (var hit in args.HitEntities) { if (!HasComp(hit)) @@ -135,7 +139,7 @@ public sealed partial class PuddleSystem if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out var solution)) return; - if (TryComp(uid, out var drink) && !drink.Opened) + if (_openable.IsClosed(uid)) return; if (args.User != null) @@ -156,7 +160,7 @@ public sealed partial class PuddleSystem if (!_solutionContainerSystem.TryGetSolution(args.Target, component.SolutionName, out var solution)) return; - if (TryComp(args.Target, out var drink) && (!drink.Opened)) + if (_openable.IsClosed(args.Target)) return; if (solution.Volume == FixedPoint2.Zero) diff --git a/Content.Server/Glue/GlueSystem.cs b/Content.Server/Glue/GlueSystem.cs index 67894e1854..ba8549be8e 100644 --- a/Content.Server/Glue/GlueSystem.cs +++ b/Content.Server/Glue/GlueSystem.cs @@ -5,7 +5,7 @@ using Content.Shared.Item; using Content.Shared.Glue; using Content.Shared.Interaction; using Content.Server.Chemistry.EntitySystems; -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.Database; using Content.Shared.Hands; using Robust.Shared.Timing; @@ -26,7 +26,7 @@ public sealed class GlueSystem : SharedGlueSystem { base.Initialize(); - SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent(OnInteract, after: new[] { typeof(OpenableSystem) }); SubscribeLocalEvent(OnGluedInit); SubscribeLocalEvent(OnHandPickUp); } @@ -40,11 +40,6 @@ public sealed class GlueSystem : SharedGlueSystem if (!args.CanReach || args.Target is not { Valid: true } target) return; - if (TryComp(uid, out var drink) && !drink.Opened) - { - return; - } - if (TryGlue(uid, component, target, args.User)) { args.Handled = true; diff --git a/Content.Server/Lube/LubeSystem.cs b/Content.Server/Lube/LubeSystem.cs index c79fb5ef85..6775b858d0 100644 --- a/Content.Server/Lube/LubeSystem.cs +++ b/Content.Server/Lube/LubeSystem.cs @@ -1,6 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Chemistry.EntitySystems; -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.EntitySystems; using Content.Shared.Database; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; @@ -23,7 +23,7 @@ public sealed class LubeSystem : EntitySystem { base.Initialize(); - SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent(OnInteract, after: new[] { typeof(OpenableSystem) }); } private void OnInteract(EntityUid uid, LubeComponent component, AfterInteractEvent args) @@ -34,11 +34,6 @@ public sealed class LubeSystem : EntitySystem if (!args.CanReach || args.Target is not { Valid: true } target) return; - if (TryComp(uid, out var drink) && !drink.Opened) - { - return; - } - if (TryLube(uid, component, target, args.User)) { args.Handled = true; diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs index fe6f9dcc9d..bb2bce544f 100644 --- a/Content.Server/Materials/MaterialReclaimerSystem.cs +++ b/Content.Server/Materials/MaterialReclaimerSystem.cs @@ -4,7 +4,7 @@ using Content.Server.Chemistry.EntitySystems; using Content.Server.Construction; using Content.Server.Fluids.EntitySystems; using Content.Server.GameTicking; -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.EntitySystems; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Stack; @@ -29,6 +29,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem [Dependency] private readonly AppearanceSystem _appearance = default!; [Dependency] private readonly GameTicker _ticker = default!; [Dependency] private readonly MaterialStorageSystem _materialStorage = default!; + [Dependency] private readonly OpenableSystem _openable = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly SharedBodySystem _body = default!; //bobby @@ -85,8 +86,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem if (TryComp(args.Used, out var managerComponent) && managerComponent.Solutions.Any(s => s.Value.AvailableVolume > 0)) { - if (TryComp(args.Used, out var drink) && - !drink.Opened) + if (_openable.IsClosed(args.Used)) return; if (TryComp(args.Used, out var transfer) && diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs index d764ab2c2e..e967964d90 100644 --- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs +++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs @@ -40,6 +40,7 @@ public sealed class NPCUtilitySystem : EntitySystem [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + [Dependency] private readonly OpenableSystem _openable = default!; [Dependency] private readonly PuddleSystem _puddle = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SolutionContainerSystem _solutions = default!; @@ -156,6 +157,10 @@ public sealed class NPCUtilitySystem : EntitySystem if (!TryComp(targetUid, out var food)) return 0f; + // mice can't eat unpeeled bananas, need monkey's help + if (_openable.IsClosed(targetUid)) + return 0f; + if (!_food.IsDigestibleBy(owner, targetUid, food)) return 0f; @@ -173,7 +178,11 @@ public sealed class NPCUtilitySystem : EntitySystem } case DrinkValueCon: { - if (!TryComp(targetUid, out var drink) || !drink.Opened) + if (!TryComp(targetUid, out var drink)) + return 0f; + + // can't drink closed drinks + if (_openable.IsClosed(targetUid)) return 0f; // only drink when thirsty diff --git a/Content.Server/Nutrition/Components/DrinkComponent.cs b/Content.Server/Nutrition/Components/DrinkComponent.cs index aa467d3e61..20d47cda88 100644 --- a/Content.Server/Nutrition/Components/DrinkComponent.cs +++ b/Content.Server/Nutrition/Components/DrinkComponent.cs @@ -1,54 +1,41 @@ using Content.Server.Nutrition.EntitySystems; -using Content.Shared.DoAfter; using Content.Shared.FixedPoint; -using JetBrains.Annotations; using Robust.Shared.Audio; -namespace Content.Server.Nutrition.Components +namespace Content.Server.Nutrition.Components; + +[RegisterComponent, Access(typeof(DrinkSystem))] +public sealed partial class DrinkComponent : Component { - [RegisterComponent] - [Access(typeof(DrinkSystem))] - public sealed partial class DrinkComponent : Component - { - [DataField("solution")] - public string SolutionName { get; set; } = DefaultSolutionName; - public const string DefaultSolutionName = "drink"; + [DataField, ViewVariables(VVAccess.ReadWrite)] + public string Solution = "drink"; - [DataField("useSound")] - public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/drink.ogg"); + [DataField] + public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/drink.ogg"); - [DataField("isOpen")] - internal bool DefaultToOpened; + [DataField, ViewVariables(VVAccess.ReadWrite)] + public FixedPoint2 TransferAmount = FixedPoint2.New(5); - [ViewVariables(VVAccess.ReadWrite)] - [DataField("transferAmount")] - public FixedPoint2 TransferAmount { get; [UsedImplicitly] private set; } = FixedPoint2.New(5); + /// + /// How long it takes to drink this yourself. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float Delay = 1; - [ViewVariables(VVAccess.ReadWrite)] - public bool Opened; + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool Examinable = true; - [DataField("openSounds")] - public SoundSpecifier OpenSounds = new SoundCollectionSpecifier("canOpenSounds"); + /// + /// If true, trying to drink when empty will not handle the event. + /// This means other systems such as equipping on use can run. + /// Example usecase is the bucket. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool IgnoreEmpty; - [DataField("pressurized")] - public bool Pressurized; - - [DataField("burstSound")] - public SoundSpecifier BurstSound = new SoundPathSpecifier("/Audio/Effects/flash_bang.ogg"); - - /// - /// How long it takes to drink this yourself. - /// - [DataField("delay")] - public float Delay = 1; - - [DataField("examinable")] - public bool Examinable = true; - - /// - /// This is how many seconds it takes to force feed someone this drink. - /// - [DataField("forceFeedDelay")] - public float ForceFeedDelay = 3; - } + /// + /// This is how many seconds it takes to force feed someone this drink. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float ForceFeedDelay = 3; } diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index 3d75fd633b..9e37af2a10 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -6,87 +6,67 @@ using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -namespace Content.Server.Nutrition.Components +namespace Content.Server.Nutrition.Components; + +[RegisterComponent, Access(typeof(FoodSystem))] +public sealed partial class FoodComponent : Component { - [RegisterComponent, Access(typeof(FoodSystem))] - public sealed partial class FoodComponent : Component - { - [DataField("solution")] - public string SolutionName { get; set; } = "food"; + [DataField] + public string Solution = "food"; - [DataField("useSound")] - public SoundSpecifier UseSound { get; set; } = new SoundPathSpecifier("/Audio/Items/eatfood.ogg"); + [DataField] + public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/eatfood.ogg"); - [DataField("trash", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? TrashPrototype { get; set; } + [DataField] + public EntProtoId? TrashPrototype; - [DataField("transferAmount")] - public FixedPoint2? TransferAmount { get; set; } = FixedPoint2.New(5); + [DataField] + public FixedPoint2? TransferAmount = FixedPoint2.New(5); - /// - /// Acceptable utensil to use - /// - [DataField("utensil")] - public UtensilType Utensil = UtensilType.Fork; //There are more "solid" than "liquid" food + /// + /// Acceptable utensil to use + /// + [DataField] + public UtensilType Utensil = UtensilType.Fork; //There are more "solid" than "liquid" food - /// - /// Is utensil required to eat this food - /// - [DataField("utensilRequired")] - public bool UtensilRequired = false; + /// + /// Is utensil required to eat this food + /// + [DataField] + public bool UtensilRequired; - /// - /// If this is set to true, food can only be eaten if you have a stomach with a - /// that includes this entity in its whitelist, - /// rather than just being digestible by anything that can eat food. - /// Whitelist the food component to allow eating of normal food. - /// - [DataField("requiresSpecialDigestion")] - public bool RequiresSpecialDigestion = false; + /// + /// If this is set to true, food can only be eaten if you have a stomach with a + /// that includes this entity in its whitelist, + /// rather than just being digestible by anything that can eat food. + /// Whitelist the food component to allow eating of normal food. + /// + [DataField] + public bool RequiresSpecialDigestion; - /// - /// Stomachs required to digest this entity. - /// Used to simulate 'ruminant' digestive systems (which can digest grass) - /// - [DataField("requiredStomachs")] - public int RequiredStomachs = 1; + /// + /// Stomachs required to digest this entity. + /// Used to simulate 'ruminant' digestive systems (which can digest grass) + /// + [DataField] + public int RequiredStomachs = 1; - /// - /// The localization identifier for the eat message. Needs a "food" entity argument passed to it. - /// - [DataField("eatMessage")] - public string EatMessage = "food-nom"; + /// + /// The localization identifier for the eat message. Needs a "food" entity argument passed to it. + /// + [DataField] + public string EatMessage = "food-nom"; - /// - /// How long it takes to eat the food personally. - /// - [DataField("delay")] - public float Delay = 1; + /// + /// How long it takes to eat the food personally. + /// + [DataField] + public float Delay = 1; - /// - /// This is how many seconds it takes to force feed someone this food. - /// Should probably be smaller for small items like pills. - /// - [DataField("forceFeedDelay")] - public float ForceFeedDelay = 3; - - [ViewVariables] - public int UsesRemaining - { - get - { - if (!EntitySystem.Get().TryGetSolution(Owner, SolutionName, out var solution)) - { - return 0; - } - - if (TransferAmount == null) - return solution.Volume == 0 ? 0 : 1; - - return solution.Volume == 0 - ? 0 - : Math.Max(1, (int) Math.Ceiling((solution.Volume / (FixedPoint2)TransferAmount).Float())); - } - } - } + /// + /// This is how many seconds it takes to force feed someone this food. + /// Should probably be smaller for small items like pills. + /// + [DataField] + public float ForceFeedDelay = 3; } diff --git a/Content.Server/Nutrition/Components/OpenableComponent.cs b/Content.Server/Nutrition/Components/OpenableComponent.cs new file mode 100644 index 0000000000..5164ed21ec --- /dev/null +++ b/Content.Server/Nutrition/Components/OpenableComponent.cs @@ -0,0 +1,46 @@ +using Content.Server.Nutrition.EntitySystems; +using Robust.Shared.Audio; + +namespace Content.Server.Nutrition.Components; + +/// +/// A drink or food that can be opened. +/// Starts closed, open it with Z or E. +/// +[RegisterComponent, Access(typeof(OpenableSystem))] +public sealed partial class OpenableComponent : Component +{ + /// + /// Whether this drink or food is opened or not. + /// Drinks can only be drunk or poured from/into when open, and food can only be eaten when open. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool Opened; + + /// + /// If this is false you cant press Z to open it. + /// Requires an OpenBehavior damage threshold or other logic to open. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public bool OpenableByHand = true; + + /// + /// Text shown when examining and its open. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public string ExamineText = "drink-component-on-examine-is-opened"; + + /// + /// The locale id for the popup shown when IsClosed is called and closed. Needs a "owner" entity argument passed to it. + /// Defaults to the popup drink uses since its "correct". + /// It's still generic enough that you should change it if you make openable non-drinks, i.e. unwrap it first, peel it first. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public string ClosedPopup = "drink-component-try-use-drink-not-open"; + + /// + /// Sound played when opening. + /// + [DataField] + public SoundSpecifier Sound = new SoundCollectionSpecifier("canOpenSounds"); +} diff --git a/Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs b/Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs new file mode 100644 index 0000000000..aafb3bc106 --- /dev/null +++ b/Content.Server/Nutrition/Components/PressurizedDrinkComponent.cs @@ -0,0 +1,27 @@ +using Content.Server.Nutrition.EntitySystems; +using Robust.Shared.Audio; + +namespace Content.Server.Nutrition.Components; + +/// +/// Lets a drink burst open when thrown while closed. +/// Requires and to work. +/// +[RegisterComponent, Access(typeof(DrinkSystem))] +public sealed partial class PressurizedDrinkComponent : Component +{ + /// + /// Chance for the drink to burst when thrown while closed. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float BurstChance = 0.25f; + + /// + /// Sound played when the drink bursts. + /// + [DataField] + public SoundSpecifier BurstSound = new SoundPathSpecifier("/Audio/Effects/flash_bang.ogg") + { + Params = AudioParams.Default.WithVolume(-4) + }; +} diff --git a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs index 2740f52b0c..9af2397720 100644 --- a/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/CreamPieSystem.cs @@ -41,7 +41,7 @@ namespace Content.Server.Nutrition.EntitySystems if (EntityManager.TryGetComponent(uid, out FoodComponent? foodComp)) { - if (_solutions.TryGetSolution(uid, foodComp.SolutionName, out var solution)) + if (_solutions.TryGetSolution(uid, foodComp.Solution, out var solution)) { _puddle.TrySpillAt(uid, solution, out _, false); } diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index 1aff7e1e9b..2c8a0a768f 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -5,6 +5,7 @@ using Content.Server.Chemistry.EntitySystems; using Content.Server.Chemistry.ReagentEffects; using Content.Server.Fluids.EntitySystems; using Content.Server.Forensics; +using Content.Server.Inventory; using Content.Server.Nutrition.Components; using Content.Server.Popups; using Content.Shared.Administration.Logs; @@ -19,7 +20,6 @@ using Content.Shared.FixedPoint; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition; using Content.Shared.Nutrition.Components; @@ -36,14 +36,15 @@ namespace Content.Server.Nutrition.EntitySystems; public sealed class DrinkSystem : EntitySystem { [Dependency] private readonly BodySystem _body = default!; - [Dependency] private readonly FoodSystem _food = default!; [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!; + [Dependency] private readonly FoodSystem _food = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly OpenableSystem _openable = default!; [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly PuddleSystem _puddleSystem = default!; + [Dependency] private readonly PuddleSystem _puddle = default!; [Dependency] private readonly ReactiveSystem _reaction = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; @@ -59,13 +60,16 @@ public sealed class DrinkSystem : EntitySystem // TODO add InteractNoHandEvent for entities like mice. SubscribeLocalEvent(OnSolutionChange); SubscribeLocalEvent(OnDrinkInit); - SubscribeLocalEvent(HandleLand); - SubscribeLocalEvent(OnUse); + // run before inventory so for bucket it always tries to drink before equipping (when empty) + // run after openable so its always open -> drink + SubscribeLocalEvent(OnUse, before: new[] { typeof(ServerInventorySystem) }, after: new[] { typeof(OpenableSystem) }); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent>(AddDrinkVerb); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnTransferAttempt); + // put drink amount after opened + SubscribeLocalEvent(OnExamined, after: new[] { typeof(OpenableSystem) }); SubscribeLocalEvent(OnDoAfter); + + SubscribeLocalEvent(OnPressurizedDrinkLand); } private FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null) @@ -73,7 +77,7 @@ public sealed class DrinkSystem : EntitySystem if (!Resolve(uid, ref component)) return FixedPoint2.Zero; - if (!_solutionContainer.TryGetSolution(uid, component.SolutionName, out var sol)) + if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var sol)) return FixedPoint2.Zero; return sol.Volume; @@ -95,7 +99,7 @@ public sealed class DrinkSystem : EntitySystem if (!Resolve(uid, ref comp)) return 0f; - if (!_solutionContainer.TryGetSolution(uid, comp.SolutionName, out var solution)) + if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out var solution)) return 0f; var total = 0f; @@ -123,51 +127,36 @@ public sealed class DrinkSystem : EntitySystem private void OnExamined(EntityUid uid, DrinkComponent component, ExaminedEvent args) { - if (!component.Opened || !args.IsInDetailsRange || !component.Examinable) + var hasOpenable = TryComp(uid, out var openable); + if (_openable.IsClosed(uid, null, openable) || !args.IsInDetailsRange || !component.Examinable) return; - var color = IsEmpty(uid, component) ? "gray" : "yellow"; - var openedText = - Loc.GetString(IsEmpty(uid, component) ? "drink-component-on-examine-is-empty" : "drink-component-on-examine-is-opened"); - args.Message.AddMarkup($"\n{Loc.GetString("drink-component-on-examine-details-text", ("colorName", color), ("text", openedText))}"); - if (!IsEmpty(uid, component)) + // put Empty / Xu after Opened, or start a new line + args.Message.AddMarkup(hasOpenable ? " - " : "\n"); + + var empty = IsEmpty(uid, component); + if (empty) { - if (TryComp(uid, out var comp)) - { - //provide exact measurement for beakers - args.Message.AddMarkup($" - {Loc.GetString("drink-component-on-examine-exact-volume", ("amount", DrinkVolume(uid, component)))}"); - } - else - { - //general approximation - var remainingString = (int) _solutionContainer.PercentFull(uid) switch - { - 100 => "drink-component-on-examine-is-full", - > 66 => "drink-component-on-examine-is-mostly-full", - > 33 => HalfEmptyOrHalfFull(args), - _ => "drink-component-on-examine-is-mostly-empty", - }; - args.Message.AddMarkup($" - {Loc.GetString(remainingString)}"); - } + args.Message.AddMarkup(Loc.GetString("drink-component-on-examine-is-empty")); + return; } - } - private void SetOpen(EntityUid uid, bool opened = false, DrinkComponent? component = null) - { - if(!Resolve(uid, ref component)) - return; - - if (opened == component.Opened) - return; - - component.Opened = opened; - - if (!_solutionContainer.TryGetSolution(uid, component.SolutionName, out _)) - return; - - if (EntityManager.TryGetComponent(uid, out var appearance)) + if (TryComp(uid, out var comp)) { - _appearance.SetData(uid, DrinkCanStateVisual.Opened, opened, appearance); + //provide exact measurement for beakers + args.Message.AddMarkup(Loc.GetString("drink-component-on-examine-exact-volume", ("amount", DrinkVolume(uid, component)))); + } + else + { + //general approximation + var remainingString = (int) _solutionContainer.PercentFull(uid) switch + { + 100 => "drink-component-on-examine-is-full", + > 66 => "drink-component-on-examine-is-mostly-full", + > 33 => HalfEmptyOrHalfFull(args), + _ => "drink-component-on-examine-is-mostly-empty", + }; + args.Message.AddMarkup(Loc.GetString(remainingString)); } } @@ -184,56 +173,47 @@ public sealed class DrinkSystem : EntitySystem if (args.Handled) return; - if (!component.Opened) - { - //Do the opening stuff like playing the sounds. - _audio.PlayPvs(_audio.GetSound(component.OpenSounds), args.User); - - SetOpen(uid, true, component); - return; - } - args.Handled = TryDrink(args.User, args.User, component, uid); } - private void HandleLand(EntityUid uid, DrinkComponent component, ref LandEvent args) + private void OnPressurizedDrinkLand(EntityUid uid, PressurizedDrinkComponent comp, ref LandEvent args) { - if (component.Pressurized && - !component.Opened && - _random.Prob(0.25f) && - _solutionContainer.TryGetSolution(uid, component.SolutionName, out var interactions)) + if (!TryComp(uid, out var drink) || !TryComp(uid, out var openable)) + return; + + if (!openable.Opened && + _random.Prob(comp.BurstChance) && + _solutionContainer.TryGetSolution(uid, drink.Solution, out var interactions)) { - component.Opened = true; - UpdateAppearance(uid, component); + // using SetOpen instead of TryOpen to not play 2 sounds + _openable.SetOpen(uid, true, openable); var solution = _solutionContainer.SplitSolution(uid, interactions, interactions.Volume); - _puddleSystem.TrySpillAt(uid, solution, out _); + _puddle.TrySpillAt(uid, solution, out _); - _audio.PlayPvs(_audio.GetSound(component.BurstSound), uid, AudioParams.Default.WithVolume(-4)); + _audio.PlayPvs(comp.BurstSound, uid); } } private void OnDrinkInit(EntityUid uid, DrinkComponent component, ComponentInit args) { - SetOpen(uid, component.DefaultToOpened, component); - - if (EntityManager.TryGetComponent(uid, out DrainableSolutionComponent? existingDrainable)) + if (TryComp(uid, out var existingDrainable)) { // Beakers have Drink component but they should use the existing Drainable - component.SolutionName = existingDrainable.Solution; + component.Solution = existingDrainable.Solution; } else { - _solutionContainer.EnsureSolution(uid, component.SolutionName); + _solutionContainer.EnsureSolution(uid, component.Solution); } UpdateAppearance(uid, component); if (TryComp(uid, out RefillableSolutionComponent? refillComp)) - refillComp.Solution = component.SolutionName; + refillComp.Solution = component.Solution; if (TryComp(uid, out DrainableSolutionComponent? drainComp)) - drainComp.Solution = component.SolutionName; + drainComp.Solution = component.Solution; } private void OnSolutionChange(EntityUid uid, DrinkComponent component, SolutionChangedEvent args) @@ -251,34 +231,23 @@ public sealed class DrinkSystem : EntitySystem var drainAvailable = DrinkVolume(uid, component); _appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance); - _appearance.SetData(uid, DrinkCanStateVisual.Opened, component.Opened, appearance); - } - - private void OnTransferAttempt(EntityUid uid, DrinkComponent component, SolutionTransferAttemptEvent args) - { - if (!component.Opened) - { - args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", uid))); - } } private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item) { - if (!EntityManager.HasComponent(target)) + if (!HasComp(target)) return false; - if (!drink.Opened) - { - _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open", - ("owner", EntityManager.GetComponent(item).EntityName)), item, user); + if (_openable.IsClosed(item, user)) return true; - } - if (!_solutionContainer.TryGetSolution(item, drink.SolutionName, out var drinkSolution) || + if (!_solutionContainer.TryGetSolution(item, drink.Solution, out var drinkSolution) || drinkSolution.Volume <= 0) { - _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", - ("entity", EntityManager.GetComponent(item).EntityName)), item, user); + if (drink.IgnoreEmpty) + return false; + + _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user); return true; } @@ -297,8 +266,7 @@ public sealed class DrinkSystem : EntitySystem { var userName = Identity.Entity(user, EntityManager); - _popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), - user, target); + _popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target); // logging _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SolutionContainerSystem.ToPrettyString(drinkSolution)}"); @@ -365,11 +333,11 @@ public sealed class DrinkSystem : EntitySystem if (!_body.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) { - _popup.PopupEntity(forceDrink ? Loc.GetString("drink-component-try-use-drink-cannot-drink-other") : Loc.GetString("drink-component-try-use-drink-had-enough"), args.Target.Value, args.User); + _popup.PopupEntity(Loc.GetString(forceDrink ? "drink-component-try-use-drink-cannot-drink-other" : "drink-component-try-use-drink-had-enough"), args.Target.Value, args.User); if (HasComp(args.Target.Value)) { - _puddleSystem.TrySpillAt(args.User, drained, out _); + _puddle.TrySpillAt(args.User, drained, out _); return; } @@ -387,7 +355,7 @@ public sealed class DrinkSystem : EntitySystem if (forceDrink) { _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Target.Value, args.User); - _puddleSystem.TrySpillAt(args.Target.Value, drained, out _); + _puddle.TrySpillAt(args.Target.Value, drained, out _); } else _solutionContainer.TryAddSolution(uid, solution, drained); @@ -423,7 +391,7 @@ public sealed class DrinkSystem : EntitySystem _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(uid):drink}"); } - _audio.PlayPvs(_audio.GetSound(component.UseSound), args.Target.Value, AudioParams.Default.WithVolume(-2f)); + _audio.PlayPvs(component.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f)); _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion); //TODO: Grab the stomach UIDs somehow without using Owner @@ -442,11 +410,12 @@ public sealed class DrinkSystem : EntitySystem if (uid == ev.User || !ev.CanInteract || !ev.CanAccess || - !EntityManager.TryGetComponent(ev.User, out BodyComponent? body) || + !TryComp(ev.User, out var body) || !_body.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) return; - if (EntityManager.TryGetComponent(uid, out var mobState) && _mobStateSystem.IsAlive(uid, mobState)) + // no drinking from living drinks, have to kill them first. + if (_mobState.IsAlive(uid)) return; AlternativeVerb verb = new() diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index 125edd2c4c..f1a4a6d5f4 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -19,7 +19,6 @@ using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; -using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition; using Content.Shared.Verbs; @@ -30,469 +29,488 @@ using Robust.Shared.Utility; using Content.Shared.Tag; using Content.Shared.Storage; -namespace Content.Server.Nutrition.EntitySystems +namespace Content.Server.Nutrition.EntitySystems; + +/// +/// Handles feeding attempts both on yourself and on the target. +/// +public sealed class FoodSystem : EntitySystem { - /// - /// Handles feeding attempts both on yourself and on the target. - /// - public sealed class FoodSystem : EntitySystem + [Dependency] private readonly BodySystem _body = default!; + [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly OpenableSystem _openable = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly ReactiveSystem _reaction = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly StackSystem _stack = default!; + [Dependency] private readonly StomachSystem _stomach = default!; + [Dependency] private readonly UtensilSystem _utensil = default!; + + public const float MaxFeedDistance = 1.0f; + + public override void Initialize() { - [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; - [Dependency] private readonly FlavorProfileSystem _flavorProfileSystem = default!; - [Dependency] private readonly BodySystem _bodySystem = default!; - [Dependency] private readonly StomachSystem _stomachSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly UtensilSystem _utensilSystem = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; - [Dependency] private readonly SharedHandsSystem _handsSystem = default!; - [Dependency] private readonly ReactiveSystem _reaction = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly StackSystem _stack = default!; + base.Initialize(); - public const float MaxFeedDistance = 1.0f; + // TODO add InteractNoHandEvent for entities like mice. + // run after openable for wrapped/peelable foods + SubscribeLocalEvent(OnUseFoodInHand, after: new[] { typeof(OpenableSystem) }); + SubscribeLocalEvent(OnFeedFood); + SubscribeLocalEvent>(AddEatVerb); + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnInventoryIngestAttempt); + } - public override void Initialize() + /// + /// Eat item + /// + private void OnUseFoodInHand(EntityUid uid, FoodComponent foodComponent, UseInHandEvent ev) + { + if (ev.Handled) + return; + + var result = TryFeed(ev.User, ev.User, uid, foodComponent); + ev.Handled = result.Handled; + } + + /// + /// Feed someone else + /// + private void OnFeedFood(EntityUid uid, FoodComponent foodComponent, AfterInteractEvent args) + { + if (args.Handled || args.Target == null || !args.CanReach) + return; + + var result = TryFeed(args.User, args.Target.Value, uid, foodComponent); + args.Handled = result.Handled; + } + + public (bool Success, bool Handled) TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp) + { + //Suppresses eating yourself and alive mobs + if (food == user || _mobState.IsAlive(food)) + return (false, false); + + // Target can't be fed or they're already eating + if (!TryComp(target, out var body)) + return (false, false); + + if (_openable.IsClosed(food, user)) + return (false, true); + + if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out var foodSolution) || foodSolution.Name == null) + return (false, false); + + if (!_body.TryGetBodyOrganComponents(target, out var stomachs, body)) + return (false, false); + + // Check for special digestibles + if (!IsDigestibleBy(food, foodComp, stomachs)) + return (false, false); + + if (!TryGetRequiredUtensils(user, foodComp, out _)) + return (false, false); + + // Check for used storage on the food item + if (TryComp(food, out var storageState) && storageState.StorageUsed != 0) { - base.Initialize(); - - // TODO add InteractNoHandEvent for entities like mice. - SubscribeLocalEvent(OnUseFoodInHand); - SubscribeLocalEvent(OnFeedFood); - SubscribeLocalEvent>(AddEatVerb); - SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnInventoryIngestAttempt); + _popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user); + return (false, true); } - /// - /// Eat item - /// - private void OnUseFoodInHand(EntityUid uid, FoodComponent foodComponent, UseInHandEvent ev) - { - if (ev.Handled) - return; + var flavors = _flavorProfile.GetLocalizedFlavorsMessage(food, user, foodSolution); - var result = TryFeed(ev.User, ev.User, uid, foodComponent); - ev.Handled = result.Handled; + if (GetUsesRemaining(food, foodComp) <= 0) + { + _popup.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user); + DeleteAndSpawnTrash(foodComp, food, user); + return (false, true); } - /// - /// Feed someone else - /// - private void OnFeedFood(EntityUid uid, FoodComponent foodComponent, AfterInteractEvent args) - { - if (args.Handled || args.Target == null || !args.CanReach) - return; + if (IsMouthBlocked(target, user)) + return (false, true); - var result = TryFeed(args.User, args.Target.Value, uid, foodComponent); - args.Handled = result.Handled; + if (!_interaction.InRangeUnobstructed(user, food, popup: true)) + return (false, true); + + if (!_interaction.InRangeUnobstructed(user, target, MaxFeedDistance, popup: true)) + return (false, true); + + // TODO make do-afters account for fixtures in the range check. + if (!Transform(user).MapPosition.InRange(Transform(target).MapPosition, MaxFeedDistance)) + { + var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); + _popup.PopupEntity(message, user, user); + return (false, true); } - public (bool Success, bool Handled) TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp) + var forceFeed = user != target; + if (forceFeed) { - //Suppresses self-eating - if (food == user || TryComp(food, out var mobState) && _mobStateSystem.IsAlive(food, mobState)) // Suppresses eating alive mobs - return (false, false); + var userName = Identity.Entity(user, EntityManager); + _popup.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)), + user, target); - // Target can't be fed or they're already eating - if (!TryComp(target, out var body)) - return (false, false); - - if (!_solutionContainerSystem.TryGetSolution(food, foodComp.SolutionName, out var foodSolution) || foodSolution.Name == null) - return (false, false); - - if (!_bodySystem.TryGetBodyOrganComponents(target, out var stomachs, body)) - return (false, false); - - var forceFeed = user != target; - - // Check for special digestibles - if (!IsDigestibleBy(food, foodComp, stomachs)) - { - _popupSystem.PopupEntity( - forceFeed - ? Loc.GetString("food-system-cant-digest-other", ("entity", food)) - : Loc.GetString("food-system-cant-digest", ("entity", food)), user, user); - return (false, true); - } - - // Check for used storage on the food item - if (TryComp(food, out var storageState) && storageState.StorageUsed != 0) - { - _popupSystem.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user); - return (false, true); - } - - var flavors = _flavorProfileSystem.GetLocalizedFlavorsMessage(food, user, foodSolution); - - if (foodComp.UsesRemaining <= 0) - { - _popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user); - DeleteAndSpawnTrash(foodComp, food, user); - return (false, true); - } - - if (IsMouthBlocked(target, user)) - return (false, true); - - if (!_interactionSystem.InRangeUnobstructed(user, food, popup: true)) - return (false, true); - - if (!_interactionSystem.InRangeUnobstructed(user, target, MaxFeedDistance, popup: true)) - return (false, true); - - // TODO make do-afters account for fixtures in the range check. - if (!Transform(user).MapPosition.InRange(Transform(target).MapPosition, MaxFeedDistance)) - { - var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); - _popupSystem.PopupEntity(message, user, user); - return (false, true); - } - - if (!TryGetRequiredUtensils(user, foodComp, out _)) - return (false, true); - - if (forceFeed) - { - var userName = Identity.Entity(user, EntityManager); - _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)), - user, target); - - // logging - _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}"); - } - else - { - // log voluntary eating - _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}"); - } - - var doAfterArgs = new DoAfterArgs(EntityManager, - user, - forceFeed ? foodComp.ForceFeedDelay : foodComp.Delay, - new ConsumeDoAfterEvent(foodSolution.Name, flavors), - eventTarget: food, - target: target, - used: food) - { - BreakOnUserMove = forceFeed, - BreakOnDamage = true, - BreakOnTargetMove = forceFeed, - MovementThreshold = 0.01f, - DistanceThreshold = MaxFeedDistance, - // Mice and the like can eat without hands. - // TODO maybe set this based on some CanEatWithoutHands event or component? - NeedHand = forceFeed, - }; - - _doAfterSystem.TryStartDoAfter(doAfterArgs); - return (true, true); + // logging + _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}"); + } + else + { + // log voluntary eating + _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}"); } - private void OnDoAfter(EntityUid uid, FoodComponent component, ConsumeDoAfterEvent args) + var doAfterArgs = new DoAfterArgs(EntityManager, + user, + forceFeed ? foodComp.ForceFeedDelay : foodComp.Delay, + new ConsumeDoAfterEvent(foodSolution.Name, flavors), + eventTarget: food, + target: target, + used: food) { - if (args.Cancelled || args.Handled || component.Deleted || args.Target == null) - return; + BreakOnUserMove = forceFeed, + BreakOnDamage = true, + BreakOnTargetMove = forceFeed, + MovementThreshold = 0.01f, + DistanceThreshold = MaxFeedDistance, + // Mice and the like can eat without hands. + // TODO maybe set this based on some CanEatWithoutHands event or component? + NeedHand = forceFeed, + }; - if (!TryComp(args.Target.Value, out var body)) - return; + _doAfter.TryStartDoAfter(doAfterArgs); + return (true, true); + } - if (!_bodySystem.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) - return; + private void OnDoAfter(EntityUid uid, FoodComponent component, ConsumeDoAfterEvent args) + { + if (args.Cancelled || args.Handled || component.Deleted || args.Target == null) + return; - if (!_solutionContainerSystem.TryGetSolution(args.Used, args.Solution, out var solution)) - return; + if (!TryComp(args.Target.Value, out var body)) + return; - if (!TryGetRequiredUtensils(args.User, component, out var utensils)) - return; + if (!_body.TryGetBodyOrganComponents(args.Target.Value, out var stomachs, body)) + return; - // TODO this should really be checked every tick. - if (IsMouthBlocked(args.Target.Value)) - return; + if (!_solutionContainer.TryGetSolution(args.Used, args.Solution, out var solution)) + return; - // TODO this should really be checked every tick. - if (!_interactionSystem.InRangeUnobstructed(args.User, args.Target.Value)) - return; + if (!TryGetRequiredUtensils(args.User, component, out var utensils)) + return; - var forceFeed = args.User != args.Target; + // TODO this should really be checked every tick. + if (IsMouthBlocked(args.Target.Value)) + return; - args.Handled = true; - var transferAmount = component.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) component.TransferAmount, solution.Volume) : solution.Volume; + // TODO this should really be checked every tick. + if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value)) + return; - var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount); + var forceFeed = args.User != args.Target; - //TODO: Get the stomach UID somehow without nabbing owner - // Get the stomach with the highest available solution volume - var highestAvailable = FixedPoint2.Zero; - StomachComponent? stomachToUse = null; - foreach (var (stomach, _) in stomachs) - { - var owner = stomach.Owner; - if (!_stomachSystem.CanTransferSolution(owner, split)) - continue; + args.Handled = true; + var transferAmount = component.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) component.TransferAmount, solution.Volume) : solution.Volume; - if (!_solutionContainerSystem.TryGetSolution(owner, StomachSystem.DefaultSolutionName, - out var stomachSol)) - continue; + var split = _solutionContainer.SplitSolution(uid, solution, transferAmount); - if (stomachSol.AvailableVolume <= highestAvailable) - continue; + //TODO: Get the stomach UID somehow without nabbing owner + // Get the stomach with the highest available solution volume + var highestAvailable = FixedPoint2.Zero; + StomachComponent? stomachToUse = null; + foreach (var (stomach, _) in stomachs) + { + var owner = stomach.Owner; + if (!_stomach.CanTransferSolution(owner, split)) + continue; - stomachToUse = stomach; - highestAvailable = stomachSol.AvailableVolume; - } + if (!_solutionContainer.TryGetSolution(owner, StomachSystem.DefaultSolutionName, + out var stomachSol)) + continue; - // No stomach so just popup a message that they can't eat. - if (stomachToUse == null) - { - _solutionContainerSystem.TryAddSolution(uid, solution, split); - _popupSystem.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other") : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User); - return; - } + if (stomachSol.AvailableVolume <= highestAvailable) + continue; - _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion); - _stomachSystem.TryTransferSolution(stomachToUse.Owner, split, stomachToUse); - - var flavors = args.FlavorMessage; - - if (forceFeed) - { - var targetName = Identity.Entity(args.Target.Value, EntityManager); - var userName = Identity.Entity(args.User, EntityManager); - _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), - uid, uid); - - _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User); - - // log successful force feed - _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(uid):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(uid):food}"); - } - else - { - _popupSystem.PopupEntity(Loc.GetString(component.EatMessage, ("food", uid), ("flavors", flavors)), args.User, args.User); - - // log successful voluntary eating - _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(uid):food}"); - } - - _audio.Play(component.UseSound, Filter.Pvs(args.Target.Value), args.Target.Value, true, AudioParams.Default.WithVolume(-1f)); - - // Try to break all used utensils - foreach (var utensil in utensils) - { - _utensilSystem.TryBreak(utensil, args.User); - } - - args.Repeat = !forceFeed; - - if (TryComp(uid, out var stack)) - { - //Not deleting whole stack piece will make troubles with grinding object - if (stack.Count > 1) - { - _stack.SetCount(uid, stack.Count - 1); - _solutionContainerSystem.TryAddSolution(uid, solution, split); - return; - } - } - else if (component.UsesRemaining > 0) - { - return; - } - - var ev = new BeforeFullyEatenEvent - { - User = args.User - }; - RaiseLocalEvent(uid, ev); - if (ev.Cancelled) - return; - - if (string.IsNullOrEmpty(component.TrashPrototype)) - QueueDel(uid); - else - DeleteAndSpawnTrash(component, uid, args.User); + stomachToUse = stomach; + highestAvailable = stomachSol.AvailableVolume; } - public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityUid? user = null) + // No stomach so just popup a message that they can't eat. + if (stomachToUse == null) { - //We're empty. Become trash. - var position = Transform(food).MapPosition; - var finisher = EntityManager.SpawnEntity(component.TrashPrototype, position); + _solutionContainer.TryAddSolution(uid, solution, split); + _popup.PopupEntity(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other") : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User); + return; + } - // If the user is holding the item - if (user != null && _handsSystem.IsHolding(user.Value, food, out var hand)) + _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion); + _stomach.TryTransferSolution(stomachToUse.Owner, split, stomachToUse); + + var flavors = args.FlavorMessage; + + if (forceFeed) + { + var targetName = Identity.Entity(args.Target.Value, EntityManager); + var userName = Identity.Entity(args.User, EntityManager); + _popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), + uid, uid); + + _popup.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User); + + // log successful force feed + _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(uid):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(uid):food}"); + } + else + { + _popup.PopupEntity(Loc.GetString(component.EatMessage, ("food", uid), ("flavors", flavors)), args.User, args.User); + + // log successful voluntary eating + _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(uid):food}"); + } + + _audio.Play(component.UseSound, Filter.Pvs(args.Target.Value), args.Target.Value, true, AudioParams.Default.WithVolume(-1f)); + + // Try to break all used utensils + foreach (var utensil in utensils) + { + _utensil.TryBreak(utensil, args.User); + } + + args.Repeat = !forceFeed; + + if (TryComp(uid, out var stack)) + { + //Not deleting whole stack piece will make troubles with grinding object + if (stack.Count > 1) { - EntityManager.DeleteEntity(food); - - // Put the trash in the user's hand - _handsSystem.TryPickup(user.Value, finisher, hand); + _stack.SetCount(uid, stack.Count - 1); + _solutionContainer.TryAddSolution(uid, solution, split); return; } - - EntityManager.QueueDeleteEntity(food); + } + else if (GetUsesRemaining(uid, component) > 0) + { + return; } - private void AddEatVerb(EntityUid uid, FoodComponent component, GetVerbsEvent ev) + var ev = new BeforeFullyEatenEvent { - if (uid == ev.User || - !ev.CanInteract || - !ev.CanAccess || - !EntityManager.TryGetComponent(ev.User, out BodyComponent? body) || - !_bodySystem.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) - return; + User = args.User + }; + RaiseLocalEvent(uid, ev); + if (ev.Cancelled) + return; - if (EntityManager.TryGetComponent(uid, out var mobState) && _mobStateSystem.IsAlive(uid, mobState)) - return; + if (string.IsNullOrEmpty(component.TrashPrototype)) + QueueDel(uid); + else + DeleteAndSpawnTrash(component, uid, args.User); + } - AlternativeVerb verb = new() + public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityUid? user = null) + { + //We're empty. Become trash. + var position = Transform(food).MapPosition; + var finisher = Spawn(component.TrashPrototype, position); + + // If the user is holding the item + if (user != null && _hands.IsHolding(user.Value, food, out var hand)) + { + Del(food); + + // Put the trash in the user's hand + _hands.TryPickup(user.Value, finisher, hand); + return; + } + + QueueDel(food); + } + + private void AddEatVerb(EntityUid uid, FoodComponent component, GetVerbsEvent ev) + { + if (uid == ev.User || + !ev.CanInteract || + !ev.CanAccess || + !TryComp(ev.User, out var body) || + !_body.TryGetBodyOrganComponents(ev.User, out var stomachs, body)) + return; + + // have to kill mouse before eating it + if (_mobState.IsAlive(uid)) + return; + + // only give moths eat verb for clothes since it would just fail otherwise + if (!IsDigestibleBy(uid, component, stomachs)) + return; + + AlternativeVerb verb = new() + { + Act = () => { - Act = () => - { - TryFeed(ev.User, ev.User, uid, component); - }, - Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png")), - Text = Loc.GetString("food-system-verb-eat"), - Priority = -1 - }; + TryFeed(ev.User, ev.User, uid, component); + }, + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png")), + Text = Loc.GetString("food-system-verb-eat"), + Priority = -1 + }; - ev.Verbs.Add(verb); - } + ev.Verbs.Add(verb); + } - /// - /// Returns true if the food item can be digested by the user. - /// - public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null) + /// + /// Returns true if the food item can be digested by the user. + /// + public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null) + { + if (!Resolve(food, ref foodComp, false)) + return false; + + if (!_body.TryGetBodyOrganComponents(uid, out var stomachs)) + return false; + + return IsDigestibleBy(food, foodComp, stomachs); + } + + /// + /// Returns true if has a that whitelists + /// this (or if they even have enough stomachs in the first place). + /// + private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(StomachComponent, OrganComponent)> stomachs) + { + var digestible = true; + + // Does the mob have enough stomachs? + if (stomachs.Count < component.RequiredStomachs) + return false; + + // Run through the mobs' stomachs + foreach (var (comp, _) in stomachs) { - if (!Resolve(food, ref foodComp, false)) - return false; - - if (!_bodySystem.TryGetBodyOrganComponents(uid, out var stomachs)) - return false; - - return IsDigestibleBy(food, foodComp, stomachs); - } - - /// - /// Returns true if has a that whitelists - /// this (or if they even have enough stomachs in the first place). - /// - private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<(StomachComponent, OrganComponent)> stomachs) - { - var digestible = true; - - // Does the mob have enough stomachs? - if (stomachs.Count < component.RequiredStomachs) - return false; - - // Run through the mobs' stomachs - foreach (var (comp, _) in stomachs) - { - // Find a stomach with a SpecialDigestible - if (comp.SpecialDigestible == null) - continue; - // Check if the food is in the whitelist - if (comp.SpecialDigestible.IsValid(food, EntityManager)) - return true; - // They can only eat whitelist food and the food isn't in the whitelist. It's not edible. - return false; - } - - if (component.RequiresSpecialDigestion) - return false; - - return digestible; - } - - private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component, - out List utensils, HandsComponent? hands = null) - { - utensils = new List(); - - if (component.Utensil != UtensilType.None) + // Find a stomach with a SpecialDigestible + if (comp.SpecialDigestible == null) + continue; + // Check if the food is in the whitelist + if (comp.SpecialDigestible.IsValid(food, EntityManager)) return true; + // They can only eat whitelist food and the food isn't in the whitelist. It's not edible. + return false; + } - if (!Resolve(user, ref hands, false)) + if (component.RequiresSpecialDigestion) return false; - var usedTypes = UtensilType.None; + return digestible; + } - foreach (var item in _handsSystem.EnumerateHeld(user, hands)) - { - // Is utensil? - if (!EntityManager.TryGetComponent(item, out UtensilComponent? utensil)) - continue; - - if ((utensil.Types & component.Utensil) != 0 && // Acceptable type? - (usedTypes & utensil.Types) != utensil.Types) // Type is not used already? (removes usage of identical utensils) - { - // Add to used list - usedTypes |= utensil.Types; - utensils.Add(item); - } - } - - // If "required" field is set, try to block eating without proper utensils used - if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil) - { - _popupSystem.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user); - return false; - } + private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component, + out List utensils, HandsComponent? hands = null) + { + utensils = new List(); + if (component.Utensil != UtensilType.None) return true; - } - /// - /// Block ingestion attempts based on the equipped mask or head-wear - /// - private void OnInventoryIngestAttempt(EntityUid uid, InventoryComponent component, IngestionAttemptEvent args) + if (!Resolve(user, ref hands, false)) + return false; + + var usedTypes = UtensilType.None; + + foreach (var item in _hands.EnumerateHeld(user, hands)) { - if (args.Cancelled) - return; + // Is utensil? + if (!TryComp(item, out var utensil)) + continue; - IngestionBlockerComponent? blocker; - - if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) && - EntityManager.TryGetComponent(maskUid, out blocker) && - blocker.Enabled) + if ((utensil.Types & component.Utensil) != 0 && // Acceptable type? + (usedTypes & utensil.Types) != utensil.Types) // Type is not used already? (removes usage of identical utensils) { - args.Blocker = maskUid; - args.Cancel(); - return; - } - - if (_inventorySystem.TryGetSlotEntity(uid, "head", out var headUid) && - EntityManager.TryGetComponent(headUid, out blocker) && - blocker.Enabled) - { - args.Blocker = headUid; - args.Cancel(); + // Add to used list + usedTypes |= utensil.Types; + utensils.Add(item); } } - - /// - /// Check whether the target's mouth is blocked by equipment (masks or head-wear). - /// - /// The target whose equipment is checked - /// Optional entity that will receive an informative pop-up identifying the blocking - /// piece of equipment. - /// - public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null) + // If "required" field is set, try to block eating without proper utensils used + if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil) { - var attempt = new IngestionAttemptEvent(); - RaiseLocalEvent(uid, attempt, false); - if (attempt.Cancelled && attempt.Blocker != null && popupUid != null) - { - var name = EntityManager.GetComponent(attempt.Blocker.Value).EntityName; - _popupSystem.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", name)), - uid, popupUid.Value); - } + _popup.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user); + return false; + } - return attempt.Cancelled; + return true; + } + + /// + /// Block ingestion attempts based on the equipped mask or head-wear + /// + private void OnInventoryIngestAttempt(EntityUid uid, InventoryComponent component, IngestionAttemptEvent args) + { + if (args.Cancelled) + return; + + IngestionBlockerComponent? blocker; + + if (_inventory.TryGetSlotEntity(uid, "mask", out var maskUid) && + TryComp(maskUid, out blocker) && + blocker.Enabled) + { + args.Blocker = maskUid; + args.Cancel(); + return; + } + + if (_inventory.TryGetSlotEntity(uid, "head", out var headUid) && + TryComp(headUid, out blocker) && + blocker.Enabled) + { + args.Blocker = headUid; + args.Cancel(); } } + + + /// + /// Check whether the target's mouth is blocked by equipment (masks or head-wear). + /// + /// The target whose equipment is checked + /// Optional entity that will receive an informative pop-up identifying the blocking + /// piece of equipment. + /// + public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null) + { + var attempt = new IngestionAttemptEvent(); + RaiseLocalEvent(uid, attempt, false); + if (attempt.Cancelled && attempt.Blocker != null && popupUid != null) + { + _popup.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)), + uid, popupUid.Value); + } + + return attempt.Cancelled; + } + + /// + /// Get the number of bites this food has left, based on how much food solution there is and how much of it to eat per bite. + /// + public int GetUsesRemaining(EntityUid uid, FoodComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return 0; + + if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out var solution) || solution.Volume == 0) + return 0; + + // eat all in 1 go, so non empty is 1 bite + if (comp.TransferAmount == null) + return 1; + + return Math.Max(1, (int) Math.Ceiling((solution.Volume / (FixedPoint2) comp.TransferAmount).Float())); + } } diff --git a/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs new file mode 100644 index 0000000000..dd6474bc74 --- /dev/null +++ b/Content.Server/Nutrition/EntitySystems/OpenableSystem.cs @@ -0,0 +1,140 @@ +using Content.Server.Chemistry.EntitySystems; +using Content.Server.Fluids.EntitySystems; +using Content.Server.Nutrition.Components; +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Nutrition.Components; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.GameObjects; + +namespace Content.Server.Nutrition.EntitySystems; + +/// +/// Provides API for openable food and drinks, handles opening on use and preventing transfer when closed. +/// +public sealed class OpenableSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnUse); + SubscribeLocalEvent(OnExamined, after: new[] { typeof(PuddleSystem) }); + SubscribeLocalEvent(OnTransferAttempt); + SubscribeLocalEvent(HandleIfClosed); + SubscribeLocalEvent(HandleIfClosed); + } + + private void OnInit(EntityUid uid, OpenableComponent comp, ComponentInit args) + { + UpdateAppearance(uid, comp); + } + + private void OnUse(EntityUid uid, OpenableComponent comp, UseInHandEvent args) + { + if (args.Handled || !comp.OpenableByHand) + return; + + args.Handled = TryOpen(uid, comp); + } + + private void OnExamined(EntityUid uid, OpenableComponent comp, ExaminedEvent args) + { + if (!comp.Opened || !args.IsInDetailsRange) + return; + + var text = Loc.GetString(comp.ExamineText); + args.PushMarkup(text); + } + + private void OnTransferAttempt(EntityUid uid, OpenableComponent comp, SolutionTransferAttemptEvent args) + { + if (!comp.Opened) + { + // message says its just for drinks, shouldn't matter since you typically dont have a food that is openable and can be poured out + args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", uid))); + } + } + + private void HandleIfClosed(EntityUid uid, OpenableComponent comp, HandledEntityEventArgs args) + { + // prevent spilling/pouring/whatever drinks when closed + args.Handled = !comp.Opened; + } + + /// + /// Returns true if the entity either does not have OpenableComponent or it is opened. + /// Drinks that don't have OpenableComponent are automatically open, so it returns true. + /// + public bool IsOpen(EntityUid uid, OpenableComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return true; + + return comp.Opened; + } + + /// + /// Returns true if the entity both has OpenableComponent and is not opened. + /// Drinks that don't have OpenableComponent are automatically open, so it returns false. + /// If user is not null a popup will be shown to them. + /// + public bool IsClosed(EntityUid uid, EntityUid? user = null, OpenableComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return false; + + if (comp.Opened) + return false; + + if (user != null) + _popup.PopupEntity(Loc.GetString(comp.ClosedPopup, ("owner", uid)), user.Value, user.Value); + + return true; + } + + /// + /// Update open visuals to the current value. + /// + public void UpdateAppearance(EntityUid uid, OpenableComponent? comp = null, AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref comp)) + return; + + _appearance.SetData(uid, OpenableVisuals.Opened, comp.Opened, appearance); + } + + /// + /// Sets the opened field and updates open visuals. + /// + public void SetOpen(EntityUid uid, bool opened = true, OpenableComponent? comp = null) + { + if (!Resolve(uid, ref comp) || opened == comp.Opened) + return; + + comp.Opened = opened; + + UpdateAppearance(uid, comp); + } + + /// + /// If closed, opens it and plays the sound. + /// + /// Whether it got opened + public bool TryOpen(EntityUid uid, OpenableComponent? comp = null) + { + if (!Resolve(uid, ref comp) || comp.Opened) + return false; + + SetOpen(uid, true, comp); + _audio.PlayPvs(comp.Sound, uid); + return true; + } +} diff --git a/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs b/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs index c4b248ff93..c344f1b325 100644 --- a/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs @@ -47,7 +47,7 @@ namespace Content.Server.Nutrition.EntitySystems return false; } - if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var solution)) + if (!_solutionContainerSystem.TryGetSolution(uid, food.Solution, out var solution)) { return false; } @@ -136,7 +136,7 @@ namespace Content.Server.Nutrition.EntitySystems { // Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition) if (TryComp(sliceUid, out var sliceFoodComp) && - _solutionContainerSystem.TryGetSolution(sliceUid, sliceFoodComp.SolutionName, out var itsSolution)) + _solutionContainerSystem.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSolution)) { _solutionContainerSystem.RemoveAllSolution(sliceUid, itsSolution); @@ -151,7 +151,7 @@ namespace Content.Server.Nutrition.EntitySystems var foodComp = EnsureComp(uid); EnsureComp(uid); - _solutionContainerSystem.EnsureSolution(uid, foodComp.SolutionName); + _solutionContainerSystem.EnsureSolution(uid, foodComp.Solution); } private void OnExamined(EntityUid uid, SliceableFoodComponent component, ExaminedEvent args) diff --git a/Content.Shared/Nutrition/Components/SharedFoodComponent.cs b/Content.Shared/Nutrition/Components/SharedFoodComponent.cs index 8147d4599b..99ddabd3ce 100644 --- a/Content.Shared/Nutrition/Components/SharedFoodComponent.cs +++ b/Content.Shared/Nutrition/Components/SharedFoodComponent.cs @@ -11,9 +11,9 @@ namespace Content.Shared.Nutrition.Components } [Serializable, NetSerializable] - public enum DrinkCanStateVisual : byte + public enum OpenableVisuals : byte { - Closed, - Opened + Opened, + Layer } } diff --git a/Resources/Locale/en-US/nutrition/components/drink-component.ftl b/Resources/Locale/en-US/nutrition/components/drink-component.ftl index f53a8a1e12..d3aede0739 100644 --- a/Resources/Locale/en-US/nutrition/components/drink-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/drink-component.ftl @@ -1,7 +1,6 @@ drink-component-on-use-is-empty = {$owner} is empty! -drink-component-on-examine-is-empty = Empty -drink-component-on-examine-is-opened = Opened -drink-component-on-examine-details-text = [color={$colorName}]{$text}[/color] +drink-component-on-examine-is-empty = [color=gray]Empty[/color] +drink-component-on-examine-is-opened = [color=yellow]Opened[/color] drink-component-on-examine-is-full = Full drink-component-on-examine-is-mostly-full = Mostly Full drink-component-on-examine-is-half-full = Halfway Full diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 884b4add36..09e10a688b 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -152,7 +152,6 @@ !type:SpreaderNode nodeGroupID: Spreader - type: Drink - isOpen: true delay: 3 transferAmount: 1 solution: puddle diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml index 4baaa3f3bc..7a1d9ec160 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml @@ -40,8 +40,6 @@ id: DrinkGlassBase abstract: true components: - - type: Drink - isOpen: true - type: Damageable damageContainer: Inorganic damageModifierSet: Glass diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml index 77d38ee00a..bd81756816 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml @@ -3,8 +3,8 @@ id: DrinkBottleBaseFull abstract: true components: - - type: Drink - openSounds: + - type: Openable + sound: collection: bottleOpenSounds - type: SolutionContainerManager solutions: @@ -130,8 +130,9 @@ reagents: - ReagentId: Grenadine Quantity: 100 - - type: Drink - isOpen: true + # intended use is to spike drinks so starts open + - type: Openable + opened: true - type: Sprite sprite: Objects/Consumable/Drinks/grenadinebottle.rsi diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml index 6cd2877bf9..a68aba3d6e 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cans.yml @@ -4,9 +4,8 @@ abstract: true components: - type: Drink - openSounds: - collection: canOpenSounds - pressurized: true + - type: Openable + - type: PressurizedDrink - type: SolutionContainerManager solutions: drink: @@ -25,7 +24,7 @@ state: icon layers: - state: icon - map: ["drinkCanIcon"] + map: ["enum.OpenableVisuals.Layer"] - type: FitsInDispenser solution: drink - type: DrawableSolution @@ -37,8 +36,8 @@ - type: Appearance - type: GenericVisualizer visuals: - enum.DrinkCanStateVisual.Opened: - drinkCanIcon: + enum.OpenableVisuals.Opened: + enum.OpenableVisuals.Layer: True: {state: "icon_open"} False: {state: "icon"} - type: Spillable diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml index fa91eb4fd4..f9e41054f8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_cups.yml @@ -26,7 +26,6 @@ - key: enum.TransferAmountUiKey.Key type: TransferAmountBoundUserInterface - type: Drink - isOpen: true - type: Sprite state: icon - type: Spillable diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml index aa29a09f74..5e8f0a729c 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_fun.yml @@ -4,9 +4,8 @@ name: space glue tube description: High performance glue intended for maintenance of extremely complex mechanical equipment. DON'T DRINK! components: - - type: Drink - isOpen: false - openSounds: + - type: Openable + sound: collection: packetOpenSounds - type: Sprite sprite: Objects/Consumable/Drinks/glue-tube.rsi @@ -40,9 +39,8 @@ name: space lube tube description: High performance lubricant intended for maintenance of extremely complex mechanical equipment. components: - - type: Drink - isOpen: false - openSounds: + - type: Openable + sound: collection: packetOpenSounds - type: Sprite sprite: Objects/Consumable/Drinks/lube-tube.rsi @@ -65,4 +63,4 @@ - type: SolutionContainerVisuals maxFillLevels: 6 fillBaseName: fill - - type: Lube \ No newline at end of file + - type: Lube diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml index 684681074e..48f612702b 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml @@ -9,7 +9,6 @@ drink: maxVol: 100 - type: Drink - isOpen: true - type: FitsInDispenser solution: drink - type: DrawableSolution diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml index a1a1eef3f3..ec0edc145f 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml @@ -16,7 +16,6 @@ canChangeTransferAmount: true maxTransferAmount: 5 - type: Drink - isOpen: true - type: MeleeWeapon soundNoDamage: path: "/Audio/Effects/Fluids/splat.ogg" diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/bowl.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/bowl.yml index 0fcd87be5a..4384542bb0 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/bowl.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/bowl.yml @@ -23,7 +23,6 @@ Blunt: 5 - type: Drink solution: food - isOpen: true - type: DrawableSolution solution: food - type: Damageable diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml index 2762d3dacb..03118bbf28 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml @@ -31,7 +31,8 @@ - type: Drink solution: food refillable: false - openSounds: + - type: Openable + sound: collection: packetOpenSounds # Since this one is closed, the only way to insert liquid is with a syringe. - type: InjectableSolution @@ -372,7 +373,8 @@ components: - type: Drink solution: food - openSounds: + - type: Openable + sound: collection: pop - type: SolutionContainerManager solutions: @@ -552,7 +554,6 @@ components: - type: Drink solution: food - isOpen: true - type: SolutionContainerManager solutions: food: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml index 22d5fbf14d..9c9890084c 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/ingredients.yml @@ -24,10 +24,11 @@ solution: food - type: Drink solution: food - openSounds: - collection: packetOpenSounds useSound: path: /Audio/Items/eating_1.ogg + - type: Openable + sound: + collection: packetOpenSounds - type: Spillable solution: food - type: MeleeWeapon diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 6d7d7a52e7..6621049c68 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -1024,9 +1024,8 @@ name: crazy glue description: A bottle of crazy glue manufactured by Honk! Co. components: - - type: Drink - isOpen: false - openSounds: + - type: Openable + sound: collection: packetOpenSounds - type: Sprite sprite: Objects/Fun/glue.rsi diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml index 37b302e177..13bd6d1665 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml @@ -160,7 +160,6 @@ - MobLayer - type: Pullable - type: Drink - isOpen: true solution: bucket - type: Appearance - type: SolutionContainerVisuals @@ -414,7 +413,6 @@ - key: enum.StorageUiKey.Key type: StorageBoundUserInterface - type: Drink - isOpen: true solution: bucket - type: ContainerContainer containers: diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml b/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml index 61cfbadbdc..0e679c7096 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml @@ -36,7 +36,6 @@ - key: enum.TransferAmountUiKey.Key type: TransferAmountBoundUserInterface - type: Drink - isOpen: true solution: beaker - type: Spillable solution: beaker diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml index 2fe701c010..da1a15e238 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry-bottles.yml @@ -25,7 +25,6 @@ maxFillLevels: 6 fillBaseName: bottle-1- - type: Drink - isOpen: true - type: SolutionContainerManager solutions: drink: # This solution name and target volume is hard-coded in ChemMasterComponent diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index 456bf8a9eb..a0e57d520f 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -45,7 +45,6 @@ - key: enum.TransferAmountUiKey.Key type: TransferAmountBoundUserInterface - type: Drink - isOpen: true solution: beaker - type: Appearance - type: SolutionContainerVisuals @@ -136,7 +135,6 @@ - key: enum.TransferAmountUiKey.Key type: TransferAmountBoundUserInterface - type: Drink - isOpen: true solution: beaker - type: Appearance - type: SolutionContainerVisuals diff --git a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml index 242c59172c..17aadff8a9 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml @@ -5,8 +5,8 @@ description: It's a boring old bucket. components: - type: Drink - isOpen: true solution: bucket + ignoreEmpty: true - type: Clickable - type: Sprite sprite: Objects/Tools/bucket.rsi