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.Body; 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; /// /// Public API for Ingestion System so you can build your own form of ingestion system. /// public sealed partial class IngestionSystem { // List of prototypes that other components or systems might want. public static readonly ProtoId Food = "Food"; public static readonly ProtoId 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 /// /// An entity is trying to ingest another entity in Space Station 14!!! /// /// The entity who is eating. /// The entity that is trying to be ingested. /// Returns true if we are now ingesting the item. public bool TryIngest(EntityUid user, EntityUid ingested) { return TryIngest(user, user, ingested); } /// /// Overload of TryIngest for if an entity is trying to make another entity ingest an entity /// The entity who is trying to make this happen. /// The entity who is being made to ingest something. /// The entity that is trying to be ingested. public bool TryIngest(EntityUid user, EntityUid target, EntityUid ingested) { return AttemptIngest(user, target, ingested, true); } /// /// Checks if we can ingest a given entity without actually ingesting it. /// /// The entity doing the ingesting. /// The ingested entity. /// Returns true if it's possible for the entity to ingest this item. public bool CanIngest(EntityUid user, EntityUid ingested) { return AttemptIngest(user, user, ingested, false); } /// /// Check whether we have an open pie-hole that's in range. /// /// The one performing the action /// The target whose mouth is checked /// public bool HasMouthAvailable(EntityUid user, EntityUid target) { return HasMouthAvailable(user, target, DefaultFlags); } /// /// 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; } /// /// The entity that is consuming /// The entity that is being consumed public bool CanConsume(EntityUid user, EntityUid ingested) { return CanConsume(user, user, ingested, out _, out _); } /// /// Checks if we can feed an edible solution from an entity to a target. /// /// The one doing the feeding /// The one being fed. /// The food item being eaten. /// Returns true if the user can feed the target with the ingested entity public bool CanConsume(EntityUid user, EntityUid target, EntityUid ingested) { return CanConsume(user, target, ingested, out _, out _); } /// /// The one doing the feeding /// The one being fed. /// The food item being eaten. /// The solution we will be consuming from. /// The time it takes us to eat this entity if any. /// Returns true if the user can feed the target with the ingested entity and also returns a solution public bool CanConsume(EntityUid user, EntityUid target, EntityUid ingested, [NotNullWhen(true)] out Entity? 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 entity, EntityUid? user = null) { if (entity.Comp.Trash.Count == 0) return; var position = _transform.GetMapCoordinates(entity); var trashes = entity.Comp.Trash; var pickup = user != null && _hands.IsHolding(user.Value, entity, out _); foreach (var trash in trashes) { var spawnedTrash = EntityManager.PredictedSpawn(trash, position); // If the user is holding the item if (!pickup) continue; // Put the trash in the user's hand // I am 100% confident we don't need this check but rider gets made at me if it's not here. if (user != null) _hands.TryPickupAnyHand(user.Value, spawnedTrash); } } public void AddTrash(Entity entity, List newTrash) { foreach (var trash in newTrash) { entity.Comp.Trash.Add(trash); } } public FixedPoint2 EdibleVolume(Entity entity) { if (!_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution)) return FixedPoint2.Zero; return solution.Volume; } public bool IsEmpty(Entity entity) { return EdibleVolume(entity) == FixedPoint2.Zero; } /// /// 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. /// /// The consumed entity /// The entity doing the consuming /// The amount of nutrition the consumable is worth public float TotalNutrition(Entity entity, EntityUid consumer) { if (!CanIngest(consumer, entity)) return 0f; return TotalNutrition(entity); } /// /// Gets the total metabolizable nutrition from an entity, assumes we can eat and metabolize it. /// /// The consumed entity /// The amount of nutrition the consumable is worth public float TotalNutrition(Entity 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(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.Factor * quantity.Quantity.Float(); } } } } return total; } /// /// 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. /// /// The consumed entity /// The entity doing the consuming /// The amount of hydration the consumable is worth public float TotalHydration(Entity entity, EntityUid consumer) { if (!CanIngest(consumer, entity)) return 0f; return TotalNutrition(entity); } /// /// Gets the total metabolizable hydration from an entity, assumes we can eat and metabolize it. /// /// The consumed entity /// The amount of hydration the consumable is worth public float TotalHydration(Entity 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(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.Factor * quantity.Quantity.Float(); } } } } return total; } #endregion #region Solutions /// /// Checks if the item is currently edible. /// /// Entity being ingested /// The entity trying to make the ingestion happening, not necessarily the one eating /// Solution we're returning /// The time it takes us to eat this entity public bool CanAccessSolution(Entity ingested, EntityUid user, [NotNullWhen(true)] out Entity? 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; } /// /// 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. /// 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 /// /// Tries to get the ingestion verbs for a given user entity and ingestible entity /// /// The one getting the verbs who would be doing the eating. /// Entity being ingested. /// Edible prototype. /// Verb we're returning. /// Returns true if we generated a verb. public bool TryGetIngestionVerb(EntityUid user, EntityUid ingested, [ForbidLiteral] ProtoId 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; } /// /// Returns the most accurate edible prototype for an entity if one exists. /// /// entity who's edible prototype we want /// The best matching prototype if one exists. public ProtoId? GetEdibleType(Entity 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 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 proto) { var prototype = _proto.Index(proto); return GetProtoNoun(prototype); } public string GetProtoNoun(EdiblePrototype proto) { return Loc.GetString(proto.Noun); } public string GetEdibleVerb(Entity 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 proto) { var prototype = _proto.Index(proto); return GetProtoVerb(prototype); } public string GetProtoVerb(EdiblePrototype proto) { return Loc.GetString(proto.Verb); } #endregion }