public void CancelItemSwap(Item itemToRemove, bool force = false)
        {
            if (!CanUpgradeSub())
            {
                DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
                return;
            }

            if (itemToRemove?.PendingItemSwap == null && string.IsNullOrEmpty(itemToRemove?.Prefab.SwappableItem?.ReplacementOnUninstall))
            {
                DebugConsole.ThrowError($"Cannot uninstall item \"{itemToRemove?.Name}\" (no replacement item configured).");
                return;
            }

            SwappableItem?swappableItem = itemToRemove.Prefab.SwappableItem;

            if (swappableItem == null)
            {
                DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\" (not configured as a swappable item).");
                return;
            }

            if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
            {
                // only make the NPC speak if more than 5 minutes have passed since the last purchased service
                if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
                {
                    UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
                    lastUpgradeSpeak = DateTime.Now;
                }
            }

            if (itemToRemove.PendingItemSwap == null)
            {
                var replacement = MapEntityPrefab.Find("", swappableItem.ReplacementOnUninstall) as ItemPrefab;
                if (replacement == null)
                {
                    DebugConsole.ThrowError($"Failed to uninstall item \"{itemToRemove.Name}\". Could not find the replacement item \"{swappableItem.ReplacementOnUninstall}\".");
                    return;
                }
                PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
                PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, replacement));
                DebugLog($"Uninstalled item item \"{itemToRemove.Name}\".", Color.Orange);
                itemToRemove.PendingItemSwap = replacement;
            }
            else
            {
                PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
                DebugLog($"Cancelled swapping the item \"{itemToRemove.Name}\" with \"{itemToRemove.PendingItemSwap.Name}\".", Color.Orange);
                itemToRemove.PendingItemSwap = null;
            }
#if CLIENT
            OnUpgradesChanged?.Invoke();
#endif
        }
        public void PurchaseUpgrade(UpgradePrefab prefab, UpgradeCategory category)
        {
            if (!CanUpgradeSub())
            {
                DebugConsole.ThrowError("Cannot upgrade when switching to another submarine.");
                return;
            }

            int price        = prefab.Price.GetBuyprice(GetUpgradeLevel(prefab, category), Campaign.Map?.CurrentLocation);
            int currentLevel = GetUpgradeLevel(prefab, category);

            if (currentLevel + 1 > prefab.MaxLevel)
            {
                DebugConsole.ThrowError($"Tried to purchase \"{prefab.Name}\" over the max level! ({currentLevel + 1} > {prefab.MaxLevel}). The transaction has been cancelled.");
                return;
            }

            if (price < 0)
            {
                Location?location = Campaign.Map?.CurrentLocation;
                LogError($"Upgrade price is less than 0! ({price})",
                         new Dictionary <string, object?>
                {
                    { "Level", currentLevel },
                    { "Saved Level", GetRealUpgradeLevel(prefab, category) },
                    { "Upgrade", $"{category.Identifier}.{prefab.Identifier}" },
                    { "Location", location?.Type },
                    { "Reputation", $"{location?.Reputation?.Value} / {location?.Reputation?.MaxReputation}" },
                    { "Base Price", prefab.Price.BasePrice }
                });
            }

            if (Campaign.Money > price)
            {
                if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
                {
                    // only make the NPC speak if more than 5 minutes have passed since the last purchased service
                    if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
                    {
                        UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
                        lastUpgradeSpeak = DateTime.Now;
                    }
                }

                Campaign.Money -= price;
                spentMoney     += price;

                PurchasedUpgrade?upgrade = FindMatchingUpgrade(prefab, category);

#if CLIENT
                DebugLog($"CLIENT: Purchased level {GetUpgradeLevel(prefab, category) + 1} {category.Name}.{prefab.Name} for ${price}", GUI.Style.Orange);
#endif

                if (upgrade == null)
                {
                    PendingUpgrades.Add(new PurchasedUpgrade(prefab, category));
                }
                else
                {
                    upgrade.Level++;
                }
#if CLIENT
                // tell the server that this item is yet to be paid for server side
                PurchasedUpgrades.Add(new PurchasedUpgrade(prefab, category));
#endif
                OnUpgradesChanged?.Invoke();
            }
            else
            {
                DebugConsole.ThrowError("Tried to purchase an upgrade with insufficient funds, the transaction has not been completed.\n" +
                                        $"Upgrade: {prefab.Name}, Cost: {price}, Have: {Campaign.Money}");
            }
        }
        public void PurchaseItemSwap(Item itemToRemove, ItemPrefab itemToInstall, bool force = false)
        {
            if (!CanUpgradeSub())
            {
                DebugConsole.ThrowError("Cannot swap items when switching to another submarine.");
                return;
            }
            if (itemToRemove == null)
            {
                DebugConsole.ThrowError($"Cannot swap null item!");
                return;
            }
            if (itemToRemove.HiddenInGame)
            {
                DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's set to be hidden in-game.");
                return;
            }
            if (!itemToRemove.AllowSwapping)
            {
                DebugConsole.ThrowError($"Cannot swap item \"{itemToRemove.Name}\" because it's configured to be non-swappable.");
                return;
            }
            if (!UpgradeCategory.Categories.Any(c => c.ItemTags.Any(t => itemToRemove.HasTag(t)) && c.ItemTags.Any(t => itemToInstall.Tags.Contains(t))))
            {
                DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\" (not in the same upgrade category).");
                return;
            }

            if (itemToRemove.prefab == itemToInstall)
            {
                DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (trying to swap with the same item!).");
                return;
            }
            SwappableItem?swappableItem = itemToRemove.Prefab.SwappableItem;

            if (swappableItem == null)
            {
                DebugConsole.ThrowError($"Failed to swap item \"{itemToRemove.Name}\" (not configured as a swappable item).");
                return;
            }

            int price = 0;

            if (!itemToRemove.AvailableSwaps.Contains(itemToInstall))
            {
                price = itemToInstall.SwappableItem.GetPrice(Campaign.Map?.CurrentLocation);
            }

            if (force)
            {
                price = 0;
            }

            if (Campaign.Money >= price)
            {
                PurchasedItemSwaps.RemoveAll(p => p.ItemToRemove == itemToRemove);
                if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
                {
                    // only make the NPC speak if more than 5 minutes have passed since the last purchased service
                    if (lastUpgradeSpeak == DateTime.MinValue || lastUpgradeSpeak.AddMinutes(5) < DateTime.Now)
                    {
                        UpgradeNPCSpeak(TextManager.Get("Dialog.UpgradePurchased"), Campaign.IsSinglePlayer);
                        lastUpgradeSpeak = DateTime.Now;
                    }
                }

                Campaign.Money -= price;

                itemToRemove.AvailableSwaps.Add(itemToRemove.Prefab);
                if (itemToInstall != null && !itemToRemove.AvailableSwaps.Contains(itemToInstall))
                {
                    itemToRemove.PurchasedNewSwap = true;
                    itemToRemove.AvailableSwaps.Add(itemToInstall);
                }

                if (itemToRemove.Prefab != itemToInstall && itemToInstall != null)
                {
                    itemToRemove.PendingItemSwap = itemToInstall;
                    PurchasedItemSwaps.Add(new PurchasedItemSwap(itemToRemove, itemToInstall));
                    DebugLog($"CLIENT: Swapped item \"{itemToRemove.Name}\" with \"{itemToInstall.Name}\".", Color.Orange);
                }
                else
                {
                    DebugLog($"CLIENT: Cancelled swapping the item \"{itemToRemove.Name}\" with \"{(itemToRemove.PendingItemSwap?.Name ?? null)}\".", Color.Orange);
                }
                OnUpgradesChanged?.Invoke();
            }
            else
            {
                DebugConsole.ThrowError("Tried to swap an item with insufficient funds, the transaction has not been completed.\n" +
                                        $"Item to remove: {itemToRemove.Name}, Item to install: {itemToInstall.Name}, Cost: {price}, Have: {Campaign.Money}");
            }
        }