1
0

SharedVendingMachineSystem.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. using Content.Shared.Emag.Components;
  2. using Robust.Shared.Prototypes;
  3. using System.Linq;
  4. using Content.Shared.Access.Components;
  5. using Content.Shared.Access.Systems;
  6. using Content.Shared.Advertise.Components;
  7. using Content.Shared.Advertise.Systems;
  8. using Content.Shared.DoAfter;
  9. using Content.Shared.Emag.Systems;
  10. using Content.Shared.Interaction;
  11. using Content.Shared.Popups;
  12. using Content.Shared.Power.EntitySystems;
  13. using Robust.Shared.Audio;
  14. using Robust.Shared.Audio.Systems;
  15. using Robust.Shared.GameStates;
  16. using Robust.Shared.Network;
  17. using Robust.Shared.Random;
  18. using Robust.Shared.Timing;
  19. namespace Content.Shared.VendingMachines;
  20. public abstract partial class SharedVendingMachineSystem : EntitySystem
  21. {
  22. [Dependency] protected readonly IGameTiming Timing = default!;
  23. [Dependency] private readonly INetManager _net = default!;
  24. [Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
  25. [Dependency] private readonly AccessReaderSystem _accessReader = default!;
  26. [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
  27. [Dependency] protected readonly SharedAudioSystem Audio = default!;
  28. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  29. [Dependency] protected readonly SharedPointLightSystem Light = default!;
  30. [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
  31. [Dependency] protected readonly SharedPopupSystem Popup = default!;
  32. [Dependency] private readonly SharedSpeakOnUIClosedSystem _speakOn = default!;
  33. [Dependency] protected readonly SharedUserInterfaceSystem UISystem = default!;
  34. [Dependency] protected readonly IRobustRandom Randomizer = default!;
  35. [Dependency] private readonly EmagSystem _emag = default!;
  36. public override void Initialize()
  37. {
  38. base.Initialize();
  39. SubscribeLocalEvent<VendingMachineComponent, ComponentGetState>(OnVendingGetState);
  40. SubscribeLocalEvent<VendingMachineComponent, MapInitEvent>(OnMapInit);
  41. SubscribeLocalEvent<VendingMachineComponent, GotEmaggedEvent>(OnEmagged);
  42. SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
  43. Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
  44. {
  45. subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
  46. });
  47. }
  48. private void OnVendingGetState(Entity<VendingMachineComponent> entity, ref ComponentGetState args)
  49. {
  50. var component = entity.Comp;
  51. var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
  52. var emaggedInventory = new Dictionary<string, VendingMachineInventoryEntry>();
  53. var contrabandInventory = new Dictionary<string, VendingMachineInventoryEntry>();
  54. foreach (var weh in component.Inventory)
  55. {
  56. inventory[weh.Key] = new(weh.Value);
  57. }
  58. foreach (var weh in component.EmaggedInventory)
  59. {
  60. emaggedInventory[weh.Key] = new(weh.Value);
  61. }
  62. foreach (var weh in component.ContrabandInventory)
  63. {
  64. contrabandInventory[weh.Key] = new(weh.Value);
  65. }
  66. args.State = new VendingMachineComponentState()
  67. {
  68. Inventory = inventory,
  69. EmaggedInventory = emaggedInventory,
  70. ContrabandInventory = contrabandInventory,
  71. Contraband = component.Contraband,
  72. EjectEnd = component.EjectEnd,
  73. DenyEnd = component.DenyEnd,
  74. DispenseOnHitEnd = component.DispenseOnHitEnd,
  75. };
  76. }
  77. public override void Update(float frameTime)
  78. {
  79. base.Update(frameTime);
  80. var query = EntityQueryEnumerator<VendingMachineComponent>();
  81. var curTime = Timing.CurTime;
  82. while (query.MoveNext(out var uid, out var comp))
  83. {
  84. if (comp.Ejecting)
  85. {
  86. if (curTime > comp.EjectEnd)
  87. {
  88. comp.EjectEnd = null;
  89. Dirty(uid, comp);
  90. EjectItem(uid, comp);
  91. UpdateUI((uid, comp));
  92. }
  93. }
  94. if (comp.Denying)
  95. {
  96. if (curTime > comp.DenyEnd)
  97. {
  98. comp.DenyEnd = null;
  99. Dirty(uid, comp);
  100. TryUpdateVisualState((uid, comp));
  101. }
  102. }
  103. if (comp.DispenseOnHitCoolingDown)
  104. {
  105. if (curTime > comp.DispenseOnHitEnd)
  106. {
  107. comp.DispenseOnHitEnd = null;
  108. Dirty(uid, comp);
  109. }
  110. }
  111. }
  112. }
  113. private void OnInventoryEjectMessage(Entity<VendingMachineComponent> entity, ref VendingMachineEjectMessage args)
  114. {
  115. if (!_receiver.IsPowered(entity.Owner) || Deleted(entity))
  116. return;
  117. if (args.Actor is not { Valid: true } actor)
  118. return;
  119. AuthorizedVend(entity.Owner, actor, args.Type, args.ID, entity.Comp);
  120. }
  121. protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args)
  122. {
  123. RestockInventoryFromPrototype(uid, component, component.InitialStockQuality);
  124. }
  125. protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { }
  126. /// <summary>
  127. /// Checks if the user is authorized to use this vending machine
  128. /// </summary>
  129. /// <param name="uid"></param>
  130. /// <param name="sender">Entity trying to use the vending machine</param>
  131. /// <param name="vendComponent"></param>
  132. public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
  133. {
  134. if (!Resolve(uid, ref vendComponent))
  135. return false;
  136. if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
  137. return true;
  138. if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp<EmaggedComponent>(uid))
  139. return true;
  140. Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid, sender);
  141. Deny((uid, vendComponent), sender);
  142. return false;
  143. }
  144. protected VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
  145. {
  146. if (!Resolve(uid, ref component))
  147. return null;
  148. if (type == InventoryType.Emagged && HasComp<EmaggedComponent>(uid))
  149. return component.EmaggedInventory.GetValueOrDefault(entryId);
  150. if (type == InventoryType.Contraband && component.Contraband)
  151. return component.ContrabandInventory.GetValueOrDefault(entryId);
  152. return component.Inventory.GetValueOrDefault(entryId);
  153. }
  154. /// <summary>
  155. /// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
  156. /// or the item doesn't exist in its inventory.
  157. /// </summary>
  158. /// <param name="uid"></param>
  159. /// <param name="type">The type of inventory the item is from</param>
  160. /// <param name="itemId">The prototype ID of the item</param>
  161. /// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
  162. /// <param name="vendComponent"></param>
  163. public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null)
  164. {
  165. if (!Resolve(uid, ref vendComponent))
  166. return;
  167. if (vendComponent.Ejecting || vendComponent.Broken || !_receiver.IsPowered(uid))
  168. {
  169. return;
  170. }
  171. var entry = GetEntry(uid, itemId, type, vendComponent);
  172. if (string.IsNullOrEmpty(entry?.ID))
  173. {
  174. Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
  175. Deny((uid, vendComponent));
  176. return;
  177. }
  178. if (entry.Amount <= 0)
  179. {
  180. Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
  181. Deny((uid, vendComponent));
  182. return;
  183. }
  184. // Start Ejecting, and prevent users from ordering while anim playing
  185. vendComponent.EjectEnd = Timing.CurTime + vendComponent.EjectDelay;
  186. vendComponent.NextItemToEject = entry.ID;
  187. vendComponent.ThrowNextItem = throwItem;
  188. if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
  189. _speakOn.TrySetFlag((uid, speakComponent));
  190. entry.Amount--;
  191. Dirty(uid, vendComponent);
  192. UpdateUI((uid, vendComponent));
  193. TryUpdateVisualState((uid, vendComponent));
  194. Audio.PlayPredicted(vendComponent.SoundVend, uid, user);
  195. }
  196. public void Deny(Entity<VendingMachineComponent?> entity, EntityUid? user = null)
  197. {
  198. if (!Resolve(entity.Owner, ref entity.Comp))
  199. return;
  200. if (entity.Comp.Denying)
  201. return;
  202. entity.Comp.DenyEnd = Timing.CurTime + entity.Comp.DenyDelay;
  203. Audio.PlayPredicted(entity.Comp.SoundDeny, entity.Owner, user, AudioParams.Default.WithVolume(-2f));
  204. TryUpdateVisualState(entity);
  205. Dirty(entity);
  206. }
  207. protected virtual void UpdateUI(Entity<VendingMachineComponent?> entity) { }
  208. /// <summary>
  209. /// Tries to update the visuals of the component based on its current state.
  210. /// </summary>
  211. public void TryUpdateVisualState(Entity<VendingMachineComponent?> entity)
  212. {
  213. if (!Resolve(entity.Owner, ref entity.Comp))
  214. return;
  215. var finalState = VendingMachineVisualState.Normal;
  216. if (entity.Comp.Broken)
  217. {
  218. finalState = VendingMachineVisualState.Broken;
  219. }
  220. else if (entity.Comp.Ejecting)
  221. {
  222. finalState = VendingMachineVisualState.Eject;
  223. }
  224. else if (entity.Comp.Denying)
  225. {
  226. finalState = VendingMachineVisualState.Deny;
  227. }
  228. else if (!_receiver.IsPowered(entity.Owner))
  229. {
  230. finalState = VendingMachineVisualState.Off;
  231. }
  232. // TODO: You know this should really live on the client with netsync off because client knows the state.
  233. if (Light.TryGetLight(entity.Owner, out var pointlight))
  234. {
  235. var lightEnabled = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
  236. Light.SetEnabled(entity.Owner, lightEnabled, pointlight);
  237. }
  238. _appearanceSystem.SetData(entity.Owner, VendingMachineVisuals.VisualState, finalState);
  239. }
  240. /// <summary>
  241. /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
  242. /// </summary>
  243. /// <param name="uid"></param>
  244. /// <param name="sender">Entity that is trying to use the vending machine</param>
  245. /// <param name="type">The type of inventory the item is from</param>
  246. /// <param name="itemId">The prototype ID of the item</param>
  247. /// <param name="component"></param>
  248. public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
  249. {
  250. if (IsAuthorized(uid, sender, component))
  251. {
  252. TryEjectVendorItem(uid, type, itemId, component.CanShoot, sender, component);
  253. }
  254. }
  255. public void RestockInventoryFromPrototype(EntityUid uid,
  256. VendingMachineComponent? component = null, float restockQuality = 1f)
  257. {
  258. if (!Resolve(uid, ref component))
  259. {
  260. return;
  261. }
  262. if (!PrototypeManager.TryIndex(component.PackPrototypeId, out VendingMachineInventoryPrototype? packPrototype))
  263. return;
  264. AddInventoryFromPrototype(uid, packPrototype.StartingInventory, InventoryType.Regular, component, restockQuality);
  265. AddInventoryFromPrototype(uid, packPrototype.EmaggedInventory, InventoryType.Emagged, component, restockQuality);
  266. AddInventoryFromPrototype(uid, packPrototype.ContrabandInventory, InventoryType.Contraband, component, restockQuality);
  267. Dirty(uid, component);
  268. }
  269. private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args)
  270. {
  271. if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
  272. return;
  273. if (_emag.CheckFlag(uid, EmagType.Interaction))
  274. return;
  275. // only emag if there are emag-only items
  276. args.Handled = component.EmaggedInventory.Count > 0;
  277. }
  278. /// <summary>
  279. /// Returns all of the vending machine's inventory. Only includes emagged and contraband inventories if
  280. /// <see cref="EmaggedComponent"/> with the EmagType.Interaction flag exists and <see cref="VendingMachineComponent.Contraband"/> is true
  281. /// are <c>true</c> respectively.
  282. /// </summary>
  283. /// <param name="uid"></param>
  284. /// <param name="component"></param>
  285. /// <returns></returns>
  286. public List<VendingMachineInventoryEntry> GetAllInventory(EntityUid uid, VendingMachineComponent? component = null)
  287. {
  288. if (!Resolve(uid, ref component))
  289. return new();
  290. var inventory = new List<VendingMachineInventoryEntry>(component.Inventory.Values);
  291. if (_emag.CheckFlag(uid, EmagType.Interaction))
  292. inventory.AddRange(component.EmaggedInventory.Values);
  293. if (component.Contraband)
  294. inventory.AddRange(component.ContrabandInventory.Values);
  295. return inventory;
  296. }
  297. public List<VendingMachineInventoryEntry> GetAvailableInventory(EntityUid uid, VendingMachineComponent? component = null)
  298. {
  299. if (!Resolve(uid, ref component))
  300. return new();
  301. return GetAllInventory(uid, component).Where(_ => _.Amount > 0).ToList();
  302. }
  303. private void AddInventoryFromPrototype(EntityUid uid, Dictionary<string, uint>? entries,
  304. InventoryType type,
  305. VendingMachineComponent? component = null, float restockQuality = 1.0f)
  306. {
  307. if (!Resolve(uid, ref component) || entries == null)
  308. {
  309. return;
  310. }
  311. Dictionary<string, VendingMachineInventoryEntry> inventory;
  312. switch (type)
  313. {
  314. case InventoryType.Regular:
  315. inventory = component.Inventory;
  316. break;
  317. case InventoryType.Emagged:
  318. inventory = component.EmaggedInventory;
  319. break;
  320. case InventoryType.Contraband:
  321. inventory = component.ContrabandInventory;
  322. break;
  323. default:
  324. return;
  325. }
  326. foreach (var (id, amount) in entries)
  327. {
  328. if (PrototypeManager.HasIndex<EntityPrototype>(id))
  329. {
  330. var restock = amount;
  331. var chanceOfMissingStock = 1 - restockQuality;
  332. var result = Randomizer.NextFloat(0, 1);
  333. if (result < chanceOfMissingStock)
  334. {
  335. restock = (uint) Math.Floor(amount * result / chanceOfMissingStock);
  336. }
  337. if (inventory.TryGetValue(id, out var entry))
  338. // Prevent a machine's stock from going over three times
  339. // the prototype's normal amount. This is an arbitrary
  340. // number and meant to be a convenience for someone
  341. // restocking a machine who doesn't want to force vend out
  342. // all the items just to restock one empty slot without
  343. // losing the rest of the restock.
  344. entry.Amount = Math.Min(entry.Amount + amount, 3 * restock);
  345. else
  346. inventory.Add(id, new VendingMachineInventoryEntry(type, id, restock));
  347. }
  348. }
  349. }
  350. }