2021-11-21 10:35:09 +03:00
using Content.Server.Body.Components ;
using Content.Server.Body.Systems ;
2024-08-02 00:30:45 -07:00
using Content.Shared.Chemistry.EntitySystems ;
2023-09-24 21:49:58 +01:00
using Content.Server.Inventory ;
2021-11-21 10:35:09 +03:00
using Content.Server.Nutrition.Components ;
2024-02-01 13:33:57 +00:00
using Content.Shared.Nutrition.Components ;
2021-11-21 10:35:09 +03:00
using Content.Server.Popups ;
2023-05-06 10:23:05 +03:00
using Content.Server.Stack ;
2021-11-29 16:27:15 +13:00
using Content.Shared.Administration.Logs ;
2021-11-21 10:35:09 +03:00
using Content.Shared.Body.Components ;
2023-05-03 19:49:25 -07:00
using Content.Shared.Body.Organ ;
2023-02-24 19:01:25 -05:00
using Content.Shared.Chemistry ;
2021-11-29 16:27:15 +13:00
using Content.Shared.Database ;
2023-02-24 19:01:25 -05:00
using Content.Shared.DoAfter ;
2021-11-21 10:35:09 +03:00
using Content.Shared.FixedPoint ;
2023-04-07 11:21:12 -07:00
using Content.Shared.Hands.Components ;
2022-07-29 14:13:12 +12:00
using Content.Shared.Hands.EntitySystems ;
using Content.Shared.IdentityManagement ;
2021-11-21 10:35:09 +03:00
using Content.Shared.Interaction ;
2023-10-25 00:51:32 -04:00
using Content.Shared.Interaction.Components ;
2022-07-29 14:13:12 +12:00
using Content.Shared.Interaction.Events ;
using Content.Shared.Inventory ;
2023-01-13 16:57:10 -08:00
using Content.Shared.Mobs.Systems ;
2023-04-03 13:13:48 +12:00
using Content.Shared.Nutrition ;
2024-04-01 06:27:39 +00:00
using Content.Shared.Nutrition.EntitySystems ;
2023-05-06 10:23:05 +03:00
using Content.Shared.Stacks ;
2023-10-10 20:06:24 -07:00
using Content.Shared.Storage ;
using Content.Shared.Verbs ;
2021-11-21 10:35:09 +03:00
using Robust.Shared.Audio ;
2023-12-28 20:45:42 -07:00
using Robust.Shared.Audio.Systems ;
2023-12-29 04:47:43 -08:00
using Robust.Shared.Utility ;
using System.Linq ;
2024-07-19 01:34:18 +03:00
using Content.Shared.Containers.ItemSlots ;
2024-05-12 07:31:54 -07:00
using Robust.Server.GameObjects ;
2024-06-03 14:40:03 -07:00
using Content.Shared.Whitelist ;
2024-10-09 11:01:32 -07:00
using Content.Shared.Destructible ;
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
namespace Content.Server.Nutrition.EntitySystems ;
/// <summary>
/// Handles feeding attempts both on yourself and on the target.
/// </summary>
public sealed class FoodSystem : EntitySystem
2021-11-21 10:35:09 +03:00
{
2023-09-23 03:10:04 +01:00
[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 ! ;
2024-08-02 00:30:45 -07:00
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default ! ;
2024-05-12 07:31:54 -07:00
[Dependency] private readonly TransformSystem _transform = default ! ;
2023-09-23 03:10:04 +01:00
[Dependency] private readonly StackSystem _stack = default ! ;
[Dependency] private readonly StomachSystem _stomach = default ! ;
[Dependency] private readonly UtensilSystem _utensil = default ! ;
2024-06-03 14:40:03 -07:00
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default ! ;
2023-09-23 03:10:04 +01:00
public const float MaxFeedDistance = 1.0f ;
public override void Initialize ( )
{
base . Initialize ( ) ;
// TODO add InteractNoHandEvent for entities like mice.
// run after openable for wrapped/peelable foods
2023-09-24 21:49:58 +01:00
SubscribeLocalEvent < FoodComponent , UseInHandEvent > ( OnUseFoodInHand , after : new [ ] { typeof ( OpenableSystem ) , typeof ( ServerInventorySystem ) } ) ;
2023-09-23 03:10:04 +01:00
SubscribeLocalEvent < FoodComponent , AfterInteractEvent > ( OnFeedFood ) ;
SubscribeLocalEvent < FoodComponent , GetVerbsEvent < AlternativeVerb > > ( AddEatVerb ) ;
SubscribeLocalEvent < FoodComponent , ConsumeDoAfterEvent > ( OnDoAfter ) ;
SubscribeLocalEvent < InventoryComponent , IngestionAttemptEvent > ( OnInventoryIngestAttempt ) ;
}
2021-11-21 10:35:09 +03:00
/// <summary>
2023-09-23 03:10:04 +01:00
/// Eat item
2021-11-21 10:35:09 +03:00
/// </summary>
2023-12-29 04:47:43 -08:00
private void OnUseFoodInHand ( Entity < FoodComponent > entity , ref UseInHandEvent ev )
2021-11-21 10:35:09 +03:00
{
2023-09-23 03:10:04 +01:00
if ( ev . Handled )
return ;
2021-11-21 10:35:09 +03:00
2023-12-29 04:47:43 -08:00
var result = TryFeed ( ev . User , ev . User , entity , entity . Comp ) ;
2023-09-23 03:10:04 +01:00
ev . Handled = result . Handled ;
}
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
/// <summary>
/// Feed someone else
/// </summary>
2023-12-29 04:47:43 -08:00
private void OnFeedFood ( Entity < FoodComponent > entity , ref AfterInteractEvent args )
2023-09-23 03:10:04 +01:00
{
if ( args . Handled | | args . Target = = null | | ! args . CanReach )
return ;
2021-11-21 10:35:09 +03:00
2023-12-29 04:47:43 -08:00
var result = TryFeed ( args . User , args . Target . Value , entity , entity . Comp ) ;
2023-09-23 03:10:04 +01:00
args . Handled = result . Handled ;
}
2021-11-29 16:27:15 +13:00
2024-07-09 16:12:40 -07:00
/// <summary>
/// Tries to feed the food item to the target entity
/// </summary>
2023-09-23 03:10:04 +01:00
public ( bool Success , bool Handled ) TryFeed ( EntityUid user , EntityUid target , EntityUid food , FoodComponent foodComp )
{
//Suppresses eating yourself and alive mobs
2023-11-17 08:51:51 +00:00
if ( food = = user | | ( _mobState . IsAlive ( food ) & & foodComp . RequireDead ) )
2023-09-23 03:10:04 +01:00
return ( false , false ) ;
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
// Target can't be fed or they're already eating
if ( ! TryComp < BodyComponent > ( target , out var body ) )
return ( false , false ) ;
2022-02-07 00:37:38 +11:00
2023-10-25 00:51:32 -04:00
if ( HasComp < UnremoveableComponent > ( food ) )
return ( false , false ) ;
2023-09-23 03:10:04 +01:00
if ( _openable . IsClosed ( food , user ) )
return ( false , true ) ;
2021-11-30 13:45:33 +03:00
2023-12-29 04:47:43 -08:00
if ( ! _solutionContainer . TryGetSolution ( food , foodComp . Solution , out _ , out var foodSolution ) )
2023-09-23 03:10:04 +01:00
return ( false , false ) ;
2021-11-21 10:35:09 +03:00
2024-08-02 00:30:45 -07:00
if ( ! _body . TryGetBodyOrganEntityComps < StomachComponent > ( ( target , body ) , out var stomachs ) )
2023-09-23 03:10:04 +01:00
return ( false , false ) ;
2023-05-03 19:49:25 -07:00
2023-09-23 03:10:04 +01:00
// Check for special digestibles
if ( ! IsDigestibleBy ( food , foodComp , stomachs ) )
return ( false , false ) ;
2023-05-03 19:49:25 -07:00
2023-09-23 03:10:04 +01:00
if ( ! TryGetRequiredUtensils ( user , foodComp , out _ ) )
return ( false , false ) ;
2023-05-03 19:49:25 -07:00
2023-09-23 03:10:04 +01:00
// Check for used storage on the food item
2023-10-30 23:55:55 -04:00
if ( TryComp < StorageComponent > ( food , out var storageState ) & & storageState . Container . ContainedEntities . Any ( ) )
2023-09-23 03:10:04 +01:00
{
_popup . PopupEntity ( Loc . GetString ( "food-has-used-storage" , ( "food" , food ) ) , user , user ) ;
return ( false , true ) ;
}
2021-11-21 10:35:09 +03:00
2024-07-19 01:34:18 +03:00
// Checks for used item slots
if ( TryComp < ItemSlotsComponent > ( food , out var itemSlots ) )
{
if ( itemSlots . Slots . Any ( slot = > slot . Value . HasItem ) )
{
_popup . PopupEntity ( Loc . GetString ( "food-has-used-storage" , ( "food" , food ) ) , user , user ) ;
return ( false , true ) ;
}
}
2023-09-23 03:10:04 +01:00
var flavors = _flavorProfile . GetLocalizedFlavorsMessage ( food , user , foodSolution ) ;
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
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 ) ;
}
2023-05-07 14:58:20 +12:00
2023-09-23 03:10:04 +01:00
if ( IsMouthBlocked ( target , user ) )
return ( false , true ) ;
2023-05-07 14:58:20 +12:00
2023-09-23 03:10:04 +01:00
if ( ! _interaction . InRangeUnobstructed ( user , food , popup : true ) )
return ( false , true ) ;
2022-02-07 00:37:38 +11:00
2023-09-23 03:10:04 +01:00
if ( ! _interaction . InRangeUnobstructed ( user , target , MaxFeedDistance , popup : true ) )
return ( false , true ) ;
2022-02-07 00:37:38 +11:00
2023-09-23 03:10:04 +01:00
// TODO make do-afters account for fixtures in the range check.
2024-05-12 07:31:54 -07:00
if ( ! _transform . GetMapCoordinates ( user ) . InRange ( _transform . GetMapCoordinates ( target ) , MaxFeedDistance ) )
2023-09-23 03:10:04 +01:00
{
var message = Loc . GetString ( "interaction-system-user-interaction-cannot-reach" ) ;
_popup . PopupEntity ( message , user , user ) ;
return ( false , true ) ;
}
2022-02-07 00:37:38 +11:00
2023-09-23 03:10:04 +01:00
var forceFeed = user ! = target ;
if ( forceFeed )
{
var userName = Identity . Entity ( user , EntityManager ) ;
_popup . PopupEntity ( Loc . GetString ( "food-system-force-feed" , ( "user" , userName ) ) ,
user , target ) ;
2021-12-03 03:51:05 +13:00
2023-09-23 03:10:04 +01:00
// logging
2024-08-02 00:30:45 -07:00
_adminLogger . Add ( LogType . ForceFeed , LogImpact . Medium , $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}" ) ;
2023-09-23 03:10:04 +01:00
}
else
{
// log voluntary eating
2024-08-02 00:30:45 -07:00
_adminLogger . Add ( LogType . Ingestion , LogImpact . Low , $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}" ) ;
2022-02-07 00:37:38 +11:00
}
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
var doAfterArgs = new DoAfterArgs ( EntityManager ,
user ,
forceFeed ? foodComp . ForceFeedDelay : foodComp . Delay ,
2023-12-29 04:47:43 -08:00
new ConsumeDoAfterEvent ( foodComp . Solution , flavors ) ,
2023-09-23 03:10:04 +01:00
eventTarget : food ,
target : target ,
used : food )
2022-02-07 00:37:38 +11:00
{
2024-07-22 02:17:57 -07:00
BreakOnHandChange = false ,
2024-03-19 12:09:00 +02:00
BreakOnMove = forceFeed ,
2023-09-23 03:10:04 +01:00
BreakOnDamage = true ,
MovementThreshold = 0.01f ,
DistanceThreshold = MaxFeedDistance ,
2024-07-09 16:12:40 -07:00
// do-after will stop if item is dropped when trying to feed someone else
// or if the item started out in the user's own hands
NeedHand = forceFeed | | _hands . IsHolding ( user , food ) ,
2023-09-23 03:10:04 +01:00
} ;
_doAfter . TryStartDoAfter ( doAfterArgs ) ;
return ( true , true ) ;
}
2022-02-07 00:37:38 +11:00
2023-12-29 04:47:43 -08:00
private void OnDoAfter ( Entity < FoodComponent > entity , ref ConsumeDoAfterEvent args )
2023-09-23 03:10:04 +01:00
{
2023-12-29 04:47:43 -08:00
if ( args . Cancelled | | args . Handled | | entity . Comp . Deleted | | args . Target = = null )
2023-09-23 03:10:04 +01:00
return ;
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
if ( ! TryComp < BodyComponent > ( args . Target . Value , out var body ) )
return ;
Flavor profiles (#10991)
* flavor profiles
TODO: every single flavor! yeah!!!
* adds basic localization, and flavors/lastFlavor values for when you get the flavor profile message
* multiple and single flavor messages
* start on flavor localization, multiple flavors in localized flavors
* flavor prototypes
* a few more flavors, descriptions on what each section of the flavor file should be doing
* localization for flavor profiles in drink/food system
* adds an event that allows a flavor profile list to be transformed base on the user entity
* raises it on the food entity too
* changes a field in flavor, adds some more flavors, starts adding flavor prototypes
* adds basic flavors to several entities, and consumable drinks, renames flavor field to 'flavors'
* changes call ordering in flavorprofile, adds flavor to ignored components server-side
flavor is really just a popup message, and those are all processed server-side
* fixes where food tried to get the flavor of the user instead of the food
* single flavors will now get the localized string
* getting the flavor message now ensures that flavors are deduplicated
* makes flavor processing more strictly unique bu making everything hashsets
* yeah, that could just not have distinctby now
* adds flavorprofile directly to food base instead for generic food taste
* FlavorProfileModificationEvent now passes a hashset of strings and not flavorprototypes
* flavorprofilesystem now broadcasts the flavor profile modification event
* adds more flavors to the flavor profile loc file
* skips a flavor, if the flavor string is null/empty
* adds some more flavors, adds generic medicine flavor to medicinal chemicals
* more food flavors, adds flavors to swallowing
* adds some cocktails to the set of flavor profiles
* regenerates flavor prototypes
* adds flavor type to all flavors, adds whitespace between variants
* adds more flavors, adds flavors to several chemicals and food items
this is the part that took the longest
* changes backup flavor message
* spelling mistake
* more flavors, and flavors on food
* readds all the type fields, whoops
* fixes localization strings for forcefeeding food/drink
* fixes multiple flavor profile
* adds flavor limit for flavors
* makes that fetch the cvardef instead
2022-09-08 16:14:49 -07:00
2024-08-02 00:30:45 -07:00
if ( ! _body . TryGetBodyOrganEntityComps < StomachComponent > ( ( args . Target . Value , body ) , out var stomachs ) )
2023-09-23 03:10:04 +01:00
return ;
2023-03-05 00:26:03 -05:00
2023-12-29 04:47:43 -08:00
if ( args . Used is null | | ! _solutionContainer . TryGetSolution ( args . Used . Value , args . Solution , out var soln , out var solution ) )
2023-09-23 03:10:04 +01:00
return ;
2023-04-03 13:13:48 +12:00
2023-12-29 04:47:43 -08:00
if ( ! TryGetRequiredUtensils ( args . User , entity . Comp , out var utensils ) )
2023-09-23 03:10:04 +01:00
return ;
2023-04-17 19:56:42 +12:00
2023-09-23 03:10:04 +01:00
// TODO this should really be checked every tick.
if ( IsMouthBlocked ( args . Target . Value ) )
return ;
2023-05-02 04:57:11 +10:00
2023-09-23 03:10:04 +01:00
// TODO this should really be checked every tick.
if ( ! _interaction . InRangeUnobstructed ( args . User , args . Target . Value ) )
return ;
2023-04-03 13:13:48 +12:00
2023-09-23 03:10:04 +01:00
var forceFeed = args . User ! = args . Target ;
Flavor profiles (#10991)
* flavor profiles
TODO: every single flavor! yeah!!!
* adds basic localization, and flavors/lastFlavor values for when you get the flavor profile message
* multiple and single flavor messages
* start on flavor localization, multiple flavors in localized flavors
* flavor prototypes
* a few more flavors, descriptions on what each section of the flavor file should be doing
* localization for flavor profiles in drink/food system
* adds an event that allows a flavor profile list to be transformed base on the user entity
* raises it on the food entity too
* changes a field in flavor, adds some more flavors, starts adding flavor prototypes
* adds basic flavors to several entities, and consumable drinks, renames flavor field to 'flavors'
* changes call ordering in flavorprofile, adds flavor to ignored components server-side
flavor is really just a popup message, and those are all processed server-side
* fixes where food tried to get the flavor of the user instead of the food
* single flavors will now get the localized string
* getting the flavor message now ensures that flavors are deduplicated
* makes flavor processing more strictly unique bu making everything hashsets
* yeah, that could just not have distinctby now
* adds flavorprofile directly to food base instead for generic food taste
* FlavorProfileModificationEvent now passes a hashset of strings and not flavorprototypes
* flavorprofilesystem now broadcasts the flavor profile modification event
* adds more flavors to the flavor profile loc file
* skips a flavor, if the flavor string is null/empty
* adds some more flavors, adds generic medicine flavor to medicinal chemicals
* more food flavors, adds flavors to swallowing
* adds some cocktails to the set of flavor profiles
* regenerates flavor prototypes
* adds flavor type to all flavors, adds whitespace between variants
* adds more flavors, adds flavors to several chemicals and food items
this is the part that took the longest
* changes backup flavor message
* spelling mistake
* more flavors, and flavors on food
* readds all the type fields, whoops
* fixes localization strings for forcefeeding food/drink
* fixes multiple flavor profile
* adds flavor limit for flavors
* makes that fetch the cvardef instead
2022-09-08 16:14:49 -07:00
2023-09-23 03:10:04 +01:00
args . Handled = true ;
2023-12-29 04:47:43 -08:00
var transferAmount = entity . Comp . TransferAmount ! = null ? FixedPoint2 . Min ( ( FixedPoint2 ) entity . Comp . TransferAmount , solution . Volume ) : solution . Volume ;
2023-05-03 19:49:25 -07:00
2023-12-29 04:47:43 -08:00
var split = _solutionContainer . SplitSolution ( soln . Value , transferAmount ) ;
2023-05-03 19:49:25 -07:00
2023-09-23 03:10:04 +01:00
// Get the stomach with the highest available solution volume
var highestAvailable = FixedPoint2 . Zero ;
2024-08-02 00:30:45 -07:00
Entity < StomachComponent > ? stomachToUse = null ;
foreach ( var ent in stomachs )
2023-09-23 03:10:04 +01:00
{
2024-08-02 00:30:45 -07:00
var owner = ent . Owner ;
if ( ! _stomach . CanTransferSolution ( owner , split , ent . Comp1 ) )
2023-09-23 03:10:04 +01:00
continue ;
2023-05-03 19:49:25 -07:00
2024-08-02 00:30:45 -07:00
if ( ! _solutionContainer . ResolveSolution ( owner , StomachSystem . DefaultSolutionName , ref ent . Comp1 . Solution , out var stomachSol ) )
2023-09-23 03:10:04 +01:00
continue ;
2023-05-03 19:49:25 -07:00
2023-09-23 03:10:04 +01:00
if ( stomachSol . AvailableVolume < = highestAvailable )
continue ;
2021-11-21 10:35:09 +03:00
2024-08-02 00:30:45 -07:00
stomachToUse = ent ;
2023-09-23 03:10:04 +01:00
highestAvailable = stomachSol . AvailableVolume ;
}
// No stomach so just popup a message that they can't eat.
if ( stomachToUse = = null )
{
2023-12-29 04:47:43 -08:00
_solutionContainer . TryAddSolution ( soln . Value , split ) ;
2023-09-23 03:10:04 +01:00
_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 ;
}
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
_reaction . DoEntityReaction ( args . Target . Value , solution , ReactionMethod . Ingestion ) ;
2024-08-02 00:30:45 -07:00
_stomach . TryTransferSolution ( stomachToUse ! . Value . Owner , split , stomachToUse ) ;
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
var flavors = args . FlavorMessage ;
Flavor profiles (#10991)
* flavor profiles
TODO: every single flavor! yeah!!!
* adds basic localization, and flavors/lastFlavor values for when you get the flavor profile message
* multiple and single flavor messages
* start on flavor localization, multiple flavors in localized flavors
* flavor prototypes
* a few more flavors, descriptions on what each section of the flavor file should be doing
* localization for flavor profiles in drink/food system
* adds an event that allows a flavor profile list to be transformed base on the user entity
* raises it on the food entity too
* changes a field in flavor, adds some more flavors, starts adding flavor prototypes
* adds basic flavors to several entities, and consumable drinks, renames flavor field to 'flavors'
* changes call ordering in flavorprofile, adds flavor to ignored components server-side
flavor is really just a popup message, and those are all processed server-side
* fixes where food tried to get the flavor of the user instead of the food
* single flavors will now get the localized string
* getting the flavor message now ensures that flavors are deduplicated
* makes flavor processing more strictly unique bu making everything hashsets
* yeah, that could just not have distinctby now
* adds flavorprofile directly to food base instead for generic food taste
* FlavorProfileModificationEvent now passes a hashset of strings and not flavorprototypes
* flavorprofilesystem now broadcasts the flavor profile modification event
* adds more flavors to the flavor profile loc file
* skips a flavor, if the flavor string is null/empty
* adds some more flavors, adds generic medicine flavor to medicinal chemicals
* more food flavors, adds flavors to swallowing
* adds some cocktails to the set of flavor profiles
* regenerates flavor prototypes
* adds flavor type to all flavors, adds whitespace between variants
* adds more flavors, adds flavors to several chemicals and food items
this is the part that took the longest
* changes backup flavor message
* spelling mistake
* more flavors, and flavors on food
* readds all the type fields, whoops
* fixes localization strings for forcefeeding food/drink
* fixes multiple flavor profile
* adds flavor limit for flavors
* makes that fetch the cvardef instead
2022-09-08 16:14:49 -07:00
2023-09-23 03:10:04 +01:00
if ( forceFeed )
{
var targetName = Identity . Entity ( args . Target . Value , EntityManager ) ;
var userName = Identity . Entity ( args . User , EntityManager ) ;
2023-12-29 04:47:43 -08:00
_popup . PopupEntity ( Loc . GetString ( "food-system-force-feed-success" , ( "user" , userName ) , ( "flavors" , flavors ) ) , entity . Owner , entity . Owner ) ;
2022-02-07 00:37:38 +11:00
2023-09-23 03:10:04 +01:00
_popup . PopupEntity ( Loc . GetString ( "food-system-force-feed-success-user" , ( "target" , targetName ) ) , args . User , args . User ) ;
2022-12-02 19:19:44 -06:00
2023-09-23 03:10:04 +01:00
// log successful force feed
2023-12-29 04:47:43 -08:00
_adminLogger . Add ( LogType . ForceFeed , LogImpact . Medium , $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}" ) ;
2023-09-23 03:10:04 +01:00
}
else
{
2023-12-29 04:47:43 -08:00
_popup . PopupEntity ( Loc . GetString ( entity . Comp . EatMessage , ( "food" , entity . Owner ) , ( "flavors" , flavors ) ) , args . User , args . User ) ;
2022-12-02 19:19:44 -06:00
2023-09-23 03:10:04 +01:00
// log successful voluntary eating
2023-12-29 04:47:43 -08:00
_adminLogger . Add ( LogType . Ingestion , LogImpact . Low , $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}" ) ;
2023-09-23 03:10:04 +01:00
}
2021-11-21 10:35:09 +03:00
2024-10-17 02:38:02 +01:00
_audio . PlayPvs ( entity . Comp . UseSound , args . Target . Value , AudioParams . Default . WithVolume ( - 1f ) . WithVariation ( 0.20f ) ) ;
2022-02-07 00:37:38 +11:00
2023-09-23 03:10:04 +01:00
// Try to break all used utensils
foreach ( var utensil in utensils )
{
_utensil . TryBreak ( utensil , args . User ) ;
}
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
args . Repeat = ! forceFeed ;
2023-05-07 14:58:20 +12:00
2023-12-29 04:47:43 -08:00
if ( TryComp < StackComponent > ( entity , out var stack ) )
2023-09-23 03:10:04 +01:00
{
//Not deleting whole stack piece will make troubles with grinding object
if ( stack . Count > 1 )
2023-04-15 18:14:26 -04:00
{
2023-12-29 04:47:43 -08:00
_stack . SetCount ( entity . Owner , stack . Count - 1 ) ;
_solutionContainer . TryAddSolution ( soln . Value , split ) ;
2022-02-07 00:37:38 +11:00
return ;
2023-04-15 18:14:26 -04:00
}
2023-09-23 03:10:04 +01:00
}
2023-12-29 04:47:43 -08:00
else if ( GetUsesRemaining ( entity . Owner , entity . Comp ) > 0 )
2023-09-23 03:10:04 +01:00
{
return ;
2021-11-21 10:35:09 +03:00
}
2023-11-17 08:51:51 +00:00
// don't try to repeat if its being deleted
args . Repeat = false ;
2023-12-29 04:47:43 -08:00
DeleteAndSpawnTrash ( entity . Comp , entity . Owner , args . User ) ;
2023-11-17 08:51:51 +00:00
}
public void DeleteAndSpawnTrash ( FoodComponent component , EntityUid food , EntityUid user )
{
2023-09-23 03:10:04 +01:00
var ev = new BeforeFullyEatenEvent
2021-11-21 10:35:09 +03:00
{
2023-11-17 08:51:51 +00:00
User = user
2023-09-23 03:10:04 +01:00
} ;
2023-11-17 08:51:51 +00:00
RaiseLocalEvent ( food , ev ) ;
2023-09-23 03:10:04 +01:00
if ( ev . Cancelled )
return ;
2024-10-09 11:01:32 -07:00
var dev = new DestructionEventArgs ( ) ;
RaiseLocalEvent ( food , dev ) ;
2024-08-10 22:31:32 +03:00
if ( component . Trash . Count = = 0 )
2023-11-17 08:51:51 +00:00
{
QueueDel ( food ) ;
return ;
}
2021-11-21 10:35:09 +03:00
2023-09-23 03:10:04 +01:00
//We're empty. Become trash.
2024-08-10 22:31:32 +03:00
//cache some data as we remove food, before spawning trash and passing it to the hand.
2024-05-12 07:31:54 -07:00
var position = _transform . GetMapCoordinates ( food ) ;
2024-08-10 22:31:32 +03:00
var trashes = component . Trash ;
var tryPickup = _hands . IsHolding ( user , food , out _ ) ;
2021-11-21 10:35:09 +03:00
2024-08-10 22:31:32 +03:00
Del ( food ) ;
foreach ( var trash in trashes )
2023-09-23 03:10:04 +01:00
{
2024-08-10 22:31:32 +03:00
var spawnedTrash = Spawn ( trash , position ) ;
2021-11-29 16:27:15 +13:00
2024-08-10 22:31:32 +03:00
// If the user is holding the item
if ( tryPickup )
{
// Put the trash in the user's hand
_hands . TryPickupAnyHand ( user , spawnedTrash ) ;
}
2021-11-21 10:35:09 +03:00
}
2023-09-23 03:10:04 +01:00
}
2023-12-29 04:47:43 -08:00
private void AddEatVerb ( Entity < FoodComponent > entity , ref GetVerbsEvent < AlternativeVerb > ev )
2023-09-23 03:10:04 +01:00
{
2023-12-29 04:47:43 -08:00
if ( entity . Owner = = ev . User | |
2023-09-23 03:10:04 +01:00
! ev . CanInteract | |
! ev . CanAccess | |
! TryComp < BodyComponent > ( ev . User , out var body ) | |
2024-08-02 00:30:45 -07:00
! _body . TryGetBodyOrganEntityComps < StomachComponent > ( ( ev . User , body ) , out var stomachs ) )
2023-09-23 03:10:04 +01:00
return ;
// have to kill mouse before eating it
2023-12-29 04:47:43 -08:00
if ( _mobState . IsAlive ( entity ) & & entity . Comp . RequireDead )
2023-09-23 03:10:04 +01:00
return ;
// only give moths eat verb for clothes since it would just fail otherwise
2023-12-29 04:47:43 -08:00
if ( ! IsDigestibleBy ( entity , entity . Comp , stomachs ) )
2023-09-23 03:10:04 +01:00
return ;
2023-12-29 04:47:43 -08:00
var user = ev . User ;
2023-09-23 03:10:04 +01:00
AlternativeVerb verb = new ( )
2021-11-21 10:35:09 +03:00
{
2023-09-23 03:10:04 +01:00
Act = ( ) = >
{
2023-12-29 04:47:43 -08:00
TryFeed ( user , user , entity , entity . Comp ) ;
2023-09-23 03:10:04 +01:00
} ,
2023-12-29 04:47:43 -08:00
Icon = new SpriteSpecifier . Texture ( new ( "/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png" ) ) ,
2023-09-23 03:10:04 +01:00
Text = Loc . GetString ( "food-system-verb-eat" ) ,
Priority = - 1
} ;
ev . Verbs . Add ( verb ) ;
}
2021-11-30 13:45:33 +03:00
2023-09-23 03:10:04 +01:00
/// <summary>
/// Returns true if the food item can be digested by the user.
/// </summary>
public bool IsDigestibleBy ( EntityUid uid , EntityUid food , FoodComponent ? foodComp = null )
{
if ( ! Resolve ( food , ref foodComp , false ) )
return false ;
2021-11-21 10:35:09 +03:00
2024-08-02 00:30:45 -07:00
if ( ! _body . TryGetBodyOrganEntityComps < StomachComponent > ( uid , out var stomachs ) )
2023-09-23 03:10:04 +01:00
return false ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
return IsDigestibleBy ( food , foodComp , stomachs ) ;
}
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
/// <summary>
/// Returns true if <paramref name="stomachs"/> has a <see cref="StomachComponent.SpecialDigestible"/> that whitelists
/// this <paramref name="food"/> (or if they even have enough stomachs in the first place).
/// </summary>
2024-08-02 00:30:45 -07:00
private bool IsDigestibleBy ( EntityUid food , FoodComponent component , List < Entity < StomachComponent , OrganComponent > > stomachs )
2023-09-23 03:10:04 +01:00
{
var digestible = true ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
// Does the mob have enough stomachs?
if ( stomachs . Count < component . RequiredStomachs )
return false ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
// Run through the mobs' stomachs
2024-08-02 00:30:45 -07:00
foreach ( var ent in stomachs )
2023-05-03 19:49:25 -07:00
{
2023-09-23 03:10:04 +01:00
// Find a stomach with a SpecialDigestible
2024-08-02 00:30:45 -07:00
if ( ent . Comp1 . SpecialDigestible = = null )
2023-09-23 03:10:04 +01:00
continue ;
// Check if the food is in the whitelist
2024-08-02 00:30:45 -07:00
if ( _whitelistSystem . IsWhitelistPass ( ent . Comp1 . SpecialDigestible , food ) )
2023-09-23 03:10:04 +01:00
return true ;
// They can only eat whitelist food and the food isn't in the whitelist. It's not edible.
return false ;
}
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
if ( component . RequiresSpecialDigestion )
2023-12-29 04:47:43 -08:00
return false ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
return digestible ;
}
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
private bool TryGetRequiredUtensils ( EntityUid user , FoodComponent component ,
out List < EntityUid > utensils , HandsComponent ? hands = null )
{
utensils = new List < EntityUid > ( ) ;
2021-11-29 16:27:15 +13:00
2024-01-08 23:33:17 -08:00
if ( component . Utensil = = UtensilType . None )
2023-09-23 03:10:04 +01:00
return true ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
if ( ! Resolve ( user , ref hands , false ) )
2024-01-09 13:17:04 -05:00
return true ; //mice
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
var usedTypes = UtensilType . None ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
foreach ( var item in _hands . EnumerateHeld ( user , hands ) )
{
// Is utensil?
if ( ! TryComp < UtensilComponent > ( item , out var utensil ) )
continue ;
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
if ( ( utensil . Types & component . Utensil ) ! = 0 & & // Acceptable type?
( usedTypes & utensil . Types ) ! = utensil . Types ) // Type is not used already? (removes usage of identical utensils)
2021-11-29 16:27:15 +13:00
{
2023-09-23 03:10:04 +01:00
// Add to used list
usedTypes | = utensil . Types ;
utensils . Add ( item ) ;
2021-11-29 16:27:15 +13:00
}
}
2023-09-23 03:10:04 +01:00
// If "required" field is set, try to block eating without proper utensils used
if ( component . UtensilRequired & & ( usedTypes & component . Utensil ) ! = component . Utensil )
2021-12-03 03:51:05 +13:00
{
2023-09-23 03:10:04 +01:00
_popup . PopupEntity ( Loc . GetString ( "food-you-need-to-hold-utensil" , ( "utensil" , component . Utensil ^ usedTypes ) ) , user , user ) ;
return false ;
}
2021-12-03 03:51:05 +13:00
2023-09-23 03:10:04 +01:00
return true ;
}
2021-12-03 03:51:05 +13:00
2023-09-23 03:10:04 +01:00
/// <summary>
/// Block ingestion attempts based on the equipped mask or head-wear
/// </summary>
2023-12-29 04:47:43 -08:00
private void OnInventoryIngestAttempt ( Entity < InventoryComponent > entity , ref IngestionAttemptEvent args )
2023-09-23 03:10:04 +01:00
{
if ( args . Cancelled )
return ;
2021-12-03 03:51:05 +13:00
2023-09-23 03:10:04 +01:00
IngestionBlockerComponent ? blocker ;
2021-11-29 16:27:15 +13:00
2023-12-29 04:47:43 -08:00
if ( _inventory . TryGetSlotEntity ( entity . Owner , "mask" , out var maskUid ) & &
2023-09-23 03:10:04 +01:00
TryComp ( maskUid , out blocker ) & &
blocker . Enabled )
{
args . Blocker = maskUid ;
args . Cancel ( ) ;
return ;
}
2021-11-29 16:27:15 +13:00
2023-12-29 04:47:43 -08:00
if ( _inventory . TryGetSlotEntity ( entity . Owner , "head" , out var headUid ) & &
2023-09-23 03:10:04 +01:00
TryComp ( headUid , out blocker ) & &
blocker . Enabled )
2021-11-29 16:27:15 +13:00
{
2023-09-23 03:10:04 +01:00
args . Blocker = headUid ;
args . Cancel ( ) ;
}
}
2021-11-29 16:27:15 +13:00
2023-09-23 03:10:04 +01:00
/// <summary>
/// Check whether the target's mouth is blocked by equipment (masks or head-wear).
/// </summary>
/// <param name="uid">The target whose equipment is checked</param>
/// <param name="popupUid">Optional entity that will receive an informative pop-up identifying the blocking
/// piece of equipment.</param>
/// <returns></returns>
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 ) ;
2021-11-29 16:27:15 +13:00
}
2023-09-23 03:10:04 +01:00
return attempt . Cancelled ;
}
/// <summary>
/// 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.
/// </summary>
public int GetUsesRemaining ( EntityUid uid , FoodComponent ? comp = null )
{
if ( ! Resolve ( uid , ref comp ) )
return 0 ;
2023-12-29 04:47:43 -08:00
if ( ! _solutionContainer . TryGetSolution ( uid , comp . Solution , out _ , out var solution ) | | solution . Volume = = 0 )
2023-09-23 03:10:04 +01:00
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 ( ) ) ) ;
2021-11-21 10:35:09 +03:00
}
}