]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Debody Food and Drink Systems, Combine Food and Drink into One System. (#39031)
authorPrincess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com>
Wed, 6 Aug 2025 16:53:38 +0000 (09:53 -0700)
committerGitHub <noreply@github.com>
Wed, 6 Aug 2025 16:53:38 +0000 (12:53 -0400)
* Shelve

* 22 file diff

* What if it was just better

* Hold that thought

* Near final Commit, then YAML hell

* 95% done with cs

* Working Commit

* Final Commit (Before reviews tear it apart and kill me)

* Add a really stupid comment.

* KILL

* EXPLODE TEST FAILS WITH MY MIND

* I hate it here

* TACTICAL NUCLEAR STRIKE

* Wait what the fuck was I doing?

* Comments

* Me when I'm stupid

* Food doesn't need solutions

* API improvements with some API weirdness

* Move non-API out of API

* Better comment

* Fixes and spelling mistakes

* Final fixes

* Final fixes for real...

* Kill food and drink localization files because I hate them.

* Water droplet fix

* Utensil fixes

* Fix verb priority (It should've been 2)

* A few minor localization fixes

* merge conflict and stuff

* MERGE CONFLICT NUCLEAR WAR!!!

* Cleanup

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
52 files changed:
Content.Server/Kitchen/EntitySystems/SharpSystem.cs
Content.Server/NPC/Systems/NPCUtilitySystem.cs
Content.Server/Nutrition/Components/MessyDrinkerComponent.cs
Content.Server/Nutrition/EntitySystems/DrinkSystem.cs
Content.Server/Nutrition/EntitySystems/MessyDrinkerSystem.cs
Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs
Content.Server/Nutrition/EntitySystems/SmokingSystem.Vape.cs
Content.Server/Polymorph/Systems/PolymorphSystem.cs
Content.Shared/Animals/WoolySystem.cs
Content.Shared/Damage/Systems/SharedGodmodeSystem.cs
Content.Shared/Inventory/InventorySystem.Relay.cs
Content.Shared/Nutrition/Components/DrinkComponent.cs
Content.Shared/Nutrition/Components/EdibleComponent.cs [new file with mode: 0644]
Content.Shared/Nutrition/Components/FoodComponent.cs
Content.Shared/Nutrition/Components/IngestionBlockerComponent.cs
Content.Shared/Nutrition/Components/OpenableComponent.cs
Content.Shared/Nutrition/Components/SealableComponent.cs
Content.Shared/Nutrition/Components/UtensilComponent.cs
Content.Shared/Nutrition/EntitySystems/FlavorProfileSystem.cs
Content.Shared/Nutrition/EntitySystems/FoodSystem.cs
Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs [deleted file]
Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs [new file with mode: 0644]
Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs [new file with mode: 0644]
Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs [new file with mode: 0644]
Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs [new file with mode: 0644]
Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs
Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs
Content.Shared/Nutrition/EntitySystems/UtensilSystem.cs
Content.Shared/Nutrition/IngestionEvents.cs
Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs [new file with mode: 0644]
Content.Shared/Stacks/SharedStackSystem.cs
Content.Shared/Storage/EntitySystems/SecretStashSystem.cs
Resources/Locale/en-US/nutrition/components/drink-component.ftl
Resources/Locale/en-US/nutrition/components/food-component.ftl [deleted file]
Resources/Locale/en-US/nutrition/components/ingestion-system.ftl [new file with mode: 0644]
Resources/Locale/en-US/nutrition/components/openable-component.ftl
Resources/Locale/en-US/nutrition/components/sealable-component.ftl [new file with mode: 0644]
Resources/Prototypes/Body/Organs/Animal/ruminant.yml
Resources/Prototypes/Entities/Clothing/Head/misc.yml
Resources/Prototypes/Entities/Effects/puddle.yml
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml
Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml
Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml
Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml
Resources/Prototypes/Entities/Objects/Materials/materials.yml
Resources/Prototypes/Entities/Objects/Misc/kudzu.yml
Resources/Prototypes/Entities/Objects/Specific/chemistry.yml
Resources/Prototypes/Entities/Objects/Tools/bucket.yml
Resources/Prototypes/NPCs/utility_queries.yml
Resources/Prototypes/Nutrition/edible.yml [new file with mode: 0644]
Resources/Prototypes/tags.yml

index cce5fb5bd31516aec3c8381c26bf96fa7f7013c2..0ba9d0990ad41e2b27d43e20b565850700ba1078 100644 (file)
@@ -38,7 +38,7 @@ public sealed class SharpSystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<SharpComponent, AfterInteractEvent>(OnAfterInteract, before: [typeof(UtensilSystem)]);
+        SubscribeLocalEvent<SharpComponent, AfterInteractEvent>(OnAfterInteract, before: [typeof(IngestionSystem)]);
         SubscribeLocalEvent<SharpComponent, SharpDoAfterEvent>(OnDoAfter);
 
         SubscribeLocalEvent<ButcherableComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
index 489ac6de558f8b160a8d25e513bed5050185060c..6de26cd05608b70cf1a6473d2a2a6cd2573fb693 100644 (file)
@@ -44,9 +44,9 @@ public sealed class NPCUtilitySystem : EntitySystem
     [Dependency] private readonly ContainerSystem _container = default!;
     [Dependency] private readonly DrinkSystem _drink = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
-    [Dependency] private readonly FoodSystem _food = default!;
     [Dependency] private readonly HandsSystem _hands = default!;
     [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly IngestionSystem _ingestion = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
     [Dependency] private readonly OpenableSystem _openable = default!;
@@ -174,14 +174,8 @@ public sealed class NPCUtilitySystem : EntitySystem
         {
             case FoodValueCon:
             {
-                if (!TryComp<FoodComponent>(targetUid, out var food))
-                    return 0f;
-
-                // mice can't eat unpeeled bananas, need monkey's help
-                if (_openable.IsClosed(targetUid))
-                    return 0f;
-
-                if (!_food.IsDigestibleBy(owner, targetUid, food))
+                // do we have a mouth available? Is the food item opened?
+                if (!_ingestion.CanConsume(owner, targetUid))
                     return 0f;
 
                 var avoidBadFood = !HasComp<IgnoreBadFoodComponent>(owner);
@@ -194,15 +188,16 @@ public sealed class NPCUtilitySystem : EntitySystem
                 if (avoidBadFood && HasComp<BadFoodComponent>(targetUid))
                     return 0f;
 
+                var nutrition = _ingestion.TotalNutrition(targetUid, owner);
+                if (nutrition <= 1.0f)
+                    return 0f;
+
                 return 1f;
             }
             case DrinkValueCon:
             {
-                if (!TryComp<DrinkComponent>(targetUid, out var drink))
-                    return 0f;
-
-                // can't drink closed drinks
-                if (_openable.IsClosed(targetUid))
+                // can't drink closed drinks and can't drink with a mask on...
+                if (!_ingestion.CanConsume(owner, targetUid))
                     return 0f;
 
                 // only drink when thirsty
@@ -214,7 +209,9 @@ public sealed class NPCUtilitySystem : EntitySystem
                     return 0f;
 
                 // needs to have something that will satiate thirst, mice wont try to drink 100% pure mutagen.
-                var hydration = _drink.TotalHydration(targetUid, drink);
+                // We don't check if the solution is metabolizable cause all drinks should be currently.
+                // If that changes then simply use the other overflow.
+                var hydration = _ingestion.TotalHydration(targetUid);
                 if (hydration <= 1.0f)
                     return 0f;
 
index 6a1a3a031959079257ac78bc28decd7539834c38..5519c5d9838dfdbc0f36ef838cfd72da079d12ef 100644 (file)
@@ -1,9 +1,11 @@
 using Content.Shared.FixedPoint;
+using Content.Shared.Nutrition.Prototypes;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Nutrition.Components;
 
 /// <summary>
-/// Entities with this component occasionally spill some of their drink when drinking.
+/// Entities with this component occasionally spill some of the solution they're ingesting.
 /// </summary>
 [RegisterComponent]
 public sealed partial class MessyDrinkerComponent : Component
@@ -17,6 +19,12 @@ public sealed partial class MessyDrinkerComponent : Component
     [DataField]
     public FixedPoint2 SpillAmount = 1.0;
 
+    /// <summary>
+    /// The types of food prototypes we can spill
+    /// </summary>
+    [DataField]
+    public List<ProtoId<EdiblePrototype>> SpillableTypes = new List<ProtoId<EdiblePrototype>> { "Drink" };
+
     [DataField]
     public LocId? SpillMessagePopup;
 }
index 6e1824c843c9da00959a9e0df081debdaf91536b..1677f1d8229eb08a1d1021e2720cbd6c9300c132 100644 (file)
@@ -1,50 +1,16 @@
-using Content.Server.Body.Systems;
-using Content.Server.Fluids.EntitySystems;
-using Content.Server.Forensics;
-using Content.Server.Inventory;
-using Content.Server.Nutrition.Events;
-using Content.Server.Popups;
-using Content.Shared.Administration.Logs;
-using Content.Shared.Body.Components;
-using Content.Shared.Body.Systems;
-using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Database;
-using Content.Shared.EntityEffects.Effects;
-using Content.Shared.FixedPoint;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Nutrition;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Nutrition.EntitySystems;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
+
 
 namespace Content.Server.Nutrition.EntitySystems;
 
 public sealed class DrinkSystem : SharedDrinkSystem
 {
-    [Dependency] private readonly BodySystem _body = default!;
-    [Dependency] private readonly FoodSystem _food = default!;
-    [Dependency] private readonly IPrototypeManager _proto = default!;
-    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly OpenableSystem _openable = default!;
-    [Dependency] private readonly PopupSystem _popup = default!;
-    [Dependency] private readonly PuddleSystem _puddle = default!;
-    [Dependency] private readonly ReactiveSystem _reaction = default!;
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
-    [Dependency] private readonly StomachSystem _stomach = default!;
-    [Dependency] private readonly ForensicsSystem _forensics = default!;
 
     public override void Initialize()
     {
@@ -55,59 +21,6 @@ public sealed class DrinkSystem : SharedDrinkSystem
         SubscribeLocalEvent<DrinkComponent, ComponentInit>(OnDrinkInit);
         // run before inventory so for bucket it always tries to drink before equipping (when empty)
         // run after openable so its always open -> drink
-        SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse, before: [typeof(ServerInventorySystem)], after: [typeof(OpenableSystem)]);
-        SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
-        SubscribeLocalEvent<DrinkComponent, ConsumeDoAfterEvent>(OnDoAfter);
-    }
-
-    /// <summary>
-    /// Get the total hydration factor contained in a drink's solution.
-    /// </summary>
-    public float TotalHydration(EntityUid uid, DrinkComponent? comp = null)
-    {
-        if (!Resolve(uid, ref comp))
-            return 0f;
-
-        if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out _, out var solution))
-            return 0f;
-
-        var total = 0f;
-        foreach (var quantity in solution.Contents)
-        {
-            var reagent = _proto.Index<ReagentPrototype>(quantity.Reagent.Prototype);
-            if (reagent.Metabolisms == null)
-                continue;
-
-            foreach (var entry in reagent.Metabolisms.Values)
-            {
-                foreach (var effect in entry.Effects)
-                {
-                    // ignores any effect conditions, just cares about how much it can hydrate
-                    if (effect is SatiateThirst thirst)
-                    {
-                        total += thirst.HydrationFactor * quantity.Quantity.Float();
-                    }
-                }
-            }
-        }
-
-        return total;
-    }
-
-    private void AfterInteract(Entity<DrinkComponent> entity, ref AfterInteractEvent args)
-    {
-        if (args.Handled || args.Target == null || !args.CanReach)
-            return;
-
-        args.Handled = TryDrink(args.User, args.Target.Value, entity.Comp, entity);
-    }
-
-    private void OnUse(Entity<DrinkComponent> entity, ref UseInHandEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        args.Handled = TryDrink(args.User, args.User, entity.Comp, entity);
     }
 
     private void OnDrinkInit(Entity<DrinkComponent> entity, ref ComponentInit args)
@@ -147,115 +60,4 @@ public sealed class DrinkSystem : SharedDrinkSystem
         var drainAvailable = DrinkVolume(uid, component);
         _appearance.SetData(uid, FoodVisuals.Visual, drainAvailable.Float(), appearance);
     }
-
-    /// <summary>
-    ///     Raised directed at a victim when someone has force fed them a drink.
-    /// </summary>
-    private void OnDoAfter(Entity<DrinkComponent> entity, ref ConsumeDoAfterEvent args)
-    {
-        if (args.Handled || args.Cancelled || entity.Comp.Deleted)
-            return;
-
-        if (!TryComp<BodyComponent>(args.Target, out var body))
-            return;
-
-        if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
-            return;
-
-        if (_openable.IsClosed(args.Used.Value, args.Target.Value, predicted: true))
-            return;
-
-        // TODO this should really be checked every tick.
-        if (_food.IsMouthBlocked(args.Target.Value))
-            return;
-
-        // TODO this should really be checked every tick.
-        if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value))
-            return;
-
-        var transferAmount = FixedPoint2.Min(entity.Comp.TransferAmount, solution.Volume);
-        var drained = _solutionContainer.SplitSolution(soln.Value, transferAmount);
-        var forceDrink = args.User != args.Target;
-
-        args.Handled = true;
-        if (transferAmount <= 0)
-            return;
-
-        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((args.Target.Value, body), out var stomachs))
-        {
-            _popup.PopupEntity(Loc.GetString(forceDrink ? "drink-component-try-use-drink-cannot-drink-other" : "drink-component-try-use-drink-had-enough"), args.Target.Value, args.User);
-
-            if (HasComp<RefillableSolutionComponent>(args.Target.Value))
-            {
-                _puddle.TrySpillAt(args.User, drained, out _);
-                return;
-            }
-
-            _solutionContainer.Refill(args.Target.Value, soln.Value, drained);
-            return;
-        }
-
-        var firstStomach = stomachs.FirstOrNull(stomach => _stomach.CanTransferSolution(stomach.Owner, drained, stomach.Comp1));
-
-        //All stomachs are full or can't handle whatever solution we have.
-        if (firstStomach == null)
-        {
-            _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"), args.Target.Value, args.Target.Value);
-
-            if (forceDrink)
-            {
-                _popup.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough-other"), args.Target.Value, args.User);
-                _puddle.TrySpillAt(args.Target.Value, drained, out _);
-            }
-            else
-                _solutionContainer.TryAddSolution(soln.Value, drained);
-
-            return;
-        }
-
-        var flavors = args.FlavorMessage;
-
-        if (forceDrink)
-        {
-            var targetName = Identity.Entity(args.Target.Value, EntityManager);
-            var userName = Identity.Entity(args.User, EntityManager);
-
-            _popup.PopupEntity(Loc.GetString("drink-component-force-feed-success", ("user", userName), ("flavors", flavors)), args.Target.Value, args.Target.Value);
-
-            _popup.PopupEntity(
-                Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)),
-                args.User, args.User);
-
-            // log successful forced drinking
-            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to drink {ToPrettyString(entity.Owner):drink}");
-        }
-        else
-        {
-            _popup.PopupEntity(
-                Loc.GetString("drink-component-try-use-drink-success-slurp-taste", ("flavors", flavors)), args.User,
-                args.User);
-            _popup.PopupEntity(
-                Loc.GetString("drink-component-try-use-drink-success-slurp"), args.User, Filter.PvsExcept(args.User), true);
-
-            // log successful voluntary drinking
-            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
-        }
-
-        _audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
-
-        var beforeDrinkEvent = new BeforeIngestDrinkEvent(entity.Owner, drained, forceDrink);
-        RaiseLocalEvent(args.Target.Value, ref beforeDrinkEvent);
-
-        _forensics.TransferDna(entity, args.Target.Value);
-
-        _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
-
-        if (drained.Volume == 0)
-            return;
-
-        _stomach.TryTransferSolution(firstStomach.Value.Owner, drained, firstStomach.Value.Comp1);
-
-        if (!forceDrink && solution.Volume > 0)
-            args.Repeat = true;
-    }
 }
index f92318d0f7113222717c35abfda31a4eeba38d30..dc8c11bb7f81cb320e6db5db92fd42a1adbb9aa6 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Fluids.EntitySystems;
 using Content.Server.Nutrition.Components;
-using Content.Server.Nutrition.Events;
+using Content.Shared.Nutrition;
+using Content.Shared.Nutrition.EntitySystems;
 using Content.Shared.Popups;
 using Robust.Shared.Random;
 
@@ -8,24 +9,30 @@ namespace Content.Server.Nutrition.EntitySystems;
 
 public sealed class MessyDrinkerSystem : EntitySystem
 {
-    [Dependency] private readonly PuddleSystem _puddle = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly IngestionSystem _ingestion = default!;
+    [Dependency] private readonly PuddleSystem _puddle = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<MessyDrinkerComponent, BeforeIngestDrinkEvent>(OnBeforeIngestDrink);
+        SubscribeLocalEvent<MessyDrinkerComponent, IngestingEvent>(OnIngested);
     }
 
-    private void OnBeforeIngestDrink(Entity<MessyDrinkerComponent> ent, ref BeforeIngestDrinkEvent ev)
+    private void OnIngested(Entity<MessyDrinkerComponent> ent, ref IngestingEvent ev)
     {
-        if (ev.Solution.Volume <= ent.Comp.SpillAmount)
+        if (ev.Split.Volume <= ent.Comp.SpillAmount)
+            return;
+
+        var proto = _ingestion.GetEdibleType(ev.Food);
+
+        if (proto == null || !ent.Comp.SpillableTypes.Contains(proto.Value))
             return;
 
         // Cannot spill if you're being forced to drink.
-        if (ev.Forced)
+        if (ev.ForceFed)
             return;
 
         if (!_random.Prob(ent.Comp.SpillChance))
@@ -34,7 +41,7 @@ public sealed class MessyDrinkerSystem : EntitySystem
         if (ent.Comp.SpillMessagePopup != null)
             _popup.PopupEntity(Loc.GetString(ent.Comp.SpillMessagePopup), ent, ent, PopupType.MediumCaution);
 
-        var split = ev.Solution.SplitSolution(ent.Comp.SpillAmount);
+        var split = ev.Split.SplitSolution(ent.Comp.SpillAmount);
 
         _puddle.TrySpillAt(ent, split, out _);
     }
index 3ce285b06c1063aa59ae015dad0f6b90b8853a6c..2a13d07797d98f4f525700626759cf7ad101b120 100644 (file)
@@ -22,6 +22,7 @@ public sealed class SliceableFoodSystem : EntitySystem
 {
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedDestructibleSystem _destroy = default!;
     [Dependency] private readonly TransformSystem _transform = default!;
     [Dependency] private readonly DoAfterSystem _doAfter = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
@@ -64,31 +65,27 @@ public sealed class SliceableFoodSystem : EntitySystem
         if (args.Cancelled || args.Handled || args.Args.Target == null)
             return;
 
-        if (TrySliceFood(entity, args.User, args.Used, entity.Comp))
+        if (TrySliceFood(entity.Owner, args.User, args.Used))
             args.Handled = true;
     }
 
-    private bool TrySliceFood(EntityUid uid,
+    private bool TrySliceFood(Entity<TransformComponent?, SliceableFoodComponent?, EdibleComponent?> entity,
         EntityUid user,
-        EntityUid? usedItem,
-        SliceableFoodComponent? component = null,
-        FoodComponent? food = null,
-        TransformComponent? transform = null)
+        EntityUid? usedItem)
     {
-        if (!Resolve(uid, ref component, ref food, ref transform) ||
-            string.IsNullOrEmpty(component.Slice))
+        if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2, ref entity.Comp3) || string.IsNullOrEmpty(entity.Comp2.Slice))
             return false;
 
-        if (!_solutionContainer.TryGetSolution(uid, food.Solution, out var soln, out var solution))
+        if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp3.Solution, out var soln, out var solution))
             return false;
 
         if (!TryComp<UtensilComponent>(usedItem, out var utensil) || (utensil.Types & UtensilType.Knife) == 0)
             return false;
 
-        var sliceVolume = solution.Volume / FixedPoint2.New(component.TotalCount);
-        for (int i = 0; i < component.TotalCount; i++)
+        var sliceVolume = solution.Volume / FixedPoint2.New(entity.Comp2.TotalCount);
+        for (int i = 0; i < entity.Comp2.TotalCount; i++)
         {
-            var sliceUid = Slice(uid, user, component, transform);
+            var sliceUid = Slice(entity, user);
 
             var lostSolution =
                 _solutionContainer.SplitSolution(soln.Value, sliceVolume);
@@ -97,11 +94,11 @@ public sealed class SliceableFoodSystem : EntitySystem
             FillSlice(sliceUid, lostSolution);
         }
 
-        _audio.PlayPvs(component.Sound, transform.Coordinates, AudioParams.Default.WithVolume(-2));
+        _audio.PlayPvs(entity.Comp2.Sound, entity.Comp1.Coordinates, AudioParams.Default.WithVolume(-2));
         var ev = new SliceFoodEvent();
-        RaiseLocalEvent(uid, ref ev);
+        RaiseLocalEvent(entity, ref ev);
 
-        DeleteFood(uid, user, food);
+        DeleteFood(entity, user);
         return true;
     }
 
@@ -109,19 +106,16 @@ public sealed class SliceableFoodSystem : EntitySystem
     /// Create a new slice in the world and returns its entity.
     /// The solutions must be set afterwards.
     /// </summary>
-    public EntityUid Slice(EntityUid uid,
-        EntityUid user,
-        SliceableFoodComponent? comp = null,
-        TransformComponent? transform = null)
+    public EntityUid Slice(Entity<TransformComponent?, SliceableFoodComponent?> entity, EntityUid user)
     {
-        if (!Resolve(uid, ref comp, ref transform))
+        if (!Resolve(entity, ref entity.Comp1, ref entity.Comp2))
             return EntityUid.Invalid;
 
-        var sliceUid = Spawn(comp.Slice, _transform.GetMapCoordinates(uid));
+        var sliceUid = Spawn(entity.Comp2.Slice, _transform.GetMapCoordinates((entity, entity.Comp1)));
 
         // try putting the slice into the container if the food being sliced is in a container!
         // this lets you do things like slice a pizza up inside of a hot food cart without making a food-everywhere mess
-        _transform.DropNextTo(sliceUid, (uid, transform));
+        _transform.DropNextTo(sliceUid, entity);
         _transform.SetLocalRotation(sliceUid, 0);
 
         if (!_container.IsEntityOrParentInContainer(sliceUid))
@@ -134,7 +128,7 @@ public sealed class SliceableFoodSystem : EntitySystem
         return sliceUid;
     }
 
-    private void DeleteFood(EntityUid uid, EntityUid user, FoodComponent foodComp)
+    private void DeleteFood(EntityUid uid, EntityUid user)
     {
         var ev = new BeforeFullySlicedEvent
         {
@@ -144,38 +138,32 @@ public sealed class SliceableFoodSystem : EntitySystem
         if (ev.Cancelled)
             return;
 
-        var dev = new DestructionEventArgs();
-        RaiseLocalEvent(uid, dev);
-
-        // Locate the sliced food and spawn its trash
-        foreach (var trash in foodComp.Trash)
-        {
-            var trashUid = Spawn(trash, _transform.GetMapCoordinates(uid));
-
-            // try putting the trash in the food's container too, to be consistent with slice spawning?
-            _transform.DropNextTo(trashUid, uid);
-            _transform.SetLocalRotation(trashUid, 0);
-        }
-
-        QueueDel(uid);
+        _destroy.DestroyEntity(uid);
     }
 
-    private void FillSlice(EntityUid sliceUid, Solution solution)
+    private void FillSlice(Entity<EdibleComponent?> slice, Solution solution)
     {
+        if (!Resolve(slice, ref slice.Comp, false))
+            return;
+
         // Replace all reagents on prototype not just copying poisons (example: slices of eaten pizza should have less nutrition)
-        if (TryComp<FoodComponent>(sliceUid, out var sliceFoodComp) &&
-            _solutionContainer.TryGetSolution(sliceUid, sliceFoodComp.Solution, out var itsSoln, out var itsSolution))
-        {
-            _solutionContainer.RemoveAllSolution(itsSoln.Value);
+        if (!_solutionContainer.TryGetSolution(slice.Owner, slice.Comp.Solution, out var itsSoln, out var itsSolution))
+            return;
 
-            var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume);
-            _solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart);
-        }
+        _solutionContainer.RemoveAllSolution(itsSoln.Value);
+
+        var lostSolutionPart = solution.SplitSolution(itsSolution.AvailableVolume);
+        _solutionContainer.TryAddSolution(itsSoln.Value, lostSolutionPart);
     }
 
     private void OnComponentStartup(Entity<SliceableFoodComponent> entity, ref ComponentStartup args)
     {
-        var foodComp = EnsureComp<FoodComponent>(entity);
+        // TODO: When Food Component is fully kill delete this awful method
+        // This exists just to make tests fail I guess, awesome!
+        // If you're here because your test just failed, make sure that:
+        // Your food has the edible component
+        // The solution listed in the edible component exists
+        var foodComp = EnsureComp<EdibleComponent>(entity);
         _solutionContainer.EnsureSolution(entity.Owner, foodComp.Solution, out _);
     }
 }
index 5a269eace53921945a2a89afa235055cb0fe69af..5de6a5a631c41a16f9185df9b82106f2653e14ec 100644 (file)
@@ -22,7 +22,7 @@ namespace Content.Server.Nutrition.EntitySystems
         [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
         [Dependency] private readonly DamageableSystem _damageableSystem = default!;
         [Dependency] private readonly EmagSystem _emag = default!;
-        [Dependency] private readonly FoodSystem _foodSystem = default!;
+        [Dependency] private readonly IngestionSystem _ingestion = default!;
         [Dependency] private readonly ExplosionSystem _explosionSystem = default!;
         [Dependency] private readonly PopupSystem _popupSystem = default!;
 
@@ -42,7 +42,8 @@ namespace Content.Server.Nutrition.EntitySystems
             if (!args.CanReach
                 || !_solutionContainerSystem.TryGetRefillableSolution(entity.Owner, out _, out var solution)
                 || !HasComp<BloodstreamComponent>(args.Target)
-                || _foodSystem.IsMouthBlocked(args.Target.Value, args.User))
+                || _ingestion.HasMouthAvailable(args.Target.Value, args.User)
+                )
             {
                 return;
             }
index 6b6284b18e2cee9d165fedc4fdfde0cf9fd5e0d0..d0f8b4e8fc4389b340f86024e313a3eb555c5cb1 100644 (file)
@@ -53,7 +53,6 @@ public sealed partial class PolymorphSystem : EntitySystem
         SubscribeLocalEvent<PolymorphableComponent, PolymorphActionEvent>(OnPolymorphActionEvent);
         SubscribeLocalEvent<PolymorphedEntityComponent, RevertPolymorphActionEvent>(OnRevertPolymorphActionEvent);
 
-        SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
         SubscribeLocalEvent<PolymorphedEntityComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
         SubscribeLocalEvent<PolymorphedEntityComponent, DestructionEventArgs>(OnDestruction);
 
@@ -126,16 +125,6 @@ public sealed partial class PolymorphSystem : EntitySystem
         Revert((ent, ent));
     }
 
-    private void OnBeforeFullyEaten(Entity<PolymorphedEntityComponent> ent, ref BeforeFullyEatenEvent args)
-    {
-        var (_, comp) = ent;
-        if (comp.Configuration.RevertOnEat)
-        {
-            args.Cancel();
-            Revert((ent, ent));
-        }
-    }
-
     private void OnBeforeFullySliced(Entity<PolymorphedEntityComponent> ent, ref BeforeFullySlicedEvent args)
     {
         var (_, comp) = ent;
index 734de2f34c9b9d3d50c20b8c84ca9be3ce966829..203def2257a05fc436b2e3a0ff22f815ac4c477b 100644 (file)
@@ -23,7 +23,6 @@ public sealed class WoolySystem : EntitySystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<WoolyComponent, BeforeFullyEatenEvent>(OnBeforeFullyEaten);
         SubscribeLocalEvent<WoolyComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<WoolyComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
     }
@@ -77,10 +76,4 @@ public sealed class WoolySystem : EntitySystem
             _solutionContainer.TryAddReagent(wooly.Solution.Value, wooly.ReagentId, wooly.Quantity, out _);
         }
     }
-
-    private void OnBeforeFullyEaten(Entity<WoolyComponent> ent, ref BeforeFullyEatenEvent args)
-    {
-        // don't want moths to delete goats after eating them
-        args.Cancel();
-    }
 }
index feb5dd4140b8aeff127198601080b472875e36f2..4bf762c479a5406c4b6235b448ebd0876aeae98a 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Shared.Damage.Components;
 using Content.Shared.Damage.Events;
 using Content.Shared.Destructible;
+using Content.Shared.Nutrition;
 using Content.Shared.Prototypes;
 using Content.Shared.Rejuvenate;
 using Content.Shared.Slippery;
@@ -24,6 +25,7 @@ public abstract class SharedGodmodeSystem : EntitySystem
         SubscribeLocalEvent<GodmodeComponent, BeforeStatusEffectAddedEvent>(OnBeforeStatusEffect);
         SubscribeLocalEvent<GodmodeComponent, BeforeOldStatusEffectAddedEvent>(OnBeforeOldStatusEffect);
         SubscribeLocalEvent<GodmodeComponent, BeforeStaminaDamageEvent>(OnBeforeStaminaDamage);
+        SubscribeLocalEvent<GodmodeComponent, IngestibleEvent>(BeforeEdible);
         SubscribeLocalEvent<GodmodeComponent, SlipAttemptEvent>(OnSlipAttempt);
         SubscribeLocalEvent<GodmodeComponent, DestructionAttemptEvent>(OnDestruction);
     }
@@ -60,6 +62,11 @@ public abstract class SharedGodmodeSystem : EntitySystem
         args.Cancel();
     }
 
+    private void BeforeEdible(Entity<GodmodeComponent> ent, ref IngestibleEvent args)
+    {
+        args.Cancelled = true;
+    }
+
     public virtual void EnableGodmode(EntityUid uid, GodmodeComponent? godmode = null)
     {
         godmode ??= EnsureComp<GodmodeComponent>(uid);
index fe3267a92f060b2016fcbe0ce47fedc093c6f3b5..f4a0ccb5de1c14bbea4055c4910798726b2ccfa0 100644 (file)
@@ -19,6 +19,7 @@ using Content.Shared.Inventory.Events;
 using Content.Shared.Movement.Events;
 using Content.Shared.Movement.Systems;
 using Content.Shared.NameModifier.EntitySystems;
+using Content.Shared.Nutrition;
 using Content.Shared.Overlays;
 using Content.Shared.Projectiles;
 using Content.Shared.Radio;
@@ -72,6 +73,7 @@ public partial class InventorySystem
         SubscribeLocalEvent<InventoryComponent, FlashAttemptEvent>(RefRelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, WieldAttemptEvent>(RefRelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, UnwieldAttemptEvent>(RefRelayInventoryEvent);
+        SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(RefRelayInventoryEvent);
 
         // Eye/vision events
         SubscribeLocalEvent<InventoryComponent, CanSeeAttemptEvent>(RelayInventoryEvent);
index 2211d58071af25ff8b1a4f37b3cc4de8c0cff983..a4d1114379bd0e97dda3a64ec983cfff032e13e2 100644 (file)
@@ -5,6 +5,7 @@ using Robust.Shared.GameStates;
 
 namespace Content.Shared.Nutrition.Components;
 
+[Obsolete("Migration to Content.Shared.Nutrition.Components.EdibleComponent is required")]
 [NetworkedComponent, AutoGenerateComponentState]
 [RegisterComponent, Access(typeof(SharedDrinkSystem))]
 public sealed partial class DrinkComponent : Component
diff --git a/Content.Shared/Nutrition/Components/EdibleComponent.cs b/Content.Shared/Nutrition/Components/EdibleComponent.cs
new file mode 100644 (file)
index 0000000..4fcd977
--- /dev/null
@@ -0,0 +1,86 @@
+using Content.Shared.Body.Components;
+using Content.Shared.FixedPoint;
+using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Nutrition.Prototypes;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Nutrition.Components;
+
+/// <summary>
+/// This is used on an entity with a solution container to flag a specific solution as being able to have its
+/// reagents consumed directly.
+/// </summary>
+[RegisterComponent, NetworkedComponent, Access(typeof(IngestionSystem))]
+public sealed partial class EdibleComponent : Component
+{
+    /// <summary>
+    /// Name of the solution that stores the consumable reagents
+    /// </summary>
+    [DataField]
+    public string Solution = "food";
+
+    /// <summary>
+    /// Should this entity be deleted when our solution is emptied?
+    /// </summary>
+    [DataField]
+    public bool DestroyOnEmpty = true;
+
+    /// <summary>
+    /// Trash we spawn when eaten, will not spawn if the item isn't deleted when empty.
+    /// </summary>
+    [DataField]
+    public List<EntProtoId> Trash = new();
+
+    /// <summary>
+    /// How much of our solution is eaten on a do-after completion. Set to null to eat the whole thing.
+    /// </summary>
+    [DataField]
+    public FixedPoint2? TransferAmount = FixedPoint2.New(5);
+
+    /// <summary>
+    /// Acceptable utensils to use
+    /// </summary>
+    [DataField]
+    public UtensilType Utensil = UtensilType.Fork; //There are more "solid" than "liquid" food
+
+    /// <summary>
+    /// Do we need a utensil to access this solution?
+    /// </summary>
+    [DataField]
+    public bool UtensilRequired;
+
+    /// <summary>
+    ///     If this is set to true, food can only be eaten if you have a stomach with a
+    ///     <see cref="StomachComponent.SpecialDigestible"/> that includes this entity in its whitelist,
+    ///     rather than just being digestible by anything that can eat food.
+    ///     Whitelist the food component to allow eating of normal food.
+    /// </summary>
+    [DataField]
+    public bool RequiresSpecialDigestion;
+
+    /// <summary>
+    /// How long it takes to eat the food personally.
+    /// </summary>
+    [DataField]
+    public TimeSpan Delay = TimeSpan.FromSeconds(1f);
+
+    /// <summary>
+    ///     This is how many seconds it takes to force-feed someone this food.
+    ///     Should probably be smaller for small items like pills.
+    /// </summary>
+    [DataField]
+    public TimeSpan ForceFeedDelay = TimeSpan.FromSeconds(3f);
+
+    /// <summary>
+    /// For mobs that are food, requires killing them before eating.
+    /// </summary>
+    [DataField]
+    public bool RequireDead = true;
+
+    /// <summary>
+    /// Verb, icon, and sound data for our edible.
+    /// </summary>
+    [DataField]
+    public ProtoId<EdiblePrototype> Edible = IngestionSystem.Food;
+}
index ce04569fcb92e890e0a52b22c33d56b39104fa1d..5f1ec4171798267acc1d78e5e87eff3d8e117920 100644 (file)
@@ -5,7 +5,7 @@ using Robust.Shared.Audio;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Nutrition.Components;
-
+[Obsolete("Migration to Content.Shared.Nutrition.Components.EdibleComponent is required")]
 [RegisterComponent, Access(typeof(FoodSystem), typeof(FoodSequenceSystem))]
 public sealed partial class FoodComponent : Component
 {
@@ -53,7 +53,7 @@ public sealed partial class FoodComponent : Component
     /// The localization identifier for the eat message. Needs a "food" entity argument passed to it.
     /// </summary>
     [DataField]
-    public LocId EatMessage = "food-nom";
+    public LocId EatMessage = "edible-nom";
 
     /// <summary>
     /// How long it takes to eat the food personally.
index 803bf1f8b20f2071dc9413bb5a9549100302607c..931d47838ba9f65eecb098b025a6d9c332bd69db 100644 (file)
@@ -9,13 +9,12 @@ namespace Content.Shared.Nutrition.Components;
 ///     In the event that more head-wear & mask functionality is added (like identity systems, or raising/lowering of
 ///     masks), then this component might become redundant.
 /// </remarks>
-[RegisterComponent, Access(typeof(FoodSystem), typeof(SharedDrinkSystem), typeof(IngestionBlockerSystem))]
+[RegisterComponent, Access(typeof(IngestionSystem))]
 public sealed partial class IngestionBlockerComponent : Component
 {
     /// <summary>
     ///     Is this component currently blocking consumption.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("enabled")]
+    [DataField]
     public bool Enabled { get; set; } = true;
 }
index 58d6665c588d07426c0ce8bb97f330e74491a1a6..6a32c0bdebea4fcd58b3d8769db7c76f0564ba45 100644 (file)
@@ -36,7 +36,7 @@ public sealed partial class OpenableComponent : Component
     /// Text shown when examining and its open.
     /// </summary>
     [DataField]
-    public LocId ExamineText = "drink-component-on-examine-is-opened";
+    public LocId ExamineText = "openable-component-on-examine-is-opened";
 
     /// <summary>
     /// The locale id for the popup shown when IsClosed is called and closed. Needs a "owner" entity argument passed to it.
@@ -44,7 +44,7 @@ public sealed partial class OpenableComponent : Component
     /// It's still generic enough that you should change it if you make openable non-drinks, i.e. unwrap it first, peel it first.
     /// </summary>
     [DataField]
-    public LocId ClosedPopup = "drink-component-try-use-drink-not-open";
+    public LocId ClosedPopup = "openable-component-try-use-closed";
 
     /// <summary>
     /// Text to show in the verb menu for the "Open" action.
index 1c2f732e7a9f2f84f7f7dc6747ab502d1ba7baf4..71a63f103bd964d66865a1cbe65a7404c18e8cb5 100644 (file)
@@ -22,11 +22,11 @@ public sealed partial class SealableComponent : Component
     /// Text shown when examining and the item's seal has not been broken.
     /// </summary>
     [DataField]
-    public LocId ExamineTextSealed = "drink-component-on-examine-is-sealed";
+    public LocId ExamineTextSealed = "sealable-component-on-examine-is-sealed";
 
     /// <summary>
     /// Text shown when examining and the item's seal has been broken.
     /// </summary>
     [DataField]
-    public LocId ExamineTextUnsealed = "drink-component-on-examine-is-unsealed";
+    public LocId ExamineTextUnsealed = "sealable-component-on-examine-is-unsealed";
 }
index e8da5881863d243bd96a92cc0196729c56112065..f3c43235922bd9879b210abf9424845d260b2769 100644 (file)
@@ -4,7 +4,7 @@ using Robust.Shared.GameStates;
 
 namespace Content.Shared.Nutrition.Components
 {
-    [RegisterComponent, NetworkedComponent, Access(typeof(UtensilSystem))]
+    [RegisterComponent, NetworkedComponent, Access(typeof(IngestionSystem))]
     public sealed partial class UtensilComponent : Component
     {
         [DataField("types")]
index 31384f3a18e891896e8a0449ab44b4ece0974660..e887486e93dfffa217a88fd24f90545da523cc07 100644 (file)
@@ -19,22 +19,30 @@ public sealed class FlavorProfileSystem : EntitySystem
 
     private int FlavorLimit => _configManager.GetCVar(CCVars.FlavorLimit);
 
-    public string GetLocalizedFlavorsMessage(EntityUid uid, EntityUid user, Solution solution,
-        FlavorProfileComponent? flavorProfile = null)
+    public string GetLocalizedFlavorsMessage(Entity<FlavorProfileComponent?> entity, EntityUid user, Solution? solution)
     {
-        if (!Resolve(uid, ref flavorProfile, false))
+        HashSet<string> flavors = new();
+        HashSet<string>? ignore = null;
+
+        if (Resolve(entity, ref entity.Comp, false))
         {
-            return Loc.GetString(BackupFlavorMessage);
+            flavors = entity.Comp.Flavors;
+            ignore = entity.Comp.IgnoreReagents;
         }
 
-        var flavors = new HashSet<string>(flavorProfile.Flavors);
-        flavors.UnionWith(GetFlavorsFromReagents(solution, FlavorLimit - flavors.Count, flavorProfile.IgnoreReagents));
+
+        if (solution != null)
+            flavors.UnionWith(GetFlavorsFromReagents(solution, FlavorLimit - flavors.Count, ignore));
 
         var ev = new FlavorProfileModificationEvent(user, flavors);
+
         RaiseLocalEvent(ev);
-        RaiseLocalEvent(uid, ev);
+        RaiseLocalEvent(entity, ev);
         RaiseLocalEvent(user, ev);
 
+        if (flavors.Count == 0)
+            return Loc.GetString(BackupFlavorMessage);
+
         return FlavorsToFlavorMessage(flavors);
     }
 
index a8b5f7ac7807527edff86247d5ee1a51b5b22698..a599a1e74e9568087262866cbbf86e830e41062f 100644 (file)
@@ -1,58 +1,36 @@
-using System.Linq;
 using Content.Shared.Administration.Logs;
-using Content.Shared.Body.Components;
-using Content.Shared.Body.Organ;
-using Content.Shared.Body.Systems;
-using Content.Shared.Chemistry;
 using Content.Shared.Chemistry.EntitySystems;
-using Content.Shared.Containers.ItemSlots;
 using Content.Shared.Database;
-using Content.Shared.Destructible;
-using Content.Shared.DoAfter;
-using Content.Shared.FixedPoint;
-using Content.Shared.Hands.Components;
+using Content.Shared.Forensics;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
-using Content.Shared.Interaction.Components;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Inventory;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Popups;
-using Content.Shared.Stacks;
-using Content.Shared.Storage;
 using Content.Shared.Verbs;
-using Content.Shared.Whitelist;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
-using Robust.Shared.Utility;
 
 namespace Content.Shared.Nutrition.EntitySystems;
 
 /// <summary>
 /// Handles feeding attempts both on yourself and on the target.
 /// </summary>
+[Obsolete("Migration to Content.Shared.Nutrition.EntitySystems.IngestionSystem is required")]
 public sealed class FoodSystem : EntitySystem
 {
-    [Dependency] private readonly SharedBodySystem _body = default!;
     [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
+    [Dependency] private readonly IngestionSystem _ingestion = 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 SharedPopupSystem _popup = default!;
-    [Dependency] private readonly ReactiveSystem _reaction = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
-    [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
-    [Dependency] private readonly SharedStackSystem _stack = default!;
-    [Dependency] private readonly StomachSystem _stomach = default!;
-    [Dependency] private readonly UtensilSystem _utensil = default!;
-    [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
 
     public const float MaxFeedDistance = 1.0f;
 
@@ -60,25 +38,35 @@ public sealed class FoodSystem : EntitySystem
     {
         base.Initialize();
 
-        // TODO add InteractNoHandEvent for entities like mice.
-        // run after openable for wrapped/peelable foods
         SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
         SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
+
         SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb);
-        SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter);
-        SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(OnInventoryIngestAttempt);
+
+        SubscribeLocalEvent<FoodComponent, BeforeIngestedEvent>(OnBeforeFoodEaten);
+        SubscribeLocalEvent<FoodComponent, IngestedEvent>(OnFoodEaten);
+        SubscribeLocalEvent<FoodComponent, FullyEatenEvent>(OnFoodFullyEaten);
+
+        SubscribeLocalEvent<FoodComponent, GetUtensilsEvent>(OnGetUtensils);
+
+        SubscribeLocalEvent<FoodComponent, IsDigestibleEvent>(OnIsFoodDigestible);
+
+        SubscribeLocalEvent<FoodComponent, EdibleEvent>(OnFood);
+
+        SubscribeLocalEvent<FoodComponent, GetEdibleTypeEvent>(OnGetEdibleType);
+
+        SubscribeLocalEvent<FoodComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
     }
 
     /// <summary>
-    /// Eat item
+    /// Eat or drink an item
     /// </summary>
     private void OnUseFoodInHand(Entity<FoodComponent> entity, ref UseInHandEvent ev)
     {
         if (ev.Handled)
             return;
 
-        var result = TryFeed(ev.User, ev.User, entity, entity.Comp);
-        ev.Handled = result.Handled;
+        ev.Handled = _ingestion.TryIngest(ev.User, ev.User, entity);
     }
 
     /// <summary>
@@ -89,271 +77,98 @@ public sealed class FoodSystem : EntitySystem
         if (args.Handled || args.Target == null || !args.CanReach)
             return;
 
-        var result = TryFeed(args.User, args.Target.Value, entity, entity.Comp);
-        args.Handled = result.Handled;
+        args.Handled = _ingestion.TryIngest(args.User, args.Target.Value, entity);
     }
 
-    /// <summary>
-    /// Tries to feed the food item to the target entity
-    /// </summary>
-    public (bool Success, bool Handled) TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp)
+    private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
     {
-        //Suppresses eating yourself and alive mobs
-        if (food == user || (_mobState.IsAlive(food) && foodComp.RequireDead))
-            return (false, false);
-
-        // Target can't be fed or they're already eating
-        if (!TryComp<BodyComponent>(target, out var body))
-            return (false, false);
-
-        if (HasComp<UnremoveableComponent>(food))
-            return (false, false);
-
-        if (_openable.IsClosed(food, user, predicted: true))
-            return (false, true);
-
-        if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution))
-            return (false, false);
-
-        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((target, body), out var stomachs))
-            return (false, false);
-
-        // Check for special digestibles
-        if (!IsDigestibleBy(food, foodComp, stomachs))
-            return (false, false);
-
-        if (!TryGetRequiredUtensils(user, foodComp, out _))
-            return (false, false);
-
-        // Check for used storage on the food item
-        if (TryComp<StorageComponent>(food, out var storageState) && storageState.Container.ContainedEntities.Any())
-        {
-            _popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
-            return (false, true);
-        }
+        var user = args.User;
 
-        // Checks for used item slots
-        if (TryComp<ItemSlotsComponent>(food, out var itemSlots))
-        {
-            if (itemSlots.Slots.Any(slot => slot.Value.HasItem))
-            {
-                _popup.PopupClient(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
-                return (false, true);
-            }
-        }
-
-        var flavors = _flavorProfile.GetLocalizedFlavorsMessage(food, user, foodSolution);
-
-        if (GetUsesRemaining(food, foodComp) <= 0)
-        {
-            _popup.PopupClient(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
-            DeleteAndSpawnTrash(foodComp, food, user);
-            return (false, true);
-        }
-
-        if (IsMouthBlocked(target, user))
-            return (false, true);
-
-        if (!_interaction.InRangeUnobstructed(user, food, popup: true))
-            return (false, true);
-
-        if (!_interaction.InRangeUnobstructed(user, target, MaxFeedDistance, popup: true))
-            return (false, true);
-
-        // TODO make do-afters account for fixtures in the range check.
-        if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
-        {
-            var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
-            _popup.PopupClient(message, user, user);
-            return (false, true);
-        }
-
-        var forceFeed = user != target;
-        if (forceFeed)
-        {
-            var userName = Identity.Entity(user, EntityManager);
-            _popup.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
-                user, target);
-
-            // logging
-            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
-        }
-        else
-        {
-            // log voluntary eating
-            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
-        }
-
-        var doAfterArgs = new DoAfterArgs(EntityManager,
-            user,
-            forceFeed ? foodComp.ForceFeedDelay : foodComp.Delay,
-            new ConsumeDoAfterEvent(foodComp.Solution, flavors),
-            eventTarget: food,
-            target: target,
-            used: food)
-        {
-            BreakOnHandChange = false,
-            BreakOnMove = forceFeed,
-            BreakOnDamage = true,
-            MovementThreshold = 0.01f,
-            DistanceThreshold = MaxFeedDistance,
-            // 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),
-        };
-
-        _doAfter.TryStartDoAfter(doAfterArgs);
-        return (true, true);
-    }
-
-    private void OnDoAfter(Entity<FoodComponent> entity, ref ConsumeDoAfterEvent args)
-    {
-        if (args.Cancelled || args.Handled || entity.Comp.Deleted || args.Target == null)
-            return;
-
-        if (!TryComp<BodyComponent>(args.Target.Value, out var body))
+        if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
             return;
 
-        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((args.Target.Value, body), out var stomachs))
+        if (!_ingestion.TryGetIngestionVerb(user, entity, IngestionSystem.Food, out var verb))
             return;
 
-        if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
-            return;
+        args.Verbs.Add(verb);
+    }
 
-        if (!TryGetRequiredUtensils(args.User, entity.Comp, out var utensils))
+    private void OnBeforeFoodEaten(Entity<FoodComponent> food, ref BeforeIngestedEvent args)
+    {
+        if (args.Cancelled || args.Solution is not { } solution)
             return;
 
-        // TODO this should really be checked every tick.
-        if (IsMouthBlocked(args.Target.Value))
-            return;
+        // Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
+        args.Transfer = food.Comp.TransferAmount ?? solution.Volume;
+    }
 
-        // TODO this should really be checked every tick.
-        if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value))
+    private void OnFoodEaten(Entity<FoodComponent> entity, ref IngestedEvent args)
+    {
+        if (args.Handled)
             return;
 
-        var forceFeed = args.User != args.Target;
-
         args.Handled = true;
-        var transferAmount = entity.Comp.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) entity.Comp.TransferAmount, solution.Volume) : solution.Volume;
-
-        var split = _solutionContainer.SplitSolution(soln.Value, transferAmount);
 
-        // Get the stomach with the highest available solution volume
-        var highestAvailable = FixedPoint2.Zero;
-        Entity<StomachComponent>? stomachToUse = null;
-        foreach (var ent in stomachs)
-        {
-            var owner = ent.Owner;
-            if (!_stomach.CanTransferSolution(owner, split, ent.Comp1))
-                continue;
-
-            if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref ent.Comp1.Solution, out var stomachSol))
-                continue;
-
-            if (stomachSol.AvailableVolume <= highestAvailable)
-                continue;
-
-            stomachToUse = ent;
-            highestAvailable = stomachSol.AvailableVolume;
-        }
-
-        // No stomach so just popup a message that they can't eat.
-        if (stomachToUse == null)
-        {
-            _solutionContainer.TryAddSolution(soln.Value, split);
-            _popup.PopupClient(forceFeed ? Loc.GetString("food-system-you-cannot-eat-any-more-other", ("target", args.Target.Value)) : Loc.GetString("food-system-you-cannot-eat-any-more"), args.Target.Value, args.User);
-            return;
-        }
+        _audio.PlayPredicted(entity.Comp.UseSound, args.Target, args.User, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
 
-        _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
-        _stomach.TryTransferSolution(stomachToUse!.Value.Owner, split, stomachToUse);
+        var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
 
-        var flavors = args.FlavorMessage;
-
-        if (forceFeed)
+        if (args.ForceFed)
         {
-            var targetName = Identity.Entity(args.Target.Value, EntityManager);
+            var targetName = Identity.Entity(args.Target, EntityManager);
             var userName = Identity.Entity(args.User, EntityManager);
-            _popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), entity.Owner, entity.Owner);
+            _popup.PopupEntity(Loc.GetString("edible-force-feed-success", ("user", userName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Food)), ("flavors", flavors)), entity, entity);
 
-            _popup.PopupClient(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
+            _popup.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Food))), args.User, args.User);
 
-            // log successful force feed
-            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}");
+            // log successful forced feeding
+            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity):food}");
         }
         else
         {
             _popup.PopupClient(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
 
             // log successful voluntary eating
-            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
+            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity):food}");
         }
 
-        _audio.PlayPredicted(entity.Comp.UseSound, args.Target.Value, args.User, entity.Comp.UseSound.Params.WithVolume(-1f).WithVariation(0.20f));
-
-        // Try to break all used utensils
-        foreach (var utensil in utensils)
+        // BREAK OUR UTENSILS
+        if (_ingestion.TryGetUtensils(args.User, entity, out var utensils))
         {
-            _utensil.TryBreak(utensil, args.User);
-        }
-
-        args.Repeat = !forceFeed;
-
-        if (TryComp<StackComponent>(entity, out var stack))
-        {
-            //Not deleting whole stack piece will make troubles with grinding object
-            if (stack.Count > 1)
+            foreach (var utensil in utensils)
             {
-                _stack.SetCount(entity.Owner, stack.Count - 1);
-                _solutionContainer.TryAddSolution(soln.Value, split);
-                return;
+                _ingestion.TryBreak(utensil, args.User);
             }
         }
-        else if (GetUsesRemaining(entity.Owner, entity.Comp) > 0)
+
+        if (_ingestion.GetUsesRemaining(entity, entity.Comp.Solution, args.Split.Volume) > 0)
         {
+            // Leave some of the consumer's DNA on the consumed item...
+            var ev = new TransferDnaEvent
+            {
+                Donor = args.Target,
+                Recipient = entity,
+                CanDnaBeCleaned = false,
+            };
+            RaiseLocalEvent(args.Target, ref ev);
+
+            args.Repeat = !args.ForceFed;
             return;
         }
 
-        // don't try to repeat if its being deleted
-        args.Repeat = false;
-        DeleteAndSpawnTrash(entity.Comp, entity.Owner, args.User);
+        // Food is always destroyed...
+        args.Destroy = true;
     }
 
-    public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityUid user)
+    private void OnFoodFullyEaten(Entity<FoodComponent> food, ref FullyEatenEvent args)
     {
-        var ev = new BeforeFullyEatenEvent
-        {
-            User = user
-        };
-        RaiseLocalEvent(food, ev);
-        if (ev.Cancelled)
-            return;
-
-        var attemptEv = new DestructionAttemptEvent();
-        RaiseLocalEvent(food, attemptEv);
-        if (attemptEv.Cancelled)
+        if (food.Comp.Trash.Count == 0)
             return;
 
-        var afterEvent = new AfterFullyEatenEvent(user);
-        RaiseLocalEvent(food, ref afterEvent);
-
-        var dev = new DestructionEventArgs();
-        RaiseLocalEvent(food, dev);
-
-        if (component.Trash.Count == 0)
-        {
-            PredictedQueueDel(food);
-            return;
-        }
-
-        //We're empty. Become trash.
-        //cache some data as we remove food, before spawning trash and passing it to the hand.
-
         var position = _transform.GetMapCoordinates(food);
-        var trashes = component.Trash;
-        var tryPickup = _hands.IsHolding(user, food, out _);
+        var trashes = food.Comp.Trash;
+        var tryPickup = _hands.IsHolding(args.User, food, out _);
 
-        PredictedDel(food);
         foreach (var trash in trashes)
         {
             var spawnedTrash = EntityManager.PredictedSpawn(trash, position);
@@ -362,192 +177,77 @@ public sealed class FoodSystem : EntitySystem
             if (tryPickup)
             {
                 // Put the trash in the user's hand
-                _hands.TryPickupAnyHand(user, spawnedTrash);
+                _hands.TryPickupAnyHand(args.User, spawnedTrash);
             }
         }
     }
 
-    private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
+    private void OnFood(Entity<FoodComponent> food, ref EdibleEvent args)
     {
-        if (entity.Owner == ev.User ||
-            !ev.CanInteract ||
-            !ev.CanAccess ||
-            !TryComp<BodyComponent>(ev.User, out var body) ||
-            !_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
+        if (args.Cancelled)
             return;
 
-        // have to kill mouse before eating it
-        if (_mobState.IsAlive(entity) && entity.Comp.RequireDead)
+        if (args.Cancelled || args.Solution != null)
             return;
 
-        // only give moths eat verb for clothes since it would just fail otherwise
-        if (!IsDigestibleBy(entity, entity.Comp, stomachs))
+        if (food.Comp.UtensilRequired && !_ingestion.HasRequiredUtensils(args.User, food.Comp.Utensil))
+        {
+            args.Cancelled = true;
             return;
+        }
 
-        var user = ev.User;
-        AlternativeVerb verb = new()
-        {
-            Act = () =>
-            {
-                TryFeed(user, user, entity, entity.Comp);
-            },
-            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png")),
-            Text = Loc.GetString("food-system-verb-eat"),
-            Priority = -1
-        };
-
-        ev.Verbs.Add(verb);
+        // Check this last
+        _solutionContainer.TryGetSolution(food.Owner, food.Comp.Solution, out args.Solution);
+        args.Time += TimeSpan.FromSeconds(food.Comp.Delay);
     }
 
-    /// <summary>
-    ///     Returns true if the food item can be digested by the user.
-    /// </summary>
-    public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null)
+    private void OnGetUtensils(Entity<FoodComponent> entity, ref GetUtensilsEvent args)
     {
-        if (!Resolve(food, ref foodComp, false))
-            return false;
-
-        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(uid, out var stomachs))
-            return false;
+        if (entity.Comp.Utensil == UtensilType.None)
+            return;
 
-        return IsDigestibleBy(food, foodComp, stomachs);
+        if (entity.Comp.UtensilRequired)
+            args.AddRequiredTypes(entity.Comp.Utensil);
+        else
+            args.Types |= entity.Comp.Utensil;
     }
 
-    /// <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>
-    private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<Entity<StomachComponent, OrganComponent>> stomachs)
+    // TODO: When DrinkComponent and FoodComponent are properly obseleted, make the IsDigestionBools in IngestionSystem private again.
+    private void OnIsFoodDigestible(Entity<FoodComponent> ent, ref IsDigestibleEvent args)
     {
-        var digestible = true;
-
-        // Does the mob have enough stomachs?
-        if (stomachs.Count < component.RequiredStomachs)
-            return false;
-
-        // Run through the mobs' stomachs
-        foreach (var ent in stomachs)
-        {
-            // Find a stomach with a SpecialDigestible
-            if (ent.Comp1.SpecialDigestible == null)
-                continue;
-            // Check if the food is in the whitelist
-            if (_whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
-                return true;
-
-            // If their diet is whitelist exclusive, then they cannot eat anything but what follows their whitelisted tags. Else, they can eat their tags AND human food.
-            if (ent.Comp1.IsSpecialDigestibleExclusive)
-                return false;
-        }
-
-        if (component.RequiresSpecialDigestion)
-            return false;
+        if (ent.Comp.RequireDead && _mobState.IsAlive(ent))
+            return;
 
-        return digestible;
+        args.AddDigestible(ent.Comp.RequiresSpecialDigestion);
     }
 
-    private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,
-        out List<EntityUid> utensils, HandsComponent? hands = null)
+    private void OnGetEdibleType(Entity<FoodComponent> ent, ref GetEdibleTypeEvent args)
     {
-        utensils = new List<EntityUid>();
-
-        if (component.Utensil == UtensilType.None)
-            return true;
-
-        if (!Resolve(user, ref hands, false))
-            return true; //mice
-
-        var usedTypes = UtensilType.None;
-
-        foreach (var item in _hands.EnumerateHeld((user, hands)))
-        {
-            // Is utensil?
-            if (!TryComp<UtensilComponent>(item, out var utensil))
-                continue;
-
-            if ((utensil.Types & component.Utensil) != 0 && // Acceptable type?
-                (usedTypes & utensil.Types) != utensil.Types) // Type is not used already? (removes usage of identical utensils)
-            {
-                // Add to used list
-                usedTypes |= utensil.Types;
-                utensils.Add(item);
-            }
-        }
-
-        // If "required" field is set, try to block eating without proper utensils used
-        if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil)
-        {
-            _popup.PopupClient(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
-            return false;
-        }
+        if (args.Type != null)
+            return;
 
-        return true;
+        args.SetPrototype(IngestionSystem.Food);
     }
 
-    /// <summary>
-    ///     Block ingestion attempts based on the equipped mask or head-wear
-    /// </summary>
-    private void OnInventoryIngestAttempt(Entity<InventoryComponent> entity, ref IngestionAttemptEvent args)
+    private void OnBeforeFullySliced(Entity<FoodComponent> food, ref BeforeFullySlicedEvent args)
     {
-        if (args.Cancelled)
+        if (food.Comp.Trash.Count == 0)
             return;
 
-        IngestionBlockerComponent? blocker;
-
-        if (_inventory.TryGetSlotEntity(entity.Owner, "mask", out var maskUid) &&
-            TryComp(maskUid, out blocker) &&
-            blocker.Enabled)
-        {
-            args.Blocker = maskUid;
-            args.Cancel();
-            return;
-        }
+        var position = _transform.GetMapCoordinates(food);
+        var trashes = food.Comp.Trash;
+        var tryPickup = _hands.IsHolding(args.User, food, out _);
 
-        if (_inventory.TryGetSlotEntity(entity.Owner, "head", out var headUid) &&
-            TryComp(headUid, out blocker) &&
-            blocker.Enabled)
+        foreach (var trash in trashes)
         {
-            args.Blocker = headUid;
-            args.Cancel();
-        }
-    }
-
+            var spawnedTrash = EntityManager.PredictedSpawn(trash, position);
 
-    /// <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.PopupClient(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
-                uid, popupUid.Value);
+            // If the user is holding the item
+            if (tryPickup)
+            {
+                // Put the trash in the user's hand
+                _hands.TryPickupAnyHand(args.User, spawnedTrash);
+            }
         }
-
-        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;
-
-        if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out _, out var solution) || solution.Volume == 0)
-            return 0;
-
-        // eat all in 1 go, so non empty is 1 bite
-        if (comp.TransferAmount == null)
-            return 1;
-
-        return Math.Max(1, (int) Math.Ceiling((solution.Volume / (FixedPoint2) comp.TransferAmount).Float()));
     }
 }
diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionBlockerSystem.cs
deleted file mode 100644 (file)
index f9cd233..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-using Content.Shared.Clothing;
-using Content.Shared.Nutrition.Components;
-
-namespace Content.Shared.Nutrition.EntitySystems;
-
-public sealed class IngestionBlockerSystem : EntitySystem
-{
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<IngestionBlockerComponent, ItemMaskToggledEvent>(OnBlockerMaskToggled);
-    }
-
-    private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
-    {
-        ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
-    }
-}
diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs
new file mode 100644 (file)
index 0000000..3a8ef33
--- /dev/null
@@ -0,0 +1,430 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.Components.SolutionManager;
+using Content.Shared.Chemistry.Reagent;
+using Content.Shared.EntityEffects.Effects;
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Nutrition.Prototypes;
+using Content.Shared.Verbs;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Nutrition.EntitySystems;
+
+/// <summary>
+/// Public API for Ingestion System so you can build your own form of ingestion system.
+/// </summary>
+public sealed partial class IngestionSystem
+{
+    // List of prototypes that other components or systems might want.
+    public static readonly ProtoId<EdiblePrototype> Food = "Food";
+    public static readonly ProtoId<EdiblePrototype> Drink = "Drink";
+
+    public const float MaxFeedDistance = 1.0f; // We should really have generic interaction ranges like short, medium, long and use those instead...
+    // BodySystem has no way of telling us where the mouth is so we're making some assumptions.
+    public const SlotFlags DefaultFlags = SlotFlags.HEAD | SlotFlags.MASK;
+
+    #region Ingestion
+
+    /// <summary>
+    /// An entity is trying to ingest another entity in Space Station 14!!!
+    /// </summary>
+    /// <param name="user">The entity who is eating.</param>
+    /// <param name="ingested">The entity that is trying to be ingested.</param>
+    /// <returns>Returns true if we are now ingesting the item.</returns>
+    public bool TryIngest(EntityUid user, EntityUid ingested)
+    {
+        return TryIngest(user, user, ingested);
+    }
+
+    /// <inheritdoc cref="TryIngest(EntityUid,EntityUid)"/>
+    /// <summary>Overload of TryIngest for if an entity is trying to make another entity ingest an entity</summary>
+    /// <param name="user">The entity who is trying to make this happen.</param>
+    /// <param name="target">The entity who is being made to ingest something.</param>
+    /// <param name="ingested">The entity that is trying to be ingested.</param>
+    public bool TryIngest(EntityUid user, EntityUid target, EntityUid ingested)
+    {
+        return AttemptIngest(user, target, ingested, true);
+    }
+
+    /// <summary>
+    /// Checks if we can ingest a given entity without actually ingesting it.
+    /// </summary>
+    /// <param name="user">The entity doing the ingesting.</param>
+    /// <param name="ingested">The ingested entity.</param>
+    /// <returns>Returns true if it's possible for the entity to ingest this item.</returns>
+    public bool CanIngest(EntityUid user, EntityUid ingested)
+    {
+        return AttemptIngest(user, user, ingested, false);
+    }
+
+    /// <summary>
+    ///     Check whether we have an open pie-hole that's in range.
+    /// </summary>
+    /// <param name="user">The one performing the action</param>
+    /// <param name="target">The target whose mouth is checked</param>
+    /// <returns></returns>
+    public bool HasMouthAvailable(EntityUid user, EntityUid target)
+    {
+        return HasMouthAvailable(user, target, DefaultFlags);
+    }
+
+    /// <inheritdoc cref="HasMouthAvailable(EntityUid, EntityUid)"/>
+    /// Overflow which takes custom flags for a mouth being blocked, in case the entity has a mouth not on the face.
+    public bool HasMouthAvailable(EntityUid user, EntityUid target, SlotFlags flags)
+    {
+        if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
+        {
+            var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
+            _popup.PopupClient(message, user, user);
+            return false;
+        }
+
+        var attempt = new IngestionAttemptEvent(flags);
+        RaiseLocalEvent(target, ref attempt);
+
+        if (!attempt.Cancelled)
+            return true;
+
+        if (attempt.Blocker != null)
+            _popup.PopupClient(Loc.GetString("ingestion-remove-mask", ("entity", attempt.Blocker.Value)), target, user);
+
+        return false;
+    }
+
+    /// <inheritdoc cref="CanConsume(EntityUid,EntityUid)"/>
+    /// <param name="user">The entity that is consuming</param>
+    /// <param name="ingested">The entity that is being consumed</param>
+    public bool CanConsume(EntityUid user, EntityUid ingested)
+    {
+        return CanConsume(user, user, ingested, out _, out _);
+    }
+
+    /// <summary>
+    ///     Checks if we can feed an edible solution from an entity to a target.
+    /// </summary>
+    /// <param name="user">The one doing the feeding</param>
+    /// <param name="target">The one being fed.</param>
+    /// <param name="ingested">The food item being eaten.</param>
+    /// <returns>Returns true if the user can feed the target with the ingested entity</returns>
+    public bool CanConsume(EntityUid user, EntityUid target, EntityUid ingested)
+    {
+        return CanConsume(user, target, ingested, out _, out _);
+    }
+
+    /// <inheritdoc cref="CanConsume(EntityUid,EntityUid,EntityUid)"/>
+    /// <param name="user">The one doing the feeding</param>
+    /// <param name="target">The one being fed.</param>
+    /// <param name="ingested">The food item being eaten.</param>
+    /// <param name="solution">The solution we will be consuming from.</param>
+    /// <param name="time">The time it takes us to eat this entity if any.</param>
+    /// <returns>Returns true if the user can feed the target with the ingested entity and also returns a solution</returns>
+    public bool CanConsume(EntityUid user,
+        EntityUid target,
+        EntityUid ingested,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? solution,
+        out TimeSpan? time)
+    {
+        solution = null;
+        time = null;
+
+        if (!HasMouthAvailable(user, target))
+            return false;
+
+        // If we don't have the tools to eat we can't eat.
+        return CanAccessSolution(ingested, user, out solution, out time);
+    }
+
+    #endregion
+
+    #region EdibleComponent
+
+    public void SpawnTrash(Entity<EdibleComponent> entity, EntityUid user)
+    {
+        if (entity.Comp.Trash.Count == 0)
+            return;
+
+        var position = _transform.GetMapCoordinates(entity);
+        var trashes = entity.Comp.Trash;
+        var tryPickup = _hands.IsHolding(user, entity, out _);
+
+        foreach (var trash in trashes)
+        {
+            var spawnedTrash = EntityManager.PredictedSpawn(trash, position);
+
+            // If the user is holding the item
+            if (tryPickup)
+            {
+                // Put the trash in the user's hand
+                _hands.TryPickupAnyHand(user, spawnedTrash);
+            }
+        }
+    }
+
+    public FixedPoint2 EdibleVolume(Entity<EdibleComponent> entity)
+    {
+        if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
+            return FixedPoint2.Zero;
+
+        return solution.Volume;
+    }
+
+    public bool IsEmpty(Entity<EdibleComponent> entity)
+    {
+        return EdibleVolume(entity) == FixedPoint2.Zero;
+    }
+
+    /// <summary>
+    /// Gets the total metabolizable nutrition from an entity, checks first if we can metabolize it.
+    /// If we can't then it's not worth any nutrition.
+    /// </summary>
+    /// <param name="entity">The consumed entity</param>
+    /// <param name="consumer">The entity doing the consuming</param>
+    /// <returns>The amount of nutrition the consumable is worth</returns>
+    public float TotalNutrition(Entity<EdibleComponent?> entity, EntityUid consumer)
+    {
+        if (!CanIngest(consumer, entity))
+            return 0f;
+
+        return TotalNutrition(entity);
+    }
+
+    /// <summary>
+    /// Gets the total metabolizable nutrition from an entity, assumes we can eat and metabolize it.
+    /// </summary>
+    /// <param name="entity">The consumed entity</param>
+    /// <returns>The amount of nutrition the consumable is worth</returns>
+    public float TotalNutrition(Entity<EdibleComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return 0f;
+
+        if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
+            return 0f;
+
+        var total = 0f;
+        foreach (var quantity in solution.Contents)
+        {
+            var reagent = _proto.Index<ReagentPrototype>(quantity.Reagent.Prototype);
+            if (reagent.Metabolisms == null)
+                continue;
+
+            foreach (var entry in reagent.Metabolisms.Values)
+            {
+                foreach (var effect in entry.Effects)
+                {
+                    // ignores any effect conditions, just cares about how much it can hydrate
+                    if (effect is SatiateHunger hunger)
+                    {
+                        total += hunger.NutritionFactor * quantity.Quantity.Float();
+                    }
+                }
+            }
+        }
+
+        return total;
+    }
+
+    /// <summary>
+    /// Gets the total metabolizable hydration from an entity, checks first if we can metabolize it.
+    /// If we can't then it's not worth any hydration.
+    /// </summary>
+    /// <param name="entity">The consumed entity</param>
+    /// <param name="consumer">The entity doing the consuming</param>
+    /// <returns>The amount of hydration the consumable is worth</returns>
+    public float TotalHydration(Entity<EdibleComponent?> entity, EntityUid consumer)
+    {
+        if (!CanIngest(consumer, entity))
+            return 0f;
+
+        return TotalNutrition(entity);
+    }
+
+    /// <summary>
+    /// Gets the total metabolizable hydration from an entity, assumes we can eat and metabolize it.
+    /// </summary>
+    /// <param name="entity">The consumed entity</param>
+    /// <returns>The amount of hydration the consumable is worth</returns>
+    public float TotalHydration(Entity<EdibleComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return 0f;
+
+        if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
+            return 0f;
+
+        var total = 0f;
+        foreach (var quantity in solution.Contents)
+        {
+            var reagent = _proto.Index<ReagentPrototype>(quantity.Reagent.Prototype);
+            if (reagent.Metabolisms == null)
+                continue;
+
+            foreach (var entry in reagent.Metabolisms.Values)
+            {
+                foreach (var effect in entry.Effects)
+                {
+                    // ignores any effect conditions, just cares about how much it can hydrate
+                    if (effect is SatiateThirst thirst)
+                    {
+                        total += thirst.HydrationFactor * quantity.Quantity.Float();
+                    }
+                }
+            }
+        }
+
+        return total;
+    }
+
+    #endregion
+
+    #region Solutions
+
+    /// <summary>
+    /// Checks if the item is currently edible.
+    /// </summary>
+    /// <param name="ingested">Entity being ingested</param>
+    /// <param name="user">The entity trying to make the ingestion happening, not necessarily the one eating</param>
+    /// <param name="solution">Solution we're returning</param>
+    /// <param name="time">The time it takes us to eat this entity</param>
+    public bool CanAccessSolution(Entity<SolutionContainerManagerComponent?> ingested,
+        EntityUid user,
+        [NotNullWhen(true)] out Entity<SolutionComponent>? solution,
+        out TimeSpan? time)
+    {
+        solution = null;
+        time = null;
+
+        if (!Resolve(ingested, ref ingested.Comp))
+        {
+            _popup.PopupClient(Loc.GetString("ingestion-try-use-is-empty", ("entity", ingested)), ingested, user);
+            return false;
+        }
+
+        var ev = new EdibleEvent(user);
+        RaiseLocalEvent(ingested, ref ev);
+
+        solution = ev.Solution;
+        time = ev.Time;
+
+        return !ev.Cancelled && solution != null;
+    }
+
+    /// <summary>
+    /// Estimate 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, string solutionName, FixedPoint2 splitVol)
+    {
+        if (!_solutionContainer.TryGetSolution(uid, solutionName, out _, out var solution) || solution.Volume == 0)
+            return 0;
+
+        return Math.Max(1, (int) Math.Ceiling((solution.Volume / splitVol).Float()));
+    }
+
+    #endregion
+
+    #region Edible Types
+
+    /// <summary>
+    /// Tries to get the ingestion verbs for a given user entity and ingestible entity
+    /// </summary>
+    /// <param name="user">The one getting the verbs who would be doing the eating.</param>
+    /// <param name="ingested">Entity being ingested.</param>
+    /// <param name="type">Edible prototype.</param>
+    /// <param name="verb">Verb we're returning.</param>
+    /// <returns>Returns true if we generated a verb.</returns>
+    public bool TryGetIngestionVerb(EntityUid user, EntityUid ingested, [ForbidLiteral] ProtoId<EdiblePrototype> type, [NotNullWhen(true)] out AlternativeVerb? verb)
+    {
+        verb = null;
+
+        // We want to see if we can ingest this item, but we don't actually want to ingest it.
+        if (!CanIngest(user, ingested))
+            return false;
+
+        var proto = _proto.Index(type);
+
+        verb = new()
+        {
+            Act = () =>
+            {
+                TryIngest(user, user, ingested);
+            },
+            Icon = proto.VerbIcon,
+            Text = Loc.GetString(proto.VerbName),
+            Priority = 2
+        };
+
+        return true;
+    }
+
+    /// <summary>
+    /// Returns the most accurate edible prototype for an entity if one exists.
+    /// </summary>
+    /// <param name="entity">entity who's edible prototype we want</param>
+    /// <returns>The best matching prototype if one exists.</returns>
+    public ProtoId<EdiblePrototype>? GetEdibleType(Entity<EdibleComponent?> entity)
+    {
+        if (Resolve(entity, ref entity.Comp, false))
+            return entity.Comp.Edible;
+
+        var ev = new GetEdibleTypeEvent();
+        RaiseLocalEvent(entity, ref ev);
+
+        return ev.Type;
+    }
+
+    public string GetEdibleNoun(Entity<EdibleComponent?> entity)
+    {
+        if (Resolve(entity, ref entity.Comp, false))
+            return GetProtoVerb(entity.Comp.Edible);
+
+        var ev = new GetEdibleTypeEvent();
+        RaiseLocalEvent(entity, ref ev);
+
+        if (ev.Type == null)
+            return Loc.GetString("edible-noun-edible");
+
+        return GetProtoNoun(ev.Type.Value);
+    }
+
+    public string GetProtoNoun([ForbidLiteral] ProtoId<EdiblePrototype> proto)
+    {
+        var prototype = _proto.Index(proto);
+
+        return GetProtoNoun(prototype);
+    }
+
+    public string GetProtoNoun(EdiblePrototype proto)
+    {
+        return Loc.GetString(proto.Noun);
+    }
+
+    public string GetEdibleVerb(Entity<EdibleComponent?> entity)
+    {
+        if (Resolve(entity, ref entity.Comp, false))
+            return GetProtoVerb(entity.Comp.Edible);
+
+        var ev = new GetEdibleTypeEvent();
+        RaiseLocalEvent(entity, ref ev);
+
+        if (ev.Type == null)
+            return Loc.GetString("edible-verb-edible");
+
+        return GetProtoVerb(ev.Type.Value);
+    }
+
+    public string GetProtoVerb([ForbidLiteral] ProtoId<EdiblePrototype> proto)
+    {
+        var prototype = _proto.Index(proto);
+
+        return GetProtoVerb(prototype);
+    }
+
+    public string GetProtoVerb(EdiblePrototype proto)
+    {
+        return Loc.GetString(proto.Verb);
+    }
+
+    #endregion
+}
diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Blockers.cs
new file mode 100644 (file)
index 0000000..e1bd480
--- /dev/null
@@ -0,0 +1,159 @@
+using System.Linq;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Clothing;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Fluids.Components;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Storage;
+
+namespace Content.Shared.Nutrition.EntitySystems;
+
+public sealed partial class IngestionSystem
+{
+    [Dependency] private readonly OpenableSystem _openable = default!;
+
+    public void InitializeBlockers()
+    {
+        SubscribeLocalEvent<UnremoveableComponent, IngestibleEvent>(OnUnremovableIngestion);
+        SubscribeLocalEvent<IngestionBlockerComponent, ItemMaskToggledEvent>(OnBlockerMaskToggled);
+        SubscribeLocalEvent<IngestionBlockerComponent, IngestionAttemptEvent>(OnIngestionBlockerAttempt);
+        SubscribeLocalEvent<IngestionBlockerComponent, InventoryRelayedEvent<IngestionAttemptEvent>>(OnIngestionBlockerAttempt);
+
+        // Edible Event
+        SubscribeLocalEvent<EdibleComponent, EdibleEvent>(OnEdible);
+        SubscribeLocalEvent<StorageComponent, EdibleEvent>(OnStorageEdible);
+        SubscribeLocalEvent<ItemSlotsComponent, EdibleEvent>(OnItemSlotsEdible);
+        SubscribeLocalEvent<OpenableComponent, EdibleEvent>(OnOpenableEdible);
+
+        // Digestion Events
+        SubscribeLocalEvent<EdibleComponent, IsDigestibleEvent>(OnEdibleIsDigestible);
+        SubscribeLocalEvent<DrainableSolutionComponent, IsDigestibleEvent>(OnDrainableIsDigestible);
+        SubscribeLocalEvent<PuddleComponent, IsDigestibleEvent>(OnPuddleIsDigestible);
+
+        SubscribeLocalEvent<PillComponent, BeforeIngestedEvent>(OnPillBeforeEaten);
+    }
+
+    private void OnUnremovableIngestion(Entity<UnremoveableComponent> entity, ref IngestibleEvent args)
+    {
+        // If we can't remove it we probably shouldn't be able to eat it.
+        // TODO: Separate glue and Unremovable component.
+        args.Cancelled = true;
+    }
+
+    private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
+    {
+        ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
+    }
+
+    private void OnIngestionBlockerAttempt(Entity<IngestionBlockerComponent> entity, ref IngestionAttemptEvent args)
+    {
+        if (!args.Cancelled && entity.Comp.Enabled)
+            args.Cancelled = true;
+    }
+
+    /// <summary>
+    ///     Block ingestion attempts based on the equipped mask or head-wear
+    /// </summary>
+    private void OnIngestionBlockerAttempt(Entity<IngestionBlockerComponent> entity, ref InventoryRelayedEvent<IngestionAttemptEvent> args)
+    {
+        if (args.Args.Cancelled || !entity.Comp.Enabled)
+            return;
+
+        args.Args.Cancelled = true;
+        args.Args.Blocker = entity;
+    }
+
+    private void OnEdible(Entity<EdibleComponent> entity, ref EdibleEvent args)
+    {
+        if (args.Cancelled || args.Solution != null)
+            return;
+
+        if (entity.Comp.UtensilRequired && !HasRequiredUtensils(args.User, entity.Comp.Utensil))
+        {
+            args.Cancelled = true;
+            return;
+        }
+
+        // Check this last
+        if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out args.Solution) || IsEmpty(entity) && !entity.Comp.DestroyOnEmpty)
+        {
+            args.Cancelled = true;
+
+            _popup.PopupClient(Loc.GetString("ingestion-try-use-is-empty", ("entity", entity)), entity, args.User);
+            return;
+        }
+
+        // Time is additive because I said so.
+        args.Time += entity.Comp.Delay;
+    }
+
+    private void OnStorageEdible(Entity<StorageComponent> ent, ref EdibleEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (!ent.Comp.Container.ContainedEntities.Any())
+            return;
+
+        args.Cancelled = true;
+
+        _popup.PopupClient(Loc.GetString("edible-has-used-storage", ("food", ent), ("verb", GetEdibleVerb(ent.Owner))), args.User, args.User);
+    }
+
+    private void OnItemSlotsEdible(Entity<ItemSlotsComponent> ent, ref EdibleEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (!ent.Comp.Slots.Any(slot => slot.Value.HasItem))
+            return;
+
+        args.Cancelled = true;
+
+        _popup.PopupClient(Loc.GetString("edible-has-used-storage", ("food", ent), ("verb", GetEdibleVerb(ent.Owner))), args.User, args.User);
+    }
+
+    private void OnOpenableEdible(Entity<OpenableComponent> ent, ref EdibleEvent args)
+    {
+        if (_openable.IsClosed(ent, args.User, ent.Comp))
+            args.Cancelled = true;
+    }
+
+    private void OnEdibleIsDigestible(Entity<EdibleComponent> ent, ref IsDigestibleEvent args)
+    {
+        if (ent.Comp.RequireDead && _mobState.IsAlive(ent))
+            return;
+
+        args.AddDigestible(ent.Comp.RequiresSpecialDigestion);
+    }
+
+    /// <remarks>
+    /// Both of these assume that having this component means there's nothing stopping you from slurping up
+    /// pure reagent juice with absolutely nothing to stop you.
+    /// </remarks>
+    private void OnDrainableIsDigestible(Entity<DrainableSolutionComponent> ent, ref IsDigestibleEvent args)
+    {
+        args.UniversalDigestion();
+    }
+
+    private void OnPuddleIsDigestible(Entity<PuddleComponent> ent, ref IsDigestibleEvent args)
+    {
+        args.UniversalDigestion();
+    }
+
+    /// <remarks>
+    /// I mean you have to eat the *whole* pill no?
+    /// </remarks>
+    private void OnPillBeforeEaten(Entity<PillComponent> ent, ref BeforeIngestedEvent args)
+    {
+        if (args.Cancelled || args.Solution is not { } sol)
+            return;
+
+        if (args.TryNewMinimum(sol.Volume))
+            return;
+
+        args.Cancelled = true;
+    }
+}
diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs
new file mode 100644 (file)
index 0000000..670fdc8
--- /dev/null
@@ -0,0 +1,157 @@
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.Hands.Components;
+using Content.Shared.Interaction;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Tools.EntitySystems;
+using Robust.Shared.Audio;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Nutrition.EntitySystems;
+
+public sealed partial class IngestionSystem
+{
+    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    private EntityQuery<UtensilComponent> _utensilsQuery;
+
+    public void InitializeUtensils()
+    {
+        SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ToolOpenableSystem) });
+
+        SubscribeLocalEvent<EdibleComponent, GetUtensilsEvent>(OnGetEdibleUtensils);
+
+        _utensilsQuery = GetEntityQuery<UtensilComponent>();
+    }
+
+    /// <summary>
+    /// Clicked with utensil
+    /// </summary>
+    private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
+    {
+        if (ev.Handled || ev.Target == null || !ev.CanReach)
+            return;
+
+        ev.Handled = TryUseUtensil(ev.User, ev.Target.Value, entity);
+    }
+
+    public bool TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
+    {
+        var ev = new GetUtensilsEvent();
+        RaiseLocalEvent(target, ref ev);
+
+        //Prevents food usage with a wrong utensil
+        if ((ev.Types & utensil.Comp.Types) == 0)
+        {
+            _popup.PopupClient(Loc.GetString("ingestion-try-use-wrong-utensil", ("verb", GetEdibleVerb(target)),("food", target), ("utensil", utensil.Owner)), user, user);
+            return true;
+        }
+
+        if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
+            return true;
+
+        return TryIngest(user, user, target);
+    }
+
+    /// <summary>
+    /// Attempt to break the utensil after interaction.
+    /// </summary>
+    /// <param name="entity">Utensil.</param>
+    /// <param name="userUid">User of the utensil.</param>
+    public void TryBreak(Entity<UtensilComponent?> entity, EntityUid userUid)
+    {
+        if (!Resolve(entity, ref entity.Comp))
+            return;
+
+        // TODO: Once we have predicted randomness delete this for something sane...
+        var seed = SharedRandomExtensions.HashCodeCombine(new() {(int)_timing.CurTick.Value, GetNetEntity(entity).Id, GetNetEntity(userUid).Id });
+        var rand = new System.Random(seed);
+
+        if (!rand.Prob(entity.Comp.BreakChance))
+            return;
+
+        _audio.PlayPredicted(entity.Comp.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f));
+        // Not prediced because no random predicted
+        PredictedDel(entity.Owner);
+    }
+
+    /// <summary>
+    /// Checks if we have the utensils required to eat a certain food item.
+    /// </summary>
+    /// <param name="entity">Entity that is trying to eat.</param>
+    /// <param name="food">The types of utensils we need.</param>
+    /// <param name="utensils">The utensils needed to eat the food item.</param>
+    /// <returns>True if we are able to eat the item.</returns>
+    public bool TryGetUtensils(Entity<HandsComponent?> entity, EntityUid food, out List<EntityUid> utensils)
+    {
+        var ev = new GetUtensilsEvent();
+        RaiseLocalEvent(food, ref ev);
+
+        return TryGetUtensils(entity, ev.Types, ev.RequiredTypes, out utensils);
+    }
+
+    public bool TryGetUtensils(Entity<HandsComponent?> entity, UtensilType types, UtensilType requiredTypes, out List<EntityUid> utensils)
+    {
+        utensils = new List<EntityUid>();
+
+        var required = requiredTypes != UtensilType.None;
+
+        // Why are we even here? Just to suffer?
+        if (types == UtensilType.None)
+            return true;
+
+        // If you don't have hands you can eat anything I guess.
+        if (!Resolve(entity, ref entity.Comp, false)) // You aren't allowed to eat with your hands in this hellish dystopia.
+            return true;
+
+        var usedTypes = UtensilType.None;
+
+        foreach (var item in _hands.EnumerateHeld(entity))
+        {
+            // Is utensil?
+            if (!_utensilsQuery.TryComp(item, out var utensil))
+                continue;
+
+            // Do we have a new and unused utensil type?
+            if ((utensil.Types & types) == 0 || (usedTypes & utensil.Types) == utensil.Types)
+                continue;
+
+            // Add to used list
+            usedTypes |= utensil.Types;
+            utensils.Add(item);
+        }
+
+        // If "required" field is set, try to block eating without proper utensils used
+        if (!required || (usedTypes & requiredTypes) == requiredTypes)
+            return true;
+
+        _popup.PopupClient(Loc.GetString("ingestion-you-need-to-hold-utensil", ("utensil", requiredTypes ^ usedTypes)), entity, entity);
+        return false;
+
+    }
+
+    /// <summary>
+    /// Checks if you have the required utensils based on a list of types.
+    /// Note it is assumed if you're calling this method that you need utensils.
+    /// </summary>
+    /// <param name="entity">The entity doing the action who has the utensils.</param>
+    /// <param name="types">The types of utensils we need.</param>
+    /// <returns>Returns true if we have the utensils we need.</returns>
+    public bool HasRequiredUtensils(EntityUid entity, UtensilType types)
+    {
+        return TryGetUtensils(entity, types, types, out _);
+    }
+
+    private void OnGetEdibleUtensils(Entity<EdibleComponent> entity, ref GetUtensilsEvent args)
+    {
+        if (entity.Comp.Utensil == UtensilType.None)
+            return;
+
+        if (entity.Comp.UtensilRequired)
+            args.AddRequiredTypes(entity.Comp.Utensil);
+        else
+            args.Types |= entity.Comp.Utensil;
+    }
+}
diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs
new file mode 100644 (file)
index 0000000..cdd366b
--- /dev/null
@@ -0,0 +1,531 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.Body.Components;
+using Content.Shared.Body.Organ;
+using Content.Shared.Body.Systems;
+using Content.Shared.Chemistry;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Database;
+using Content.Shared.Destructible;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Forensics;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Popups;
+using Content.Shared.Tools.EntitySystems;
+using Content.Shared.Verbs;
+using Content.Shared.Whitelist;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Nutrition.EntitySystems;
+
+/// <remarks>
+/// I was warned about puddle system, I knew the risks with body system, but food and drink system?
+/// Food and Drink system was a sleeping titan, and I walked directly into it's gaping maw.
+/// Between copy-pasted code, strange reliance on systems, being a pillar of chemistry for some reason,
+/// nothing could've prepared me for the horror that I had to endure. I saw the signs, comments of those who
+/// turned back, code that was made to be "just good enough" the fact that I got soaped by soap.yml, but I
+/// ignored them and pressed on.
+/// Let this remark be a reminder to those who come after, that I was here, and that I vanquished a great beast.
+/// Let young little contributors rest easy at night not knowing the horrible system that once lived beneath the
+/// bedrock of the codebase they now commit to.
+/// </remarks>
+/// <summary>
+/// This handles the ingestion of solutions and entities.
+/// </summary>
+public sealed partial class IngestionSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+    [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+    [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
+    [Dependency] private readonly MobStateSystem _mobState = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    // Body Component Dependencies
+    [Dependency] private readonly SharedBodySystem _body = default!;
+    [Dependency] private readonly ReactiveSystem _reaction = default!;
+    [Dependency] private readonly StomachSystem _stomach = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<EdibleComponent, ComponentInit>(OnEdibleInit);
+
+        // Interactions
+        SubscribeLocalEvent<EdibleComponent, UseInHandEvent>(OnUseEdibleInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
+        SubscribeLocalEvent<EdibleComponent, AfterInteractEvent>(OnEdibleInteract, after: new[] { typeof(ToolOpenableSystem) });
+
+        // Generic Eating Handlers
+        SubscribeLocalEvent<EdibleComponent, BeforeIngestedEvent>(OnBeforeIngested);
+        SubscribeLocalEvent<EdibleComponent, IngestedEvent>(OnEdibleIngested);
+        SubscribeLocalEvent<EdibleComponent, FullyEatenEvent>(OnFullyEaten);
+
+        // Body Component eating handler
+        SubscribeLocalEvent<BodyComponent, AttemptIngestEvent>(OnTryIngest);
+        SubscribeLocalEvent<BodyComponent, EatingDoAfterEvent>(OnEatingDoAfter);
+
+        // Verbs
+        SubscribeLocalEvent<EdibleComponent, GetVerbsEvent<AlternativeVerb>>(AddEdibleVerbs);
+        SubscribeLocalEvent<EdibleComponent, SolutionContainerChangedEvent>(OnSolutionContainerChanged);
+
+        // Misc
+        SubscribeLocalEvent<EdibleComponent, AttemptShakeEvent>(OnAttemptShake);
+        SubscribeLocalEvent<EdibleComponent, BeforeFullySlicedEvent>(OnBeforeFullySliced);
+
+        InitializeBlockers();
+        InitializeUtensils();
+    }
+
+    /// <summary>
+    /// Eat or drink an item
+    /// </summary>
+    private void OnUseEdibleInHand(Entity<EdibleComponent> entity, ref UseInHandEvent ev)
+    {
+        if (ev.Handled)
+            return;
+
+        ev.Handled = TryIngest(ev.User, entity);
+    }
+
+    /// <summary>
+    /// Feed someone else
+    /// </summary>
+    private void OnEdibleInteract(Entity<EdibleComponent> entity, ref AfterInteractEvent args)
+    {
+        if (args.Handled || args.Target == null || !args.CanReach)
+            return;
+
+        args.Handled = TryIngest(args.User, args.Target.Value, entity);
+    }
+
+    /// <summary>Raises events to see if it's possible to ingest </summary>
+    /// <param name="user">The entity who is trying to make this happen.</param>
+    /// <param name="target">The entity who is being made to ingest something.</param>
+    /// <param name="ingested">The entity that is trying to be ingested.</param>
+    /// <param name="ingest">Bool that determines whethere this is a Try or a Can effectively.
+    /// When set to true, it tries to ingest, when false it checks if we can.</param>
+    /// <returns>Returns true if we can ingest the item.</returns>
+    private bool AttemptIngest(EntityUid user, EntityUid target, EntityUid ingested, bool ingest)
+    {
+        var eatEv = new IngestibleEvent();
+        RaiseLocalEvent(ingested, ref eatEv);
+
+        if (eatEv.Cancelled)
+            return false;
+
+        var ingestionEv = new AttemptIngestEvent(user, ingested, ingest);
+        RaiseLocalEvent(target, ref ingestionEv);
+
+        return ingestionEv.Handled;
+    }
+
+    private void OnEdibleInit(Entity<EdibleComponent> entity, ref ComponentInit args)
+    {
+        // TODO: When Food and Drink component are kill make sure to nuke both TryComps and just have it update appearance...
+        // Beakers, Soap and other items have drainable, and we should be able to eat that solution...
+        // If I could make drainable properly support sound effects and such I'd just have it use TryIngest itself
+        // Does this exist just to make tests fail? That way you have the proper yaml???
+        if (TryComp<DrainableSolutionComponent>(entity, out var existingDrainable))
+            entity.Comp.Solution = existingDrainable.Solution;
+
+        UpdateAppearance(entity);
+
+        if (TryComp(entity, out RefillableSolutionComponent? refillComp))
+            refillComp.Solution = entity.Comp.Solution;
+    }
+
+    #region Appearance System
+
+    public void UpdateAppearance(Entity<EdibleComponent, AppearanceComponent?> entity)
+    {
+        if (!Resolve(entity, ref entity.Comp2, false))
+            return;
+
+        var drainAvailable = EdibleVolume(entity);
+        _appearance.SetData(entity, FoodVisuals.Visual, drainAvailable.Float(), entity.Comp2);
+    }
+
+    private void OnSolutionContainerChanged(Entity<EdibleComponent> entity, ref SolutionContainerChangedEvent args)
+    {
+        UpdateAppearance(entity);
+    }
+
+    #endregion
+
+    #region BodySystem
+
+    // TODO: The IsDigestibleBy bools should be API but they're too specific to the BodySystem to be API. Requires BodySystem rework.
+    /// <summary>
+    /// Generic method which takes a list of stomachs, and checks if a given food item passes any stomach's whitelist
+    /// in a given list of stomachs.
+    /// </summary>
+    /// <param name="food">Entity being eaten</param>
+    /// <param name="stomachs">Stomachs available to digest</param>
+    public bool IsDigestibleBy(EntityUid food, List<Entity<StomachComponent, OrganComponent>> stomachs)
+    {
+        var ev = new IsDigestibleEvent();
+        RaiseLocalEvent(food, ref ev);
+
+        if (!ev.Digestible)
+            return false;
+
+        if (ev.Universal)
+            return true;
+
+        if (ev.SpecialDigestion)
+        {
+            foreach (var ent in stomachs)
+            {
+                // We need one stomach that can digest our special food.
+                if (_whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
+                    return true;
+            }
+        }
+        else
+        {
+            foreach (var ent in stomachs)
+            {
+                // We need one stomach that can digest normal food.
+                if (ent.Comp1.SpecialDigestible == null
+                    || !ent.Comp1.IsSpecialDigestibleExclusive
+                    || _whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
+                    return true;
+            }
+        }
+
+        // If we didn't find a stomach that can digest our food then it doesn't exist.
+        return false;
+    }
+
+    /// <summary>
+    /// Generic method which takes a single stomach into account, and checks if a given food item passes a stomach whitelist.
+    /// </summary>
+    /// <param name="food">Entity being eaten</param>
+    /// <param name="stomach">Stomachs that is attempting to digest.</param>
+    public bool IsDigestibleBy(EntityUid food, Entity<StomachComponent, OrganComponent> stomach)
+    {
+        var ev = new IsDigestibleEvent();
+        RaiseLocalEvent(food, ref ev);
+
+        if (!ev.Digestible)
+            return false;
+
+        if (ev.Universal)
+            return true;
+
+        if (ev.SpecialDigestion)
+            return _whitelistSystem.IsWhitelistPass(stomach.Comp1.SpecialDigestible, food);
+
+        if (stomach.Comp1.SpecialDigestible == null || !stomach.Comp1.IsSpecialDigestibleExclusive || _whitelistSystem.IsWhitelistPass(stomach.Comp1.SpecialDigestible, food))
+            return true;
+
+        return false;
+    }
+
+    private void OnTryIngest(Entity<BodyComponent> entity, ref AttemptIngestEvent args)
+    {
+        var food = args.Ingested;
+        var forceFed = args.User != entity.Owner;
+
+        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(entity!, out var stomachs))
+            return;
+
+        // Can we digest the specific item we're trying to eat?
+        if (!IsDigestibleBy(args.Ingested, stomachs))
+        {
+            if (forceFed)
+            {
+                _popup.PopupClient(Loc.GetString("ingestion-cant-digest-other", ("target", entity), ("entity", food)), entity, args.User);
+            }
+            else
+                _popup.PopupClient(Loc.GetString("ingestion-cant-digest", ("entity", food)), entity, entity);
+
+            return;
+        }
+
+
+        // Exit early if we're just trying to get verbs
+        if (!args.Ingest)
+        {
+            args.Handled = true;
+            return;
+        }
+
+        // Check if despite being able to digest the item something is blocking us from eating.
+        if (!CanConsume(args.User, entity, args.Ingested, out var solution, out var time))
+            return;
+
+        if (!_doAfter.TryStartDoAfter(GetEdibleDoAfterArgs(args.User, entity, food, time ?? TimeSpan.Zero)))
+            return;
+
+        args.Handled = true;
+        var foodSolution = solution.Value.Comp.Solution;
+
+        if (forceFed)
+        {
+            var userName = Identity.Entity(args.User, EntityManager);
+            _popup.PopupEntity(Loc.GetString("edible-force-feed", ("user", userName), ("verb", GetEdibleVerb(food))), args.User, entity);
+
+            // logging
+            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(args.User):user} is forcing {ToPrettyString(entity):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
+        }
+        else
+        {
+            // log voluntary eating
+            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(entity):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
+        }
+    }
+
+    private void OnEatingDoAfter(Entity<BodyComponent> entity, ref EatingDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled || entity.Comp.Deleted || args.Target == null)
+            return;
+
+        var food = args.Target.Value;
+
+        var blockerEv = new IngestibleEvent();
+        RaiseLocalEvent(food, ref blockerEv);
+
+        if (blockerEv.Cancelled)
+            return;
+
+        if (!CanConsume(args.User, entity, food, out var solution, out _))
+            return;
+
+        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(entity!, out var stomachs))
+            return;
+
+        var forceFed = args.User != entity.Owner;
+
+        var highestAvailable = FixedPoint2.Zero;
+        Entity<StomachComponent>? stomachToUse = null;
+        foreach (var ent in stomachs)
+        {
+            var owner = ent.Owner;
+            if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref ent.Comp1.Solution, out var stomachSol))
+                continue;
+
+            if (stomachSol.AvailableVolume <= highestAvailable)
+                continue;
+
+            if (!IsDigestibleBy(food, ent))
+                continue;
+
+            stomachToUse = ent;
+            highestAvailable = stomachSol.AvailableVolume;
+        }
+
+        // All stomachs are full or we have no stomachs
+        if (stomachToUse == null)
+        {
+            // Very long
+            _popup.PopupClient(Loc.GetString("ingestion-you-cannot-ingest-any-more", ("verb", GetEdibleVerb(food))), entity, entity);
+            if (!forceFed)
+                return;
+
+            _popup.PopupClient(Loc.GetString("ingestion-other-cannot-ingest-any-more", ("target", entity), ("verb", GetEdibleVerb(food))),  args.Target.Value, args.User);
+            return;
+        }
+
+        var beforeEv = new BeforeIngestedEvent(FixedPoint2.Zero, highestAvailable, solution.Value.Comp.Solution);
+        RaiseLocalEvent(food, ref beforeEv);
+        RaiseLocalEvent(entity, ref beforeEv);
+
+        if (beforeEv.Cancelled || beforeEv.Min > beforeEv.Max)
+        {
+            // Very long x2
+            _popup.PopupClient(Loc.GetString("ingestion-you-cannot-ingest-any-more", ("verb", GetEdibleVerb(food))), entity, entity);
+            if (!forceFed)
+                return;
+
+            _popup.PopupClient(Loc.GetString("ingestion-other-cannot-ingest-any-more", ("target", entity), ("verb", GetEdibleVerb(food))),  args.Target.Value, args.User);
+            return;
+        }
+
+        var transfer = FixedPoint2.Clamp(beforeEv.Transfer, beforeEv.Min, beforeEv.Max);
+
+        var split = _solutionContainer.SplitSolution(solution.Value, transfer);
+
+        var ingestEv = new IngestingEvent(food, split, forceFed);
+        RaiseLocalEvent(entity, ref ingestEv);
+
+        _reaction.DoEntityReaction(entity, split, ReactionMethod.Ingestion);
+
+        // Everything is good to go item has been successfuly eaten
+        var afterEv = new IngestedEvent(args.User, entity, split, forceFed);
+        RaiseLocalEvent(food, ref afterEv);
+
+        if (afterEv.Refresh)
+            _solutionContainer.TryAddSolution(solution.Value, split);
+
+        _stomach.TryTransferSolution(stomachToUse.Value.Owner, split, stomachToUse);
+
+        if (!afterEv.Destroy)
+        {
+            args.Repeat = afterEv.Repeat;
+            return;
+        }
+
+        var ev = new DestructionAttemptEvent();
+        RaiseLocalEvent(food, ev);
+        if (ev.Cancelled)
+            return;
+
+        // Tell the food that it's time to die.
+        var finishedEv = new FullyEatenEvent(args.User);
+        RaiseLocalEvent(food, ref finishedEv);
+
+        var eventArgs = new DestructionEventArgs();
+        RaiseLocalEvent(food, eventArgs);
+
+        PredictedDel(food);
+
+        // Don't try to repeat if its being deleted
+        args.Repeat = false;
+    }
+
+    /// <summary>
+    /// Gets the DoAfterArgs for the specific event
+    /// </summary>
+    /// <param name="user">Entity that is doing the action.</param>
+    /// <param name="target">Entity that is eating.</param>
+    /// <param name="food">Food entity we're trying to eat.</param>
+    /// <param name="delay">The time delay for our DoAfter</param>
+    /// <returns>Returns true if it was able to successfully start the DoAfter</returns>
+    private DoAfterArgs GetEdibleDoAfterArgs(EntityUid user, EntityUid target, EntityUid food, TimeSpan delay = default)
+    {
+        var forceFeed = user != target;
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, new EatingDoAfterEvent(), target, food)
+        {
+            BreakOnHandChange = false,
+            BreakOnMove = forceFeed,
+            BreakOnDamage = true,
+            MovementThreshold = 0.01f,
+            DistanceThreshold = MaxFeedDistance,
+            // 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),
+        };
+
+        return doAfterArgs;
+    }
+
+    #endregion
+
+    private void OnBeforeIngested(Entity<EdibleComponent> food, ref BeforeIngestedEvent args)
+    {
+        if (args.Cancelled || args.Solution is not { } solution)
+            return;
+
+        // Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
+        args.Transfer = food.Comp.TransferAmount ?? solution.Volume;
+    }
+
+    private void OnEdibleIngested(Entity<EdibleComponent> entity, ref IngestedEvent args)
+    {
+        // This is a lot but there wasn't really a way to separate this from the EdibleComponent otherwise I would've moved it.
+
+        if (args.Handled)
+            return;
+
+        args.Handled = true;
+
+        var edible = _proto.Index(entity.Comp.Edible);
+
+        _audio.PlayPredicted(edible.UseSound, args.Target, args.User);
+
+        var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
+
+        if (args.ForceFed)
+        {
+            var targetName = Identity.Entity(args.Target, EntityManager);
+            var userName = Identity.Entity(args.User, EntityManager);
+            _popup.PopupEntity(Loc.GetString("edible-force-feed-success", ("user", userName), ("verb", edible.Verb), ("flavors", flavors)), entity, entity);
+
+            _popup.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", edible.Verb)), args.User, args.User);
+
+            // log successful forced feeding
+            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity):food}");
+        }
+        else
+        {
+            _popup.PopupClient(Loc.GetString(edible.Message, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
+
+            // log successful voluntary eating
+            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity):food}");
+        }
+
+        // BREAK OUR UTENSILS
+        if (TryGetUtensils(args.User, entity, out var utensils))
+        {
+            foreach (var utensil in utensils)
+            {
+                TryBreak(utensil, args.User);
+            }
+        }
+
+        // This also prevents us from repeating if it's empty
+        if (!IsEmpty(entity))
+        {
+            // Leave some of the consumer's DNA on the consumed item...
+            var ev = new TransferDnaEvent
+            {
+                Donor = args.Target,
+                Recipient = entity,
+                CanDnaBeCleaned = false,
+            };
+            RaiseLocalEvent(args.Target, ref ev);
+
+            args.Repeat = !args.ForceFed;
+            return;
+        }
+
+        args.Destroy = entity.Comp.DestroyOnEmpty;
+    }
+
+    private void OnFullyEaten(Entity<EdibleComponent> entity, ref FullyEatenEvent args)
+    {
+        SpawnTrash(entity, args.User);
+    }
+
+    private void OnBeforeFullySliced(Entity<EdibleComponent> entity, ref BeforeFullySlicedEvent args)
+    {
+        SpawnTrash(entity, args.User);
+    }
+
+    private void AddEdibleVerbs(Entity<EdibleComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        var user = args.User;
+
+        if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
+            return;
+
+        if (!TryGetIngestionVerb(user, entity, entity.Comp.Edible, out var verb))
+            return;
+
+        args.Verbs.Add(verb);
+    }
+
+    private void OnAttemptShake(Entity<EdibleComponent> entity, ref AttemptShakeEvent args)
+    {
+        if (IsEmpty(entity))
+            args.Cancelled = true;
+    }
+}
index 2f276fa93d4d9bab30093a1fdef4a6f086dfbe72..ab462557d96da2ec6edd88f3e30a2c4347c2f18a 100644 (file)
@@ -121,11 +121,8 @@ public sealed partial class OpenableSystem : EntitySystem
 
     private void OnTransferAttempt(Entity<OpenableComponent> ent, ref SolutionTransferAttemptEvent args)
     {
-        if (!ent.Comp.Opened)
-        {
-            // message says its just for drinks, shouldn't matter since you typically dont have a food that is openable and can be poured out
-            args.Cancel(Loc.GetString("drink-component-try-use-drink-not-open", ("owner", ent.Owner)));
-        }
+        if (ent.Comp.Opened)
+            args.Cancel(Loc.GetString(ent.Comp.ClosedPopup, ("owner", ent.Owner)));
     }
 
     private void OnAttemptShake(Entity<OpenableComponent> entity, ref AttemptShakeEvent args)
index 66e4834d0d1f81725659e70ae2a431fe05ef45d4..303d94d55f609e4f135f382466c85d253defa230 100644 (file)
@@ -1,34 +1,28 @@
 using Content.Shared.Administration.Logs;
-using Content.Shared.Body.Components;
-using Content.Shared.Body.Systems;
-using Content.Shared.Chemistry.Components.SolutionManager;
 using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.Examine;
 using Content.Shared.FixedPoint;
-using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Forensics;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Interaction;
-using Content.Shared.Mobs.Systems;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Inventory;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Popups;
 using Content.Shared.Verbs;
-using Robust.Shared.Utility;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Player;
 
 namespace Content.Shared.Nutrition.EntitySystems;
 
+[Obsolete("Migration to Content.Shared.Nutrition.EntitySystems.IngestionSystem is required")]
 public abstract partial class SharedDrinkSystem : EntitySystem
 {
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
-    [Dependency] private readonly SharedBodySystem _body = default!;
-    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
-    [Dependency] private readonly FoodSystem _food = default!;
-    [Dependency] private readonly SharedHandsSystem _hands = default!;
-    [Dependency] private readonly SharedInteractionSystem _interaction = default!;
-    [Dependency] private readonly MobStateSystem _mobState = default!;
-    [Dependency] private readonly OpenableSystem _openable = default!;
+    [Dependency] private readonly IngestionSystem _ingestion = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
 
@@ -36,8 +30,21 @@ public abstract partial class SharedDrinkSystem : EntitySystem
     {
         base.Initialize();
 
+        SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUseDrinkInHand, after: new[] { typeof(OpenableSystem), typeof(InventorySystem) });
+        SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(OnUseDrink);
+
         SubscribeLocalEvent<DrinkComponent, AttemptShakeEvent>(OnAttemptShake);
+
         SubscribeLocalEvent<DrinkComponent, GetVerbsEvent<AlternativeVerb>>(AddDrinkVerb);
+
+        SubscribeLocalEvent<DrinkComponent, BeforeIngestedEvent>(OnBeforeDrinkEaten);
+        SubscribeLocalEvent<DrinkComponent, IngestedEvent>(OnDrinkEaten);
+
+        SubscribeLocalEvent<DrinkComponent, EdibleEvent>(OnDrink);
+
+        SubscribeLocalEvent<DrinkComponent, IsDigestibleEvent>(OnIsDigestible);
+
+        SubscribeLocalEvent<DrinkComponent, GetEdibleTypeEvent>(OnGetEdibleType);
     }
 
     protected void OnAttemptShake(Entity<DrinkComponent> entity, ref AttemptShakeEvent args)
@@ -46,38 +53,6 @@ public abstract partial class SharedDrinkSystem : EntitySystem
             args.Cancelled = true;
     }
 
-    private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
-    {
-        if (entity.Owner == ev.User ||
-            !ev.CanInteract ||
-            !ev.CanAccess ||
-            !TryComp<BodyComponent>(ev.User, out var body) ||
-            !_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
-            return;
-
-        // Make sure the solution exists
-        if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out var solution))
-            return;
-
-        // no drinking from living drinks, have to kill them first.
-        if (_mobState.IsAlive(entity))
-            return;
-
-        var user = ev.User;
-        AlternativeVerb verb = new()
-        {
-            Act = () =>
-            {
-                TryDrink(user, user, entity.Comp, entity);
-            },
-            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/drink.svg.192dpi.png")),
-            Text = Loc.GetString("drink-system-verb-drink"),
-            Priority = 2
-        };
-
-        ev.Verbs.Add(verb);
-    }
-
     protected FixedPoint2 DrinkVolume(EntityUid uid, DrinkComponent? component = null)
     {
         if (!Resolve(uid, ref component))
@@ -98,72 +73,123 @@ public abstract partial class SharedDrinkSystem : EntitySystem
     }
 
     /// <summary>
-    /// Tries to feed the drink item to the target entity
+    /// Eat or drink an item
     /// </summary>
-    protected bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink, EntityUid item)
+    private void OnUseDrinkInHand(Entity<DrinkComponent> entity, ref UseInHandEvent ev)
     {
-        if (!HasComp<BodyComponent>(target))
-            return false;
+        if (ev.Handled)
+            return;
 
-        if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(target, out var stomachs))
-            return false;
+        ev.Handled = _ingestion.TryIngest(ev.User, ev.User, entity);
+    }
 
-        if (_openable.IsClosed(item, user, predicted: true))
-            return true;
+    /// <summary>
+    /// Feed someone else
+    /// </summary>
+    private void OnUseDrink(Entity<DrinkComponent> entity, ref AfterInteractEvent args)
+    {
+        if (args.Handled || args.Target == null || !args.CanReach)
+            return;
 
-        if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
-        {
-            if (drink.IgnoreEmpty)
-                return false;
+        args.Handled = _ingestion.TryIngest(args.User, args.Target.Value, entity);
+    }
 
-            _popup.PopupClient(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
-            return true;
-        }
+    private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        var user = args.User;
 
-        if (_food.IsMouthBlocked(target, user))
-            return true;
+        if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
+            return;
 
-        if (!_interaction.InRangeUnobstructed(user, item, popup: true))
-            return true;
+        if (!_ingestion.TryGetIngestionVerb(user, entity, IngestionSystem.Drink, out var verb))
+            return;
 
-        var forceDrink = user != target;
+        args.Verbs.Add(verb);
+    }
+
+    private void OnBeforeDrinkEaten(Entity<DrinkComponent> food, ref BeforeIngestedEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        // Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
+        args.Transfer = food.Comp.TransferAmount;
+    }
+
+    private void OnDrinkEaten(Entity<DrinkComponent> entity, ref IngestedEvent args)
+    {
+        if (args.Handled)
+            return;
 
-        if (forceDrink)
+        args.Handled = true;
+
+        _audio.PlayPredicted(entity.Comp.UseSound, args.Target, args.User, AudioParams.Default.WithVolume(-2f).WithVariation(0.25f));
+
+        var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
+
+        if (args.ForceFed)
         {
-            var userName = Identity.Entity(user, EntityManager);
+            var targetName = Identity.Entity(args.Target, EntityManager);
+            var userName = Identity.Entity(args.User, EntityManager);
+
+            _popup.PopupEntity(Loc.GetString("edible-force-feed-success", ("user", userName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Drink)), ("flavors", flavors)), entity, entity);
 
-            _popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
+            _popup.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Drink))), args.User, args.User);
 
-            // logging
-            _adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
+            // log successful forced drinking
+            _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to drink {ToPrettyString(entity.Owner):drink}");
         }
         else
         {
-            // log voluntary drinking
-            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is drinking {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
+            _popup.PopupClient(Loc.GetString("edible-slurp", ("flavors", flavors)), args.User, args.User);
+            _popup.PopupEntity(Loc.GetString("edible-slurp"), args.User, Filter.PvsExcept(args.User), true);
+
+            // log successful voluntary drinking
+            _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}");
         }
 
-        var flavors = _flavorProfile.GetLocalizedFlavorsMessage(user, drinkSolution);
+        if (_ingestion.GetUsesRemaining(entity, entity.Comp.Solution, args.Split.Volume) <= 0)
+            return;
 
-        var doAfterEventArgs = new DoAfterArgs(EntityManager,
-            user,
-            forceDrink ? drink.ForceFeedDelay : drink.Delay,
-            new ConsumeDoAfterEvent(drink.Solution, flavors),
-            eventTarget: item,
-            target: target,
-            used: item)
+        // Leave some of the consumer's DNA on the consumed item...
+        var ev = new TransferDnaEvent
         {
-            BreakOnHandChange = false,
-            BreakOnMove = forceDrink,
-            BreakOnDamage = true,
-            MovementThreshold = 0.01f,
-            DistanceThreshold = 1.0f,
-            // 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 = forceDrink || _hands.IsHolding(user, item),
+            Donor = args.Target,
+            Recipient = entity,
+            CanDnaBeCleaned = false,
         };
+        RaiseLocalEvent(args.Target, ref ev);
+
+        args.Repeat = !args.ForceFed;
+    }
+
+    private void OnDrink(Entity<DrinkComponent> drink, ref EdibleEvent args)
+    {
+        if (args.Cancelled || args.Solution != null)
+            return;
+
+        if (!_solutionContainer.TryGetSolution(drink.Owner, drink.Comp.Solution, out args.Solution) || IsEmpty(drink))
+        {
+            args.Cancelled = true;
+
+            _popup.PopupClient(Loc.GetString("ingestion-try-use-is-empty", ("entity", drink)), drink, args.User);
+            return;
+        }
+
+        args.Time += TimeSpan.FromSeconds(drink.Comp.Delay);
+    }
+
+    private void OnIsDigestible(Entity<DrinkComponent> ent, ref IsDigestibleEvent args)
+    {
+        // Anyone can drink from puddles on the floor!
+        args.UniversalDigestion();
+    }
+
+    private void OnGetEdibleType(Entity<DrinkComponent> ent, ref GetEdibleTypeEvent args)
+    {
+        if (args.Type != null)
+            return;
 
-        _doAfter.TryStartDoAfter(doAfterEventArgs);
-        return true;
+        args.SetPrototype(IngestionSystem.Drink);
     }
 }
index 63fe822186645a3d388827bc29a56799f54c465c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -1,73 +0,0 @@
-using Content.Shared.Containers.ItemSlots;
-using Content.Shared.Interaction;
-using Content.Shared.Nutrition.Components;
-using Content.Shared.Popups;
-using Content.Shared.Tools.EntitySystems;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Random;
-
-namespace Content.Shared.Nutrition.EntitySystems;
-
-public sealed class UtensilSystem : EntitySystem
-{
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly FoodSystem _foodSystem = default!;
-    [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
-    [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
-    [Dependency] private readonly IRobustRandom _robustRandom = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<UtensilComponent, AfterInteractEvent>(OnAfterInteract, after: new[] { typeof(ItemSlotsSystem), typeof(ToolOpenableSystem) });
-    }
-
-    /// <summary>
-    /// Clicked with utensil
-    /// </summary>
-    private void OnAfterInteract(Entity<UtensilComponent> entity, ref AfterInteractEvent ev)
-    {
-        if (ev.Handled || ev.Target == null || !ev.CanReach)
-            return;
-
-        var result = TryUseUtensil(ev.User, ev.Target.Value, entity);
-        ev.Handled = result.Handled;
-    }
-
-    public (bool Success, bool Handled) TryUseUtensil(EntityUid user, EntityUid target, Entity<UtensilComponent> utensil)
-    {
-        if (!TryComp(target, out FoodComponent? food))
-            return (false, false);
-
-        //Prevents food usage with a wrong utensil
-        if ((food.Utensil & utensil.Comp.Types) == 0)
-        {
-            _popupSystem.PopupClient(Loc.GetString("food-system-wrong-utensil", ("food", target), ("utensil", utensil.Owner)), user, user);
-            return (false, true);
-        }
-
-        if (!_interactionSystem.InRangeUnobstructed(user, target, popup: true))
-            return (false, true);
-
-        return _foodSystem.TryFeed(user, user, target, food);
-    }
-
-    /// <summary>
-    /// Attempt to break the utensil after interaction.
-    /// </summary>
-    /// <param name="uid">Utensil.</param>
-    /// <param name="userUid">User of the utensil.</param>
-    public void TryBreak(EntityUid uid, EntityUid userUid, UtensilComponent? component = null)
-    {
-        if (!Resolve(uid, ref component))
-            return;
-
-        if (_robustRandom.Prob(component.BreakChance))
-        {
-            _audio.PlayPredicted(component.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f));
-            Del(uid);
-        }
-    }
-}
index 685b08b1bd3973f34be631613366973b2181a803..27988c898db6f2466154401b3ab7b69448b198dd 100644 (file)
@@ -1,9 +1,52 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.FixedPoint;
+using Content.Shared.Inventory;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Nutrition.Prototypes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
 namespace Content.Shared.Nutrition;
 
 /// <summary>
-///     Raised directed at the consumer when attempting to ingest something.
+/// Raised on an entity that is trying to be ingested to see if it has universal blockers preventing it from being
+/// ingested.
+/// </summary>
+[ByRefEvent]
+public record struct IngestibleEvent(bool Cancelled = false);
+
+/// <summary>
+/// Raised on an entity with the <see cref="EdibleComponent"/> to check if anything is stopping
+/// another entity from consuming the delicious reagents stored inside.
+/// </summary>
+/// <param name="User">The entity trying to feed us to an entity.</param>
+[ByRefEvent]
+public record struct EdibleEvent(EntityUid User)
+{
+    public Entity<SolutionComponent>? Solution = null;
+
+    public TimeSpan Time = TimeSpan.Zero;
+
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Raised when an entity is trying to ingest an entity to see if it has any component that can ingest it.
+/// </summary>
+/// <param name="Handled">Did a system successfully ingest this item?</param>
+/// <param name="User">The entity that is trying to feed and therefore raising the event</param>
+/// <param name="Ingested">What are we trying to ingest?</param>
+/// <param name="Ingest">Should we actually try and ingest? Or are we just testing if it's even possible </param>
+[ByRefEvent]
+public record struct AttemptIngestEvent(EntityUid User, EntityUid Ingested, bool Ingest, bool Handled = false);
+
+/// <summary>
+///     Raised on an entity that is consuming another entity to see if there is anything attached to the entity
+///     that is preventing it from doing the consumption.
 /// </summary>
-public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
+[ByRefEvent]
+public record struct IngestionAttemptEvent(SlotFlags TargetSlots, bool Cancelled = false) : IInventoryRelayEvent
 {
     /// <summary>
     ///     The equipment that is blocking consumption. Should only be non-null if the event was canceled.
@@ -12,22 +55,113 @@ public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
 }
 
 /// <summary>
-/// Raised directed at the food after finishing eating a food before it's deleted.
-/// Cancel this if you want to do something special before a food is deleted.
+///     Raised on an entity that is trying to be digested, aka turned from an entity into reagents.
+///     Returns its digestive properties or how difficult it is to convert to reagents.
 /// </summary>
-public sealed class BeforeFullyEatenEvent : CancellableEntityEventArgs
+/// <remarks>This method is currently needed for backwards compatibility with food and drink component.
+///          It also might be needed in the event items like trash and plushies have their edible component removed.
+///          There's no way to know whether this event will be made obsolete or not after Food and Drink Components
+///          are removed until after a proper body and digestion rework. Oh well!
+/// </remarks>
+[ByRefEvent]
+public record struct IsDigestibleEvent()
 {
-    /// <summary>
-    /// The person that ate the food.
-    /// </summary>
-    public EntityUid User;
+    public bool Digestible = false;
+
+    public bool SpecialDigestion = false;
+
+    // If this is true, SpecialDigestion will be ignored
+    public bool Universal = false;
+
+    // If it requires special digestion then it has to be digestible...
+    public void AddDigestible(bool special)
+    {
+        SpecialDigestion = special;
+        Digestible = true;
+    }
+
+    // This should only be used for if you're trying to drink pure reagents from a puddle or cup or something...
+    public void UniversalDigestion()
+    {
+        Universal = true;
+        Digestible = true;
+    }
+}
+
+/// <summary>
+/// Do After Event for trying to put food solution into stomach entity.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed partial class EatingDoAfterEvent : SimpleDoAfterEvent;
+
+/// <summary>
+/// We use this to determine if an entity should abort giving up its reagents at the last minute,
+/// as well as specifying how much of its reagents it should give up including minimums and maximums.
+/// If minimum exceeds the  maximum, the event will abort.
+/// </summary>
+/// <param name="Min">The minimum amount we can transfer.</param>
+/// <param name="Max">The maximum amount we can transfer.</param>
+/// <param name="Solution">The solution we are transferring.</param>
+[ByRefEvent]
+public record struct BeforeIngestedEvent(FixedPoint2 Min, FixedPoint2 Max, Solution? Solution)
+{
+    // How much we would like to transfer, gets clamped by Min and Max.
+    public FixedPoint2 Transfer;
+
+    // Whether this event, and therefore eat attempt, should be cancelled.
+    public bool Cancelled;
+
+    public bool TryNewMinimum(FixedPoint2 newMin)
+    {
+        if (newMin > Max)
+            return false;
+
+        Min = newMin;
+        return true;
+    }
+
+    public bool TryNewMaximum(FixedPoint2 newMax)
+    {
+        if (newMax < Min)
+            return false;
+
+        Min = newMax;
+        return true;
+    }
+}
+
+[ByRefEvent]
+public record struct IngestingEvent(EntityUid Food, Solution Split, bool ForceFed);
+
+/// <summary>
+/// Raised on an entity when it is being made to be eaten.
+/// </summary>
+/// <param name="User">Who is doing the action?</param>
+/// <param name="Target">Who is doing the eating?</param>
+/// <param name="Split">The solution we're currently eating.</param>
+/// <param name="ForceFed">Whether we're being fed by someone else, checkec enough I might as well pass it.</param>
+[ByRefEvent]
+public record struct IngestedEvent(EntityUid User, EntityUid Target, Solution Split, bool ForceFed)
+{
+    // Should we refill the solution now that we've eaten it?
+    // This bool basically only exists because of stackable system.
+    public bool Refresh;
+
+    // Should we destroy the ingested entity?
+    public bool Destroy;
+
+    // Has this eaten event been handled? Used to prevent duplicate flavor popups and sound effects.
+    public bool Handled;
+
+    // Should we try eating again?
+    public bool Repeat;
 }
 
 /// <summary>
 /// Raised directed at the food after finishing eating it and before it's deleted.
 /// </summary>
 [ByRefEvent]
-public readonly record struct AfterFullyEatenEvent(EntityUid User)
+public readonly record struct FullyEatenEvent(EntityUid User)
 {
     /// <summary>
     /// The entity that ate the food.
@@ -35,6 +169,38 @@ public readonly record struct AfterFullyEatenEvent(EntityUid User)
     public readonly EntityUid User = User;
 }
 
+/// <summary>
+/// Returns a list of Utensils that can be used to consume the entity, as well as a list of required types.
+/// </summary>
+[ByRefEvent]
+public record struct GetUtensilsEvent()
+{
+    public UtensilType Types = UtensilType.None;
+
+    public UtensilType RequiredTypes = UtensilType.None;
+
+    // Forces you to add to both lists if a utensil is required.
+    public void AddRequiredTypes(UtensilType type)
+    {
+        RequiredTypes |= type;
+        Types |= type;
+    }
+}
+
+/// <summary>
+/// Tries to get the best fitting edible type for an entity.
+/// </summary>
+[ByRefEvent]
+public record struct GetEdibleTypeEvent
+{
+    public ProtoId<EdiblePrototype>? Type { get; private set; }
+
+    public void SetPrototype([ForbidLiteral] ProtoId<EdiblePrototype> proto)
+    {
+        Type = proto;
+    }
+}
+
 /// <summary>
 /// Raised directed at the food being sliced before it's deleted.
 /// Cancel this if you want to do something special before a food is deleted.
diff --git a/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs b/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs
new file mode 100644 (file)
index 0000000..0f4c238
--- /dev/null
@@ -0,0 +1,54 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Nutrition.Prototypes;
+
+/// <summary>
+/// This stores unique data for an item that is edible, such as verbs, verb icons, verb names, sounds, ect.
+/// </summary>
+[Prototype]
+public sealed partial class EdiblePrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    /// <summary>
+    /// The sound we make when eaten.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier UseSound = new SoundCollectionSpecifier("eating");
+
+    /// <summary>
+    /// The localization identifier for the ingestion message.
+    /// </summary>
+    [DataField]
+    public LocId Message;
+
+    /// <summary>
+    /// Localization verb used when consuming this item.
+    /// </summary>
+    [DataField]
+    public LocId Verb;
+
+    /// <summary>
+    /// Localization noun used when consuming this item.
+    /// </summary>
+    [DataField]
+    public LocId Noun;
+
+    /// <summary>
+    /// What type of food are we, currently used for determining verbs and some checks.
+    /// </summary>
+    [DataField]
+    public LocId VerbName;
+
+    /// <summary>
+    /// What type of food are we, currently used for determining verbs and some checks.
+    /// </summary>
+    [DataField]
+    public SpriteSpecifier? VerbIcon;
+
+
+}
index cd2f38d47f6abf0bc8c7ebcce3c8c5d61ae8d4fd..912089379a8b99b11b83bb02c665c4256f23e119 100644 (file)
@@ -3,6 +3,7 @@ using Content.Shared.Examine;
 using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
+using Content.Shared.Nutrition;
 using Content.Shared.Popups;
 using Content.Shared.Storage.EntitySystems;
 using JetBrains.Annotations;
@@ -37,6 +38,8 @@ namespace Content.Shared.Stacks
             SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
             SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
             SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
+            SubscribeLocalEvent<StackComponent, BeforeIngestedEvent>(OnBeforeEaten);
+            SubscribeLocalEvent<StackComponent, IngestedEvent>(OnEaten);
 
             _vvm.GetTypeHandler<StackComponent>()
                 .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount);
@@ -389,6 +392,51 @@ namespace Content.Shared.Stacks
                 )
             );
         }
+
+        private void OnBeforeEaten(Entity<StackComponent> eaten, ref BeforeIngestedEvent args)
+        {
+            if (args.Cancelled)
+                return;
+
+            if (args.Solution is not { } sol)
+                return;
+
+            // If the entity is empty and is a lingering entity we can't eat from it.
+            if (eaten.Comp.Count <= 0)
+            {
+                args.Cancelled = true;
+                return;
+            }
+
+            /*
+            Edible stacked items is near completely evil so we must choose one of the following:
+            - Option 1: Eat the entire solution each bite and reduce the stack by 1.
+            - Option 2: Multiply the solution eaten by the stack size.
+            - Option 3: Divide the solution consumed by stack size.
+            The easiest and safest option is and always will be Option 1 otherwise we risk reagent deletion or duplication.
+            That is why we cancel if we cannot set the minimum to the entire volume of the solution.
+            */
+            if(args.TryNewMinimum(sol.Volume))
+                return;
+
+            args.Cancelled = true;
+        }
+
+        private void OnEaten(Entity<StackComponent> eaten, ref IngestedEvent args)
+        {
+            if (!Use(eaten, 1))
+                return;
+
+            // We haven't eaten the whole stack yet or are unable to eat it completely.
+            if (eaten.Comp.Count > 0 || eaten.Comp.Lingering)
+            {
+                args.Refresh = true;
+                return;
+            }
+
+            // Here to tell the food system to do destroy stuff.
+            args.Destroy = true;
+        }
     }
 
     /// <summary>
index 51615c5afaf440b15cf672c33ad3697297a6e329..3691f6e2b667bf185276a2267c00737ede51020e 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Interaction;
 using Content.Shared.Item;
 using Content.Shared.Materials;
 using Content.Shared.Nutrition;
+using Content.Shared.Nutrition.EntitySystems;
 using Content.Shared.Popups;
 using Content.Shared.Storage.Components;
 using Content.Shared.Tools.EntitySystems;
@@ -25,6 +26,7 @@ namespace Content.Shared.Storage.EntitySystems;
 /// </summary>
 public sealed class SecretStashSystem : EntitySystem
 {
+    [Dependency] private readonly IngestionSystem _ingestion = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
     [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
     [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -41,7 +43,7 @@ public sealed class SecretStashSystem : EntitySystem
         SubscribeLocalEvent<SecretStashComponent, DestructionEventArgs>(OnDestroyed);
         SubscribeLocalEvent<SecretStashComponent, GotReclaimedEvent>(OnReclaimed);
         SubscribeLocalEvent<SecretStashComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ToolOpenableSystem), typeof(AnchorableSystem) });
-        SubscribeLocalEvent<SecretStashComponent, AfterFullyEatenEvent>(OnEaten);
+        SubscribeLocalEvent<SecretStashComponent, FullyEatenEvent>(OnFullyEaten);
         SubscribeLocalEvent<SecretStashComponent, InteractHandEvent>(OnInteractHand);
         SubscribeLocalEvent<SecretStashComponent, GetVerbsEvent<InteractionVerb>>(OnGetVerb);
     }
@@ -61,7 +63,7 @@ public sealed class SecretStashSystem : EntitySystem
         DropContentsAndAlert(entity, args.ReclaimerCoordinates);
     }
 
-    private void OnEaten(Entity<SecretStashComponent> entity, ref AfterFullyEatenEvent args)
+    private void OnFullyEaten(Entity<SecretStashComponent> entity, ref FullyEatenEvent args)
     {
         // TODO: When newmed is finished should do damage to teeth (Or something like that!)
         var damage = entity.Comp.DamageEatenItemInside;
index ab458746ddfb2dfa028f6cba0340fae768dd2980..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -1,18 +0,0 @@
-drink-component-on-use-is-empty = {CAPITALIZE(THE($owner))} is empty!
-drink-component-on-examine-is-opened = [color=yellow]Opened[/color]
-drink-component-on-examine-is-sealed = The seal is intact.
-drink-component-on-examine-is-unsealed = The seal is broken.
-drink-component-try-use-drink-not-open = Open {$owner} first!
-drink-component-try-use-drink-is-empty = {CAPITALIZE(THE($entity))} is empty!
-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-try-use-drink-success-slurp-taste = Slurp. {$flavors}
-drink-component-force-feed = {CAPITALIZE(THE($user))} is trying to make you drink something!
-drink-component-force-feed-success = {CAPITALIZE(THE($user))} forced you to drink something! {$flavors}
-drink-component-force-feed-success-user = You successfully feed {THE($target)}
-
-
-drink-system-verb-drink = Drink
diff --git a/Resources/Locale/en-US/nutrition/components/food-component.ftl b/Resources/Locale/en-US/nutrition/components/food-component.ftl
deleted file mode 100644 (file)
index 2247ef6..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-
-### Interaction Messages
-
-# 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 {INDEFINITE($utensil)} {$utensil} to eat that!
-
-food-nom = Nom. {$flavors}
-food-swallow = You swallow { THE($food) }. {$flavors}
-
-food-has-used-storage = You cannot eat { THE($food) } with an item stored inside.
-
-food-system-remove-mask = You need to take off the {$entity} first.
-
-## System
-
-food-system-you-cannot-eat-any-more = You can't eat any more!
-food-system-you-cannot-eat-any-more-other = {CAPITALIZE(SUBJECT($target))} can't eat any more!
-food-system-try-use-food-is-empty = {CAPITALIZE(THE($entity))} is empty!
-food-system-wrong-utensil = You can't eat {THE($food)} with {INDEFINITE($utensil)} {$utensil}.
-food-system-cant-digest = You can't digest {THE($entity)}!
-food-system-cant-digest-other = {CAPITALIZE(SUBJECT($target))} can't digest {THE($entity)}!
-
-food-system-verb-eat = Eat
-
-## Force feeding
-
-food-system-force-feed = {CAPITALIZE(THE($user))} is trying to feed you something!
-food-system-force-feed-success = {CAPITALIZE(THE($user))} forced you to eat something! {$flavors}
-food-system-force-feed-success-user = You successfully feed {THE($target)}
diff --git a/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl b/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl
new file mode 100644 (file)
index 0000000..692100e
--- /dev/null
@@ -0,0 +1,53 @@
+### Interaction Messages
+
+# System
+
+## When trying to ingest without the required utensil... but you gotta hold it
+ingestion-you-need-to-hold-utensil = You need to be holding {INDEFINITE($utensil)} {$utensil} to eat that!
+
+ingestion-try-use-is-empty = {CAPITALIZE(THE($entity))} is empty!
+ingestion-try-use-wrong-utensil = You can't {$verb} {THE($food)} with {INDEFINITE($utensil)} {$utensil}.
+
+ingestion-remove-mask = You need to take off the {$entity} first.
+
+## Failed Ingestion
+
+ingestion-you-cannot-ingest-any-more = You can't {$verb} any more!
+ingestion-other-cannot-ingest-any-more = {CAPITALIZE(SUBJECT($target))} can't {$verb} any more!
+
+ingestion-cant-digest = You can't digest {THE($entity)}!
+ingestion-cant-digest-other = {CAPITALIZE(SUBJECT($target))} can't digest {THE($entity)}!
+
+## Action Verbs, not to be confused with Verbs
+
+ingestion-verb-food = Eat
+ingestion-verb-drink = Drink
+
+# Edible Component
+
+edible-nom = Nom. {$flavors}
+edible-slurp = Slurp. {$flavors}
+edible-swallow = You swallow { THE($food) }
+edible-gulp = Gulp. {$flavors}
+
+edible-has-used-storage = You cannot {$verb} { THE($food) } with an item stored inside.
+
+## Nouns
+
+edible-noun-edible = edible
+edible-noun-food = food
+edible-noun-drink = drink
+edible-noun-pill = pill
+
+## Verbs
+
+edible-verb-edible = ingest
+edible-verb-food = eat
+edible-verb-drink = drink
+edible-verb-pill = swallow
+
+## Force feeding
+
+edible-force-feed = {CAPITALIZE(THE($user))} is trying to make you {$verb} something!
+edible-force-feed-success = {CAPITALIZE(THE($user))} forced you to {$verb} something! {$flavors}
+edible-force-feed-success-user = You successfully feed {THE($target)}
index 3acc24cf532d26a1855d77fbb8e4bf6bc50cf258..786885e65870622f6ceb7a25ca9c768a3f0436a3 100644 (file)
@@ -1,2 +1,5 @@
 openable-component-verb-open = Open
 openable-component-verb-close = Close
+
+openable-component-on-examine-is-opened = [color=yellow]Opened[/color]
+openable-component-try-use-closed = Open {$owner} first!
diff --git a/Resources/Locale/en-US/nutrition/components/sealable-component.ftl b/Resources/Locale/en-US/nutrition/components/sealable-component.ftl
new file mode 100644 (file)
index 0000000..e826e17
--- /dev/null
@@ -0,0 +1,2 @@
+sealable-component-on-examine-is-sealed = The seal is intact.
+sealable-component-on-examine-is-unsealed = The seal is broken.
index 3b00e1a2237d11b47520ced0c793cdc2ee239e30..6b64aa4c1d661ec83c0504447dc9f777eea8cf2b 100644 (file)
@@ -4,6 +4,12 @@
   name: ruminant stomach
   categories: [ HideSpawnMenu ]
   components:
+  - type: Stomach
+    specialDigestible:
+      tags:
+      - Ruminant
+      - Wheat
+      - BananaPeel
   - type: SolutionContainerManager
     solutions:
       stomach:
index be8727008a34aa8130452c4381dd859e1652fc9a..96844d40177d5641f555dddda968efa1aa02e643 100644 (file)
   - type: Clothing
     slots:
     - HEAD
-  - type: Food
+  - type: Edible
+    edible: Drink
     solution: drink
-    useSound: /Audio/Items/drink.ogg
-    eatMessage: drink-component-try-use-drink-success-slurp
     delay: 0.5
     forceFeedDelay: 1.5
   - type: FlavorProfile
     flavors:
       - water
+  - type: DrainableSolution
+    solution: drink
   - type: SolutionContainerManager
     solutions:
       drink:
index 1b84dcd7d0871bf08dbadc00009188ea4580ed45..cc3df59c55c48b56e973692252245e380a064ea7 100644 (file)
   - type: EdgeSpreader
     id: Puddle
   - type: StepTrigger
-  - type: Drink
+  - type: Edible
+    edible: Drink
     delay: 3
     transferAmount: 1
     solution: puddle
index 920468605f5a96e5f58860c75546e3cf6937cad6..da68ee109be682557ae9339dfa81e37bfa42249d 100644 (file)
     growthDelay: 20
   - type: ExaminableHunger
   - type: Wooly
-  - type: Food
+  - type: Edible
+    destroyOnEmpty: false
     solution: wool
     requiresSpecialDigestion: true
-    # Wooly prevents eating wool deleting the goat so its fine
     requireDead: false
   - type: FlavorProfile
     flavors:
index 2d8fadfa43e1f49f82c0e09c12306f07c056105a..4df720d541744c4d9bdc04ea0dcaa77d98e74218 100644 (file)
     solution: drink
   - type: SolutionTransfer
     canChangeTransferAmount: true
-  - type: Drink
+  - type: Edible
+    edible: Drink
+    solution: drink
+    destroyOnEmpty: false
   - type: Sprite
     state: icon
   - type: MeleeWeapon
index b226620953bf046ee20dc133a3c67f8814c74fba..6d17f4fbbbd7c36242424ceaf6ecee51018afa03 100644 (file)
   id: FoodBreadBunBottom
   parent: FoodBreadSliceBase
   name: bottom bun
-  description: It's time to start building the burger tower. 
+  description: It's time to start building the burger tower.
   components:
   - type: Item
-    size: Normal #patch until there is an adequate resizing system in place 
+    size: Normal #patch until there is an adequate resizing system in place
   - type: Food
   - type: Sprite
     drawdepth: Mobs
@@ -83,7 +83,7 @@
   - type: FoodSequenceElement
     entries:
       Burger: BunTopBurger
-  
+
 # Base
 
 - type: entity
@@ -95,8 +95,6 @@
     flavors:
       - bun
       - meaty
-  - type: Food
-    transferAmount: 5
   - type: Sprite
     sprite: Objects/Consumable/Food/burger.rsi
   - type: SolutionContainerManager
         - ReagentId: Vitamin
           Quantity: 8
         - ReagentId: Sulfur # What you get for eating something with a flare in it
-          Quantity: 5 
+          Quantity: 5
   - type: Tag
     tags:
     - Meat
   description: An elusive rib shaped burger with limited availability across the galaxy. Not as good as you remember it.
   components:
   - type: Food
-    trash: 
+    trash:
     - FoodKebabSkewer
   - type: FlavorProfile
     flavors:
   - type: SolutionContainerManager
     solutions:
       food:
-        maxVol: 30 
+        maxVol: 30
         reagents:
         - ReagentId: Nutriment
           Quantity: 11
   - type: Tag
     tags:
     - Meat
-    
+
index ddacf71a03ee3d2ac6bcfebc70cc0db236eafbf5..fedce70e799adf1dccc5ad815881244992f4215b 100644 (file)
@@ -9,7 +9,7 @@
   - type: FlavorProfile
     flavors:
       - food
-  - type: Food
+  - type: Edible
   - type: Sprite
   - type: StaticPrice
     price: 0
index 93c48b69c25fba6731c1cdbb1720b70aa491e4c7..721c2e3e38d77ce97444a34eb58b746852bb06c3 100644 (file)
   - type: Sprite
     state: produce
   # let cows eat raw produce like wheat and oats
-  - type: Food
-    requiredStomachs: 2
+  - type: Edible
+    requiresSpecialDigestion: true
   - type: Produce
   - type: PotencyVisuals
   - type: Appearance
   - type: Extractable
     grindableSolutionName: food
+  - type: Tag
+    tags:
+    - Ruminant
 
 # For produce that can be immediately eaten
 
@@ -57,6 +60,7 @@
   - type: Tag
     tags:
     - Wheat
+    - Ruminant
 
 - type: entity
   name: meatwheat bushel
   - type: Tag
     tags:
     - Vegetable
+    - Ruminant
 
 - type: entity
   name: tower-cap log
   - type: FlavorProfile
     flavors:
       - banana
-  - type: Food
+  - type: Edible
     trash:
     - TrashBananaPeel
   - type: SolutionContainerManager
     flavors:
       - banana
       - nothing
-  - type: Food
+  - type: Edible
     trash:
     - TrashMimanaPeel
   - type: SolutionContainerManager
     - Recyclable
     - Trash
     - BananaPeel
+    - Ruminant
     - WhitelistChameleon
     - HamsterWearable
   - type: SolutionContainerManager
   - type: Extractable
     grindableSolutionName: food
   - type: SpaceGarbage
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: Clothing
     sprite: Objects/Specific/Hydroponics/banana.rsi
   - type: FlavorProfile
     flavors:
       - corn
-  - type: Food
+  - type: Edible
     trash:
     - FoodCornTrash
   - type: SolutionContainerManager
     sprite: Objects/Specific/Hydroponics/gatfruit.rsi
   - type: Produce
     seedId: gatfruit
-  - type: Food
+  - type: Edible
     trash:
     - WeaponRevolverPython
   - type: Tag
     heldPrefix: produce
   - type: Produce
     seedId: realCapfruit
-  - type: Food
+  - type: Edible
     trash:
     - RevolverCapGun
   - type: Tag
   components:
   - type: Produce
     seedId: fakeCapfruit
-  - type: Food
+  - type: Edible
     trash:
     - RevolverCapGunFake
 
   - type: FlavorProfile
     flavors:
       - bungo
-  - type: Food
+  - type: Edible
     trash:
     - FoodBungoPit
   - type: SolutionContainerManager
   - type: FlavorProfile
     flavors:
       - cotton
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: SolutionContainerManager
     solutions:
   - type: FlavorProfile
     flavors:
       - pyrotton
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: SolutionContainerManager
     solutions:
   - type: FlavorProfile
     flavors:
       - cherry
-  - type: Food
+  - type: Edible
     trash:
     - TrashCherryPit
   - type: SolutionContainerManager
     heldPrefix: produce
   - type: Produce
     seedId: anomalyBerry
-  - type: Food
+  - type: Edible
     trash:
     - EffectAnomalyFloraBulb # Random loot
   - type: SolutionContainerManager
index 5e04ac55dc85b9e95e2942a8d28e50fd085bd738..a64566f2588085206aba79789170260f7fc7a61b 100644 (file)
     - state: cloth_3
       map: ["base"]
   - type: Appearance
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: FlavorProfile
     flavors:
   - type: Construction
     graph: Durathread
     node: MaterialDurathread
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: SolutionContainerManager
     solutions:
     - state: cotton_3
       map: ["base"]
   - type: Appearance
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: FlavorProfile
     flavors:
     - state: pyrotton_3
       map: ["base"]
   - type: Appearance
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: SolutionContainerManager
     solutions:
   - type: FlavorProfile
     flavors:
       - banana
-  - type: Food
+  - type: Edible
     trash:
     - TrashBananiumPeel
   - type: BadFood
   - type: Stack
     count: 50
     stackType: WebSilk
-  - type: Food
+  - type: Edible
     requiresSpecialDigestion: true
   - type: FlavorProfile
     flavors:
     - state: cotton_3
       map: ["base"]
   - type: Appearance
-  - type: Food
+  - type: Edible
   - type: BadFood
   - type: SolutionContainerManager
     solutions:
index e35e915b092556f7442dce48550447269af25acf..6c01a2a8dd3d0f4efbf81d7dbd52c4e46d7b0fe4 100644 (file)
@@ -90,7 +90,7 @@
         components:
         - IgnoreKudzu
     - type: Food
-      requiredStomachs: 2 # ruminants have 4 stomachs but i dont care to give them literally 4 stomachs. 2 is good
+      requiresSpecialDigestion: true
       delay: 0.5
     - type: FlavorProfile
       flavors:
           reagents:
           - ReagentId: Nutriment
             Quantity: 2
+    - type: Tag
+      tags:
+      - Ruminant
 
 - type: entity
   id: WeakKudzu
index 3a7e44775f3e5722646829c8d4094dc16e3fb12c..70ea8080ae5d4fc637a12235e38df6a7d81c4928 100644 (file)
     size: Tiny
     sprite: Objects/Specific/Chemistry/pills.rsi
   - type: Pill
-  - type: Food
+  - type: Edible
     delay: 0.6
     forceFeedDelay: 2
     transferAmount: null
-    eatMessage: food-swallow
-    useSound: /Audio/Items/pill.ogg
+    edible: Pill
   - type: BadFood
   - type: FlavorProfile
     ignoreReagents: []
index 992c3313e2e8debb74d67faf80c363532776dcab..d77e6cd2b80d99506f0bf78a3b535c025a52be2f 100644 (file)
@@ -1,13 +1,14 @@
 - type: entity
-  parent: BaseItem
+  parent: DrinkBase
   id: Bucket
   name: bucket
   description: It's a boring old bucket.
   components:
-  - type: Drink
-    solution: bucket
-    ignoreEmpty: true
   - type: Clickable
+  - type: Edible
+    edible: Drink
+    solution: bucket
+    destroyOnEmpty: false
   - type: Sprite
     sprite: Objects/Tools/bucket.rsi
     layers:
index 23ad7a59a1f49c8d7f87443204aa03bacb8c4238..3274bdf97746bd1aea22a2dd670735cb01380e69 100644 (file)
@@ -31,7 +31,7 @@
   query:
     - !type:ComponentQuery
       components:
-      - type: Food
+      - type: Edible
   considerations:
     - !type:TargetIsAliveCon
       curve: !type:InverseBoolCurve
@@ -50,7 +50,7 @@
   query:
   - !type:ComponentQuery
     components:
-    - type: Drink
+    - type: Edible
   considerations:
   - !type:TargetIsAliveCon
     curve: !type:InverseBoolCurve
diff --git a/Resources/Prototypes/Nutrition/edible.yml b/Resources/Prototypes/Nutrition/edible.yml
new file mode 100644 (file)
index 0000000..1b29bbe
--- /dev/null
@@ -0,0 +1,47 @@
+# If you add a new prototype, you may want to consider adding it to IngestionSystem.API for other systems to use.
+# But only if other systems/components might want it.
+
+# Food
+
+- type: edible
+  id: Food
+  useSound: !type:SoundCollectionSpecifier
+    params:
+      variation: 0.2
+      volume: -1
+    collection: eating # I think this *should* grab the sound specifier...
+  message: edible-nom
+  verb: edible-verb-food
+  noun: edible-noun-food
+  verbName: ingestion-verb-food
+  verbIcon: /Textures/Interface/VerbIcons/cutlery.svg.192dpi.png
+
+# Drink
+
+- type: edible
+  id: Drink
+  useSound: !type:SoundPathSpecifier
+    params:
+      variation: 0.25
+      volume: -2
+    path: /Audio/Items/drink.ogg
+  message: edible-slurp
+  verb: edible-verb-drink
+  noun: edible-noun-drink
+  verbName: ingestion-verb-drink
+  verbIcon: /Textures/Interface/VerbIcons/drink.svg.192dpi.png
+
+# Pills!
+
+- type: edible
+  id: Pill
+  useSound: !type:SoundPathSpecifier
+    params:
+      variation: 0.2
+      volume: -1
+    path: /Audio/Items/pill.ogg
+  message: edible-swallow
+  verb: edible-verb-pill
+  noun: edible-noun-pill
+  verbName: ingestion-verb-food
+  verbIcon: /Textures/Interface/VerbIcons/cutlery.svg.192dpi.png
index 24e21742c302dce9cf6977a9102f909cef94a275..5d34f0f30cb5a4c1f93f0e25a4a73e986fb20b7f 100644 (file)
 - type: Tag
   id: RollingPin
 
+- type: Tag
+  id: Ruminant
+
 - type: Tag
   id: SaltShaker