Client LuaCsForBarotrauma
Store.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Diagnostics;
7 using System.Globalization;
8 using System.Linq;
9 using PlayerBalanceElement = Barotrauma.CampaignUI.PlayerBalanceElement;
10 
11 namespace Barotrauma
12 {
13  class Store
14  {
15  class ItemQuantity
16  {
17  public int Total { get; private set; }
18  public int NonEmpty { get; private set; }
19  public bool AllNonEmpty => NonEmpty == Total;
20 
21  public ItemQuantity(int total, bool areNonEmpty = true)
22  {
23  Total = total;
24  NonEmpty = areNonEmpty ? total : 0;
25  }
26 
27  public void Add(int amount, bool areNonEmpty)
28  {
29  Total += amount;
30  if (areNonEmpty) { NonEmpty += amount; }
31  }
32  }
33 
34  private readonly CampaignUI campaignUI;
35  private readonly GUIComponent parentComponent;
36  private readonly List<GUIButton> storeTabButtons = new List<GUIButton>();
37  private readonly List<GUIButton> itemCategoryButtons = new List<GUIButton>();
38  private readonly Dictionary<StoreTab, GUIListBox> tabLists = new Dictionary<StoreTab, GUIListBox>();
39  private readonly Dictionary<StoreTab, SortingMethod> tabSortingMethods = new Dictionary<StoreTab, SortingMethod>();
40  private readonly List<PurchasedItem> itemsToSell = new List<PurchasedItem>();
41  private readonly List<PurchasedItem> itemsToSellFromSub = new List<PurchasedItem>();
42 
43  private GUIMessageBox deliveryPrompt;
44 
45  private StoreTab activeTab = StoreTab.Buy;
46  private MapEntityCategory? selectedItemCategory;
47  private bool suppressBuySell;
48  private int buyTotal, sellTotal, sellFromSubTotal;
49 
50  private GUITextBlock storeNameBlock;
51  private GUITextBlock reputationEffectBlock;
52  private GUIDropDown sortingDropDown;
53  private GUITextBox searchBox;
54  private GUILayoutGroup categoryButtonContainer;
55  private GUIListBox storeBuyList, storeSellList, storeSellFromSubList;
59  private GUILayoutGroup storeDailySpecialsGroup, storeRequestedGoodGroup, storeRequestedSubGoodGroup;
60  private Color storeSpecialColor;
61 
62  private GUIListBox shoppingCrateBuyList, shoppingCrateSellList, shoppingCrateSellFromSubList;
63  private GUITextBlock relevantBalanceName, shoppingCrateTotal;
64  private GUIButton clearAllButton, confirmButton;
65 
66  private bool needsRefresh, needsBuyingRefresh, needsSellingRefresh, needsItemsToSellRefresh, needsSellingFromSubRefresh, needsItemsToSellFromSubRefresh;
67 
68  private Point resolutionWhenCreated;
69 
70  private PlayerBalanceElement? playerBalanceElement;
71 
72  private Dictionary<ItemPrefab, ItemQuantity> OwnedItems { get; } = new Dictionary<ItemPrefab, ItemQuantity>();
73  private Location.StoreInfo ActiveStore { get; set; }
74 
75  private CargoManager CargoManager => campaignUI.Campaign.CargoManager;
76  private Location CurrentLocation => campaignUI.Campaign.Map?.CurrentLocation;
77  private int Balance => campaignUI.Campaign.GetBalance();
78 
79  private bool IsBuying => activeTab switch
80  {
81  StoreTab.Buy => true,
82  StoreTab.Sell => false,
83  StoreTab.SellSub => false,
84  _ => throw new NotImplementedException()
85  };
86  private GUIListBox ActiveShoppingCrateList => activeTab switch
87  {
88  StoreTab.Buy => shoppingCrateBuyList,
89  StoreTab.Sell => shoppingCrateSellList,
90  StoreTab.SellSub => shoppingCrateSellFromSubList,
91  _ => throw new NotImplementedException()
92  };
93 
94  public enum StoreTab
95  {
99  Buy,
103  Sell,
107  SellSub
108  }
109 
110  private enum SortingMethod
111  {
112  AlphabeticalAsc,
113  AlphabeticalDesc,
114  PriceAsc,
115  PriceDesc,
116  CategoryAsc
117  }
118 
119  #region Permissions
120 
121  private bool hadBuyPermissions, hadSellInventoryPermissions, hadSellSubPermissions;
122 
123  private bool HasBuyPermissions
124  {
125  get => HasPermissionToUseTab(StoreTab.Buy);
126  set => hadBuyPermissions = value;
127  }
128  private bool HasSellInventoryPermissions
129  {
130  get => HasPermissionToUseTab(StoreTab.Sell);
131  set => hadSellInventoryPermissions = value;
132  }
133  private bool HasSellSubPermissions
134  {
135  get => HasPermissionToUseTab(StoreTab.SellSub);
136  set => hadSellSubPermissions = value;
137  }
138 
139  private static bool HasPermissionToUseTab(StoreTab tab)
140  {
141  return tab switch
142  {
143  StoreTab.Buy => true,
144  StoreTab.Sell => CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.SellInventoryItems),
145  StoreTab.SellSub => CampaignMode.AllowedToManageCampaign(Networking.ClientPermissions.SellSubItems),
146  _ => false,
147  };
148  }
149 
150  private void UpdatePermissions()
151  {
152  HasBuyPermissions = HasPermissionToUseTab(StoreTab.Buy);
153  HasSellInventoryPermissions = HasPermissionToUseTab(StoreTab.Sell);
154  HasSellSubPermissions = HasPermissionToUseTab(StoreTab.SellSub);
155  }
156 
157  private bool HasTabPermissions(StoreTab tab)
158  {
159  return tab switch
160  {
161  StoreTab.Buy => HasBuyPermissions,
162  StoreTab.Sell => HasSellInventoryPermissions,
163  StoreTab.SellSub => HasSellSubPermissions,
164  _ => false
165  };
166  }
167 
168  private bool HasActiveTabPermissions()
169  {
170  return HasTabPermissions(activeTab);
171  }
172 
173  private bool HavePermissionsChanged(StoreTab tab)
174  {
175  bool hadTabPermissions = tab switch
176  {
177  StoreTab.Buy => hadBuyPermissions,
178  StoreTab.Sell => hadSellInventoryPermissions,
179  StoreTab.SellSub => hadSellSubPermissions,
180  _ => false
181  };
182  return hadTabPermissions != HasTabPermissions(tab);
183  }
184 
185  #endregion
186 
187  public Store(CampaignUI campaignUI, GUIComponent parentComponent)
188  {
189  this.campaignUI = campaignUI;
190  this.parentComponent = parentComponent;
191  UpdatePermissions();
192  CreateUI();
193  Identifier refreshStoreId = new Identifier("RefreshStore");
194  campaignUI.Campaign.Map.OnLocationChanged.RegisterOverwriteExisting(
195  refreshStoreId,
196  (locationChangeInfo) => UpdateLocation(locationChangeInfo.PrevLocation, locationChangeInfo.NewLocation));
197 
198  CurrentLocation?.Reputation?.OnReputationValueChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsRefresh = true);
199  CargoManager cargoManager = campaignUI.Campaign.CargoManager;
200  cargoManager.OnItemsInBuyCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsBuyingRefresh = true);
201  cargoManager.OnPurchasedItemsChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsRefresh = true);
202  cargoManager.OnItemsInSellCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsSellingRefresh = true);
203  cargoManager.OnSoldItemsChanged.RegisterOverwriteExisting(refreshStoreId, _ =>
204  {
205  needsItemsToSellRefresh = true;
206  needsItemsToSellFromSubRefresh = true;
207  needsRefresh = true;
208  });
209  cargoManager.OnItemsInSellFromSubCrateChanged.RegisterOverwriteExisting(refreshStoreId, _ => needsSellingFromSubRefresh = true);
210  }
211 
212  public void SelectStore(Character merchant)
213  {
214  Identifier storeIdentifier = merchant?.MerchantIdentifier ?? Identifier.Empty;
215  if (CurrentLocation?.Stores != null)
216  {
217  if (!storeIdentifier.IsEmpty && CurrentLocation.GetStore(storeIdentifier) is { } store)
218  {
219  ActiveStore = store;
220  if (storeNameBlock != null)
221  {
222  var storeName = TextManager.Get($"storename.{store.Identifier}");
223  if (storeName.IsNullOrEmpty())
224  {
225  storeName = TextManager.Get("store");
226  }
227  storeNameBlock.SetRichText(storeName);
228  }
229  ActiveStore.SetMerchantFaction(merchant.Faction);
230  }
231  else
232  {
233  ActiveStore = null;
234  string errorId, msg;
235  if (storeIdentifier.IsEmpty)
236  {
237  errorId = "Store.SelectStore:IdentifierEmpty";
238  msg = $"Error selecting store at {CurrentLocation}: identifier is empty.";
239  }
240  else
241  {
242  errorId = "Store.SelectStore:StoreDoesntExist";
243  msg = $"Error selecting store with identifier \"{storeIdentifier}\" at {CurrentLocation}: store with the identifier doesn't exist at the location.";
244  }
245  DebugConsole.LogError(msg);
246  GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg);
247  }
248  }
249  else
250  {
251  ActiveStore = null;
252  string errorId = "", msg = "";
253  if (campaignUI.Campaign.Map == null)
254  {
255  errorId = "Store.SelectStore:MapNull";
256  msg = $"Error selecting store with identifier \"{storeIdentifier}\": Map is null.";
257  }
258  else if (CurrentLocation == null)
259  {
260  errorId = "Store.SelectStore:CurrentLocationNull";
261  msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation is null.";
262  }
263  else if (CurrentLocation.Stores == null)
264  {
265  errorId = "Store.SelectStore:StoresNull";
266  msg = $"Error selecting store with identifier \"{storeIdentifier}\": CurrentLocation.Stores is null.";
267  }
268  if (!msg.IsNullOrEmpty())
269  {
270  DebugConsole.LogError(msg);
271  GameAnalyticsManager.AddErrorEventOnce(errorId, GameAnalyticsManager.ErrorSeverity.Error, msg);
272  }
273  }
275  Refresh();
276  }
277 
278  public void Refresh(bool updateOwned = true)
279  {
280  UpdatePermissions();
281  if (updateOwned) { UpdateOwnedItems(); }
282  RefreshBuying(updateOwned: false);
283  RefreshSelling(updateOwned: false);
284  RefreshSellingFromSub(updateOwned: false);
285  SetConfirmButtonBehavior();
286  needsRefresh = false;
287  }
288 
289  private void RefreshBuying(bool updateOwned = true)
290  {
291  if (updateOwned) { UpdateOwnedItems(); }
292  RefreshShoppingCrateBuyList();
293  RefreshStoreBuyList();
294  bool hasPermissions = HasTabPermissions(StoreTab.Buy);
295  storeBuyList.Enabled = hasPermissions;
296  shoppingCrateBuyList.Enabled = hasPermissions;
297  needsBuyingRefresh = false;
298  }
299 
300  private void RefreshSelling(bool updateOwned = true)
301  {
302  if (updateOwned) { UpdateOwnedItems(); }
303  RefreshShoppingCrateSellList();
304  RefreshStoreSellList();
305  bool hasPermissions = HasTabPermissions(StoreTab.Sell);
306  storeSellList.Enabled = hasPermissions;
307  shoppingCrateSellList.Enabled = hasPermissions;
308  needsSellingRefresh = false;
309  }
310 
311  private void RefreshSellingFromSub(bool updateOwned = true, bool updateItemsToSellFromSub = true)
312  {
313  if (updateOwned) { UpdateOwnedItems(); }
314  if (updateItemsToSellFromSub) RefreshItemsToSellFromSub();
315  RefreshShoppingCrateSellFromSubList();
316  RefreshStoreSellFromSubList();
317  bool hasPermissions = HasTabPermissions(StoreTab.SellSub);
318  storeSellFromSubList.Enabled = hasPermissions;
319  shoppingCrateSellFromSubList.Enabled = hasPermissions;
320  needsSellingFromSubRefresh = false;
321  }
322 
323  private void CreateUI()
324  {
325  if (parentComponent.FindChild(c => c.UserData as string == "glow") is GUIComponent glowChild)
326  {
327  parentComponent.RemoveChild(glowChild);
328  }
329  if (parentComponent.FindChild(c => c.UserData as string == "container") is GUIComponent containerChild)
330  {
331  parentComponent.RemoveChild(containerChild);
332  }
333 
334  new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), parentComponent.RectTransform, Anchor.Center), style: "OuterGlow", color: Color.Black * 0.7f)
335  {
336  CanBeFocused = false,
337  UserData = "glow"
338  };
339  new GUIFrame(new RectTransform(new Vector2(0.95f), parentComponent.RectTransform, anchor: Anchor.Center), style: null)
340  {
341  CanBeFocused = false,
342  UserData = "container"
343  };
344 
345  var panelMaxWidth = (int)(GUI.xScale * (GUI.HorizontalAspectRatio < 1.4f ? 650 : 560));
346  var storeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, Anchor.BottomLeft)
347  {
348  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height - HUDLayoutSettings.ButtonAreaTop.Bottom)
349  })
350  {
351  Stretch = true,
352  RelativeSpacing = 0.01f
353  };
354 
355  // Store header ------------------------------------------------
356  var headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.95f / 14.0f), storeContent.RectTransform), isHorizontal: true)
357  {
358  RelativeSpacing = 0.005f
359  };
360  var imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width;
361  new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreTradingIcon");
362  storeNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("store"), font: GUIStyle.LargeFont)
363  {
364  CanBeFocused = false,
366  };
367 
368  // Merchant balance ------------------------------------------------
369  var balanceAndValueGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), storeContent.RectTransform), isHorizontal: true)
370  {
371  RelativeSpacing = 0.005f
372  };
373 
374  var merchantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform))
375  {
376  RelativeSpacing = 0.005f
377  };
378  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform),
379  TextManager.Get("campaignstore.storebalance"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft)
380  {
381  AutoScaleVertical = true,
383  };
384  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), merchantBalanceContainer.RectTransform), "",
385  color: Color.White, font: GUIStyle.SubHeadingFont)
386  {
387  AutoScaleVertical = true,
388  TextScale = 1.1f,
389  TextGetter = () => GetMerchantBalanceText()
390  };
391 
392  // Item sell value ------------------------------------------------
393  var reputationEffectContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), balanceAndValueGroup.RectTransform))
394  {
395  CanBeFocused = true,
396  RelativeSpacing = 0.005f,
397  ToolTip = TextManager.Get("campaignstore.reputationtooltip")
398  };
399  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform),
400  TextManager.Get("reputationmodifier"), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft)
401  {
402  AutoScaleVertical = true,
403  CanBeFocused = false,
405  };
406  reputationEffectBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), reputationEffectContainer.RectTransform), "", font: GUIStyle.SubHeadingFont)
407  {
408  AutoScaleVertical = true,
409  CanBeFocused = false,
410  TextScale = 1.1f,
411  TextGetter = () =>
412  {
413  if (ActiveStore is not null)
414  {
415  Color textColor = GUIStyle.ColorReputationNeutral;
416  string sign = "";
417  int reputationModifier = (int)MathF.Round((ActiveStore.GetReputationModifier(activeTab == StoreTab.Buy) - 1) * 100);
418  if (reputationModifier > 0)
419  {
420  textColor = IsBuying ? GUIStyle.ColorReputationLow : GUIStyle.ColorReputationHigh;
421  sign = "+";
422  }
423  else if (reputationModifier < 0)
424  {
425  textColor = IsBuying ? GUIStyle.ColorReputationHigh : GUIStyle.ColorReputationLow;
426  }
427  reputationEffectBlock.TextColor = textColor;
428  return $"{sign}{reputationModifier}%";
429  }
430  else
431  {
432  return "";
433  }
434  }
435  };
436 
437  // Store mode buttons ------------------------------------------------
438  var modeButtonFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f / 14.0f), storeContent.RectTransform), style: null);
439  var modeButtonContainer = new GUILayoutGroup(new RectTransform(Vector2.One, modeButtonFrame.RectTransform), isHorizontal: true);
440 
441  var tabs = Enum.GetValues(typeof(StoreTab));
442  storeTabButtons.Clear();
443  tabSortingMethods.Clear();
444  foreach (StoreTab tab in tabs)
445  {
446  LocalizedString text = tab switch
447  {
448  StoreTab.SellSub => TextManager.Get("submarine"),
449  _ => TextManager.Get("campaignstoretab." + tab)
450  };
451  var tabButton = new GUIButton(new RectTransform(new Vector2(1.0f / (tabs.Length + 1), 1.0f), modeButtonContainer.RectTransform),
452  text: text, style: "GUITabButton")
453  {
454  UserData = tab,
455  OnClicked = (button, userData) =>
456  {
457  ChangeStoreTab((StoreTab)userData);
458  return true;
459  }
460  };
461  storeTabButtons.Add(tabButton);
462  tabSortingMethods.Add(tab, SortingMethod.AlphabeticalAsc);
463  }
464 
465  var storeInventoryContainer = new GUILayoutGroup(
466  new RectTransform(
467  new Vector2(0.9f, 0.95f),
468  new GUIFrame(new RectTransform(new Vector2(1.0f, 11.9f / 14.0f), storeContent.RectTransform)).RectTransform,
469  anchor: Anchor.Center),
470  isHorizontal: true)
471  {
472  RelativeSpacing = 0.015f,
473  Stretch = true
474  };
475 
476  // Item category buttons ------------------------------------------------
477  categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.08f, 1.0f), storeInventoryContainer.RectTransform))
478  {
479  RelativeSpacing = 0.02f
480  };
481 
482  List<MapEntityCategory> itemCategories = Enum.GetValues(typeof(MapEntityCategory)).Cast<MapEntityCategory>().ToList();
483  itemCategories.Remove(MapEntityCategory.None);
484  //don't show categories with no buyable items
485  itemCategories.RemoveAll(c => !ItemPrefab.Prefabs.Any(ep => ep.Category.HasFlag(c) && ep.CanBeBought));
486  itemCategoryButtons.Clear();
487  var categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform), style: "CategoryButton.All")
488  {
489  ToolTip = TextManager.Get("MapEntityCategory.All"),
490  OnClicked = OnClickedCategoryButton
491  };
492  itemCategoryButtons.Add(categoryButton);
493  foreach (MapEntityCategory category in itemCategories)
494  {
495  categoryButton = new GUIButton(new RectTransform(new Point(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Width), categoryButtonContainer.RectTransform),
496  style: "CategoryButton." + category)
497  {
498  ToolTip = TextManager.Get("MapEntityCategory." + category),
499  UserData = category,
500  OnClicked = OnClickedCategoryButton
501  };
502  itemCategoryButtons.Add(categoryButton);
503  }
504  bool OnClickedCategoryButton(GUIButton button, object userData)
505  {
506  MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null;
507  if (newCategory.HasValue) { searchBox.Text = ""; }
508  if (newCategory != selectedItemCategory) { tabLists[activeTab].ScrollBar.BarScroll = 0f; }
509  FilterStoreItems(newCategory, searchBox.Text);
510  return true;
511  }
512  foreach (var btn in itemCategoryButtons)
513  {
514  btn.RectTransform.SizeChanged += () =>
515  {
516  if (btn.Frame.sprites == null) { return; }
517  var sprite = btn.Frame.sprites[GUIComponent.ComponentState.None].First();
518  btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width)));
519  };
520  }
521 
522  GUILayoutGroup sortFilterListContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.92f, 1.0f), storeInventoryContainer.RectTransform))
523  {
524  RelativeSpacing = 0.015f,
525  Stretch = true
526  };
527  GUILayoutGroup sortFilterGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), sortFilterListContainer.RectTransform), isHorizontal: true)
528  {
529  RelativeSpacing = 0.015f,
530  Stretch = true
531  };
532 
533  GUILayoutGroup sortGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), sortFilterGroup.RectTransform))
534  {
535  Stretch = true
536  };
537  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby"));
538  sortingDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.5f), sortGroup.RectTransform), text: TextManager.Get("campaignstore.sortby"), elementCount: 3)
539  {
540  OnSelected = (child, userData) =>
541  {
542  SortActiveTabItems((SortingMethod)userData);
543  return true;
544  }
545  };
546  var tag = "sortingmethod.";
547  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.AlphabeticalAsc), userData: SortingMethod.AlphabeticalAsc);
548  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceAsc), userData: SortingMethod.PriceAsc);
549  sortingDropDown.AddItem(TextManager.Get(tag + SortingMethod.PriceDesc), userData: SortingMethod.PriceDesc);
550 
551  GUILayoutGroup filterGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1.0f), sortFilterGroup.RectTransform))
552  {
553  Stretch = true
554  };
555  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), filterGroup.RectTransform), TextManager.Get("serverlog.filter"));
556  searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), filterGroup.RectTransform), createClearButton: true);
557  searchBox.OnTextChanged += (textBox, text) => { FilterStoreItems(null, text); return true; };
558 
559  var storeItemListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.92f), sortFilterListContainer.RectTransform), style: null);
560  tabLists.Clear();
561 
562  storeBuyList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform))
563  {
564  AutoHideScrollBar = false,
565  Visible = false
566  };
567  storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, CurrentLocation?.DailySpecialsCount ?? 1);
568  tabLists.Add(StoreTab.Buy, storeBuyList);
569 
570  storeSellList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform))
571  {
572  AutoHideScrollBar = false,
573  Visible = false
574  };
575  storeRequestedGoodGroup = CreateDealsGroup(storeSellList, CurrentLocation?.RequestedGoodsCount ?? 1);
576  tabLists.Add(StoreTab.Sell, storeSellList);
577 
578  storeSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, storeItemListContainer.RectTransform))
579  {
580  AutoHideScrollBar = false,
581  Visible = false
582  };
583  storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, CurrentLocation?.RequestedGoodsCount ?? 1);
584  tabLists.Add(StoreTab.SellSub, storeSellFromSubList);
585 
586  // Shopping Crate ------------------------------------------------------------------------------------------------------------------------------------------
587 
588  var shoppingCrateContent = new GUILayoutGroup(new RectTransform(new Vector2(0.45f, 1.0f), campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).RectTransform, anchor: Anchor.BottomRight)
589  {
590  MaxSize = new Point(panelMaxWidth, campaignUI.GetTabContainer(CampaignMode.InteractionType.Store).Rect.Height - HUDLayoutSettings.ButtonAreaTop.Bottom)
591  })
592  {
593  Stretch = true,
594  RelativeSpacing = 0.01f
595  };
596 
597  // Shopping crate header ------------------------------------------------
598  headerGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.75f / 14.0f), shoppingCrateContent.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight)
599  {
600  RelativeSpacing = 0.005f
601  };
602  imageWidth = (float)headerGroup.Rect.Height / headerGroup.Rect.Width;
603  new GUIImage(new RectTransform(new Vector2(imageWidth, 1.0f), headerGroup.RectTransform), "StoreShoppingCrateIcon");
604  new GUITextBlock(new RectTransform(new Vector2(1.0f - imageWidth, 1.0f), headerGroup.RectTransform), TextManager.Get("campaignstore.shoppingcrate"), font: GUIStyle.LargeFont, textAlignment: Alignment.Right)
605  {
606  CanBeFocused = false,
608  };
609 
610  // Player balance ------------------------------------------------
611  playerBalanceElement = CampaignUI.AddBalanceElement(shoppingCrateContent, new Vector2(1.0f, 0.75f / 14.0f));
612 
613  // Divider ------------------------------------------------
614  var dividerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.6f / 14.0f), shoppingCrateContent.RectTransform), style: null);
615  new GUIImage(new RectTransform(Vector2.One, dividerFrame.RectTransform, anchor: Anchor.BottomCenter), "HorizontalLine");
616 
617  var shoppingCrateInventoryContainer = new GUILayoutGroup(
618  new RectTransform(
619  new Vector2(0.9f, 0.95f),
620  new GUIFrame(new RectTransform(new Vector2(1.0f, 11.9f / 14.0f), shoppingCrateContent.RectTransform)).RectTransform,
621  anchor: Anchor.Center))
622  {
623  RelativeSpacing = 0.015f,
624  Stretch = true
625  };
626  var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null);
627  shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
628  shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
629  shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
630 
631  var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true)
632  {
633  Stretch = true
634  };
635  relevantBalanceName = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", font: GUIStyle.Font)
636  {
637  CanBeFocused = false
638  };
639  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), relevantBalanceContainer.RectTransform), "", textColor: Color.White, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right)
640  {
641  CanBeFocused = false,
642  TextScale = 1.1f,
643  TextGetter = () => IsBuying ? CampaignUI.GetTotalBalance() : GetMerchantBalanceText()
644  };
645 
646  var totalContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true)
647  {
648  Stretch = true
649  };
650  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), TextManager.Get("campaignstore.total"), font: GUIStyle.Font)
651  {
652  CanBeFocused = false
653  };
654  shoppingCrateTotal = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), totalContainer.RectTransform), "", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right)
655  {
656  CanBeFocused = false,
657  TextScale = 1.1f
658  };
659 
660  var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.TopRight);
661  confirmButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform))
662  {
664  };
665  SetConfirmButtonBehavior();
666  clearAllButton = new GUIButton(new RectTransform(new Vector2(0.35f, 1.0f), buttonContainer.RectTransform), TextManager.Get("campaignstore.clearall"))
667  {
668  ClickSound = GUISoundType.Cart,
669  Enabled = HasActiveTabPermissions(),
671  OnClicked = (button, userData) =>
672  {
673  if (!HasActiveTabPermissions()) { return false; }
674  var itemsToRemove = activeTab switch
675  {
676  StoreTab.Buy => new List<PurchasedItem>(CargoManager.GetBuyCrateItems(ActiveStore)),
677  StoreTab.Sell => new List<PurchasedItem>(CargoManager.GetSellCrateItems(ActiveStore)),
678  StoreTab.SellSub => new List<PurchasedItem>(CargoManager.GetSubCrateItems(ActiveStore)),
679  _ => throw new NotImplementedException(),
680  };
681  itemsToRemove.ForEach(i => ClearFromShoppingCrate(i));
682  return true;
683  }
684  };
685 
686  ChangeStoreTab(activeTab);
687  resolutionWhenCreated = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
688  }
689 
690  private LocalizedString GetMerchantBalanceText() => TextManager.FormatCurrency(ActiveStore?.Balance ?? 0);
691 
692  private GUILayoutGroup CreateDealsGroup(GUIListBox parentList, int elementCount)
693  {
694  // Add 1 for the header
695  elementCount++;
696  var elementHeight = (int)(GUI.yScale * 80);
697  var frame = new GUIFrame(new RectTransform(new Point(parentList.Content.Rect.Width, elementCount * elementHeight + 3), parent: parentList.Content.RectTransform), style: null)
698  {
699  UserData = "deals"
700  };
701  var dealsGroup = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform, anchor: Anchor.Center), childAnchor: Anchor.TopCenter);
702  var dealsHeader = new GUILayoutGroup(new RectTransform(new Point((int)(0.95f * parentList.Content.Rect.Width), elementHeight), parent: dealsGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
703  {
704  UserData = "header"
705  };
706  var iconWidth = (0.9f * dealsHeader.Rect.Height) / dealsHeader.Rect.Width;
707  var dealsIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 0.9f), dealsHeader.RectTransform), "StoreDealIcon", scaleToFit: true);
708  var text = TextManager.Get(parentList == storeBuyList ? "campaignstore.dailyspecials" : "campaignstore.requestedgoods");
709  var dealsText = new GUITextBlock(new RectTransform(new Vector2(1.0f - iconWidth, 0.9f), dealsHeader.RectTransform), text, font: GUIStyle.LargeFont);
710  storeSpecialColor = dealsIcon.Color;
711  dealsText.TextColor = storeSpecialColor;
712  var divider = new GUIImage(new RectTransform(new Point(dealsGroup.Rect.Width, 3), dealsGroup.RectTransform), "HorizontalLine")
713  {
714  UserData = "divider"
715  };
716  frame.CanBeFocused = dealsGroup.CanBeFocused = dealsHeader.CanBeFocused = dealsIcon.CanBeFocused = dealsText.CanBeFocused = divider.CanBeFocused = false;
717  return dealsGroup;
718  }
719 
720  private void UpdateLocation(Location prevLocation, Location newLocation)
721  {
722  if (prevLocation == newLocation) { return; }
723  if (prevLocation?.Reputation != null)
724  {
725  prevLocation.Reputation.OnReputationValueChanged.Dispose();
726  }
727  if (ItemPrefab.Prefabs.Any(p => p.CanBeBoughtFrom(newLocation)))
728  {
729  selectedItemCategory = null;
730  searchBox.Text = "";
731  ChangeStoreTab(StoreTab.Buy);
732  if (newLocation?.Reputation != null)
733  {
734  newLocation.Reputation.OnReputationValueChanged.RegisterOverwriteExisting("RefreshStore".ToIdentifier(), _ => { SetNeedsRefresh(); });
735  }
736  }
737 
738  void SetNeedsRefresh()
739  {
740  needsRefresh = true;
741  }
742  }
743 
744  private void UpdateCategoryButtons()
745  {
746  var tabItems = activeTab switch
747  {
748  StoreTab.Buy => ActiveStore?.Stock,
749  StoreTab.Sell => itemsToSell,
750  StoreTab.SellSub => itemsToSellFromSub,
751  _ => null
752  } ?? Enumerable.Empty<PurchasedItem>();
753  foreach (var button in itemCategoryButtons)
754  {
755  if (button.UserData is not MapEntityCategory category)
756  {
757  continue;
758  }
759  bool isButtonEnabled = false;
760  foreach (var item in tabItems)
761  {
762  if (item.ItemPrefab.Category.HasFlag(category))
763  {
764  isButtonEnabled = true;
765  break;
766  }
767  }
768  button.Enabled = isButtonEnabled;
769  }
770  }
771 
772  private void ChangeStoreTab(StoreTab tab)
773  {
774  activeTab = tab;
775  foreach (GUIButton tabButton in storeTabButtons)
776  {
777  tabButton.Selected = (StoreTab)tabButton.UserData == activeTab;
778  }
779  sortingDropDown.SelectItem(tabSortingMethods[tab]);
780  relevantBalanceName.Text = IsBuying ? TextManager.Get("campaignstore.balance") : TextManager.Get("campaignstore.storebalance");
781  UpdateCategoryButtons();
782  SetShoppingCrateTotalText();
783  SetClearAllButtonStatus();
784  SetConfirmButtonBehavior();
785  SetConfirmButtonStatus();
786  FilterStoreItems();
787  switch (tab)
788  {
789  case StoreTab.Buy:
790  storeSellList.Visible = false;
791  if (storeSellFromSubList != null)
792  {
793  storeSellFromSubList.Visible = false;
794  }
795  storeBuyList.Visible = true;
796  shoppingCrateSellList.Visible = false;
797  if (shoppingCrateSellFromSubList != null)
798  {
799  shoppingCrateSellFromSubList.Visible = false;
800  }
801  shoppingCrateBuyList.Visible = true;
802  break;
803  case StoreTab.Sell:
804  storeBuyList.Visible = false;
805  if (storeSellFromSubList != null)
806  {
807  storeSellFromSubList.Visible = false;
808  }
809  storeSellList.Visible = true;
810  shoppingCrateBuyList.Visible = false;
811  if (shoppingCrateSellFromSubList != null)
812  {
813  shoppingCrateSellFromSubList.Visible = false;
814  }
815  shoppingCrateSellList.Visible = true;
816  break;
817  case StoreTab.SellSub:
818  storeBuyList.Visible = false;
819  storeSellList.Visible = false;
820  if (storeSellFromSubList != null)
821  {
822  storeSellFromSubList.Visible = true;
823  }
824  shoppingCrateBuyList.Visible = false;
825  shoppingCrateSellList.Visible = false;
826  if (shoppingCrateSellFromSubList != null)
827  {
828  shoppingCrateSellFromSubList.Visible = true;
829  }
830  break;
831  }
832  }
833 
834  private void FilterStoreItems(MapEntityCategory? category, string filter)
835  {
836  selectedItemCategory = category;
837  var list = tabLists[activeTab];
838  filter = filter?.ToLower();
839  foreach (GUIComponent child in list.Content.Children)
840  {
841  var item = child.UserData as PurchasedItem;
842  if (item?.ItemPrefab?.Name == null) { continue; }
843  child.Visible =
844  (IsBuying || item.Quantity > 0) &&
845  (!category.HasValue || item.ItemPrefab.Category.HasFlag(category.Value)) &&
846  (string.IsNullOrEmpty(filter) || item.ItemPrefab.Name.Contains(filter, StringComparison.OrdinalIgnoreCase));
847  }
848  foreach (GUIButton btn in itemCategoryButtons)
849  {
850  btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory;
851  }
852  list.UpdateScrollBarSize();
853  }
854 
855  private void FilterStoreItems()
856  {
857  //only select a specific category if the search box is empty (items from all categories are shown when searching)
858  MapEntityCategory? category = string.IsNullOrEmpty(searchBox.Text) ? selectedItemCategory : null;
859  FilterStoreItems(category, searchBox.Text);
860  }
861 
862  private static KeyValuePair<Identifier, float>? GetReputationRequirement(PriceInfo priceInfo)
863  {
864  return GameMain.GameSession?.Campaign is not null
865  ? priceInfo.MinReputation.FirstOrNull()
866  : null;
867  }
868 
869  private static KeyValuePair<Identifier, float>? GetTooLowReputation(PriceInfo priceInfo)
870  {
871  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
872  {
873  foreach (var minRep in priceInfo.MinReputation)
874  {
875  if (MathF.Round(campaign.GetReputation(minRep.Key)) < minRep.Value)
876  {
877  return minRep;
878  }
879  }
880  }
881  return null;
882  }
883 
884  int prevDailySpecialCount, prevRequestedGoodsCount, prevSubRequestedGoodsCount;
885 
886  private void RefreshStoreBuyList()
887  {
888  float prevBuyListScroll = storeBuyList.BarScroll;
889  float prevShoppingCrateScroll = shoppingCrateBuyList.BarScroll;
890 
891  int dailySpecialCount = ActiveStore?.DailySpecials.Count(s => s.CanCharacterBuy()) ?? 0;
892  if ((ActiveStore == null && storeDailySpecialsGroup != null) || (storeDailySpecialsGroup != null) != ActiveStore.DailySpecials.Any() || dailySpecialCount != prevDailySpecialCount)
893  {
894  storeBuyList.RemoveChild(storeDailySpecialsGroup?.Parent);
895  if (ActiveStore != null && (storeDailySpecialsGroup == null || dailySpecialCount != prevDailySpecialCount))
896  {
897  storeDailySpecialsGroup = CreateDealsGroup(storeBuyList, dailySpecialCount);
898  storeDailySpecialsGroup.Parent.SetAsFirstChild();
899  }
900  else
901  {
902  storeDailySpecialsGroup = null;
903  }
904  storeBuyList.RecalculateChildren();
905  prevDailySpecialCount = dailySpecialCount;
906  }
907 
908  bool hasPermissions = HasTabPermissions(StoreTab.Buy);
909  var existingItemFrames = new HashSet<GUIComponent>();
910  if (ActiveStore != null)
911  {
912  foreach (PurchasedItem item in ActiveStore.Stock)
913  {
914  CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
915  }
916  foreach (ItemPrefab itemPrefab in ActiveStore.DailySpecials)
917  {
918  if (ActiveStore.Stock.Any(pi => pi.ItemPrefab == itemPrefab)) { continue; }
919  CreateOrUpdateItemFrame(itemPrefab, 0);
920  }
921  }
922 
923  void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int quantity)
924  {
925  if (itemPrefab.CanBeBoughtFrom(ActiveStore, out PriceInfo priceInfo) && itemPrefab.CanCharacterBuy())
926  {
927  bool isDailySpecial = ActiveStore.DailySpecials.Contains(itemPrefab);
928  var itemFrame = isDailySpecial ?
929  storeDailySpecialsGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) :
930  storeBuyList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab);
931 
932  quantity = Math.Max(quantity - CargoManager.GetPurchasedItemCount(ActiveStore, itemPrefab), 0);
933  if (CargoManager.GetBuyCrateItem(ActiveStore, itemPrefab) is { } buyCrateItem)
934  {
935  quantity = Math.Max(quantity - buyCrateItem.Quantity, 0);
936  }
937  if (itemFrame == null)
938  {
939  var parentComponent = isDailySpecial ? storeDailySpecialsGroup : storeBuyList as GUIComponent;
940  itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, quantity), parentComponent, StoreTab.Buy, forceDisable: !hasPermissions);
941  }
942  else
943  {
944  (itemFrame.UserData as PurchasedItem).Quantity = quantity;
945  SetQuantityLabelText(StoreTab.Buy, itemFrame);
946  SetOwnedText(itemFrame);
947  SetPriceGetters(itemFrame, true);
948  }
949 
950  SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && !GetTooLowReputation(priceInfo).HasValue);
951  existingItemFrames.Add(itemFrame);
952  }
953  }
954 
955  var removedItemFrames = storeBuyList.Content.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList();
956  if (storeDailySpecialsGroup != null)
957  {
958  removedItemFrames.AddRange(storeDailySpecialsGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList());
959  }
960  removedItemFrames.ForEach(f => f.RectTransform.Parent = null);
961  if (activeTab == StoreTab.Buy)
962  {
963  UpdateCategoryButtons();
964  FilterStoreItems();
965  }
966  SortItems(StoreTab.Buy);
967 
968  storeBuyList.BarScroll = prevBuyListScroll;
969  shoppingCrateBuyList.BarScroll = prevShoppingCrateScroll;
970  }
971 
972  private void RefreshStoreSellList()
973  {
974  float prevSellListScroll = storeSellList.BarScroll;
975  float prevShoppingCrateScroll = shoppingCrateSellList.BarScroll;
976 
977  int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
978  if ((ActiveStore == null && storeRequestedGoodGroup != null) || (storeRequestedGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevRequestedGoodsCount)
979  {
980  storeSellList.RemoveChild(storeRequestedGoodGroup?.Parent);
981  if (ActiveStore != null && (storeRequestedGoodGroup == null || requestedGoodsCount != prevRequestedGoodsCount))
982  {
983  storeRequestedGoodGroup = CreateDealsGroup(storeSellList, requestedGoodsCount);
984  storeRequestedGoodGroup.Parent.SetAsFirstChild();
985  }
986  else
987  {
988  storeRequestedGoodGroup = null;
989  }
990  storeSellList.RecalculateChildren();
991  prevRequestedGoodsCount = requestedGoodsCount;
992  }
993 
994  bool hasPermissions = HasTabPermissions(StoreTab.Sell);
995  var existingItemFrames = new HashSet<GUIComponent>();
996  if (ActiveStore != null)
997  {
998  foreach (PurchasedItem item in itemsToSell)
999  {
1000  CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
1001  }
1002  foreach (var requestedGood in ActiveStore.RequestedGoods)
1003  {
1004  if (itemsToSell.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
1005  CreateOrUpdateItemFrame(requestedGood, 0);
1006  }
1007  }
1008 
1009  void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
1010  {
1011  PriceInfo priceInfo = itemPrefab.GetPriceInfo(ActiveStore);
1012  if (priceInfo == null) { return; }
1013  var isRequestedGood = ActiveStore.RequestedGoods.Contains(itemPrefab);
1014  var itemFrame = isRequestedGood ?
1015  storeRequestedGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) :
1016  storeSellList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab);
1017  if (CargoManager.GetSellCrateItem(ActiveStore, itemPrefab) is { } sellCrateItem)
1018  {
1019  itemQuantity = Math.Max(itemQuantity - sellCrateItem.Quantity, 0);
1020  }
1021  if (itemFrame == null)
1022  {
1023  var parentComponent = isRequestedGood ? storeRequestedGoodGroup : storeSellList as GUIComponent;
1024  itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.Sell, forceDisable: !hasPermissions);
1025  }
1026  else
1027  {
1028  (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity;
1029  SetQuantityLabelText(StoreTab.Sell, itemFrame);
1030  SetOwnedText(itemFrame);
1031  SetPriceGetters(itemFrame, false);
1032  }
1033  SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0);
1034  if (itemQuantity < 1 && !isRequestedGood)
1035  {
1036  itemFrame.Visible = false;
1037  }
1038  existingItemFrames.Add(itemFrame);
1039  }
1040 
1041  var removedItemFrames = storeSellList.Content.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList();
1042  if (storeRequestedGoodGroup != null)
1043  {
1044  removedItemFrames.AddRange(storeRequestedGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList());
1045  }
1046  removedItemFrames.ForEach(f => f.RectTransform.Parent = null);
1047 
1048  if (activeTab == StoreTab.Sell)
1049  {
1050  UpdateCategoryButtons();
1051  FilterStoreItems();
1052  }
1053  SortItems(StoreTab.Sell);
1054 
1055  storeSellList.BarScroll = prevSellListScroll;
1056  shoppingCrateSellList.BarScroll = prevShoppingCrateScroll;
1057  }
1058 
1059  private void RefreshStoreSellFromSubList()
1060  {
1061  float prevSellListScroll = storeSellFromSubList.BarScroll;
1062  float prevShoppingCrateScroll = shoppingCrateSellFromSubList.BarScroll;
1063 
1064  int requestedGoodsCount = ActiveStore?.RequestedGoods.Count ?? 0;
1065  if ((ActiveStore == null && storeRequestedSubGoodGroup != null) || (storeRequestedSubGoodGroup != null) != ActiveStore.RequestedGoods.Any() || requestedGoodsCount != prevSubRequestedGoodsCount)
1066  {
1067  storeSellFromSubList.RemoveChild(storeRequestedSubGoodGroup?.Parent);
1068  if (ActiveStore != null && (storeRequestedSubGoodGroup == null || requestedGoodsCount != prevSubRequestedGoodsCount))
1069  {
1070  storeRequestedSubGoodGroup = CreateDealsGroup(storeSellFromSubList, requestedGoodsCount);
1071  storeRequestedSubGoodGroup.Parent.SetAsFirstChild();
1072  }
1073  else
1074  {
1075  storeRequestedSubGoodGroup = null;
1076  }
1077  storeSellFromSubList.RecalculateChildren();
1078  prevSubRequestedGoodsCount = requestedGoodsCount;
1079  }
1080 
1081  bool hasPermissions = HasSellSubPermissions;
1082  var existingItemFrames = new HashSet<GUIComponent>();
1083  if (ActiveStore != null)
1084  {
1085  foreach (PurchasedItem item in itemsToSellFromSub)
1086  {
1087  CreateOrUpdateItemFrame(item.ItemPrefab, item.Quantity);
1088  }
1089  foreach (var requestedGood in ActiveStore.RequestedGoods)
1090  {
1091  if (itemsToSellFromSub.Any(pi => pi.ItemPrefab == requestedGood)) { continue; }
1092  CreateOrUpdateItemFrame(requestedGood, 0);
1093  }
1094  }
1095 
1096  void CreateOrUpdateItemFrame(ItemPrefab itemPrefab, int itemQuantity)
1097  {
1098  PriceInfo priceInfo = itemPrefab.GetPriceInfo(ActiveStore);
1099  if (priceInfo == null) { return; }
1100  bool isRequestedGood = ActiveStore.RequestedGoods.Contains(itemPrefab);
1101  var itemFrame = isRequestedGood ?
1102  storeRequestedSubGoodGroup.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab) :
1103  storeSellFromSubList.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab == itemPrefab);
1104  if (CargoManager.GetSubCrateItem(ActiveStore, itemPrefab) is { } subCrateItem)
1105  {
1106  itemQuantity = Math.Max(itemQuantity - subCrateItem.Quantity, 0);
1107  }
1108  if (itemFrame == null)
1109  {
1110  var parentComponent = isRequestedGood ? storeRequestedSubGoodGroup : storeSellFromSubList as GUIComponent;
1111  itemFrame = CreateItemFrame(new PurchasedItem(itemPrefab, itemQuantity), parentComponent, StoreTab.SellSub, forceDisable: !hasPermissions);
1112  }
1113  else
1114  {
1115  (itemFrame.UserData as PurchasedItem).Quantity = itemQuantity;
1116  SetQuantityLabelText(StoreTab.SellSub, itemFrame);
1117  SetOwnedText(itemFrame);
1118  SetPriceGetters(itemFrame, false);
1119  }
1120  SetItemFrameStatus(itemFrame, hasPermissions && itemQuantity > 0);
1121  if (itemQuantity < 1 && !isRequestedGood)
1122  {
1123  itemFrame.Visible = false;
1124  }
1125  existingItemFrames.Add(itemFrame);
1126  }
1127 
1128  var removedItemFrames = storeSellFromSubList.Content.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList();
1129  if (storeRequestedSubGoodGroup != null)
1130  {
1131  removedItemFrames.AddRange(storeRequestedSubGoodGroup.Children.Where(c => c.UserData is PurchasedItem).Except(existingItemFrames).ToList());
1132  }
1133  removedItemFrames.ForEach(f => f.RectTransform.Parent = null);
1134 
1135  if (activeTab == StoreTab.SellSub)
1136  {
1137  UpdateCategoryButtons();
1138  FilterStoreItems();
1139  }
1140  SortItems(StoreTab.SellSub);
1141 
1142  storeSellFromSubList.BarScroll = prevSellListScroll;
1143  shoppingCrateSellFromSubList.BarScroll = prevShoppingCrateScroll;
1144  }
1145 
1146  private void SetPriceGetters(GUIComponent itemFrame, bool buying)
1147  {
1148  if (itemFrame == null || itemFrame.UserData is not PurchasedItem pi) { return; }
1149 
1150  if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock)
1151  {
1152  if (buying)
1153  {
1154  undiscountedPriceBlock.TextGetter = () => TextManager.FormatCurrency(
1155  ActiveStore?.GetAdjustedItemBuyPrice(pi.ItemPrefab, considerDailySpecials: false) ?? 0);
1156  }
1157  else
1158  {
1159  undiscountedPriceBlock.TextGetter = () => TextManager.FormatCurrency(
1160  ActiveStore?.GetAdjustedItemSellPrice(pi.ItemPrefab, considerRequestedGoods: false) ?? 0);
1161  }
1162  }
1163 
1164  if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock)
1165  {
1166  if (buying)
1167  {
1168  priceBlock.TextGetter = () => TextManager.FormatCurrency(ActiveStore?.GetAdjustedItemBuyPrice(pi.ItemPrefab) ?? 0);
1169  }
1170  else
1171  {
1172  priceBlock.TextGetter = () => TextManager.FormatCurrency(ActiveStore?.GetAdjustedItemSellPrice(pi.ItemPrefab) ?? 0);
1173  }
1174  }
1175  }
1176 
1177  public void RefreshItemsToSell()
1178  {
1179  itemsToSell.Clear();
1180  if (ActiveStore == null) { return; }
1182  foreach (Item playerItem in playerItems)
1183  {
1184  if (itemsToSell.FirstOrDefault(i => i.ItemPrefab == playerItem.Prefab) is PurchasedItem item)
1185  {
1186  item.Quantity += 1;
1187  }
1188  else if (playerItem.Prefab.GetPriceInfo(ActiveStore) != null)
1189  {
1190  itemsToSell.Add(new PurchasedItem(playerItem.Prefab, 1));
1191  }
1192  }
1193 
1194  // Remove items from sell crate if they aren't in player inventory anymore
1195  var itemsInCrate = new List<PurchasedItem>(CargoManager.GetSellCrateItems(ActiveStore));
1196  foreach (PurchasedItem crateItem in itemsInCrate)
1197  {
1198  var playerItem = itemsToSell.Find(i => i.ItemPrefab == crateItem.ItemPrefab);
1199  var playerItemQuantity = playerItem != null ? playerItem.Quantity : 0;
1200  if (crateItem.Quantity > playerItemQuantity)
1201  {
1202  CargoManager.ModifyItemQuantityInSellCrate(ActiveStore.Identifier, crateItem.ItemPrefab, playerItemQuantity - crateItem.Quantity);
1203  }
1204  }
1205  needsItemsToSellRefresh = false;
1206  }
1207 
1209  {
1210  itemsToSellFromSub.Clear();
1211  if (ActiveStore == null) { return; }
1212  var subItems = CargoManager.GetSellableItemsFromSub();
1213  foreach (Item subItem in subItems)
1214  {
1215  if (itemsToSellFromSub.FirstOrDefault(i => i.ItemPrefab == subItem.Prefab) is PurchasedItem item)
1216  {
1217  item.Quantity += 1;
1218  }
1219  else if (subItem.Prefab.GetPriceInfo(ActiveStore) != null)
1220  {
1221  itemsToSellFromSub.Add(new PurchasedItem(subItem.Prefab, 1));
1222  }
1223  }
1224 
1225  // Remove items from sell crate if they aren't on the sub anymore
1226  var itemsInCrate = new List<PurchasedItem>(CargoManager.GetSubCrateItems(ActiveStore));
1227  foreach (PurchasedItem crateItem in itemsInCrate)
1228  {
1229  var subItem = itemsToSellFromSub.Find(i => i.ItemPrefab == crateItem.ItemPrefab);
1230  var subItemQuantity = subItem != null ? subItem.Quantity : 0;
1231  if (crateItem.Quantity > subItemQuantity)
1232  {
1233  CargoManager.ModifyItemQuantityInSubSellCrate(ActiveStore.Identifier, crateItem.ItemPrefab, subItemQuantity - crateItem.Quantity);
1234  }
1235  }
1236  sellableItemsFromSubUpdateTimer = 0.0f;
1237  needsItemsToSellFromSubRefresh = false;
1238  }
1239 
1240  private void RefreshShoppingCrateList(IEnumerable<PurchasedItem> items, GUIListBox listBox, StoreTab tab)
1241  {
1242  bool hasPermissions = HasTabPermissions(tab);
1243  HashSet<GUIComponent> existingItemFrames = new HashSet<GUIComponent>();
1244  int totalPrice = 0;
1245  if (ActiveStore != null)
1246  {
1247  foreach (PurchasedItem item in items.ToList())
1248  {
1249  if (item.ItemPrefab.GetPriceInfo(ActiveStore) is not { } priceInfo) { continue; }
1250  GUINumberInput numInput = null;
1251  if (!(listBox.Content.FindChild(c => c.UserData is PurchasedItem pi && pi.ItemPrefab.Identifier == item.ItemPrefab.Identifier) is { } itemFrame))
1252  {
1253  itemFrame = CreateItemFrame(item, listBox, tab, forceDisable: !hasPermissions);
1254  numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
1255  }
1256  else
1257  {
1258  itemFrame.UserData = item;
1259  numInput = itemFrame.FindChild(c => c is GUINumberInput, recursive: true) as GUINumberInput;
1260  if (numInput != null)
1261  {
1262  numInput.UserData = item;
1263  numInput.Enabled = hasPermissions;
1264  numInput.MaxValueInt = GetMaxAvailable(item.ItemPrefab, tab);
1265  }
1266  SetOwnedText(itemFrame);
1267  SetItemFrameStatus(itemFrame, hasPermissions);
1268  }
1269  existingItemFrames.Add(itemFrame);
1270 
1271  suppressBuySell = true;
1272  if (numInput != null)
1273  {
1274  if (numInput.IntValue != item.Quantity) { itemFrame.Flash(GUIStyle.Green); }
1275  numInput.IntValue = item.Quantity;
1276  }
1277  suppressBuySell = false;
1278 
1279  try
1280  {
1281  int price = tab switch
1282  {
1283  StoreTab.Buy => ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo),
1284  StoreTab.Sell => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
1285  StoreTab.SellSub => ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo),
1286  _ => throw new NotImplementedException()
1287  };
1288  totalPrice += item.Quantity * price;
1289  }
1290  catch (NotImplementedException e)
1291  {
1292  DebugConsole.LogError($"Error getting item price: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
1293  }
1294  }
1295  }
1296 
1297  var removedItemFrames = listBox.Content.Children.Except(existingItemFrames).ToList();
1298  removedItemFrames.ForEach(f => listBox.Content.RemoveChild(f));
1299 
1300  SortItems(listBox, SortingMethod.CategoryAsc);
1301  listBox.UpdateScrollBarSize();
1302  switch (tab)
1303  {
1304  case StoreTab.Buy:
1305  buyTotal = totalPrice;
1306  break;
1307  case StoreTab.Sell:
1308  sellTotal = totalPrice;
1309  break;
1310  case StoreTab.SellSub:
1311  sellFromSubTotal = totalPrice;
1312  break;
1313  }
1314  if (activeTab == tab)
1315  {
1316  SetShoppingCrateTotalText();
1317  }
1318  SetClearAllButtonStatus();
1319  SetConfirmButtonStatus();
1320  }
1321 
1322  private void RefreshShoppingCrateBuyList() => RefreshShoppingCrateList(CargoManager.GetBuyCrateItems(ActiveStore), shoppingCrateBuyList, StoreTab.Buy);
1323 
1324  private void RefreshShoppingCrateSellList() => RefreshShoppingCrateList(CargoManager.GetSellCrateItems(ActiveStore), shoppingCrateSellList, StoreTab.Sell);
1325 
1326  private void RefreshShoppingCrateSellFromSubList() => RefreshShoppingCrateList(CargoManager.GetSubCrateItems(ActiveStore), shoppingCrateSellFromSubList, StoreTab.SellSub);
1327 
1328  private void SortItems(GUIListBox list, SortingMethod sortingMethod)
1329  {
1330  if (CurrentLocation == null || ActiveStore == null) { return; }
1331 
1332  if (sortingMethod == SortingMethod.AlphabeticalAsc || sortingMethod == SortingMethod.AlphabeticalDesc)
1333  {
1334  list.Content.RectTransform.SortChildren(CompareByName);
1335  if (GetSpecialsGroup() is GUILayoutGroup specialsGroup)
1336  {
1337  specialsGroup.RectTransform.SortChildren(CompareByName);
1338  specialsGroup.Recalculate();
1339  }
1340 
1341  int CompareByName(RectTransform x, RectTransform y)
1342  {
1343  if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY)
1344  {
1345  int reputationCompare = CompareByReputationRestriction(itemX, itemY);
1346  if (reputationCompare != 0) { return reputationCompare; }
1347  int sortResult = itemX.ItemPrefab.Name != itemY.ItemPrefab.Name ?
1348  itemX.ItemPrefab.Name.CompareTo(itemY.ItemPrefab.Name) :
1349  itemX.ItemPrefab.Identifier.CompareTo(itemY.ItemPrefab.Identifier);
1350  if (sortingMethod == SortingMethod.AlphabeticalDesc) { sortResult *= -1; }
1351  return sortResult;
1352  }
1353  else
1354  {
1355  return CompareByElement(x, y);
1356  }
1357  }
1358  }
1359  else if (sortingMethod == SortingMethod.PriceAsc || sortingMethod == SortingMethod.PriceDesc)
1360  {
1361  SortItems(list, SortingMethod.AlphabeticalAsc);
1362  if (list != storeBuyList && list != shoppingCrateBuyList)
1363  {
1364  list.Content.RectTransform.SortChildren(CompareBySellPrice);
1365  if (GetSpecialsGroup() is GUILayoutGroup specialsGroup)
1366  {
1367  specialsGroup.RectTransform.SortChildren(CompareBySellPrice);
1368  specialsGroup.Recalculate();
1369  }
1370 
1371  int CompareBySellPrice(RectTransform x, RectTransform y)
1372  {
1373  if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY)
1374  {
1375  int reputationCompare = CompareByReputationRestriction(itemX, itemY);
1376  if (reputationCompare != 0) { return reputationCompare; }
1377  int sortResult = ActiveStore.GetAdjustedItemSellPrice(itemX.ItemPrefab).CompareTo(
1378  ActiveStore.GetAdjustedItemSellPrice(itemY.ItemPrefab));
1379  if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; }
1380  return sortResult;
1381  }
1382  else
1383  {
1384  return CompareByElement(x, y);
1385  }
1386  }
1387  }
1388  else
1389  {
1390  list.Content.RectTransform.SortChildren(CompareByBuyPrice);
1391  if (GetSpecialsGroup() is GUILayoutGroup specialsGroup)
1392  {
1393  specialsGroup.RectTransform.SortChildren(CompareByBuyPrice);
1394  specialsGroup.Recalculate();
1395  }
1396 
1397  int CompareByBuyPrice(RectTransform x, RectTransform y)
1398  {
1399  if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY)
1400  {
1401  int reputationCompare = CompareByReputationRestriction(itemX, itemY);
1402  if (reputationCompare != 0) { return reputationCompare; }
1403  int sortResult = ActiveStore.GetAdjustedItemBuyPrice(itemX.ItemPrefab).CompareTo(
1404  ActiveStore.GetAdjustedItemBuyPrice(itemY.ItemPrefab));
1405  if (sortingMethod == SortingMethod.PriceDesc) { sortResult *= -1; }
1406  return sortResult;
1407  }
1408  else
1409  {
1410  return CompareByElement(x, y);
1411  }
1412  }
1413  }
1414  }
1415  else if (sortingMethod == SortingMethod.CategoryAsc)
1416  {
1417  SortItems(list, SortingMethod.AlphabeticalAsc);
1418  list.Content.RectTransform.SortChildren(CompareByCategory);
1419  if (GetSpecialsGroup() is GUILayoutGroup specialsGroup)
1420  {
1421  specialsGroup.RectTransform.SortChildren(CompareByCategory);
1422  specialsGroup.Recalculate();
1423  }
1424 
1425  int CompareByCategory(RectTransform x, RectTransform y)
1426  {
1427  if (x.GUIComponent.UserData is PurchasedItem itemX && y.GUIComponent.UserData is PurchasedItem itemY)
1428  {
1429  int reputationCompare = CompareByReputationRestriction(itemX, itemY);
1430  if (reputationCompare != 0) { return reputationCompare; }
1431  return itemX.ItemPrefab.Category.CompareTo(itemY.ItemPrefab.Category);
1432  }
1433  else
1434  {
1435  return CompareByElement(x, y);
1436  }
1437  }
1438  }
1439 
1440  GUILayoutGroup GetSpecialsGroup()
1441  {
1442  if (list == storeBuyList)
1443  {
1444  return storeDailySpecialsGroup;
1445  }
1446  else if (list == storeSellList)
1447  {
1448  return storeRequestedGoodGroup;
1449  }
1450  else if (list == storeSellFromSubList)
1451  {
1452  return storeRequestedSubGoodGroup;
1453  }
1454  else
1455  {
1456  return null;
1457  }
1458  }
1459 
1460  int CompareByReputationRestriction(PurchasedItem item1, PurchasedItem item2)
1461  {
1462  PriceInfo priceInfo1 = item1.ItemPrefab.GetPriceInfo(ActiveStore);
1463  PriceInfo priceInfo2 = item2.ItemPrefab.GetPriceInfo(ActiveStore);
1464  if (priceInfo1 != null && priceInfo2 != null)
1465  {
1466  var requiredReputation1 = GetTooLowReputation(priceInfo1)?.Value ?? 0.0f;
1467  var requiredReputation2 = GetTooLowReputation(priceInfo2)?.Value ?? 0.0f;
1468  return requiredReputation1.CompareTo(requiredReputation2);
1469  }
1470  return 0;
1471  }
1472 
1473  static int CompareByElement(RectTransform x, RectTransform y)
1474  {
1475  if (ShouldBeOnTop(x) || ShouldBeOnBottom(y))
1476  {
1477  return -1;
1478  }
1479  else if (ShouldBeOnBottom(x) || ShouldBeOnTop(y))
1480  {
1481  return 1;
1482  }
1483  else
1484  {
1485  return 0;
1486  }
1487 
1488  static bool ShouldBeOnTop(RectTransform rt) =>
1489  rt.GUIComponent.UserData is string id && (id == "deals" || id == "header");
1490 
1491  static bool ShouldBeOnBottom(RectTransform rt) =>
1492  rt.GUIComponent.UserData is string id && id == "divider";
1493  }
1494  }
1495 
1496  private void SortItems(StoreTab tab, SortingMethod sortingMethod)
1497  {
1498  tabSortingMethods[tab] = sortingMethod;
1499  SortItems(tabLists[tab], sortingMethod);
1500  }
1501 
1502  private void SortItems(StoreTab tab)
1503  {
1504  SortItems(tab, tabSortingMethods[tab]);
1505  }
1506 
1507  private void SortActiveTabItems(SortingMethod sortingMethod) => SortItems(activeTab, sortingMethod);
1508 
1509  private GUIComponent CreateItemFrame(PurchasedItem pi, GUIComponent parentComponent, StoreTab containingTab, bool forceDisable = false)
1510  {
1511  GUIListBox parentListBox = parentComponent as GUIListBox;
1512  int width = 0;
1513  RectTransform parent = null;
1514  if (parentListBox != null)
1515  {
1516  width = parentListBox.Content.Rect.Width;
1517  parent = parentListBox.Content.RectTransform;
1518  }
1519  else
1520  {
1521  width = parentComponent.Rect.Width;
1522  parent = parentComponent.RectTransform;
1523  }
1524  GUIFrame frame = new GUIFrame(new RectTransform(new Point(width, (int)(GUI.yScale * 80)), parent: parent), style: "ListBoxElement")
1525  {
1526  UserData = pi
1527  };
1528 
1529  GUILayoutGroup mainGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 1.0f), frame.RectTransform, Anchor.Center),
1530  isHorizontal: true, childAnchor: Anchor.CenterLeft)
1531  {
1532  RelativeSpacing = 0.01f,
1533  Stretch = true
1534  };
1535 
1536  var nameAndIconRelativeWidth = 0.635f;
1537  var iconRelativeWidth = 0.0f;
1538  var priceAndButtonRelativeWidth = 1.0f - nameAndIconRelativeWidth;
1539 
1540  if ((pi.ItemPrefab.InventoryIcon ?? pi.ItemPrefab.Sprite) is { } itemIcon)
1541  {
1542  iconRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width;
1543  GUIImage img = new GUIImage(new RectTransform(new Vector2(iconRelativeWidth, 0.9f), mainGroup.RectTransform), itemIcon, scaleToFit: true)
1544  {
1545  CanBeFocused = false,
1546  Color = (itemIcon == pi.ItemPrefab.InventoryIcon ? pi.ItemPrefab.InventoryIconColor : pi.ItemPrefab.SpriteColor) * (forceDisable ? 0.5f : 1.0f),
1547  UserData = "icon"
1548  };
1549  img.RectTransform.MaxSize = img.Rect.Size;
1550  }
1551 
1552  GUIFrame nameAndQuantityFrame = new GUIFrame(new RectTransform(new Vector2(nameAndIconRelativeWidth - iconRelativeWidth, 1.0f), mainGroup.RectTransform), style: null)
1553  {
1554  CanBeFocused = false
1555  };
1556  GUILayoutGroup nameAndQuantityGroup = new GUILayoutGroup(new RectTransform(Vector2.One, nameAndQuantityFrame.RectTransform))
1557  {
1558  CanBeFocused = false,
1559  Stretch = true
1560  };
1561  bool isSellingRelatedList = containingTab != StoreTab.Buy;
1562  bool locationHasDealOnItem = isSellingRelatedList ?
1563  ActiveStore.RequestedGoods.Contains(pi.ItemPrefab) : ActiveStore.DailySpecials.Contains(pi.ItemPrefab);
1564  GUITextBlock nameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), nameAndQuantityGroup.RectTransform),
1565  pi.ItemPrefab.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft)
1566  {
1567  CanBeFocused = false,
1568  Shadow = locationHasDealOnItem,
1569  TextColor = Color.White * (forceDisable ? 0.5f : 1.0f),
1570  TextScale = 0.85f,
1571  UserData = "name"
1572  };
1573  if (locationHasDealOnItem)
1574  {
1575  var relativeWidth = (0.9f * nameAndQuantityFrame.Rect.Height) / nameAndQuantityFrame.Rect.Width;
1576  var dealIcon = new GUIImage(
1577  new RectTransform(new Vector2(relativeWidth, 0.9f), nameAndQuantityFrame.RectTransform, anchor: Anchor.CenterLeft)
1578  {
1579  AbsoluteOffset = new Point((int)nameBlock.Padding.X, 0)
1580  },
1581  "StoreDealIcon", scaleToFit: true)
1582  {
1583  CanBeFocused = false
1584  };
1585  dealIcon.SetAsFirstChild();
1586  }
1587  bool isParentOnLeftSideOfInterface = parentComponent == storeBuyList || parentComponent == storeDailySpecialsGroup ||
1588  parentComponent == storeSellList || parentComponent == storeRequestedGoodGroup ||
1589  parentComponent == storeSellFromSubList || parentComponent == storeRequestedSubGoodGroup;
1590  GUILayoutGroup shoppingCrateAmountGroup = null;
1591  GUINumberInput amountInput = null;
1592  if (isParentOnLeftSideOfInterface)
1593  {
1594  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform),
1595  CreateQuantityLabelText(containingTab, pi.Quantity), font: GUIStyle.Font, textAlignment: Alignment.BottomLeft)
1596  {
1597  CanBeFocused = false,
1598  Shadow = locationHasDealOnItem,
1599  TextColor = Color.White * (forceDisable ? 0.5f : 1.0f),
1600  TextScale = 0.85f,
1601  UserData = "quantitylabel"
1602  };
1603  }
1604  else
1605  {
1606  var relativePadding = nameBlock.Padding.X / nameBlock.Rect.Width;
1607  shoppingCrateAmountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativePadding, 0.6f), nameAndQuantityGroup.RectTransform) { RelativeOffset = new Vector2(relativePadding, 0) },
1608  isHorizontal: true)
1609  {
1610  RelativeSpacing = 0.02f
1611  };
1612  amountInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), shoppingCrateAmountGroup.RectTransform), NumberType.Int)
1613  {
1614  MinValueInt = 0,
1615  MaxValueInt = GetMaxAvailable(pi.ItemPrefab, containingTab),
1616  UserData = pi,
1617  IntValue = pi.Quantity
1618  };
1619  amountInput.Enabled = !forceDisable;
1620  amountInput.TextBox.OnSelected += (sender, key) => { suppressBuySell = true; };
1621  amountInput.TextBox.OnDeselected += (sender, key) => { suppressBuySell = false; amountInput.OnValueChanged?.Invoke(amountInput); };
1622  amountInput.OnValueChanged += (numberInput) =>
1623  {
1624  if (suppressBuySell) { return; }
1625  PurchasedItem purchasedItem = numberInput.UserData as PurchasedItem;
1626  if (!HasActiveTabPermissions())
1627  {
1628  numberInput.IntValue = purchasedItem.Quantity;
1629  return;
1630  }
1631  AddToShoppingCrate(purchasedItem, quantity: numberInput.IntValue - purchasedItem.Quantity);
1632  };
1633  frame.HoverColor = frame.SelectedColor = Color.Transparent;
1634  }
1635 
1636  // Amount in players' inventories and on the sub
1637  var rectTransform = shoppingCrateAmountGroup == null ?
1638  new RectTransform(new Vector2(1.0f, 0.3f), nameAndQuantityGroup.RectTransform) :
1639  new RectTransform(new Vector2(0.6f, 1.0f), shoppingCrateAmountGroup.RectTransform);
1640  var ownedLabel = new GUITextBlock(rectTransform, string.Empty, font: GUIStyle.Font, textAlignment: shoppingCrateAmountGroup == null ? Alignment.TopLeft : Alignment.CenterLeft)
1641  {
1642  CanBeFocused = false,
1643  Shadow = locationHasDealOnItem,
1644  TextColor = Color.White * (forceDisable ? 0.5f : 1.0f),
1645  TextScale = 0.85f,
1646  UserData = "owned"
1647  };
1648  SetOwnedText(frame, ownedLabel);
1649  shoppingCrateAmountGroup?.Recalculate();
1650 
1651  var buttonRelativeWidth = (0.9f * mainGroup.Rect.Height) / mainGroup.Rect.Width;
1652 
1653  var priceFrame = new GUIFrame(new RectTransform(new Vector2(priceAndButtonRelativeWidth - buttonRelativeWidth, 1.0f), mainGroup.RectTransform), style: null)
1654  {
1655  CanBeFocused = false
1656  };
1657  var priceBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), priceFrame.RectTransform, anchor: Anchor.Center),
1658  "0 MK", font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right)
1659  {
1660  CanBeFocused = false,
1661  TextColor = locationHasDealOnItem ? storeSpecialColor : Color.White,
1662  UserData = "price"
1663  };
1664  priceBlock.Color *= (forceDisable ? 0.5f : 1.0f);
1665  priceBlock.CalculateHeightFromText();
1666  if (locationHasDealOnItem)
1667  {
1668  var undiscounterPriceBlock = new GUITextBlock(
1669  new RectTransform(new Vector2(1.0f, 0.25f), priceFrame.RectTransform, anchor: Anchor.Center)
1670  {
1671  AbsoluteOffset = new Point(0, priceBlock.RectTransform.ScaledSize.Y)
1672  }, "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center)
1673  {
1674  CanBeFocused = false,
1675  Strikethrough = new GUITextBlock.StrikethroughSettings(color: priceBlock.TextColor, expand: 1),
1676  TextColor = priceBlock.TextColor,
1677  UserData = "undiscountedprice"
1678  };
1679  }
1680  SetPriceGetters(frame, !isSellingRelatedList);
1681 
1682  if (isParentOnLeftSideOfInterface)
1683  {
1684  new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreAddToCrateButton")
1685  {
1686  ClickSound = GUISoundType.Cart,
1687  Enabled = !forceDisable && pi.Quantity > 0,
1689  UserData = "addbutton",
1690  OnClicked = (button, userData) => AddToShoppingCrate(pi)
1691  };
1692  }
1693  else
1694  {
1695  new GUIButton(new RectTransform(new Vector2(buttonRelativeWidth, 0.9f), mainGroup.RectTransform), style: "StoreRemoveFromCrateButton")
1696  {
1697  ClickSound = GUISoundType.Cart,
1698  Enabled = !forceDisable,
1700  UserData = "removebutton",
1701  OnClicked = (button, userData) => ClearFromShoppingCrate(pi)
1702  };
1703  }
1704 
1705  if (parentListBox != null)
1706  {
1707  parentListBox.RecalculateChildren();
1708  }
1709  else if (parentComponent is GUILayoutGroup parentLayoutGroup)
1710  {
1711  parentLayoutGroup.Recalculate();
1712  }
1713  mainGroup.Recalculate();
1714  mainGroup.RectTransform.RecalculateChildren(true, true);
1715  amountInput?.LayoutGroup.Recalculate();
1716  nameBlock.Text = ToolBox.LimitString(nameBlock.Text, nameBlock.Font, nameBlock.Rect.Width);
1717  mainGroup.RectTransform.Children.ForEach(c => c.IsFixedSize = true);
1718 
1719  return frame;
1720  }
1721 
1722  private void UpdateOwnedItems()
1723  {
1724  OwnedItems.Clear();
1725 
1726  if (ActiveStore == null) { return; }
1727 
1728  // Add items on the sub(s)
1729  if (Submarine.MainSub?.GetItems(true) is List<Item> subItems)
1730  {
1731  foreach (var subItem in subItems)
1732  {
1733  if (!subItem.Components.All(c => c is not Holdable h || !h.Attachable || !h.Attached)) { continue; }
1734  if (!subItem.Components.All(c => c is not Wire w || w.Connections.All(c => c == null))) { continue; }
1735  if (!ItemAndAllContainersInteractable(subItem)) { continue; }
1736  //don't list items in a character inventory (the ones in a crew member's inventory are counted below)
1737  var rootInventoryOwner = subItem.GetRootInventoryOwner();
1738  if (rootInventoryOwner is Character) { continue; }
1739  AddOwnedItem(subItem);
1740  }
1741  }
1742 
1743  // Add items in character inventories
1744  foreach (var item in Item.ItemList)
1745  {
1746  if (item == null || item.Removed) { continue; }
1747  var rootInventoryOwner = item.GetRootInventoryOwner();
1748  var ownedByCrewMember = GameMain.GameSession.CrewManager.GetCharacters().Any(c => c == rootInventoryOwner);
1749  if (!ownedByCrewMember) { continue; }
1750  AddOwnedItem(item);
1751  }
1752 
1753  // Add items already purchased
1754  CargoManager?.GetPurchasedItems(ActiveStore).Where(pi => !pi.DeliverImmediately).ForEach(pi => AddNonEmptyOwnedItems(pi));
1755 
1756  ownedItemsUpdateTimer = 0.0f;
1757 
1758  static bool ItemAndAllContainersInteractable(Item item)
1759  {
1760  do
1761  {
1762  if (!item.IsPlayerTeamInteractable) { return false; }
1763  item = item.Container;
1764  } while (item != null);
1765  return true;
1766  }
1767 
1768  void AddOwnedItem(Item item)
1769  {
1770  if (item?.Prefab.GetPriceInfo(ActiveStore) is not PriceInfo priceInfo) { return; }
1771  bool isNonEmpty = !priceInfo.DisplayNonEmpty || item.ConditionPercentage > 5.0f;
1772  if (OwnedItems.TryGetValue(item.Prefab, out ItemQuantity itemQuantity))
1773  {
1774  OwnedItems[item.Prefab].Add(1, isNonEmpty);
1775  }
1776  else
1777  {
1778  OwnedItems.Add(item.Prefab, new ItemQuantity(1, areNonEmpty: isNonEmpty));
1779  }
1780  }
1781 
1782  void AddNonEmptyOwnedItems(PurchasedItem purchasedItem)
1783  {
1784  if (purchasedItem == null) { return; }
1785  if (OwnedItems.TryGetValue(purchasedItem.ItemPrefab, out ItemQuantity itemQuantity))
1786  {
1787  OwnedItems[purchasedItem.ItemPrefab].Add(purchasedItem.Quantity, true);
1788  }
1789  else
1790  {
1791  OwnedItems.Add(purchasedItem.ItemPrefab, new ItemQuantity(purchasedItem.Quantity));
1792  }
1793  }
1794  }
1795 
1796  private void SetItemFrameStatus(GUIComponent itemFrame, bool enabled)
1797  {
1798  if (itemFrame?.UserData is not PurchasedItem pi) { return; }
1799  bool refreshFrameStatus = !pi.IsStoreComponentEnabled.HasValue || pi.IsStoreComponentEnabled.Value != enabled;
1800  if (!refreshFrameStatus) { return; }
1801  if (itemFrame.FindChild("icon", recursive: true) is GUIImage icon)
1802  {
1803  if (pi.ItemPrefab?.InventoryIcon != null)
1804  {
1805  icon.Color = pi.ItemPrefab.InventoryIconColor * (enabled ? 1.0f : 0.5f);
1806  }
1807  else if (pi.ItemPrefab?.Sprite != null)
1808  {
1809  icon.Color = pi.ItemPrefab.SpriteColor * (enabled ? 1.0f : 0.5f);
1810  }
1811  };
1812  var color = Color.White * (enabled ? 1.0f : 0.5f);
1813  if (itemFrame.FindChild("name", recursive: true) is GUITextBlock name)
1814  {
1815  name.TextColor = color;
1816  }
1817  if (itemFrame.FindChild("quantitylabel", recursive: true) is GUITextBlock qty)
1818  {
1819  qty.TextColor = color;
1820  }
1821  else if (itemFrame.FindChild(c => c is GUINumberInput, recursive: true) is GUINumberInput numberInput)
1822  {
1823  numberInput.Enabled = enabled;
1824  }
1825  if (itemFrame.FindChild("owned", recursive: true) is GUITextBlock ownedBlock)
1826  {
1827  ownedBlock.TextColor = color;
1828  }
1829  bool isDiscounted = false;
1830  if (itemFrame.FindChild("undiscountedprice", recursive: true) is GUITextBlock undiscountedPriceBlock)
1831  {
1832  undiscountedPriceBlock.TextColor = color;
1833  undiscountedPriceBlock.Strikethrough.Color = color;
1834  isDiscounted = true;
1835  }
1836  if (itemFrame.FindChild("price", recursive: true) is GUITextBlock priceBlock)
1837  {
1838  priceBlock.TextColor = isDiscounted ? storeSpecialColor * (enabled ? 1.0f : 0.5f) : color;
1839  }
1840  if (itemFrame.FindChild("addbutton", recursive: true) is GUIButton addButton)
1841  {
1842  addButton.Enabled = enabled;
1843  }
1844  else if (itemFrame.FindChild("removebutton", recursive: true) is GUIButton removeButton)
1845  {
1846  removeButton.Enabled = enabled;
1847  }
1848  pi.IsStoreComponentEnabled = enabled;
1849  itemFrame.UserData = pi;
1850  }
1851 
1852  private static void SetQuantityLabelText(StoreTab mode, GUIComponent itemFrame)
1853  {
1854  if (itemFrame?.FindChild("quantitylabel", recursive: true) is GUITextBlock label)
1855  {
1856  label.Text = CreateQuantityLabelText(mode, (itemFrame.UserData as PurchasedItem).Quantity);
1857  }
1858  }
1859 
1860  private static LocalizedString CreateQuantityLabelText(StoreTab mode, int quantity)
1861  {
1862  try
1863  {
1864  string textTag = mode switch
1865  {
1866  StoreTab.Buy => "campaignstore.instock",
1867  StoreTab.Sell => "campaignstore.ownedinventory",
1868  StoreTab.SellSub => "campaignstore.ownedsub",
1869  _ => throw new NotImplementedException()
1870  };
1871  return TextManager.GetWithVariable(textTag, "[amount]", quantity.ToString());
1872  }
1873  catch (NotImplementedException e)
1874  {
1875  string errorMsg = $"Error creating a store quantity label text: unknown store tab.\n{e.StackTrace.CleanupStackTrace()}";
1876 #if DEBUG
1877  DebugConsole.LogError(errorMsg);
1878 #else
1879  DebugConsole.AddWarning(errorMsg);
1880 #endif
1881  }
1882  return string.Empty;
1883  }
1884 
1885  private void SetOwnedText(GUIComponent itemComponent, GUITextBlock ownedLabel = null)
1886  {
1887  ownedLabel ??= itemComponent?.FindChild("owned", recursive: true) as GUITextBlock;
1888  if (itemComponent == null && ownedLabel == null) { return; }
1889  PurchasedItem purchasedItem = itemComponent?.UserData as PurchasedItem;
1890  ItemQuantity itemQuantity = null;
1891  LocalizedString ownedLabelText = string.Empty;
1892  if (purchasedItem != null && OwnedItems.TryGetValue(purchasedItem.ItemPrefab, out itemQuantity) && itemQuantity.Total > 0)
1893  {
1894  if (itemQuantity.AllNonEmpty)
1895  {
1896  ownedLabelText = TextManager.GetWithVariable("campaignstore.owned", "[amount]", itemQuantity.Total.ToString());
1897  }
1898  else
1899  {
1900  ownedLabelText = TextManager.GetWithVariables("campaignstore.ownedspecific",
1901  ("[nonempty]", itemQuantity.NonEmpty.ToString()),
1902  ("[total]", itemQuantity.Total.ToString()));
1903  }
1904  }
1905  if (itemComponent != null)
1906  {
1907  LocalizedString toolTip = string.Empty;
1908  if (purchasedItem.ItemPrefab != null)
1909  {
1910  toolTip = purchasedItem.ItemPrefab.GetTooltip(Character.Controlled);
1911  if (itemQuantity != null)
1912  {
1913  if (itemQuantity.AllNonEmpty)
1914  {
1915  toolTip += $"\n\n{ownedLabelText}";
1916  }
1917  else
1918  {
1919  toolTip += $"\n\n{TextManager.GetWithVariable("campaignstore.ownednonempty", "[amount]", itemQuantity.NonEmpty.ToString())}";
1920  toolTip += $"\n{TextManager.GetWithVariable("campaignstore.ownedtotal", "[amount]", itemQuantity.Total.ToString())}";
1921  }
1922  }
1923 
1924  PriceInfo priceInfo = purchasedItem.ItemPrefab.GetPriceInfo(ActiveStore);
1925  var campaign = GameMain.GameSession?.Campaign;
1926  if (priceInfo != null && campaign != null)
1927  {
1928  var requiredReputation = GetReputationRequirement(priceInfo);
1929  if (requiredReputation != null)
1930  {
1931  var repStr = TextManager.GetWithVariables(
1932  "campaignstore.reputationrequired",
1933  ("[amount]", ((int)requiredReputation.Value.Value).ToString()),
1934  ("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value));
1935  Color color = MathF.Round(campaign.GetReputation(requiredReputation.Value.Key)) < requiredReputation.Value.Value ?
1936  GUIStyle.Orange : GUIStyle.Green;
1937  toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖";
1938  }
1939  }
1940  }
1941  itemComponent.ToolTip = RichString.Rich(toolTip);
1942  }
1943  if (ownedLabel != null)
1944  {
1945  ownedLabel.Text = ownedLabelText;
1946  }
1947  }
1948 
1949  private int GetMaxAvailable(ItemPrefab itemPrefab, StoreTab mode)
1950  {
1951  List<PurchasedItem> list = null;
1952  try
1953  {
1954  list = mode switch
1955  {
1956  StoreTab.Buy => ActiveStore?.Stock,
1957  StoreTab.Sell => itemsToSell,
1958  StoreTab.SellSub => itemsToSellFromSub,
1959  _ => throw new NotImplementedException()
1960  };
1961  }
1962  catch (NotImplementedException e)
1963  {
1964  DebugConsole.LogError($"Error getting item availability: Unknown store tab type. {e.StackTrace.CleanupStackTrace()}");
1965  }
1966  if (list != null && list.Find(i => i.ItemPrefab == itemPrefab) is PurchasedItem item)
1967  {
1968  if (mode == StoreTab.Buy)
1969  {
1970  return Math.Max(item.Quantity - CargoManager.GetPurchasedItemCount(ActiveStore, item.ItemPrefab), 0);
1971  }
1972  return item.Quantity;
1973  }
1974  else
1975  {
1976  return 0;
1977  }
1978  }
1979 
1980  private bool ModifyBuyQuantity(PurchasedItem item, int quantity)
1981  {
1982  if (item?.ItemPrefab == null) { return false; }
1983  if (!HasBuyPermissions) { return false; }
1984  if (quantity > 0)
1985  {
1986  var crateItem = CargoManager.GetBuyCrateItem(ActiveStore, item.ItemPrefab);
1987  if (crateItem != null && crateItem.Quantity >= CargoManager.MaxQuantity) { return false; }
1988  // Make sure there's enough available in the store
1989  var totalQuantityToBuy = crateItem != null ? crateItem.Quantity + quantity : quantity;
1990  if (totalQuantityToBuy > GetMaxAvailable(item.ItemPrefab, StoreTab.Buy)) { return false; }
1991  }
1992  CargoManager.ModifyItemQuantityInBuyCrate(ActiveStore.Identifier, item.ItemPrefab, quantity);
1993  GameMain.Client?.SendCampaignState();
1994  return true;
1995  }
1996 
1997  private bool ModifySellQuantity(PurchasedItem item, int quantity)
1998  {
1999  if (item?.ItemPrefab == null) { return false; }
2000  if (!HasSellInventoryPermissions) { return false; }
2001  if (quantity > 0)
2002  {
2003  // Make sure there's enough available to sell
2004  var itemToSell = CargoManager.GetSellCrateItem(ActiveStore, item.ItemPrefab);
2005  var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity;
2006  if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.Sell)) { return false; }
2007  }
2008  CargoManager.ModifyItemQuantityInSellCrate(ActiveStore.Identifier, item.ItemPrefab, quantity);
2009  return true;
2010  }
2011 
2012  private bool ModifySellFromSubQuantity(PurchasedItem item, int quantity)
2013  {
2014  if (item?.ItemPrefab == null) { return false; }
2015  if (!HasSellSubPermissions) { return false; }
2016  if (quantity > 0)
2017  {
2018  // Make sure there's enough available to sell
2019  var itemToSell = CargoManager.GetSubCrateItem(ActiveStore, item.ItemPrefab);
2020  var totalQuantityToSell = itemToSell != null ? itemToSell.Quantity + quantity : quantity;
2021  if (totalQuantityToSell > GetMaxAvailable(item.ItemPrefab, StoreTab.SellSub)) { return false; }
2022  }
2023  CargoManager.ModifyItemQuantityInSubSellCrate(ActiveStore.Identifier, item.ItemPrefab, quantity);
2024  GameMain.Client?.SendCampaignState();
2025  return true;
2026  }
2027 
2028  private bool AddToShoppingCrate(PurchasedItem item, int quantity = 1)
2029  {
2030  if (item == null) { return false; }
2031  try
2032  {
2033  return activeTab switch
2034  {
2035  StoreTab.Buy => ModifyBuyQuantity(item, quantity),
2036  StoreTab.Sell => ModifySellQuantity(item, quantity),
2037  StoreTab.SellSub => ModifySellFromSubQuantity(item, quantity),
2038  _ => throw new NotImplementedException()
2039  };
2040  }
2041  catch (NotImplementedException e)
2042  {
2043  DebugConsole.LogError($"Error adding an item to the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
2044  return false;
2045  }
2046  }
2047 
2048  private bool ClearFromShoppingCrate(PurchasedItem item)
2049  {
2050  if (item == null) { return false; }
2051  try
2052  {
2053  return activeTab switch
2054  {
2055  StoreTab.Buy => ModifyBuyQuantity(item, -item.Quantity),
2056  StoreTab.Sell => ModifySellQuantity(item, -item.Quantity),
2057  StoreTab.SellSub => ModifySellFromSubQuantity(item, -item.Quantity),
2058  _ => throw new NotImplementedException(),
2059  };
2060  }
2061  catch (NotImplementedException e)
2062  {
2063  DebugConsole.LogError($"Error clearing the shopping crate: Uknown store tab type. {e.StackTrace.CleanupStackTrace()}");
2064  return false;
2065  }
2066  }
2067 
2068  private bool BuyItems()
2069  {
2070  if (!HasBuyPermissions) { return false; }
2071  var itemsToPurchase = new List<PurchasedItem>(CargoManager.GetBuyCrateItems(ActiveStore));
2072  var itemsToRemove = new List<PurchasedItem>();
2073  int totalPrice = 0;
2074  foreach (var item in itemsToPurchase)
2075  {
2076  if (item is null) { continue; }
2077 
2078  if (item.ItemPrefab == null || !item.ItemPrefab.CanBeBoughtFrom(ActiveStore, out var priceInfo))
2079  {
2080  itemsToRemove.Add(item);
2081  continue;
2082  }
2083 
2084  if (item.ItemPrefab.DefaultPrice.RequiresUnlock)
2085  {
2086  if (!CargoManager.HasUnlockedStoreItem(item.ItemPrefab))
2087  {
2088  itemsToRemove.Add(item);
2089  continue;
2090  }
2091  }
2092 
2093  totalPrice += item.Quantity * ActiveStore.GetAdjustedItemBuyPrice(item.ItemPrefab, priceInfo: priceInfo);
2094  }
2095  itemsToRemove.ForEach(i => itemsToPurchase.Remove(i));
2096  if (itemsToPurchase.None() || Balance < totalPrice) { return false; }
2097 
2098  if (CampaignMode.AllowImmediateItemDelivery())
2099  {
2100  deliveryPrompt = new GUIMessageBox(
2101  TextManager.Get("newsupplies"),
2102  TextManager.Get("suppliespurchased.deliverymethod"),
2103  new LocalizedString[]
2104  {
2105  TextManager.Get("suppliespurchased.deliverymethod.deliverimmediately"),
2106  TextManager.Get("suppliespurchased.deliverymethod.delivertosub")
2107  });
2108  deliveryPrompt.Buttons[0].OnClicked = (btn, userdata) =>
2109  {
2110  ConfirmPurchase(deliverImmediately: true);
2111  deliveryPrompt?.Close();
2112  return true;
2113  };
2114  deliveryPrompt.Buttons[1].OnClicked = (btn, userdata) =>
2115  {
2116  ConfirmPurchase(deliverImmediately: false);
2117  deliveryPrompt?.Close();
2118  return true;
2119  };
2120  }
2121  else
2122  {
2123  ConfirmPurchase(deliverImmediately: false);
2124  }
2125 
2126  void ConfirmPurchase(bool deliverImmediately)
2127  {
2128  itemsToPurchase.ForEach(it => it.DeliverImmediately = deliverImmediately);
2129  CargoManager.PurchaseItems(ActiveStore.Identifier, itemsToPurchase, removeFromCrate: true);
2130  GameMain.Client?.SendCampaignState();
2131  if (!deliverImmediately)
2132  {
2133  var dialog = new GUIMessageBox(
2134  TextManager.Get("newsupplies"),
2135  TextManager.GetWithVariable("suppliespurchasedmessage", "[location]", campaignUI?.Campaign?.Map?.CurrentLocation?.DisplayName));
2136  dialog.Buttons[0].OnClicked += dialog.Close;
2137  }
2138  }
2139  return false;
2140  }
2141 
2142  public void OnDeselected()
2143  {
2144  deliveryPrompt?.Close();
2145  deliveryPrompt = null;
2146  }
2147 
2148  private bool SellItems()
2149  {
2150  if (!HasActiveTabPermissions()) { return false; }
2151  List<PurchasedItem> itemsToSell;
2152  try
2153  {
2154  itemsToSell = activeTab switch
2155  {
2156  StoreTab.Sell => new List<PurchasedItem>(CargoManager.GetSellCrateItems(ActiveStore)),
2157  StoreTab.SellSub => new List<PurchasedItem>(CargoManager.GetSubCrateItems(ActiveStore)),
2158  _ => throw new NotImplementedException()
2159  };
2160  }
2161  catch (NotImplementedException e)
2162  {
2163  DebugConsole.LogError($"Error confirming the store transaction: Unknown store tab type. {e.StackTrace.CleanupStackTrace()}");
2164  return false;
2165  }
2166  var itemsToRemove = new List<PurchasedItem>();
2167  int totalValue = 0;
2168  foreach (PurchasedItem item in itemsToSell)
2169  {
2170  if (item?.ItemPrefab?.GetPriceInfo(ActiveStore) is PriceInfo priceInfo)
2171  {
2172  totalValue += item.Quantity * ActiveStore.GetAdjustedItemSellPrice(item.ItemPrefab, priceInfo: priceInfo);
2173  }
2174  else
2175  {
2176  itemsToRemove.Add(item);
2177  }
2178  }
2179  itemsToRemove.ForEach(i => itemsToSell.Remove(i));
2180  if (itemsToSell.None() || totalValue > ActiveStore.Balance) { return false; }
2181  CargoManager.SellItems(ActiveStore.Identifier, itemsToSell, activeTab);
2182  GameMain.Client?.SendCampaignState();
2183  return false;
2184  }
2185 
2186  private void SetShoppingCrateTotalText()
2187  {
2188  if (ActiveStore == null)
2189  {
2190  shoppingCrateTotal.Text = TextManager.FormatCurrency(0);
2191  shoppingCrateTotal.TextColor = Color.White;
2192  }
2193  else if (IsBuying)
2194  {
2195  shoppingCrateTotal.Text = TextManager.FormatCurrency(buyTotal);
2196  shoppingCrateTotal.TextColor = Balance < buyTotal ? Color.Red : Color.White;
2197  }
2198  else
2199  {
2200  int total = activeTab switch
2201  {
2202  StoreTab.Sell => sellTotal,
2203  StoreTab.SellSub => sellFromSubTotal,
2204  _ => throw new NotImplementedException(),
2205  };
2206  shoppingCrateTotal.Text = TextManager.FormatCurrency(total);
2207  shoppingCrateTotal.TextColor = CurrentLocation != null && total > ActiveStore.Balance ? Color.Red : Color.White;
2208  }
2209  }
2210 
2211  private void SetConfirmButtonBehavior()
2212  {
2213  if (ActiveStore == null)
2214  {
2215  confirmButton.OnClicked = null;
2216  }
2217  else if (IsBuying)
2218  {
2219  confirmButton.ClickSound = GUISoundType.ConfirmTransaction;
2220  confirmButton.Text = TextManager.Get("CampaignStore.Purchase");
2221  confirmButton.OnClicked = (b, o) => BuyItems();
2222  }
2223  else
2224  {
2225  confirmButton.ClickSound = GUISoundType.Select;
2226  confirmButton.Text = TextManager.Get("CampaignStoreTab.Sell");
2227  confirmButton.OnClicked = (b, o) =>
2228  {
2229  var confirmDialog = new GUIMessageBox(
2230  TextManager.Get("FireWarningHeader"),
2231  TextManager.Get("CampaignStore.SellWarningText"),
2232  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
2233  confirmDialog.Buttons[0].ClickSound = GUISoundType.ConfirmTransaction;
2234  confirmDialog.Buttons[0].OnClicked = (b, o) => SellItems();
2235  confirmDialog.Buttons[0].OnClicked += confirmDialog.Close;
2236  confirmDialog.Buttons[1].OnClicked = confirmDialog.Close;
2237  return true;
2238  };
2239  }
2240  }
2241 
2242  private void SetConfirmButtonStatus()
2243  {
2244  confirmButton.Enabled =
2245  ActiveStore != null &&
2246  HasActiveTabPermissions() &&
2247  ActiveShoppingCrateList.Content.RectTransform.Children.Any() &&
2248  activeTab switch
2249  {
2250  StoreTab.Buy => Balance >= buyTotal,
2251  StoreTab.Sell => CurrentLocation != null && sellTotal <= ActiveStore.Balance,
2252  StoreTab.SellSub => CurrentLocation != null && sellFromSubTotal <= ActiveStore.Balance,
2253  _ => false
2254  };
2255  confirmButton.Visible = ActiveStore != null;
2256  }
2257 
2258  private void SetClearAllButtonStatus()
2259  {
2260  clearAllButton.Enabled =
2261  HasActiveTabPermissions() &&
2262  ActiveShoppingCrateList.Content.RectTransform.Children.Any();
2263  }
2264 
2265  private int prevBalance;
2266  private float ownedItemsUpdateTimer = 0.0f, sellableItemsFromSubUpdateTimer = 0.0f;
2267  private const float timerUpdateInterval = 1.5f;
2268  private readonly Stopwatch updateStopwatch = new Stopwatch();
2269 
2270  public void Update(float deltaTime)
2271  {
2272  updateStopwatch.Restart();
2273 
2274  if (GameMain.GraphicsWidth != resolutionWhenCreated.X || GameMain.GraphicsHeight != resolutionWhenCreated.Y)
2275  {
2276  CreateUI();
2277  needsRefresh = true;
2278  }
2279  else
2280  {
2281  playerBalanceElement = CampaignUI.UpdateBalanceElement(playerBalanceElement);
2282 
2283  // Update the owned items at short intervals and check if the interface should be refreshed
2284  ownedItemsUpdateTimer += deltaTime;
2285  if (ownedItemsUpdateTimer >= timerUpdateInterval)
2286  {
2287  bool checkForRefresh = !needsItemsToSellRefresh || !needsRefresh;
2288  var prevOwnedItems = checkForRefresh ? new Dictionary<ItemPrefab, ItemQuantity>(OwnedItems) : null;
2289  UpdateOwnedItems();
2290  if (checkForRefresh)
2291  {
2292  bool refresh = OwnedItems.Count != prevOwnedItems.Count ||
2293  OwnedItems.Values.Sum(v => v.Total) != prevOwnedItems.Values.Sum(v => v.Total) ||
2294  OwnedItems.Any(kvp => !prevOwnedItems.TryGetValue(kvp.Key, out ItemQuantity v) || kvp.Value.Total != v.Total) ||
2295  prevOwnedItems.Any(kvp => !OwnedItems.ContainsKey(kvp.Key));
2296  if (refresh)
2297  {
2298  needsItemsToSellRefresh = true;
2299  needsRefresh = true;
2300  }
2301  }
2302  }
2303  // Update the sellable sub items at short intervals and check if the interface should be refreshed
2304  sellableItemsFromSubUpdateTimer += deltaTime;
2305  if (sellableItemsFromSubUpdateTimer >= timerUpdateInterval)
2306  {
2307  bool checkForRefresh = !needsRefresh;
2308  var prevSubItems = checkForRefresh ? new List<PurchasedItem>(itemsToSellFromSub) : null;
2310  if (checkForRefresh)
2311  {
2312  needsRefresh = itemsToSellFromSub.Count != prevSubItems.Count ||
2313  itemsToSellFromSub.Sum(i => i.Quantity) != prevSubItems.Sum(i => i.Quantity) ||
2314  itemsToSellFromSub.Any(i => prevSubItems.FirstOrDefault(prev => prev.ItemPrefab == i.ItemPrefab) is not PurchasedItem prev || i.Quantity != prev.Quantity) ||
2315  prevSubItems.Any(prev => itemsToSellFromSub.None(i => i.ItemPrefab == prev.ItemPrefab));
2316  }
2317  }
2318  }
2319  // Refresh the interface if balance changes and the buy tab is open
2320  if (activeTab == StoreTab.Buy)
2321  {
2322  int currBalance = Balance;
2323  if (prevBalance != currBalance)
2324  {
2325  needsBuyingRefresh = true;
2326  prevBalance = currBalance;
2327  }
2328  }
2329  if (ActiveStore != null)
2330  {
2331  if (needsItemsToSellRefresh)
2332  {
2334  }
2335  if (needsItemsToSellFromSubRefresh)
2336  {
2338  }
2339  if (needsRefresh)
2340  {
2341  Refresh(updateOwned: ownedItemsUpdateTimer > 0.0f);
2342  }
2343  if (needsBuyingRefresh || HavePermissionsChanged(StoreTab.Buy))
2344  {
2345  RefreshBuying(updateOwned: ownedItemsUpdateTimer > 0.0f);
2346  }
2347  if (needsSellingRefresh || HavePermissionsChanged(StoreTab.Sell))
2348  {
2349  RefreshSelling(updateOwned: ownedItemsUpdateTimer > 0.0f);
2350  }
2351  if (needsSellingFromSubRefresh || HavePermissionsChanged(StoreTab.SellSub))
2352  {
2353  RefreshSellingFromSub(updateOwned: ownedItemsUpdateTimer > 0.0f, updateItemsToSellFromSub: sellableItemsFromSubUpdateTimer > 0.0f);
2354  }
2355  }
2356 
2357  updateStopwatch.Stop();
2358  GameMain.PerformanceCounter.AddElapsedTicks("Update:GameSession:Store", updateStopwatch.ElapsedTicks);
2359  }
2360  }
2361 }
static ? PlayerBalanceElement UpdateBalanceElement(PlayerBalanceElement? playerBalanceElement)
Definition: CampaignUI.cs:717
CampaignMode Campaign
Definition: CampaignUI.cs:40
List< PurchasedItem > GetSubCrateItems(Identifier identifier, bool create=false)
List< PurchasedItem > GetSellCrateItems(Identifier identifier, bool create=false)
void PurchaseItems(Identifier storeIdentifier, List< PurchasedItem > itemsToPurchase, bool removeFromCrate, Client client=null)
void ModifyItemQuantityInSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity)
List< PurchasedItem > GetPurchasedItems(Identifier identifier, bool create=false)
void ModifyItemQuantityInSubSellCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client=null)
void ModifyItemQuantityInBuyCrate(Identifier storeIdentifier, ItemPrefab itemPrefab, int changeInQuantity, Client client=null)
void SellItems(Identifier storeIdentifier, List< PurchasedItem > itemsToSell, Store.StoreTab sellingMode)
readonly NamedEvent< CargoManager > OnItemsInSellFromSubCrateChanged
int GetPurchasedItemCount(Location.StoreInfo store, ItemPrefab prefab)
PurchasedItem GetBuyCrateItem(Identifier identifier, ItemPrefab prefab)
PurchasedItem GetSellCrateItem(Identifier identifier, ItemPrefab prefab)
IEnumerable< Item > GetSellableItems(Character character)
List< PurchasedItem > GetBuyCrateItems(Identifier identifier, bool create=false)
PurchasedItem GetSubCrateItem(Identifier identifier, ItemPrefab prefab)
OnClickedHandler OnClicked
Definition: GUIButton.cs:16
override bool Enabled
Definition: GUIButton.cs:27
LocalizedString Text
Definition: GUIButton.cs:138
GUISoundType ClickSound
Definition: GUIButton.cs:167
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual Rectangle Rect
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIComponent AddItem(LocalizedString text, object userData=null, LocalizedString toolTip=null, Color? color=null, Color? textColor=null)
Definition: GUIDropDown.cs:247
override void RemoveChild(GUIComponent child)
Definition: GUIListBox.cs:1249
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
List< GUIButton > Buttons
void SetRichText(LocalizedString richText)
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
Definition: GUITextBox.cs:38
static int GraphicsWidth
Definition: GameMain.cs:162
static PerformanceCounter PerformanceCounter
Definition: GameMain.cs:37
static int GraphicsHeight
Definition: GameMain.cs:168
PriceInfo GetPriceInfo(Location.StoreInfo store)
StoreInfo GetStore(Identifier identifier)
Definition: Location.cs:1299
Reputation Reputation
Definition: Location.cs:103
Dictionary< Identifier, StoreInfo > Stores
Definition: Location.cs:395
readonly NamedEvent< LocationChangeInfo > OnLocationChanged
From -> To
void AddElapsedTicks(string identifier, long ticks)
readonly Identifier Identifier
Definition: Prefab.cs:34
IEnumerable< RectTransform > Children
readonly NamedEvent< Reputation > OnReputationValueChanged
Definition: Reputation.cs:121
void RefreshItemsToSellFromSub()
Definition: Store.cs:1208
void Update(float deltaTime)
Definition: Store.cs:2270
void Refresh(bool updateOwned=true)
Definition: Store.cs:278
void RefreshItemsToSell()
Definition: Store.cs:1177
void SelectStore(Character merchant)
Definition: Store.cs:212
void OnDeselected()
Definition: Store.cs:2142
Store(CampaignUI campaignUI, GUIComponent parentComponent)
Definition: Store.cs:187
NumberType
Definition: Enums.cs:715
GUISoundType
Definition: GUI.cs:21