SharedStorageSystem.cs 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658
  1. using System.Collections.Frozen;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Linq;
  4. using Content.Shared.ActionBlocker;
  5. using Content.Shared.Administration.Logs;
  6. using Content.Shared.CCVar;
  7. using Content.Shared.Containers.ItemSlots;
  8. using Content.Shared.Database;
  9. using Content.Shared.Destructible;
  10. using Content.Shared.DoAfter;
  11. using Content.Shared.Hands.Components;
  12. using Content.Shared.Hands.EntitySystems;
  13. using Content.Shared.Implants.Components;
  14. using Content.Shared.Input;
  15. using Content.Shared.Interaction;
  16. using Content.Shared.Interaction.Components;
  17. using Content.Shared.Inventory;
  18. using Content.Shared.Item;
  19. using Content.Shared.Lock;
  20. using Content.Shared.Materials;
  21. using Content.Shared.Placeable;
  22. using Content.Shared.Popups;
  23. using Content.Shared.Stacks;
  24. using Content.Shared.Storage.Components;
  25. using Content.Shared.Timing;
  26. using Content.Shared.Storage.Events;
  27. using Content.Shared.Verbs;
  28. using Content.Shared.Whitelist;
  29. using Robust.Shared.Audio;
  30. using Robust.Shared.Audio.Systems;
  31. using Robust.Shared.Configuration;
  32. using Robust.Shared.Containers;
  33. using Robust.Shared.GameStates;
  34. using Robust.Shared.Input.Binding;
  35. using Robust.Shared.Map;
  36. using Robust.Shared.Player;
  37. using Robust.Shared.Prototypes;
  38. using Robust.Shared.Random;
  39. using Robust.Shared.Serialization;
  40. using Robust.Shared.Timing;
  41. using Robust.Shared.Utility;
  42. namespace Content.Shared.Storage.EntitySystems;
  43. public abstract class SharedStorageSystem : EntitySystem
  44. {
  45. [Dependency] private readonly IConfigurationManager _cfg = default!;
  46. [Dependency] protected readonly IGameTiming Timing = default!;
  47. [Dependency] private readonly IPrototypeManager _prototype = default!;
  48. [Dependency] protected readonly IRobustRandom Random = default!;
  49. [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
  50. [Dependency] protected readonly ActionBlockerSystem ActionBlocker = default!;
  51. [Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
  52. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  53. [Dependency] private readonly InventorySystem _inventory = default!;
  54. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  55. [Dependency] protected readonly SharedAudioSystem Audio = default!;
  56. [Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
  57. [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
  58. [Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
  59. [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
  60. [Dependency] protected readonly SharedItemSystem ItemSystem = default!;
  61. [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
  62. [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
  63. [Dependency] private readonly SharedStackSystem _stack = default!;
  64. [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
  65. [Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
  66. [Dependency] protected readonly UseDelaySystem UseDelay = default!;
  67. private EntityQuery<ItemComponent> _itemQuery;
  68. private EntityQuery<StackComponent> _stackQuery;
  69. private EntityQuery<TransformComponent> _xformQuery;
  70. private EntityQuery<UserInterfaceUserComponent> _userQuery;
  71. /// <summary>
  72. /// Whether we're allowed to go up-down storage via UI.
  73. /// </summary>
  74. public bool NestedStorage = true;
  75. [ValidatePrototypeId<ItemSizePrototype>]
  76. public const string DefaultStorageMaxItemSize = "Normal";
  77. public const float AreaInsertDelayPerItem = 0.075f;
  78. private static AudioParams _audioParams = AudioParams.Default
  79. .WithMaxDistance(7f)
  80. .WithVolume(-2f);
  81. private ItemSizePrototype _defaultStorageMaxItemSize = default!;
  82. /// <summary>
  83. /// Flag for whether we're checking for nested storage interactions.
  84. /// </summary>
  85. private bool _nestedCheck;
  86. public bool CheckingCanInsert;
  87. private readonly List<EntityUid> _entList = new();
  88. private readonly HashSet<EntityUid> _entSet = new();
  89. private readonly List<ItemSizePrototype> _sortedSizes = new();
  90. private FrozenDictionary<string, ItemSizePrototype> _nextSmallest = FrozenDictionary<string, ItemSizePrototype>.Empty;
  91. private const string QuickInsertUseDelayID = "quickInsert";
  92. private const string OpenUiUseDelayID = "storage";
  93. /// <summary>
  94. /// How many storage windows are allowed to be open at once.
  95. /// </summary>
  96. private int _openStorageLimit = -1;
  97. protected readonly List<string> CantFillReasons = [];
  98. /// <inheritdoc />
  99. public override void Initialize()
  100. {
  101. base.Initialize();
  102. _itemQuery = GetEntityQuery<ItemComponent>();
  103. _stackQuery = GetEntityQuery<StackComponent>();
  104. _xformQuery = GetEntityQuery<TransformComponent>();
  105. _userQuery = GetEntityQuery<UserInterfaceUserComponent>();
  106. _prototype.PrototypesReloaded += OnPrototypesReloaded;
  107. Subs.CVar(_cfg, CCVars.StorageLimit, OnStorageLimitChanged, true);
  108. Subs.BuiEvents<StorageComponent>(StorageComponent.StorageUiKey.Key, subs =>
  109. {
  110. subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
  111. });
  112. SubscribeLocalEvent<StorageComponent, ComponentRemove>(OnRemove);
  113. SubscribeLocalEvent<StorageComponent, MapInitEvent>(OnMapInit);
  114. SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb);
  115. SubscribeLocalEvent<StorageComponent, ComponentGetState>(OnStorageGetState);
  116. SubscribeLocalEvent<StorageComponent, ComponentInit>(OnComponentInit, before: new[] { typeof(SharedContainerSystem) });
  117. SubscribeLocalEvent<StorageComponent, GetVerbsEvent<UtilityVerb>>(AddTransferVerbs);
  118. SubscribeLocalEvent<StorageComponent, InteractUsingEvent>(OnInteractUsing, after: new[] { typeof(ItemSlotsSystem) });
  119. SubscribeLocalEvent<StorageComponent, ActivateInWorldEvent>(OnActivate);
  120. SubscribeLocalEvent<StorageComponent, OpenStorageImplantEvent>(OnImplantActivate);
  121. SubscribeLocalEvent<StorageComponent, AfterInteractEvent>(AfterInteract);
  122. SubscribeLocalEvent<StorageComponent, DestructionEventArgs>(OnDestroy);
  123. SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundUIAttempt);
  124. SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
  125. SubscribeLocalEvent<StorageComponent, LockToggledEvent>(OnLockToggled);
  126. SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
  127. SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
  128. SubscribeLocalEvent<StorageComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
  129. SubscribeLocalEvent<StorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
  130. SubscribeLocalEvent<StorageComponent, AreaPickupDoAfterEvent>(OnDoAfter);
  131. SubscribeAllEvent<OpenNestedStorageEvent>(OnStorageNested);
  132. SubscribeAllEvent<StorageTransferItemEvent>(OnStorageTransfer);
  133. SubscribeAllEvent<StorageInteractWithItemEvent>(OnInteractWithItem);
  134. SubscribeAllEvent<StorageSetItemLocationEvent>(OnSetItemLocation);
  135. SubscribeAllEvent<StorageInsertItemIntoLocationEvent>(OnInsertItemIntoLocation);
  136. SubscribeAllEvent<StorageSaveItemLocationEvent>(OnSaveItemLocation);
  137. SubscribeLocalEvent<StorageComponent, GotReclaimedEvent>(OnReclaimed);
  138. CommandBinds.Builder
  139. .Bind(ContentKeyFunctions.OpenBackpack, InputCmdHandler.FromDelegate(HandleOpenBackpack, handle: false))
  140. .Bind(ContentKeyFunctions.OpenBelt, InputCmdHandler.FromDelegate(HandleOpenBelt, handle: false))
  141. .Register<SharedStorageSystem>();
  142. Subs.CVar(_cfg, CCVars.NestedStorage, OnNestedStorageCvar, true);
  143. UpdatePrototypeCache();
  144. }
  145. private void OnNestedStorageCvar(bool obj)
  146. {
  147. NestedStorage = obj;
  148. }
  149. private void OnStorageLimitChanged(int obj)
  150. {
  151. _openStorageLimit = obj;
  152. }
  153. private void OnRemove(Entity<StorageComponent> entity, ref ComponentRemove args)
  154. {
  155. UI.CloseUi(entity.Owner, StorageComponent.StorageUiKey.Key);
  156. }
  157. private void OnMapInit(Entity<StorageComponent> entity, ref MapInitEvent args)
  158. {
  159. UseDelay.SetLength(entity.Owner, entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID);
  160. UseDelay.SetLength(entity.Owner, entity.Comp.OpenUiCooldown, OpenUiUseDelayID);
  161. }
  162. private void OnStorageGetState(EntityUid uid, StorageComponent component, ref ComponentGetState args)
  163. {
  164. var storedItems = new Dictionary<NetEntity, ItemStorageLocation>();
  165. foreach (var (ent, location) in component.StoredItems)
  166. {
  167. storedItems[GetNetEntity(ent)] = location;
  168. }
  169. args.State = new StorageComponentState()
  170. {
  171. Grid = new List<Box2i>(component.Grid),
  172. MaxItemSize = component.MaxItemSize,
  173. StoredItems = storedItems,
  174. SavedLocations = component.SavedLocations,
  175. Whitelist = component.Whitelist,
  176. Blacklist = component.Blacklist
  177. };
  178. }
  179. public override void Shutdown()
  180. {
  181. _prototype.PrototypesReloaded -= OnPrototypesReloaded;
  182. }
  183. private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
  184. {
  185. if (args.ByType.ContainsKey(typeof(ItemSizePrototype))
  186. || (args.Removed?.ContainsKey(typeof(ItemSizePrototype)) ?? false))
  187. {
  188. UpdatePrototypeCache();
  189. }
  190. }
  191. private void UpdatePrototypeCache()
  192. {
  193. _defaultStorageMaxItemSize = _prototype.Index<ItemSizePrototype>(DefaultStorageMaxItemSize);
  194. _sortedSizes.Clear();
  195. _sortedSizes.AddRange(_prototype.EnumeratePrototypes<ItemSizePrototype>());
  196. _sortedSizes.Sort();
  197. var nextSmallest = new KeyValuePair<string, ItemSizePrototype>[_sortedSizes.Count];
  198. for (var i = 0; i < _sortedSizes.Count; i++)
  199. {
  200. var k = _sortedSizes[i].ID;
  201. var v = _sortedSizes[Math.Max(i - 1, 0)];
  202. nextSmallest[i] = new(k, v);
  203. }
  204. _nextSmallest = nextSmallest.ToFrozenDictionary();
  205. }
  206. private void OnComponentInit(EntityUid uid, StorageComponent storageComp, ComponentInit args)
  207. {
  208. storageComp.Container = ContainerSystem.EnsureContainer<Container>(uid, StorageComponent.ContainerId);
  209. UpdateAppearance((uid, storageComp, null));
  210. }
  211. /// <summary>
  212. /// If the user has nested-UIs open (e.g., PDA UI open when pda is in a backpack), close them.
  213. /// </summary>
  214. private void CloseNestedInterfaces(EntityUid uid, EntityUid actor, StorageComponent? storageComp = null)
  215. {
  216. if (!Resolve(uid, ref storageComp))
  217. return;
  218. // for each containing thing
  219. // if it has a storage comp
  220. // ensure unsubscribe from session
  221. // if it has a ui component
  222. // close ui
  223. foreach (var entity in storageComp.Container.ContainedEntities)
  224. {
  225. UI.CloseUis(entity, actor);
  226. }
  227. }
  228. private void OnBoundUIClosed(EntityUid uid, StorageComponent storageComp, BoundUIClosedEvent args)
  229. {
  230. CloseNestedInterfaces(uid, args.Actor, storageComp);
  231. // If UI is closed for everyone
  232. if (!UI.IsUiOpen(uid, args.UiKey))
  233. {
  234. UpdateAppearance((uid, storageComp, null));
  235. Audio.PlayPredicted(storageComp.StorageCloseSound, uid, args.Actor);
  236. }
  237. }
  238. private void AddUiVerb(EntityUid uid, StorageComponent component, GetVerbsEvent<ActivationVerb> args)
  239. {
  240. if (component.ShowVerb == false || !CanInteract(args.User, (uid, component), args.CanAccess && args.CanInteract))
  241. return;
  242. // Does this player currently have the storage UI open?
  243. var uiOpen = UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User);
  244. ActivationVerb verb = new()
  245. {
  246. Act = () =>
  247. {
  248. if (uiOpen)
  249. {
  250. UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
  251. }
  252. else
  253. {
  254. OpenStorageUI(uid, args.User, component, false);
  255. }
  256. }
  257. };
  258. if (uiOpen)
  259. {
  260. verb.Text = Loc.GetString("comp-storage-verb-close-storage");
  261. verb.Icon = new SpriteSpecifier.Texture(
  262. new("/Textures/Interface/VerbIcons/close.svg.192dpi.png"));
  263. }
  264. else
  265. {
  266. verb.Text = Loc.GetString("comp-storage-verb-open-storage");
  267. verb.Icon = new SpriteSpecifier.Texture(
  268. new("/Textures/Interface/VerbIcons/open.svg.192dpi.png"));
  269. }
  270. args.Verbs.Add(verb);
  271. }
  272. public void OpenStorageUI(EntityUid uid, EntityUid actor, StorageComponent? storageComp = null, bool silent = true)
  273. {
  274. // Handle recursively opening nested storages.
  275. if (ContainerSystem.TryGetContainingContainer(uid, out var container) &&
  276. UI.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, actor))
  277. {
  278. _nestedCheck = true;
  279. HideStorageWindow(container.Owner, actor);
  280. OpenStorageUIInternal(uid, actor, storageComp, silent: true);
  281. _nestedCheck = false;
  282. }
  283. else
  284. {
  285. // If you need something more sophisticated for multi-UI you'll need to code some smarter
  286. // interactions.
  287. if (_openStorageLimit == 1)
  288. UI.CloseUserUis<StorageComponent.StorageUiKey>(actor);
  289. OpenStorageUIInternal(uid, actor, storageComp, silent: silent);
  290. }
  291. }
  292. /// <summary>
  293. /// Opens the storage UI for an entity
  294. /// </summary>
  295. /// <param name="entity">The entity to open the UI for</param>
  296. private void OpenStorageUIInternal(EntityUid uid, EntityUid entity, StorageComponent? storageComp = null, bool silent = true)
  297. {
  298. if (!Resolve(uid, ref storageComp, false))
  299. return;
  300. // prevent spamming bag open / honkerton honk sound
  301. silent |= TryComp<UseDelayComponent>(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay), id: OpenUiUseDelayID);
  302. if (!CanInteract(entity, (uid, storageComp), silent: silent))
  303. return;
  304. if (!UI.TryOpenUi(uid, StorageComponent.StorageUiKey.Key, entity))
  305. return;
  306. if (!silent)
  307. {
  308. Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
  309. if (useDelay != null)
  310. UseDelay.TryResetDelay((uid, useDelay), id: OpenUiUseDelayID);
  311. }
  312. }
  313. public virtual void UpdateUI(Entity<StorageComponent?> entity) {}
  314. private void AddTransferVerbs(EntityUid uid, StorageComponent component, GetVerbsEvent<UtilityVerb> args)
  315. {
  316. if (!args.CanAccess || !args.CanInteract)
  317. return;
  318. var entities = component.Container.ContainedEntities;
  319. if (entities.Count == 0 || !CanInteract(args.User, (uid, component)))
  320. return;
  321. // if the target is storage, add a verb to transfer storage.
  322. if (TryComp(args.Target, out StorageComponent? targetStorage)
  323. && (!TryComp(args.Target, out LockComponent? targetLock) || !targetLock.Locked))
  324. {
  325. UtilityVerb verb = new()
  326. {
  327. Text = Loc.GetString("storage-component-transfer-verb"),
  328. IconEntity = GetNetEntity(args.Using),
  329. Act = () => TransferEntities(uid, args.Target, args.User, component, null, targetStorage, targetLock)
  330. };
  331. args.Verbs.Add(verb);
  332. }
  333. }
  334. /// <summary>
  335. /// Inserts storable entities into this storage container if possible, otherwise return to the hand of the user
  336. /// </summary>
  337. /// <returns>true if inserted, false otherwise</returns>
  338. private void OnInteractUsing(EntityUid uid, StorageComponent storageComp, InteractUsingEvent args)
  339. {
  340. if (args.Handled || !storageComp.ClickInsert || !CanInteract(args.User, (uid, storageComp), silent: false))
  341. return;
  342. var attemptEv = new StorageInteractUsingAttemptEvent();
  343. RaiseLocalEvent(uid, ref attemptEv);
  344. if (attemptEv.Cancelled)
  345. return;
  346. PlayerInsertHeldEntity((uid, storageComp), args.User);
  347. // Always handle it, even if insertion fails.
  348. // We don't want to trigger any AfterInteract logic here.
  349. // Example issue would be placing wires if item doesn't fit in backpack.
  350. args.Handled = true;
  351. }
  352. /// <summary>
  353. /// Sends a message to open the storage UI
  354. /// </summary>
  355. private void OnActivate(EntityUid uid, StorageComponent storageComp, ActivateInWorldEvent args)
  356. {
  357. if (args.Handled || !args.Complex || !storageComp.OpenOnActivate || !CanInteract(args.User, (uid, storageComp)))
  358. return;
  359. // Toggle
  360. if (UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.User))
  361. {
  362. UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.User);
  363. }
  364. else
  365. {
  366. OpenStorageUI(uid, args.User, storageComp, false);
  367. }
  368. args.Handled = true;
  369. }
  370. protected virtual void HideStorageWindow(EntityUid uid, EntityUid actor)
  371. {
  372. }
  373. protected virtual void ShowStorageWindow(EntityUid uid, EntityUid actor)
  374. {
  375. }
  376. /// <summary>
  377. /// Specifically for storage implants.
  378. /// </summary>
  379. private void OnImplantActivate(EntityUid uid, StorageComponent storageComp, OpenStorageImplantEvent args)
  380. {
  381. if (args.Handled)
  382. return;
  383. var uiOpen = UI.IsUiOpen(uid, StorageComponent.StorageUiKey.Key, args.Performer);
  384. if (uiOpen)
  385. UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, args.Performer);
  386. else
  387. OpenStorageUI(uid, args.Performer, storageComp, false);
  388. args.Handled = true;
  389. }
  390. /// <summary>
  391. /// Allows a user to pick up entities by clicking them, or pick up all entities in a certain radius
  392. /// around a click.
  393. /// </summary>
  394. /// <returns></returns>
  395. private void AfterInteract(EntityUid uid, StorageComponent storageComp, AfterInteractEvent args)
  396. {
  397. if (args.Handled || !args.CanReach || !UseDelay.TryResetDelay(uid, checkDelayed: true, id: QuickInsertUseDelayID))
  398. return;
  399. // Pick up all entities in a radius around the clicked location.
  400. // The last half of the if is because carpets exist and this is terrible
  401. if (storageComp.AreaInsert && (args.Target == null || !HasComp<ItemComponent>(args.Target.Value)))
  402. {
  403. _entList.Clear();
  404. _entSet.Clear();
  405. _entityLookupSystem.GetEntitiesInRange(args.ClickLocation, storageComp.AreaInsertRadius, _entSet, LookupFlags.Dynamic | LookupFlags.Sundries);
  406. var delay = 0f;
  407. foreach (var entity in _entSet)
  408. {
  409. if (entity == args.User
  410. || !_itemQuery.TryGetComponent(entity, out var itemComp) // Need comp to get item size to get weight
  411. || !_prototype.TryIndex(itemComp.Size, out var itemSize)
  412. || !CanInsert(uid, entity, out _, storageComp, item: itemComp)
  413. || !_interactionSystem.InRangeUnobstructed(args.User, entity))
  414. {
  415. continue;
  416. }
  417. _entList.Add(entity);
  418. delay += itemSize.Weight * AreaInsertDelayPerItem;
  419. if (_entList.Count >= StorageComponent.AreaPickupLimit)
  420. break;
  421. }
  422. //If there's only one then let's be generous
  423. if (_entList.Count >= 1)
  424. {
  425. var doAfterArgs = new DoAfterArgs(EntityManager, args.User, delay, new AreaPickupDoAfterEvent(GetNetEntityList(_entList)), uid, target: uid)
  426. {
  427. BreakOnDamage = true,
  428. BreakOnMove = true,
  429. NeedHand = true,
  430. };
  431. _doAfterSystem.TryStartDoAfter(doAfterArgs);
  432. args.Handled = true;
  433. }
  434. return;
  435. }
  436. // Pick up the clicked entity
  437. if (storageComp.QuickInsert)
  438. {
  439. if (args.Target is not { Valid: true } target)
  440. return;
  441. if (ContainerSystem.IsEntityInContainer(target)
  442. || target == args.User
  443. || !_itemQuery.HasComponent(target))
  444. {
  445. return;
  446. }
  447. if (TryComp(uid, out TransformComponent? transformOwner) && TryComp(target, out TransformComponent? transformEnt))
  448. {
  449. var parent = transformOwner.ParentUid;
  450. var position = TransformSystem.ToCoordinates(
  451. parent.IsValid() ? parent : uid,
  452. TransformSystem.GetMapCoordinates(transformEnt)
  453. );
  454. args.Handled = true;
  455. if (PlayerInsertEntityInWorld((uid, storageComp), args.User, target))
  456. {
  457. EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(GetNetEntity(uid),
  458. new List<NetEntity> { GetNetEntity(target) },
  459. new List<NetCoordinates> { GetNetCoordinates(position) },
  460. new List<Angle> { transformOwner.LocalRotation }), args.User);
  461. }
  462. }
  463. }
  464. }
  465. private void OnDoAfter(EntityUid uid, StorageComponent component, AreaPickupDoAfterEvent args)
  466. {
  467. if (args.Handled || args.Cancelled)
  468. return;
  469. args.Handled = true;
  470. var successfullyInserted = new List<EntityUid>();
  471. var successfullyInsertedPositions = new List<EntityCoordinates>();
  472. var successfullyInsertedAngles = new List<Angle>();
  473. if (!_xformQuery.TryGetComponent(uid, out var xform))
  474. {
  475. return;
  476. }
  477. var entCount = Math.Min(StorageComponent.AreaPickupLimit, args.Entities.Count);
  478. for (var i = 0; i < entCount; i++)
  479. {
  480. var entity = GetEntity(args.Entities[i]);
  481. // Check again, situation may have changed for some entities, but we'll still pick up any that are valid
  482. if (ContainerSystem.IsEntityInContainer(entity)
  483. || entity == args.Args.User
  484. || !_itemQuery.HasComponent(entity))
  485. {
  486. continue;
  487. }
  488. if (!_xformQuery.TryGetComponent(entity, out var targetXform) ||
  489. targetXform.MapID != xform.MapID)
  490. {
  491. continue;
  492. }
  493. var position = TransformSystem.ToCoordinates(
  494. xform.ParentUid.IsValid() ? xform.ParentUid : uid,
  495. new MapCoordinates(TransformSystem.GetWorldPosition(targetXform), targetXform.MapID)
  496. );
  497. var angle = targetXform.LocalRotation;
  498. if (PlayerInsertEntityInWorld((uid, component), args.Args.User, entity, playSound: false))
  499. {
  500. successfullyInserted.Add(entity);
  501. successfullyInsertedPositions.Add(position);
  502. successfullyInsertedAngles.Add(angle);
  503. }
  504. }
  505. // If we picked up at least one thing, play a sound and do a cool animation!
  506. if (successfullyInserted.Count > 0)
  507. {
  508. Audio.PlayPredicted(component.StorageInsertSound, uid, args.User, _audioParams);
  509. EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(
  510. GetNetEntity(uid),
  511. GetNetEntityList(successfullyInserted),
  512. GetNetCoordinatesList(successfullyInsertedPositions),
  513. successfullyInsertedAngles), args.User);
  514. }
  515. args.Handled = true;
  516. }
  517. private void OnReclaimed(EntityUid uid, StorageComponent storageComp, GotReclaimedEvent args)
  518. {
  519. ContainerSystem.EmptyContainer(storageComp.Container, destination: args.ReclaimerCoordinates);
  520. }
  521. private void OnDestroy(EntityUid uid, StorageComponent storageComp, DestructionEventArgs args)
  522. {
  523. var coordinates = TransformSystem.GetMoverCoordinates(uid);
  524. // Being destroyed so need to recalculate.
  525. ContainerSystem.EmptyContainer(storageComp.Container, destination: coordinates);
  526. }
  527. /// <summary>
  528. /// This function gets called when the user clicked on an item in the storage UI. This will either place the
  529. /// item in the user's hand if it is currently empty, or interact with the item using the user's currently
  530. /// held item.
  531. /// </summary>
  532. private void OnInteractWithItem(StorageInteractWithItemEvent msg, EntitySessionEventArgs args)
  533. {
  534. if (!ValidateInput(args, msg.StorageUid, msg.InteractedItemUid, out var player, out var storage, out var item))
  535. return;
  536. // If the user's active hand is empty, try pick up the item.
  537. if (player.Comp.ActiveHandEntity == null)
  538. {
  539. _adminLog.Add(
  540. LogType.Storage,
  541. LogImpact.Low,
  542. $"{ToPrettyString(player):player} is attempting to take {ToPrettyString(item):item} out of {ToPrettyString(storage):storage}");
  543. if (_sharedHandsSystem.TryPickupAnyHand(player, item, handsComp: player.Comp)
  544. && storage.Comp.StorageRemoveSound != null)
  545. {
  546. Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams);
  547. }
  548. return;
  549. }
  550. _adminLog.Add(
  551. LogType.Storage,
  552. LogImpact.Low,
  553. $"{ToPrettyString(player):player} is interacting with {ToPrettyString(item):item} while it is stored in {ToPrettyString(storage):storage} using {ToPrettyString(player.Comp.ActiveHandEntity):used}");
  554. // Else, interact using the held item
  555. if (_interactionSystem.InteractUsing(player,
  556. player.Comp.ActiveHandEntity.Value,
  557. item,
  558. Transform(item).Coordinates,
  559. checkCanInteract: false))
  560. return;
  561. var failedEv = new StorageInsertFailedEvent((storage, storage.Comp), (player, player.Comp));
  562. RaiseLocalEvent(storage, ref failedEv);
  563. }
  564. private void OnSetItemLocation(StorageSetItemLocationEvent msg, EntitySessionEventArgs args)
  565. {
  566. if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item))
  567. return;
  568. _adminLog.Add(
  569. LogType.Storage,
  570. LogImpact.Low,
  571. $"{ToPrettyString(player):player} is updating the location of {ToPrettyString(item):item} within {ToPrettyString(storage):storage}");
  572. TrySetItemStorageLocation(item!, storage!, msg.Location);
  573. }
  574. private void OnStorageNested(OpenNestedStorageEvent msg, EntitySessionEventArgs args)
  575. {
  576. if (!NestedStorage)
  577. return;
  578. if (!TryGetEntity(msg.InteractedItemUid, out var itemEnt))
  579. return;
  580. _nestedCheck = true;
  581. var result = ValidateInput(args,
  582. msg.StorageUid,
  583. msg.InteractedItemUid,
  584. out var player,
  585. out var storage,
  586. out var item);
  587. if (!result)
  588. {
  589. _nestedCheck = false;
  590. return;
  591. }
  592. HideStorageWindow(storage.Owner, player.Owner);
  593. OpenStorageUI(item.Owner, player.Owner, silent: true);
  594. _nestedCheck = false;
  595. }
  596. private void OnStorageTransfer(StorageTransferItemEvent msg, EntitySessionEventArgs args)
  597. {
  598. if (!TryGetEntity(msg.ItemEnt, out var itemEnt))
  599. return;
  600. var localPlayer = args.SenderSession.AttachedEntity;
  601. if (!TryComp(localPlayer, out HandsComponent? handsComp) || !_sharedHandsSystem.TryPickup(localPlayer.Value, itemEnt.Value, handsComp: handsComp, animate: false))
  602. return;
  603. if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true))
  604. return;
  605. _adminLog.Add(
  606. LogType.Storage,
  607. LogImpact.Low,
  608. $"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}");
  609. InsertAt(storage!, item!, msg.Location, out _, player, stackAutomatically: false);
  610. }
  611. private void OnInsertItemIntoLocation(StorageInsertItemIntoLocationEvent msg, EntitySessionEventArgs args)
  612. {
  613. if (!ValidateInput(args, msg.StorageEnt, msg.ItemEnt, out var player, out var storage, out var item, held: true))
  614. return;
  615. _adminLog.Add(
  616. LogType.Storage,
  617. LogImpact.Low,
  618. $"{ToPrettyString(player):player} is inserting {ToPrettyString(item):item} into {ToPrettyString(storage):storage}");
  619. InsertAt(storage!, item!, msg.Location, out _, player, stackAutomatically: false);
  620. }
  621. private void OnSaveItemLocation(StorageSaveItemLocationEvent msg, EntitySessionEventArgs args)
  622. {
  623. if (!ValidateInput(args, msg.Storage, msg.Item, out var player, out var storage, out var item))
  624. return;
  625. SaveItemLocation(storage!, item.Owner);
  626. }
  627. private void OnBoundUIOpen(Entity<StorageComponent> ent, ref BoundUIOpenedEvent args)
  628. {
  629. UpdateAppearance((ent.Owner, ent.Comp, null));
  630. }
  631. private void OnBoundUIAttempt(BoundUserInterfaceMessageAttempt args)
  632. {
  633. if (args.UiKey is not StorageComponent.StorageUiKey.Key ||
  634. _openStorageLimit == -1 ||
  635. _nestedCheck ||
  636. args.Message is not OpenBoundInterfaceMessage)
  637. return;
  638. var uid = args.Target;
  639. var actor = args.Actor;
  640. var count = 0;
  641. if (_userQuery.TryComp(actor, out var userComp))
  642. {
  643. foreach (var (ui, keys) in userComp.OpenInterfaces)
  644. {
  645. if (ui == uid)
  646. continue;
  647. foreach (var key in keys)
  648. {
  649. if (key is not StorageComponent.StorageUiKey)
  650. continue;
  651. count++;
  652. if (count >= _openStorageLimit)
  653. {
  654. args.Cancel();
  655. }
  656. break;
  657. }
  658. }
  659. }
  660. }
  661. private void OnEntInserted(Entity<StorageComponent> entity, ref EntInsertedIntoContainerMessage args)
  662. {
  663. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
  664. if (entity.Comp.Container == null)
  665. return;
  666. if (args.Container.ID != StorageComponent.ContainerId)
  667. return;
  668. if (!entity.Comp.StoredItems.ContainsKey(args.Entity))
  669. {
  670. if (!TryGetAvailableGridSpace((entity.Owner, entity.Comp), (args.Entity, null), out var location))
  671. {
  672. ContainerSystem.Remove(args.Entity, args.Container, force: true);
  673. return;
  674. }
  675. entity.Comp.StoredItems[args.Entity] = location.Value;
  676. Dirty(entity, entity.Comp);
  677. }
  678. UpdateAppearance((entity, entity.Comp, null));
  679. UpdateUI((entity, entity.Comp));
  680. }
  681. private void OnEntRemoved(Entity<StorageComponent> entity, ref EntRemovedFromContainerMessage args)
  682. {
  683. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
  684. if (entity.Comp.Container == null)
  685. return;
  686. if (args.Container.ID != StorageComponent.ContainerId)
  687. return;
  688. entity.Comp.StoredItems.Remove(args.Entity);
  689. Dirty(entity, entity.Comp);
  690. UpdateAppearance((entity, entity.Comp, null));
  691. UpdateUI((entity, entity.Comp));
  692. }
  693. private void OnInsertAttempt(EntityUid uid, StorageComponent component, ContainerIsInsertingAttemptEvent args)
  694. {
  695. if (args.Cancelled || args.Container.ID != StorageComponent.ContainerId)
  696. return;
  697. // don't run cyclical CanInsert() loops
  698. if (CheckingCanInsert)
  699. return;
  700. if (!CanInsert(uid, args.EntityUid, out var reason, component, ignoreStacks: true))
  701. {
  702. #if DEBUG
  703. if (reason != null)
  704. CantFillReasons.Add(reason);
  705. #endif
  706. args.Cancel();
  707. }
  708. }
  709. public void UpdateAppearance(Entity<StorageComponent?, AppearanceComponent?> entity)
  710. {
  711. // TODO STORAGE remove appearance data and just use the data on the component.
  712. var (uid, storage, appearance) = entity;
  713. if (!Resolve(uid, ref storage, ref appearance, false))
  714. return;
  715. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
  716. if (storage.Container == null)
  717. return; // component hasn't yet been initialized.
  718. var capacity = storage.Grid.GetArea();
  719. var used = GetCumulativeItemAreas((uid, storage));
  720. var isOpen = UI.IsUiOpen(entity.Owner, StorageComponent.StorageUiKey.Key);
  721. _appearance.SetData(uid, StorageVisuals.StorageUsed, used, appearance);
  722. _appearance.SetData(uid, StorageVisuals.Capacity, capacity, appearance);
  723. _appearance.SetData(uid, StorageVisuals.Open, isOpen, appearance);
  724. _appearance.SetData(uid, SharedBagOpenVisuals.BagState, isOpen ? SharedBagState.Open : SharedBagState.Closed, appearance);
  725. // HideClosedStackVisuals true sets the StackVisuals.Hide to the open state of the storage.
  726. // This is for containers that only show their contents when open. (e.g. donut boxes)
  727. if (storage.HideStackVisualsWhenClosed)
  728. _appearance.SetData(uid, StackVisuals.Hide, !isOpen, appearance);
  729. }
  730. /// <summary>
  731. /// Move entities from one storage to another.
  732. /// </summary>
  733. public void TransferEntities(EntityUid source, EntityUid target, EntityUid? user = null,
  734. StorageComponent? sourceComp = null, LockComponent? sourceLock = null,
  735. StorageComponent? targetComp = null, LockComponent? targetLock = null)
  736. {
  737. if (!Resolve(source, ref sourceComp) || !Resolve(target, ref targetComp))
  738. return;
  739. var entities = sourceComp.Container.ContainedEntities;
  740. if (entities.Count == 0)
  741. return;
  742. if (Resolve(source, ref sourceLock, false) && sourceLock.Locked
  743. || Resolve(target, ref targetLock, false) && targetLock.Locked)
  744. return;
  745. foreach (var entity in entities.ToArray())
  746. {
  747. Insert(target, entity, out _, user: user, targetComp, playSound: false);
  748. }
  749. Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user, _audioParams);
  750. }
  751. /// <summary>
  752. /// Verifies if an entity can be stored and if it fits
  753. /// </summary>
  754. /// <param name="uid">The entity to check</param>
  755. /// <param name="insertEnt"></param>
  756. /// <param name="reason">If returning false, the reason displayed to the player</param>
  757. /// <param name="storageComp"></param>
  758. /// <param name="item"></param>
  759. /// <param name="ignoreStacks"></param>
  760. /// <param name="ignoreLocation"></param>
  761. /// <returns>true if it can be inserted, false otherwise</returns>
  762. public bool CanInsert(
  763. EntityUid uid,
  764. EntityUid insertEnt,
  765. out string? reason,
  766. StorageComponent? storageComp = null,
  767. ItemComponent? item = null,
  768. bool ignoreStacks = false,
  769. bool ignoreLocation = false)
  770. {
  771. if (!Resolve(uid, ref storageComp) || !Resolve(insertEnt, ref item, false))
  772. {
  773. reason = null;
  774. return false;
  775. }
  776. if (Transform(insertEnt).Anchored)
  777. {
  778. reason = "comp-storage-anchored-failure";
  779. return false;
  780. }
  781. if (_whitelistSystem.IsWhitelistFail(storageComp.Whitelist, insertEnt) ||
  782. _whitelistSystem.IsBlacklistPass(storageComp.Blacklist, insertEnt))
  783. {
  784. reason = "comp-storage-invalid-container";
  785. return false;
  786. }
  787. if (!ignoreStacks
  788. && _stackQuery.TryGetComponent(insertEnt, out var stack)
  789. && HasSpaceInStacks((uid, storageComp), stack.StackTypeId))
  790. {
  791. reason = null;
  792. return true;
  793. }
  794. var maxSize = GetMaxItemSize((uid, storageComp));
  795. if (ItemSystem.GetSizePrototype(item.Size) > maxSize)
  796. {
  797. reason = "comp-storage-too-big";
  798. return false;
  799. }
  800. if (TryComp<StorageComponent>(insertEnt, out var insertStorage)
  801. && GetMaxItemSize((insertEnt, insertStorage)) >= maxSize)
  802. {
  803. reason = "comp-storage-too-big";
  804. return false;
  805. }
  806. if (!ignoreLocation && !storageComp.StoredItems.ContainsKey(insertEnt))
  807. {
  808. if (!TryGetAvailableGridSpace((uid, storageComp), (insertEnt, item), out _))
  809. {
  810. reason = "comp-storage-insufficient-capacity";
  811. return false;
  812. }
  813. }
  814. CheckingCanInsert = true;
  815. if (!ContainerSystem.CanInsert(insertEnt, storageComp.Container))
  816. {
  817. CheckingCanInsert = false;
  818. reason = null;
  819. return false;
  820. }
  821. CheckingCanInsert = false;
  822. reason = null;
  823. return true;
  824. }
  825. /// <summary>
  826. /// Inserts into the storage container at a given location
  827. /// </summary>
  828. /// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
  829. /// inserted.</returns>
  830. public bool InsertAt(
  831. Entity<StorageComponent?> uid,
  832. Entity<ItemComponent?> insertEnt,
  833. ItemStorageLocation location,
  834. out EntityUid? stackedEntity,
  835. EntityUid? user = null,
  836. bool playSound = true,
  837. bool stackAutomatically = true)
  838. {
  839. stackedEntity = null;
  840. if (!Resolve(uid, ref uid.Comp))
  841. return false;
  842. if (!ItemFitsInGridLocation(insertEnt, uid, location))
  843. return false;
  844. uid.Comp.StoredItems[insertEnt] = location;
  845. Dirty(uid, uid.Comp);
  846. if (Insert(uid,
  847. insertEnt,
  848. out stackedEntity,
  849. out _,
  850. user: user,
  851. storageComp: uid.Comp,
  852. playSound: playSound,
  853. stackAutomatically: stackAutomatically))
  854. {
  855. return true;
  856. }
  857. uid.Comp.StoredItems.Remove(insertEnt);
  858. return false;
  859. }
  860. /// <summary>
  861. /// Inserts into the storage container
  862. /// </summary>
  863. /// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
  864. /// inserted.</returns>
  865. public bool Insert(
  866. EntityUid uid,
  867. EntityUid insertEnt,
  868. out EntityUid? stackedEntity,
  869. EntityUid? user = null,
  870. StorageComponent? storageComp = null,
  871. bool playSound = true,
  872. bool stackAutomatically = true)
  873. {
  874. return Insert(uid, insertEnt, out stackedEntity, out _, user: user, storageComp: storageComp, playSound: playSound, stackAutomatically: stackAutomatically);
  875. }
  876. /// <summary>
  877. /// Inserts into the storage container
  878. /// </summary>
  879. /// <returns>true if the entity was inserted, false otherwise. This will also return true if a stack was partially
  880. /// inserted</returns>
  881. public bool Insert(
  882. EntityUid uid,
  883. EntityUid insertEnt,
  884. out EntityUid? stackedEntity,
  885. out string? reason,
  886. EntityUid? user = null,
  887. StorageComponent? storageComp = null,
  888. bool playSound = true,
  889. bool stackAutomatically = true)
  890. {
  891. stackedEntity = null;
  892. reason = null;
  893. if (!Resolve(uid, ref storageComp))
  894. return false;
  895. /*
  896. * 1. If the inserted thing is stackable then try to stack it to existing stacks
  897. * 2. If anything remains insert whatever is possible.
  898. * 3. If insertion is not possible then leave the stack as is.
  899. * At either rate still play the insertion sound
  900. *
  901. * For now we just treat items as always being the same size regardless of stack count.
  902. */
  903. if (!stackAutomatically || !_stackQuery.TryGetComponent(insertEnt, out var insertStack))
  904. {
  905. if (!ContainerSystem.Insert(insertEnt, storageComp.Container))
  906. return false;
  907. if (playSound)
  908. Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
  909. return true;
  910. }
  911. var toInsertCount = insertStack.Count;
  912. foreach (var ent in storageComp.Container.ContainedEntities)
  913. {
  914. if (!_stackQuery.TryGetComponent(ent, out var containedStack))
  915. continue;
  916. if (!_stack.TryAdd(insertEnt, ent, insertStack, containedStack))
  917. continue;
  918. stackedEntity = ent;
  919. if (insertStack.Count == 0)
  920. break;
  921. }
  922. // Still stackable remaining
  923. if (insertStack.Count > 0
  924. && !ContainerSystem.Insert(insertEnt, storageComp.Container)
  925. && toInsertCount == insertStack.Count)
  926. {
  927. // Failed to insert anything.
  928. return false;
  929. }
  930. if (playSound)
  931. Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
  932. return true;
  933. }
  934. /// <summary>
  935. /// Inserts an entity into storage from the player's active hand
  936. /// </summary>
  937. /// <param name="ent">The storage entity and component to insert into.</param>
  938. /// <param name="player">The player and hands component to insert the held entity from.</param>
  939. /// <returns>True if inserted, otherwise false.</returns>
  940. public bool PlayerInsertHeldEntity(Entity<StorageComponent?> ent, Entity<HandsComponent?> player)
  941. {
  942. if (!Resolve(ent.Owner, ref ent.Comp)
  943. || !Resolve(player.Owner, ref player.Comp)
  944. || player.Comp.ActiveHandEntity == null)
  945. return false;
  946. var toInsert = player.Comp.ActiveHandEntity;
  947. if (!CanInsert(ent, toInsert.Value, out var reason, ent.Comp))
  948. {
  949. _popupSystem.PopupClient(Loc.GetString(reason ?? "comp-storage-cant-insert"), ent, player);
  950. return false;
  951. }
  952. if (!_sharedHandsSystem.CanDrop(player, toInsert.Value, player.Comp))
  953. {
  954. _popupSystem.PopupClient(Loc.GetString("comp-storage-cant-drop", ("entity", toInsert.Value)), ent, player);
  955. return false;
  956. }
  957. return PlayerInsertEntityInWorld((ent, ent.Comp), player, toInsert.Value);
  958. }
  959. /// <summary>
  960. /// Inserts an Entity (<paramref name="toInsert"/>) in the world into storage, informing <paramref name="player"/> if it fails.
  961. /// <paramref name="toInsert"/> is *NOT* held, see <see cref="PlayerInsertHeldEntity(Entity{StorageComponent?},Entity{HandsComponent?})"/>.
  962. /// </summary>
  963. /// <param name="uid"></param>
  964. /// <param name="player">The player to insert an entity with</param>
  965. /// <param name="toInsert"></param>
  966. /// <returns>true if inserted, false otherwise</returns>
  967. public bool PlayerInsertEntityInWorld(Entity<StorageComponent?> uid, EntityUid player, EntityUid toInsert, bool playSound = true)
  968. {
  969. if (!Resolve(uid, ref uid.Comp) || !_interactionSystem.InRangeUnobstructed(player, uid.Owner))
  970. return false;
  971. if (!Insert(uid, toInsert, out _, user: player, uid.Comp, playSound: playSound))
  972. {
  973. _popupSystem.PopupClient(Loc.GetString("comp-storage-cant-insert"), uid, player);
  974. return false;
  975. }
  976. return true;
  977. }
  978. /// <summary>
  979. /// Attempts to set the location of an item already inside of a storage container.
  980. /// </summary>
  981. public bool TrySetItemStorageLocation(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, ItemStorageLocation location)
  982. {
  983. if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
  984. return false;
  985. if (!storageEnt.Comp.Container.ContainedEntities.Contains(itemEnt))
  986. return false;
  987. if (!ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation))
  988. return false;
  989. storageEnt.Comp.StoredItems[itemEnt] = location;
  990. UpdateUI(storageEnt);
  991. Dirty(storageEnt, storageEnt.Comp);
  992. return true;
  993. }
  994. /// <summary>
  995. /// Tries to find the first available spot on a storage grid.
  996. /// starts at the top-left and goes right and down.
  997. /// </summary>
  998. public bool TryGetAvailableGridSpace(
  999. Entity<StorageComponent?> storageEnt,
  1000. Entity<ItemComponent?> itemEnt,
  1001. [NotNullWhen(true)] out ItemStorageLocation? storageLocation)
  1002. {
  1003. storageLocation = null;
  1004. if (!Resolve(storageEnt, ref storageEnt.Comp) || !Resolve(itemEnt, ref itemEnt.Comp))
  1005. return false;
  1006. // if the item has an available saved location, use that
  1007. if (FindSavedLocation(storageEnt, itemEnt, out storageLocation))
  1008. return true;
  1009. var storageBounding = storageEnt.Comp.Grid.GetBoundingBox();
  1010. Angle startAngle;
  1011. if (storageEnt.Comp.DefaultStorageOrientation == null)
  1012. {
  1013. startAngle = Angle.FromDegrees(-itemEnt.Comp.StoredRotation);
  1014. }
  1015. else
  1016. {
  1017. if (storageBounding.Width < storageBounding.Height)
  1018. {
  1019. startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Horizontal
  1020. ? Angle.Zero
  1021. : Angle.FromDegrees(90);
  1022. }
  1023. else
  1024. {
  1025. startAngle = storageEnt.Comp.DefaultStorageOrientation == StorageDefaultOrientation.Vertical
  1026. ? Angle.Zero
  1027. : Angle.FromDegrees(90);
  1028. }
  1029. }
  1030. for (var y = storageBounding.Bottom; y <= storageBounding.Top; y++)
  1031. {
  1032. for (var x = storageBounding.Left; x <= storageBounding.Right; x++)
  1033. {
  1034. for (var angle = startAngle; angle <= Angle.FromDegrees(360 - startAngle); angle += Math.PI / 2f)
  1035. {
  1036. var location = new ItemStorageLocation(angle, (x, y));
  1037. if (ItemFitsInGridLocation(itemEnt, storageEnt, location))
  1038. {
  1039. storageLocation = location;
  1040. return true;
  1041. }
  1042. }
  1043. }
  1044. }
  1045. return false;
  1046. }
  1047. /// <summary>
  1048. /// Tries to find a saved location for an item from its name.
  1049. /// If none are saved or they are all blocked nothing is returned.
  1050. /// </summary>
  1051. public bool FindSavedLocation(
  1052. Entity<StorageComponent?> ent,
  1053. Entity<ItemComponent?> item,
  1054. [NotNullWhen(true)] out ItemStorageLocation? storageLocation)
  1055. {
  1056. storageLocation = null;
  1057. if (!Resolve(ent, ref ent.Comp))
  1058. return false;
  1059. var name = Name(item);
  1060. if (!ent.Comp.SavedLocations.TryGetValue(name, out var list))
  1061. return false;
  1062. foreach (var location in list)
  1063. {
  1064. if (ItemFitsInGridLocation(item, ent, location))
  1065. {
  1066. storageLocation = location;
  1067. return true;
  1068. }
  1069. }
  1070. return false;
  1071. }
  1072. /// <summary>
  1073. /// Saves an item's location in the grid for later insertion to use.
  1074. /// </summary>
  1075. public void SaveItemLocation(Entity<StorageComponent?> ent, Entity<MetaDataComponent?> item)
  1076. {
  1077. if (!Resolve(ent, ref ent.Comp))
  1078. return;
  1079. // needs to actually be stored in it somewhere to save it
  1080. if (!ent.Comp.StoredItems.TryGetValue(item, out var location))
  1081. return;
  1082. var name = Name(item, item.Comp);
  1083. if (ent.Comp.SavedLocations.TryGetValue(name, out var list))
  1084. {
  1085. // iterate to make sure its not already been saved
  1086. for (int i = 0; i < list.Count; i++)
  1087. {
  1088. var saved = list[i];
  1089. if (saved == location)
  1090. {
  1091. list.Remove(location);
  1092. return;
  1093. }
  1094. }
  1095. list.Add(location);
  1096. }
  1097. else
  1098. {
  1099. list = new List<ItemStorageLocation>()
  1100. {
  1101. location
  1102. };
  1103. ent.Comp.SavedLocations[name] = list;
  1104. }
  1105. Dirty(ent, ent.Comp);
  1106. UpdateUI((ent.Owner, ent.Comp));
  1107. }
  1108. /// <summary>
  1109. /// Checks if an item fits into a specific spot on a storage grid.
  1110. /// </summary>
  1111. public bool ItemFitsInGridLocation(
  1112. Entity<ItemComponent?> itemEnt,
  1113. Entity<StorageComponent?> storageEnt,
  1114. ItemStorageLocation location)
  1115. {
  1116. return ItemFitsInGridLocation(itemEnt, storageEnt, location.Position, location.Rotation);
  1117. }
  1118. /// <summary>
  1119. /// Checks if an item fits into a specific spot on a storage grid.
  1120. /// </summary>
  1121. public bool ItemFitsInGridLocation(
  1122. Entity<ItemComponent?> itemEnt,
  1123. Entity<StorageComponent?> storageEnt,
  1124. Vector2i position,
  1125. Angle rotation)
  1126. {
  1127. if (!Resolve(itemEnt, ref itemEnt.Comp) || !Resolve(storageEnt, ref storageEnt.Comp))
  1128. return false;
  1129. var gridBounds = storageEnt.Comp.Grid.GetBoundingBox();
  1130. if (!gridBounds.Contains(position))
  1131. return false;
  1132. var itemShape = ItemSystem.GetAdjustedItemShape(itemEnt, rotation, position);
  1133. foreach (var box in itemShape)
  1134. {
  1135. for (var offsetY = box.Bottom; offsetY <= box.Top; offsetY++)
  1136. {
  1137. for (var offsetX = box.Left; offsetX <= box.Right; offsetX++)
  1138. {
  1139. var pos = (offsetX, offsetY);
  1140. if (!IsGridSpaceEmpty(itemEnt, storageEnt, pos))
  1141. return false;
  1142. }
  1143. }
  1144. }
  1145. return true;
  1146. }
  1147. /// <summary>
  1148. /// Checks if a space on a grid is valid and not occupied by any other pieces.
  1149. /// </summary>
  1150. public bool IsGridSpaceEmpty(Entity<ItemComponent?> itemEnt, Entity<StorageComponent?> storageEnt, Vector2i location)
  1151. {
  1152. if (!Resolve(storageEnt, ref storageEnt.Comp))
  1153. return false;
  1154. var validGrid = false;
  1155. foreach (var grid in storageEnt.Comp.Grid)
  1156. {
  1157. if (grid.Contains(location))
  1158. {
  1159. validGrid = true;
  1160. break;
  1161. }
  1162. }
  1163. if (!validGrid)
  1164. return false;
  1165. foreach (var (ent, storedItem) in storageEnt.Comp.StoredItems)
  1166. {
  1167. if (ent == itemEnt.Owner)
  1168. continue;
  1169. if (!_itemQuery.TryGetComponent(ent, out var itemComp))
  1170. continue;
  1171. var adjustedShape = ItemSystem.GetAdjustedItemShape((ent, itemComp), storedItem);
  1172. foreach (var box in adjustedShape)
  1173. {
  1174. if (box.Contains(location))
  1175. return false;
  1176. }
  1177. }
  1178. return true;
  1179. }
  1180. /// <summary>
  1181. /// Returns true if there is enough space to theoretically fit another item.
  1182. /// </summary>
  1183. public bool HasSpace(Entity<StorageComponent?> uid)
  1184. {
  1185. if (!Resolve(uid, ref uid.Comp))
  1186. return false;
  1187. return GetCumulativeItemAreas(uid) < uid.Comp.Grid.GetArea() || HasSpaceInStacks(uid);
  1188. }
  1189. private bool HasSpaceInStacks(Entity<StorageComponent?> uid, string? stackType = null)
  1190. {
  1191. if (!Resolve(uid, ref uid.Comp))
  1192. return false;
  1193. foreach (var contained in uid.Comp.Container.ContainedEntities)
  1194. {
  1195. if (!_stackQuery.TryGetComponent(contained, out var stack))
  1196. continue;
  1197. if (stackType != null && !stack.StackTypeId.Equals(stackType))
  1198. continue;
  1199. if (_stack.GetAvailableSpace(stack) == 0)
  1200. continue;
  1201. return true;
  1202. }
  1203. return false;
  1204. }
  1205. /// <summary>
  1206. /// Returns the sum of all the ItemSizes of the items inside of a storage.
  1207. /// </summary>
  1208. public int GetCumulativeItemAreas(Entity<StorageComponent?> entity)
  1209. {
  1210. if (!Resolve(entity, ref entity.Comp))
  1211. return 0;
  1212. var sum = 0;
  1213. foreach (var item in entity.Comp.Container.ContainedEntities)
  1214. {
  1215. if (!_itemQuery.TryGetComponent(item, out var itemComp))
  1216. continue;
  1217. sum += ItemSystem.GetItemShape((item, itemComp)).GetArea();
  1218. }
  1219. return sum;
  1220. }
  1221. public ItemSizePrototype GetMaxItemSize(Entity<StorageComponent?> uid)
  1222. {
  1223. if (!Resolve(uid, ref uid.Comp))
  1224. return _defaultStorageMaxItemSize;
  1225. // If we specify a max item size, use that
  1226. if (uid.Comp.MaxItemSize != null)
  1227. {
  1228. if (_prototype.TryIndex(uid.Comp.MaxItemSize.Value, out var proto))
  1229. return proto;
  1230. Log.Error($"{ToPrettyString(uid.Owner)} tried to get invalid item size prototype: {uid.Comp.MaxItemSize.Value}. Stack trace:\\n{Environment.StackTrace}");
  1231. }
  1232. if (!_itemQuery.TryGetComponent(uid, out var item))
  1233. return _defaultStorageMaxItemSize;
  1234. // if there is no max item size specified, the value used
  1235. // is one below the item size of the storage entity.
  1236. return _nextSmallest[item.Size];
  1237. }
  1238. /// <summary>
  1239. /// Checks if a storage's UI is open by anyone when locked, and closes it.
  1240. /// </summary>
  1241. private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockToggledEvent args)
  1242. {
  1243. if (!args.Locked)
  1244. return;
  1245. // Gets everyone looking at the UI
  1246. foreach (var actor in UI.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList())
  1247. {
  1248. if (!CanInteract(actor, (uid, component)))
  1249. UI.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor);
  1250. }
  1251. }
  1252. private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args)
  1253. {
  1254. if (ContainerSystem.TryGetContainingContainer((uid, null, component), out var container) &&
  1255. container.ID == StorageComponent.ContainerId)
  1256. {
  1257. UpdateAppearance(container.Owner);
  1258. UpdateUI(container.Owner);
  1259. }
  1260. }
  1261. private void HandleOpenBackpack(ICommonSession? session)
  1262. {
  1263. HandleToggleSlotUI(session, "back");
  1264. }
  1265. private void HandleOpenBelt(ICommonSession? session)
  1266. {
  1267. HandleToggleSlotUI(session, "belt");
  1268. }
  1269. private void HandleToggleSlotUI(ICommonSession? session, string slot)
  1270. {
  1271. if (session is not { } playerSession)
  1272. return;
  1273. if (playerSession.AttachedEntity is not { Valid: true } playerEnt || !Exists(playerEnt))
  1274. return;
  1275. if (!_inventory.TryGetSlotEntity(playerEnt, slot, out var storageEnt))
  1276. return;
  1277. if (!ActionBlocker.CanInteract(playerEnt, storageEnt))
  1278. return;
  1279. if (!UI.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt))
  1280. {
  1281. OpenStorageUI(storageEnt.Value, playerEnt, silent: false);
  1282. }
  1283. else
  1284. {
  1285. UI.CloseUi(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt);
  1286. }
  1287. }
  1288. protected void ClearCantFillReasons()
  1289. {
  1290. #if DEBUG
  1291. CantFillReasons.Clear();
  1292. #endif
  1293. }
  1294. private bool CanInteract(EntityUid user, Entity<StorageComponent> storage, bool canInteract = true, bool silent = true)
  1295. {
  1296. if (HasComp<BypassInteractionChecksComponent>(user))
  1297. return true;
  1298. if (!canInteract)
  1299. return false;
  1300. var ev = new StorageInteractAttemptEvent(silent);
  1301. RaiseLocalEvent(storage, ref ev);
  1302. return !ev.Cancelled;
  1303. }
  1304. /// <summary>
  1305. /// Plays a clientside pickup animation for the specified uid.
  1306. /// </summary>
  1307. public abstract void PlayPickupAnimation(EntityUid uid, EntityCoordinates initialCoordinates,
  1308. EntityCoordinates finalCoordinates, Angle initialRotation, EntityUid? user = null);
  1309. private bool ValidateInput(
  1310. EntitySessionEventArgs args,
  1311. NetEntity netStorage,
  1312. out Entity<HandsComponent> player,
  1313. out Entity<StorageComponent> storage)
  1314. {
  1315. player = default;
  1316. storage = default;
  1317. if (args.SenderSession.AttachedEntity is not { } playerUid)
  1318. return false;
  1319. if (!TryComp(playerUid, out HandsComponent? hands) || hands.Count == 0)
  1320. return false;
  1321. if (!TryGetEntity(netStorage, out var storageUid))
  1322. return false;
  1323. if (!TryComp(storageUid, out StorageComponent? storageComp))
  1324. return false;
  1325. // TODO STORAGE use BUI events
  1326. // This would automatically validate that the UI is open & that the user can interact.
  1327. // However, we still need to manually validate that items being used are in the users hands or in the storage.
  1328. if (!UI.IsUiOpen(storageUid.Value, StorageComponent.StorageUiKey.Key, playerUid))
  1329. return false;
  1330. if (!ActionBlocker.CanInteract(playerUid, storageUid))
  1331. return false;
  1332. player = new(playerUid, hands);
  1333. storage = new(storageUid.Value, storageComp);
  1334. return true;
  1335. }
  1336. private bool ValidateInput(EntitySessionEventArgs args,
  1337. NetEntity netStorage,
  1338. NetEntity netItem,
  1339. out Entity<HandsComponent> player,
  1340. out Entity<StorageComponent> storage,
  1341. out Entity<ItemComponent> item,
  1342. bool held = false)
  1343. {
  1344. item = default!;
  1345. if (!ValidateInput(args, netStorage, out player, out storage))
  1346. return false;
  1347. if (!TryGetEntity(netItem, out var itemUid))
  1348. return false;
  1349. if (held)
  1350. {
  1351. if (!_sharedHandsSystem.IsHolding(player, itemUid, out _))
  1352. return false;
  1353. }
  1354. else
  1355. {
  1356. if (!storage.Comp.Container.Contains(itemUid.Value))
  1357. return false;
  1358. DebugTools.Assert(storage.Comp.StoredItems.ContainsKey(itemUid.Value));
  1359. }
  1360. if (!TryComp(itemUid, out ItemComponent? itemComp))
  1361. return false;
  1362. if (!ActionBlocker.CanInteract(player, itemUid))
  1363. return false;
  1364. item = new(itemUid.Value, itemComp);
  1365. return true;
  1366. }
  1367. [Serializable, NetSerializable]
  1368. protected sealed class StorageComponentState : ComponentState
  1369. {
  1370. public Dictionary<NetEntity, ItemStorageLocation> StoredItems = new();
  1371. public Dictionary<string, List<ItemStorageLocation>> SavedLocations = new();
  1372. public List<Box2i> Grid = new();
  1373. public ProtoId<ItemSizePrototype>? MaxItemSize;
  1374. public EntityWhitelist? Whitelist;
  1375. public EntityWhitelist? Blacklist;
  1376. }
  1377. }