using System.Linq; using Content.Server._NF.Market.Components; using Content.Server._NF.Market.Extensions; using Content.Server.Cargo.Systems; using Content.Server.Storage.Components; using Content.Shared._NF.Market; using Content.Shared._NF.Market.BUI; using Content.Shared._NF.Market.Events; using Content.Shared._NF.Bank.Components; using Content.Shared.Containers.ItemSlots; using Content.Shared.Power; using Content.Shared.Stacks; using Content.Shared.Storage; using Content.Shared.Materials; using Robust.Shared.Prototypes; namespace Content.Server._NF.Market.Systems; public sealed partial class MarketSystem { [Dependency] private readonly SharedMaterialStorageSystem _sharedMaterialStorageSystem = default!; private void InitializeConsole() { SubscribeLocalEvent(OnEntitySoldEvent); SubscribeLocalEvent(OnConsoleUiOpened); SubscribeLocalEvent(OnCartMessage); SubscribeLocalEvent(OnPowerChanged); } private void OnPowerChanged(EntityUid uid, MarketConsoleComponent component, ref PowerChangedEvent args) { if (args.Powered) return; _ui.CloseUi(uid, MarketConsoleUiKey.Default); } /// /// This event signifies that something has been sold at a cargo pallet. /// /// The details of the event private void OnEntitySoldEvent(ref EntitySoldEvent entitySoldEvent) { var station = _station.GetOwningStation(entitySoldEvent.Grid); if (station is null || !_entityManager.TryGetComponent(station, out var market)) { return; } foreach (var sold in entitySoldEvent.Sold) { if (_entityManager.TryGetComponent(sold, out var materialStorageComponent)) UpsertMaterialStorage(market, materialStorageComponent, sold); else if (_entityManager.TryGetComponent(sold, out var storageComponent)) UpsertStorage(market, storageComponent); else if (_entityManager.TryGetComponent(sold, out var entityStorageComponent)) UpsertEntityStorage(market, entityStorageComponent); else if (_entityManager.TryGetComponent(sold, out var itemSlotsComponent)) UpsertItemSlots(market, itemSlotsComponent); UpsertMetadata(market, sold); } } private void UpsertMetadata(CargoMarketDataComponent marketDataComponent, EntityUid sold) { // Get the MetaDataComponent from the sold entity if (!_entityManager.TryGetComponent(sold, out var metaDataComponent)) return; // Get the prototype ID of the sold entity if (metaDataComponent.EntityPrototype == null) return; var count = 1; var entityPrototype = metaDataComponent.EntityPrototype; string? stackPrototypeId = null; // Get amount of items in the stack if it's a stackable item. // If it's a stackable item, get the singular item id instead. if (_entityManager.TryGetComponent(sold, out var stackComponent)) { count = stackComponent.Count; stackPrototypeId = stackComponent.StackTypeId; var singularId = _prototypeManager.Index(stackComponent.StackTypeId).Spawn.Id; _prototypeManager.TryIndex(singularId, out entityPrototype); } // If this is null, probably couldnt find the stack type id. if (entityPrototype == null) return; // Check whitelist/blacklist for particular prototype if (_whitelistSystem.IsWhitelistPassOrNull(marketDataComponent.Whitelist, sold) && _whitelistSystem.IsBlacklistFailOrNull(marketDataComponent.Blacklist, sold) || _whitelistSystem.IsWhitelistPassOrNull(marketDataComponent.WhitelistOverride, sold)) { var estimatedPrice = _pricingSystem.GetPrice(sold) / count; // Increase the count in the MarketData for this entity // Assuming the quantity to increase is 1 for each sold entity marketDataComponent.MarketDataList.Upsert(entityPrototype.ID, count, estimatedPrice, stackPrototypeId); } } /// /// Recursively updates or inserts market data for entities contained within an EntityStorageComponent. /// /// The MarketDataComponent to update. /// The EntityStorageComponent containing entities to process. private void UpsertEntityStorage(CargoMarketDataComponent marketDataComponent, EntityStorageComponent entityStorageComponent) { foreach (var entityUid in entityStorageComponent.Contents.ContainedEntities) { if (_entityManager.TryGetComponent(entityUid, out var storageComponent)) { UpsertStorage(marketDataComponent, storageComponent); } else if (_entityManager.TryGetComponent(entityUid, out var nestedEntityStorageComponent)) { UpsertEntityStorage(marketDataComponent, nestedEntityStorageComponent); } UpsertMetadata(marketDataComponent, entityUid); } } /// /// Recursively updates or inserts market data for entities contained within an ItemSlotsComponent. /// /// The MarketDataComponent to update. /// The ItemSlotsComponent containing item slots to process. private void UpsertItemSlots(CargoMarketDataComponent marketDataComponent, ItemSlotsComponent itemSlotsComponent) { foreach (var slot in itemSlotsComponent.Slots.Values) { if (slot.Item is not { Valid: true } entityUid) continue; if (_entityManager.TryGetComponent(entityUid, out var storageComponent)) { UpsertStorage(marketDataComponent, storageComponent); } else if (_entityManager.TryGetComponent(entityUid, out var entityStorageComponent)) { UpsertEntityStorage(marketDataComponent, entityStorageComponent); } UpsertMetadata(marketDataComponent, entityUid); } } /// /// Recursively checks the contents of the storage. /// /// /// private void UpsertStorage(CargoMarketDataComponent marketDataComponent, StorageComponent storageComponent) { foreach (var entityUid in storageComponent.Container.ContainedEntities.ToArray()) { if (_entityManager.TryGetComponent(entityUid, out var comp)) UpsertStorage(marketDataComponent, comp); UpsertMetadata(marketDataComponent, entityUid); } } /// /// Inserts market data for all materials contained within a MaterialStorageComponent. /// /// /// private void UpsertMaterialStorage(CargoMarketDataComponent marketDataComponent, MaterialStorageComponent materialStorageComponent, EntityUid sold) { foreach (var (materialProto, amount) in materialStorageComponent.Storage) { if (!_prototypeManager.TryIndex(materialProto, out var material)) { Log.Error("Failed to index material prototype " + materialProto); continue; } if (amount <= 0 || material.StackEntity == null) continue; var entProto = _prototypeManager.Index(material.StackEntity); if (!entProto.TryGetComponent(out var composition)) continue; var materialPerStack = composition.MaterialComposition[material.ID]; var amountToSpawn = amount / materialPerStack; var price = material.Price * materialPerStack; if (amountToSpawn == 0) continue; var overflowMaterial = amount - amountToSpawn * materialPerStack; _sharedMaterialStorageSystem.TrySetMaterialAmount(sold, materialProto, overflowMaterial, materialStorageComponent); // Increase the count in the MarketData for this material marketDataComponent.MarketDataList.Upsert(entProto.ID, amountToSpawn, price, material.StackEntity); } } /// /// Calculates the total number of entities in the market data list, taking into account the maximum stack count for stackable items. /// /// The list of market data to calculate the total entity count from. /// The total number of entities in the market data list. public int CalculateEntityAmount(List marketDataList) { var count = 0; foreach (var data in marketDataList) { if (data.StackPrototype != null && _prototypeManager.TryIndex(data.StackPrototype, out var stackPrototype)) { var maxStackCount = stackPrototype.MaxCount; if (maxStackCount != null) count += (int)Math.Ceiling((double)data.Quantity / int.Max(1, maxStackCount.Value)); // Ensure denominator is positive else count += 1; } else { count += 1; } } return count; } /// /// Calculates the amount of items that can fit within an entity's worth of space for a given item type. /// /// The item type to calculate. /// The number of items that can fit within an entity's worth of space. Null if infinite. public int? GetAmountPerEntitySpace(MarketData data) { if (data.StackPrototype != null && _prototypeManager.TryIndex(data.StackPrototype, out var stackPrototype)) { var maxStackCount = stackPrototype.MaxCount; if (maxStackCount != null) return int.Max(1, maxStackCount.Value); // Ensure amount is positive. else return null; // Infinity. } else { return 1; } } /// /// Occurs whenever something is added to the cart. /// If args.Amount is too high it will automatically be clamped to the maximum amount possible. /// This also prevents desync if there are two different consoles. /// /// The uuid of the console where it was added. /// The console component /// The arguments for the cart event private void OnCartMessage( EntityUid consoleUid, MarketConsoleComponent consoleComponent, ref MarketConsoleCartMessage args ) { if (args.Actor is not { Valid: true } player) return; if (!TryComp(player, out var bank)) return; var marketMultiplier = 1.0f; if (TryComp(consoleUid, out var priceMod)) { marketMultiplier = priceMod.Mod; } // Try to get the EntityPrototype that matches marketData.Prototype if (!_prototypeManager.TryIndex(args.ItemPrototype!, out var prototype)) { return; // Skip this iteration if the prototype was not found } // No data set for market data, can't update cart, no data. var stationUid = _station.GetOwningStation(consoleUid); if (!TryComp(stationUid, out var market)) return; var marketData = market.MarketDataList; if (args.RemoveFromCart) { consoleComponent.CartDataList.Move(marketData, prototype.ID); } else { var maxQuantityToWithdraw = marketData.GetMaxQuantityToWithdraw(prototype); var toWithdraw = MathHelper.Clamp(args.Amount, 1, maxQuantityToWithdraw); var existing = FindMarketDataByPrototype(marketData, args.ItemPrototype!); if (existing == null) return; // Calculate maximum we can fit. var entityAmount = CalculateEntityAmount(consoleComponent.CartDataList); var amountPerEntity = GetAmountPerEntitySpace(existing); int amountLeft; if (amountPerEntity == null) { amountLeft = int.MaxValue; // Infinite stack, infinite space. } else { amountLeft = (30 - entityAmount) * amountPerEntity.Value; var existingCart = FindMarketDataByPrototype(consoleComponent.CartDataList, args.ItemPrototype!); if (existingCart != null) { // Find if there's a partially filled entity in the cart. var quantityMod = existingCart.Quantity % amountPerEntity.Value; if (quantityMod != 0) { amountLeft += amountPerEntity.Value - quantityMod; } } amountLeft = int.Max(0, amountLeft); // If we're over the limit as-is, don't move anything. } toWithdraw = int.Min(toWithdraw, amountLeft); marketData.Upsert(existing.Prototype, -toWithdraw, existing.Price, existing.StackPrototype); consoleComponent.CartDataList.Upsert(existing.Prototype, toWithdraw, existing.Price, existing.StackPrototype); } RefreshState( consoleUid, bank.Balance, marketMultiplier, consoleComponent ); } /// /// Finds a MarketData item in the list that has the same prototype. /// /// The list of market data to search in. /// The prototype ID to search for. /// The MarketData item with the matching prototype, or null if not found. public MarketData? FindMarketDataByPrototype(List marketDataList, string prototypeId) { foreach (var marketData in marketDataList) { if (marketData.Prototype == prototypeId) { return marketData; } } return null; } private void OnConsoleUiOpened(EntityUid uid, MarketConsoleComponent component, BoundUIOpenedEvent args) { if (args.Actor is not { Valid: true } player) return; if (!TryComp(player, out var bank)) return; var marketMultiplier = 1.0f; if (TryComp(uid, out var priceMod)) { marketMultiplier = priceMod.Mod; } RefreshState(uid, bank.Balance, marketMultiplier, component); } private void RefreshState( EntityUid consoleUid, int balance, float marketMultiplier, MarketConsoleComponent? component ) { if (!Resolve(consoleUid, ref component)) return; // Ensures that when this console is no longer attached to a grid and is powered somehow, it won't work. if (Transform(consoleUid).GridUid == null) return; // Get the market data for this grid. var cartData = component.CartDataList; var marketData = new List(); // Get station and the market data attached to it. var consoleStationUid = _station.GetOwningStation(consoleUid); if (TryComp(consoleStationUid, out var market)) { marketData = market.MarketDataList; } var cartBalance = MarketDataExtensions.GetMarketValue(cartData, marketMultiplier); var newState = new MarketConsoleInterfaceState( balance, marketMultiplier, marketData, cartData, cartBalance, true, // TODO add enable/disable functionality component.TransactionCost, CalculateEntityAmount(cartData) ); _ui.SetUiState(consoleUid, MarketConsoleUiKey.Default, newState); } }