Debody Food and Drink Systems, Combine Food and Drink into One System. (#39031)

* 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>
This commit is contained in:
Princess Cheeseballs
2025-08-06 09:53:38 -07:00
committed by GitHub
parent 02382045ab
commit 91854e0776
52 changed files with 2169 additions and 1024 deletions

View 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);

View 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;

View 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;
}

View 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;
}
}

View 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 _);
}

View 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);
_destroy.DestroyEntity(uid);
}
QueueDel(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))
{
if (!_solutionContainer.TryGetSolution(slice.Owner, slice.Comp.Solution, out var itsSoln, out var itsSolution))
return;
_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 _);
}
}

View 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;
}

View 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;

View 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();
}
}

View 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);

View 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);

View 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

View File

@@ -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;
}

View 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.

View 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;
}

View 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.

View 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";
}

View 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")]

View 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);
}

View 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);
var user = args.User;
// 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);
}
// 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)
if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
return;
if (!TryComp<BodyComponent>(args.Target.Value, out var body))
if (!_ingestion.TryGetIngestionVerb(user, entity, IngestionSystem.Food, out var verb))
return;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((args.Target.Value, body), out var stomachs))
args.Verbs.Add(verb);
}
private void OnBeforeFoodEaten(Entity<FoodComponent> food, ref BeforeIngestedEvent args)
{
if (args.Cancelled || args.Solution is not { } solution)
return;
if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
return;
// Set it to transfer amount if it exists, otherwise eat the whole volume if possible.
args.Transfer = food.Comp.TransferAmount ?? solution.Volume;
}
if (!TryGetRequiredUtensils(args.User, entity.Comp, out var utensils))
private void OnFoodEaten(Entity<FoodComponent> entity, ref IngestedEvent args)
{
if (args.Handled)
return;
// TODO this should really be checked every tick.
if (IsMouthBlocked(args.Target.Value))
return;
// TODO this should really be checked every tick.
if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value))
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);
_audio.PlayPredicted(entity.Comp.UseSound, args.Target, args.User, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
// Get the stomach with the highest available solution volume
var highestAvailable = FixedPoint2.Zero;
Entity<StomachComponent>? stomachToUse = null;
foreach (var ent in stomachs)
var flavors = _flavorProfile.GetLocalizedFlavorsMessage(entity.Owner, args.Target, args.Split);
if (args.ForceFed)
{
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;
}
_reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
_stomach.TryTransferSolution(stomachToUse!.Value.Owner, split, stomachToUse);
var flavors = args.FlavorMessage;
if (forceFeed)
{
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
// BREAK OUR UTENSILS
if (_ingestion.TryGetUtensils(args.User, entity, out var utensils))
{
foreach (var utensil in utensils)
{
_utensil.TryBreak(utensil, args.User);
_ingestion.TryBreak(utensil, args.User);
}
}
args.Repeat = !forceFeed;
if (TryComp<StackComponent>(entity, out var stack))
if (_ingestion.GetUsesRemaining(entity, entity.Comp.Solution, args.Split.Volume) > 0)
{
//Not deleting whole stack piece will make troubles with grinding object
if (stack.Count > 1)
// Leave some of the consumer's DNA on the consumed item...
var ev = new TransferDnaEvent
{
_stack.SetCount(entity.Owner, stack.Count - 1);
_solutionContainer.TryAddSolution(soln.Value, split);
return;
}
}
else if (GetUsesRemaining(entity.Owner, entity.Comp) > 0)
{
return;
}
// don't try to repeat if its being deleted
args.Repeat = false;
DeleteAndSpawnTrash(entity.Comp, entity.Owner, args.User);
}
public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityUid user)
{
var ev = new BeforeFullyEatenEvent
{
User = user
Donor = args.Target,
Recipient = entity,
CanDnaBeCleaned = false,
};
RaiseLocalEvent(food, ev);
if (ev.Cancelled)
return;
RaiseLocalEvent(args.Target, ref ev);
var attemptEv = new DestructionAttemptEvent();
RaiseLocalEvent(food, attemptEv);
if (attemptEv.Cancelled)
return;
var afterEvent = new AfterFullyEatenEvent(user);
RaiseLocalEvent(food, ref afterEvent);
var dev = new DestructionEventArgs();
RaiseLocalEvent(food, dev);
if (component.Trash.Count == 0)
{
PredictedQueueDel(food);
args.Repeat = !args.ForceFed;
return;
}
//We're empty. Become trash.
//cache some data as we remove food, before spawning trash and passing it to the hand.
// Food is always destroyed...
args.Destroy = true;
}
private void OnFoodFullyEaten(Entity<FoodComponent> food, ref FullyEatenEvent args)
{
if (food.Comp.Trash.Count == 0)
return;
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)
{
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;
// have to kill mouse before eating it
if (_mobState.IsAlive(entity) && entity.Comp.RequireDead)
return;
// only give moths eat verb for clothes since it would just fail otherwise
if (!IsDigestibleBy(entity, entity.Comp, stomachs))
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);
}
/// <summary>
/// Returns true if the food item can be digested by the user.
/// </summary>
public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null)
{
if (!Resolve(food, ref foodComp, false))
return false;
if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(uid, out var stomachs))
return false;
return IsDigestibleBy(food, foodComp, stomachs);
}
/// <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)
{
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;
return digestible;
}
private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,
out List<EntityUid> utensils, HandsComponent? hands = null)
{
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;
}
return true;
}
/// <summary>
/// Block ingestion attempts based on the equipped mask or head-wear
/// </summary>
private void OnInventoryIngestAttempt(Entity<InventoryComponent> entity, ref IngestionAttemptEvent args)
private void OnFood(Entity<FoodComponent> food, ref EdibleEvent args)
{
if (args.Cancelled)
return;
IngestionBlockerComponent? blocker;
if (args.Cancelled || args.Solution != null)
return;
if (_inventory.TryGetSlotEntity(entity.Owner, "mask", out var maskUid) &&
TryComp(maskUid, out blocker) &&
blocker.Enabled)
if (food.Comp.UtensilRequired && !_ingestion.HasRequiredUtensils(args.User, food.Comp.Utensil))
{
args.Blocker = maskUid;
args.Cancel();
args.Cancelled = true;
return;
}
if (_inventory.TryGetSlotEntity(entity.Owner, "head", out var headUid) &&
TryComp(headUid, out blocker) &&
blocker.Enabled)
{
args.Blocker = headUid;
args.Cancel();
}
// Check this last
_solutionContainer.TryGetSolution(food.Owner, food.Comp.Solution, out args.Solution);
args.Time += TimeSpan.FromSeconds(food.Comp.Delay);
}
private void OnGetUtensils(Entity<FoodComponent> entity, ref GetUtensilsEvent args)
{
if (entity.Comp.Utensil == UtensilType.None)
return;
/// <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 (entity.Comp.UtensilRequired)
args.AddRequiredTypes(entity.Comp.Utensil);
else
args.Types |= entity.Comp.Utensil;
}
return attempt.Cancelled;
// TODO: When DrinkComponent and FoodComponent are properly obseleted, make the IsDigestionBools in IngestionSystem private again.
private void OnIsFoodDigestible(Entity<FoodComponent> ent, ref IsDigestibleEvent args)
{
if (ent.Comp.RequireDead && _mobState.IsAlive(ent))
return;
args.AddDigestible(ent.Comp.RequiresSpecialDigestion);
}
/// <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)
private void OnGetEdibleType(Entity<FoodComponent> ent, ref GetEdibleTypeEvent args)
{
if (!Resolve(uid, ref comp))
return 0;
if (args.Type != null)
return;
if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out _, out var solution) || solution.Volume == 0)
return 0;
args.SetPrototype(IngestionSystem.Food);
}
// eat all in 1 go, so non empty is 1 bite
if (comp.TransferAmount == null)
return 1;
private void OnBeforeFullySliced(Entity<FoodComponent> food, ref BeforeFullySlicedEvent args)
{
if (food.Comp.Trash.Count == 0)
return;
return Math.Max(1, (int) Math.Ceiling((solution.Volume / (FixedPoint2) comp.TransferAmount).Float()));
var position = _transform.GetMapCoordinates(food);
var trashes = food.Comp.Trash;
var tryPickup = _hands.IsHolding(args.User, food, 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(args.User, spawnedTrash);
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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)

View 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;
if (_openable.IsClosed(item, user, predicted: true))
return true;
if (!_solutionContainer.TryGetSolution(item, drink.Solution, out _, out var drinkSolution) || drinkSolution.Volume <= 0)
{
if (drink.IgnoreEmpty)
return false;
_popup.PopupClient(Loc.GetString("drink-component-try-use-drink-is-empty", ("entity", item)), item, user);
return true;
ev.Handled = _ingestion.TryIngest(ev.User, ev.User, entity);
}
if (_food.IsMouthBlocked(target, user))
return true;
if (!_interaction.InRangeUnobstructed(user, item, popup: true))
return true;
var forceDrink = user != target;
if (forceDrink)
/// <summary>
/// Feed someone else
/// </summary>
private void OnUseDrink(Entity<DrinkComponent> entity, ref AfterInteractEvent args)
{
var userName = Identity.Entity(user, EntityManager);
if (args.Handled || args.Target == null || !args.CanReach)
return;
_popup.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)), user, target);
args.Handled = _ingestion.TryIngest(args.User, args.Target.Value, entity);
}
// logging
_adminLogger.Add(LogType.ForceFeed, LogImpact.High, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(item):drink} {SharedSolutionContainerSystem.ToPrettyString(drinkSolution)}");
private void AddDrinkVerb(Entity<DrinkComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
var user = args.User;
if (entity.Owner == user || !args.CanInteract || !args.CanAccess)
return;
if (!_ingestion.TryGetIngestionVerb(user, entity, IngestionSystem.Drink, out var verb))
return;
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;
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 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.PopupClient(Loc.GetString("edible-force-feed-success-user", ("target", targetName), ("verb", _ingestion.GetProtoVerb(IngestionSystem.Drink))), 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
{
// 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);
_doAfter.TryStartDoAfter(doAfterEventArgs);
return true;
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;
args.SetPrototype(IngestionSystem.Drink);
}
}

View 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);
}
}
}

View 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>
public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
[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>
[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.

View File

@@ -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;
}

View 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>

View 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;

View 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

View File

@@ -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)}

View File

@@ -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)}

View 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!

View File

@@ -0,0 +1,2 @@
sealable-component-on-examine-is-sealed = The seal is intact.
sealable-component-on-examine-is-unsealed = The seal is broken.

View File

@@ -4,6 +4,12 @@
name: ruminant stomach
categories: [ HideSpawnMenu ]
components:
- type: Stomach
specialDigestible:
tags:
- Ruminant
- Wheat
- BananaPeel
- type: SolutionContainerManager
solutions:
stomach:

View File

@@ -315,15 +315,16 @@
- 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:

View File

@@ -201,7 +201,8 @@
- type: EdgeSpreader
id: Puddle
- type: StepTrigger
- type: Drink
- type: Edible
edible: Drink
delay: 3
transferAmount: 1
solution: puddle

View File

@@ -1054,10 +1054,10 @@
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:

View File

@@ -14,7 +14,10 @@
solution: drink
- type: SolutionTransfer
canChangeTransferAmount: true
- type: Drink
- type: Edible
edible: Drink
solution: drink
destroyOnEmpty: false
- type: Sprite
state: icon
- type: MeleeWeapon

View File

@@ -95,8 +95,6 @@
flavors:
- bun
- meaty
- type: Food
transferAmount: 5
- type: Sprite
sprite: Objects/Consumable/Food/burger.rsi
- type: SolutionContainerManager

View File

@@ -9,7 +9,7 @@
- type: FlavorProfile
flavors:
- food
- type: Food
- type: Edible
- type: Sprite
- type: StaticPrice
price: 0

View File

@@ -10,13 +10,16 @@
- 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
@@ -176,6 +180,7 @@
- type: Tag
tags:
- Vegetable
- Ruminant
- type: entity
name: tower-cap log
@@ -323,7 +328,7 @@
- type: FlavorProfile
flavors:
- banana
- type: Food
- type: Edible
trash:
- TrashBananaPeel
- type: SolutionContainerManager
@@ -365,7 +370,7 @@
flavors:
- banana
- nothing
- type: Food
- type: Edible
trash:
- TrashMimanaPeel
- type: SolutionContainerManager
@@ -437,6 +442,7 @@
- Recyclable
- Trash
- BananaPeel
- Ruminant
- WhitelistChameleon
- HamsterWearable
- type: SolutionContainerManager
@@ -449,7 +455,7 @@
- type: Extractable
grindableSolutionName: food
- type: SpaceGarbage
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: Clothing
sprite: Objects/Specific/Hydroponics/banana.rsi
@@ -1190,7 +1196,7 @@
- type: FlavorProfile
flavors:
- corn
- type: Food
- type: Edible
trash:
- FoodCornTrash
- type: SolutionContainerManager
@@ -1895,7 +1901,7 @@
sprite: Objects/Specific/Hydroponics/gatfruit.rsi
- type: Produce
seedId: gatfruit
- type: Food
- type: Edible
trash:
- WeaponRevolverPython
- type: Tag
@@ -1930,7 +1936,7 @@
heldPrefix: produce
- type: Produce
seedId: realCapfruit
- type: Food
- type: Edible
trash:
- RevolverCapGun
- type: Tag
@@ -1949,7 +1955,7 @@
components:
- type: Produce
seedId: fakeCapfruit
- type: Food
- type: Edible
trash:
- RevolverCapGunFake
@@ -2353,7 +2359,7 @@
- type: FlavorProfile
flavors:
- bungo
- type: Food
- type: Edible
trash:
- FoodBungoPit
- type: SolutionContainerManager
@@ -2588,7 +2594,7 @@
- type: FlavorProfile
flavors:
- cotton
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: SolutionContainerManager
solutions:
@@ -2620,7 +2626,7 @@
- type: FlavorProfile
flavors:
- pyrotton
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: SolutionContainerManager
solutions:
@@ -2654,7 +2660,7 @@
- type: FlavorProfile
flavors:
- cherry
- type: Food
- type: Edible
trash:
- TrashCherryPit
- type: SolutionContainerManager
@@ -2729,7 +2735,7 @@
heldPrefix: produce
- type: Produce
seedId: anomalyBerry
- type: Food
- type: Edible
trash:
- EffectAnomalyFloraBulb # Random loot
- type: SolutionContainerManager

View File

@@ -123,7 +123,7 @@
- state: cloth_3
map: ["base"]
- type: Appearance
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: FlavorProfile
flavors:
@@ -192,7 +192,7 @@
- type: Construction
graph: Durathread
node: MaterialDurathread
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: SolutionContainerManager
solutions:
@@ -421,7 +421,7 @@
- state: cotton_3
map: ["base"]
- type: Appearance
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: FlavorProfile
flavors:
@@ -480,7 +480,7 @@
- state: pyrotton_3
map: ["base"]
- type: Appearance
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: SolutionContainerManager
solutions:
@@ -540,7 +540,7 @@
- type: FlavorProfile
flavors:
- banana
- type: Food
- type: Edible
trash:
- TrashBananiumPeel
- type: BadFood
@@ -592,7 +592,7 @@
- type: Stack
count: 50
stackType: WebSilk
- type: Food
- type: Edible
requiresSpecialDigestion: true
- type: FlavorProfile
flavors:
@@ -735,7 +735,7 @@
- state: cotton_3
map: ["base"]
- type: Appearance
- type: Food
- type: Edible
- type: BadFood
- type: SolutionContainerManager
solutions:

View 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:
@@ -101,6 +101,9 @@
reagents:
- ReagentId: Nutriment
Quantity: 2
- type: Tag
tags:
- Ruminant
- type: entity
id: WeakKudzu

View File

@@ -602,12 +602,11 @@
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: []

View 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:

View 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

View File

@@ -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

View File

@@ -1199,6 +1199,9 @@
- type: Tag
id: RollingPin
- type: Tag
id: Ruminant
- type: Tag
id: SaltShaker