diff --git a/Content.Server/Nutrition/Components/DrinkComponent.cs b/Content.Server/Nutrition/Components/DrinkComponent.cs index 54d9393cac..2dfc6c495d 100644 --- a/Content.Server/Nutrition/Components/DrinkComponent.cs +++ b/Content.Server/Nutrition/Components/DrinkComponent.cs @@ -42,5 +42,16 @@ namespace Content.Server.Nutrition.Components [DataField("burstSound")] public SoundSpecifier BurstSound = new SoundPathSpecifier("/Audio/Effects/flash_bang.ogg"); + + /// + /// This is how many seconds it takes to force feed someone this drink. + /// + [DataField("forceFeedDelay")] + public float ForceFeedDelay = 3; + + /// + /// If true, this drink has some DoAfter active (someone is being force fed). + /// + public bool InUse = false; } } diff --git a/Content.Server/Nutrition/Components/FoodComponent.cs b/Content.Server/Nutrition/Components/FoodComponent.cs index 073fd74c3d..b567e5cdd0 100644 --- a/Content.Server/Nutrition/Components/FoodComponent.cs +++ b/Content.Server/Nutrition/Components/FoodComponent.cs @@ -44,10 +44,21 @@ namespace Content.Server.Nutrition.Components [DataField("utensilRequired")] public bool UtensilRequired = false; - [DataField("eatMessage")] public string EatMessage = "food-nom"; + /// + /// 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; + + /// + /// If true, this food has some DoAfter active (someone is being force fed). + /// + public bool InUse = false; + [ViewVariables] public int UsesRemaining { diff --git a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs index ad6ae769d2..5a02820804 100644 --- a/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/DrinkSystem.cs @@ -1,14 +1,18 @@ using System.Linq; -using Content.Server.Body.Behavior; using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Chemistry.EntitySystems; +using Content.Server.DoAfter; using Content.Server.Fluids.Components; using Content.Server.Nutrition.Components; using Content.Server.Popups; +using Content.Shared.ActionBlocker; +using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; +using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.Interaction; @@ -34,6 +38,9 @@ namespace Content.Server.Nutrition.EntitySystems [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly BodySystem _bodySystem = default!; [Dependency] private readonly StomachSystem _stomachSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedAdminLogSystem _logSystem = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; public override void Initialize() { @@ -45,6 +52,8 @@ namespace Content.Server.Nutrition.EntitySystems SubscribeLocalEvent(OnUse); SubscribeLocalEvent(AfterInteract); SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnForceDrink); + SubscribeLocalEvent(OnForceDrinkCancelled); } public bool IsEmpty(EntityUid uid, DrinkComponent? component = null) @@ -103,19 +112,49 @@ namespace Content.Server.Nutrition.EntitySystems private void AfterInteract(EntityUid uid, DrinkComponent component, AfterInteractEvent args) { - if (args.Handled) + if (args.Handled || args.TargetUid == null) return; - if (args.Target == null) + if (!_actionBlockerSystem.CanInteract(args.UserUid) || !_actionBlockerSystem.CanUse(args.UserUid)) return; - if (TryUseDrink(uid, args.User.Uid, args.Target.Uid, true, component)) + if (!args.UserUid.InRangeUnobstructed(uid, popup: true)) + { args.Handled = true; + return; + } + + if (args.UserUid == args.TargetUid) + { + args.Handled = TryUseDrink(uid, args.UserUid); + return; + } + + if (!args.UserUid.InRangeUnobstructed(args.TargetUid.Value, popup: true)) + { + args.Handled = true; + return; + } + + if (args.User == args.Target) + args.Handled = TryUseDrink(uid, args.UserUid, component); + else + args.Handled = TryForceDrink(uid, args.UserUid, args.TargetUid.Value, component); } private void OnUse(EntityUid uid, DrinkComponent component, UseInHandEvent args) { if (args.Handled) return; + + if (!_actionBlockerSystem.CanInteract(args.UserUid) || !_actionBlockerSystem.CanUse(args.UserUid)) + return; + + if (!args.UserUid.InRangeUnobstructed(uid, popup: true)) + { + args.Handled = true; + return; + } + if (!component.Opened) { //Do the opening stuff like playing the sounds. @@ -131,8 +170,7 @@ namespace Content.Server.Nutrition.EntitySystems return; } - if (TryUseDrink(uid, args.User.Uid, args.User.Uid, false, component)) - args.Handled = true; + args.Handled = TryUseDrink(uid, args.UserUid, component); } private void HandleLand(EntityUid uid, DrinkComponent component, LandEvent args) @@ -189,68 +227,208 @@ namespace Content.Server.Nutrition.EntitySystems appearance.SetData(DrinkCanStateVisual.Opened, component.Opened); } - private bool TryUseDrink(EntityUid uid, EntityUid userUid, EntityUid targetUid, bool forced, DrinkComponent? component = null) + /// + /// Attempt to drink some of a drink. Returns true if any interaction took place, including generation of + /// pop-up messages. + /// + private bool TryUseDrink(EntityUid uid, EntityUid userUid, DrinkComponent? drink = null) { - if(!Resolve(uid, ref component)) + if (!Resolve(uid, ref drink)) return false; - var owner = component.Owner; - - if (!component.Opened) + if (!drink.Opened) { - _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", owner)), targetUid, Filter.Entities(userUid)); - return false; + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open", + ("owner", drink.Owner.Name)), uid, Filter.Entities(userUid)); + return true; } - if (!_solutionContainerSystem.TryGetDrainableSolution(component.OwnerUid, out var interactions) || - interactions.DrainAvailable <= 0) - { - if (!forced) - { - _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", owner)), targetUid, Filter.Entities(userUid)); - } - + if (!EntityManager.TryGetComponent(userUid, out SharedBodyComponent? body)) return false; + + if (!_solutionContainerSystem.TryGetDrainableSolution(drink.OwnerUid, out var drinkSolution) || + drinkSolution.DrainAvailable <= 0) + { + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", + ("entity", drink.Owner.Name)), uid, Filter.Entities(userUid)); + return true; } - if (!EntityManager.TryGetComponent(targetUid, out SharedBodyComponent? body) || - !_bodySystem.TryGetComponentsOnMechanisms(targetUid, out var stomachs, body)) + if (!_bodySystem.TryGetComponentsOnMechanisms(userUid, out var stomachs, body)) { - _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink", ("owner", owner)), targetUid, Filter.Entities(targetUid)); - return false; + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink"), + userUid, Filter.Entities(userUid)); + return true; } - if (userUid != targetUid && !userUid.InRangeUnobstructed(targetUid, popup: true)) - return false; - - var transferAmount = FixedPoint2.Min(component.TransferAmount, interactions.DrainAvailable); - var drain = _solutionContainerSystem.Drain(owner.Uid, interactions, transferAmount); - var firstStomach = stomachs.FirstOrDefault(stomach => _stomachSystem.CanTransferSolution(stomach.OwnerUid, drain)); + var transferAmount = FixedPoint2.Min(drink.TransferAmount, drinkSolution.DrainAvailable); + var drain = _solutionContainerSystem.Drain(uid, drinkSolution, transferAmount); + var firstStomach = stomachs.FirstOrDefault( + stomach => _stomachSystem.CanTransferSolution(stomach.OwnerUid, drain)); // All stomach are full or can't handle whatever solution we have. if (firstStomach == null) { - _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough", ("owner", owner)), targetUid, Filter.Entities(targetUid)); + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"), + userUid, Filter.Entities(userUid)); if (EntityManager.HasComponent(uid)) { - drain.SpillAt(targetUid, "PuddleSmear"); - return false; + drain.SpillAt(userUid, "PuddleSmear"); + return true; } - _solutionContainerSystem.Refill(owner.Uid, interactions, drain); - return false; + _solutionContainerSystem.Refill(uid, drinkSolution, drain); + return true; } - SoundSystem.Play(Filter.Pvs(targetUid), component.UseSound.GetSound(), targetUid, AudioParams.Default.WithVolume(-2f)); + SoundSystem.Play(Filter.Pvs(userUid), drink.UseSound.GetSound(), userUid, + AudioParams.Default.WithVolume(-2f)); - _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-success-slurp"), targetUid, Filter.Pvs(targetUid)); + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-success-slurp"), userUid, + Filter.Pvs(userUid)); - // TODO: Account for partial transfer. - drain.DoEntityReaction(targetUid, ReactionMethod.Ingestion); + drain.DoEntityReaction(userUid, ReactionMethod.Ingestion); _stomachSystem.TryTransferSolution(firstStomach.OwnerUid, drain, firstStomach); return true; } + + /// + /// Attempt to force someone else to drink some of a drink. Returns true if any interaction took place, + /// including generation of pop-up messages. + /// + private bool TryForceDrink(EntityUid uid, EntityUid userUid, EntityUid targetUid, + DrinkComponent? drink = null) + { + if (!Resolve(uid, ref drink)) + return false; + + // cannot stack do-afters + if (drink.InUse) + return false; + + if (!EntityManager.HasComponent(targetUid)) + return false; + + if (!drink.Opened) + { + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open", + ("owner", drink.Owner.Name)), uid, Filter.Entities(userUid)); + return true; + } + + if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var drinkSolution) || + drinkSolution.DrainAvailable <= 0) + { + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty", + ("entity", drink.Owner.Name)), uid, Filter.Entities(userUid)); + return true; + } + + EntityManager.TryGetComponent(userUid, out MetaDataComponent? meta); + var userName = meta?.EntityName ?? string.Empty; + + _popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), + userUid, Filter.Entities(targetUid)); + + _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, target: targetUid) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = true, + MovementThreshold = 1.0f, + TargetFinishedEvent = new ForceDrinkEvent(userUid, drink, drinkSolution), + BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink) + }); + + // logging + var user = EntityManager.GetEntity(userUid); + var target = EntityManager.GetEntity(targetUid); + var drinkable = EntityManager.GetEntity(uid); + _logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{user} is forcing {target} to drink {drinkable}"); + + drink.InUse = true; + return true; + } + + /// + /// Raised directed at a victim when someone has force fed them a drink. + /// + private void OnForceDrink(EntityUid uid, SharedBodyComponent body, ForceDrinkEvent args) + { + args.Drink.InUse = false; + var transferAmount = FixedPoint2.Min(args.Drink.TransferAmount, args.DrinkSolution.DrainAvailable); + var drained = _solutionContainerSystem.Drain(args.Drink.OwnerUid, args.DrinkSolution, transferAmount); + + if (!_bodySystem.TryGetComponentsOnMechanisms(uid, out var stomachs, body)) + { + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink-other"), + uid, Filter.Entities(args.User)); + + drained.SpillAt(uid, "PuddleSmear"); + return; + } + + var firstStomach = stomachs.FirstOrDefault( + stomach => _stomachSystem.CanTransferSolution(stomach.OwnerUid, drained)); + + // All stomach are full or can't handle whatever solution we have. + if (firstStomach == null) + { + _popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), + uid, Filter.Entities(args.User)); + + drained.SpillAt(uid, "PuddleSmear"); + return; + } + + EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta); + var targetName = targetMeta?.EntityName ?? string.Empty; + + EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta); + var userName = userMeta?.EntityName ?? string.Empty; + + _popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed-success", ("user", userName)), + uid, Filter.Entities(uid)); + + _popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)), + args.User, Filter.Entities(args.User)); + + SoundSystem.Play(Filter.Pvs(uid), args.Drink.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-2f)); + + drained.DoEntityReaction(uid, ReactionMethod.Ingestion); + _stomachSystem.TryTransferSolution(firstStomach.OwnerUid, drained, firstStomach); + } + + private void OnForceDrinkCancelled(ForceDrinkCancelledEvent args) + { + args.Drink.InUse = false; + } + } + + public sealed class ForceDrinkEvent : EntityEventArgs + { + public readonly EntityUid User; + public readonly DrinkComponent Drink; + public readonly Solution DrinkSolution; + + public ForceDrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution) + { + User = user; + Drink = drink; + DrinkSolution = drinkSolution; + } + } + + public sealed class ForceDrinkCancelledEvent : EntityEventArgs + { + public readonly DrinkComponent Drink; + + public ForceDrinkCancelledEvent( DrinkComponent drink) + { + Drink = drink; + } } } diff --git a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs index d8201824a6..5ec6e00f49 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSystem.cs @@ -1,12 +1,17 @@ using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.EntitySystems; +using Content.Server.DoAfter; using Content.Server.Hands.Components; using Content.Server.Items; using Content.Server.Nutrition.Components; using Content.Server.Popups; +using Content.Shared.ActionBlocker; +using Content.Shared.Administration.Logs; using Content.Shared.Body.Components; +using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Interaction; using Content.Shared.Interaction.Helpers; @@ -15,7 +20,6 @@ using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; -using Robust.Shared.Log; using Robust.Shared.Player; using System.Collections.Generic; using System.Linq; @@ -32,6 +36,9 @@ namespace Content.Server.Nutrition.EntitySystems [Dependency] private readonly StomachSystem _stomachSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly UtensilSystem _utensilSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedAdminLogSystem _logSystem = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; public override void Initialize() { @@ -40,6 +47,8 @@ namespace Content.Server.Nutrition.EntitySystems SubscribeLocalEvent(OnUseFoodInHand); SubscribeLocalEvent(OnFeedFood); SubscribeLocalEvent(AddEatVerb); + SubscribeLocalEvent(OnForceFeed); + SubscribeLocalEvent(OnForceFeedCancelled); } /// @@ -50,30 +59,58 @@ namespace Content.Server.Nutrition.EntitySystems if (ev.Handled) return; - if (TryUseFood(uid, ev.UserUid, ev.UserUid)) + if (!_actionBlockerSystem.CanInteract(ev.UserUid) || !_actionBlockerSystem.CanUse(ev.UserUid)) + return; + + if (!ev.UserUid.InRangeUnobstructed(uid, popup: true)) + { ev.Handled = true; + return; + } + + ev.Handled = TryUseFood(uid, ev.UserUid); } /// /// Feed someone else /// - private void OnFeedFood(EntityUid uid, FoodComponent foodComponent, AfterInteractEvent ev) + private void OnFeedFood(EntityUid uid, FoodComponent foodComponent, AfterInteractEvent args) { - if (ev.Handled || ev.Target == null) + if (args.Handled || args.TargetUid == null) return; - if (TryUseFood(uid, ev.UserUid, ev.Target.Uid)) - ev.Handled = true; + if (!_actionBlockerSystem.CanInteract(args.UserUid) || !_actionBlockerSystem.CanUse(args.UserUid)) + return; + + if (!args.UserUid.InRangeUnobstructed(uid, popup: true)) + { + args.Handled = true; + return; + } + + if (args.UserUid == args.TargetUid) + { + args.Handled = TryUseFood(uid, args.UserUid); + return; + } + + if (!args.UserUid.InRangeUnobstructed(args.TargetUid.Value, popup: true)) + { + args.Handled = true; + return; + } + + args.Handled = TryForceFeed(uid, args.UserUid, args.TargetUid.Value); } /// - /// Tries to feed specified target + /// Tries to eat some food /// /// Food entity. /// Feeding initiator. /// Feeding target. - /// True if the portion of food was consumed - public bool TryUseFood(EntityUid uid, EntityUid userUid, EntityUid targetUid, FoodComponent? component = null) + /// True if an interaction occurred (i.e., food was consumed, or a pop-up message was created) + public bool TryUseFood(EntityUid uid, EntityUid userUid, FoodComponent? component = null) { if (!Resolve(uid, ref component)) return false; @@ -84,51 +121,21 @@ namespace Content.Server.Nutrition.EntitySystems if (component.UsesRemaining <= 0) { _popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", EntityManager.GetEntity(uid))), userUid, Filter.Entities(userUid)); - DeleteAndSpawnTrash(userUid, component); - return false; + DeleteAndSpawnTrash(component, userUid); + return true; } - if (!EntityManager.TryGetComponent(targetUid, out SharedBodyComponent ? body) || - !_bodySystem.TryGetComponentsOnMechanisms(targetUid, out var stomachs, body)) + if (!EntityManager.TryGetComponent(userUid, out SharedBodyComponent ? body) || + !_bodySystem.TryGetComponentsOnMechanisms(userUid, out var stomachs, body)) return false; var usedUtensils = new List(); - //Not blocking eating itself if "required" filed is not set (allows usage of multiple types of utensils) - //TODO: maybe a chance to spill soup on eating without spoon?! - if (component.Utensil != UtensilType.None) - { - if (EntityManager.TryGetComponent(userUid, out HandsComponent? hands)) - { - var usedTypes = UtensilType.None; + if (!TryGetRequiredUtensils(userUid, component, out var utensils)) + return true; - foreach (var item in hands.GetAllHeldItems()) - { - // Is utensil? - if (!item.Owner.TryGetComponent(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; - usedUtensils.Add(utensil); - } - } - - // 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)), userUid, Filter.Entities(userUid)); - return false; - } - } - } - - if (!userUid.InRangeUnobstructed(uid, popup: true) || - userUid != targetUid && !userUid.InRangeUnobstructed(targetUid, popup: true)) - return false; + if (!userUid.InRangeUnobstructed(uid, popup: true)) + return true; var transferAmount = component.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) component.TransferAmount, solution.CurrentVolume) : solution.CurrentVolume; var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount); @@ -137,16 +144,16 @@ namespace Content.Server.Nutrition.EntitySystems if (firstStomach == null) { _solutionContainerSystem.TryAddSolution(uid, solution, split); - _popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), targetUid, Filter.Entities(targetUid)); - return false; + _popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), userUid, Filter.Entities(userUid)); + return true; } // TODO: Account for partial transfer. - split.DoEntityReaction(targetUid, ReactionMethod.Ingestion); + split.DoEntityReaction(userUid, ReactionMethod.Ingestion); _stomachSystem.TryTransferSolution(firstStomach.OwnerUid, split, firstStomach); - SoundSystem.Play(Filter.Pvs(targetUid), component.UseSound.GetSound(), targetUid, AudioParams.Default.WithVolume(-1f)); - _popupSystem.PopupEntity(Loc.GetString(component.EatMessage, ("food", component.Owner)), targetUid, Filter.Entities(targetUid)); + SoundSystem.Play(Filter.Pvs(userUid), component.UseSound.GetSound(), userUid, AudioParams.Default.WithVolume(-1f)); + _popupSystem.PopupEntity(Loc.GetString(component.EatMessage, ("food", component.Owner)), userUid, Filter.Entities(userUid)); // Try to break all used utensils foreach (var utensil in usedUtensils) @@ -160,27 +167,25 @@ namespace Content.Server.Nutrition.EntitySystems } if (string.IsNullOrEmpty(component.TrashPrototype)) - { - component.Owner.QueueDelete(); - return true; - } - - DeleteAndSpawnTrash(userUid, component); + EntityManager.QueueDeleteEntity(component.OwnerUid); + else + DeleteAndSpawnTrash(component, userUid); return true; } - private void DeleteAndSpawnTrash(EntityUid userUid, FoodComponent component) + private void DeleteAndSpawnTrash(FoodComponent component, EntityUid? userUid = null) { //We're empty. Become trash. var position = component.Owner.Transform.Coordinates; var finisher = component.Owner.EntityManager.SpawnEntity(component.TrashPrototype, position); // If the user is holding the item - if (EntityManager.TryGetComponent(userUid, out HandsComponent? handsComponent) && + if (userUid != null && + EntityManager.TryGetComponent(userUid.Value, out HandsComponent? handsComponent) && handsComponent.IsHolding(component.Owner)) { - component.Owner.Delete(); + EntityManager.DeleteEntity(component.OwnerUid); // Put the trash in the user's hand if (finisher.TryGetComponent(out ItemComponent? item) && @@ -188,15 +193,12 @@ namespace Content.Server.Nutrition.EntitySystems { handsComponent.PutInHand(item); } + return; } - else - { - component.Owner.Delete(); - } + + EntityManager.QueueDeleteEntity(component.OwnerUid); } - //No hands - //TODO: DoAfter based on delay after food & drinks delay PR merged... private void AddEatVerb(EntityUid uid, FoodComponent component, GetInteractionVerbsEvent ev) { if (!ev.CanInteract || @@ -208,12 +210,228 @@ namespace Content.Server.Nutrition.EntitySystems Verb verb = new(); verb.Act = () => { - TryUseFood(uid, ev.User.Uid, ev.User.Uid, component); + TryUseFood(uid, ev.User.Uid, component); }; verb.Text = Loc.GetString("food-system-verb-eat"); verb.Priority = -1; ev.Verbs.Add(verb); } + + + /// + /// Attempts to force feed a target. Returns true if any interaction occurred, including pop-up generation + /// + public bool TryForceFeed(EntityUid uid, EntityUid userUid, EntityUid targetUid, FoodComponent? food = null) + { + if (!Resolve(uid, ref food)) + return false; + + if (!EntityManager.HasComponent(targetUid)) + return false; + + if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution)) + return false; + + if (food.UsesRemaining <= 0) + { + _popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", + ("entity", EntityManager.GetEntity(uid))), userUid, Filter.Entities(userUid)); + DeleteAndSpawnTrash(food, userUid); + return true; + } + + if (!TryGetRequiredUtensils(userUid, food, out var utensils)) + return true; + + EntityManager.TryGetComponent(userUid, out MetaDataComponent? meta); + var userName = meta?.EntityName ?? string.Empty; + + _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)), + userUid, Filter.Entities(targetUid)); + + _doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, food.ForceFeedDelay, target: targetUid) + { + BreakOnUserMove = true, + BreakOnDamage = true, + BreakOnStun = true, + BreakOnTargetMove = true, + MovementThreshold = 1.0f, + TargetFinishedEvent = new ForceFeedEvent(userUid, food, foodSolution, utensils), + BroadcastCancelledEvent = new ForceFeedCancelledEvent(food) + }); + + // logging + var user = EntityManager.GetEntity(userUid); + var target = EntityManager.GetEntity(targetUid); + var edible = EntityManager.GetEntity(uid); + _logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{user} is forcing {target} to eat {edible}"); + + food.InUse = true; + return true; + } + + private void OnForceFeed(EntityUid uid, SharedBodyComponent body, ForceFeedEvent args) + { + args.Food.InUse = false; + + if (!_bodySystem.TryGetComponentsOnMechanisms(uid, out var stomachs, body)) + return; + + var transferAmount = args.Food.TransferAmount != null + ? FixedPoint2.Min((FixedPoint2) args.Food.TransferAmount, args.FoodSolution.CurrentVolume) + : args.FoodSolution.CurrentVolume; + + var split = _solutionContainerSystem.SplitSolution(args.Food.OwnerUid, args.FoodSolution, transferAmount); + var firstStomach = stomachs.FirstOrDefault(stomach => _stomachSystem.CanTransferSolution(stomach.OwnerUid, split)); + + if (firstStomach == null) + { + _solutionContainerSystem.TryAddSolution(uid, args.FoodSolution, split); + _popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more-other"), uid, Filter.Entities(args.User)); + return; + } + + split.DoEntityReaction(uid, ReactionMethod.Ingestion); + _stomachSystem.TryTransferSolution(firstStomach.OwnerUid, split, firstStomach); + + EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta); + var targetName = targetMeta?.EntityName ?? string.Empty; + + EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta); + var userName = userMeta?.EntityName ?? string.Empty; + + _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName)), + uid, Filter.Entities(uid)); + + _popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), + args.User, Filter.Entities(args.User)); + + SoundSystem.Play(Filter.Pvs(uid), args.Food.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-1f)); + + // Try to break all used utensils + foreach (var utensil in args.Utensils) + { + _utensilSystem.TryBreak(utensil.OwnerUid, args.User); + } + + if (args.Food.UsesRemaining > 0) + return; + + if (string.IsNullOrEmpty(args.Food.TrashPrototype)) + EntityManager.QueueDeleteEntity(args.Food.OwnerUid); + else + DeleteAndSpawnTrash(args.Food, args.User); + } + + /// + /// Force feeds someone remotely. Does not require utensils (well, not the normal type anyways). + /// + public void ProjectileForceFeed(EntityUid uid, EntityUid target, EntityUid? user, FoodComponent? food = null, BodyComponent? body = null) + { + if (!Resolve(uid, ref food) || !Resolve(target, ref body, false)) + return; + + if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution)) + return; + + if (!_bodySystem.TryGetComponentsOnMechanisms(target, out var stomachs, body)) + return; + + if (food.UsesRemaining <= 0) + DeleteAndSpawnTrash(food); + + var firstStomach = stomachs.FirstOrDefault(stomach => _stomachSystem.CanTransferSolution(stomach.OwnerUid, foodSolution)); + if (firstStomach == null) + return; + + // logging + var userEntity = (user == null) ? null : EntityManager.GetEntity(user.Value); + var targetEntity = EntityManager.GetEntity(target); + var edible = EntityManager.GetEntity(uid); + if (userEntity == null) + _logSystem.Add(LogType.ForceFeed, $"{edible} was thrown into the mouth of {targetEntity}"); + else + _logSystem.Add(LogType.ForceFeed, $"{userEntity} threw {edible} into the mouth of {targetEntity}"); + _popupSystem.PopupEntity(Loc.GetString(food.EatMessage), target, Filter.Entities(target)); + + foodSolution.DoEntityReaction(uid, ReactionMethod.Ingestion); + _stomachSystem.TryTransferSolution(firstStomach.OwnerUid, foodSolution, firstStomach); + SoundSystem.Play(Filter.Pvs(target), food.UseSound.GetSound(), target, AudioParams.Default.WithVolume(-1f)); + + if (string.IsNullOrEmpty(food.TrashPrototype)) + EntityManager.QueueDeleteEntity(food.OwnerUid); + else + DeleteAndSpawnTrash(food); + } + + private bool TryGetRequiredUtensils(EntityUid userUid, FoodComponent component, + out List utensils, HandsComponent? hands = null) + { + utensils = new(); + + if (component.Utensil != UtensilType.None) + return true; + + if (!Resolve(userUid, ref hands, false)) + return false; + + var usedTypes = UtensilType.None; + + foreach (var item in hands.GetAllHeldItems()) + { + // Is utensil? + if (!item.Owner.TryGetComponent(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(utensil); + } + } + + // 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)), userUid, Filter.Entities(userUid)); + return false; + } + + return true; + } + + private void OnForceFeedCancelled(ForceFeedCancelledEvent args) + { + args.Food.InUse = false; + } + } + + public sealed class ForceFeedEvent : EntityEventArgs + { + public readonly EntityUid User; + public readonly FoodComponent Food; + public readonly Solution FoodSolution; + public readonly List Utensils; + + public ForceFeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List utensils) + { + User = user; + Food = food; + FoodSolution = foodSolution; + Utensils = utensils; + } + } + + public sealed class ForceFeedCancelledEvent : EntityEventArgs + { + public readonly FoodComponent Food; + + public ForceFeedCancelledEvent(FoodComponent food) + { + Food = food; + } } } diff --git a/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs b/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs index afa1a370d6..9c07118667 100644 --- a/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/ForcefeedOnCollideSystem.cs @@ -19,13 +19,7 @@ namespace Content.Server.Nutrition.EntitySystems private void OnThrowDoHit(EntityUid uid, ForcefeedOnCollideComponent component, ThrowDoHitEvent args) { - if (!args.Target.HasComponent()) - return; - if (!EntityManager.TryGetComponent(uid, out var food)) - return; - - // the 'target' isnt really the 'user' per se.. but.. - _foodSystem.TryUseFood(food.OwnerUid, args.Target.Uid, args.Target.Uid); + _foodSystem.ProjectileForceFeed(uid, args.Target.Uid, args.User?.Uid); } private void OnLand(EntityUid uid, ForcefeedOnCollideComponent component, LandEvent args) diff --git a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs index 8d103dde61..a129363b00 100644 --- a/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/UtensilSystem.cs @@ -54,7 +54,7 @@ namespace Content.Server.Nutrition.EntitySystems if (!userUid.InRangeUnobstructed(targetUid, popup: true)) return false; - return _foodSystem.TryUseFood(targetUid, userUid, userUid); + return _foodSystem.TryUseFood(targetUid, userUid); } /// diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index a6359999c5..e6a5fe9bed 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -40,6 +40,7 @@ public enum LogType Pickup = 36, Drop = 37, BulletHit = 38, + ForceFeed = 40, MeleeHit = 41, HitScanHit = 42, Suicide = 43, diff --git a/Resources/Locale/en-US/nutrition/components/drink-component.ftl b/Resources/Locale/en-US/nutrition/components/drink-component.ftl index 97649a551b..6205122286 100644 --- a/Resources/Locale/en-US/nutrition/components/drink-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/drink-component.ftl @@ -4,6 +4,11 @@ drink-component-on-examine-is-opened = Opened drink-component-on-examine-details-text = [color={$colorName}]{$text}[/color] drink-component-try-use-drink-not-open = Open {$owner} first! drink-component-try-use-drink-is-empty = {$entity} is empty! -drink-component-try-use-drink-cannot-drink = You can't drink {$owner}! -drink-component-try-use-drink-had-enough = You've had enough {$owner}! -drink-component-try-use-drink-success-slurp = Slurp \ No newline at end of file +drink-component-try-use-drink-cannot-drink = You can't drink anything! +drink-component-try-use-drink-had-enough = You can't drink more! +drink-component-try-use-drink-cannot-drink-other = They can't drink anything! +drink-component-try-use-drink-had-enough-other = They can't drink more! +drink-component-try-use-drink-success-slurp = Slurp +drink-component-force-feed = {$user} is trying to make you drink something! +drink-component-force-feed-success = {$user} forced you to drink something! +drink-component-force-feed-success-user = You successfully feed {$target} \ No newline at end of file diff --git a/Resources/Locale/en-US/nutrition/components/food-component.ftl b/Resources/Locale/en-US/nutrition/components/food-component.ftl index 658654b7dc..cd56a690e3 100644 --- a/Resources/Locale/en-US/nutrition/components/food-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/food-component.ftl @@ -1,9 +1,6 @@ ### Interaction Messages -# When trying to eat food without the required utensil -food-you-need-utensil = You need to use a {$utensil} to eat that! - # When trying to eat food without the required utensil... but you gotta hold it food-you-need-to-hold-utensil = You need to be holding a {$utensil} to eat that! @@ -13,7 +10,14 @@ food-swallow = You swallow the {$food}. ## System food-system-you-cannot-eat-any-more = You can't eat any more! +food-system-you-cannot-eat-any-more-other = They can't eat any more! food-system-try-use-food-is-empty = {$entity} is empty! food-system-wrong-utensil = you can't eat {$food} with a {$utensil}. food-system-verb-eat = Eat + +## Force feeding + +food-system-force-feed = {$user} is trying feed you something! +food-system-force-feed-success = {$user} forced you to eat something! +food-system-force-feed-success-user = You successfully feed {$target} diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml index 8c2af761da..b97db0c1ea 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/chemistry.yml @@ -237,6 +237,7 @@ tags: - Pill - type: Food + forceFeedDelay: 1 transferAmount: null eatMessage: food-swallow useSound: /Audio/Items/pill.ogg