using Content.Server.Cargo.Components; using Content.Shared.Stacks; using Content.Shared.Cargo; using Content.Shared.Cargo.BUI; using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Events; using Content.Shared.GameTicking; using Robust.Shared.Map; using Robust.Shared.Random; using Robust.Shared.Audio; namespace Content.Server.Cargo.Systems; public sealed partial class CargoSystem { /* * Handles cargo shuttle / trade mechanics. */ private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg"); private void InitializeShuttle() { SubscribeLocalEvent(OnTradeSplit); SubscribeLocalEvent(OnCargoShuttleConsoleStartup); SubscribeLocalEvent(OnPalletSale); SubscribeLocalEvent(OnPalletAppraise); SubscribeLocalEvent(OnPalletUIOpen); SubscribeLocalEvent(OnRoundRestart); } #region Console private void UpdateCargoShuttleConsoles(EntityUid shuttleUid, CargoShuttleComponent _) { // Update pilot consoles that are already open. _console.RefreshDroneConsoles(); // Update order consoles. var shuttleConsoleQuery = AllEntityQuery(); while (shuttleConsoleQuery.MoveNext(out var uid, out var _)) { var stationUid = _station.GetOwningStation(uid); if (stationUid != shuttleUid) continue; UpdateShuttleState(uid, stationUid); } } private void UpdatePalletConsoleInterface(EntityUid uid) { if (Transform(uid).GridUid is not EntityUid gridUid) { _uiSystem.SetUiState(uid, CargoPalletConsoleUiKey.Sale, new CargoPalletConsoleInterfaceState(0, 0, false)); return; } GetPalletGoods(gridUid, out var toSell, out var amount); _uiSystem.SetUiState(uid, CargoPalletConsoleUiKey.Sale, new CargoPalletConsoleInterfaceState((int) amount, toSell.Count, true)); } private void OnPalletUIOpen(EntityUid uid, CargoPalletConsoleComponent component, BoundUIOpenedEvent args) { var player = args.Actor; if (player == null) return; UpdatePalletConsoleInterface(uid); } /// /// Ok so this is just the same thing as opening the UI, its a refresh button. /// I know this would probably feel better if it were like predicted and dynamic as pallet contents change /// However. /// I dont want it to explode if cargo uses a conveyor to move 8000 pineapple slices or whatever, they are /// known for their entity spam i wouldnt put it past them /// private void OnPalletAppraise(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletAppraiseMessage args) { var player = args.Actor; if (player == null) return; UpdatePalletConsoleInterface(uid); } private void OnCargoShuttleConsoleStartup(EntityUid uid, CargoShuttleConsoleComponent component, ComponentStartup args) { var station = _station.GetOwningStation(uid); UpdateShuttleState(uid, station); } private void UpdateShuttleState(EntityUid uid, EntityUid? station = null) { TryComp(station, out var orderDatabase); TryComp(orderDatabase?.Shuttle, out var shuttle); var orders = GetProjectedOrders(station ?? EntityUid.Invalid, orderDatabase, shuttle); var shuttleName = orderDatabase?.Shuttle != null ? MetaData(orderDatabase.Shuttle.Value).EntityName : string.Empty; if (_uiSystem.HasUi(uid, CargoConsoleUiKey.Shuttle)) _uiSystem.SetUiState(uid, CargoConsoleUiKey.Shuttle, new CargoShuttleConsoleBoundUserInterfaceState( station != null ? MetaData(station.Value).EntityName : Loc.GetString("cargo-shuttle-console-station-unknown"), string.IsNullOrEmpty(shuttleName) ? Loc.GetString("cargo-shuttle-console-shuttle-not-found") : shuttleName, orders )); } #endregion private void OnTradeSplit(EntityUid uid, TradeStationComponent component, ref GridSplitEvent args) { // If the trade station gets bombed it's still a trade station. foreach (var gridUid in args.NewGrids) { EnsureComp(gridUid); } } #region Shuttle /// /// Returns the orders that can fit on the cargo shuttle. /// private List GetProjectedOrders( EntityUid shuttleUid, StationCargoOrderDatabaseComponent? component = null, CargoShuttleComponent? shuttle = null) { var orders = new List(); if (component == null || shuttle == null || component.Orders.Count == 0) return orders; var spaceRemaining = GetCargoSpace(shuttleUid); for (var i = 0; i < component.Orders.Count && spaceRemaining > 0; i++) { var order = component.Orders[i]; if (order.Approved) { var numToShip = order.OrderQuantity - order.NumDispatched; if (numToShip > spaceRemaining) { // We won't be able to fit the whole order on, so make one // which represents the space we do have left: var reducedOrder = new CargoOrderData(order.OrderId, order.ProductId, order.ProductName, order.Price, spaceRemaining, order.Requester, order.Reason); orders.Add(reducedOrder); } else { orders.Add(order); } spaceRemaining -= numToShip; } } return orders; } /// /// Get the amount of space the cargo shuttle can fit for orders. /// private int GetCargoSpace(EntityUid gridUid) { var space = GetCargoPallets(gridUid, BuySellType.Buy).Count; return space; } /// GetCargoPallets(gridUid, BuySellType.Sell) to return only Sell pads /// GetCargoPallets(gridUid, BuySellType.Buy) to return only Buy pads private List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent PalletXform)> GetCargoPallets(EntityUid gridUid, BuySellType requestType = BuySellType.All) { _pads.Clear(); var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var compXform)) { if (compXform.ParentUid != gridUid || !compXform.Anchored) { continue; } if ((requestType & comp.PalletType) == 0) { continue; } _pads.Add((uid, comp, compXform)); } return _pads; } private List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> GetFreeCargoPallets(EntityUid gridUid, List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> pallets) { _setEnts.Clear(); List<(EntityUid Entity, CargoPalletComponent Component, TransformComponent Transform)> outList = new(); foreach (var pallet in pallets) { var aabb = _lookup.GetAABBNoContainer(pallet.Entity, pallet.Transform.LocalPosition, pallet.Transform.LocalRotation); if (_lookup.AnyLocalEntitiesIntersecting(gridUid, aabb, LookupFlags.Dynamic)) continue; outList.Add(pallet); } return outList; } #endregion #region Station private bool SellPallets(EntityUid gridUid, out double amount) { GetPalletGoods(gridUid, out var toSell, out amount); Log.Debug($"Cargo sold {toSell.Count} entities for {amount}"); if (toSell.Count == 0) return false; var ev = new EntitySoldEvent(toSell); RaiseLocalEvent(ref ev); foreach (var ent in toSell) { Del(ent); } return true; } private void GetPalletGoods(EntityUid gridUid, out HashSet toSell, out double amount) { amount = 0; toSell = new HashSet(); foreach (var (palletUid, _, _) in GetCargoPallets(gridUid, BuySellType.Sell)) { // Containers should already get the sell price of their children so can skip those. _setEnts.Clear(); _lookup.GetEntitiesIntersecting(palletUid, _setEnts, LookupFlags.Dynamic | LookupFlags.Sundries); foreach (var ent in _setEnts) { // Dont sell: // - anything already being sold // - anything anchored (e.g. light fixtures) // - anything blacklisted (e.g. players). if (toSell.Contains(ent) || _xformQuery.TryGetComponent(ent, out var xform) && (xform.Anchored || !CanSell(ent, xform))) { continue; } if (_blacklistQuery.HasComponent(ent)) continue; var price = _pricing.GetPrice(ent); if (price == 0) continue; toSell.Add(ent); amount += price; } } } private bool CanSell(EntityUid uid, TransformComponent xform) { if (_mobQuery.HasComponent(uid)) { return false; } var complete = IsBountyComplete(uid, out var bountyEntities); // Recursively check for mobs at any point. var children = xform.ChildEnumerator; while (children.MoveNext(out var child)) { if (complete && bountyEntities.Contains(child)) continue; if (!CanSell(child, _xformQuery.GetComponent(child))) return false; } return true; } private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletSellMessage args) { var player = args.Actor; if (player == null) return; var xform = Transform(uid); if (xform.GridUid is not EntityUid gridUid) { _uiSystem.SetUiState(uid, CargoPalletConsoleUiKey.Sale, new CargoPalletConsoleInterfaceState(0, 0, false)); return; } if (!SellPallets(gridUid, out var price)) return; var stackPrototype = _protoMan.Index(component.CashType); _stack.Spawn((int) price, stackPrototype, xform.Coordinates); _audio.PlayPvs(ApproveSound, uid); UpdatePalletConsoleInterface(uid); } #endregion private void OnRoundRestart(RoundRestartCleanupEvent ev) { Reset(); } } /// /// Event broadcast raised by-ref before it is sold and /// deleted but after the price has been calculated. /// [ByRefEvent] public readonly record struct EntitySoldEvent(HashSet Sold);