1
0

SharedStackSystem.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. using System.Numerics;
  2. using Content.Shared.Examine;
  3. using Content.Shared.Hands.Components;
  4. using Content.Shared.Hands.EntitySystems;
  5. using Content.Shared.Interaction;
  6. using Content.Shared.Popups;
  7. using Content.Shared.Storage.EntitySystems;
  8. using JetBrains.Annotations;
  9. using Robust.Shared.GameStates;
  10. using Robust.Shared.Physics.Systems;
  11. using Robust.Shared.Player;
  12. using Robust.Shared.Prototypes;
  13. using Robust.Shared.Timing;
  14. namespace Content.Shared.Stacks
  15. {
  16. [UsedImplicitly]
  17. public abstract class SharedStackSystem : EntitySystem
  18. {
  19. [Dependency] private readonly IGameTiming _gameTiming = default!;
  20. [Dependency] private readonly IPrototypeManager _prototype = default!;
  21. [Dependency] private readonly IViewVariablesManager _vvm = default!;
  22. [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
  23. [Dependency] protected readonly SharedHandsSystem Hands = default!;
  24. [Dependency] protected readonly SharedTransformSystem Xform = default!;
  25. [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
  26. [Dependency] private readonly SharedPhysicsSystem _physics = default!;
  27. [Dependency] protected readonly SharedPopupSystem Popup = default!;
  28. [Dependency] private readonly SharedStorageSystem _storage = default!;
  29. public override void Initialize()
  30. {
  31. base.Initialize();
  32. SubscribeLocalEvent<StackComponent, ComponentGetState>(OnStackGetState);
  33. SubscribeLocalEvent<StackComponent, ComponentHandleState>(OnStackHandleState);
  34. SubscribeLocalEvent<StackComponent, ComponentStartup>(OnStackStarted);
  35. SubscribeLocalEvent<StackComponent, ExaminedEvent>(OnStackExamined);
  36. SubscribeLocalEvent<StackComponent, InteractUsingEvent>(OnStackInteractUsing);
  37. _vvm.GetTypeHandler<StackComponent>()
  38. .AddPath(nameof(StackComponent.Count), (_, comp) => comp.Count, SetCount);
  39. }
  40. public override void Shutdown()
  41. {
  42. base.Shutdown();
  43. _vvm.GetTypeHandler<StackComponent>()
  44. .RemovePath(nameof(StackComponent.Count));
  45. }
  46. private void OnStackInteractUsing(EntityUid uid, StackComponent stack, InteractUsingEvent args)
  47. {
  48. if (args.Handled)
  49. return;
  50. if (!TryComp(args.Used, out StackComponent? recipientStack))
  51. return;
  52. var localRotation = Transform(args.Used).LocalRotation;
  53. if (!TryMergeStacks(uid, args.Used, out var transfered, stack, recipientStack))
  54. return;
  55. args.Handled = true;
  56. // interaction is done, the rest is just generating a pop-up
  57. if (!_gameTiming.IsFirstTimePredicted)
  58. return;
  59. var popupPos = args.ClickLocation;
  60. var userCoords = Transform(args.User).Coordinates;
  61. if (!popupPos.IsValid(EntityManager))
  62. {
  63. popupPos = userCoords;
  64. }
  65. switch (transfered)
  66. {
  67. case > 0:
  68. Popup.PopupCoordinates($"+{transfered}", popupPos, Filter.Local(), false);
  69. if (GetAvailableSpace(recipientStack) == 0)
  70. {
  71. Popup.PopupCoordinates(Loc.GetString("comp-stack-becomes-full"),
  72. popupPos.Offset(new Vector2(0, -0.5f)), Filter.Local(), false);
  73. }
  74. break;
  75. case 0 when GetAvailableSpace(recipientStack) == 0:
  76. Popup.PopupCoordinates(Loc.GetString("comp-stack-already-full"), popupPos, Filter.Local(), false);
  77. break;
  78. }
  79. _storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
  80. }
  81. private bool TryMergeStacks(
  82. EntityUid donor,
  83. EntityUid recipient,
  84. out int transferred,
  85. StackComponent? donorStack = null,
  86. StackComponent? recipientStack = null)
  87. {
  88. transferred = 0;
  89. if (donor == recipient)
  90. return false;
  91. if (!Resolve(recipient, ref recipientStack, false) || !Resolve(donor, ref donorStack, false))
  92. return false;
  93. if (string.IsNullOrEmpty(recipientStack.StackTypeId) || !recipientStack.StackTypeId.Equals(donorStack.StackTypeId))
  94. return false;
  95. transferred = Math.Min(donorStack.Count, GetAvailableSpace(recipientStack));
  96. SetCount(donor, donorStack.Count - transferred, donorStack);
  97. SetCount(recipient, recipientStack.Count + transferred, recipientStack);
  98. return transferred > 0;
  99. }
  100. /// <summary>
  101. /// If the given item is a stack, this attempts to find a matching stack in the users hand, and merge with that.
  102. /// </summary>
  103. /// <remarks>
  104. /// If the interaction fails to fully merge the stack, or if this is just not a stack, it will instead try
  105. /// to place it in the user's hand normally.
  106. /// </remarks>
  107. public void TryMergeToHands(
  108. EntityUid item,
  109. EntityUid user,
  110. StackComponent? itemStack = null,
  111. HandsComponent? hands = null)
  112. {
  113. if (!Resolve(user, ref hands, false))
  114. return;
  115. if (!Resolve(item, ref itemStack, false))
  116. {
  117. // This isn't even a stack. Just try to pickup as normal.
  118. Hands.PickupOrDrop(user, item, handsComp: hands);
  119. return;
  120. }
  121. // This is shit code until hands get fixed and give an easy way to enumerate over items, starting with the currently active item.
  122. foreach (var held in Hands.EnumerateHeld(user, hands))
  123. {
  124. TryMergeStacks(item, held, out _, donorStack: itemStack);
  125. if (itemStack.Count == 0)
  126. return;
  127. }
  128. Hands.PickupOrDrop(user, item, handsComp: hands);
  129. }
  130. public virtual void SetCount(EntityUid uid, int amount, StackComponent? component = null)
  131. {
  132. if (!Resolve(uid, ref component))
  133. return;
  134. // Do nothing if amount is already the same.
  135. if (amount == component.Count)
  136. return;
  137. // Store old value for event-raising purposes...
  138. var old = component.Count;
  139. // Clamp the value.
  140. amount = Math.Min(amount, GetMaxCount(component));
  141. amount = Math.Max(amount, 0);
  142. // Server-side override deletes the entity if count == 0
  143. component.Count = amount;
  144. Dirty(uid, component);
  145. Appearance.SetData(uid, StackVisuals.Actual, component.Count);
  146. RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count));
  147. }
  148. /// <summary>
  149. /// Try to use an amount of items on this stack. Returns whether this succeeded.
  150. /// </summary>
  151. public bool Use(EntityUid uid, int amount, StackComponent? stack = null)
  152. {
  153. if (!Resolve(uid, ref stack))
  154. return false;
  155. // Check if we have enough things in the stack for this...
  156. if (stack.Count < amount)
  157. {
  158. // Not enough things in the stack, return false.
  159. return false;
  160. }
  161. // We do have enough things in the stack, so remove them and change.
  162. if (!stack.Unlimited)
  163. {
  164. SetCount(uid, stack.Count - amount, stack);
  165. }
  166. return true;
  167. }
  168. /// <summary>
  169. /// Tries to merge a stack into any of the stacks it is touching.
  170. /// </summary>
  171. /// <returns>Whether or not it was successfully merged into another stack</returns>
  172. public bool TryMergeToContacts(EntityUid uid, StackComponent? stack = null, TransformComponent? xform = null)
  173. {
  174. if (!Resolve(uid, ref stack, ref xform, false))
  175. return false;
  176. var map = xform.MapID;
  177. var bounds = _physics.GetWorldAABB(uid);
  178. var intersecting = new HashSet<Entity<StackComponent>>();
  179. _entityLookup.GetEntitiesIntersecting(map, bounds, intersecting, LookupFlags.Dynamic | LookupFlags.Sundries);
  180. var merged = false;
  181. foreach (var otherStack in intersecting)
  182. {
  183. var otherEnt = otherStack.Owner;
  184. // if you merge a ton of stacks together, you will end up deleting a few by accident.
  185. if (TerminatingOrDeleted(otherEnt) || EntityManager.IsQueuedForDeletion(otherEnt))
  186. continue;
  187. if (!TryMergeStacks(uid, otherEnt, out _, stack, otherStack))
  188. continue;
  189. merged = true;
  190. if (stack.Count <= 0)
  191. break;
  192. }
  193. return merged;
  194. }
  195. /// <summary>
  196. /// Gets the amount of items in a stack. If it cannot be stacked, returns 1.
  197. /// </summary>
  198. /// <param name="uid"></param>
  199. /// <param name="component"></param>
  200. /// <returns></returns>
  201. public int GetCount(EntityUid uid, StackComponent? component = null)
  202. {
  203. return Resolve(uid, ref component, false) ? component.Count : 1;
  204. }
  205. /// <summary>
  206. /// Gets the max count for a given entity prototype
  207. /// </summary>
  208. /// <param name="entityId"></param>
  209. /// <returns></returns>
  210. [PublicAPI]
  211. public int GetMaxCount(string entityId)
  212. {
  213. var entProto = _prototype.Index<EntityPrototype>(entityId);
  214. entProto.TryGetComponent<StackComponent>(out var stackComp, EntityManager.ComponentFactory);
  215. return GetMaxCount(stackComp);
  216. }
  217. /// <summary>
  218. /// Gets the max count for a given entity
  219. /// </summary>
  220. /// <param name="uid"></param>
  221. /// <returns></returns>
  222. [PublicAPI]
  223. public int GetMaxCount(EntityUid uid)
  224. {
  225. return GetMaxCount(CompOrNull<StackComponent>(uid));
  226. }
  227. /// <summary>
  228. /// Gets the maximum amount that can be fit on a stack.
  229. /// </summary>
  230. /// <remarks>
  231. /// <p>
  232. /// if there's no stackcomp, this equals 1. Otherwise, if there's a max
  233. /// count override, it equals that. It then checks for a max count value
  234. /// on the prototype. If there isn't one, it defaults to the max integer
  235. /// value (unlimimted).
  236. /// </p>
  237. /// </remarks>
  238. /// <param name="component"></param>
  239. /// <returns></returns>
  240. public int GetMaxCount(StackComponent? component)
  241. {
  242. if (component == null)
  243. return 1;
  244. if (component.MaxCountOverride != null)
  245. return component.MaxCountOverride.Value;
  246. if (string.IsNullOrEmpty(component.StackTypeId))
  247. return 1;
  248. var stackProto = _prototype.Index<StackPrototype>(component.StackTypeId);
  249. return stackProto.MaxCount ?? int.MaxValue;
  250. }
  251. /// <summary>
  252. /// Gets the remaining space in a stack.
  253. /// </summary>
  254. /// <param name="component"></param>
  255. /// <returns></returns>
  256. [PublicAPI]
  257. public int GetAvailableSpace(StackComponent component)
  258. {
  259. return GetMaxCount(component) - component.Count;
  260. }
  261. /// <summary>
  262. /// Tries to add one stack to another. May have some leftover count in the inserted entity.
  263. /// </summary>
  264. public bool TryAdd(EntityUid insertEnt, EntityUid targetEnt, StackComponent? insertStack = null, StackComponent? targetStack = null)
  265. {
  266. if (!Resolve(insertEnt, ref insertStack) || !Resolve(targetEnt, ref targetStack))
  267. return false;
  268. var count = insertStack.Count;
  269. return TryAdd(insertEnt, targetEnt, count, insertStack, targetStack);
  270. }
  271. /// <summary>
  272. /// Tries to add one stack to another. May have some leftover count in the inserted entity.
  273. /// </summary>
  274. public bool TryAdd(EntityUid insertEnt, EntityUid targetEnt, int count, StackComponent? insertStack = null, StackComponent? targetStack = null)
  275. {
  276. if (!Resolve(insertEnt, ref insertStack) || !Resolve(targetEnt, ref targetStack))
  277. return false;
  278. if (insertStack.StackTypeId != targetStack.StackTypeId)
  279. return false;
  280. var available = GetAvailableSpace(targetStack);
  281. if (available <= 0)
  282. return false;
  283. var change = Math.Min(available, count);
  284. SetCount(targetEnt, targetStack.Count + change, targetStack);
  285. SetCount(insertEnt, insertStack.Count - change, insertStack);
  286. return true;
  287. }
  288. private void OnStackStarted(EntityUid uid, StackComponent component, ComponentStartup args)
  289. {
  290. // on client, lingering stacks that start at 0 need to be darkened
  291. // on server this does nothing
  292. SetCount(uid, component.Count, component);
  293. if (!TryComp(uid, out AppearanceComponent? appearance))
  294. return;
  295. Appearance.SetData(uid, StackVisuals.Actual, component.Count, appearance);
  296. Appearance.SetData(uid, StackVisuals.MaxCount, GetMaxCount(component), appearance);
  297. Appearance.SetData(uid, StackVisuals.Hide, false, appearance);
  298. }
  299. private void OnStackGetState(EntityUid uid, StackComponent component, ref ComponentGetState args)
  300. {
  301. args.State = new StackComponentState(component.Count, component.MaxCountOverride, component.Lingering);
  302. }
  303. private void OnStackHandleState(EntityUid uid, StackComponent component, ref ComponentHandleState args)
  304. {
  305. if (args.Current is not StackComponentState cast)
  306. return;
  307. component.MaxCountOverride = cast.MaxCount;
  308. component.Lingering = cast.Lingering;
  309. // This will change the count and call events.
  310. SetCount(uid, cast.Count, component);
  311. }
  312. private void OnStackExamined(EntityUid uid, StackComponent component, ExaminedEvent args)
  313. {
  314. if (!args.IsInDetailsRange)
  315. return;
  316. args.PushMarkup(
  317. Loc.GetString("comp-stack-examine-detail-count",
  318. ("count", component.Count),
  319. ("markupCountColor", "lightgray")
  320. )
  321. );
  322. }
  323. }
  324. /// <summary>
  325. /// Event raised when a stack's count has changed.
  326. /// </summary>
  327. public sealed class StackCountChangedEvent : EntityEventArgs
  328. {
  329. /// <summary>
  330. /// The old stack count.
  331. /// </summary>
  332. public int OldCount;
  333. /// <summary>
  334. /// The new stack count.
  335. /// </summary>
  336. public int NewCount;
  337. public StackCountChangedEvent(int oldCount, int newCount)
  338. {
  339. OldCount = oldCount;
  340. NewCount = newCount;
  341. }
  342. }
  343. }