From aba0cac442ba33213248efff499cc559aab14bb7 Mon Sep 17 00:00:00 2001 From: NathanKell Date: Sun, 25 Jun 2023 18:44:53 -0700 Subject: [PATCH] Convert UnlockSubsidyHandler to UnlockCreditHandler - now only track total unlock credit. Add an upgrade script to transition. --- GameData/RP-1/Localization/en-us.cfg | 2 +- GameData/RP-1/Localization/zh-cn.cfg | 2 +- Source/Avionics/ProceduralAvionicsWindow.cs | 2 +- Source/Harmony/PartListTooltip.cs | 2 +- Source/Harmony/RDController.cs | 3 +- Source/Harmony/RDPartList.cs | 4 +- Source/Harmony/RealFuels.cs | 4 +- Source/Harmony/ScienceWidget.cs | 2 +- .../BuildItems/TechItem.cs | 4 +- .../Utilities/Utilities.cs | 2 +- .../VesselBuildValidator.cs | 4 +- Source/Properties/AssemblyInfo.cs | 6 +- Source/RP0.csproj | 4 +- Source/UnlockCreditHandler.cs | 259 +++++++++ Source/UnlockSubsidyHandler.cs | 518 ------------------ Source/UpgradeScripts/UpgradeUnlockCredit.cs | 42 ++ 16 files changed, 322 insertions(+), 538 deletions(-) create mode 100644 Source/UnlockCreditHandler.cs delete mode 100644 Source/UnlockSubsidyHandler.cs create mode 100644 Source/UpgradeScripts/UpgradeUnlockCredit.cs diff --git a/GameData/RP-1/Localization/en-us.cfg b/GameData/RP-1/Localization/en-us.cfg index 96632870483..f8cdc658e44 100644 --- a/GameData/RP-1/Localization/en-us.cfg +++ b/GameData/RP-1/Localization/en-us.cfg @@ -110,7 +110,7 @@ #rp0_FacilityContextMenu_UpgradeInProgress = Construction already in progress! // Unlock Credit - #rp0_UnlockCredit_NodeInfo = Unlock Credit: <<1>>\nIncluding Parents: <<2>> + #rp0_UnlockCredit_NodeInfo = Unlock Credit: <<1>> #rp0_UnlockCredit_CostAfterCredit = Cost after Credit: √<<1>> diff --git a/GameData/RP-1/Localization/zh-cn.cfg b/GameData/RP-1/Localization/zh-cn.cfg index bae103d1c5a..e503e2b6ecd 100644 --- a/GameData/RP-1/Localization/zh-cn.cfg +++ b/GameData/RP-1/Localization/zh-cn.cfg @@ -110,7 +110,7 @@ Localization #rp0_FacilityContextMenu_UpgradeInProgress = 正在进行建设中! // Unlock Credit - #rp0_UnlockCredit_NodeInfo = 解锁的贷款额度: <<1>>\nIncluding Parents: <<2>> + #rp0_UnlockCredit_NodeInfo = 解锁的贷款额度: <<1>> #rp0_UnlockCredit_CostAfterCredit = 除去贷款后价格: √<<1>> diff --git a/Source/Avionics/ProceduralAvionicsWindow.cs b/Source/Avionics/ProceduralAvionicsWindow.cs index de4cdf81559..457a28f1dc1 100644 --- a/Source/Avionics/ProceduralAvionicsWindow.cs +++ b/Source/Avionics/ProceduralAvionicsWindow.cs @@ -306,7 +306,7 @@ private bool DrawUnlockButton(string curCfgName, ProceduralAvionicsTechNode tech if (unlockCost <= 0) return switchedConfig; var cmq = CurrencyModifierQueryRP0.RunQuery(TransactionReasonsRP0.PartOrUpgradeUnlock, -unlockCost, 0d, 0d); double trueCost = -cmq.GetTotal(CurrencyRP0.Funds); - double creditToUse = Math.Min(trueCost, UnlockSubsidyHandler.Instance.GetCreditAmount(techNode.TechNodeName)); + double creditToUse = Math.Min(trueCost, UnlockCreditHandler.Instance.GetCreditAmount(techNode.TechNodeName)); cmq.AddDeltaAuthorized(CurrencyRP0.Funds, creditToUse); GUI.enabled = techNode.IsAvailable && cmq.CanAfford(); _gc.text = $"Unlock ({BuildCostString(Math.Max(0d, trueCost - creditToUse), trueCost)})"; diff --git a/Source/Harmony/PartListTooltip.cs b/Source/Harmony/PartListTooltip.cs index cd79cd63b00..9722b5c7faa 100644 --- a/Source/Harmony/PartListTooltip.cs +++ b/Source/Harmony/PartListTooltip.cs @@ -56,7 +56,7 @@ private static void PatchButtons(PartListTooltip __instance, AvailablePart avail else if (__instance.buttonPurchase.gameObject.activeSelf || __instance.buttonPurchaseRed.gameObject.activeSelf) { var cmq = CurrencyModifierQueryRP0.RunQuery(TransactionReasonsRP0.PartOrUpgradeUnlock, -eCost, 0d, 0d); - cmq.AddDeltaAuthorized(CurrencyRP0.Funds, Math.Min(-cmq.GetTotal(CurrencyRP0.Funds), UnlockSubsidyHandler.Instance.GetCreditAmount(techID))); + cmq.AddDeltaAuthorized(CurrencyRP0.Funds, Math.Min(-cmq.GetTotal(CurrencyRP0.Funds), UnlockCreditHandler.Instance.GetCreditAmount(techID))); // If we still can't afford, bail without setting tooltip if (!cmq.CanAfford()) return; diff --git a/Source/Harmony/RDController.cs b/Source/Harmony/RDController.cs index 1595af88570..997a4c65b56 100644 --- a/Source/Harmony/RDController.cs +++ b/Source/Harmony/RDController.cs @@ -31,8 +31,7 @@ internal static void Postfix_ShowNodePanel(RDController __instance, RDNode node) if (showCredit) { extraText = Localizer.Format("#rp0_UnlockCredit_NodeInfo", - UnlockSubsidyHandler.Instance.GetLocalCreditAmount(node.tech.techID).ToString("N0"), - UnlockSubsidyHandler.Instance.GetCreditAmount(node.tech.techID).ToString("N0")) + "\n"; + UnlockCreditHandler.Instance.GetCreditAmount(node.tech.techID).ToString("N0")) + "\n"; } else { diff --git a/Source/Harmony/RDPartList.cs b/Source/Harmony/RDPartList.cs index 078b40a497e..06abc0bcd88 100644 --- a/Source/Harmony/RDPartList.cs +++ b/Source/Harmony/RDPartList.cs @@ -29,7 +29,7 @@ private static bool Prefix_AddPartListItem(RDPartList __instance, AvailablePart if (!cmq.CanAfford()) { // try again, with credit - cmq.AddDeltaAuthorized(CurrencyRP0.Funds, System.Math.Min(-cmq.GetTotal(CurrencyRP0.Funds), UnlockSubsidyHandler.Instance.GetCreditAmount(part.TechRequired))); + cmq.AddDeltaAuthorized(CurrencyRP0.Funds, System.Math.Min(-cmq.GetTotal(CurrencyRP0.Funds), UnlockCreditHandler.Instance.GetCreditAmount(part.TechRequired))); if (!cmq.CanAfford()) { // still can't afford, so use the can't afford color @@ -75,7 +75,7 @@ private static bool Prefix_AddUpgradeListItem(RDPartList __instance, PartUpgrade // BUT if we can't afford normally, but can with credit let's fix the coloring. if (!cmq.CanAfford()) { - cmq.AddDeltaAuthorized(CurrencyRP0.Funds, System.Math.Min(-cmq.GetTotal(CurrencyRP0.Funds), UnlockSubsidyHandler.Instance.GetCreditAmount(upgrade.techRequired))); + cmq.AddDeltaAuthorized(CurrencyRP0.Funds, System.Math.Min(-cmq.GetTotal(CurrencyRP0.Funds), UnlockCreditHandler.Instance.GetCreditAmount(upgrade.techRequired))); if (!cmq.CanAfford()) { cmq = CurrencyModifierQueryRP0.RunQuery(TransactionReasonsRP0.PartOrUpgradeUnlock, -upgrade.entryCost, 0d, 0d); diff --git a/Source/Harmony/RealFuels.cs b/Source/Harmony/RealFuels.cs index 4238040406e..951ebbdf7c5 100644 --- a/Source/Harmony/RealFuels.cs +++ b/Source/Harmony/RealFuels.cs @@ -71,14 +71,14 @@ internal static bool PatchedPurchaseConfig(RealFuels.EntryCostManager __instance var cmq = CurrencyModifierQueryRP0.RunQuery(TransactionReasonsRP0.PartOrUpgradeUnlock, -cfgCost, 0d, 0d); double postCMQcost = -cmq.GetTotal(CurrencyRP0.Funds); double invertCMQop = cfgCost / postCMQcost; - double credit = UnlockSubsidyHandler.Instance.GetCreditAmount(techNode); + double credit = UnlockCreditHandler.Instance.GetCreditAmount(techNode); cmq.AddDeltaAuthorized(CurrencyRP0.Funds, credit); if (!cmq.CanAfford()) { return false; } - double excessCost = UnlockSubsidyHandler.Instance.SpendCredit(techNode, postCMQcost); + double excessCost = UnlockCreditHandler.Instance.SpendCredit(techNode, postCMQcost); if (excessCost > 0d) { Funding.Instance.AddFunds(-excessCost * invertCMQop, TransactionReasonsRP0.PartOrUpgradeUnlock.Stock()); diff --git a/Source/Harmony/ScienceWidget.cs b/Source/Harmony/ScienceWidget.cs index edc0aaf8c06..792e61bf6c5 100644 --- a/Source/Harmony/ScienceWidget.cs +++ b/Source/Harmony/ScienceWidget.cs @@ -27,7 +27,7 @@ private static string GetTooltipText() { return Localizer.Format("#rp0_Widgets_Science_Tooltip", System.Math.Max(0, KerbalConstructionTimeData.Instance.SciPointsTotal).ToString("N1"), - UnlockSubsidyHandler.Instance.TotalCredit.ToString("N0")); + UnlockCreditHandler.Instance.TotalCredit.ToString("N0")); } } } diff --git a/Source/KerbalConstructionTime/BuildItems/TechItem.cs b/Source/KerbalConstructionTime/BuildItems/TechItem.cs index ead6e9bf558..2f1b39de5ac 100644 --- a/Source/KerbalConstructionTime/BuildItems/TechItem.cs +++ b/Source/KerbalConstructionTime/BuildItems/TechItem.cs @@ -281,11 +281,11 @@ public double IncrementProgress(double UTDiff) KCTGameStates.RecalculateBuildRates(); // this might change other rates double portion = toGo / increment; - RP0.UnlockSubsidyHandler.Instance.IncrementCreditTime(techID, portion * UTDiff); + RP0.UnlockCreditHandler.Instance.IncrementCreditTime(techID, portion * UTDiff); return (1d - portion) * UTDiff; } - RP0.UnlockSubsidyHandler.Instance.IncrementCreditTime(techID, UTDiff); + RP0.UnlockCreditHandler.Instance.IncrementCreditTime(techID, UTDiff); return 0d; } diff --git a/Source/KerbalConstructionTime/Utilities/Utilities.cs b/Source/KerbalConstructionTime/Utilities/Utilities.cs index 109e9a8ec2b..384014ec68b 100644 --- a/Source/KerbalConstructionTime/Utilities/Utilities.cs +++ b/Source/KerbalConstructionTime/Utilities/Utilities.cs @@ -675,7 +675,7 @@ public static int FindUnlockCost(List availableParts) public static void UnlockExperimentalParts(List availableParts) { // this will spend the funds, which is why we set costsFunds=false below. - RP0.UnlockSubsidyHandler.Instance.SpendCreditAndCost(availableParts); + RP0.UnlockCreditHandler.Instance.SpendCreditAndCost(availableParts); foreach (var ap in availableParts) { diff --git a/Source/KerbalConstructionTime/VesselBuildValidator.cs b/Source/KerbalConstructionTime/VesselBuildValidator.cs index 2c01ba09f5c..0f85eeb5bdf 100644 --- a/Source/KerbalConstructionTime/VesselBuildValidator.cs +++ b/Source/KerbalConstructionTime/VesselBuildValidator.cs @@ -169,7 +169,7 @@ private void ProcessPartAvailability(BuildListVessel blv) var cmq = CurrencyModifierQueryRP0.RunQuery(TransactionReasonsRP0.PartOrUpgradeUnlock, -unlockCost, 0d, 0d); double postCMQUnlockCost = -cmq.GetTotal(CurrencyRP0.Funds); - double credit = UnlockSubsidyHandler.Instance.GetCreditAmount(partList); + double credit = UnlockCreditHandler.Instance.GetCreditAmount(partList); double spentCredit = Math.Min(postCMQUnlockCost, credit); cmq.AddDeltaAuthorized(CurrencyRP0.Funds, spentCredit); @@ -374,7 +374,7 @@ private DialogGUIBase[] ConstructPartConfigErrorsUI(Dictionary - + @@ -191,6 +191,7 @@ + @@ -392,6 +393,7 @@ + diff --git a/Source/UnlockCreditHandler.cs b/Source/UnlockCreditHandler.cs new file mode 100644 index 00000000000..10c2c1d6a1b --- /dev/null +++ b/Source/UnlockCreditHandler.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using UnityEngine; +using KerbalConstructionTime; +using System; + +namespace RP0 +{ + [KSPScenario(ScenarioCreationOptions.AddToAllGames, new GameScenes[] { GameScenes.EDITOR, GameScenes.FLIGHT, GameScenes.SPACECENTER, GameScenes.TRACKSTATION })] + public class UnlockCreditHandler : ScenarioModule + { + public static UnlockCreditHandler Instance { get; private set; } + + [KSPField(isPersistant = true)] + private double _totalCredit = 0; + + public double TotalCredit => _totalCredit; + + public double GetCreditAmount(string tech) => _totalCredit; + public double GetCreditAmount(List partList) => _totalCredit; + + /// + /// Note this is CMQ-neutral. + /// + /// + /// + public void IncrementCreditTime(string tech, double UT) => IncrementCredit(tech, UT * MaintenanceHandler.Instance.ResearchSalaryPerDay / 86400d * MaintenanceHandler.Settings.researcherSalaryToUnlockCredit); + + /// + /// Note this is CMQ-neutral. + /// + /// + /// + public void IncrementCredit(string tech, double amount) + { + // Will also catch NaN + if (!(amount > 0)) + return; + + _totalCredit += amount; + } + + private void FillECMs(string ecmName, Dictionary ecmToCost) + { + // if this ECM is unlocked, we're done. + if (RealFuels.EntryCostDatabase.IsUnlocked(ecmName)) + return; + + var holder = RealFuels.EntryCostDatabase.GetHolder(ecmName); + if (holder == null) + return; + + // Fill from scratch and recurse. + ecmToCost[holder.name] = holder.cost; + foreach (var childName in holder.children) + FillECMs(childName, ecmToCost); + } + + private void AddPartToDicts(AvailablePart ap, Dictionary ecmToCost) + { + string sanitizedName = RealFuels.Utilities.SanitizeName(ap.name); + + if (RealFuels.EntryCostDatabase.GetHolder(sanitizedName) != null) + { + FillECMs(sanitizedName, ecmToCost); + return; + } + + // It shouldn't already contain the key, but you never know. + if (!ecmToCost.ContainsKey(sanitizedName)) + { + ecmToCost[sanitizedName] = ap.entryCost; + } + } + + public void SpendCreditAndCost(List parts) + { + // This is going to be expensive, because we have to chase down all the ECMs. + double cmqMultiplier = -CurrencyUtils.Funds(TransactionReasonsRP0.PartOrUpgradeUnlock, -1d); + if (cmqMultiplier == 0d) + return; + + double recipCMQMult = 1d / cmqMultiplier; + + Dictionary ecmToCost = new Dictionary(); + foreach (var p in parts) + AddPartToDicts(p, ecmToCost); + + double total = 0d; + foreach (var kvp in ecmToCost) + total += kvp.Value; + + total *= cmqMultiplier; + double excess = SpendCredit(total); + + // Finally, pay for the excess. + if (excess > 0d) + Funding.Instance.AddFunds(-excess * recipCMQMult, TransactionReasonsRP0.PartOrUpgradeUnlock.Stock()); + } + + public double SpendCredit(string tech, double cost) => SpendCredit(cost); + + public double SpendCredit(double cost) + { + if (_totalCredit == 0d) + return cost; + + double excessCost; + if (_totalCredit < cost) + { + excessCost = cost - _totalCredit; + _totalCredit = 0; + } + else + { + excessCost = 0d; + _totalCredit -= cost; + } + return excessCost; + } + + /// + /// Transforms entrycost to post-strategy entrycost, spends credit, + /// and returns remaining (unsubsidized) cost + /// + /// + /// + /// + private float ProcessCredit(float entryCost, string tech) + { + if (entryCost == 0f) + return 0f; + + double postCMQCost = -CurrencyUtils.Funds(TransactionReasonsRP0.PartOrUpgradeUnlock, -entryCost); + if (double.IsNaN(postCMQCost)) + { + Debug.LogError("[RP-0] CMQ for a credit unlock returned NaN, ignoring and going back to regular cost."); + postCMQCost = entryCost; + } + else if (postCMQCost == 0d) + { + Debug.LogError("[RP-0] CMQ for a credit unlock returned 0, not spending any credit."); + return 0f; + } + + // Actually spend credit and get (post-effect) remainder + double remainingCost = SpendCredit(postCMQCost); + + // Refresh description to show new credit remaining + if (KSP.UI.Screens.RDController.Instance != null) + KSP.UI.Screens.RDController.Instance.ShowNodePanel(KSP.UI.Screens.RDController.Instance.node_selected); + + //return the remainder after transforming to pre-effect numbers + return (float)(remainingCost * (entryCost / postCMQCost)); + } + + private void OnPartPurchased(AvailablePart ap) + { + UnlockCreditUtility.StoredPartEntryCost = ap.entryCost; + if (ap.costsFunds) + { + int remainingCost = (int)ProcessCredit(ap.entryCost, ap.TechRequired); + ap.SetEntryCost(remainingCost); + } + } + + private void OnPartUpgradePurchased(PartUpgradeHandler.Upgrade up) + { + UnlockCreditUtility.StoredUpgradeEntryCost = up.entryCost; + float remainingCost = ProcessCredit(up.entryCost, up.techRequired); + up.entryCost = remainingCost; + } + + public override void OnLoad(ConfigNode node) + { + } + + public override void OnSave(ConfigNode node) + { + } + + public override void OnAwake() + { + base.OnAwake(); + + if (Instance != null) + Destroy(Instance); + + Instance = this; + } + + public void Start() + { + // This runs after KSP's Funding's OnAwake and thus we can bind after (and run before) + GameEvents.OnPartPurchased.Add(OnPartPurchased); + GameEvents.OnPartUpgradePurchased.Add(OnPartUpgradePurchased); + } + + public void OnDestroy() + { + GameEvents.OnPartPurchased.Remove(OnPartPurchased); + GameEvents.OnPartUpgradePurchased.Remove(OnPartUpgradePurchased); + + if (Instance == this) + Instance = null; + } + } + + [KSPAddon(KSPAddon.Startup.Instantly, true)] + public class UnlockCreditUtility : MonoBehaviour + { + public static float StoredUpgradeEntryCost = -1f; + public static int StoredPartEntryCost = -1; + + public static UnityEngine.UI.Button Button = null; + public static string TooltipText = null; + private static System.Reflection.MethodInfo IsHighlightedMethod = typeof(UnityEngine.UI.Selectable).GetMethod("IsHighlighted", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + public static int WindowID = "PartListTooltip".GetHashCode(); + + public void Awake() + { + GameEvents.OnPartPurchased.Add(ResetPartCost); + GameEvents.OnPartUpgradePurchased.Add(ResetUpgradeCost); + DontDestroyOnLoad(this); + } + + public void OnGUI() + { + if (Button == null || !Button.gameObject.activeSelf) + return; + + bool highlighted = (bool)IsHighlightedMethod.Invoke(Button, null); + if (highlighted) + { + Tooltip.Instance.RecordTooltip(WindowID, false, TooltipText); + Tooltip.Instance.ShowTooltip(WindowID); + } + else + { + Tooltip.Instance.RecordTooltip(WindowID, false, TooltipText); + } + } + + private void ResetPartCost(AvailablePart ap) + { + if (StoredPartEntryCost >= 0) + ap.SetEntryCost(StoredPartEntryCost); + + StoredPartEntryCost = -1; + } + + private void ResetUpgradeCost(PartUpgradeHandler.Upgrade up) + { + if (StoredUpgradeEntryCost >= 0) + up.entryCost = StoredUpgradeEntryCost; + + StoredUpgradeEntryCost = -1f; + } + } +} diff --git a/Source/UnlockSubsidyHandler.cs b/Source/UnlockSubsidyHandler.cs deleted file mode 100644 index ba70ef7df61..00000000000 --- a/Source/UnlockSubsidyHandler.cs +++ /dev/null @@ -1,518 +0,0 @@ -using System.Collections.Generic; -using UnityEngine; -using KerbalConstructionTime; -using System; - -namespace RP0 -{ - [KSPScenario(ScenarioCreationOptions.AddToAllGames, new GameScenes[] { GameScenes.EDITOR, GameScenes.FLIGHT, GameScenes.SPACECENTER, GameScenes.TRACKSTATION })] - public class UnlockSubsidyHandler : ScenarioModule - { - public class UnlockCreditNode - { - [Persistent] - public string tech; - - [Persistent] - public double funds; - - public UnlockCreditNode() { } - - public UnlockCreditNode(UnlockCreditNode src) - { - tech = src.tech; - funds = src.funds; - } - } - - public static UnlockSubsidyHandler Instance { get; private set; } - - private readonly Dictionary _creditStorage = new Dictionary(); - private static readonly Dictionary _cacheDict = new Dictionary(); - private static readonly HashSet _seenNodes = new HashSet(); - - public double TotalCredit - { - get - { - double amt = 0; - foreach (var node in _creditStorage.Values) - amt += node.funds; - - return amt; - } - } - - /// - /// Note this is CMQ-neutral. - /// - /// - /// - public void IncrementCreditTime(string tech, double UT) => IncrementCredit(tech, UT * MaintenanceHandler.Instance.ResearchSalaryPerDay / 86400d * MaintenanceHandler.Settings.researcherSalaryToUnlockCredit); - - /// - /// Note this is CMQ-neutral. - /// - /// - /// - public void IncrementCredit(string tech, double amount) - { - // Will also catch NaN - if (!(amount > 0)) - return; - - if (!_creditStorage.TryGetValue(tech, out var sNode)) - { - sNode = new UnlockCreditNode() { tech = tech }; - _creditStorage[tech] = sNode; - } - - sNode.funds += amount; - } - - public double GetLocalCreditAmount(string tech, Dictionary dict = null) - { - if (dict == null) - dict = _creditStorage; - - if (!dict.TryGetValue(tech, out var sNode)) - return 0d; - - return sNode.funds; - } - - public double GetCreditAmount(string tech, Dictionary dict = null) - { - _seenNodes.Clear(); - return _GetCreditAmount(tech, dict); - } - - private double _GetCreditAmount(string tech, Dictionary dict = null) - { - if (tech == null) - return 0d; - - if (dict == null) - dict = _creditStorage; - - double amount = GetLocalCreditAmount(tech, dict); - - List parentList; - if (!KerbalConstructionTimeData.techNameToParents.TryGetValue(tech, out parentList)) - { - Debug.LogError($"[KCT] Could not find techToParent for tech {tech}"); - return amount; - } - foreach (var parent in parentList) - { - if (_seenNodes.Contains(parent)) - continue; - - _seenNodes.Add(parent); - amount += _GetCreditAmount(parent, dict); - } - - _cacheDict[tech] = amount; - return amount; - } - - // This is not optimal, but it's better than naive unsorted. - public double GetCreditAmount(List parts) - { - double creditAmount = 0d; - var cloneDict = new Dictionary(); - foreach (var kvp in _creditStorage) - cloneDict.Add(kvp.Key, new UnlockCreditNode(kvp.Value)); - - - var partCostDict = new Dictionary(); - // first pull from local. - foreach (var p in parts) - { - double cost = p.entryCost; - if (cloneDict.TryGetValue(p.TechRequired, out var sNode)) - { - double local = sNode.funds; - if (local > cost) - local = cost; - cost -= local; - sNode.funds -= local; - creditAmount += local; - } - partCostDict[p] = cost; - } - foreach (var p in parts) - { - double cost = partCostDict[p]; - double excess = SpendCredit(p.TechRequired, cost, cloneDict); - creditAmount += (cost - excess); - } - - return creditAmount; - } - - private void FillECMs(string techID, string ecmName, Dictionary ecmToCost, Dictionary> ecmToTech) - { - // if this ECM is unlocked, we're done. - if (RealFuels.EntryCostDatabase.IsUnlocked(ecmName)) - return; - - var holder = RealFuels.EntryCostDatabase.GetHolder(ecmName); - if (holder == null) - return; - - // If we've already seen this ECM, just add the tech and recurse - // Note that by definition we will have already added all children - // to the cost dict already. - if (ecmToTech.TryGetValue(ecmName, out var techs)) - { - techs.Add(techID); - foreach (var childName in holder.children) - FillECMs(techID, childName, ecmToCost, ecmToTech); - - return; - } - - // Fill from scratch and recurse. - ecmToCost[holder.name] = holder.cost; - techs = new HashSet(); - techs.Add(techID); - ecmToTech[holder.name] = techs; - - foreach (var childName in holder.children) - FillECMs(techID, childName, ecmToCost, ecmToTech); - } - - private void AddPartToDicts(AvailablePart ap, Dictionary ecmToCost, Dictionary> ecmToTech) - { - string sanitizedName = RealFuels.Utilities.SanitizeName(ap.name); - - if (RealFuels.EntryCostDatabase.GetHolder(sanitizedName) != null) - { - FillECMs(ap.TechRequired, sanitizedName, ecmToCost, ecmToTech); - return; - } - - // It shouldn't already contain the key, but you never know. - if (!ecmToCost.ContainsKey(sanitizedName)) - { - ecmToCost[sanitizedName] = ap.entryCost; - - // ditto - if (!ecmToTech.TryGetValue(sanitizedName, out var techs)) - techs = new HashSet(); - - techs.Add(ap.TechRequired); - ecmToTech[sanitizedName] = techs; - } - } - - public void SpendCreditAndCost(List parts) - { - // This is going to be expensive, because we have to chase down all the ECMs. - double cmqMultiplier = -CurrencyUtils.Funds(TransactionReasonsRP0.PartOrUpgradeUnlock, -1d); - if (cmqMultiplier == 0d) - return; - - double recipCMQMult = 1d / cmqMultiplier; - - Dictionary ecmToCost = new Dictionary(); - Dictionary> ecmToTech = new Dictionary>(); - foreach (var p in parts) - AddPartToDicts(p, ecmToCost, ecmToTech); - - // First apply our CMQ result - foreach (var kvp in ecmToTech) - ecmToCost[kvp.Key] = ecmToCost[kvp.Key] * cmqMultiplier; - - // first try to spend local credit in each case - foreach (var kvp in ecmToTech) - { - foreach (string tech in kvp.Value) - { - if (!_creditStorage.TryGetValue(tech, out var sNode)) - continue; - - double cost = ecmToCost[kvp.Key]; - double local = sNode.funds; - - // This check is needed because we might have multiple techs. - if (cost == 0 || local == 0) - continue; - - // Now deduct local funds and lower cost. - if (local > cost) - local = cost; - cost -= local; - sNode.funds -= local; - ecmToCost[kvp.Key] = cost; - } - } - - // Now pull full credit - foreach (var kvp in ecmToTech) - { - foreach (string tech in kvp.Value) - { - double cost = ecmToCost[kvp.Key]; - - // This check is needed because we might have multiple techs. - if (cost == 0d) - continue; - - ecmToCost[kvp.Key] = SpendCredit(tech, cost); - } - } - - // Finally, pay for the excess. - double totalCost = 0d; - foreach (double d in ecmToCost.Values) - totalCost += d; - - if (totalCost > 0d) - Funding.Instance.AddFunds(-totalCost * recipCMQMult, TransactionReasonsRP0.PartOrUpgradeUnlock.Stock()); - } - - public double SpendCredit(string tech, double cost, Dictionary dict = null) - { - double amount = GetCreditAmount(tech, dict); - if (amount == 0d) - return cost; - - if (dict == null) - dict = _creditStorage; - - double excessCost; - if (amount < cost) - { - excessCost = cost - amount; - cost = amount; - } - else - { - excessCost = 0d; - } - - _SpendCredit(tech, cost, excessCost > 0d, dict); - return excessCost; - } - - private void _SpendCredit(string tech, double cost, bool spendAll, Dictionary dict) - { - if (dict == null) - dict = _creditStorage; - - if (dict.TryGetValue(tech, out var sNode)) - { - if (spendAll) - { - sNode.funds = 0d; - } - else - { - if (sNode.funds <= cost) - { - cost -= sNode.funds; - sNode.funds = 0d; - } - else - { - sNode.funds -= cost; - cost = 0d; - } - - if (cost < 0.01d) - return; // done - } - } - - // Distribute cost proportionally amongst parents - List parentList; - if (!KerbalConstructionTimeData.techNameToParents.TryGetValue(tech, out parentList)) - { - Debug.LogError($"[KCT] Could not find techToParent for tech {tech}"); - return; // error; - } - - if (spendAll) - { - foreach (var parent in parentList) - { - _SpendCredit(parent, cost, true, dict); - } - return; - } - - double parentCreditTotal = 0d; - foreach (var parent in parentList) - { - double amount; - if (!_cacheDict.TryGetValue(parent, out amount)) - { - amount = GetCreditAmount(parent, dict); - _cacheDict[parent] = amount; - } - parentCreditTotal += amount; - } - - if (parentCreditTotal > 0d) - { - foreach (var parent in parentList) - { - double portion = _cacheDict[parent] / parentCreditTotal * cost; - _SpendCredit(parent, portion, false, dict); - } - } - } - - /// - /// Transforms entrycost to post-strategy entrycost, spends credit, - /// and returns remaining (unsubsidized) cost - /// - /// - /// - /// - private float ProcessCredit(float entryCost, string tech) - { - if (entryCost == 0f) - return 0f; - - double postCMQCost = -CurrencyUtils.Funds(TransactionReasonsRP0.PartOrUpgradeUnlock, -entryCost); - if (double.IsNaN(postCMQCost)) - { - Debug.LogError("[RP-0] CMQ for a credit unlock returned NaN, ignoring and going back to regular cost."); - postCMQCost = entryCost; - } - else if (postCMQCost == 0d) - { - Debug.LogError("[RP-0] CMQ for a credit unlock returned 0, not spending any credit."); - return 0f; - } - - // Actually spend credit and get (post-effect) remainder - double remainingCost = SpendCredit(tech, postCMQCost); - - // Refresh description to show new credit remaining - if (KSP.UI.Screens.RDController.Instance != null) - KSP.UI.Screens.RDController.Instance.ShowNodePanel(KSP.UI.Screens.RDController.Instance.node_selected); - - //return the remainder after transforming to pre-effect numbers - return (float)(remainingCost * (entryCost / postCMQCost)); - } - - private void OnPartPurchased(AvailablePart ap) - { - UnlockCreditUtility.StoredPartEntryCost = ap.entryCost; - if (ap.costsFunds) - { - int remainingCost = (int)ProcessCredit(ap.entryCost, ap.TechRequired); - ap.SetEntryCost(remainingCost); - } - } - - private void OnPartUpgradePurchased(PartUpgradeHandler.Upgrade up) - { - UnlockCreditUtility.StoredUpgradeEntryCost = up.entryCost; - float remainingCost = ProcessCredit(up.entryCost, up.techRequired); - up.entryCost = remainingCost; - } - - public override void OnLoad(ConfigNode node) - { - _creditStorage.Clear(); - foreach (var cn in node.GetNodes("UnlockSubsidyNode")) - { - var sNode = new UnlockCreditNode(); - ConfigNode.LoadObjectFromConfig(sNode, cn); - _creditStorage[sNode.tech] = sNode; - } - } - - public override void OnSave(ConfigNode node) - { - foreach (var sNode in _creditStorage.Values) - { - var cn = node.AddNode("UnlockSubsidyNode"); - ConfigNode.CreateConfigFromObject(sNode, cn); - } - } - - public override void OnAwake() - { - base.OnAwake(); - - if (Instance != null) - Destroy(Instance); - - Instance = this; - } - - public void Start() - { - // This runs after KSP's Funding's OnAwake and thus we can bind after (and run before) - GameEvents.OnPartPurchased.Add(OnPartPurchased); - GameEvents.OnPartUpgradePurchased.Add(OnPartUpgradePurchased); - } - - public void OnDestroy() - { - GameEvents.OnPartPurchased.Remove(OnPartPurchased); - GameEvents.OnPartUpgradePurchased.Remove(OnPartUpgradePurchased); - - if (Instance == this) - Instance = null; - } - } - - [KSPAddon(KSPAddon.Startup.Instantly, true)] - public class UnlockCreditUtility : MonoBehaviour - { - public static float StoredUpgradeEntryCost = -1f; - public static int StoredPartEntryCost = -1; - - public static UnityEngine.UI.Button Button = null; - public static string TooltipText = null; - private static System.Reflection.MethodInfo IsHighlightedMethod = typeof(UnityEngine.UI.Selectable).GetMethod("IsHighlighted", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - public static int WindowID = "PartListTooltip".GetHashCode(); - - public void Awake() - { - GameEvents.OnPartPurchased.Add(ResetPartCost); - GameEvents.OnPartUpgradePurchased.Add(ResetUpgradeCost); - DontDestroyOnLoad(this); - } - - public void OnGUI() - { - if (Button == null || !Button.gameObject.activeSelf) - return; - - bool highlighted = (bool)IsHighlightedMethod.Invoke(Button, null); - if (highlighted) - { - Tooltip.Instance.RecordTooltip(WindowID, false, TooltipText); - Tooltip.Instance.ShowTooltip(WindowID); - } - else - { - Tooltip.Instance.RecordTooltip(WindowID, false, TooltipText); - } - } - - private void ResetPartCost(AvailablePart ap) - { - if (StoredPartEntryCost >= 0) - ap.SetEntryCost(StoredPartEntryCost); - - StoredPartEntryCost = -1; - } - - private void ResetUpgradeCost(PartUpgradeHandler.Upgrade up) - { - if (StoredUpgradeEntryCost >= 0) - up.entryCost = StoredUpgradeEntryCost; - - StoredUpgradeEntryCost = -1f; - } - } -} diff --git a/Source/UpgradeScripts/UpgradeUnlockCredit.cs b/Source/UpgradeScripts/UpgradeUnlockCredit.cs new file mode 100644 index 00000000000..eff1fa596cb --- /dev/null +++ b/Source/UpgradeScripts/UpgradeUnlockCredit.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using SaveUpgradePipeline; +using UnityEngine; + +namespace RP0.UpgradeScripts +{ + [UpgradeModule(LoadContext.SFS, sfsNodeUrl = "GAME/SCENARIO")] + public class UpgradeUnlockCredit : UpgradeScript + { + public override string Name { get => "RP-1 Unlock Credit Upgrader"; } + public override string Description { get => "Updates Unlock Credit from per-node to global"; } + public override Version EarliestCompatibleVersion { get => new Version(2, 0, 0); } + protected static Version _targetVersion = new Version(2, 3, 0); + public override Version TargetVersion => _targetVersion; + + public override TestResult OnTest(ConfigNode node, LoadContext loadContext, ref string nodeName) + { + nodeName = node.GetValue("name"); + return nodeName == "UnlockSubsidyHandler" ? TestResult.Upgradeable : TestResult.Pass; + } + + public override void OnUpgrade(ConfigNode node, LoadContext loadContext, ConfigNode parentNode) + { + string name = node.GetValue("name"); + if (name != "UnlockSubsidyHandler") + return; + + node.SetValue("name", "UnlockCreditHandler"); + double totalCredit = 0; + foreach (ConfigNode n in node.nodes) + { + double credit = 0d; + n.TryGetValue("funds", ref credit); + totalCredit += Math.Max(0d, credit); + } + node.AddValue("_totalCredit", totalCredit); + + Debug.Log($"[RP-0] UpgradePipeline context {loadContext} updated UnlockSubsidyHandler to UnlockCreditHandler, total credit {totalCredit:N0}"); + } + } +}