CargoSystem.Orders.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. using System.Diagnostics.CodeAnalysis;
  2. using Content.Server.Cargo.Components;
  3. using Content.Server.Labels.Components;
  4. using Content.Server.Station.Components;
  5. using Content.Shared.Cargo;
  6. using Content.Shared.Cargo.BUI;
  7. using Content.Shared.Cargo.Components;
  8. using Content.Shared.Cargo.Events;
  9. using Content.Shared.Cargo.Prototypes;
  10. using Content.Shared.Database;
  11. using Content.Shared.Emag.Systems;
  12. using Content.Shared.IdentityManagement;
  13. using Content.Shared.Interaction;
  14. using Content.Shared.Paper;
  15. using Robust.Shared.Map;
  16. using Robust.Shared.Prototypes;
  17. using Robust.Shared.Utility;
  18. namespace Content.Server.Cargo.Systems
  19. {
  20. public sealed partial class CargoSystem
  21. {
  22. [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
  23. [Dependency] private readonly EmagSystem _emag = default!;
  24. /// <summary>
  25. /// How much time to wait (in seconds) before increasing bank accounts balance.
  26. /// </summary>
  27. private const int Delay = 10;
  28. /// <summary>
  29. /// Keeps track of how much time has elapsed since last balance increase.
  30. /// </summary>
  31. private float _timer;
  32. private void InitializeConsole()
  33. {
  34. SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleAddOrderMessage>(OnAddOrderMessage);
  35. SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleRemoveOrderMessage>(OnRemoveOrderMessage);
  36. SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleApproveOrderMessage>(OnApproveOrderMessage);
  37. SubscribeLocalEvent<CargoOrderConsoleComponent, BoundUIOpenedEvent>(OnOrderUIOpened);
  38. SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit);
  39. SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing);
  40. SubscribeLocalEvent<CargoOrderConsoleComponent, BankBalanceUpdatedEvent>(OnOrderBalanceUpdated);
  41. SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
  42. Reset();
  43. }
  44. private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
  45. {
  46. if (!HasComp<CashComponent>(args.Used))
  47. return;
  48. var price = _pricing.GetPrice(args.Used);
  49. if (price == 0)
  50. return;
  51. var stationUid = _station.GetOwningStation(args.Used);
  52. if (!TryComp(stationUid, out StationBankAccountComponent? bank))
  53. return;
  54. _audio.PlayPvs(component.ConfirmSound, uid);
  55. UpdateBankAccount((stationUid.Value, bank), (int) price);
  56. QueueDel(args.Used);
  57. args.Handled = true;
  58. }
  59. private void OnInit(EntityUid uid, CargoOrderConsoleComponent orderConsole, ComponentInit args)
  60. {
  61. var station = _station.GetOwningStation(uid);
  62. UpdateOrderState(uid, station);
  63. }
  64. private void Reset()
  65. {
  66. _timer = 0;
  67. }
  68. private void OnEmagged(Entity<CargoOrderConsoleComponent> ent, ref GotEmaggedEvent args)
  69. {
  70. if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
  71. return;
  72. if (_emag.CheckFlag(ent, EmagType.Interaction))
  73. return;
  74. args.Handled = true;
  75. }
  76. private void UpdateConsole(float frameTime)
  77. {
  78. _timer += frameTime;
  79. // TODO: Doesn't work with serialization and shouldn't just be updating every delay
  80. // client can just interp this just fine on its own.
  81. while (_timer > Delay)
  82. {
  83. _timer -= Delay;
  84. var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
  85. while (stationQuery.MoveNext(out var uid, out var bank))
  86. {
  87. var balanceToAdd = bank.IncreasePerSecond * Delay;
  88. UpdateBankAccount((uid, bank), balanceToAdd);
  89. }
  90. var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
  91. while (query.MoveNext(out var uid, out var _))
  92. {
  93. if (!_uiSystem.IsUiOpen(uid, CargoConsoleUiKey.Orders)) continue;
  94. var station = _station.GetOwningStation(uid);
  95. UpdateOrderState(uid, station);
  96. }
  97. }
  98. }
  99. #region Interface
  100. private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleApproveOrderMessage args)
  101. {
  102. if (args.Actor is not { Valid: true } player)
  103. return;
  104. if (!_accessReaderSystem.IsAllowed(player, uid))
  105. {
  106. ConsolePopup(args.Actor, Loc.GetString("cargo-console-order-not-allowed"));
  107. PlayDenySound(uid, component);
  108. return;
  109. }
  110. var station = _station.GetOwningStation(uid);
  111. // No station to deduct from.
  112. if (!TryComp(station, out StationBankAccountComponent? bank) ||
  113. !TryComp(station, out StationDataComponent? stationData) ||
  114. !TryGetOrderDatabase(station, out var orderDatabase))
  115. {
  116. ConsolePopup(args.Actor, Loc.GetString("cargo-console-station-not-found"));
  117. PlayDenySound(uid, component);
  118. return;
  119. }
  120. // Find our order again. It might have been dispatched or approved already
  121. var order = orderDatabase.Orders.Find(order => args.OrderId == order.OrderId && !order.Approved);
  122. if (order == null)
  123. {
  124. return;
  125. }
  126. // Invalid order
  127. if (!_protoMan.HasIndex<EntityPrototype>(order.ProductId))
  128. {
  129. ConsolePopup(args.Actor, Loc.GetString("cargo-console-invalid-product"));
  130. PlayDenySound(uid, component);
  131. return;
  132. }
  133. var amount = GetOutstandingOrderCount(orderDatabase);
  134. var capacity = orderDatabase.Capacity;
  135. // Too many orders, avoid them getting spammed in the UI.
  136. if (amount >= capacity)
  137. {
  138. ConsolePopup(args.Actor, Loc.GetString("cargo-console-too-many"));
  139. PlayDenySound(uid, component);
  140. return;
  141. }
  142. // Cap orders so someone can't spam thousands.
  143. var cappedAmount = Math.Min(capacity - amount, order.OrderQuantity);
  144. if (cappedAmount != order.OrderQuantity)
  145. {
  146. order.OrderQuantity = cappedAmount;
  147. ConsolePopup(args.Actor, Loc.GetString("cargo-console-snip-snip"));
  148. PlayDenySound(uid, component);
  149. }
  150. var cost = order.Price * order.OrderQuantity;
  151. // Not enough balance
  152. if (cost > bank.Balance)
  153. {
  154. ConsolePopup(args.Actor, Loc.GetString("cargo-console-insufficient-funds", ("cost", cost)));
  155. PlayDenySound(uid, component);
  156. return;
  157. }
  158. var ev = new FulfillCargoOrderEvent((station.Value, stationData), order, (uid, component));
  159. RaiseLocalEvent(ref ev);
  160. ev.FulfillmentEntity ??= station.Value;
  161. if (!ev.Handled)
  162. {
  163. ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order, orderDatabase);
  164. if (ev.FulfillmentEntity == null)
  165. {
  166. ConsolePopup(args.Actor, Loc.GetString("cargo-console-unfulfilled"));
  167. PlayDenySound(uid, component);
  168. return;
  169. }
  170. }
  171. order.Approved = true;
  172. _audio.PlayPvs(component.ConfirmSound, uid);
  173. if (!_emag.CheckFlag(uid, EmagType.Interaction))
  174. {
  175. var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(uid, player);
  176. RaiseLocalEvent(tryGetIdentityShortInfoEvent);
  177. order.SetApproverData(tryGetIdentityShortInfoEvent.Title);
  178. var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast",
  179. ("productName", Loc.GetString(order.ProductName)),
  180. ("orderAmount", order.OrderQuantity),
  181. ("approver", order.Approver ?? string.Empty),
  182. ("cost", cost));
  183. _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
  184. }
  185. ConsolePopup(args.Actor, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName)));
  186. // Log order approval
  187. _adminLogger.Add(LogType.Action, LogImpact.Low,
  188. $"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
  189. orderDatabase.Orders.Remove(order);
  190. UpdateBankAccount((station.Value, bank), -cost);
  191. UpdateOrders(station.Value);
  192. }
  193. private EntityUid? TryFulfillOrder(Entity<StationDataComponent> stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase)
  194. {
  195. // No slots at the trade station
  196. _listEnts.Clear();
  197. GetTradeStations(stationData, ref _listEnts);
  198. EntityUid? tradeDestination = null;
  199. // Try to fulfill from any station where possible, if the pad is not occupied.
  200. foreach (var trade in _listEnts)
  201. {
  202. var tradePads = GetCargoPallets(trade, BuySellType.Buy);
  203. _random.Shuffle(tradePads);
  204. var freePads = GetFreeCargoPallets(trade, tradePads);
  205. if (freePads.Count >= order.OrderQuantity) //check if the station has enough free pallets
  206. {
  207. foreach (var pad in freePads)
  208. {
  209. var coordinates = new EntityCoordinates(trade, pad.Transform.LocalPosition);
  210. if (FulfillOrder(order, coordinates, orderDatabase.PrinterOutput))
  211. {
  212. tradeDestination = trade;
  213. order.NumDispatched++;
  214. if (order.OrderQuantity <= order.NumDispatched) //Spawn a crate on free pellets until the order is fulfilled.
  215. break;
  216. }
  217. }
  218. }
  219. if (tradeDestination != null)
  220. break;
  221. }
  222. return tradeDestination;
  223. }
  224. private void GetTradeStations(StationDataComponent data, ref List<EntityUid> ents)
  225. {
  226. foreach (var gridUid in data.Grids)
  227. {
  228. if (!_tradeQuery.HasComponent(gridUid))
  229. continue;
  230. ents.Add(gridUid);
  231. }
  232. }
  233. private void OnRemoveOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleRemoveOrderMessage args)
  234. {
  235. var station = _station.GetOwningStation(uid);
  236. if (!TryGetOrderDatabase(station, out var orderDatabase))
  237. return;
  238. RemoveOrder(station.Value, args.OrderId, orderDatabase);
  239. }
  240. private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args)
  241. {
  242. if (args.Actor is not { Valid: true } player)
  243. return;
  244. if (args.Amount <= 0)
  245. return;
  246. var stationUid = _station.GetOwningStation(uid);
  247. if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
  248. return;
  249. if (!_protoMan.TryIndex<CargoProductPrototype>(args.CargoProductId, out var product))
  250. {
  251. Log.Error($"Tried to add invalid cargo product {args.CargoProductId} as order!");
  252. return;
  253. }
  254. if (!component.AllowedGroups.Contains(product.Group))
  255. return;
  256. var data = GetOrderData(args, product, GenerateOrderId(orderDatabase));
  257. if (!TryAddOrder(stationUid.Value, data, orderDatabase))
  258. {
  259. PlayDenySound(uid, component);
  260. return;
  261. }
  262. // Log order addition
  263. _adminLogger.Add(LogType.Action, LogImpact.Low,
  264. $"{ToPrettyString(player):user} added order [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]");
  265. }
  266. private void OnOrderUIOpened(EntityUid uid, CargoOrderConsoleComponent component, BoundUIOpenedEvent args)
  267. {
  268. var station = _station.GetOwningStation(uid);
  269. UpdateOrderState(uid, station);
  270. }
  271. #endregion
  272. private void OnOrderBalanceUpdated(Entity<CargoOrderConsoleComponent> ent, ref BankBalanceUpdatedEvent args)
  273. {
  274. if (!_uiSystem.IsUiOpen(ent.Owner, CargoConsoleUiKey.Orders))
  275. return;
  276. UpdateOrderState(ent, args.Station);
  277. }
  278. private void UpdateOrderState(EntityUid consoleUid, EntityUid? station)
  279. {
  280. if (station == null ||
  281. !TryComp<StationCargoOrderDatabaseComponent>(station, out var orderDatabase) ||
  282. !TryComp<StationBankAccountComponent>(station, out var bankAccount)) return;
  283. if (_uiSystem.HasUi(consoleUid, CargoConsoleUiKey.Orders))
  284. {
  285. _uiSystem.SetUiState(consoleUid, CargoConsoleUiKey.Orders, new CargoConsoleInterfaceState(
  286. MetaData(station.Value).EntityName,
  287. GetOutstandingOrderCount(orderDatabase),
  288. orderDatabase.Capacity,
  289. bankAccount.Balance,
  290. orderDatabase.Orders
  291. ));
  292. }
  293. }
  294. private void ConsolePopup(EntityUid actor, string text)
  295. {
  296. _popup.PopupCursor(text, actor);
  297. }
  298. private void PlayDenySound(EntityUid uid, CargoOrderConsoleComponent component)
  299. {
  300. _audio.PlayPvs(_audio.ResolveSound(component.ErrorSound), uid);
  301. }
  302. private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id)
  303. {
  304. return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
  305. }
  306. public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component)
  307. {
  308. var amount = 0;
  309. foreach (var order in component.Orders)
  310. {
  311. if (!order.Approved)
  312. continue;
  313. amount += order.OrderQuantity - order.NumDispatched;
  314. }
  315. return amount;
  316. }
  317. /// <summary>
  318. /// Updates all of the cargo-related consoles for a particular station.
  319. /// This should be called whenever orders change.
  320. /// </summary>
  321. private void UpdateOrders(EntityUid dbUid)
  322. {
  323. // Order added so all consoles need updating.
  324. var orderQuery = AllEntityQuery<CargoOrderConsoleComponent>();
  325. while (orderQuery.MoveNext(out var uid, out var _))
  326. {
  327. var station = _station.GetOwningStation(uid);
  328. if (station != dbUid)
  329. continue;
  330. UpdateOrderState(uid, station);
  331. }
  332. var consoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
  333. while (consoleQuery.MoveNext(out var uid, out var _))
  334. {
  335. var station = _station.GetOwningStation(uid);
  336. if (station != dbUid)
  337. continue;
  338. UpdateShuttleState(uid, station);
  339. }
  340. }
  341. public bool AddAndApproveOrder(
  342. EntityUid dbUid,
  343. string spawnId,
  344. string name,
  345. int cost,
  346. int qty,
  347. string sender,
  348. string description,
  349. string dest,
  350. StationCargoOrderDatabaseComponent component,
  351. Entity<StationDataComponent> stationData
  352. )
  353. {
  354. DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(spawnId));
  355. // Make an order
  356. var id = GenerateOrderId(component);
  357. var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description);
  358. // Approve it now
  359. order.SetApproverData(dest, sender);
  360. order.Approved = true;
  361. // Log order addition
  362. _adminLogger.Add(LogType.Action, LogImpact.Low,
  363. $"AddAndApproveOrder {description} added order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}]");
  364. // Add it to the list
  365. return TryAddOrder(dbUid, order, component) && TryFulfillOrder(stationData, order, component).HasValue;
  366. }
  367. private bool TryAddOrder(EntityUid dbUid, CargoOrderData data, StationCargoOrderDatabaseComponent component)
  368. {
  369. component.Orders.Add(data);
  370. UpdateOrders(dbUid);
  371. return true;
  372. }
  373. private static int GenerateOrderId(StationCargoOrderDatabaseComponent orderDB)
  374. {
  375. // We need an arbitrary unique ID to identify orders, since they may
  376. // want to be cancelled later.
  377. return ++orderDB.NumOrdersCreated;
  378. }
  379. public void RemoveOrder(EntityUid dbUid, int index, StationCargoOrderDatabaseComponent orderDB)
  380. {
  381. var sequenceIdx = orderDB.Orders.FindIndex(order => order.OrderId == index);
  382. if (sequenceIdx != -1)
  383. {
  384. orderDB.Orders.RemoveAt(sequenceIdx);
  385. }
  386. UpdateOrders(dbUid);
  387. }
  388. public void ClearOrders(StationCargoOrderDatabaseComponent component)
  389. {
  390. if (component.Orders.Count == 0)
  391. return;
  392. component.Orders.Clear();
  393. }
  394. private static bool PopFrontOrder(StationCargoOrderDatabaseComponent orderDB, [NotNullWhen(true)] out CargoOrderData? orderOut)
  395. {
  396. var orderIdx = orderDB.Orders.FindIndex(order => order.Approved);
  397. if (orderIdx == -1)
  398. {
  399. orderOut = null;
  400. return false;
  401. }
  402. orderOut = orderDB.Orders[orderIdx];
  403. orderOut.NumDispatched++;
  404. if (orderOut.NumDispatched >= orderOut.OrderQuantity)
  405. {
  406. // Order is complete. Remove from the queue.
  407. orderDB.Orders.RemoveAt(orderIdx);
  408. }
  409. return true;
  410. }
  411. /// <summary>
  412. /// Tries to fulfill the next outstanding order.
  413. /// </summary>
  414. private bool FulfillNextOrder(StationCargoOrderDatabaseComponent orderDB, EntityCoordinates spawn, string? paperProto)
  415. {
  416. if (!PopFrontOrder(orderDB, out var order))
  417. return false;
  418. return FulfillOrder(order, spawn, paperProto);
  419. }
  420. /// <summary>
  421. /// Fulfills the specified cargo order and spawns paper attached to it.
  422. /// </summary>
  423. private bool FulfillOrder(CargoOrderData order, EntityCoordinates spawn, string? paperProto)
  424. {
  425. // Create the item itself
  426. var item = Spawn(order.ProductId, spawn);
  427. // Ensure the item doesn't start anchored
  428. _transformSystem.Unanchor(item, Transform(item));
  429. // Create a sheet of paper to write the order details on
  430. var printed = EntityManager.SpawnEntity(paperProto, spawn);
  431. if (TryComp<PaperComponent>(printed, out var paper))
  432. {
  433. // fill in the order data
  434. var val = Loc.GetString("cargo-console-paper-print-name", ("orderNumber", order.OrderId));
  435. _metaSystem.SetEntityName(printed, val);
  436. _paperSystem.SetContent((printed, paper), Loc.GetString(
  437. "cargo-console-paper-print-text",
  438. ("orderNumber", order.OrderId),
  439. ("itemName", MetaData(item).EntityName),
  440. ("orderQuantity", order.OrderQuantity),
  441. ("requester", order.Requester),
  442. ("reason", order.Reason),
  443. ("approver", order.Approver ?? string.Empty)));
  444. // attempt to attach the label to the item
  445. if (TryComp<PaperLabelComponent>(item, out var label))
  446. {
  447. _slots.TryInsert(item, label.LabelSlot, printed, null);
  448. }
  449. }
  450. return true;
  451. }
  452. #region Station
  453. private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)
  454. {
  455. return TryComp(stationUid, out dbComp);
  456. }
  457. #endregion
  458. }
  459. }