3 using System.Collections.Generic;
8 using Microsoft.Xna.Framework;
12 internal class PurchasedUpgrade
14 public readonly UpgradeCategory Category;
15 public readonly UpgradePrefab Prefab;
18 public PurchasedUpgrade(UpgradePrefab upgradePrefab, UpgradeCategory category,
int level = 1)
21 Prefab = upgradePrefab;
25 public void Deconstruct(out UpgradePrefab prefab, out UpgradeCategory category, out
int level)
33 internal class PurchasedItemSwap
35 public readonly
Item ItemToRemove;
36 public readonly ItemPrefab ItemToInstall;
38 public PurchasedItemSwap(Item itemToRemove, ItemPrefab itemToInstall)
40 ItemToRemove = itemToRemove;
41 ItemToInstall = itemToInstall;
70 private List<PurchasedUpgrade>? loadedUpgrades;
80 public readonly List<PurchasedUpgrade>
PendingUpgrades =
new List<PurchasedUpgrade>();
84 private CampaignMetadata Metadata => Campaign.CampaignMetadata;
87 public readonly NamedEvent<UpgradeManager>
OnUpgradesChanged =
new NamedEvent<UpgradeManager>();
91 UpgradeCategory.Categories.ForEach(c => c.DeterminePrefabsThatAllowUpgrades());
93 DebugConsole.Log(
"Created brand new upgrade manager.");
99 DebugConsole.Log($
"Restored upgrade manager from save file, ({element.Elements().Count()} pending upgrades).");
103 if (element.Name.LocalName.Equals(
"pendingupgrades", StringComparison.OrdinalIgnoreCase))
105 LoadPendingUpgrades(element, isSingleplayer);
109 foreach (var subElement
in element.Elements())
111 switch (subElement.Name.ToString().ToLowerInvariant())
113 case "pendingupgrades":
114 LoadPendingUpgrades(subElement, isSingleplayer);
123 if (replacement ==
null)
126 if (replacement ==
null)
128 DebugConsole.ThrowError(
"Failed to determine swap cost for item \"{}\". Trying to uninstall the item but no replacement item found.");
134 if (replacement == item.
Prefab)
157 price -= item.
Prefab.SwappableItem.GetPrice(Campaign?.
Map?.CurrentLocation);
163 private DateTime lastUpgradeSpeak, lastErrorSpeak;
172 public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category,
bool force =
false,
Client? client =
null)
176 DebugConsole.ThrowError(
"Cannot upgrade when switching to another submarine.");
180 int price = prefab.Price.GetBuyPrice(prefab,
GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
182 int newLevel = currentLevel + 1;
184 int maxLevel = prefab.GetMaxLevelForCurrentSub();
185 if (currentLevel + 1 > maxLevel)
187 DebugConsole.ThrowError($
"Tried to purchase \"{prefab.Name}\" over the max level! ({newLevel} > {maxLevel}). The transaction has been cancelled.");
191 bool TryTakeResources(
Character character)
193 bool result = prefab.TryTakeResources(character, newLevel);
196 DebugConsole.ThrowError($
"Tried to purchase \"{prefab.Name}\" but the player does not have the required resources.");
206 if (!TryTakeResources(controlled)) {
return; }
208 case { IsClient:
true }:
211 case { IsServer:
true } when client?.
Character is { } character:
212 if (!TryTakeResources(character)) {
return; }
215 DebugConsole.ThrowError($
"Tried to purchase \"{prefab.Name}\" without a player.");
222 Location? location = Campaign.Map?.CurrentLocation;
223 LogError($
"Upgrade price is less than 0! ({price})",
224 new Dictionary<string, object?>
226 {
"Level", currentLevel },
228 {
"Upgrade", $
"{category.Identifier}.{prefab.Identifier}" },
229 {
"Location", location?.
Type },
230 {
"Reputation", $
"{location?.Reputation?.Value} / {location?.Reputation?.MaxReputation}" },
231 {
"Base Price", prefab.Price.BasePrice }
240 if (force || Campaign.TryPurchase(client, price))
245 if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
247 UpgradeNPCSpeak(TextManager.Get(
"Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
248 lastUpgradeSpeak = DateTime.Now;
252 GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineUpgrade, prefab.Identifier.Value);
254 PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
257 DebugLog($
"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for {price}", GUIStyle.Orange);
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}");
287 int maxLevel = prefab.GetMaxLevelForCurrentSub();
289 if (currentLevel + 1 > maxLevel) {
return; }
302 DebugConsole.ThrowError(
"Cannot swap items when switching to another submarine.");
305 if (itemToRemove ==
null)
307 DebugConsole.ThrowError($
"Cannot swap null item!");
312 DebugConsole.ThrowError($
"Cannot swap item \"{itemToRemove.Name}\" because it's set to be hidden in-game.");
317 DebugConsole.ThrowError($
"Cannot swap item \"{itemToRemove.Name}\" because it's configured to be non-swappable.");
320 if (!UpgradeCategory.Categories.Any(c => c.ItemTags.Any(t => itemToRemove.
HasTag(t)) && c.ItemTags.Any(t => itemToInstall.
Tags.Contains(t))))
322 DebugConsole.ThrowError($
"Failed to swap item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\" (not in the same upgrade category).");
328 DebugConsole.ThrowError($
"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!).");
332 if (swappableItem ==
null)
334 DebugConsole.ThrowError($
"Failed to swap item \"{itemToRemove.Name}\" (not configured as a swappable item).");
346 if (isNetworkMessage)
353 if (isNetworkMessage || Campaign.TryPurchase(client, price))
359 if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
361 UpgradeNPCSpeak(TextManager.Get(
"Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
362 lastUpgradeSpeak = DateTime.Now;
366 GameAnalyticsManager.AddMoneySpentEvent(price, GameAnalyticsManager.MoneySink.SubmarineWeapon, itemToInstall.
Identifier.Value);
368 foreach (
Item itemToSwap
in linkedItems)
371 if (itemToInstall !=
null && !itemToSwap.
AvailableSwaps.Contains(itemToInstall))
377 if (itemToSwap.
Prefab != itemToInstall && itemToInstall !=
null)
381 DebugLog($
"CLIENT: Swapped item \"{itemToSwap.Name}\" with \"{itemToInstall.Name}\".", Color.Orange);
385 DebugLog($
"CLIENT: Cancelled swapping the item \"{itemToSwap.Name}\" with \"{(itemToSwap.PendingItemSwap?.Name ?? null)}\".", Color.Orange);
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}");
405 DebugConsole.ThrowError(
"Cannot swap items when switching to another submarine.");
409 if (itemToRemove?.PendingItemSwap ==
null && (itemToRemove?.
Prefab.SwappableItem?.ReplacementOnUninstall.IsEmpty ??
true))
411 DebugConsole.ThrowError($
"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured).");
416 if (swappableItem ==
null)
418 DebugConsole.ThrowError($
"Failed to uninstall item \"{itemToRemove.Name}\" (not configured as a swappable item).");
425 if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
427 UpgradeNPCSpeak(TextManager.Get(
"Dialog.UpgradePurchased").Value, Campaign.IsSinglePlayer);
428 lastUpgradeSpeak = DateTime.Now;
434 foreach (
Item itemToCancel
in linkedItems)
440 DebugConsole.ThrowError($
"Failed to uninstall item \"{itemToCancel.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
445 DebugLog($
"Uninstalled item item \"{itemToCancel.Name}\".", Color.Orange);
451 DebugLog($
"Cancelled swapping the item \"{itemToCancel.Name}\" with \"{itemToCancel.PendingItemSwap.Name}\".", Color.Orange);
463 HashSet<Item> linkedItems =
new HashSet<Item>() { item };
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))
473 linkedItems.Add(linkedItem);
502 if (
Level.
Loaded is { Type: LevelData.LevelType.Outpost })
509 if (loadedUpgrades !=
null)
512 pendingUpgrades = loadedUpgrades;
516 DebugConsole.Log(
"Applying upgrades...");
517 foreach (var (prefab, category, level) in pendingUpgrades)
520 DebugConsole.Log($
" - {category.Identifier}.{prefab.Identifier} lvl. {level}, new: ({newLevel})");
525 loadedUpgrades?.Clear();
526 loadedUpgrades =
null;
532 if (lastErrorSpeak == DateTime.MinValue || lastErrorSpeak.AddSeconds(10) < DateTime.Now)
534 UpgradeNPCSpeak(text, isSinglePlayer, character);
535 lastErrorSpeak = DateTime.Now;
554 partial
void UpgradeNPCSpeak(
string text,
bool isSinglePlayer,
Character? character =
null);
564 if (submarine is
null) {
return; }
569 foreach (UpgradeCategory category
in UpgradeCategory.Categories)
571 foreach (UpgradePrefab prefab
in UpgradePrefab.Prefabs)
573 if (!prefab.IsWallUpgrade) {
continue; }
574 TryFixUpgrade(wall, category, prefab);
582 foreach (UpgradeCategory category
in UpgradeCategory.Categories)
584 foreach (UpgradePrefab prefab
in UpgradePrefab.Prefabs)
586 TryFixUpgrade(item, category, prefab);
591 void TryFixUpgrade(
MapEntity entity, UpgradeCategory category, UpgradePrefab prefab)
593 if (!category.CanBeApplied(entity, prefab)) {
return; }
596 int maxLevel = submarine.Info is { } info ? prefab.GetMaxLevel(info) : prefab.MaxLevel;
597 if (maxLevel < level) { level = maxLevel; }
599 if (level == 0) {
return; }
601 Upgrade? upgrade = entity.
GetUpgrade(prefab.Identifier);
603 if (upgrade ==
null || upgrade.Level != level)
605 DebugLog($
"{entity.Prefab.Name} has incorrect \"{prefab.Name}\" level! Expected {level} but got {upgrade?.Level ?? 0}. Fixing...");
611 private static void FixUpgradeOnItem(
ISerializableEntity target, UpgradePrefab prefab,
int level)
616 if (level == 0) {
return; }
618 mapEntity.SetUpgrade(
new Upgrade(target, prefab, level),
false);
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)
633 if (parentSub ==
null)
635 upgradedSubs.Clear();
637 upgradedSubs.Add(submarine);
639 int? newLevel =
null;
640 if (category.IsWallUpgrade)
644 Upgrade upgrade =
new Upgrade(structure, prefab, level);
645 structure.AddUpgrade(upgrade, createNetworkEvent:
false);
647 Upgrade? newUpgrade = structure.GetUpgrade(prefab.Identifier);
648 if (newUpgrade !=
null)
650 SanityCheck(newUpgrade, structure);
651 newLevel ??= newUpgrade.Level;
659 if (category.CanBeApplied(item, prefab))
661 Upgrade upgrade =
new Upgrade(item, prefab, level);
662 item.AddUpgrade(upgrade, createNetworkEvent:
false);
664 Upgrade? newUpgrade = item.GetUpgrade(prefab.Identifier);
665 if (newUpgrade !=
null)
667 SanityCheck(newUpgrade, item);
668 newLevel ??= newUpgrade.Level;
674 foreach (Submarine loadedSub
in Submarine.Loaded)
676 if (loadedSub == parentSub || loadedSub == submarine) {
continue; }
677 if (loadedSub.Info?.Type !=
SubmarineType.Player) {
continue; }
678 if (upgradedSubs.Contains(loadedSub)) {
continue; }
680 XElement? root = loadedSub.Info?.SubmarineElement;
681 if (root ==
null) {
continue; }
683 if (root.Name.ToString().Equals(
"LinkedSubmarine", StringComparison.OrdinalIgnoreCase))
685 if (root.Attribute(
"location") ==
null) {
continue; }
688 ushort dockingPortID = (ushort)root.GetAttributeInt(
"originallinkedto", 0);
689 if (dockingPortID > 0 && submarine.GetItems(alsoFromConnectedSubs:
true).Any(item => item.ID == dockingPortID))
691 BuyUpgrade(prefab, category, loadedSub, level, submarine);
696 return newLevel ?? -1;
698 void SanityCheck(Upgrade newUpgrade, MapEntity target)
700 if (newLevel !=
null && newLevel != newUpgrade.Level)
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);
717 if (!Metadata.HasKey(FormatIdentifier(prefab, category))) {
return GetPendingLevel(); }
719 int maxLevel = info is
null ? prefab.GetMaxLevelForCurrentSub() : prefab.GetMaxLevel(info);
722 int GetPendingLevel()
724 PurchasedUpgrade? upgrade = FindMatchingUpgrade(prefab, category);
725 return upgrade?.Level ?? 0;
734 return !Metadata.HasKey(FormatIdentifier(prefab, category)) ? 0 : Metadata.GetInt(FormatIdentifier(prefab, category), 0);
748 private void SetUpgradeLevel(UpgradePrefab prefab, UpgradeCategory category,
int level)
750 Metadata.SetValue(FormatIdentifier(prefab, category), level);
756 Campaign.PendingSubmarineSwitch ==
null ||
760 public void Save(XElement? parent)
762 if (parent ==
null) {
return; }
764 var upgradeManagerElement =
new XElement(
"upgrademanager");
765 parent.Add(upgradeManagerElement);
770 private static void SavePendingUpgrades(XElement? parent, List<PurchasedUpgrade> upgrades)
772 if (parent ==
null) {
return; }
774 DebugConsole.Log(
"Saving pending upgrades to save file...");
775 XElement upgradeElement =
new XElement(
"PendingUpgrades");
776 foreach (var (prefab, category, level) in upgrades)
778 upgradeElement.Add(
new XElement(
"PendingUpgrade",
779 new XAttribute(
"category", category.Identifier),
780 new XAttribute(
"prefab", prefab.Identifier),
781 new XAttribute(
"level", level)));
784 DebugConsole.Log($
"Saved {upgradeElement.Elements().Count()} pending upgrades.");
785 parent.Add(upgradeElement);
788 private void LoadPendingUpgrades(XElement? element,
bool isSingleplayer =
true)
790 if (element is not { HasElements:
true }) {
return; }
792 List<PurchasedUpgrade> pendingUpgrades =
new List<PurchasedUpgrade>();
794 foreach (XElement upgrade
in element.Elements())
796 Identifier categoryIdentifier = upgrade.GetAttributeIdentifier(
"category", Identifier.Empty);
797 UpgradeCategory? category = UpgradeCategory.Find(categoryIdentifier);
798 if (categoryIdentifier.IsEmpty || category ==
null) {
continue; }
800 Identifier prefabIdentifier = upgrade.GetAttributeIdentifier(
"prefab", Identifier.Empty);
801 UpgradePrefab? prefab = UpgradePrefab.Find(prefabIdentifier);
802 if (prefabIdentifier.IsEmpty || prefab ==
null) {
continue; }
804 int level = upgrade.GetAttributeInt(
"level", -1);
805 if (level < 0) {
continue; }
807 pendingUpgrades.Add(
new PurchasedUpgrade(prefab, category, level));
817 loadedUpgrades = pendingUpgrades;
824 public static void LogError(
string text, Dictionary<string, object?> data, Exception? e =
null)
826 string error = $
"{text}\n";
827 foreach (var (label, value) in data)
829 error += $
" - {label}: {value ?? "NULL
"}\n";
832 DebugConsole.ThrowError(error.TrimEnd(
'\n'), e);
846 public static void DebugLog(
string msg, Color? color =
null)
849 DebugConsole.NewMessage(msg, color ?? Color.GreenYellow);
851 DebugConsole.Log(msg);
855 private PurchasedUpgrade? FindMatchingUpgrade(UpgradePrefab prefab, UpgradeCategory category) =>
PendingUpgrades.Find(u => u.Prefab == prefab && u.Category == category);
857 private static Identifier FormatIdentifier(UpgradePrefab prefab, UpgradeCategory category) => $
"upgrade.{category.Identifier}.{prefab.Identifier}".ToIdentifier();
Character(CharacterPrefab prefab, Vector2 position, string seed, CharacterInfo characterInfo=null, ushort id=Entity.NullEntityID, bool isRemotePlayer=false, RagdollParams ragdollParams=null, bool spawnInitialItems=true)
static Character? Controlled
static GameSession?? GameSession
static NetworkMember NetworkMember
readonly HashSet< ItemPrefab > AvailableSwaps
ItemPrefab PendingItemSwap
bool HasTag(Identifier tag)
static ItemPrefab Find(string name, Identifier identifier)
override ImmutableHashSet< Identifier > Tags
SwappableItem SwappableItem
readonly MapEntityPrefab Prefab
readonly List< MapEntity > linkedTo
Upgrade GetUpgrade(Identifier identifier)
static MapEntityPrefab FindByIdentifier(Identifier identifier)
readonly Identifier Identifier
int GetPrice(Location location=null)
readonly Identifier ReplacementOnUninstall
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 DetermineItemSwapCost(Item item, ItemPrefab? replacement)
readonly List< PurchasedUpgrade > PendingUpgrades
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)
void Save(XElement? parent)
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...
UpgradeManager(CampaignMode campaign)
readonly NamedEvent< UpgradeManager > OnUpgradesChanged
static void LogError(string text, Dictionary< string, object?> data, Exception? e=null)
static void DebugLog(string msg, Color? color=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< PurchasedItemSwap > PurchasedItemSwaps
static ICollection< Item > GetLinkedItemsToSwap(Item item)
readonly List< PurchasedUpgrade > PurchasedUpgrades
This is used by the client to notify the server which upgrades are yet to be paid for.