using Barotrauma.Extensions; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class CargoManager { private class SoldEntity { public enum SellStatus { /// /// Entity sold in SP. Or, entity sold by client and confirmed by server in MP. /// Confirmed, /// /// Entity sold by client in MP. Client has received at least one update from server after selling, but this entity wasn't yet confirmed. /// Unconfirmed, /// /// Entity sold by client in MP. Client hasn't yet received an update from server after selling. /// Local } public Item Item { get; } public SellStatus Status { get; set; } private SoldEntity(Item item, SellStatus status) { Item = item; Status = status; } public static SoldEntity CreateInSinglePlayer(Item item) => new SoldEntity(item, SellStatus.Confirmed); public static SoldEntity CreateInMultiPlayer(Item item) => new SoldEntity(item, SellStatus.Local); } private List SoldEntities { get; } = new List(); public List GetSellableItems(Character character) { if (character == null) { return new List(); } // 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 var soldEntities = SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed); var sellables = Item.ItemList.FindAll(i => i?.Prefab != null && !i.Removed && i.GetRootInventoryOwner() == character && !i.SpawnedInOutpost && (i.ContainedItems == null || i.ContainedItems.None() || i.ContainedItems.All(ci => soldEntities.Any(se => se.Item == ci))) && (i.Condition >= 0.9f * i.MaxCondition || i.Prefab.AllowSellingWhenBroken) && soldEntities.None(se => se.Item == i)); // Prevent selling items in equipment slots var equipmentSlots = new List() { InvSlotType.Head, InvSlotType.InnerClothes, InvSlotType.OuterClothes, InvSlotType.Headset, InvSlotType.Card }; foreach (InvSlotType slot in equipmentSlots) { var index = character.Inventory.FindLimbSlot(slot); if (character.Inventory.Items[index] is Item item) { // Don't prevent selling of items which can only be put in equipment slots (like diving suits) if (item.AllowedSlots.Contains(InvSlotType.Any)) { sellables.Remove(item); } } } // Prevent selling items contained inside equipped items foreach (InvSlotType slot in equipmentSlots) { var index = character.Inventory.FindLimbSlot(slot); if (character.Inventory.Items[index] is Item item && item.ContainedItems != null && item.AllowedSlots.Contains(InvSlotType.Any)) { RemoveContainedFromSellables(item); } } void RemoveContainedFromSellables(Item item) { foreach (Item containedItem in item.ContainedItems) { if (containedItem == null) { continue; } if (containedItem.ContainedItems != null) { RemoveContainedFromSellables(containedItem); } sellables.Remove(containedItem); } } return sellables; } public void SetItemsInBuyCrate(List items) { ItemsInBuyCrate.Clear(); ItemsInBuyCrate.AddRange(items); OnItemsInBuyCrateChanged?.Invoke(); } public void SetSoldItems(List items) { SoldItems.Clear(); SoldItems.AddRange(items); foreach (SoldEntity se in SoldEntities) { if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; } if (SoldItems.Any(si => si.ID == se.Item.ID && si.ItemPrefab == se.Item.Prefab && (GameMain.Client == null || GameMain.Client.ID == si.SellerID))) { se.Status = SoldEntity.SellStatus.Confirmed; } else { se.Status = SoldEntity.SellStatus.Unconfirmed; } } OnSoldItemsChanged?.Invoke(); } public void ModifyItemQuantityInSellCrate(ItemPrefab itemPrefab, int changeInQuantity) { PurchasedItem 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 SellItems(List itemsToSell) { var itemsInInventory = GetSellableItems(Character.Controlled); var canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null; var sellerId = GameMain.Client?.ID ?? 0; foreach (PurchasedItem item in itemsToSell) { var itemValue = GetSellValueAtCurrentLocation(item.ItemPrefab, quantity: item.Quantity); // check if the store can afford the item if (Location.StoreCurrentBalance < itemValue) { continue; } var matchingItems = itemsInInventory.FindAll(i => i.Prefab == item.ItemPrefab); if (matchingItems.Count <= item.Quantity) { foreach (Item i in matchingItems) { SoldItems.Add(new SoldItem(i.Prefab, i.ID, canAddToRemoveQueue, sellerId)); SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(i) : SoldEntity.CreateInMultiPlayer(i)); if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(i); } } } else { for (int i = 0; i < item.Quantity; i++) { var matchingItem = matchingItems[i]; SoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId)); SoldEntities.Add(campaign.IsSinglePlayer ? SoldEntity.CreateInSinglePlayer(matchingItem) : SoldEntity.CreateInMultiPlayer(matchingItem)); if (canAddToRemoveQueue) { Entity.Spawner.AddToRemoveQueue(matchingItem); } } } // Exchange money Location.StoreCurrentBalance -= itemValue; campaign.Money += itemValue; // Remove from the sell crate if (ItemsInSellCrate.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell) { itemToSell.Quantity -= item.Quantity; if (itemToSell.Quantity < 1) { ItemsInSellCrate.Remove(itemToSell); } } } OnSoldItemsChanged?.Invoke(); } public void ClearSoldItemsProjSpecific() { SoldItems.Clear(); SoldEntities.Clear(); } } }