mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-22 16:17:00 +03:00
## Mirror of PR #25955: [Trading Outpost now has half buy-only and half sell-only pallets](https://github.com/space-wizards/space-station-14/pull/25955) from <img src="https://avatars.githubusercontent.com/u/10567778?v=4" alt="space-wizards" width="22"/> [space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14) ###### `c0bbbc33c19eafcc8defaa7f1ec2df42e485b435` PR opened by <img src="https://avatars.githubusercontent.com/u/1471082?v=4" width="16"/><a href="https://github.com/wafehling"> wafehling</a> at 2024-03-10 02:47:02 UTC --- PR changed 8 files with 226 additions and 123 deletions. The PR had the following labels: - Changes: Sprites - Status: Needs Review --- <details open="true"><summary><h1>Original Body</h1></summary> > ## About the PR > I added two new entity types, a buy and sell only cargo pallet, and added them to the trading outpost in place of the old pallets. > > ## Why / Balance > Since the day this was added, every single shift, at some point I'll hear someone in cargo yelling about how **"You sold stuff I just bought!"** > > This seemed like the easiest way to fix the problem. > > ## Technical details > Added a variable to the cargo pallet component for labeling which type it is, added two new varieties of the cargo pallet to the yml, and adjusted all the calls for GetCargoPallets() to have boolean variables for buyOnly and sellOnly to tell the function what kind of call you're looking for. > > If you don't give it any specifics it'll still treat any pallet as a two-way one, but this function is only called 2 or 3 times and I've adjusted them all and commented the function itself for anyone looking to change/add it in the future. > > The old pallet is still in the game and is set to "both" by default, so it should work just fine for backwards compatibility. > > ## Media >  >  >  >  >  > > > - [X] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase > > ## Breaking changes > > Shouldn't break anything, I've left the old pallets fully in, and updated them to work with the new GetCargoPallets() function just like they used to. Not sure if/where you'd still need them, but better safe than sorry. > > **Changelog** > 🆑 > - tweak: The trading outpost now has dedicated buy-only and sell-only pallets. No more accidentally selling orders you just bought. Cargonians rejoice! > </details> Co-authored-by: SimpleStation14 <Unknown>
520 lines
19 KiB
C#
520 lines
19 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using Content.Server.Cargo.Components;
|
|
using Content.Server.Labels.Components;
|
|
using Content.Server.Paper;
|
|
using Content.Server.Station.Components;
|
|
using Content.Shared.Cargo;
|
|
using Content.Shared.Cargo.BUI;
|
|
using Content.Shared.Cargo.Components;
|
|
using Content.Shared.Cargo.Events;
|
|
using Content.Shared.Cargo.Prototypes;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Interaction;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Server.Cargo.Systems
|
|
{
|
|
public sealed partial class CargoSystem
|
|
{
|
|
/// <summary>
|
|
/// How much time to wait (in seconds) before increasing bank accounts balance.
|
|
/// </summary>
|
|
private const int Delay = 10;
|
|
|
|
/// <summary>
|
|
/// Keeps track of how much time has elapsed since last balance increase.
|
|
/// </summary>
|
|
private float _timer;
|
|
|
|
private void InitializeConsole()
|
|
{
|
|
SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleAddOrderMessage>(OnAddOrderMessage);
|
|
SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleRemoveOrderMessage>(OnRemoveOrderMessage);
|
|
SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleApproveOrderMessage>(OnApproveOrderMessage);
|
|
SubscribeLocalEvent<CargoOrderConsoleComponent, BoundUIOpenedEvent>(OnOrderUIOpened);
|
|
SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit);
|
|
SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing);
|
|
Reset();
|
|
}
|
|
|
|
private void OnInteractUsing(EntityUid uid, CargoOrderConsoleComponent component, ref InteractUsingEvent args)
|
|
{
|
|
if (!HasComp<CashComponent>(args.Used))
|
|
return;
|
|
|
|
var price = _pricing.GetPrice(args.Used);
|
|
|
|
if (price == 0)
|
|
return;
|
|
|
|
var stationUid = _station.GetOwningStation(args.Used);
|
|
|
|
if (!TryComp(stationUid, out StationBankAccountComponent? bank))
|
|
return;
|
|
|
|
_audio.PlayPvs(component.ConfirmSound, uid);
|
|
UpdateBankAccount(stationUid.Value, bank, (int) price);
|
|
QueueDel(args.Used);
|
|
}
|
|
|
|
private void OnInit(EntityUid uid, CargoOrderConsoleComponent orderConsole, ComponentInit args)
|
|
{
|
|
var station = _station.GetOwningStation(uid);
|
|
UpdateOrderState(uid, station);
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
_timer = 0;
|
|
}
|
|
|
|
private void UpdateConsole(float frameTime)
|
|
{
|
|
_timer += frameTime;
|
|
|
|
// TODO: Doesn't work with serialization and shouldn't just be updating every delay
|
|
// client can just interp this just fine on its own.
|
|
while (_timer > Delay)
|
|
{
|
|
_timer -= Delay;
|
|
|
|
foreach (var account in EntityQuery<StationBankAccountComponent>())
|
|
{
|
|
account.Balance += account.IncreasePerSecond * Delay;
|
|
}
|
|
|
|
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
|
|
while (query.MoveNext(out var uid, out var _))
|
|
{
|
|
if (!_uiSystem.IsUiOpen(uid, CargoConsoleUiKey.Orders)) continue;
|
|
|
|
var station = _station.GetOwningStation(uid);
|
|
UpdateOrderState(uid, station);
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Interface
|
|
|
|
private void OnApproveOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleApproveOrderMessage args)
|
|
{
|
|
if (args.Session.AttachedEntity is not { Valid: true } player)
|
|
return;
|
|
|
|
if (!_accessReaderSystem.IsAllowed(player, uid))
|
|
{
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-order-not-allowed"));
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
var station = _station.GetOwningStation(uid);
|
|
|
|
// No station to deduct from.
|
|
if (!TryComp(station, out StationBankAccountComponent? bank) ||
|
|
!TryComp(station, out StationDataComponent? stationData) ||
|
|
!TryGetOrderDatabase(station, out var orderDatabase))
|
|
{
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-station-not-found"));
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
// Find our order again. It might have been dispatched or approved already
|
|
var order = orderDatabase.Orders.Find(order => args.OrderId == order.OrderId && !order.Approved);
|
|
if (order == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Invalid order
|
|
if (!_protoMan.HasIndex<EntityPrototype>(order.ProductId))
|
|
{
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-invalid-product"));
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
var amount = GetOutstandingOrderCount(orderDatabase);
|
|
var capacity = orderDatabase.Capacity;
|
|
|
|
// Too many orders, avoid them getting spammed in the UI.
|
|
if (amount >= capacity)
|
|
{
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-too-many"));
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
// Cap orders so someone can't spam thousands.
|
|
var cappedAmount = Math.Min(capacity - amount, order.OrderQuantity);
|
|
|
|
if (cappedAmount != order.OrderQuantity)
|
|
{
|
|
order.OrderQuantity = cappedAmount;
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-snip-snip"));
|
|
PlayDenySound(uid, component);
|
|
}
|
|
|
|
var cost = order.Price * order.OrderQuantity;
|
|
|
|
// Not enough balance
|
|
if (cost > bank.Balance)
|
|
{
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-insufficient-funds", ("cost", cost)));
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
var tradeDestination = TryFulfillOrder(stationData, order, orderDatabase);
|
|
|
|
if (tradeDestination == null)
|
|
{
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-unfulfilled"));
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
_idCardSystem.TryFindIdCard(player, out var idCard);
|
|
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
|
|
order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle);
|
|
_audio.PlayPvs(component.ConfirmSound, uid);
|
|
|
|
ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(tradeDestination.Value).EntityName)));
|
|
|
|
// Log order approval
|
|
_adminLogger.Add(LogType.Action, LogImpact.Low,
|
|
$"{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}");
|
|
|
|
orderDatabase.Orders.Remove(order);
|
|
DeductFunds(bank, cost);
|
|
UpdateOrders(station.Value, orderDatabase);
|
|
}
|
|
|
|
private EntityUid? TryFulfillOrder(StationDataComponent stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase)
|
|
{
|
|
// No slots at the trade station
|
|
_listEnts.Clear();
|
|
GetTradeStations(stationData, ref _listEnts);
|
|
EntityUid? tradeDestination = null;
|
|
|
|
// Try to fulfill from any station where possible, if the pad is not occupied.
|
|
foreach (var trade in _listEnts)
|
|
{
|
|
var tradePads = GetCargoPallets(trade, BuySellType.Buy);
|
|
_random.Shuffle(tradePads);
|
|
|
|
var freePads = GetFreeCargoPallets(trade, tradePads);
|
|
if (freePads.Count >= order.OrderQuantity) //check if the station has enough free pallets
|
|
{
|
|
foreach (var pad in freePads)
|
|
{
|
|
var coordinates = new EntityCoordinates(trade, pad.Transform.LocalPosition);
|
|
|
|
if (FulfillOrder(order, coordinates, orderDatabase.PrinterOutput))
|
|
{
|
|
tradeDestination = trade;
|
|
order.NumDispatched++;
|
|
if (order.OrderQuantity <= order.NumDispatched) //Spawn a crate on free pellets until the order is fulfilled.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tradeDestination != null)
|
|
break;
|
|
}
|
|
|
|
return tradeDestination;
|
|
}
|
|
|
|
private void GetTradeStations(StationDataComponent data, ref List<EntityUid> ents)
|
|
{
|
|
var tradeStationQuery = AllEntityQuery<TradeStationComponent>(); // We *could* cache this, but I don't know where it'd go
|
|
|
|
while (tradeStationQuery.MoveNext(out var uid, out _))
|
|
{
|
|
//if (!_tradeQuery.HasComponent(uid))
|
|
// continue;
|
|
|
|
ents.Add(uid);
|
|
}
|
|
}
|
|
|
|
private void OnRemoveOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleRemoveOrderMessage args)
|
|
{
|
|
var station = _station.GetOwningStation(uid);
|
|
|
|
if (!TryGetOrderDatabase(station, out var orderDatabase))
|
|
return;
|
|
|
|
RemoveOrder(station.Value, args.OrderId, orderDatabase);
|
|
}
|
|
|
|
private void OnAddOrderMessage(EntityUid uid, CargoOrderConsoleComponent component, CargoConsoleAddOrderMessage args)
|
|
{
|
|
if (args.Session.AttachedEntity is not { Valid: true } player)
|
|
return;
|
|
|
|
if (args.Amount <= 0)
|
|
return;
|
|
|
|
var stationUid = _station.GetOwningStation(uid);
|
|
|
|
if (!TryGetOrderDatabase(stationUid, out var orderDatabase))
|
|
return;
|
|
|
|
if (!_protoMan.TryIndex<CargoProductPrototype>(args.CargoProductId, out var product))
|
|
{
|
|
Log.Error($"Tried to add invalid cargo product {args.CargoProductId} as order!");
|
|
return;
|
|
}
|
|
|
|
if (!component.AllowedGroups.Contains(product.Group))
|
|
return;
|
|
|
|
var data = GetOrderData(args, product, GenerateOrderId(orderDatabase));
|
|
|
|
if (!TryAddOrder(stationUid.Value, data, orderDatabase))
|
|
{
|
|
PlayDenySound(uid, component);
|
|
return;
|
|
}
|
|
|
|
// Log order addition
|
|
_adminLogger.Add(LogType.Action, LogImpact.Low,
|
|
$"{ToPrettyString(player):user} added order [orderId:{data.OrderId}, quantity:{data.OrderQuantity}, product:{data.ProductId}, requester:{data.Requester}, reason:{data.Reason}]");
|
|
|
|
}
|
|
|
|
private void OnOrderUIOpened(EntityUid uid, CargoOrderConsoleComponent component, BoundUIOpenedEvent args)
|
|
{
|
|
var station = _station.GetOwningStation(uid);
|
|
UpdateOrderState(uid, station);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void UpdateOrderState(EntityUid consoleUid, EntityUid? station)
|
|
{
|
|
if (station == null ||
|
|
!TryComp<StationCargoOrderDatabaseComponent>(station, out var orderDatabase) ||
|
|
!TryComp<StationBankAccountComponent>(station, out var bankAccount)) return;
|
|
|
|
if (_uiSystem.TryGetUi(consoleUid, CargoConsoleUiKey.Orders, out var bui))
|
|
{
|
|
_uiSystem.SetUiState(bui, new CargoConsoleInterfaceState(
|
|
MetaData(station.Value).EntityName,
|
|
GetOutstandingOrderCount(orderDatabase),
|
|
orderDatabase.Capacity,
|
|
bankAccount.Balance,
|
|
orderDatabase.Orders
|
|
));
|
|
}
|
|
}
|
|
|
|
private void ConsolePopup(ICommonSession session, string text)
|
|
{
|
|
_popup.PopupCursor(text, session);
|
|
}
|
|
|
|
private void PlayDenySound(EntityUid uid, CargoOrderConsoleComponent component)
|
|
{
|
|
_audio.PlayPvs(_audio.GetSound(component.ErrorSound), uid);
|
|
}
|
|
|
|
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct, int id)
|
|
{
|
|
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
|
|
}
|
|
|
|
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component)
|
|
{
|
|
var amount = 0;
|
|
|
|
foreach (var order in component.Orders)
|
|
{
|
|
if (!order.Approved)
|
|
continue;
|
|
amount += order.OrderQuantity - order.NumDispatched;
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates all of the cargo-related consoles for a particular station.
|
|
/// This should be called whenever orders change.
|
|
/// </summary>
|
|
private void UpdateOrders(EntityUid dbUid, StationCargoOrderDatabaseComponent _)
|
|
{
|
|
// Order added so all consoles need updating.
|
|
var orderQuery = AllEntityQuery<CargoOrderConsoleComponent>();
|
|
|
|
while (orderQuery.MoveNext(out var uid, out var _))
|
|
{
|
|
var station = _station.GetOwningStation(uid);
|
|
if (station != dbUid)
|
|
continue;
|
|
|
|
UpdateOrderState(uid, station);
|
|
}
|
|
|
|
var consoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
|
|
while (consoleQuery.MoveNext(out var uid, out var _))
|
|
{
|
|
var station = _station.GetOwningStation(uid);
|
|
if (station != dbUid)
|
|
continue;
|
|
|
|
UpdateShuttleState(uid, station);
|
|
}
|
|
}
|
|
|
|
public bool AddAndApproveOrder(
|
|
EntityUid dbUid,
|
|
string spawnId,
|
|
int cost,
|
|
int qty,
|
|
string sender,
|
|
string description,
|
|
string dest,
|
|
StationCargoOrderDatabaseComponent component,
|
|
StationDataComponent stationData
|
|
)
|
|
{
|
|
DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(spawnId));
|
|
// Make an order
|
|
var id = GenerateOrderId(component);
|
|
var order = new CargoOrderData(id, spawnId, cost, qty, sender, description);
|
|
|
|
// Approve it now
|
|
order.SetApproverData(dest, sender);
|
|
|
|
// Log order addition
|
|
_adminLogger.Add(LogType.Action, LogImpact.Low,
|
|
$"AddAndApproveOrder {description} added order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}]");
|
|
|
|
// Add it to the list
|
|
return TryAddOrder(dbUid, order, component) && TryFulfillOrder(stationData, order, component).HasValue;
|
|
}
|
|
|
|
private bool TryAddOrder(EntityUid dbUid, CargoOrderData data, StationCargoOrderDatabaseComponent component)
|
|
{
|
|
component.Orders.Add(data);
|
|
UpdateOrders(dbUid, component);
|
|
return true;
|
|
}
|
|
|
|
private static int GenerateOrderId(StationCargoOrderDatabaseComponent orderDB)
|
|
{
|
|
// We need an arbitrary unique ID to identify orders, since they may
|
|
// want to be cancelled later.
|
|
return ++orderDB.NumOrdersCreated;
|
|
}
|
|
|
|
public void RemoveOrder(EntityUid dbUid, int index, StationCargoOrderDatabaseComponent orderDB)
|
|
{
|
|
var sequenceIdx = orderDB.Orders.FindIndex(order => order.OrderId == index);
|
|
if (sequenceIdx != -1)
|
|
{
|
|
orderDB.Orders.RemoveAt(sequenceIdx);
|
|
}
|
|
UpdateOrders(dbUid, orderDB);
|
|
}
|
|
|
|
public void ClearOrders(StationCargoOrderDatabaseComponent component)
|
|
{
|
|
if (component.Orders.Count == 0)
|
|
return;
|
|
|
|
component.Orders.Clear();
|
|
}
|
|
|
|
private static bool PopFrontOrder(StationCargoOrderDatabaseComponent orderDB, [NotNullWhen(true)] out CargoOrderData? orderOut)
|
|
{
|
|
var orderIdx = orderDB.Orders.FindIndex(order => order.Approved);
|
|
if (orderIdx == -1)
|
|
{
|
|
orderOut = null;
|
|
return false;
|
|
}
|
|
|
|
orderOut = orderDB.Orders[orderIdx];
|
|
orderOut.NumDispatched++;
|
|
|
|
if (orderOut.NumDispatched >= orderOut.OrderQuantity)
|
|
{
|
|
// Order is complete. Remove from the queue.
|
|
orderDB.Orders.RemoveAt(orderIdx);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to fulfill the next outstanding order.
|
|
/// </summary>
|
|
private bool FulfillNextOrder(StationCargoOrderDatabaseComponent orderDB, EntityCoordinates spawn, string? paperProto)
|
|
{
|
|
if (!PopFrontOrder(orderDB, out var order))
|
|
return false;
|
|
|
|
return FulfillOrder(order, spawn, paperProto);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fulfills the specified cargo order and spawns paper attached to it.
|
|
/// </summary>
|
|
private bool FulfillOrder(CargoOrderData order, EntityCoordinates spawn, string? paperProto)
|
|
{
|
|
// Create the item itself
|
|
var item = Spawn(order.ProductId, spawn);
|
|
|
|
// Create a sheet of paper to write the order details on
|
|
var printed = EntityManager.SpawnEntity(paperProto, spawn);
|
|
if (TryComp<PaperComponent>(printed, out var paper))
|
|
{
|
|
// fill in the order data
|
|
var val = Loc.GetString("cargo-console-paper-print-name", ("orderNumber", order.OrderId));
|
|
_metaSystem.SetEntityName(printed, val);
|
|
|
|
_paperSystem.SetContent(printed, Loc.GetString(
|
|
"cargo-console-paper-print-text",
|
|
("orderNumber", order.OrderId),
|
|
("itemName", MetaData(item).EntityName),
|
|
("requester", order.Requester),
|
|
("reason", order.Reason),
|
|
("approver", order.Approver ?? string.Empty)),
|
|
paper);
|
|
|
|
// attempt to attach the label to the item
|
|
if (TryComp<PaperLabelComponent>(item, out var label))
|
|
{
|
|
_slots.TryInsert(item, label.LabelSlot, printed, null);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
private void DeductFunds(StationBankAccountComponent component, int amount)
|
|
{
|
|
component.Balance = Math.Max(0, component.Balance - amount);
|
|
}
|
|
|
|
#region Station
|
|
|
|
private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)
|
|
{
|
|
return TryComp(stationUid, out dbComp);
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|