Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/GameSession/UpgradeManager.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Xml.Linq;
8 using Microsoft.Xna.Framework;
9 
10 namespace Barotrauma
11 {
12  internal class PurchasedUpgrade
13  {
14  public readonly UpgradeCategory Category;
15  public readonly UpgradePrefab Prefab;
16  public int Level;
17 
18  public PurchasedUpgrade(UpgradePrefab upgradePrefab, UpgradeCategory category, int level = 1)
19  {
20  Category = category;
21  Prefab = upgradePrefab;
22  Level = level;
23  }
24 
25  public void Deconstruct(out UpgradePrefab prefab, out UpgradeCategory category, out int level)
26  {
27  prefab = Prefab;
28  category = Category;
29  level = Level;
30  }
31  }
32 
33  internal class PurchasedItemSwap
34  {
35  public readonly Item ItemToRemove;
36  public readonly ItemPrefab ItemToInstall;
37 
38  public PurchasedItemSwap(Item itemToRemove, ItemPrefab itemToInstall)
39  {
40  ItemToRemove = itemToRemove;
41  ItemToInstall = itemToInstall;
42  }
43  }
44 
55  partial class UpgradeManager
56  {
61  public const bool UpgradeAlsoConnectedSubs = true;
62 
70  private List<PurchasedUpgrade>? loadedUpgrades;
71 
78  public readonly List<PurchasedUpgrade> PurchasedUpgrades = new List<PurchasedUpgrade>();
79 
80  public readonly List<PurchasedUpgrade> PendingUpgrades = new List<PurchasedUpgrade>();
81 
82  public readonly List<PurchasedItemSwap> PurchasedItemSwaps = new List<PurchasedItemSwap>();
83 
84  private CampaignMetadata Metadata => Campaign.CampaignMetadata;
85  private readonly CampaignMode Campaign;
86 
87  public readonly NamedEvent<UpgradeManager> OnUpgradesChanged = new NamedEvent<UpgradeManager>();
88 
89  public UpgradeManager(CampaignMode campaign)
90  {
91  UpgradeCategory.Categories.ForEach(c => c.DeterminePrefabsThatAllowUpgrades());
92 
93  DebugConsole.Log("Created brand new upgrade manager.");
94  Campaign = campaign;
95  }
96 
97  public UpgradeManager(CampaignMode campaign, XElement element, bool isSingleplayer) : this(campaign)
98  {
99  DebugConsole.Log($"Restored upgrade manager from save file, ({element.Elements().Count()} pending upgrades).");
100 
101  //backwards compatibility:
102  //upgrades used to be saved to a <pendingupgrades> element, now upgrades and item swaps are saved separately under a <upgrademanager> element
103  if (element.Name.LocalName.Equals("pendingupgrades", StringComparison.OrdinalIgnoreCase))
104  {
105  LoadPendingUpgrades(element, isSingleplayer);
106  }
107  else
108  {
109  foreach (var subElement in element.Elements())
110  {
111  switch (subElement.Name.ToString().ToLowerInvariant())
112  {
113  case "pendingupgrades":
114  LoadPendingUpgrades(subElement, isSingleplayer);
115  break;
116  }
117  }
118  }
119  }
120 
121  public int DetermineItemSwapCost(Item item, ItemPrefab? replacement)
122  {
123  if (replacement == null)
124  {
126  if (replacement == null)
127  {
128  DebugConsole.ThrowError("Failed to determine swap cost for item \"{}\". Trying to uninstall the item but no replacement item found.");
129  return 0;
130  }
131  }
132 
133  int price = 0;
134  if (replacement == item.Prefab)
135  {
136  if (item.PendingItemSwap != null)
137  {
138  //refund the pending swap
139  price -= item.PendingItemSwap.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
140  //buy back the current item
141  price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
142  }
143  }
144  else
145  {
146  price = replacement.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
147  if (item.PendingItemSwap != null)
148  {
149  //refund the pending swap
150  price -= item.PendingItemSwap.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
151  //buy back the current item
152  price += item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
153  }
154  //refund the current item
155  if (replacement != ((MapEntity)item).Prefab)
156  {
157  price -= item.Prefab.SwappableItem.GetPrice(Campaign?.Map?.CurrentLocation);
158  }
159  }
160  return price;
161  }
162 
163  private DateTime lastUpgradeSpeak, lastErrorSpeak;
164 
172  public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force = false, Client? client = null)
173  {
174  if (!CanUpgradeSub())
175  {
176  DebugConsole.ThrowError("Cannot upgrade when switching to another submarine.");
177  return;
178  }
179 
180  int price = prefab.Price.GetBuyPrice(prefab, GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
181  int currentLevel = GetUpgradeLevel(prefab, category);
182  int newLevel = currentLevel + 1;
183 
184  int maxLevel = prefab.GetMaxLevelForCurrentSub();
185  if (currentLevel + 1 > maxLevel)
186  {
187  DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled.");
188  return;
189  }
190 
191  bool TryTakeResources(Character character)
192  {
193  bool result = prefab.TryTakeResources(character, newLevel);
194  if (!result)
195  {
196  DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources.");
197  }
198  return result;
199  }
200 
201  if (!force)
202  {
203  switch (GameMain.NetworkMember)
204  {
205  case null when Character.Controlled is { } controlled: // singleplayer
206  if (!TryTakeResources(controlled)) { return; }
207  break;
208  case { IsClient: true }:
209  if (!prefab.HasResourcesToUpgrade(Character.Controlled, newLevel)) { return; }
210  break;
211  case { IsServer: true } when client?.Character is { } character:
212  if (!TryTakeResources(character)) { return; }
213  break;
214  default:
215  DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" without a player.");
216  return;
217  }
218  }
219 
220  if (price < 0)
221  {
222  Location? location = Campaign.Map?.CurrentLocation;
223  LogError($"Upgrade price is less than 0! ({price})",
224  new Dictionary<string, object?>
225  {
226  { "Level", currentLevel },
227  { "Saved Level", GetRealUpgradeLevel(prefab, category) },
228  { "Upgrade", $"{category.Identifier}.{prefab.Identifier}" },
229  { "Location", location?.Type },
230  { "Reputation", $"{location?.Reputation?.Value} / {location?.Reputation?.MaxReputation}" },
231  { "Base Price", prefab.Price.BasePrice }
232  });
233  }
234 
235  if (force)
236  {
237  price = 0;
238  }
239 
240  if (force || Campaign.TryPurchase(client, price))
241  {
242  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
243  {
244  // only make the NPC speak if more than 5 minutes have passed since the last purchased service
245  if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
246  {
247  UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
248  lastUpgradeSpeak = DateTime.Now;
249  }
250  }
251 
252  GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value);
253 
254  PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
255 
256 #if CLIENT
257  DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUIStyle.Orange);
258 #endif
259 
260  if (upgrade == null)
261  {
262  PendingUpgrades.Add(new PurchasedUpgrade(prefab, category));
263  }
264  else
265  {
266  upgrade.Level++;
267  }
268 #if CLIENT
269  // tell the server that this item is yet to be paid for server side
270  PurchasedUpgrades.Add(new PurchasedUpgrade(prefab, category));
271 #endif
272  OnUpgradesChanged?.Invoke(this);
273  }
274  else
275  {
276  DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
277  $"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
278  }
279  }
280 
285  public void AddUpgradeExternally(UpgradePrefab prefab, UpgradeCategory category, int level)
286  {
287  int maxLevel = prefab.GetMaxLevelForCurrentSub();
288  int currentLevel = GetUpgradeLevel(prefab, category);
289  if (currentLevel + 1 > maxLevel) { return; }
290 
291  PendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level));
292  OnUpgradesChanged?.Invoke(this);
293  }
294 
298  public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage = false, Client? client = null)
299  {
300  if (!CanUpgradeSub())
301  {
302  DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
303  return;
304  }
305  if (itemToRemove == null)
306  {
307  DebugConsole.ThrowError($"Cannot swap null item!");
308  return;
309  }
310  if (itemToRemove.HiddenInGame)
311  {
312  DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's set to be hidden in-game.");
313  return;
314  }
315  if (!itemToRemove.AllowSwapping)
316  {
317  DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's configured to be non-swappable.");
318  return;
319  }
320  if (!UpgradeCategory.Categories.Any(c => c.ItemTags.Any(t => itemToRemove.HasTag(t)) && c.ItemTags.Any(t => itemToInstall.Tags.Contains(t))))
321  {
322  DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\" (not in the same upgrade category).");
323  return;
324  }
325 
326  if (((MapEntity)itemToRemove).Prefab == itemToInstall)
327  {
328  DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!).");
329  return;
330  }
331  SwappableItem? swappableItem = itemToRemove.Prefab.SwappableItem;
332  if (swappableItem == null)
333  {
334  DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (not configured as a swappable item).");
335  return;
336  }
337 
338  var linkedItems = GetLinkedItemsToSwap(itemToRemove);
339 
340  int price = 0;
341  if (!itemToRemove.AvailableSwaps.Contains(itemToInstall))
342  {
343  price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation) * linkedItems.Count;
344  }
345 
346  if (isNetworkMessage)
347  {
348  price = 0;
349  }
350 
351  //do not try to purchase if this is a network message (if the server is telling us that an item swap was purchased)
352  //we want to do the purchase no matter what, and the server handles deducting the money
353  if (isNetworkMessage || Campaign.TryPurchase(client, price))
354  {
355  PurchasedItemSwaps.RemoveAll(p => linkedItems.Contains(p.ItemToRemove));
356  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
357  {
358  // only make the NPC speak if more than 5 minutes have passed since the last purchased service
359  if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
360  {
361  UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
362  lastUpgradeSpeak = DateTime.Now;
363  }
364  }
365 
366  GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.Identifier.Value);
367 
368  foreach (Item itemToSwap in linkedItems)
369  {
370  itemToSwap.AvailableSwaps.Add(itemToSwap.Prefab);
371  if (itemToInstall != null && !itemToSwap.AvailableSwaps.Contains(itemToInstall))
372  {
373  itemToSwap.PurchasedNewSwap = true;
374  itemToSwap.AvailableSwaps.Add(itemToInstall);
375  }
376 
377  if (itemToSwap.Prefab != itemToInstall && itemToInstall != null)
378  {
379  itemToSwap.PendingItemSwap = itemToInstall;
380  PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToSwap, itemToInstall));
381  DebugLog($"CLIENT: Swapped item \"{itemToSwap.Name}\" with \"{itemToInstall.Name}\".", Color.Orange);
382  }
383  else
384  {
385  DebugLog($"CLIENT: Cancelled swapping the item \"{itemToSwap.Name}\" with \"{(itemToSwap.PendingItemSwap?.Name ?? null)}\".", Color.Orange);
386  }
387  }
388 
389  OnUpgradesChanged?.Invoke(this);
390  }
391  else
392  {
393  DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" +
394  $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.GetWallet(client).Balance}");
395  }
396  }
397 
401  public void CancelItemSwap(Item itemToRemove, bool force = false)
402  {
403  if (!CanUpgradeSub())
404  {
405  DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
406  return;
407  }
408 
409  if (itemToRemove?.PendingItemSwap == null && (itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall.IsEmpty ?? true))
410  {
411  DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured).");
412  return;
413  }
414 
415  SwappableItem? swappableItem = itemToRemove.Prefab.SwappableItem;
416  if (swappableItem == null)
417  {
418  DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\" (not configured as a swappable item).");
419  return;
420  }
421 
422  if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
423  {
424  // only make the NPC speak if more than 5 minutes have passed since the last purchased service
425  if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
426  {
427  UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
428  lastUpgradeSpeak = DateTime.Now;
429  }
430  }
431 
432  var linkedItems = GetLinkedItemsToSwap(itemToRemove);
433 
434  foreach (Item itemToCancel in linkedItems)
435  {
436  if (itemToCancel.PendingItemSwap == null)
437  {
438  if (MapEntityPrefab.FindByIdentifier(swappableItem.ReplacementOnUninstall) is not ItemPrefab replacement)
439  {
440  DebugConsole.ThrowError($"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
441  return;
442  }
443  PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToCancel);
444  PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToCancel, replacement));
445  DebugLog($"Uninstalled item item \"{itemToCancel.Name}\".", Color.Orange);
446  itemToCancel.PendingItemSwap = replacement;
447  }
448  else
449  {
450  PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToCancel);
451  DebugLog($"Cancelled swapping the item \"{itemToCancel.Name}\" with \"{itemToCancel.PendingItemSwap.Name}\".", Color.Orange);
452  itemToCancel.PendingItemSwap = null;
453  }
454  }
455 
456 #if CLIENT
457  OnUpgradesChanged?.Invoke(this);
458 #endif
459  }
460 
461  public static ICollection<Item> GetLinkedItemsToSwap(Item item)
462  {
463  HashSet<Item> linkedItems = new HashSet<Item>() { item };
464  foreach (MapEntity linkedEntity in item.linkedTo)
465  {
466  foreach (MapEntity secondLinkedEntity in linkedEntity.linkedTo)
467  {
468  if (secondLinkedEntity is not Item linkedItem || linkedItem == item) { continue; }
469  if (linkedItem.AllowSwapping &&
470  linkedItem.Prefab.SwappableItem != null && (linkedItem.Prefab.SwappableItem.CanBeBought || item.Prefab.SwappableItem.ReplacementOnUninstall == ((MapEntity)linkedItem).Prefab.Identifier) &&
471  linkedItem.Prefab.SwappableItem.SwapIdentifier.Equals(item.Prefab.SwappableItem.SwapIdentifier, StringComparison.OrdinalIgnoreCase))
472  {
473  linkedItems.Add(linkedItem);
474  }
475  }
476  }
477  return linkedItems;
478  }
479 
494  public void ApplyUpgrades()
495  {
496  PurchasedUpgrades.Clear();
497  PurchasedItemSwaps.Clear();
498  if (Submarine.MainSub == null) { return; }
499 
500  List<PurchasedUpgrade> pendingUpgrades = PendingUpgrades;
501 
502  if (Level.Loaded is { Type: LevelData.LevelType.Outpost })
503  {
504  return;
505  }
506 
507  if (GameMain.NetworkMember is { IsClient: true })
508  {
509  if (loadedUpgrades != null)
510  {
511  // client receives pending upgrades from the save file
512  pendingUpgrades = loadedUpgrades;
513  }
514  }
515 
516  DebugConsole.Log("Applying upgrades...");
517  foreach (var (prefab, category, level) in pendingUpgrades)
518  {
519  int newLevel = BuyUpgrade(prefab, category, Submarine.MainSub, level);
520  DebugConsole.Log($" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})");
521  SetUpgradeLevel(prefab, category, GetRealUpgradeLevel(prefab, category) + level);
522  }
523 
524  PendingUpgrades.Clear();
525  loadedUpgrades?.Clear();
526  loadedUpgrades = null;
527  }
528 
529  public void CreateUpgradeErrorMessage(string text, bool isSinglePlayer, Character character)
530  {
531  // 10 second cooldown on the error message but not the UI sound
532  if (lastErrorSpeak == DateTime.MinValue || lastErrorSpeak.AddSeconds(10) < DateTime.Now)
533  {
534  UpgradeNPCSpeak(text, isSinglePlayer, character);
535  lastErrorSpeak = DateTime.Now;
536  }
537 #if CLIENT
538  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
539 #endif
540  }
541 
554  partial void UpgradeNPCSpeak(string text, bool isSinglePlayer, Character? character = null);
555 
561  public void SanityCheckUpgrades()
562  {
564  if (submarine is null) { return; }
565 
566  // check walls
567  foreach (Structure wall in submarine.GetWalls(UpgradeAlsoConnectedSubs))
568  {
569  foreach (UpgradeCategory category in UpgradeCategory.Categories)
570  {
571  foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
572  {
573  if (!prefab.IsWallUpgrade) { continue; }
574  TryFixUpgrade(wall, category, prefab);
575  }
576  }
577  }
578 
579  // Check items
580  foreach (Item item in submarine.GetItems(UpgradeAlsoConnectedSubs))
581  {
582  foreach (UpgradeCategory category in UpgradeCategory.Categories)
583  {
584  foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
585  {
586  TryFixUpgrade(item, category, prefab);
587  }
588  }
589  }
590 
591  void TryFixUpgrade(MapEntity entity, UpgradeCategory category, UpgradePrefab prefab)
592  {
593  if (!category.CanBeApplied(entity, prefab)) { return; }
594 
595  int level = GetRealUpgradeLevel(prefab, category);
596  int maxLevel = submarine.Info is { } info ? prefab.GetMaxLevel(info) : prefab.MaxLevel;
597  if (maxLevel < level) { level = maxLevel; }
598 
599  if (level == 0) { return; }
600 
601  Upgrade? upgrade = entity.GetUpgrade(prefab.Identifier);
602 
603  if (upgrade == null || upgrade.Level != level)
604  {
605  DebugLog($"{entity.Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
606  FixUpgradeOnItem((ISerializableEntity)entity, prefab, level);
607  }
608  }
609  }
610 
611  private static void FixUpgradeOnItem(ISerializableEntity target, UpgradePrefab prefab, int level)
612  {
613  if (target is MapEntity mapEntity)
614  {
615  // do not fix what's not broken
616  if (level == 0) { return; }
617 
618  mapEntity.SetUpgrade(new Upgrade(target, prefab, level), false);
619  }
620  }
621 
622  private readonly static HashSet<Submarine> upgradedSubs = new HashSet<Submarine>();
631  private static int BuyUpgrade(UpgradePrefab prefab, UpgradeCategory category, Submarine submarine, int level = 1, Submarine? parentSub = null)
632  {
633  if (parentSub == null)
634  {
635  upgradedSubs.Clear();
636  }
637  upgradedSubs.Add(submarine);
638 
639  int? newLevel = null;
640  if (category.IsWallUpgrade)
641  {
642  foreach (Structure structure in submarine.GetWalls(UpgradeAlsoConnectedSubs))
643  {
644  Upgrade upgrade = new Upgrade(structure, prefab, level);
645  structure.AddUpgrade(upgrade, createNetworkEvent: false);
646 
647  Upgrade? newUpgrade = structure.GetUpgrade(prefab.Identifier);
648  if (newUpgrade != null)
649  {
650  SanityCheck(newUpgrade, structure);
651  newLevel ??= newUpgrade.Level;
652  }
653  }
654  }
655  else
656  {
657  foreach (Item item in submarine.GetItems(UpgradeAlsoConnectedSubs))
658  {
659  if (category.CanBeApplied(item, prefab))
660  {
661  Upgrade upgrade = new Upgrade(item, prefab, level);
662  item.AddUpgrade(upgrade, createNetworkEvent: false);
663 
664  Upgrade? newUpgrade = item.GetUpgrade(prefab.Identifier);
665  if (newUpgrade != null)
666  {
667  SanityCheck(newUpgrade, item);
668  newLevel ??= newUpgrade.Level;
669  }
670  }
671  }
672  }
673 
674  foreach (Submarine loadedSub in Submarine.Loaded)
675  {
676  if (loadedSub == parentSub || loadedSub == submarine) { continue; }
677  if (loadedSub.Info?.Type != SubmarineType.Player) { continue; }
678  if (upgradedSubs.Contains(loadedSub)) { continue; }
679 
680  XElement? root = loadedSub.Info?.SubmarineElement;
681  if (root == null) { continue; }
682 
683  if (root.Name.ToString().Equals("LinkedSubmarine", StringComparison.OrdinalIgnoreCase))
684  {
685  if (root.Attribute("location") == null) { continue; }
686 
687  // Check if this is our linked submarine
688  ushort dockingPortID = (ushort)root.GetAttributeInt("originallinkedto", 0);
689  if (dockingPortID > 0 && submarine.GetItems(alsoFromConnectedSubs: true).Any(item => item.ID == dockingPortID))
690  {
691  BuyUpgrade(prefab, category, loadedSub, level, submarine);
692  }
693  }
694  }
695 
696  return newLevel ?? -1;
697 
698  void SanityCheck(Upgrade newUpgrade, MapEntity target)
699  {
700  if (newLevel != null && newLevel != newUpgrade.Level)
701  {
702  // automatically fix this if it ever happens?
703  DebugConsole.AddWarning($"The upgrade {newUpgrade.Prefab.Name} in {target.Name} has a different level compared to other items! \n" +
704  $"Expected level was ${newLevel} but got {newUpgrade.Level} instead.",
705  newUpgrade.Prefab.ContentPackage);
706  }
707  }
708  }
709 
715  public int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info = null)
716  {
717  if (!Metadata.HasKey(FormatIdentifier(prefab, category))) { return GetPendingLevel(); }
718 
719  int maxLevel = info is null ? prefab.GetMaxLevelForCurrentSub() : prefab.GetMaxLevel(info);
720  return Math.Min(GetRealUpgradeLevel(prefab, category) + GetPendingLevel(), maxLevel);
721 
722  int GetPendingLevel()
723  {
724  PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
725  return upgrade?.Level ?? 0;
726  }
727  }
728 
732  public int GetRealUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
733  {
734  return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
735  }
736 
740  public int GetRealUpgradeLevelForSub(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo info)
741  {
742  return Math.Min(GetRealUpgradeLevel(prefab, category), prefab.GetMaxLevel(info));
743  }
744 
748  private void SetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, int level)
749  {
750  Metadata.SetValue(FormatIdentifier(prefab, category), level);
751  }
752 
753  public bool CanUpgradeSub()
754  {
755  return
756  Campaign.PendingSubmarineSwitch == null ||
757  Campaign.PendingSubmarineSwitch.Name == Submarine.MainSub.Info.Name;
758  }
759 
760  public void Save(XElement? parent)
761  {
762  if (parent == null) { return; }
763 
764  var upgradeManagerElement = new XElement("upgrademanager");
765  parent.Add(upgradeManagerElement);
766 
767  SavePendingUpgrades(upgradeManagerElement, PendingUpgrades);
768  }
769 
770  private static void SavePendingUpgrades(XElement? parent, List<PurchasedUpgrade> upgrades)
771  {
772  if (parent == null) { return; }
773 
774  DebugConsole.Log("Saving pending upgrades to save file...");
775  XElement upgradeElement = new XElement("PendingUpgrades");
776  foreach (var (prefab, category, level) in upgrades)
777  {
778  upgradeElement.Add(new XElement("PendingUpgrade",
779  new XAttribute("category", category.Identifier),
780  new XAttribute("prefab", prefab.Identifier),
781  new XAttribute("level", level)));
782  }
783 
784  DebugConsole.Log($"Saved {upgradeElement.Elements().Count()} pending upgrades.");
785  parent.Add(upgradeElement);
786  }
787 
788  private void LoadPendingUpgrades(XElement? element, bool isSingleplayer = true)
789  {
790  if (element is not { HasElements: true }) { return; }
791 
792  List<PurchasedUpgrade> pendingUpgrades = new List<PurchasedUpgrade>();
793 
794  foreach (XElement upgrade in element.Elements())
795  {
796  Identifier categoryIdentifier = upgrade.GetAttributeIdentifier("category", Identifier.Empty);
797  UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier);
798  if (categoryIdentifier.IsEmpty || category == null) { continue; }
799 
800  Identifier prefabIdentifier = upgrade.GetAttributeIdentifier("prefab", Identifier.Empty);
801  UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier);
802  if (prefabIdentifier.IsEmpty || prefab == null) { continue; }
803 
804  int level = upgrade.GetAttributeInt("level", -1);
805  if (level < 0) { continue; }
806 
807  pendingUpgrades.Add(new PurchasedUpgrade(prefab, category, level));
808  }
809 
810 #if CLIENT
811  if (isSingleplayer)
812  {
813  SetPendingUpgrades(pendingUpgrades);
814  }
815  else
816  {
817  loadedUpgrades = pendingUpgrades;
818  }
819 #else
820  SetPendingUpgrades(pendingUpgrades);
821 #endif
822  }
823 
824  public static void LogError(string text, Dictionary<string, object?> data, Exception? e = null)
825  {
826  string error = $"{text}\n";
827  foreach (var (label, value) in data)
828  {
829  error += $" - {label}: {value ?? "NULL"}\n";
830  }
831 
832  DebugConsole.ThrowError(error.TrimEnd('\n'), e);
833  }
834 
839  public void SetPendingUpgrades(List<PurchasedUpgrade> upgrades)
840  {
841  PendingUpgrades.Clear();
842  PendingUpgrades.AddRange(upgrades);
843  OnUpgradesChanged?.Invoke(this);
844  }
845 
846  public static void DebugLog(string msg, Color? color = null)
847  {
848 #if DEBUG
849  DebugConsole.NewMessage(msg, color ?? Color.GreenYellow);
850 #else
851  DebugConsole.Log(msg);
852 #endif
853  }
854 
855  private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) => PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category);
856 
857  private static Identifier FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $"upgrade.{category.Identifier}.{prefab.Identifier}".ToIdentifier();
858  }
859 }
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
readonly HashSet< ItemPrefab > AvailableSwaps
static ItemPrefab Find(string name, Identifier identifier)
override ImmutableHashSet< Identifier > Tags
LocationType Type
Definition: Location.cs:91
static MapEntityPrefab FindByIdentifier(Identifier identifier)
readonly Identifier Identifier
Definition: Prefab.cs:34
This class handles all upgrade logic. Storing, applying, checking and validation of upgrades.
void AddUpgradeExternally(UpgradePrefab prefab, UpgradeCategory category, int level)
Used for purchasing upgrades from outside the upgrade store. Doesn't deduct the credit,...
int GetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo? info=null)
Gets the progress that is shown on the store interface. Includes values stored in the metadata and Pe...
int GetRealUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category)
Gets the level of the upgrade that is stored in the metadata. May be higher than the apparent level o...
void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool isNetworkMessage=false, Client? client=null)
Purchases an item swap and handles logic for deducting the credit.
void CancelItemSwap(Item itemToRemove, bool force=false)
Cancels the currently pending item swap, or uninstalls the item if there's no swap pending
const bool UpgradeAlsoConnectedSubs
This one toggles whether or not connected submarines get upgraded too. Could probably be removed,...
void CreateUpgradeErrorMessage(string text, bool isSinglePlayer, Character character)
int GetRealUpgradeLevelForSub(UpgradePrefab prefab, UpgradeCategory category, SubmarineInfo info)
Gets the level of the upgrade that is stored in the metadata. Takes into account the limits of the pr...
static void LogError(string text, Dictionary< string, object?> data, Exception? e=null)
void SanityCheckUpgrades()
Validates that upgrade values stored in CampaignMetadata matches the values on the submarine and fixe...
UpgradeManager(CampaignMode campaign, XElement element, bool isSingleplayer)
void ApplyUpgrades()
Applies all our pending upgrades to the submarine.
void SetPendingUpgrades(List< PurchasedUpgrade > upgrades)
Used to sync the pending upgrades list in multiplayer.
void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category, bool force=false, Client? client=null)
Purchases an upgrade and handles logic for deducting the credit.
readonly List< PurchasedUpgrade > PurchasedUpgrades
This is used by the client to notify the server which upgrades are yet to be paid for.
GUISoundType
Definition: GUI.cs:21