StoreSystem.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. using Content.Server.Store.Components;
  2. using Content.Shared.UserInterface;
  3. using Content.Shared.FixedPoint;
  4. using Content.Shared.Implants.Components;
  5. using Content.Shared.Interaction;
  6. using Content.Shared.Popups;
  7. using Content.Shared.Stacks;
  8. using Content.Shared.Store.Components;
  9. using JetBrains.Annotations;
  10. using Robust.Shared.Prototypes;
  11. using Robust.Shared.Utility;
  12. using System.Linq;
  13. using Robust.Shared.Timing;
  14. using Content.Shared.Mind;
  15. namespace Content.Server.Store.Systems;
  16. /// <summary>
  17. /// Manages general interactions with a store and different entities,
  18. /// getting listings for stores, and interfacing with the store UI.
  19. /// </summary>
  20. public sealed partial class StoreSystem : EntitySystem
  21. {
  22. [Dependency] private readonly IPrototypeManager _proto = default!;
  23. [Dependency] private readonly SharedPopupSystem _popup = default!;
  24. [Dependency] private readonly IGameTiming _timing = default!;
  25. public override void Initialize()
  26. {
  27. base.Initialize();
  28. SubscribeLocalEvent<StoreComponent, ActivatableUIOpenAttemptEvent>(OnStoreOpenAttempt);
  29. SubscribeLocalEvent<CurrencyComponent, AfterInteractEvent>(OnAfterInteract);
  30. SubscribeLocalEvent<StoreComponent, BeforeActivatableUIOpenEvent>(BeforeActivatableUiOpen);
  31. SubscribeLocalEvent<StoreComponent, MapInitEvent>(OnMapInit);
  32. SubscribeLocalEvent<StoreComponent, ComponentStartup>(OnStartup);
  33. SubscribeLocalEvent<StoreComponent, ComponentShutdown>(OnShutdown);
  34. SubscribeLocalEvent<StoreComponent, OpenUplinkImplantEvent>(OnImplantActivate);
  35. InitializeUi();
  36. InitializeCommand();
  37. InitializeRefund();
  38. }
  39. private void OnMapInit(EntityUid uid, StoreComponent component, MapInitEvent args)
  40. {
  41. RefreshAllListings(component);
  42. component.StartingMap = Transform(uid).MapUid;
  43. }
  44. private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args)
  45. {
  46. // for traitors, because the StoreComponent for the PDA can be added at any time.
  47. if (MetaData(uid).EntityLifeStage == EntityLifeStage.MapInitialized)
  48. {
  49. RefreshAllListings(component);
  50. }
  51. var ev = new StoreAddedEvent();
  52. RaiseLocalEvent(uid, ref ev, true);
  53. }
  54. private void OnShutdown(EntityUid uid, StoreComponent component, ComponentShutdown args)
  55. {
  56. var ev = new StoreRemovedEvent();
  57. RaiseLocalEvent(uid, ref ev, true);
  58. }
  59. private void OnStoreOpenAttempt(EntityUid uid, StoreComponent component, ActivatableUIOpenAttemptEvent args)
  60. {
  61. if (!component.OwnerOnly)
  62. return;
  63. if (!_mind.TryGetMind(args.User, out var mind, out _))
  64. return;
  65. component.AccountOwner ??= mind;
  66. DebugTools.Assert(component.AccountOwner != null);
  67. if (component.AccountOwner == mind)
  68. return;
  69. _popup.PopupEntity(Loc.GetString("store-not-account-owner", ("store", uid)), uid, args.User);
  70. args.Cancel();
  71. }
  72. private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args)
  73. {
  74. if (args.Handled || !args.CanReach)
  75. return;
  76. if (!TryComp<StoreComponent>(args.Target, out var store))
  77. return;
  78. var ev = new CurrencyInsertAttemptEvent(args.User, args.Target.Value, args.Used, store);
  79. RaiseLocalEvent(args.Target.Value, ev);
  80. if (ev.Cancelled)
  81. return;
  82. if (!TryAddCurrency((uid, component), (args.Target.Value, store)))
  83. return;
  84. args.Handled = true;
  85. var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target));
  86. _popup.PopupEntity(msg, args.Target.Value, args.User);
  87. }
  88. private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args)
  89. {
  90. ToggleUi(args.Performer, uid, component);
  91. }
  92. /// <summary>
  93. /// Gets the value from an entity's currency component.
  94. /// Scales with stacks.
  95. /// </summary>
  96. /// <remarks>
  97. /// If this result is intended to be used with <see cref="TryAddCurrency(Robust.Shared.GameObjects.Entity{Content.Server.Store.Components.CurrencyComponent?},Robust.Shared.GameObjects.Entity{Content.Shared.Store.Components.StoreComponent?})"/>,
  98. /// consider using <see cref="TryAddCurrency(Robust.Shared.GameObjects.Entity{Content.Server.Store.Components.CurrencyComponent?},Robust.Shared.GameObjects.Entity{Content.Shared.Store.Components.StoreComponent?})"/> instead to ensure that the currency is consumed in the process.
  99. /// </remarks>
  100. /// <param name="uid"></param>
  101. /// <param name="component"></param>
  102. /// <returns>The value of the currency</returns>
  103. public Dictionary<string, FixedPoint2> GetCurrencyValue(EntityUid uid, CurrencyComponent component)
  104. {
  105. var amount = EntityManager.GetComponentOrNull<StackComponent>(uid)?.Count ?? 1;
  106. return component.Price.ToDictionary(v => v.Key, p => p.Value * amount);
  107. }
  108. /// <summary>
  109. /// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process.
  110. /// </summary>
  111. public bool TryAddCurrency(Entity<CurrencyComponent?> currency, Entity<StoreComponent?> store)
  112. {
  113. if (!Resolve(currency.Owner, ref currency.Comp))
  114. return false;
  115. if (!Resolve(store.Owner, ref store.Comp))
  116. return false;
  117. var value = currency.Comp.Price;
  118. if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1)
  119. {
  120. value = currency.Comp.Price
  121. .ToDictionary(v => v.Key, p => p.Value * stack.Count);
  122. }
  123. if (!TryAddCurrency(value, store, store.Comp))
  124. return false;
  125. // Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the
  126. // same tick
  127. currency.Comp.Price.Clear();
  128. if (stack != null)
  129. _stack.SetCount(currency.Owner, 0, stack);
  130. QueueDel(currency);
  131. return true;
  132. }
  133. /// <summary>
  134. /// Tries to add a currency to a store's balance
  135. /// </summary>
  136. /// <param name="currency">The value to add to the store</param>
  137. /// <param name="uid"></param>
  138. /// <param name="store">The store to add it to</param>
  139. /// <returns>Whether or not the currency was succesfully added</returns>
  140. public bool TryAddCurrency(Dictionary<string, FixedPoint2> currency, EntityUid uid, StoreComponent? store = null)
  141. {
  142. if (!Resolve(uid, ref store))
  143. return false;
  144. //verify these before values are modified
  145. foreach (var type in currency)
  146. {
  147. if (!store.CurrencyWhitelist.Contains(type.Key))
  148. return false;
  149. }
  150. foreach (var type in currency)
  151. {
  152. if (!store.Balance.TryAdd(type.Key, type.Value))
  153. store.Balance[type.Key] += type.Value;
  154. }
  155. UpdateUserInterface(null, uid, store);
  156. return true;
  157. }
  158. }
  159. public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs
  160. {
  161. public readonly EntityUid User;
  162. public readonly EntityUid Target;
  163. public readonly EntityUid Used;
  164. public readonly StoreComponent Store;
  165. public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid used, StoreComponent store)
  166. {
  167. User = user;
  168. Target = target;
  169. Used = used;
  170. Store = store;
  171. }
  172. }