ItemSlotsSystem.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Shared.ActionBlocker;
  3. using Content.Shared.Administration.Logs;
  4. using Content.Shared.Database;
  5. using Content.Shared.Destructible;
  6. using Content.Shared.Hands.Components;
  7. using Content.Shared.Hands.EntitySystems;
  8. using Content.Shared.Interaction;
  9. using Content.Shared.Interaction.Events;
  10. using Content.Shared.Popups;
  11. using Content.Shared.Verbs;
  12. using Content.Shared.Whitelist;
  13. using Robust.Shared.Audio.Systems;
  14. using Robust.Shared.Containers;
  15. using Robust.Shared.GameStates;
  16. using Robust.Shared.Utility;
  17. namespace Content.Shared.Containers.ItemSlots
  18. {
  19. /// <summary>
  20. /// A class that handles interactions related to inserting/ejecting items into/from an item slot.
  21. /// </summary>
  22. /// <remarks>
  23. /// Note when using popups on entities with many slots with InsertOnInteract, EjectOnInteract or EjectOnUse:
  24. /// A single use will try to insert to/eject from every slot and generate a popup for each that fails.
  25. /// </remarks>
  26. public sealed partial class ItemSlotsSystem : EntitySystem
  27. {
  28. [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
  29. [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
  30. [Dependency] private readonly SharedContainerSystem _containers = default!;
  31. [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
  32. [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
  33. [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
  34. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  35. public override void Initialize()
  36. {
  37. base.Initialize();
  38. InitializeLock();
  39. SubscribeLocalEvent<ItemSlotsComponent, MapInitEvent>(OnMapInit);
  40. SubscribeLocalEvent<ItemSlotsComponent, ComponentInit>(Oninitialize);
  41. SubscribeLocalEvent<ItemSlotsComponent, InteractUsingEvent>(OnInteractUsing);
  42. SubscribeLocalEvent<ItemSlotsComponent, InteractHandEvent>(OnInteractHand);
  43. SubscribeLocalEvent<ItemSlotsComponent, UseInHandEvent>(OnUseInHand);
  44. SubscribeLocalEvent<ItemSlotsComponent, GetVerbsEvent<AlternativeVerb>>(AddAlternativeVerbs);
  45. SubscribeLocalEvent<ItemSlotsComponent, GetVerbsEvent<InteractionVerb>>(AddInteractionVerbsVerbs);
  46. SubscribeLocalEvent<ItemSlotsComponent, BreakageEventArgs>(OnBreak);
  47. SubscribeLocalEvent<ItemSlotsComponent, DestructionEventArgs>(OnBreak);
  48. SubscribeLocalEvent<ItemSlotsComponent, ComponentGetState>(GetItemSlotsState);
  49. SubscribeLocalEvent<ItemSlotsComponent, ComponentHandleState>(HandleItemSlotsState);
  50. SubscribeLocalEvent<ItemSlotsComponent, ItemSlotButtonPressedEvent>(HandleButtonPressed);
  51. }
  52. #region ComponentManagement
  53. /// <summary>
  54. /// Spawn in starting items for any item slots that should have one.
  55. /// </summary>
  56. private void OnMapInit(EntityUid uid, ItemSlotsComponent itemSlots, MapInitEvent args)
  57. {
  58. foreach (var slot in itemSlots.Slots.Values)
  59. {
  60. if (slot.HasItem || string.IsNullOrEmpty(slot.StartingItem))
  61. continue;
  62. var item = Spawn(slot.StartingItem, Transform(uid).Coordinates);
  63. if (slot.ContainerSlot != null)
  64. _containers.Insert(item, slot.ContainerSlot);
  65. }
  66. }
  67. /// <summary>
  68. /// Ensure item slots have containers.
  69. /// </summary>
  70. private void Oninitialize(EntityUid uid, ItemSlotsComponent itemSlots, ComponentInit args)
  71. {
  72. foreach (var (id, slot) in itemSlots.Slots)
  73. {
  74. slot.ContainerSlot = _containers.EnsureContainer<ContainerSlot>(uid, id);
  75. }
  76. }
  77. /// <summary>
  78. /// Given a new item slot, store it in the <see cref="ItemSlotsComponent"/> and ensure the slot has an item
  79. /// container.
  80. /// </summary>
  81. public void AddItemSlot(EntityUid uid, string id, ItemSlot slot, ItemSlotsComponent? itemSlots = null)
  82. {
  83. itemSlots ??= EnsureComp<ItemSlotsComponent>(uid);
  84. DebugTools.AssertOwner(uid, itemSlots);
  85. if (itemSlots.Slots.TryGetValue(id, out var existing))
  86. {
  87. if (existing.Local)
  88. Log.Error(
  89. $"Duplicate item slot key. Entity: {EntityManager.GetComponent<MetaDataComponent>(uid).EntityName} ({uid}), key: {id}");
  90. else
  91. // server state takes priority
  92. slot.CopyFrom(existing);
  93. }
  94. slot.ContainerSlot = _containers.EnsureContainer<ContainerSlot>(uid, id);
  95. itemSlots.Slots[id] = slot;
  96. Dirty(uid, itemSlots);
  97. }
  98. /// <summary>
  99. /// Remove an item slot. This should generally be called whenever a component that added a slot is being
  100. /// removed.
  101. /// </summary>
  102. public void RemoveItemSlot(EntityUid uid, ItemSlot slot, ItemSlotsComponent? itemSlots = null)
  103. {
  104. if (Terminating(uid) || slot.ContainerSlot == null)
  105. return;
  106. _containers.ShutdownContainer(slot.ContainerSlot);
  107. // Don't log missing resolves. when an entity has all of its components removed, the ItemSlotsComponent may
  108. // have been removed before some other component that added an item slot (and is now trying to remove it).
  109. if (!Resolve(uid, ref itemSlots, logMissing: false))
  110. return;
  111. itemSlots.Slots.Remove(slot.ContainerSlot.ID);
  112. if (itemSlots.Slots.Count == 0)
  113. EntityManager.RemoveComponent(uid, itemSlots);
  114. else
  115. Dirty(uid, itemSlots);
  116. }
  117. public bool TryGetSlot(EntityUid uid,
  118. string slotId,
  119. [NotNullWhen(true)] out ItemSlot? itemSlot,
  120. ItemSlotsComponent? component = null)
  121. {
  122. itemSlot = null;
  123. if (!Resolve(uid, ref component))
  124. return false;
  125. return component.Slots.TryGetValue(slotId, out itemSlot);
  126. }
  127. #endregion
  128. #region Interactions
  129. /// <summary>
  130. /// Attempt to take an item from a slot, if any are set to EjectOnInteract.
  131. /// </summary>
  132. private void OnInteractHand(EntityUid uid, ItemSlotsComponent itemSlots, InteractHandEvent args)
  133. {
  134. if (args.Handled)
  135. return;
  136. foreach (var slot in itemSlots.Slots.Values)
  137. {
  138. if (!slot.EjectOnInteract || slot.Item == null || !CanEject(uid, args.User, slot, popup: args.User))
  139. continue;
  140. args.Handled = true;
  141. TryEjectToHands(uid, slot, args.User, true);
  142. break;
  143. }
  144. }
  145. /// <summary>
  146. /// Attempt to eject an item from the first valid item slot.
  147. /// </summary>
  148. private void OnUseInHand(EntityUid uid, ItemSlotsComponent itemSlots, UseInHandEvent args)
  149. {
  150. if (args.Handled)
  151. return;
  152. foreach (var slot in itemSlots.Slots.Values)
  153. {
  154. if (!slot.EjectOnUse || slot.Item == null || !CanEject(uid, args.User, slot, popup: args.User))
  155. continue;
  156. args.Handled = true;
  157. TryEjectToHands(uid, slot, args.User, true);
  158. break;
  159. }
  160. }
  161. /// <summary>
  162. /// Tries to insert a held item in any fitting item slot. If a valid slot already contains an item, it will
  163. /// swap it out and place the old one in the user's hand.
  164. /// </summary>
  165. /// <remarks>
  166. /// This only handles the event if the user has an applicable entity that can be inserted. This allows for
  167. /// other interactions to still happen (e.g., open UI, or toggle-open), despite the user holding an item.
  168. /// Maybe this is undesirable.
  169. /// </remarks>
  170. private void OnInteractUsing(EntityUid uid, ItemSlotsComponent itemSlots, InteractUsingEvent args)
  171. {
  172. if (args.Handled)
  173. return;
  174. if (!EntityManager.TryGetComponent(args.User, out HandsComponent? hands))
  175. return;
  176. if (itemSlots.Slots.Count == 0)
  177. return;
  178. // If any slot can be inserted into don't show popup.
  179. // If any whitelist passes, but slot is locked, then show locked.
  180. // If whitelist fails all, show whitelist fail.
  181. // valid, insertable slots (if any)
  182. var slots = new List<ItemSlot>();
  183. string? whitelistFailPopup = null;
  184. string? lockedFailPopup = null;
  185. foreach (var slot in itemSlots.Slots.Values)
  186. {
  187. if (!slot.InsertOnInteract)
  188. continue;
  189. if (CanInsert(uid, args.Used, args.User, slot, slot.Swap))
  190. {
  191. slots.Add(slot);
  192. }
  193. else
  194. {
  195. var allowed = CanInsertWhitelist(args.Used, slot);
  196. if (lockedFailPopup == null && slot.LockedFailPopup != null && allowed && slot.Locked)
  197. lockedFailPopup = slot.LockedFailPopup;
  198. if (whitelistFailPopup == null && slot.WhitelistFailPopup != null)
  199. whitelistFailPopup = slot.WhitelistFailPopup;
  200. }
  201. }
  202. if (slots.Count == 0)
  203. {
  204. // it's a bit weird that the popupMessage is stored with the item slots themselves, but in practice
  205. // the popup messages will just all be the same, so it's probably fine.
  206. //
  207. // doing a check to make sure that they're all the same or something is probably frivolous
  208. if (lockedFailPopup != null)
  209. _popupSystem.PopupClient(Loc.GetString(lockedFailPopup), uid, args.User);
  210. else if (whitelistFailPopup != null)
  211. _popupSystem.PopupClient(Loc.GetString(whitelistFailPopup), uid, args.User);
  212. return;
  213. }
  214. // Drop the held item onto the floor. Return if the user cannot drop.
  215. if (!_handsSystem.TryDrop(args.User, args.Used, handsComp: hands))
  216. return;
  217. slots.Sort(SortEmpty);
  218. foreach (var slot in slots)
  219. {
  220. if (slot.Item != null)
  221. _handsSystem.TryPickupAnyHand(args.User, slot.Item.Value, handsComp: hands);
  222. Insert(uid, slot, args.Used, args.User, excludeUserAudio: true);
  223. if (slot.InsertSuccessPopup.HasValue)
  224. _popupSystem.PopupClient(Loc.GetString(slot.InsertSuccessPopup), uid, args.User);
  225. args.Handled = true;
  226. return;
  227. }
  228. }
  229. #endregion
  230. #region Insert
  231. /// <summary>
  232. /// Insert an item into a slot. This does not perform checks, so make sure to also use <see
  233. /// cref="CanInsert"/> or just use <see cref="TryInsert"/> instead.
  234. /// </summary>
  235. /// <param name="excludeUserAudio">If true, will exclude the user when playing sound. Does nothing client-side.
  236. /// Useful for predicted interactions</param>
  237. private void Insert(EntityUid uid,
  238. ItemSlot slot,
  239. EntityUid item,
  240. EntityUid? user,
  241. bool excludeUserAudio = false)
  242. {
  243. bool? inserted = slot.ContainerSlot != null ? _containers.Insert(item, slot.ContainerSlot) : null;
  244. // ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage
  245. // Logging
  246. if (inserted != null && inserted.Value && user != null)
  247. _adminLogger.Add(LogType.Action,
  248. LogImpact.Low,
  249. $"{ToPrettyString(user.Value)} inserted {ToPrettyString(item)} into {slot.ContainerSlot?.ID + " slot of "}{ToPrettyString(uid)}");
  250. _audioSystem.PlayPredicted(slot.InsertSound, uid, excludeUserAudio ? user : null);
  251. }
  252. /// <summary>
  253. /// Check whether a given item can be inserted into a slot. Unless otherwise specified, this will return
  254. /// false if the slot is already filled.
  255. /// </summary>
  256. public bool CanInsert(EntityUid uid,
  257. EntityUid usedUid,
  258. EntityUid? user,
  259. ItemSlot slot,
  260. bool swap = false)
  261. {
  262. if (slot.ContainerSlot == null)
  263. return false;
  264. if (slot.HasItem && (!swap || swap && !CanEject(uid, user, slot)))
  265. return false;
  266. if (!CanInsertWhitelist(usedUid, slot))
  267. return false;
  268. if (slot.Locked)
  269. return false;
  270. var ev = new ItemSlotInsertAttemptEvent(uid, usedUid, user, slot);
  271. RaiseLocalEvent(uid, ref ev);
  272. RaiseLocalEvent(usedUid, ref ev);
  273. if (ev.Cancelled)
  274. {
  275. return false;
  276. }
  277. return _containers.CanInsert(usedUid, slot.ContainerSlot, assumeEmpty: swap);
  278. }
  279. private bool CanInsertWhitelist(EntityUid usedUid, ItemSlot slot)
  280. {
  281. if (_whitelistSystem.IsWhitelistFail(slot.Whitelist, usedUid)
  282. || _whitelistSystem.IsBlacklistPass(slot.Blacklist, usedUid))
  283. return false;
  284. return true;
  285. }
  286. /// <summary>
  287. /// Tries to insert item into a specific slot.
  288. /// </summary>
  289. /// <returns>False if failed to insert item</returns>
  290. public bool TryInsert(EntityUid uid,
  291. string id,
  292. EntityUid item,
  293. EntityUid? user,
  294. ItemSlotsComponent? itemSlots = null,
  295. bool excludeUserAudio = false)
  296. {
  297. if (!Resolve(uid, ref itemSlots))
  298. return false;
  299. if (!itemSlots.Slots.TryGetValue(id, out var slot))
  300. return false;
  301. return TryInsert(uid, slot, item, user, excludeUserAudio: excludeUserAudio);
  302. }
  303. /// <summary>
  304. /// Tries to insert item into a specific slot.
  305. /// </summary>
  306. /// <returns>False if failed to insert item</returns>
  307. public bool TryInsert(EntityUid uid,
  308. ItemSlot slot,
  309. EntityUid item,
  310. EntityUid? user,
  311. bool excludeUserAudio = false)
  312. {
  313. if (!CanInsert(uid, item, user, slot))
  314. return false;
  315. Insert(uid, slot, item, user, excludeUserAudio: excludeUserAudio);
  316. return true;
  317. }
  318. /// <summary>
  319. /// Tries to insert item into a specific slot from an entity's hand.
  320. /// Does not check action blockers.
  321. /// </summary>
  322. /// <returns>False if failed to insert item</returns>
  323. public bool TryInsertFromHand(EntityUid uid,
  324. ItemSlot slot,
  325. EntityUid user,
  326. HandsComponent? hands = null,
  327. bool excludeUserAudio = false)
  328. {
  329. if (!Resolve(user, ref hands, false))
  330. return false;
  331. if (hands.ActiveHand?.HeldEntity is not { } held)
  332. return false;
  333. if (!CanInsert(uid, held, user, slot))
  334. return false;
  335. // hands.Drop(item) checks CanDrop action blocker
  336. if (!_handsSystem.TryDrop(user, hands.ActiveHand))
  337. return false;
  338. Insert(uid, slot, held, user, excludeUserAudio: excludeUserAudio);
  339. return true;
  340. }
  341. /// <summary>
  342. /// Tries to insert an item into any empty slot.
  343. /// </summary>
  344. /// <param name="ent">The entity that has the item slots.</param>
  345. /// <param name="item">The item to be inserted.</param>
  346. /// <param name="user">The entity performing the interaction.</param>
  347. /// <param name="excludeUserAudio">
  348. /// If true, will exclude the user when playing sound. Does nothing client-side.
  349. /// Useful for predicted interactions
  350. /// </param>
  351. /// <returns>False if failed to insert item</returns>
  352. public bool TryInsertEmpty(Entity<ItemSlotsComponent?> ent,
  353. EntityUid item,
  354. EntityUid? user,
  355. bool excludeUserAudio = false)
  356. {
  357. if (!Resolve(ent, ref ent.Comp, false))
  358. return false;
  359. TryComp(user, out HandsComponent? handsComp);
  360. if (!TryGetAvailableSlot(ent,
  361. item,
  362. user == null ? null : (user.Value, handsComp),
  363. out var itemSlot,
  364. emptyOnly: true))
  365. return false;
  366. if (user != null && !_handsSystem.TryDrop(user.Value, item, handsComp: handsComp))
  367. return false;
  368. Insert(ent, itemSlot, item, user, excludeUserAudio: excludeUserAudio);
  369. return true;
  370. }
  371. /// <summary>
  372. /// Tries to get any slot that the <paramref name="item"/> can be inserted into.
  373. /// </summary>
  374. /// <param name="ent">Entity that <paramref name="item"/> is being inserted into.</param>
  375. /// <param name="item">Entity being inserted into <paramref name="ent"/>.</param>
  376. /// <param name="userEnt">Entity inserting <paramref name="item"/> into <paramref name="ent"/>.</param>
  377. /// <param name="itemSlot">The ItemSlot on <paramref name="ent"/> to insert <paramref name="item"/> into.</param>
  378. /// <param name="emptyOnly"> True only returns slots that are empty.
  379. /// False returns any slot that is able to receive <paramref name="item"/>.</param>
  380. /// <returns>True when a slot is found. Otherwise, false.</returns>
  381. public bool TryGetAvailableSlot(Entity<ItemSlotsComponent?> ent,
  382. EntityUid item,
  383. Entity<HandsComponent?>? userEnt,
  384. [NotNullWhen(true)] out ItemSlot? itemSlot,
  385. bool emptyOnly = false)
  386. {
  387. itemSlot = null;
  388. if (userEnt is { } user
  389. && Resolve(user, ref user.Comp)
  390. && _handsSystem.IsHolding(user, item))
  391. {
  392. if (!_handsSystem.CanDrop(user, item, user.Comp))
  393. return false;
  394. }
  395. if (!Resolve(ent, ref ent.Comp, false))
  396. return false;
  397. var slots = new List<ItemSlot>();
  398. foreach (var slot in ent.Comp.Slots.Values)
  399. {
  400. if (emptyOnly && slot.ContainerSlot?.ContainedEntity != null)
  401. continue;
  402. if (CanInsert(ent, item, userEnt, slot))
  403. slots.Add(slot);
  404. }
  405. if (slots.Count == 0)
  406. return false;
  407. slots.Sort(SortEmpty);
  408. itemSlot = slots[0];
  409. return true;
  410. }
  411. private static int SortEmpty(ItemSlot a, ItemSlot b)
  412. {
  413. var aEnt = a.ContainerSlot?.ContainedEntity;
  414. var bEnt = b.ContainerSlot?.ContainedEntity;
  415. if (aEnt == null && bEnt == null)
  416. return a.Priority.CompareTo(b.Priority);
  417. if (aEnt == null)
  418. return -1;
  419. return 1;
  420. }
  421. #endregion
  422. #region Eject
  423. /// <summary>
  424. /// Check whether an ejection from a given slot may happen.
  425. /// </summary>
  426. /// <remarks>
  427. /// If a popup entity is given, this will generate a popup message if any are configured on the the item slot.
  428. /// </remarks>
  429. public bool CanEject(EntityUid uid, EntityUid? user, ItemSlot slot, EntityUid? popup = null)
  430. {
  431. if (slot.Locked)
  432. {
  433. if (popup.HasValue && slot.LockedFailPopup.HasValue)
  434. _popupSystem.PopupClient(Loc.GetString(slot.LockedFailPopup), uid, popup.Value);
  435. return false;
  436. }
  437. if (slot.ContainerSlot?.ContainedEntity is not { } item)
  438. return false;
  439. var ev = new ItemSlotEjectAttemptEvent(uid, item, user, slot);
  440. RaiseLocalEvent(uid, ref ev);
  441. RaiseLocalEvent(item, ref ev);
  442. if (ev.Cancelled)
  443. return false;
  444. return _containers.CanRemove(item, slot.ContainerSlot);
  445. }
  446. /// <summary>
  447. /// Eject an item from a slot. This does not perform checks (e.g., is the slot locked?), so you should
  448. /// probably just use <see cref="TryEject"/> instead.
  449. /// </summary>
  450. /// <param name="excludeUserAudio">If true, will exclude the user when playing sound. Does nothing client-side.
  451. /// Useful for predicted interactions</param>
  452. private void Eject(EntityUid uid, ItemSlot slot, EntityUid item, EntityUid? user, bool excludeUserAudio = false)
  453. {
  454. bool? ejected = slot.ContainerSlot != null ? _containers.Remove(item, slot.ContainerSlot) : null;
  455. // ContainerSlot automatically raises a directed EntRemovedFromContainerMessage
  456. // Logging
  457. if (ejected != null && ejected.Value && user != null)
  458. _adminLogger.Add(LogType.Action,
  459. LogImpact.Low,
  460. $"{ToPrettyString(user.Value)} ejected {ToPrettyString(item)} from {slot.ContainerSlot?.ID + " slot of "}{ToPrettyString(uid)}");
  461. _audioSystem.PlayPredicted(slot.EjectSound, uid, excludeUserAudio ? user : null);
  462. }
  463. /// <summary>
  464. /// Try to eject an item from a slot.
  465. /// </summary>
  466. /// <returns>False if item slot is locked or has no item inserted</returns>
  467. public bool TryEject(EntityUid uid,
  468. ItemSlot slot,
  469. EntityUid? user,
  470. [NotNullWhen(true)] out EntityUid? item,
  471. bool excludeUserAudio = false)
  472. {
  473. item = null;
  474. // This handles logic with the slot itself
  475. if (!CanEject(uid, user, slot))
  476. return false;
  477. item = slot.Item;
  478. // This handles user logic
  479. if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value))
  480. return false;
  481. Eject(uid, slot, item!.Value, user, excludeUserAudio);
  482. return true;
  483. }
  484. /// <summary>
  485. /// Try to eject item from a slot.
  486. /// </summary>
  487. /// <returns>False if the id is not valid, the item slot is locked, or it has no item inserted</returns>
  488. public bool TryEject(EntityUid uid,
  489. string id,
  490. EntityUid? user,
  491. [NotNullWhen(true)] out EntityUid? item,
  492. ItemSlotsComponent? itemSlots = null,
  493. bool excludeUserAudio = false)
  494. {
  495. item = null;
  496. if (!Resolve(uid, ref itemSlots))
  497. return false;
  498. if (!itemSlots.Slots.TryGetValue(id, out var slot))
  499. return false;
  500. return TryEject(uid, slot, user, out item, excludeUserAudio);
  501. }
  502. /// <summary>
  503. /// Try to eject item from a slot directly into a user's hands. If they have no hands, the item will still
  504. /// be ejected onto the floor.
  505. /// </summary>
  506. /// <returns>
  507. /// False if the id is not valid, the item slot is locked, or it has no item inserted. True otherwise, even
  508. /// if the user has no hands.
  509. /// </returns>
  510. public bool TryEjectToHands(EntityUid uid, ItemSlot slot, EntityUid? user, bool excludeUserAudio = false)
  511. {
  512. if (!TryEject(uid, slot, user, out var item, excludeUserAudio))
  513. return false;
  514. if (user != null)
  515. _handsSystem.PickupOrDrop(user.Value, item.Value);
  516. return true;
  517. }
  518. #endregion
  519. #region Verbs
  520. private void AddAlternativeVerbs(EntityUid uid,
  521. ItemSlotsComponent itemSlots,
  522. GetVerbsEvent<AlternativeVerb> args)
  523. {
  524. if (args.Hands == null || !args.CanAccess || !args.CanInteract)
  525. {
  526. return;
  527. }
  528. // Add the insert-item verbs
  529. if (args.Using != null && _actionBlockerSystem.CanDrop(args.User))
  530. {
  531. var canInsertAny = false;
  532. foreach (var slot in itemSlots.Slots.Values)
  533. {
  534. // Disable slot insert if InsertOnInteract is true
  535. if (slot.InsertOnInteract || !CanInsert(uid, args.Using.Value, args.User, slot))
  536. continue;
  537. var verbSubject = slot.Name != string.Empty
  538. ? Loc.GetString(slot.Name)
  539. : Name(args.Using.Value);
  540. AlternativeVerb verb = new()
  541. {
  542. IconEntity = GetNetEntity(args.Using),
  543. Act = () => Insert(uid, slot, args.Using.Value, args.User, excludeUserAudio: true)
  544. };
  545. if (slot.InsertVerbText != null)
  546. {
  547. verb.Text = Loc.GetString(slot.InsertVerbText);
  548. verb.Icon = new SpriteSpecifier.Texture(
  549. new("/Textures/Interface/VerbIcons/insert.svg.192dpi.png"));
  550. }
  551. else if (slot.EjectOnInteract)
  552. {
  553. // Inserting/ejecting is a primary interaction for this entity. Instead of using the insert
  554. // category, we will use a single "Place <item>" verb.
  555. verb.Text = Loc.GetString("place-item-verb-text", ("subject", verbSubject));
  556. verb.Icon = new SpriteSpecifier.Texture(
  557. new("/Textures/Interface/VerbIcons/drop.svg.192dpi.png"));
  558. }
  559. else
  560. {
  561. verb.Category = VerbCategory.Insert;
  562. verb.Text = verbSubject;
  563. }
  564. verb.Priority = slot.Priority;
  565. args.Verbs.Add(verb);
  566. canInsertAny = true;
  567. }
  568. // If can insert then insert. Don't run eject verbs.
  569. if (canInsertAny)
  570. return;
  571. }
  572. // Add the eject-item verbs
  573. foreach (var slot in itemSlots.Slots.Values)
  574. {
  575. if (slot.EjectOnInteract || slot.DisableEject)
  576. // For this item slot, ejecting/inserting is a primary interaction. Instead of an eject category
  577. // alt-click verb, there will be a "Take item" primary interaction verb.
  578. continue;
  579. if (!CanEject(uid, args.User, slot))
  580. continue;
  581. if (!_actionBlockerSystem.CanPickup(args.User, slot.Item!.Value))
  582. continue;
  583. var verbSubject = slot.Name != string.Empty
  584. ? Loc.GetString(slot.Name)
  585. : EntityManager.GetComponent<MetaDataComponent>(slot.Item.Value).EntityName ?? string.Empty;
  586. AlternativeVerb verb = new()
  587. {
  588. IconEntity = GetNetEntity(slot.Item),
  589. Act = () => TryEjectToHands(uid, slot, args.User, excludeUserAudio: true)
  590. };
  591. if (slot.EjectVerbText == null)
  592. {
  593. verb.Text = verbSubject;
  594. verb.Category = VerbCategory.Eject;
  595. }
  596. else
  597. {
  598. verb.Text = Loc.GetString(slot.EjectVerbText);
  599. }
  600. verb.Priority = slot.Priority;
  601. args.Verbs.Add(verb);
  602. }
  603. }
  604. private void AddInteractionVerbsVerbs(EntityUid uid,
  605. ItemSlotsComponent itemSlots,
  606. GetVerbsEvent<InteractionVerb> args)
  607. {
  608. if (args.Hands == null || !args.CanAccess || !args.CanInteract)
  609. return;
  610. // If there are any slots that eject on left-click, add a "Take <item>" verb.
  611. foreach (var slot in itemSlots.Slots.Values)
  612. {
  613. if (!slot.EjectOnInteract || !CanEject(uid, args.User, slot))
  614. continue;
  615. if (!_actionBlockerSystem.CanPickup(args.User, slot.Item!.Value))
  616. continue;
  617. var verbSubject = slot.Name != string.Empty
  618. ? Loc.GetString(slot.Name)
  619. : Name(slot.Item!.Value);
  620. InteractionVerb takeVerb = new()
  621. {
  622. IconEntity = GetNetEntity(slot.Item),
  623. Act = () => TryEjectToHands(uid, slot, args.User, excludeUserAudio: true)
  624. };
  625. if (slot.EjectVerbText == null)
  626. takeVerb.Text = Loc.GetString("take-item-verb-text", ("subject", verbSubject));
  627. else
  628. takeVerb.Text = Loc.GetString(slot.EjectVerbText);
  629. takeVerb.Priority = slot.Priority;
  630. args.Verbs.Add(takeVerb);
  631. }
  632. // Next, add the insert-item verbs
  633. if (args.Using == null || !_actionBlockerSystem.CanDrop(args.User))
  634. return;
  635. foreach (var slot in itemSlots.Slots.Values)
  636. {
  637. if (!slot.InsertOnInteract || !CanInsert(uid, args.Using.Value, args.User, slot))
  638. continue;
  639. var verbSubject = slot.Name != string.Empty
  640. ? Loc.GetString(slot.Name)
  641. : Name(args.Using.Value);
  642. InteractionVerb insertVerb = new()
  643. {
  644. IconEntity = GetNetEntity(args.Using),
  645. Act = () => Insert(uid, slot, args.Using.Value, args.User, excludeUserAudio: true)
  646. };
  647. if (slot.InsertVerbText != null)
  648. {
  649. insertVerb.Text = Loc.GetString(slot.InsertVerbText);
  650. insertVerb.Icon =
  651. new SpriteSpecifier.Texture(
  652. new ResPath("/Textures/Interface/VerbIcons/insert.svg.192dpi.png"));
  653. }
  654. else if (slot.EjectOnInteract)
  655. {
  656. // Inserting/ejecting is a primary interaction for this entity. Instead of using the insert
  657. // category, we will use a single "Place <item>" verb.
  658. insertVerb.Text = Loc.GetString("place-item-verb-text", ("subject", verbSubject));
  659. insertVerb.Icon =
  660. new SpriteSpecifier.Texture(
  661. new ResPath("/Textures/Interface/VerbIcons/drop.svg.192dpi.png"));
  662. }
  663. else
  664. {
  665. insertVerb.Category = VerbCategory.Insert;
  666. insertVerb.Text = verbSubject;
  667. }
  668. insertVerb.Priority = slot.Priority;
  669. args.Verbs.Add(insertVerb);
  670. }
  671. }
  672. #endregion
  673. #region BUIs
  674. private void HandleButtonPressed(EntityUid uid, ItemSlotsComponent component, ItemSlotButtonPressedEvent args)
  675. {
  676. if (!component.Slots.TryGetValue(args.SlotId, out var slot))
  677. return;
  678. if (args.TryEject && slot.HasItem)
  679. TryEjectToHands(uid, slot, args.Actor, true);
  680. else if (args.TryInsert && !slot.HasItem)
  681. TryInsertFromHand(uid, slot, args.Actor);
  682. }
  683. #endregion
  684. /// <summary>
  685. /// Eject items from (some) slots when the entity is destroyed.
  686. /// </summary>
  687. private void OnBreak(EntityUid uid, ItemSlotsComponent component, EntityEventArgs args)
  688. {
  689. foreach (var slot in component.Slots.Values)
  690. {
  691. if (slot.EjectOnBreak && slot.HasItem)
  692. {
  693. SetLock(uid, slot, false, component);
  694. TryEject(uid, slot, null, out var _);
  695. }
  696. }
  697. }
  698. /// <summary>
  699. /// Get the contents of some item slot.
  700. /// </summary>
  701. /// <returns>The item in the slot, or null if the slot is empty or the entity doesn't have an <see cref="ItemSlotsComponent"/>.</returns>
  702. public EntityUid? GetItemOrNull(EntityUid uid, string id, ItemSlotsComponent? itemSlots = null)
  703. {
  704. if (!Resolve(uid, ref itemSlots, logMissing: false))
  705. return null;
  706. return itemSlots.Slots.GetValueOrDefault(id)?.Item;
  707. }
  708. /// <summary>
  709. /// Lock an item slot. This stops items from being inserted into or ejected from this slot.
  710. /// </summary>
  711. public void SetLock(EntityUid uid, string id, bool locked, ItemSlotsComponent? itemSlots = null)
  712. {
  713. if (!Resolve(uid, ref itemSlots))
  714. return;
  715. if (!itemSlots.Slots.TryGetValue(id, out var slot))
  716. return;
  717. SetLock(uid, slot, locked, itemSlots);
  718. }
  719. /// <summary>
  720. /// Lock an item slot. This stops items from being inserted into or ejected from this slot.
  721. /// </summary>
  722. public void SetLock(EntityUid uid, ItemSlot slot, bool locked, ItemSlotsComponent? itemSlots = null)
  723. {
  724. if (!Resolve(uid, ref itemSlots))
  725. return;
  726. slot.Locked = locked;
  727. Dirty(uid, itemSlots);
  728. }
  729. /// <summary>
  730. /// Update the locked state of the managed item slots.
  731. /// </summary>
  732. /// <remarks>
  733. /// Note that the slot's ContainerSlot performs its own networking, so we don't need to send information
  734. /// about the contained entity.
  735. /// </remarks>
  736. private void HandleItemSlotsState(EntityUid uid, ItemSlotsComponent component, ref ComponentHandleState args)
  737. {
  738. if (args.Current is not ItemSlotsComponentState state)
  739. return;
  740. foreach (var (key, slot) in component.Slots)
  741. {
  742. if (!state.Slots.ContainsKey(key))
  743. RemoveItemSlot(uid, slot, component);
  744. }
  745. foreach (var (serverKey, serverSlot) in state.Slots)
  746. {
  747. if (component.Slots.TryGetValue(serverKey, out var itemSlot))
  748. {
  749. itemSlot.CopyFrom(serverSlot);
  750. itemSlot.ContainerSlot = _containers.EnsureContainer<ContainerSlot>(uid, serverKey);
  751. }
  752. else
  753. {
  754. var slot = new ItemSlot(serverSlot);
  755. slot.Local = false;
  756. AddItemSlot(uid, serverKey, slot);
  757. }
  758. }
  759. }
  760. private void GetItemSlotsState(EntityUid uid, ItemSlotsComponent component, ref ComponentGetState args)
  761. {
  762. args.State = new ItemSlotsComponentState(component.Slots);
  763. }
  764. }
  765. }