| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- 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<TradeStationComponent, GridSplitEvent>(OnTradeSplit);
- SubscribeLocalEvent<CargoShuttleConsoleComponent, ComponentStartup>(OnCargoShuttleConsoleStartup);
- SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletSellMessage>(OnPalletSale);
- SubscribeLocalEvent<CargoPalletConsoleComponent, CargoPalletAppraiseMessage>(OnPalletAppraise);
- SubscribeLocalEvent<CargoPalletConsoleComponent, BoundUIOpenedEvent>(OnPalletUIOpen);
- SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
- }
- #region Console
- private void UpdateCargoShuttleConsoles(EntityUid shuttleUid, CargoShuttleComponent _)
- {
- // Update pilot consoles that are already open.
- _console.RefreshDroneConsoles();
- // Update order consoles.
- var shuttleConsoleQuery = AllEntityQuery<CargoShuttleConsoleComponent>();
- 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)
- {
- UpdatePalletConsoleInterface(uid);
- }
- /// <summary>
- /// 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
- /// </summary>
- private void OnPalletAppraise(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletAppraiseMessage args)
- {
- 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<StationCargoOrderDatabaseComponent>(station, out var orderDatabase);
- TryComp<CargoShuttleComponent>(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<TradeStationComponent>(gridUid);
- }
- }
- #region Shuttle
- /// <summary>
- /// Returns the orders that can fit on the cargo shuttle.
- /// </summary>
- private List<CargoOrderData> GetProjectedOrders(
- EntityUid shuttleUid,
- StationCargoOrderDatabaseComponent? component = null,
- CargoShuttleComponent? shuttle = null)
- {
- var orders = new List<CargoOrderData>();
- 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;
- }
- /// <summary>
- /// Get the amount of space the cargo shuttle can fit for orders.
- /// </summary>
- 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<CargoPalletComponent, TransformComponent>();
- 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<EntityUid> toSell, out double amount)
- {
- amount = 0;
- toSell = new HashSet<EntityUid>();
- 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 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<StackPrototype>(component.CashType);
- _stack.Spawn((int) price, stackPrototype, xform.Coordinates);
- _audio.PlayPvs(ApproveSound, uid);
- UpdatePalletConsoleInterface(uid);
- }
- #endregion
- private void OnRoundRestart(RoundRestartCleanupEvent ev)
- {
- Reset();
- }
- }
- /// <summary>
- /// Event broadcast raised by-ref before it is sold and
- /// deleted but after the price has been calculated.
- /// </summary>
- [ByRefEvent]
- public readonly record struct EntitySoldEvent(HashSet<EntityUid> Sold);
|