using Barotrauma.Extensions; using Barotrauma.Items.Components; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class CargoManager { private List SoldEntities { get; } = new List(); // The bag slot is intentionally left out since we want to be able to sell items from there private readonly List equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; public IEnumerable GetSellableItems(Character character) { if (character == null) { return new List(); } var confirmedSoldEntities = GetConfirmedSoldEntities(); return character.Inventory.FindAllItems(item => { if (!IsItemSellable(item, confirmedSoldEntities)) { return false; } // Item must be in a non-equipment slot if possible if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; } // Item must not be contained inside an item in an equipment slot if (item.GetRootContainer() is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; } return true; }, recursive: true).Distinct(); bool IsInEquipmentSlot(Item item) { foreach (InvSlotType slot in equipmentSlots) { if (character.Inventory.IsInLimbSlot(item, slot)) { return true; } } return false; } } private IEnumerable GetConfirmedSoldEntities() { // Only consider items which have been: // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or // b) sold locally in multiplayer (SellStatus.Local), but the client has not received a campaing state update yet after selling them return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); } public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); ItemsInBuyCrate.AddRange(items); OnItemsInBuyCrateChanged?.Invoke(); } public void SetItemsInSubSellCrate(List items) { ItemsInSellFromSubCrate.Clear(); ItemsInSellFromSubCrate.AddRange(items); OnItemsInSellFromSubCrateChanged?.Invoke(); } public void SetSoldItems(List items) { SoldItems.Clear(); SoldItems.AddRange(items); foreach (var se in SoldEntities) { if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } if (SoldItems.Any(si => Match(si, se, true))) { se.Status = SoldEntity.SellStatus.Confirmed; } else { se.Status = SoldEntity.SellStatus.Unconfirmed; } } foreach (var si in SoldItems) { if (si.Origin != SoldItem.SellOrigin.Submarine) { continue; } if (!(SoldEntities.FirstOrDefault(se => se.Item == null && Match(si, se, false)) is SoldEntity soldEntityMatch)) { continue; } if (!(Entity.FindEntityByID(si.ID) is Item item)) { continue; } soldEntityMatch.SetItem(item); soldEntityMatch.Status = SoldEntity.SellStatus.Confirmed; } OnSoldItemsChanged?.Invoke(); static bool Match(SoldItem soldItem, SoldEntity soldEntity, bool matchId) { if (soldItem.ItemPrefab != soldEntity.ItemPrefab) { return false; } if (matchId && (soldEntity.Item == null || soldItem.ID != soldEntity.Item.ID)) { return false; } if (soldItem.Origin == SoldItem.SellOrigin.Character && GameMain.Client != null && soldItem.SellerID != GameMain.Client.ID) { return false; } return true; } } public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) { var itemToSell = ItemsInSellCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemToSell != null) { itemToSell.Quantity += changeInQuantity; if (itemToSell.Quantity < 1) { ItemsInSellCrate.Remove(itemToSell); } } else if (changeInQuantity > 0) { itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); ItemsInSellCrate.Add(itemToSell); } OnItemsInSellCrateChanged?.Invoke(); } public void ModifyItemQuantityInSellFromSubCrate(ItemPrefab itemPrefab, int changeInQuantity) { var itemToSell = ItemsInSellFromSubCrate.Find(i => i.ItemPrefab == itemPrefab); if (itemToSell != null) { itemToSell.Quantity += changeInQuantity; if (itemToSell.Quantity < 1) { ItemsInSellFromSubCrate.Remove(itemToSell); } } else if (changeInQuantity > 0) { itemToSell = new PurchasedItem(itemPrefab, changeInQuantity); ItemsInSellFromSubCrate.Add(itemToSell); } OnItemsInSellFromSubCrateChanged?.Invoke(); } public void SellItems(List itemsToSell, Store.StoreTab sellingMode) { IEnumerable sellableItems; try { sellableItems = sellingMode switch { Store.StoreTab.Sell => GetSellableItems(Character.Controlled), Store.StoreTab.SellSub => GetSellableItemsFromSub(), _ => throw new NotImplementedException() }; } catch (NotImplementedException e) { DebugConsole.ShowError($"Error selling items: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}"); return; } bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; byte sellerId = GameMain.Client?.ID ?? 0; // Check all the prices before starting the transaction // to make sure the modifiers stay the same for the whole transaction Dictionary sellValues = GetSellValuesAtCurrentLocation(itemsToSell.Select(i => i.ItemPrefab)); foreach (PurchasedItem item in itemsToSell) { int itemValue = item.Quantity * sellValues[item.ItemPrefab]; // check if the store can afford the item if (Location.StoreCurrentBalance < itemValue) { continue; } // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton) var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab); int count = Math.Min(item.Quantity, matchingItems.Count()); SoldItem.SellOrigin origin = sellingMode == Store.StoreTab.Sell ? SoldItem.SellOrigin.Character : SoldItem.SellOrigin.Submarine; if (origin == SoldItem.SellOrigin.Character || GameMain.IsSingleplayer) { for (int i = 0; i < count; i++) { var matchingItem = matchingItems.ElementAt(i); SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local)); if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } } } else { // When selling from the sub in multiplayer, the server will determine the items that are sold for (int i = 0; i < count; i++) { SoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin)); SoldEntities.Add(new SoldEntity(item.ItemPrefab, SoldEntity.SellStatus.Local)); } } // Exchange money Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier); // Remove from the sell crate if ((sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) { itemToSell.Quantity -= item.Quantity; if (itemToSell.Quantity < 1) { (sellingMode == Store.StoreTab.Sell ? ItemsInSellCrate : ItemsInSellFromSubCrate)?.Remove(itemToSell); } } } OnSoldItemsChanged?.Invoke(); } public void ClearSoldItemsProjSpecific() { SoldItems.Clear(); SoldEntities.Clear(); } } }