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