1
0

FoodSystem.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. using Content.Server.Body.Components;
  2. using Content.Server.Body.Systems;
  3. using Content.Shared.Chemistry.EntitySystems;
  4. using Content.Server.Inventory;
  5. using Content.Server.Nutrition.Components;
  6. using Content.Shared.Nutrition.Components;
  7. using Content.Server.Popups;
  8. using Content.Server.Stack;
  9. using Content.Shared.Administration.Logs;
  10. using Content.Shared.Body.Components;
  11. using Content.Shared.Body.Organ;
  12. using Content.Shared.Chemistry;
  13. using Content.Shared.Database;
  14. using Content.Shared.DoAfter;
  15. using Content.Shared.FixedPoint;
  16. using Content.Shared.Hands.Components;
  17. using Content.Shared.Hands.EntitySystems;
  18. using Content.Shared.IdentityManagement;
  19. using Content.Shared.Interaction;
  20. using Content.Shared.Interaction.Components;
  21. using Content.Shared.Interaction.Events;
  22. using Content.Shared.Inventory;
  23. using Content.Shared.Mobs.Systems;
  24. using Content.Shared.Nutrition;
  25. using Content.Shared.Nutrition.EntitySystems;
  26. using Content.Shared.Stacks;
  27. using Content.Shared.Storage;
  28. using Content.Shared.Verbs;
  29. using Robust.Shared.Audio;
  30. using Robust.Shared.Audio.Systems;
  31. using Robust.Shared.Utility;
  32. using System.Linq;
  33. using Content.Shared.Containers.ItemSlots;
  34. using Robust.Server.GameObjects;
  35. using Content.Shared.Whitelist;
  36. using Content.Shared.Destructible;
  37. namespace Content.Server.Nutrition.EntitySystems;
  38. /// <summary>
  39. /// Handles feeding attempts both on yourself and on the target.
  40. /// </summary>
  41. public sealed class FoodSystem : EntitySystem
  42. {
  43. [Dependency] private readonly BodySystem _body = default!;
  44. [Dependency] private readonly FlavorProfileSystem _flavorProfile = default!;
  45. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  46. [Dependency] private readonly InventorySystem _inventory = default!;
  47. [Dependency] private readonly MobStateSystem _mobState = default!;
  48. [Dependency] private readonly OpenableSystem _openable = default!;
  49. [Dependency] private readonly PopupSystem _popup = default!;
  50. [Dependency] private readonly ReactiveSystem _reaction = default!;
  51. [Dependency] private readonly SharedAudioSystem _audio = default!;
  52. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  53. [Dependency] private readonly SharedHandsSystem _hands = default!;
  54. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  55. [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
  56. [Dependency] private readonly TransformSystem _transform = default!;
  57. [Dependency] private readonly StackSystem _stack = default!;
  58. [Dependency] private readonly StomachSystem _stomach = default!;
  59. [Dependency] private readonly UtensilSystem _utensil = default!;
  60. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  61. public const float MaxFeedDistance = 1.0f;
  62. public override void Initialize()
  63. {
  64. base.Initialize();
  65. // TODO add InteractNoHandEvent for entities like mice.
  66. // run after openable for wrapped/peelable foods
  67. SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand, after: new[] { typeof(OpenableSystem), typeof(ServerInventorySystem) });
  68. SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
  69. SubscribeLocalEvent<FoodComponent, GetVerbsEvent<AlternativeVerb>>(AddEatVerb);
  70. SubscribeLocalEvent<FoodComponent, ConsumeDoAfterEvent>(OnDoAfter);
  71. SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(OnInventoryIngestAttempt);
  72. }
  73. /// <summary>
  74. /// Eat item
  75. /// </summary>
  76. private void OnUseFoodInHand(Entity<FoodComponent> entity, ref UseInHandEvent ev)
  77. {
  78. if (ev.Handled)
  79. return;
  80. var result = TryFeed(ev.User, ev.User, entity, entity.Comp);
  81. ev.Handled = result.Handled;
  82. }
  83. /// <summary>
  84. /// Feed someone else
  85. /// </summary>
  86. private void OnFeedFood(Entity<FoodComponent> entity, ref AfterInteractEvent args)
  87. {
  88. if (args.Handled || args.Target == null || !args.CanReach)
  89. return;
  90. var result = TryFeed(args.User, args.Target.Value, entity, entity.Comp);
  91. args.Handled = result.Handled;
  92. }
  93. /// <summary>
  94. /// Tries to feed the food item to the target entity
  95. /// </summary>
  96. public (bool Success, bool Handled) TryFeed(EntityUid user, EntityUid target, EntityUid food, FoodComponent foodComp)
  97. {
  98. //Suppresses eating yourself and alive mobs
  99. if (food == user || (_mobState.IsAlive(food) && foodComp.RequireDead))
  100. return (false, false);
  101. // Target can't be fed or they're already eating
  102. if (!TryComp<BodyComponent>(target, out var body))
  103. return (false, false);
  104. if (HasComp<UnremoveableComponent>(food))
  105. return (false, false);
  106. if (_openable.IsClosed(food, user))
  107. return (false, true);
  108. if (!_solutionContainer.TryGetSolution(food, foodComp.Solution, out _, out var foodSolution))
  109. return (false, false);
  110. if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((target, body), out var stomachs))
  111. return (false, false);
  112. // Check for special digestibles
  113. if (!IsDigestibleBy(food, foodComp, stomachs))
  114. return (false, false);
  115. if (!TryGetRequiredUtensils(user, foodComp, out _))
  116. return (false, false);
  117. // Check for used storage on the food item
  118. if (TryComp<StorageComponent>(food, out var storageState) && storageState.Container.ContainedEntities.Any())
  119. {
  120. _popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
  121. return (false, true);
  122. }
  123. // Checks for used item slots
  124. if (TryComp<ItemSlotsComponent>(food, out var itemSlots))
  125. {
  126. if (itemSlots.Slots.Any(slot => slot.Value.HasItem))
  127. {
  128. _popup.PopupEntity(Loc.GetString("food-has-used-storage", ("food", food)), user, user);
  129. return (false, true);
  130. }
  131. }
  132. var flavors = _flavorProfile.GetLocalizedFlavorsMessage(food, user, foodSolution);
  133. if (GetUsesRemaining(food, foodComp) <= 0)
  134. {
  135. _popup.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", food)), user, user);
  136. DeleteAndSpawnTrash(foodComp, food, user);
  137. return (false, true);
  138. }
  139. if (IsMouthBlocked(target, user))
  140. return (false, true);
  141. if (!_interaction.InRangeUnobstructed(user, food, popup: true))
  142. return (false, true);
  143. if (!_interaction.InRangeUnobstructed(user, target, MaxFeedDistance, popup: true))
  144. return (false, true);
  145. // TODO make do-afters account for fixtures in the range check.
  146. if (!_transform.GetMapCoordinates(user).InRange(_transform.GetMapCoordinates(target), MaxFeedDistance))
  147. {
  148. var message = Loc.GetString("interaction-system-user-interaction-cannot-reach");
  149. _popup.PopupEntity(message, user, user);
  150. return (false, true);
  151. }
  152. var forceFeed = user != target;
  153. if (forceFeed)
  154. {
  155. var userName = Identity.Entity(user, EntityManager);
  156. _popup.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
  157. user, target);
  158. // logging
  159. _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
  160. }
  161. else
  162. {
  163. // log voluntary eating
  164. _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(target):target} is eating {ToPrettyString(food):food} {SharedSolutionContainerSystem.ToPrettyString(foodSolution)}");
  165. }
  166. var doAfterArgs = new DoAfterArgs(EntityManager,
  167. user,
  168. forceFeed ? foodComp.ForceFeedDelay : foodComp.Delay,
  169. new ConsumeDoAfterEvent(foodComp.Solution, flavors),
  170. eventTarget: food,
  171. target: target,
  172. used: food)
  173. {
  174. BreakOnHandChange = false,
  175. BreakOnMove = forceFeed,
  176. BreakOnDamage = true,
  177. MovementThreshold = 0.01f,
  178. DistanceThreshold = MaxFeedDistance,
  179. // do-after will stop if item is dropped when trying to feed someone else
  180. // or if the item started out in the user's own hands
  181. NeedHand = forceFeed || _hands.IsHolding(user, food),
  182. };
  183. _doAfter.TryStartDoAfter(doAfterArgs);
  184. return (true, true);
  185. }
  186. private void OnDoAfter(Entity<FoodComponent> entity, ref ConsumeDoAfterEvent args)
  187. {
  188. if (args.Cancelled || args.Handled || entity.Comp.Deleted || args.Target == null)
  189. return;
  190. if (!TryComp<BodyComponent>(args.Target.Value, out var body))
  191. return;
  192. if (!_body.TryGetBodyOrganEntityComps<StomachComponent>((args.Target.Value, body), out var stomachs))
  193. return;
  194. if (args.Used is null || !_solutionContainer.TryGetSolution(args.Used.Value, args.Solution, out var soln, out var solution))
  195. return;
  196. if (!TryGetRequiredUtensils(args.User, entity.Comp, out var utensils))
  197. return;
  198. // TODO this should really be checked every tick.
  199. if (IsMouthBlocked(args.Target.Value))
  200. return;
  201. // TODO this should really be checked every tick.
  202. if (!_interaction.InRangeUnobstructed(args.User, args.Target.Value))
  203. return;
  204. var forceFeed = args.User != args.Target;
  205. args.Handled = true;
  206. var transferAmount = entity.Comp.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) entity.Comp.TransferAmount, solution.Volume) : solution.Volume;
  207. var split = _solutionContainer.SplitSolution(soln.Value, transferAmount);
  208. // Get the stomach with the highest available solution volume
  209. var highestAvailable = FixedPoint2.Zero;
  210. Entity<StomachComponent>? stomachToUse = null;
  211. foreach (var ent in stomachs)
  212. {
  213. var owner = ent.Owner;
  214. if (!_stomach.CanTransferSolution(owner, split, ent.Comp1))
  215. continue;
  216. if (!_solutionContainer.ResolveSolution(owner, StomachSystem.DefaultSolutionName, ref ent.Comp1.Solution, out var stomachSol))
  217. continue;
  218. if (stomachSol.AvailableVolume <= highestAvailable)
  219. continue;
  220. stomachToUse = ent;
  221. highestAvailable = stomachSol.AvailableVolume;
  222. }
  223. // No stomach so just popup a message that they can't eat.
  224. if (stomachToUse == null)
  225. {
  226. _solutionContainer.TryAddSolution(soln.Value, split);
  227. _popup.PopupEntity(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);
  228. return;
  229. }
  230. _reaction.DoEntityReaction(args.Target.Value, solution, ReactionMethod.Ingestion);
  231. _stomach.TryTransferSolution(stomachToUse!.Value.Owner, split, stomachToUse);
  232. var flavors = args.FlavorMessage;
  233. if (forceFeed)
  234. {
  235. var targetName = Identity.Entity(args.Target.Value, EntityManager);
  236. var userName = Identity.Entity(args.User, EntityManager);
  237. _popup.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName), ("flavors", flavors)), entity.Owner, entity.Owner);
  238. _popup.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)), args.User, args.User);
  239. // log successful force feed
  240. _adminLogger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity.Owner):user} forced {ToPrettyString(args.User):target} to eat {ToPrettyString(entity.Owner):food}");
  241. }
  242. else
  243. {
  244. _popup.PopupEntity(Loc.GetString(entity.Comp.EatMessage, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User);
  245. // log successful voluntary eating
  246. _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity.Owner):food}");
  247. }
  248. _audio.PlayPvs(entity.Comp.UseSound, args.Target.Value, AudioParams.Default.WithVolume(-1f).WithVariation(0.20f));
  249. // Try to break all used utensils
  250. foreach (var utensil in utensils)
  251. {
  252. _utensil.TryBreak(utensil, args.User);
  253. }
  254. args.Repeat = !forceFeed;
  255. if (TryComp<StackComponent>(entity, out var stack))
  256. {
  257. //Not deleting whole stack piece will make troubles with grinding object
  258. if (stack.Count > 1)
  259. {
  260. _stack.SetCount(entity.Owner, stack.Count - 1);
  261. _solutionContainer.TryAddSolution(soln.Value, split);
  262. return;
  263. }
  264. }
  265. else if (GetUsesRemaining(entity.Owner, entity.Comp) > 0)
  266. {
  267. return;
  268. }
  269. // don't try to repeat if its being deleted
  270. args.Repeat = false;
  271. DeleteAndSpawnTrash(entity.Comp, entity.Owner, args.User);
  272. }
  273. public void DeleteAndSpawnTrash(FoodComponent component, EntityUid food, EntityUid user)
  274. {
  275. var ev = new BeforeFullyEatenEvent
  276. {
  277. User = user
  278. };
  279. RaiseLocalEvent(food, ev);
  280. if (ev.Cancelled)
  281. return;
  282. var dev = new DestructionEventArgs();
  283. RaiseLocalEvent(food, dev);
  284. if (component.Trash.Count == 0)
  285. {
  286. QueueDel(food);
  287. return;
  288. }
  289. //We're empty. Become trash.
  290. //cache some data as we remove food, before spawning trash and passing it to the hand.
  291. var position = _transform.GetMapCoordinates(food);
  292. var trashes = component.Trash;
  293. var tryPickup = _hands.IsHolding(user, food, out _);
  294. Del(food);
  295. foreach (var trash in trashes)
  296. {
  297. var spawnedTrash = Spawn(trash, position);
  298. // If the user is holding the item
  299. if (tryPickup)
  300. {
  301. // Put the trash in the user's hand
  302. _hands.TryPickupAnyHand(user, spawnedTrash);
  303. }
  304. }
  305. }
  306. private void AddEatVerb(Entity<FoodComponent> entity, ref GetVerbsEvent<AlternativeVerb> ev)
  307. {
  308. if (entity.Owner == ev.User ||
  309. !ev.CanInteract ||
  310. !ev.CanAccess ||
  311. !TryComp<BodyComponent>(ev.User, out var body) ||
  312. !_body.TryGetBodyOrganEntityComps<StomachComponent>((ev.User, body), out var stomachs))
  313. return;
  314. // have to kill mouse before eating it
  315. if (_mobState.IsAlive(entity) && entity.Comp.RequireDead)
  316. return;
  317. // only give moths eat verb for clothes since it would just fail otherwise
  318. if (!IsDigestibleBy(entity, entity.Comp, stomachs))
  319. return;
  320. var user = ev.User;
  321. AlternativeVerb verb = new()
  322. {
  323. Act = () =>
  324. {
  325. TryFeed(user, user, entity, entity.Comp);
  326. },
  327. Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png")),
  328. Text = Loc.GetString("food-system-verb-eat"),
  329. Priority = -1
  330. };
  331. ev.Verbs.Add(verb);
  332. }
  333. /// <summary>
  334. /// Returns true if the food item can be digested by the user.
  335. /// </summary>
  336. public bool IsDigestibleBy(EntityUid uid, EntityUid food, FoodComponent? foodComp = null)
  337. {
  338. if (!Resolve(food, ref foodComp, false))
  339. return false;
  340. if (!_body.TryGetBodyOrganEntityComps<StomachComponent>(uid, out var stomachs))
  341. return false;
  342. return IsDigestibleBy(food, foodComp, stomachs);
  343. }
  344. /// <summary>
  345. /// Returns true if <paramref name="stomachs"/> has a <see cref="StomachComponent.SpecialDigestible"/> that whitelists
  346. /// this <paramref name="food"/> (or if they even have enough stomachs in the first place).
  347. /// </summary>
  348. private bool IsDigestibleBy(EntityUid food, FoodComponent component, List<Entity<StomachComponent, OrganComponent>> stomachs)
  349. {
  350. var digestible = true;
  351. // Does the mob have enough stomachs?
  352. if (stomachs.Count < component.RequiredStomachs)
  353. return false;
  354. // Run through the mobs' stomachs
  355. foreach (var ent in stomachs)
  356. {
  357. // Find a stomach with a SpecialDigestible
  358. if (ent.Comp1.SpecialDigestible == null)
  359. continue;
  360. // Check if the food is in the whitelist
  361. if (_whitelistSystem.IsWhitelistPass(ent.Comp1.SpecialDigestible, food))
  362. return true;
  363. // They can only eat whitelist food and the food isn't in the whitelist. It's not edible.
  364. return false;
  365. }
  366. if (component.RequiresSpecialDigestion)
  367. return false;
  368. return digestible;
  369. }
  370. private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,
  371. out List<EntityUid> utensils, HandsComponent? hands = null)
  372. {
  373. utensils = new List<EntityUid>();
  374. if (component.Utensil == UtensilType.None)
  375. return true;
  376. if (!Resolve(user, ref hands, false))
  377. return true; //mice
  378. var usedTypes = UtensilType.None;
  379. foreach (var item in _hands.EnumerateHeld(user, hands))
  380. {
  381. // Is utensil?
  382. if (!TryComp<UtensilComponent>(item, out var utensil))
  383. continue;
  384. if ((utensil.Types & component.Utensil) != 0 && // Acceptable type?
  385. (usedTypes & utensil.Types) != utensil.Types) // Type is not used already? (removes usage of identical utensils)
  386. {
  387. // Add to used list
  388. usedTypes |= utensil.Types;
  389. utensils.Add(item);
  390. }
  391. }
  392. // If "required" field is set, try to block eating without proper utensils used
  393. if (component.UtensilRequired && (usedTypes & component.Utensil) != component.Utensil)
  394. {
  395. _popup.PopupEntity(Loc.GetString("food-you-need-to-hold-utensil", ("utensil", component.Utensil ^ usedTypes)), user, user);
  396. return false;
  397. }
  398. return true;
  399. }
  400. /// <summary>
  401. /// Block ingestion attempts based on the equipped mask or head-wear
  402. /// </summary>
  403. private void OnInventoryIngestAttempt(Entity<InventoryComponent> entity, ref IngestionAttemptEvent args)
  404. {
  405. if (args.Cancelled)
  406. return;
  407. IngestionBlockerComponent? blocker;
  408. if (_inventory.TryGetSlotEntity(entity.Owner, "mask", out var maskUid) &&
  409. TryComp(maskUid, out blocker) &&
  410. blocker.Enabled)
  411. {
  412. args.Blocker = maskUid;
  413. args.Cancel();
  414. return;
  415. }
  416. if (_inventory.TryGetSlotEntity(entity.Owner, "head", out var headUid) &&
  417. TryComp(headUid, out blocker) &&
  418. blocker.Enabled)
  419. {
  420. args.Blocker = headUid;
  421. args.Cancel();
  422. }
  423. }
  424. /// <summary>
  425. /// Check whether the target's mouth is blocked by equipment (masks or head-wear).
  426. /// </summary>
  427. /// <param name="uid">The target whose equipment is checked</param>
  428. /// <param name="popupUid">Optional entity that will receive an informative pop-up identifying the blocking
  429. /// piece of equipment.</param>
  430. /// <returns></returns>
  431. public bool IsMouthBlocked(EntityUid uid, EntityUid? popupUid = null)
  432. {
  433. var attempt = new IngestionAttemptEvent();
  434. RaiseLocalEvent(uid, attempt, false);
  435. if (attempt.Cancelled && attempt.Blocker != null && popupUid != null)
  436. {
  437. _popup.PopupEntity(Loc.GetString("food-system-remove-mask", ("entity", attempt.Blocker.Value)),
  438. uid, popupUid.Value);
  439. }
  440. return attempt.Cancelled;
  441. }
  442. /// <summary>
  443. /// 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.
  444. /// </summary>
  445. public int GetUsesRemaining(EntityUid uid, FoodComponent? comp = null)
  446. {
  447. if (!Resolve(uid, ref comp))
  448. return 0;
  449. if (!_solutionContainer.TryGetSolution(uid, comp.Solution, out _, out var solution) || solution.Volume == 0)
  450. return 0;
  451. // eat all in 1 go, so non empty is 1 bite
  452. if (comp.TransferAmount == null)
  453. return 1;
  454. return Math.Max(1, (int) Math.Ceiling((solution.Volume / (FixedPoint2) comp.TransferAmount).Float()));
  455. }
  456. }