Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/GameSession/CargoManager.cs
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 
7 namespace Barotrauma
8 {
9  partial class CargoManager
10  {
11  private List<SoldEntity> SoldEntities { get; } = new List<SoldEntity>();
12 
13  // The bag slot is intentionally left out since we want to be able to sell items from there
14  private static readonly HashSet<InvSlotType> equipmentSlots = new HashSet<InvSlotType>()
15  {
16  InvSlotType.Head,
17  InvSlotType.InnerClothes,
18  InvSlotType.OuterClothes,
19  InvSlotType.Headset,
20  InvSlotType.Card,
21  InvSlotType.HealthInterface
22  };
23 
24  public IEnumerable<Item> GetSellableItems(Character character)
25  {
26  if (character == null) { return new List<Item>(); }
27  var confirmedSoldEntities = GetConfirmedSoldEntities();
28  return character.Inventory.FindAllItems(item =>
29  {
30  if (!IsItemSellable(item, confirmedSoldEntities)) { return false; }
31  // Item must be in a non-equipment slot if possible
32  if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; }
33  // Item must not be contained inside an item in an equipment slot
34  if (item.RootContainer is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; }
35  return true;
36  }, recursive: true).Distinct();
37 
38  bool IsInEquipmentSlot(Item item)
39  {
40  foreach (InvSlotType slot in equipmentSlots)
41  {
42  if (character.Inventory.IsInLimbSlot(item, slot)) { return true; }
43  }
44  return false;
45  }
46  }
47 
48  private IEnumerable<SoldEntity> GetConfirmedSoldEntities()
49  {
50  // Only consider items which have been:
51  // a) sold in singleplayer or confirmed by server (SellStatus.Confirmed); or
52  // b) sold locally in multiplayer (SellStatus.Local), but the client has not received a campaing state update yet after selling them
53  return SoldEntities.Where(se => se.Status != SoldEntity.SellStatus.Unconfirmed);
54  }
55 
56  public void SetItemsInBuyCrate(Dictionary<Identifier, List<PurchasedItem>> items)
57  {
58  ItemsInBuyCrate.Clear();
59  foreach (var entry in items)
60  {
61  ItemsInBuyCrate.Add(entry.Key, entry.Value);
62  }
63  OnItemsInBuyCrateChanged?.Invoke(this);
64  }
65 
66  public void SetItemsInSubSellCrate(Dictionary<Identifier, List<PurchasedItem>> items)
67  {
69  foreach (var entry in items)
70  {
71  ItemsInSellFromSubCrate.Add(entry.Key, entry.Value);
72  }
74  }
75 
76  public void SetSoldItems(Dictionary<Identifier, List<SoldItem>> items)
77  {
78  if (SoldItems.Count == 0 && items.Count == 0) { return; }
79 
80  SoldItems.Clear();
81  foreach (var entry in items)
82  {
83  SoldItems.Add(entry.Key, entry.Value);
84  }
85  foreach (var se in SoldEntities)
86  {
87  if (se.Status == SoldEntity.SellStatus.Confirmed) { continue; }
88  if (SoldItems.Any(si => si.Value.Any(si => Match(si, se, true))))
89  {
90  se.Status = SoldEntity.SellStatus.Confirmed;
91  }
92  else
93  {
94  se.Status = SoldEntity.SellStatus.Unconfirmed;
95  }
96  }
97  foreach (var soldItems in SoldItems.Values)
98  {
99  foreach (var si in soldItems)
100  {
101  if (si.Origin != SoldItem.SellOrigin.Submarine) { continue; }
102  if (!(SoldEntities.FirstOrDefault(se => se.Item == null && Match(si, se, false)) is SoldEntity soldEntityMatch)) { continue; }
103  if (!(Entity.FindEntityByID(si.ID) is Item item)) { continue; }
104  soldEntityMatch.SetItem(item);
105  soldEntityMatch.Status = SoldEntity.SellStatus.Confirmed;
106  }
107  }
108  OnSoldItemsChanged?.Invoke(this);
109 
110  static bool Match(SoldItem soldItem, SoldEntity soldEntity, bool matchId)
111  {
112  if (soldItem.ItemPrefab != soldEntity.ItemPrefab) { return false; }
113  if (matchId && (soldEntity.Item == null || soldItem.ID != soldEntity.Item.ID)) { return false; }
114  if (soldItem.Origin == SoldItem.SellOrigin.Character && GameMain.Client != null && soldItem.SellerID != GameMain.Client.SessionId) { return false; }
115  return true;
116  }
117  }
118 
119  public void ModifyItemQuantityInSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity)
120  {
121  if (GetSellCrateItem(storeIdentifier, itemPrefab) is { } item)
122  {
123  item.Quantity += changeInQuantity;
124  if (item.Quantity < 1)
125  {
126  GetSellCrateItems(storeIdentifier)?.Remove(item);
127  }
128  }
129  else if (changeInQuantity > 0)
130  {
131  GetSellCrateItems(storeIdentifier, create: true).Add(new PurchasedItem(itemPrefab, changeInQuantity));
132  }
133  OnItemsInSellCrateChanged?.Invoke(this);
134  }
135 
136  public void SellItems(Identifier storeIdentifier, List<PurchasedItem> itemsToSell, Store.StoreTab sellingMode)
137  {
138  IEnumerable<Item> sellableItems;
139  try
140  {
141  sellableItems = sellingMode switch
142  {
145  _ => throw new NotImplementedException()
146  };
147  }
148  catch (NotImplementedException e)
149  {
150  DebugConsole.LogError($"Error selling items: unknown store tab type \"{sellingMode}\".\n{e.StackTrace.CleanupStackTrace()}");
151  return;
152  }
153  bool canAddToRemoveQueue = campaign.IsSinglePlayer && Entity.Spawner != null;
154  byte sellerId = GameMain.Client?.SessionId ?? 0;
155  // Check all the prices before starting the transaction to make sure the modifiers stay the same for the whole transaction
156  var sellValues = GetSellValuesAtCurrentLocation(storeIdentifier, itemsToSell.Select(i => i.ItemPrefab));
157  if (!(Location.GetStore(storeIdentifier) is { } store))
158  {
159  DebugConsole.LogError($"Error selling items at {Location}: no store with identifier \"{storeIdentifier}\" exists.\n{Environment.StackTrace.CleanupStackTrace()}");
160  return;
161  }
162  var storeSpecificSoldItems = GetSoldItems(storeIdentifier, create: true);
163  foreach (var item in itemsToSell)
164  {
165  int itemValue = item.Quantity * sellValues[item.ItemPrefab];
166  // check if the store can afford the item
167  if (store.Balance < itemValue) { continue; }
168  // TODO: Write logic for prioritizing certain items over others (e.g. lone Battery Cell should be preferred over one inside a Stun Baton)
169  var matchingItems = sellableItems.Where(i => i.Prefab == item.ItemPrefab);
170  int count = Math.Min(item.Quantity, matchingItems.Count());
171  SoldItem.SellOrigin origin = sellingMode == Store.StoreTab.Sell ? SoldItem.SellOrigin.Character : SoldItem.SellOrigin.Submarine;
172  if (origin == SoldItem.SellOrigin.Character || GameMain.IsSingleplayer)
173  {
174  for (int i = 0; i < count; i++)
175  {
176  var matchingItem = matchingItems.ElementAt(i);
177  storeSpecificSoldItems.Add(new SoldItem(matchingItem.Prefab, matchingItem.ID, canAddToRemoveQueue, sellerId, origin));
178  SoldEntities.Add(new SoldEntity(matchingItem, campaign.IsSinglePlayer ? SoldEntity.SellStatus.Confirmed : SoldEntity.SellStatus.Local));
179  if (canAddToRemoveQueue) { Entity.Spawner.AddItemToRemoveQueue(matchingItem); }
180  }
181  }
182  else
183  {
184  // When selling from the sub in multiplayer, the server will determine the items that are sold
185  for (int i = 0; i < count; i++)
186  {
187  storeSpecificSoldItems.Add(new SoldItem(item.ItemPrefab, Entity.NullEntityID, canAddToRemoveQueue, sellerId, origin));
188  SoldEntities.Add(new SoldEntity(item.ItemPrefab, SoldEntity.SellStatus.Local));
189  }
190  }
191  // Exchange money
192  store.Balance -= itemValue;
194  {
195  campaign.Bank.Give(itemValue);
196  }
197  GameAnalyticsManager.AddMoneyGainedEvent(itemValue, GameAnalyticsManager.MoneySource.Store, item.ItemPrefab.Identifier.Value);
198 
199  // Remove from the sell crate
200  var sellCrate = (sellingMode == Store.StoreTab.Sell ? GetSellCrateItems(storeIdentifier) : GetSubCrateItems(storeIdentifier));
201  if (sellCrate?.Find(pi => pi.ItemPrefab == item.ItemPrefab) is { } itemToSell)
202  {
203  itemToSell.Quantity -= item.Quantity;
204  if (itemToSell.Quantity < 1)
205  {
206  sellCrate.Remove(itemToSell);
207  }
208  }
209  }
210  OnSoldItemsChanged?.Invoke(this);
211  }
212 
214  {
215  SoldItems.Clear();
216  SoldEntities.Clear();
217  }
218  }
219 }
List< PurchasedItem > GetSubCrateItems(Identifier identifier, bool create=false)
List< PurchasedItem > GetSellCrateItems(Identifier identifier, bool create=false)
void SetSoldItems(Dictionary< Identifier, List< SoldItem >> items)
void ModifyItemQuantityInSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity)
void SetItemsInSubSellCrate(Dictionary< Identifier, List< PurchasedItem >> items)
Dictionary< Identifier, List< PurchasedItem > > ItemsInSellFromSubCrate
void SellItems(Identifier storeIdentifier, List< PurchasedItem > itemsToSell, Store.StoreTab sellingMode)
Dictionary< Identifier, List< PurchasedItem > > ItemsInBuyCrate
readonly NamedEvent< CargoManager > OnItemsInSellFromSubCrateChanged
PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab)
List< SoldItem > GetSoldItems(Identifier identifier, bool create=false)
IEnumerable< Item > GetSellableItems(Character character)
Dictionary< Identifier, List< SoldItem > > SoldItems
Dictionary< ItemPrefab, int > GetSellValuesAtCurrentLocation(Identifier storeIdentifier, IEnumerable< ItemPrefab > items)
void SetItemsInBuyCrate(Dictionary< Identifier, List< PurchasedItem >> items)
static EntitySpawner Spawner
Definition: Entity.cs:31
const ushort NullEntityID
Definition: Entity.cs:14
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
static bool IsSingleplayer
Definition: GameMain.cs:34
static GameClient Client
Definition: GameMain.cs:188
List< Item > FindAllItems(Func< Item, bool > predicate=null, bool recursive=false, List< Item > list=null)
ItemPrefab(ContentXElement element, ItemFile file)
StoreInfo GetStore(Identifier identifier)
Definition: Location.cs:1299