InventorySystem.Equip.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Shared.Armor;
  3. using Content.Shared.Clothing.Components;
  4. using Content.Shared.DoAfter;
  5. using Content.Shared.Hands;
  6. using Content.Shared.Hands.Components;
  7. using Content.Shared.Hands.EntitySystems;
  8. using Content.Shared.Interaction;
  9. using Content.Shared.Inventory.Events;
  10. using Content.Shared.Item;
  11. using Content.Shared.Movement.Systems;
  12. using Content.Shared.Popups;
  13. using Content.Shared.Strip;
  14. using Content.Shared.Strip.Components;
  15. using Content.Shared.Whitelist;
  16. using Robust.Shared.Audio.Systems;
  17. using Robust.Shared.Containers;
  18. using Robust.Shared.Timing;
  19. using Robust.Shared.Utility;
  20. namespace Content.Shared.Inventory;
  21. public abstract partial class InventorySystem
  22. {
  23. [Dependency] private readonly SharedPopupSystem _popup = default!;
  24. [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
  25. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  26. [Dependency] private readonly SharedItemSystem _item = default!;
  27. [Dependency] private readonly SharedAudioSystem _audio = default!;
  28. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  29. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  30. [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
  31. [Dependency] private readonly IGameTiming _gameTiming = default!;
  32. [Dependency] private readonly SharedTransformSystem _transform = default!;
  33. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  34. [Dependency] private readonly SharedStrippableSystem _strippable = default!;
  35. [ValidatePrototypeId<ItemSizePrototype>]
  36. private const string PocketableItemSize = "Small";
  37. private void InitializeEquip()
  38. {
  39. //these events ensure that the client also gets its proper events raised when getting its containerstate updated
  40. SubscribeLocalEvent<InventoryComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
  41. SubscribeLocalEvent<InventoryComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
  42. SubscribeAllEvent<UseSlotNetworkMessage>(OnUseSlot);
  43. }
  44. private void OnEntRemoved(EntityUid uid, InventoryComponent component, EntRemovedFromContainerMessage args)
  45. {
  46. if (!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component))
  47. return;
  48. var unequippedEvent = new DidUnequipEvent(uid, args.Entity, slotDef);
  49. RaiseLocalEvent(uid, unequippedEvent, true);
  50. var gotUnequippedEvent = new GotUnequippedEvent(uid, args.Entity, slotDef);
  51. RaiseLocalEvent(args.Entity, gotUnequippedEvent, true);
  52. }
  53. private void OnEntInserted(EntityUid uid, InventoryComponent component, EntInsertedIntoContainerMessage args)
  54. {
  55. if (!TryGetSlot(uid, args.Container.ID, out var slotDef, inventory: component))
  56. return;
  57. var equippedEvent = new DidEquipEvent(uid, args.Entity, slotDef);
  58. RaiseLocalEvent(uid, equippedEvent, true);
  59. var gotEquippedEvent = new GotEquippedEvent(uid, args.Entity, slotDef);
  60. RaiseLocalEvent(args.Entity, gotEquippedEvent, true);
  61. }
  62. /// <summary>
  63. /// Will attempt to equip or unequip an item to/from the clicked slot. If the user clicked on an occupied slot
  64. /// with some entity, will instead attempt to interact with this entity.
  65. /// </summary>
  66. private void OnUseSlot(UseSlotNetworkMessage ev, EntitySessionEventArgs eventArgs)
  67. {
  68. if (eventArgs.SenderSession.AttachedEntity is not { Valid: true } actor)
  69. return;
  70. if (!TryComp(actor, out InventoryComponent? inventory) || !TryComp<HandsComponent>(actor, out var hands))
  71. return;
  72. var held = hands.ActiveHandEntity;
  73. TryGetSlotEntity(actor, ev.Slot, out var itemUid, inventory);
  74. // attempt to perform some interaction
  75. if (held != null && itemUid != null)
  76. {
  77. _interactionSystem.InteractUsing(actor, held.Value, itemUid.Value,
  78. Transform(itemUid.Value).Coordinates);
  79. return;
  80. }
  81. // unequip the item.
  82. if (itemUid != null)
  83. {
  84. if (!TryUnequip(actor, ev.Slot, out var item, predicted: true, inventory: inventory, checkDoafter: true))
  85. return;
  86. _handsSystem.PickupOrDrop(actor, item.Value);
  87. return;
  88. }
  89. // finally, just try to equip the held item.
  90. if (held == null)
  91. return;
  92. // before we drop the item, check that it can be equipped in the first place.
  93. if (!CanEquip(actor, held.Value, ev.Slot, out var reason))
  94. {
  95. _popup.PopupCursor(Loc.GetString(reason));
  96. return;
  97. }
  98. if (!_handsSystem.CanDropHeld(actor, hands.ActiveHand!, checkActionBlocker: false))
  99. return;
  100. RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor));
  101. TryEquip(actor, actor, held.Value, ev.Slot, predicted: true, inventory: inventory, force: true, checkDoafter: true);
  102. }
  103. public bool TryEquip(EntityUid uid, EntityUid itemUid, string slot, bool silent = false, bool force = false, bool predicted = false,
  104. InventoryComponent? inventory = null, ClothingComponent? clothing = null, bool checkDoafter = false) =>
  105. TryEquip(uid, uid, itemUid, slot, silent, force, predicted, inventory, clothing, checkDoafter);
  106. public bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, bool silent = false, bool force = false, bool predicted = false,
  107. InventoryComponent? inventory = null, ClothingComponent? clothing = null, bool checkDoafter = false)
  108. {
  109. if (!Resolve(target, ref inventory, false))
  110. {
  111. if(!silent)
  112. _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"));
  113. return false;
  114. }
  115. // Not required to have, since pockets can take any item.
  116. // CanEquip will still check, so we don't have to worry about it.
  117. Resolve(itemUid, ref clothing, false);
  118. if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory))
  119. {
  120. if(!silent)
  121. _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"));
  122. return false;
  123. }
  124. if (!force && !CanEquip(actor, target, itemUid, slot, out var reason, slotDefinition, inventory, clothing))
  125. {
  126. if(!silent)
  127. _popup.PopupCursor(Loc.GetString(reason));
  128. return false;
  129. }
  130. if (checkDoafter &&
  131. clothing != null &&
  132. clothing.EquipDelay > TimeSpan.Zero &&
  133. (clothing.Slots & slotDefinition.SlotFlags) != 0 &&
  134. _containerSystem.CanInsert(itemUid, slotContainer))
  135. {
  136. var args = new DoAfterArgs(
  137. EntityManager,
  138. actor,
  139. clothing.EquipDelay,
  140. new ClothingEquipDoAfterEvent(slot),
  141. itemUid,
  142. target,
  143. itemUid)
  144. {
  145. BreakOnMove = true,
  146. NeedHand = true,
  147. };
  148. _doAfter.TryStartDoAfter(args);
  149. return false;
  150. }
  151. if (!_containerSystem.Insert(itemUid, slotContainer))
  152. {
  153. if(!silent)
  154. _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"));
  155. return false;
  156. }
  157. if (!silent && clothing != null)
  158. {
  159. _audio.PlayPredicted(clothing.EquipSound, target, actor);
  160. }
  161. Dirty(target, inventory);
  162. _movementSpeed.RefreshMovementSpeedModifiers(target);
  163. return true;
  164. }
  165. public bool CanAccess(EntityUid actor, EntityUid target, EntityUid itemUid)
  166. {
  167. // if the item is something like a hardsuit helmet, it may be contained within the hardsuit.
  168. // in that case, we check accesibility for the owner-entity instead.
  169. if (TryComp(itemUid, out AttachedClothingComponent? attachedComp))
  170. itemUid = attachedComp.AttachedUid;
  171. // Can the actor reach the target?
  172. if (actor != target && !(_interactionSystem.InRangeUnobstructed(actor, target) && _containerSystem.IsInSameOrParentContainer(actor, target)))
  173. return false;
  174. // Can the actor reach the item?
  175. if (_interactionSystem.InRangeAndAccessible(actor, itemUid))
  176. return true;
  177. // Is the actor currently stripping the target? Here we could check if the actor has the stripping UI open, but
  178. // that requires server/client specific code.
  179. // Uhhh TODO, fix this. This doesn't even fucking check if the target item is IN the targets inventory.
  180. return actor != target &&
  181. HasComp<StrippableComponent>(target) &&
  182. HasComp<StrippingComponent>(actor) &&
  183. HasComp<HandsComponent>(actor);
  184. }
  185. public bool CanEquip(EntityUid uid, EntityUid itemUid, string slot, [NotNullWhen(false)] out string? reason,
  186. SlotDefinition? slotDefinition = null, InventoryComponent? inventory = null,
  187. ClothingComponent? clothing = null, ItemComponent? item = null) =>
  188. CanEquip(uid, uid, itemUid, slot, out reason, slotDefinition, inventory, clothing, item);
  189. public bool CanEquip(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, [NotNullWhen(false)] out string? reason, SlotDefinition? slotDefinition = null,
  190. InventoryComponent? inventory = null, ClothingComponent? clothing = null, ItemComponent? item = null)
  191. {
  192. reason = "inventory-component-can-equip-cannot";
  193. if (!Resolve(target, ref inventory, false))
  194. return false;
  195. Resolve(itemUid, ref clothing, ref item, false);
  196. if (slotDefinition == null && !TryGetSlot(target, slot, out slotDefinition, inventory: inventory))
  197. return false;
  198. DebugTools.Assert(slotDefinition.Name == slot);
  199. if (slotDefinition.DependsOn != null)
  200. {
  201. if (!TryGetSlotEntity(target, slotDefinition.DependsOn, out EntityUid? slotEntity, inventory))
  202. return false;
  203. if (slotDefinition.DependsOnComponents is { } componentRegistry)
  204. {
  205. foreach (var (_, entry) in componentRegistry)
  206. {
  207. if (!HasComp(slotEntity, entry.Component.GetType()))
  208. return false;
  209. if (TryComp<AllowSuitStorageComponent>(slotEntity, out var comp) &&
  210. _whitelistSystem.IsWhitelistFailOrNull(comp.Whitelist, itemUid))
  211. return false;
  212. }
  213. }
  214. }
  215. var fittingInPocket = slotDefinition.SlotFlags.HasFlag(SlotFlags.POCKET) &&
  216. item != null &&
  217. _item.GetSizePrototype(item.Size) <= _item.GetSizePrototype(PocketableItemSize);
  218. if (clothing == null && !fittingInPocket
  219. || clothing != null && !clothing.Slots.HasFlag(slotDefinition.SlotFlags) && !fittingInPocket)
  220. {
  221. reason = "inventory-component-can-equip-does-not-fit";
  222. return false;
  223. }
  224. if (!CanAccess(actor, target, itemUid))
  225. {
  226. reason = "interaction-system-user-interaction-cannot-reach";
  227. return false;
  228. }
  229. if (_whitelistSystem.IsWhitelistFail(slotDefinition.Whitelist, itemUid) ||
  230. _whitelistSystem.IsBlacklistPass(slotDefinition.Blacklist, itemUid))
  231. {
  232. reason = "inventory-component-can-equip-does-not-fit";
  233. return false;
  234. }
  235. var attemptEvent = new IsEquippingAttemptEvent(actor, target, itemUid, slotDefinition);
  236. RaiseLocalEvent(target, attemptEvent, true);
  237. if (attemptEvent.Cancelled)
  238. {
  239. reason = attemptEvent.Reason ?? reason;
  240. return false;
  241. }
  242. if (actor != target)
  243. {
  244. //reuse the event. this is gucci, right?
  245. attemptEvent.Reason = null;
  246. RaiseLocalEvent(actor, attemptEvent, true);
  247. if (attemptEvent.Cancelled)
  248. {
  249. reason = attemptEvent.Reason ?? reason;
  250. return false;
  251. }
  252. }
  253. var itemAttemptEvent = new BeingEquippedAttemptEvent(actor, target, itemUid, slotDefinition);
  254. RaiseLocalEvent(itemUid, itemAttemptEvent, true);
  255. if (itemAttemptEvent.Cancelled)
  256. {
  257. reason = itemAttemptEvent.Reason ?? reason;
  258. return false;
  259. }
  260. return true;
  261. }
  262. public bool TryUnequip(
  263. EntityUid uid,
  264. string slot,
  265. bool silent = false,
  266. bool force = false,
  267. bool predicted = false,
  268. InventoryComponent? inventory = null,
  269. ClothingComponent? clothing = null,
  270. bool reparent = true,
  271. bool checkDoafter = false)
  272. {
  273. return TryUnequip(uid, uid, slot, silent, force, predicted, inventory, clothing, reparent, checkDoafter);
  274. }
  275. public bool TryUnequip(
  276. EntityUid actor,
  277. EntityUid target,
  278. string slot,
  279. bool silent = false,
  280. bool force = false,
  281. bool predicted = false,
  282. InventoryComponent? inventory = null,
  283. ClothingComponent? clothing = null,
  284. bool reparent = true,
  285. bool checkDoafter = false)
  286. {
  287. return TryUnequip(actor, target, slot, out _, silent, force, predicted, inventory, clothing, reparent, checkDoafter);
  288. }
  289. public bool TryUnequip(
  290. EntityUid uid,
  291. string slot,
  292. [NotNullWhen(true)] out EntityUid? removedItem,
  293. bool silent = false,
  294. bool force = false,
  295. bool predicted = false,
  296. InventoryComponent? inventory = null,
  297. ClothingComponent? clothing = null,
  298. bool reparent = true,
  299. bool checkDoafter = false)
  300. {
  301. return TryUnequip(uid, uid, slot, out removedItem, silent, force, predicted, inventory, clothing, reparent, checkDoafter);
  302. }
  303. public bool TryUnequip(
  304. EntityUid actor,
  305. EntityUid target,
  306. string slot,
  307. [NotNullWhen(true)] out EntityUid? removedItem,
  308. bool silent = false,
  309. bool force = false,
  310. bool predicted = false,
  311. InventoryComponent? inventory = null,
  312. ClothingComponent? clothing = null,
  313. bool reparent = true,
  314. bool checkDoafter = false)
  315. {
  316. var itemsDropped = 0;
  317. return TryUnequip(actor, target, slot, out removedItem, ref itemsDropped,
  318. silent, force, predicted, inventory, clothing, reparent, checkDoafter);
  319. }
  320. private bool TryUnequip(
  321. EntityUid actor,
  322. EntityUid target,
  323. string slot,
  324. [NotNullWhen(true)] out EntityUid? removedItem,
  325. ref int itemsDropped,
  326. bool silent = false,
  327. bool force = false,
  328. bool predicted = false,
  329. InventoryComponent? inventory = null,
  330. ClothingComponent? clothing = null,
  331. bool reparent = true,
  332. bool checkDoafter = false)
  333. {
  334. removedItem = null;
  335. if (TerminatingOrDeleted(target))
  336. return false;
  337. if (!Resolve(target, ref inventory, false))
  338. {
  339. if(!silent)
  340. _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"));
  341. return false;
  342. }
  343. if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory))
  344. {
  345. if(!silent)
  346. _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"));
  347. return false;
  348. }
  349. removedItem = slotContainer.ContainedEntity;
  350. if (!removedItem.HasValue || TerminatingOrDeleted(removedItem.Value))
  351. return false;
  352. if (!force && !CanUnequip(actor, target, slot, out var reason, slotContainer, slotDefinition, inventory))
  353. {
  354. if(!silent)
  355. _popup.PopupCursor(Loc.GetString(reason));
  356. return false;
  357. }
  358. //we need to do this to make sure we are 100% removing this entity, since we are now dropping dependant slots
  359. if (!force && !_containerSystem.CanRemove(removedItem.Value, slotContainer))
  360. return false;
  361. if (checkDoafter &&
  362. Resolve(removedItem.Value, ref clothing, false) &&
  363. (clothing.Slots & slotDefinition.SlotFlags) != 0 &&
  364. clothing.UnequipDelay > TimeSpan.Zero)
  365. {
  366. var args = new DoAfterArgs(
  367. EntityManager,
  368. actor,
  369. clothing.UnequipDelay,
  370. new ClothingUnequipDoAfterEvent(slot),
  371. removedItem.Value,
  372. target,
  373. removedItem.Value)
  374. {
  375. BreakOnMove = true,
  376. NeedHand = true,
  377. };
  378. _doAfter.TryStartDoAfter(args);
  379. return false;
  380. }
  381. if (!_containerSystem.Remove(removedItem.Value, slotContainer, force: force, reparent: reparent))
  382. return false;
  383. // this is in order to keep track of whether this is the first instance of a recursion call
  384. var firstRun = itemsDropped == 0;
  385. ++itemsDropped;
  386. foreach (var slotDef in inventory.Slots)
  387. {
  388. if (slotDef != slotDefinition && slotDef.DependsOn == slotDefinition.Name)
  389. {
  390. //this recursive call might be risky
  391. TryUnequip(actor, target, slotDef.Name, out _, ref itemsDropped, true, true, predicted, inventory, reparent: reparent);
  392. }
  393. }
  394. // we check if any items were dropped, and make a popup if they were.
  395. // the reason we check for > 1 is because the first item is always the one we are trying to unequip,
  396. // whereas we only want to notify for extra dropped items.
  397. if (!silent && _gameTiming.IsFirstTimePredicted && firstRun && itemsDropped > 1)
  398. _popup.PopupClient(Loc.GetString("inventory-component-dropped-from-unequip", ("items", itemsDropped - 1)), target, target);
  399. // TODO: Inventory needs a hot cleanup hoo boy
  400. // Check if something else (AKA toggleable) dumped it into a container.
  401. if (!_containerSystem.IsEntityInContainer(removedItem.Value))
  402. _transform.DropNextTo(removedItem.Value, target);
  403. if (!silent && Resolve(removedItem.Value, ref clothing, false) && clothing.UnequipSound != null)
  404. {
  405. _audio.PlayPredicted(clothing.UnequipSound, target, actor);
  406. }
  407. Dirty(target, inventory);
  408. _movementSpeed.RefreshMovementSpeedModifiers(target);
  409. return true;
  410. }
  411. public bool CanUnequip(EntityUid uid, string slot, [NotNullWhen(false)] out string? reason,
  412. ContainerSlot? containerSlot = null, SlotDefinition? slotDefinition = null,
  413. InventoryComponent? inventory = null) =>
  414. CanUnequip(uid, uid, slot, out reason, containerSlot, slotDefinition, inventory);
  415. public bool CanUnequip(EntityUid actor, EntityUid target, string slot, [NotNullWhen(false)] out string? reason, ContainerSlot? containerSlot = null, SlotDefinition? slotDefinition = null, InventoryComponent? inventory = null)
  416. {
  417. reason = "inventory-component-can-unequip-cannot";
  418. if (!Resolve(target, ref inventory, false))
  419. return false;
  420. if ((containerSlot == null || slotDefinition == null) && !TryGetSlotContainer(target, slot, out containerSlot, out slotDefinition, inventory))
  421. return false;
  422. if (containerSlot.ContainedEntity is not { } itemUid)
  423. return false;
  424. if (!_containerSystem.CanRemove(itemUid, containerSlot))
  425. return false;
  426. // make sure the user can actually reach the target
  427. if (!CanAccess(actor, target, itemUid))
  428. {
  429. reason = "interaction-system-user-interaction-cannot-reach";
  430. return false;
  431. }
  432. var attemptEvent = new IsUnequippingAttemptEvent(actor, target, itemUid, slotDefinition);
  433. RaiseLocalEvent(target, attemptEvent, true);
  434. if (attemptEvent.Cancelled)
  435. {
  436. reason = attemptEvent.Reason ?? reason;
  437. return false;
  438. }
  439. if (actor != target)
  440. {
  441. //reuse the event. this is gucci, right?
  442. attemptEvent.Reason = null;
  443. RaiseLocalEvent(actor, attemptEvent, true);
  444. if (attemptEvent.Cancelled)
  445. {
  446. reason = attemptEvent.Reason ?? reason;
  447. return false;
  448. }
  449. }
  450. var itemAttemptEvent = new BeingUnequippedAttemptEvent(actor, target, itemUid, slotDefinition);
  451. RaiseLocalEvent(itemUid, itemAttemptEvent, true);
  452. if (itemAttemptEvent.Cancelled)
  453. {
  454. reason = attemptEvent.Reason ?? reason;
  455. return false;
  456. }
  457. return true;
  458. }
  459. public bool TryGetSlotEntity(EntityUid uid, string slot, [NotNullWhen(true)] out EntityUid? entityUid, InventoryComponent? inventoryComponent = null, ContainerManagerComponent? containerManagerComponent = null)
  460. {
  461. entityUid = null;
  462. if (!Resolve(uid, ref inventoryComponent, ref containerManagerComponent, false)
  463. || !TryGetSlotContainer(uid, slot, out var container, out _, inventoryComponent, containerManagerComponent))
  464. return false;
  465. entityUid = container.ContainedEntity;
  466. return entityUid != null;
  467. }
  468. }