using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; using Content.Server.UserInterface; using Content.Server.Shuttles.Systems; using Content.Server.Stack; 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.Cargo.Prototypes; using Content.Shared.CCVar; using Content.Shared.Dataset; using Content.Shared.GameTicking; using Content.Shared.Whitelist; using Robust.Server.GameObjects; using Robust.Shared.Configuration; using Robust.Shared.Map; using Robust.Shared.Random; using Robust.Shared.Utility; using Robust.Shared.Prototypes; using Content.Shared.Coordinates; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; namespace Content.Server.Cargo.Systems; public sealed partial class CargoSystem { /* * Handles cargo shuttle mechanics, including cargo shuttle consoles. */ [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IConfigurationManager _configManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly MapLoaderSystem _map = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly ShuttleConsoleSystem _console = default!; [Dependency] private readonly StackSystem _stack = default!; public MapId? CargoMap { get; private set; } private int _index; /// /// Whether cargo shuttles are enabled at all. Mainly used to disable cargo shuttle loading for performance reasons locally. /// private bool _enabled; private void InitializeShuttle() { _enabled = _configManager.GetCVar(CCVars.CargoShuttles); // Don't want to immediately call this as shuttles will get setup in the natural course of things. _configManager.OnValueChanged(CCVars.CargoShuttles, SetCargoShuttleEnabled); SubscribeLocalEvent(OnCargoFTLStarted); SubscribeLocalEvent(OnCargoFTLCompleted); SubscribeLocalEvent(OnCargoFTLTag); SubscribeLocalEvent(OnCargoShuttleConsoleStartup); SubscribeLocalEvent(OnPalletSale); SubscribeLocalEvent(OnPalletAppraise); SubscribeLocalEvent(OnPalletUIOpen); SubscribeLocalEvent(OnCargoGetConsole); SubscribeLocalEvent(OnCargoPilotConsoleOpen); SubscribeLocalEvent(OnCargoPilotConsoleClose); SubscribeLocalEvent(OnCargoOrderStartup); SubscribeLocalEvent(OnRoundRestart); } private void OnCargoFTLTag(EntityUid uid, CargoShuttleComponent component, ref FTLTagEvent args) { if (args.Handled) return; // Just saves mappers forgetting. args.Handled = true; args.Tag = "DockCargo"; } private void ShutdownShuttle() { _configManager.UnsubValueChanged(CCVars.CargoShuttles, SetCargoShuttleEnabled); } private void SetCargoShuttleEnabled(bool value) { if (_enabled == value) return; _enabled = value; if (value) { Setup(); var query = AllEntityQuery(); while (query.MoveNext(out var stationUid, out var station)) { AddShuttle(stationUid, station); } } else { CleanupShuttle(); } } #region Cargo Pilot Console private void OnCargoPilotConsoleOpen(EntityUid uid, CargoPilotConsoleComponent component, AfterActivatableUIOpenEvent args) { component.Entity = GetShuttleConsole(uid); } private void OnCargoPilotConsoleClose(EntityUid uid, CargoPilotConsoleComponent component, BoundUIClosedEvent args) { component.Entity = null; } private void OnCargoGetConsole(EntityUid uid, CargoPilotConsoleComponent component, ref ConsoleShuttleEvent args) { args.Console = GetShuttleConsole(uid); } private EntityUid? GetShuttleConsole(EntityUid uid) { var stationUid = _station.GetOwningStation(uid); if (!TryComp(stationUid, out var orderDatabase) || !TryComp(orderDatabase.Shuttle, out var shuttle)) { return null; } return GetShuttleConsole(orderDatabase.Shuttle.Value, shuttle); } #endregion #region Console private void UpdateCargoShuttleConsoles(CargoShuttleComponent component) { // Update pilot consoles that are already open. var pilotConsoleQuery = AllEntityQuery(); while (pilotConsoleQuery.MoveNext(out var uid, out var console)) { var stationUid = _station.GetOwningStation(uid); if (stationUid == null || stationUid != component.Station) continue; console.Entity = GetShuttleConsole(stationUid.Value); } // Update order consoles. var shuttleConsoleQuery = AllEntityQuery(); while (shuttleConsoleQuery.MoveNext(out var uid, out var console)) { var stationUid = _station.GetOwningStation(uid); if (stationUid != component.Station) continue; UpdateShuttleState(uid, stationUid); } } private void UpdatePalletConsoleInterface(EntityUid uid) { var bui = _uiSystem.GetUi(uid, CargoPalletConsoleUiKey.Sale); if (Transform(uid).GridUid is not EntityUid gridUid) { _uiSystem.SetUiState(bui, new CargoPalletConsoleInterfaceState(0, 0, false)); return; } GetPalletGoods(gridUid, out var toSell, out var amount); _uiSystem.SetUiState(bui, new CargoPalletConsoleInterfaceState((int) amount, toSell.Count, true)); } private void OnPalletUIOpen(EntityUid uid, CargoPalletConsoleComponent component, BoundUIOpenedEvent args) { var player = args.Session.AttachedEntity; 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.Session.AttachedEntity; 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; _uiSystem.GetUiOrNull(uid, CargoConsoleUiKey.Shuttle)?.SetState( 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 #region Shuttle public EntityUid? GetShuttleConsole(EntityUid uid, CargoShuttleComponent component) { var query = AllEntityQuery(); while (query.MoveNext(out var cUid, out var comp, out var xform)) { if (xform.ParentUid != uid) continue; return cUid; } return null; } /// /// 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 oder on, so make one // which represents the space we do have left: var reducedOrder = new CargoOrderData(order.OrderId, order.ProductId, 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).Count; return space; } private List<(EntityUid Entity, CargoPalletComponent Component)> GetCargoPallets(EntityUid gridUid) { var pads = new List<(EntityUid, CargoPalletComponent)>(); var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp, out var compXform)) { if (compXform.ParentUid != gridUid || !compXform.Anchored) { continue; } pads.Add((uid, comp)); } return pads; } #endregion #region Station private void OnCargoOrderStartup(EntityUid uid, StationCargoOrderDatabaseComponent component, ComponentStartup args) { if (!_enabled) return; // Stations get created first but if any are added at runtime then do this. AddShuttle(uid, component); } private void AddShuttle(EntityUid stationUid, StationCargoOrderDatabaseComponent component) { Setup(); if (CargoMap == null || component.Shuttle != null || component.CargoShuttleProto == null) { return; } var prototype = _protoMan.Index(component.CargoShuttleProto); var possibleNames = _protoMan.Index(prototype.NameDataset).Values; var name = _random.Pick(possibleNames); if (!_map.TryLoad(CargoMap.Value, prototype.Path.ToString(), out var gridList)) { _sawmill.Error($"Could not load the cargo shuttle!"); return; } var shuttleUid = gridList[0]; var xform = Transform(shuttleUid); MetaData(shuttleUid).EntityName = name; // TODO: Something better like a bounds check. _transform.SetLocalPosition(xform, xform.LocalPosition + 100 * _index); var comp = EnsureComp(shuttleUid); comp.Station = stationUid; component.Shuttle = shuttleUid; UpdateCargoShuttleConsoles(comp); _index++; _sawmill.Info($"Added cargo shuttle to {ToPrettyString(shuttleUid)}"); } private void SellPallets(EntityUid gridUid, out double amount) { GetPalletGoods(gridUid, out var toSell, out amount); _sawmill.Debug($"Cargo sold {toSell.Count} entities for {amount}"); foreach (var ent in toSell) { Del(ent); } } private void GetPalletGoods(EntityUid gridUid, out HashSet toSell, out double amount) { amount = 0; var xformQuery = GetEntityQuery(); var blacklistQuery = GetEntityQuery(); var mobStateQuery = GetEntityQuery(); toSell = new HashSet(); foreach (var (palletUid, _) in GetCargoPallets(gridUid)) { // Containers should already get the sell price of their children so can skip those. foreach (var ent in _lookup.GetEntitiesIntersecting(palletUid, LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Approximate)) { // 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, mobStateQuery)) { 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, EntityQuery mobStateQuery) { if (mobStateQuery.TryGetComponent(uid, out var mobState) && mobState.CurrentState != MobState.Dead) { return false; } return true; } private void AddCargoContents(EntityUid shuttleUid, CargoShuttleComponent shuttle, StationCargoOrderDatabaseComponent orderDatabase) { var xformQuery = GetEntityQuery(); var pads = GetCargoPallets(shuttleUid); while (pads.Count > 0) { var coordinates = new EntityCoordinates(shuttleUid, xformQuery.GetComponent(_random.PickAndTake(pads).Entity).LocalPosition); if(!FulfillOrder(orderDatabase, coordinates, shuttle.PrinterOutput)) { break; } } } private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletSellMessage args) { var player = args.Session.AttachedEntity; if (player == null) return; var bui = _uiSystem.GetUi(uid, CargoPalletConsoleUiKey.Sale); if (Transform(uid).GridUid is not EntityUid gridUid) { _uiSystem.SetUiState(bui, new CargoPalletConsoleInterfaceState(0, 0, false)); return; } SellPallets(gridUid, out var price); var stackPrototype = _prototypeManager.Index(component.CashType); _stack.Spawn((int)price, stackPrototype, uid.ToCoordinates()); UpdatePalletConsoleInterface(uid); } private void OnCargoFTLStarted(EntityUid uid, CargoShuttleComponent component, ref FTLStartedEvent args) { var stationUid = component.Station; // Called if (CargoMap == null || args.FromMapUid != _mapManager.GetMapEntityId(CargoMap.Value) || !TryComp(stationUid, out var orderDatabase)) { return; } AddCargoContents(uid, component, orderDatabase); UpdateOrders(orderDatabase); UpdateCargoShuttleConsoles(component); } private void OnCargoFTLCompleted(EntityUid uid, CargoShuttleComponent component, ref FTLCompletedEvent args) { var xform = Transform(uid); // Recalled if (xform.MapID != CargoMap) return; var stationUid = component.Station; if (TryComp(stationUid, out var bank)) { SellPallets(uid, out var amount); bank.Balance += (int) amount; } } #endregion private void OnRoundRestart(RoundRestartCleanupEvent ev) { CleanupShuttle(); } private void CleanupShuttle() { if (CargoMap == null || !_mapManager.MapExists(CargoMap.Value)) { CargoMap = null; DebugTools.Assert(!EntityQuery().Any()); return; } _mapManager.DeleteMap(CargoMap.Value); CargoMap = null; // Shuttle may not have been in the cargo dimension (e.g. on the station map) so need to delete. var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp)) { if (TryComp(comp.Station, out var station)) { station.Shuttle = null; } QueueDel(uid); } } private void Setup() { if (!_enabled || CargoMap != null && _mapManager.MapExists(CargoMap.Value)) { return; } // It gets mapinit which is okay... buuutt we still want it paused to avoid power draining. CargoMap = _mapManager.CreateMap(); var mapUid = _mapManager.GetMapEntityId(CargoMap.Value); var ftl = EnsureComp(_mapManager.GetMapEntityId(CargoMap.Value)); ftl.Whitelist = new EntityWhitelist() { Components = new[] { _factory.GetComponentName(typeof(CargoShuttleComponent)) } }; MetaData(mapUid).EntityName = $"Trading post {_random.Next(1000):000}"; var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var comp)) { AddShuttle(uid, comp); } _console.RefreshShuttleConsoles(); } }