From 9107409253e0a39870d75e4db50602539d7b8d4c Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 14 Dec 2020 14:35:15 -0600 Subject: [PATCH 01/10] Use correct filename case for building on Linux --- source/BattletechPerformanceFix.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 4c46833..2d6cb21 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -67,7 +67,7 @@ - + @@ -120,4 +120,4 @@ yes | cp "$(TargetDir)$(TargetName).dll" "$(SolutionDir)../../Mods/BattletechPerformanceFix/$(TargetName).dll" yes | cp "$(TargetDir)$(TargetName).dll" "$(SolutionDir)../../Mods/BattletechPerformanceFix/$(TargetName).dll" - \ No newline at end of file + From 0d9f42c7b9cf72311fbdfa180bb21348a5fd5bc8 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 22 Jan 2021 20:38:30 -0600 Subject: [PATCH 02/10] First set of memory leak fixes, mostly involving save file loading --- source/BattletechPerformanceFix.csproj | 1 + source/Main.cs | 1 + source/MemoryLeakFix.cs | 220 +++++++++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 source/MemoryLeakFix.cs diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 2d6cb21..ea5767a 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -73,6 +73,7 @@ + diff --git a/source/Main.cs b/source/Main.cs index 0231c9e..8d2c582 100644 --- a/source/Main.cs +++ b/source/Main.cs @@ -99,6 +99,7 @@ public static void Start(string modDirectory, string json) { typeof(DisableSimAnimations), false }, { typeof(RemovedContractsFix), true }, { typeof(VersionManifestPatches), true }, + { typeof(MemoryLeakFix), true }, { typeof(EnableConsole), false }, }; diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs new file mode 100644 index 0000000..434b1d3 --- /dev/null +++ b/source/MemoryLeakFix.cs @@ -0,0 +1,220 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Collections.Generic; +using System.Linq; +using Harmony; +using static BattletechPerformanceFix.Extensions; +using BattleTech; +using BattleTech.Analytics.Sim; +using BattleTech.Framework; +using BattleTech.UI; +using Localize; +using HBS.Collections; + +namespace BattletechPerformanceFix +{ + class MemoryLeakFix: Feature + { + private static Type self = typeof(MemoryLeakFix); + + public void Activate() { + // fixes group 1: occurs on save file load + // fix 1.1: allow the BattleTechSimAnalytics class to properly remove its message subscriptions + "BeginSession".Transpile("_SessionTranspiler"); + "EndSession".Transpile("_SessionTranspiler"); + // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with + "OnSimGameInitializeComplete".Post("_OnSimGameInitializeComplete_Post"); + // fix 1.3.1: clear InterpolatedText objects that aren't supposed to live forever + "ClearSimulation".Pre("_ClearSimulation_Pre"); + "ClearSimulation".Post("_ClearSimulation_Post"); + // fix 1.3.2: patch methods making an InterpolatedText object and doesn't store it anywhere + // FIXME may also need to patch calls to LocalizableText.UpdateTMPText() [this uses LocalizableText objects as well] [untested] + // TODO/FIXME also patch over the class' finalizers with a nop so we don't have doubled calls to RemoveSubscriber() + "RunMadLibs".Transpile("_MadLib_TagSet_Transpiler"); + "RunMadLibsOnLanceDef".Transpile("_MadLib_TagSet_Transpiler"); + // this method uses both overloads of RunMadLib, so it needs two transpiler passes + "RunMadLib".Transpile("_MadLib_TagSet_Transpiler"); + "RunMadLib".Transpile("_MadLib_string_Transpiler"); + } + + private static IEnumerable _SessionTranspiler(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_UpdateMessageSubscriptions"); + return TranspileReplaceCall(ins, "UpdateMessageSubscriptions", meth); + } + + private static void _UpdateMessageSubscriptions(BattleTechSimAnalytics __instance, bool subscribe) + { + LogSpam("_UpdateMessageSubscriptions called"); + var mc = __instance.messageCenter; + if (mc != null) { + mc.Subscribe(MessageCenterMessageType.OnReportMechwarriorSkillUp, + new ReceiveMessageCenterMessage(__instance.ReportMechWarriorSkilledUp), subscribe); + mc.Subscribe(MessageCenterMessageType.OnReportMechwarriorHired, + new ReceiveMessageCenterMessage(__instance.ReportMechWarriorHired), subscribe); + mc.Subscribe(MessageCenterMessageType.OnReportMechWarriorKilled, + new ReceiveMessageCenterMessage(__instance.ReportMechWarriorKilled), subscribe); + mc.Subscribe(MessageCenterMessageType.OnReportShipUpgradePurchased, + new ReceiveMessageCenterMessage(__instance.ReportShipUpgradePurchased), subscribe); + mc.Subscribe(MessageCenterMessageType.OnSimGameContractComplete, + new ReceiveMessageCenterMessage(__instance.ReportContractComplete), subscribe); + mc.Subscribe(MessageCenterMessageType.OnSimRoomStateChanged, + new ReceiveMessageCenterMessage(__instance.ReportSimGameRoomChange), subscribe); + } + } + + private static void _OnSimGameInitializeComplete_Post(SimGameUXCreator __instance) + { + LogSpam("_OnSimGameInitializeComplete_Post called"); + __instance.sim.MessageCenter.RemoveSubscriber( + MessageCenterMessageType.OnSimGameInitialized, + new ReceiveMessageCenterMessage(__instance.OnSimGameInitializeComplete)); + } + + private static void _ClearSimulation_Pre(GameInstance __instance, ref SimGameState __state) + { + LogSpam("_ClearSimulation_Pre called"); + __state = __instance.Simulation; + } + + private static void _ClearSimulation_Post(GameInstance __instance, ref SimGameState __state) + { + LogSpam("_ClearSimulation_Post called"); + if (__state == null) { + LogSpam("SimGameState was null (ok if first load)"); + return; + } + + var mc = __instance.MessageCenter; + + foreach (var contract in __state.GetAllCurrentlySelectableContracts()) { + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(contract.OnLanguageChanged)); + + if (contract.Override == null) { + LogDebug("contract.Override is null!"); + continue; + } + + foreach (var dialogue in contract.Override.dialogueList) { + foreach (var content in dialogue.dialogueContent) { + RemoveTextSubscriber(mc, (InterpolatedText) content.GetWords()); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(content.OnLanguageChanged)); + } + } + + foreach (var objective in contract.Override.contractObjectiveList) { + RemoveTextSubscriber(mc, (InterpolatedText) objective.GetTitle()); + RemoveTextSubscriber(mc, (InterpolatedText) objective.GetDescription()); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); + } + + foreach (var objective in contract.Override.objectiveList) { + RemoveTextSubscriber(mc, (InterpolatedText) objective._Title); + RemoveTextSubscriber(mc, (InterpolatedText) objective._Description); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); + } + } + } + + private static void RemoveTextSubscriber(MessageCenter mc, InterpolatedText text) { + if (text != null) { + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(text.OnLanguageChanged)); + } + } + + private static IEnumerable _MadLib_string_Transpiler(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(string)}); + return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", + new Type[]{typeof(string)}, meth); + } + + private static IEnumerable _MadLib_TagSet_Transpiler(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(TagSet)}); + return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", + new Type[]{typeof(TagSet)}, meth); + } + + private static IEnumerable + TranspileReplaceCall(IEnumerable ins, string originalMethodName, + MethodInfo replacementMethod) + { + LogInfo($"TranspileReplaceCall: {originalMethodName} -> {replacementMethod.ToString()}"); + return ins.SelectMany(i => { + if (i.opcode == OpCodes.Call && + (i.operand as MethodInfo).Name.StartsWith(originalMethodName)) { + i.operand = replacementMethod; + } + return Sequence(i); + }); + } + + private static IEnumerable + TranspileReplaceOverloadedCall(IEnumerable ins, Type originalMethodClass, + string originalMethodName, Type[] originalParamTypes, + MethodInfo replacementMethod) + { + LogInfo($"TranspileReplaceOverloadedCall: {originalMethodClass.ToString()}.{originalMethodName}" + + $"({String.Concat(originalParamTypes.Select(x => x.ToString()))}) -> {replacementMethod.ToString()}"); + return ins.SelectMany(i => { + var methInfo = i.operand as MethodInfo; + if (i.opcode == OpCodes.Callvirt && + methInfo.DeclaringType == originalMethodClass && + methInfo.Name.StartsWith(originalMethodName) && + Enumerable.SequenceEqual(methInfo.GetParameters().Select(x => x.ParameterType), originalParamTypes)) + { + i.operand = replacementMethod; + } + return Sequence(i); + }); + } + + private static string _Contract_RunMadLib(Contract __instance, string text) + { + LogSpam("_Contract_RunMadLib(string) called"); + if (string.IsNullOrEmpty(text)) + { + return ""; + } + InterpolatedText iText = __instance.Interpolate(text); + text = iText.ToString(false); + __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); + return text; + } + + private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) + { + LogSpam("_Contract_RunMadLib(tagSet) called"); + if (tagSet == null) + { + return; + } + string[] array = tagSet.ToArray(); + if (array == null) + { + return; + } + for (int i = 0; i < array.Length; i++) + { + string text = array[i]; + InterpolatedText iText = __instance.Interpolate(text); + text = iText.ToString(false); + __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); + array[i] = text.ToLower(); + } + tagSet.Clear(); + tagSet.AddRange(array); + } + } +} From d90f37e1c8246733edeacba2b4bcaa8da8d5fd24 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sat, 6 Feb 2021 09:07:47 -0600 Subject: [PATCH 03/10] cleanup & expand on message subscription fixes --- source/MemoryLeakFix.cs | 328 ++++++++++++++++++++++++++++++++-------- 1 file changed, 262 insertions(+), 66 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index 434b1d3..f6d6aaa 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Collections.Generic; @@ -7,10 +8,14 @@ using static BattletechPerformanceFix.Extensions; using BattleTech; using BattleTech.Analytics.Sim; +using BattleTech.Data; using BattleTech.Framework; using BattleTech.UI; +using BattleTech.UI.Tooltips; +using BattleTech.UI.TMProWrapper; using Localize; using HBS.Collections; +using HBS.Util; namespace BattletechPerformanceFix { @@ -21,24 +26,65 @@ class MemoryLeakFix: Feature public void Activate() { // fixes group 1: occurs on save file load // fix 1.1: allow the BattleTechSimAnalytics class to properly remove its message subscriptions - "BeginSession".Transpile("_SessionTranspiler"); - "EndSession".Transpile("_SessionTranspiler"); + "BeginSession".Transpile("Session_Transpile"); + "EndSession".Transpile("Session_Transpile"); // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with - "OnSimGameInitializeComplete".Post("_OnSimGameInitializeComplete_Post"); + "OnSimGameInitializeComplete".Post(); // fix 1.3.1: clear InterpolatedText objects that aren't supposed to live forever - "ClearSimulation".Pre("_ClearSimulation_Pre"); - "ClearSimulation".Post("_ClearSimulation_Post"); + "Destroy".Post(); // fix 1.3.2: patch methods making an InterpolatedText object and doesn't store it anywhere - // FIXME may also need to patch calls to LocalizableText.UpdateTMPText() [this uses LocalizableText objects as well] [untested] - // TODO/FIXME also patch over the class' finalizers with a nop so we don't have doubled calls to RemoveSubscriber() - "RunMadLibs".Transpile("_MadLib_TagSet_Transpiler"); - "RunMadLibsOnLanceDef".Transpile("_MadLib_TagSet_Transpiler"); - // this method uses both overloads of RunMadLib, so it needs two transpiler passes - "RunMadLib".Transpile("_MadLib_TagSet_Transpiler"); - "RunMadLib".Transpile("_MadLib_string_Transpiler"); + "RunMadLibs".Transpile("MadLib_Transpile"); + "RunMadLibsOnLanceDef".Transpile("MadLib_Transpile"); + "RunMadLib".Transpile("MadLib_Transpile"); + "UpdateTMPText".Transpile("ToTMP_Transpile"); + // these finalizers could never run to begin with, and they only did RemoveSubscriber; nop them + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + "Finalize".Transpile("Nop_Transpile"); + // fix 1.4: when a savefile is created, two copies of each contract are created and stored; + // when that savefile is loaded, one set of copies overwrites the other, and both sets register subs. + // this patch unregisters subs for the about-to-be-overwritten contracts + "Rehydrate".Pre(); + // fix 1.5: TODO explain this + // FIXME unable to attach to a generic method with generic parameter (how???) + //var paramTypes = new Type[]{typeof(ContractOverride), typeof(string)}; + //var genericsTypes = new Type[]{typeof(ContractOverride)}; + //var meth = AccessTools.Method(typeof(JSONSerializationUtility), "FromJSON", paramTypes, genericsTypes); + //var patch = new HarmonyMethod(AccessTools.Method(self, "FromJSON_Post")); + // NOTE ideally we would patch FromJSON() for T : ContractOverride, + // but Harmony has trouble dealing with generic methods, so we patch this instead. + var paramTypes = new Type[]{typeof(object), typeof(Dictionary), typeof(string), typeof(HBS.Stopwatch), + typeof(HBS.Stopwatch), typeof(JSONSerializationUtility.RehydrationFilteringMode), + typeof(Func[])}; + var meth = AccessTools.Method(typeof(JSONSerializationUtility), "RehydrateObjectFromDictionary", paramTypes); + var patch = new HarmonyMethod(AccessTools.Method(self, "RehydrateObjectFromDictionary_Post")); + Main.harmony.Patch(meth, null, patch); + + // fixes group 2: occurs on entering/exiting a contract + // fix 2.2: when a contract completes, remove its OnLanguageChanged subs + "ResolveCompleteContract".Pre(); + + // fixes group 3: occurs on transiting between star systems + // fix 3.1: when a star system removes its contracts, remove those contracts' OnLanguageChanged subs + "ResetContracts".Pre(); + // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs + "StartGeneratePotentialContractsRoutine".Pre(); + + // fixes group 4: occurs on accepting & completing a travel contract + // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, let the caller handle it + paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), + typeof(bool), typeof(int), typeof(int), typeof(int?)}; + var ctor = AccessTools.Constructor(typeof(Contract), paramTypes); + patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); + Main.harmony.Patch(ctor, null, null, patch); + "StartGeneratePotentialContractsRoutine".Transpile(); + } - private static IEnumerable _SessionTranspiler(IEnumerable ins) + private static IEnumerable Session_Transpile(IEnumerable ins) { var meth = AccessTools.Method(self, "_UpdateMessageSubscriptions"); return TranspileReplaceCall(ins, "UpdateMessageSubscriptions", meth); @@ -46,7 +92,7 @@ private static IEnumerable _SessionTranspiler(IEnumerable _MadLib_string_Transpiler(IEnumerable ins) - { - var meth = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(string)}); - return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", - new Type[]{typeof(string)}, meth); - } - - private static IEnumerable _MadLib_TagSet_Transpiler(IEnumerable ins) + private static IEnumerable MadLib_Transpile(IEnumerable ins) { - var meth = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(TagSet)}); - return TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", - new Type[]{typeof(TagSet)}, meth); + var methString = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(string)}); + var methTagSet = AccessTools.Method(self, "_Contract_RunMadLib", + new Type[]{typeof(Contract), typeof(TagSet)}); + var firstPass = TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", + new Type[]{typeof(string)}, methString); + return TranspileReplaceOverloadedCall(firstPass, typeof(Contract), "RunMadLib", + new Type[]{typeof(TagSet)}, methTagSet); } private static IEnumerable @@ -180,7 +228,6 @@ private static IEnumerable private static string _Contract_RunMadLib(Contract __instance, string text) { - LogSpam("_Contract_RunMadLib(string) called"); if (string.IsNullOrEmpty(text)) { return ""; @@ -194,7 +241,6 @@ private static string _Contract_RunMadLib(Contract __instance, string text) private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) { - LogSpam("_Contract_RunMadLib(tagSet) called"); if (tagSet == null) { return; @@ -216,5 +262,155 @@ private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) tagSet.Clear(); tagSet.AddRange(array); } + + private static IEnumerable ToTMP_Transpile(IEnumerable ins) + { + var originalTypes = new Type[]{typeof(GameContext), typeof(TextTooltipFormatOptions)}; + var replacementTypes = new Type[]{typeof(TextTooltipParser), typeof(GameContext), typeof(TextTooltipFormatOptions)}; + var meth = AccessTools.Method(self, "_TextTooltipParser_ToTMP", replacementTypes); + return TranspileReplaceOverloadedCall(ins, typeof(TextTooltipParser), "ToTMP", originalTypes, meth); + } + + private static Text + _TextTooltipParser_ToTMP(TextTooltipParser __instance, GameContext gameContext, TextTooltipFormatOptions formatOptions) + { + var mc = HBS.SceneSingletonBehavior.Instance.Game.MessageCenter; + return __instance.GenerateFinalString((TextTooltipData x) => { + InterpolatedText iText = x.ToTMP(gameContext, formatOptions); + mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, + new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); + return iText; + }); + } + + private static IEnumerable Nop_Transpile(IEnumerable ins) + { + return ins.SelectMany(i => { + i.opcode = OpCodes.Nop; + i.operand = null; + return Sequence(i); + }); + } + + private static void Rehydrate_Pre(StarSystem __instance, SimGameState sim) + { + if (__instance.activeSystemContracts != null) { + foreach (var contract in __instance.activeSystemContracts) { + RemoveContractSubscriptions(sim.MessageCenter, contract); + } + } + if (__instance.activeSystemBreadcrumbs != null) { + foreach (var contract in __instance.activeSystemBreadcrumbs) { + RemoveContractSubscriptions(sim.MessageCenter, contract); + } + } + } + + private static void RehydrateObjectFromDictionary_Post(object target) + { + if (target.GetType() != typeof(ContractOverride)) return; + //LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called with target type ContractOverride"); + var mc = HBS.SceneSingletonBehavior.Instance.Game.MessageCenter; + RemoveContractOverrideSubscriptions(mc, (ContractOverride) target); + } + + private static void ResolveCompleteContract_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.CompletedContract != null) { + RemoveContractSubscriptions(__instance.MessageCenter, __instance.CompletedContract); + } + } + + private static void ResetContracts_Pre(StarSystem __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + foreach (var contract in __instance.activeSystemContracts) { + RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); + } + foreach (var contract in __instance.activeSystemBreadcrumbs) { + RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); + } + } + + private static void + StartGeneratePotentialContractsRoutine_Pre(SimGameState __instance, bool clearExistingContracts, + StarSystem systemOverride) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (clearExistingContracts) { + List contracts = (systemOverride != null) ? __instance.CurSystem.SystemBreadcrumbs : + __instance.CurSystem.SystemContracts; + LogSpam($"clearExistingContracts is set; removing subscriptions for {contracts.Count} contracts"); + foreach(var contract in contracts) { + RemoveContractSubscriptions(__instance.MessageCenter, contract); + } + } + } + + private static IEnumerable Contract_Transpile(IEnumerable ins) + { + LogInfo($"Contract_Transpile: nopping call to Copy()"); + var meth = AccessTools.Method(typeof(ContractOverride), "Copy"); + CodeInstruction toBeNopped = new CodeInstruction(OpCodes.Callvirt, meth); + return ins.SelectMany(i => { + if (i.opcode == toBeNopped.opcode && i.operand == toBeNopped.operand) { + + i.opcode = OpCodes.Nop; + i.operand = null; + } + return Sequence(i); + }); + } + + private static IEnumerable + StartGeneratePotentialContractsRoutine_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + var types = new Type[]{typeof(SimGameState), typeof(StarSystem), typeof(bool), typeof(MapAndEncounters), + typeof(SimGameState.MapEncounterContractData), typeof(GameContext)}; + var meth = AccessTools.Method(self, "_CreateProceduralContract", types); + return TranspileReplaceCall(ins, "CreateProceduralContract", meth); + } + + private Contract + _CreateProceduralContract(SimGameState __instance, StarSystem system, bool usingBreadcrumbs, MapAndEncounters level, + SimGameState.MapEncounterContractData MapEncounterContractData, GameContext gameContext) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + WeightedList flatContracts = MapEncounterContractData.FlatContracts; + __instance.FilterContracts(flatContracts); + SimGameState.PotentialContract next = flatContracts.GetNext(true); + int id = next.contractOverride.ContractTypeValue.ID; + MapEncounterContractData.Encounters[id].Shuffle(); + string encounterLayerGUID = MapEncounterContractData.Encounters[id][0].EncounterLayerGUID; + ContractOverride contractOverride = next.contractOverride; + FactionValue employer = next.employer; + FactionValue target = next.target; + FactionValue employerAlly = next.employerAlly; + FactionValue targetAlly = next.targetAlly; + FactionValue neutralToAll = next.NeutralToAll; + FactionValue hostileToAll = next.HostileToAll; + int difficulty = next.difficulty; + Contract contract; + if (usingBreadcrumbs) { + contract = __instance.CreateTravelContract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, + next.contractOverride.ContractTypeValue, contractOverride, + gameContext, employer, target, targetAlly, employerAlly, + neutralToAll, hostileToAll, false, difficulty); + } else { + LogSpam("copying contractOverride"); + contractOverride = contractOverride.Copy(); + contract = new Contract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, + next.contractOverride.ContractTypeValue, __instance.BattleTechGame, + contractOverride, gameContext, true, difficulty, 0, null); + } + __instance.mapDiscardPile.Add(level.Map.MapID); + __instance.contractDiscardPile.Add(contractOverride.ID); + __instance.PrepContract(contract, employer, employerAlly, target, targetAlly, neutralToAll, + hostileToAll, level.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, system); + return contract; + } } } +// vim: ts=4:sw=4 From 4791c14e233828604ef8704487b746435c1cba8f Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Fri, 12 Feb 2021 13:46:24 -0600 Subject: [PATCH 04/10] add more fixes for OnLanguageChanged subs including travel contract handling --- source/MemoryLeakFix.cs | 426 +++++++++++++++++++++++++++++++++++----- 1 file changed, 376 insertions(+), 50 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index f6d6aaa..550461f 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; +using System.Collections; using System.Collections.Generic; using System.Linq; using Harmony; @@ -10,12 +11,17 @@ using BattleTech.Analytics.Sim; using BattleTech.Data; using BattleTech.Framework; +using BattleTech.Framework.Save; +using BattleTech.Save; +using BattleTech.Save.Test; using BattleTech.UI; using BattleTech.UI.Tooltips; using BattleTech.UI.TMProWrapper; using Localize; using HBS.Collections; +using HBS.FSM; using HBS.Util; +using UnityEngine; namespace BattletechPerformanceFix { @@ -46,42 +52,62 @@ public void Activate() { // fix 1.4: when a savefile is created, two copies of each contract are created and stored; // when that savefile is loaded, one set of copies overwrites the other, and both sets register subs. // this patch unregisters subs for the about-to-be-overwritten contracts - "Rehydrate".Pre(); - // fix 1.5: TODO explain this - // FIXME unable to attach to a generic method with generic parameter (how???) - //var paramTypes = new Type[]{typeof(ContractOverride), typeof(string)}; - //var genericsTypes = new Type[]{typeof(ContractOverride)}; - //var meth = AccessTools.Method(typeof(JSONSerializationUtility), "FromJSON", paramTypes, genericsTypes); - //var patch = new HarmonyMethod(AccessTools.Method(self, "FromJSON_Post")); - // NOTE ideally we would patch FromJSON() for T : ContractOverride, - // but Harmony has trouble dealing with generic methods, so we patch this instead. - var paramTypes = new Type[]{typeof(object), typeof(Dictionary), typeof(string), typeof(HBS.Stopwatch), - typeof(HBS.Stopwatch), typeof(JSONSerializationUtility.RehydrationFilteringMode), - typeof(Func[])}; - var meth = AccessTools.Method(typeof(JSONSerializationUtility), "RehydrateObjectFromDictionary", paramTypes); - var patch = new HarmonyMethod(AccessTools.Method(self, "RehydrateObjectFromDictionary_Post")); - Main.harmony.Patch(meth, null, patch); + "Rehydrate".Pre("SS_Rehydrate_Pre"); + // fix 1.5: when the ContractOverrides are read from JSON, subs are created, but these COs get Copy()ed + // before getting attached to a Contract, and Copy also creates subs. the first set of subs + // are never seen by the user, so modify the JSON deserialization process to immediately unsub + "FullRehydrate".Post("CO_JSON_Post"); + "FromJSON".Post("CO_JSON_Post"); + // fix 1.6: when loading a campaign save, contracts & subs are created for all previously completed + // story missions (why? dunno) and then overwritten later on (ugh...) with the globalContracts + // defined in the file. completed contract subs were created and must be removed + "Rehydrate".Pre("SGS_Rehydrate_Pre"); + "Rehydrate".Post("SGS_Rehydrate_Post"); + "Rehydrate".Transpile("SGS_Rehydrate_Transpile"); // fixes group 2: occurs on entering/exiting a contract - // fix 2.2: when a contract completes, remove its OnLanguageChanged subs + // fix 2.1: when a contract completes, remove its OnLanguageChanged subs "ResolveCompleteContract".Pre(); + // fix 2.2: none of these classes need to store a CombatGameState + "ContractInitialize".Post("DialogueContent_ContractInitialize_Post"); + "ContractInitialize".Post("ConversationContent_ContractInitialize_Post"); + "ContractInitialize".Post("DialogBucketDef_ContractInitialize_Post"); // fixes group 3: occurs on transiting between star systems // fix 3.1: when a star system removes its contracts, remove those contracts' OnLanguageChanged subs "ResetContracts".Pre(); - // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs - "StartGeneratePotentialContractsRoutine".Pre(); + // fix 3.2: see below // fixes group 4: occurs on accepting & completing a travel contract - // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, let the caller handle it - paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, + // let the caller handle it... + var paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), typeof(bool), typeof(int), typeof(int), typeof(int?)}; var ctor = AccessTools.Constructor(typeof(Contract), paramTypes); - patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); + var patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); Main.harmony.Patch(ctor, null, null, patch); - "StartGeneratePotentialContractsRoutine".Transpile(); - + // ...and perform a Copy() of the ObjectiveOverride in the one spot that needs it + // NOTE there's no clear way for Harmony to transpile an IEnumerator-returning method directly + // (something about having to patch *all* instances of IEnumerator.MoveNext() [lol no]), + // so instead just reimplement the method and patch its callers to use the reimplementation + "GeneratePotentialContracts".Transpile(); + + // fixes group 5: occurs when completing and/or cancelling a travel contract + // fix 5.1: when arriving at a travel contract's destination, + // remove the breadcrumb's ("pointer" to destination contract's) subs + "FinishCompleteBreadcrumbProcess".Pre(); + "FinishCompleteBreadcrumbProcess".Post(); + // fix 5.2.1: when backing out of a travel contract proper (ie not breadcrumb), remove its subs + "OnLanceConfigurationCancelled".Pre(); + // fix 5.2.2: same but for campaign mission + "CancelStoryOrConsecutiveLanceConfiguration".Pre(); + // fix 5.3: when cancelling a travel contract, remove its breadcrumb's subs + "FailBreadcrumb".Pre(); + + // fixes group 6: occurs on creating a new savefile + // fix 6.1: clean up the GameInstanceSave.references after serialization is complete + "PostSerialization".Post(); } private static IEnumerable Session_Transpile(IEnumerable ins) @@ -141,7 +167,6 @@ private static void RemoveContractSubscriptions(MessageCenter mc, Contract contr new ReceiveMessageCenterMessage(contract.OnLanguageChanged)); if (contract.Override != null) { - LogSpam(" contract.Override is set, removing its subs"); RemoveContractOverrideSubscriptions(mc, contract.Override); } } @@ -292,7 +317,7 @@ private static IEnumerable Nop_Transpile(IEnumerable.Instance.Game.MessageCenter; - RemoveContractOverrideSubscriptions(mc, (ContractOverride) target); + RemoveContractOverrideSubscriptions(mc, __instance); + } + + private static void SGS_Rehydrate_Pre(SimGameState __instance, ref List __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __state = __instance.globalContracts; + } + + private static void SGS_Rehydrate_Post(SimGameState __instance, ref List __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + LogSpam($"{__state?.Count} contracts"); + foreach (var contract in __state) { + RemoveContractSubscriptions(__instance.MessageCenter, contract); + } + } + + private static IEnumerable SGS_Rehydrate_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + var types = new Type[]{typeof(SimGameState), typeof(SimGameState.AddContractData)}; + var meth = AccessTools.Method(self, "_AddContract", types); + return TranspileReplaceCall(ins, "AddContract", meth); + } + + private static Contract _AddContract(SimGameState __instance, SimGameState.AddContractData contractData) + { + StarSystem starSystem; + if (!string.IsNullOrEmpty(contractData.TargetSystem)) + { + string validatedSystemString = __instance.GetValidatedSystemString(contractData.TargetSystem); + if (!__instance.starDict.ContainsKey(validatedSystemString)) + { + return null; + } + starSystem = __instance.starDict[validatedSystemString]; + } + else + { + starSystem = __instance.CurSystem; + } + FactionValue factionValueFromString = __instance.GetFactionValueFromString(contractData.Target); + FactionValue factionValueFromString2 = __instance.GetFactionValueFromString(contractData.Employer); + FactionValue factionValue = __instance.GetFactionValueFromString(contractData.TargetAlly); + FactionValue factionValue2 = __instance.GetFactionValueFromString(contractData.EmployerAlly); + FactionValue factionValueFromString3 = __instance.GetFactionValueFromString(contractData.NeutralToAll); + FactionValue factionValueFromString4 = __instance.GetFactionValueFromString(contractData.HostileToAll); + if (factionValueFromString.IsInvalidUnset || factionValueFromString2.IsInvalidUnset) + { + return null; + } + factionValue = (factionValue.IsInvalidUnset ? factionValueFromString : factionValue); + factionValue2 = (factionValue2.IsInvalidUnset ? factionValueFromString2 : factionValue2); + ContractOverride contractOverride = __instance.DataManager.ContractOverrides.Get(contractData.ContractName).Copy(); + RemoveContractOverrideSubscriptions(__instance.MessageCenter, contractOverride); + ContractTypeValue contractTypeValue = contractOverride.ContractTypeValue; + if (contractTypeValue.IsTravelOnly) + { + return __instance.AddTravelContract(contractOverride, starSystem, factionValueFromString2); + } + List releasedMapsAndEncountersByContractTypeAndOwnership = MetadataDatabase.Instance.GetReleasedMapsAndEncountersByContractTypeAndOwnership(contractTypeValue.ID, false); + if (releasedMapsAndEncountersByContractTypeAndOwnership == null || releasedMapsAndEncountersByContractTypeAndOwnership.Count == 0) + { + UnityEngine.Debug.LogError(string.Format("There are no playable maps for __instance contract type[{0}]. Was your map published?", contractTypeValue.Name)); + } + MapAndEncounters mapAndEncounters = releasedMapsAndEncountersByContractTypeAndOwnership[0]; + List list = new List(); + foreach (EncounterLayer_MDD encounterLayer_MDD in mapAndEncounters.Encounters) + { + if (encounterLayer_MDD.ContractTypeRow.ContractTypeID == (long)contractTypeValue.ID) + { + list.Add(encounterLayer_MDD); + } + } + if (list.Count <= 0) + { + throw new Exception("Map does not contain any encounters of type: " + contractTypeValue.Name); + } + string encounterLayerGUID = list[__instance.NetworkRandom.Int(0, list.Count)].EncounterLayerGUID; + GameContext gameContext = new GameContext(__instance.Context); + gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, starSystem); + if (contractData.IsGlobal) + { + Contract contract = __instance.CreateTravelContract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, contractOverride, gameContext, factionValueFromString2, factionValueFromString, factionValue, factionValue2, factionValueFromString3, factionValueFromString4, contractData.IsGlobal, contractOverride.difficulty); + __instance.PrepContract(contract, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, starSystem); + __instance.GlobalContracts.Add(contract); + return contract; + } + Contract contract2 = new Contract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, __instance.BattleTechGame, contractOverride, gameContext, true, contractOverride.difficulty, 0, null); + if (!contractData.FromSave) + { + ContractData contractData2 = new ContractData(contractData.ContractName, contractData.Target, contractData.Employer, contractData.TargetSystem, contractData.TargetAlly, contractData.EmployerAlly); + contractData2.SetGuid(Guid.NewGuid().ToString()); + contract2.SetGuid(contractData2.GUID); + __instance.contractBits.Add(contractData2); + } + if (contractData.FromSave) + { + contract2.SetGuid(contractData.SaveGuid); + } + __instance.PrepContract(contract2, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract2.Override.travelSeed, starSystem); + starSystem.SystemContracts.Add(contract2); + return contract2; } private static void ResolveCompleteContract_Pre(SimGameState __instance) @@ -322,6 +449,24 @@ private static void ResolveCompleteContract_Pre(SimGameState __instance) } } + private static void DialogueContent_ContractInitialize_Post(DialogueContent __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.combat = null; + } + + private static void ConversationContent_ContractInitialize_Post(ConversationContent __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.combat = null; + } + + private static void DialogBucketDef_ContractInitialize_Post(DialogBucketDef __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.combat = null; + } + private static void ResetContracts_Pre(StarSystem __instance) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); @@ -333,21 +478,6 @@ private static void ResetContracts_Pre(StarSystem __instance) } } - private static void - StartGeneratePotentialContractsRoutine_Pre(SimGameState __instance, bool clearExistingContracts, - StarSystem systemOverride) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (clearExistingContracts) { - List contracts = (systemOverride != null) ? __instance.CurSystem.SystemBreadcrumbs : - __instance.CurSystem.SystemContracts; - LogSpam($"clearExistingContracts is set; removing subscriptions for {contracts.Count} contracts"); - foreach(var contract in contracts) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } - } - } - private static IEnumerable Contract_Transpile(IEnumerable ins) { LogInfo($"Contract_Transpile: nopping call to Copy()"); @@ -364,16 +494,113 @@ private static IEnumerable Contract_Transpile(IEnumerable - StartGeneratePotentialContractsRoutine_Transpile(IEnumerable ins) + GeneratePotentialContracts_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + var types = new Type[]{typeof(SimGameState), typeof(bool), typeof(Action), typeof(StarSystem), typeof(bool)}; + var meth = AccessTools.Method(self, "_StartGeneratePotentialContractsRoutine", types); + return TranspileReplaceCall(ins, "StartGeneratePotentialContractsRoutine", meth); + } + + private static IEnumerator + _StartGeneratePotentialContractsRoutine(SimGameState __instance, bool clearExistingContracts, + Action onContractGenComplete, StarSystem systemOverride, bool useWait) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var types = new Type[]{typeof(SimGameState), typeof(StarSystem), typeof(bool), typeof(MapAndEncounters), - typeof(SimGameState.MapEncounterContractData), typeof(GameContext)}; - var meth = AccessTools.Method(self, "_CreateProceduralContract", types); - return TranspileReplaceCall(ins, "CreateProceduralContract", meth); + int debugCount = 0; + bool usingBreadcrumbs = systemOverride != null; + if (useWait) + { + yield return new WaitForSeconds(0.2f); + } + StarSystem system; + List contractList; + int maxContracts; + if (usingBreadcrumbs) + { + system = systemOverride; + contractList = __instance.CurSystem.SystemBreadcrumbs; + maxContracts = __instance.CurSystem.CurMaxBreadcrumbs; + } + else + { + system = __instance.CurSystem; + contractList = __instance.CurSystem.SystemContracts; + maxContracts = Mathf.CeilToInt(system.CurMaxContracts); + } + if (clearExistingContracts) + { + // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs + LogSpam($"clearExistingContracts is set; removing subscriptions for {contractList.Count} contractList"); + foreach(var contract in contractList) { + RemoveContractSubscriptions(__instance.MessageCenter, contract); + } + contractList.Clear(); + } + SimGameState.ContractDifficultyRange difficultyRange = __instance.GetContractRangeDifficultyRange(system, __instance.SimGameMode, __instance.GlobalDifficulty); + Dictionary> potentialContracts = __instance.GetSinglePlayerProceduralContractOverrides(difficultyRange); + WeightedList playableMaps = SimGameState.GetSinglePlayerProceduralPlayableMaps(system); + Dictionary> validParticipants = __instance.GetValidParticipants(system); + if (!__instance.HasValidMaps(system, playableMaps) || !__instance.HasValidContracts(difficultyRange, potentialContracts) || !__instance.HasValidParticipants(system, validParticipants)) + { + if (onContractGenComplete != null) + { + onContractGenComplete(); + } + yield break; + } + __instance.ClearUsedBiomeFromDiscardPile(playableMaps); + while (contractList.Count < maxContracts && debugCount < 1000) + { + int num = debugCount; + debugCount = num + 1; + IEnumerable source = from map in playableMaps + select map.Map.Weight; + WeightedList weightedList = new WeightedList(WeightedListType.WeightedRandom, playableMaps.ToList(), source.ToList(), 0); + __instance.FilterActiveMaps(weightedList, contractList); + weightedList.Reset(false); + MapAndEncounters next = weightedList.GetNext(false); + SimGameState.MapEncounterContractData mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); + while (!mapEncounterContractData.HasContracts && weightedList.ActiveListCount > 0) + { + next = weightedList.GetNext(false); + mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); + } + system.SetCurrentContractFactions(null, null); + if (mapEncounterContractData == null || mapEncounterContractData.Contracts.Count == 0) + { + if (__instance.mapDiscardPile.Count > 0) + { + __instance.mapDiscardPile.Clear(); + } + else + { + debugCount = 1000; + SimGameState.logger.LogError(string.Format("[CONTRACT] Unable to find any valid contracts for available map pool. Alert designers."/*, Array.Empty()*/)); + } + } + GameContext gameContext = new GameContext(__instance.Context); + gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, system); + // see fix 4.1 + Contract item = _CreateProceduralContract(__instance, system, usingBreadcrumbs, next, mapEncounterContractData, gameContext); + contractList.Add(item); + if (useWait) + { + yield return new WaitForSeconds(0.2f); + } + } + if (debugCount >= 1000) + { + SimGameState.logger.LogError("Unable to fill contract list. Please inform AJ Immediately"); + } + if (onContractGenComplete != null) + { + onContractGenComplete(); + } + yield break; } - private Contract + private static Contract _CreateProceduralContract(SimGameState __instance, StarSystem system, bool usingBreadcrumbs, MapAndEncounters level, SimGameState.MapEncounterContractData MapEncounterContractData, GameContext gameContext) { @@ -399,6 +626,7 @@ private Contract gameContext, employer, target, targetAlly, employerAlly, neutralToAll, hostileToAll, false, difficulty); } else { + // see fix 4.1 LogSpam("copying contractOverride"); contractOverride = contractOverride.Copy(); contract = new Contract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, @@ -411,6 +639,104 @@ private Contract hostileToAll, level.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, system); return contract; } + + private static void FinishCompleteBreadcrumbProcess_Pre(SimGameState __instance, ref Contract __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.activeBreadcrumb != null) { + LogSpam($"activeBreadcrumb: {__instance.activeBreadcrumb.GetHashCode()}"); + } + if (__instance.pendingBreadcrumb != null) { + LogSpam($"pendingBreadcrumb: {__instance.pendingBreadcrumb.GetHashCode()}"); + } + __state = __instance.activeBreadcrumb; + } + + private static void FinishCompleteBreadcrumbProcess_Post(SimGameState __instance, ref Contract __state) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.pendingBreadcrumb == null && __state != null) { + LogSpam($"activeBreadcrumb and pendingBreadcrumb are both null, __state isn't null; contract was removed"); + RemoveContractSubscriptions(__instance.MessageCenter, __state); + } + } + + private static void OnLanceConfigurationCancelled_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.SelectedContract != null && !__instance.SelectedContract.IsPriorityContract && + __instance.pendingBreadcrumb != null && __instance.IsSelectedContractForced) { + if (__instance.CurSystem.SystemContracts.Contains(__instance.SelectedContract) || + __instance.GlobalContracts.Contains(__instance.SelectedContract)) { + RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); + } + } + } + + private static void CancelStoryOrConsecutiveLanceConfiguration_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.SelectedContract != null) { + LogSpam($"SelectedContract: {__instance.SelectedContract.GetHashCode()}"); + RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); + } + } + + private static void FailBreadcrumb_Pre(SimGameState __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (__instance.activeBreadcrumb != null) { + RemoveContractSubscriptions(__instance.MessageCenter, __instance.activeBreadcrumb); + } + } + + private static void ClearMessageMemory(MessageMemory msgMemory) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + if (msgMemory.subscribedMessages != null) { + LogSpam($" {msgMemory.subscribedMessages.Count} subscribed "); + foreach (var key in msgMemory.subscribedMessages.Keys) { + foreach (var sub in msgMemory.subscribedMessages[key]) { + msgMemory.messageCenter.RemoveSubscriber(key, sub); + } + msgMemory.subscribedMessages[key].Clear(); + } + msgMemory.subscribedMessages.Clear(); + } + if (msgMemory.trackedMessages != null) { + LogSpam($" {msgMemory.trackedMessages.Count} tracked "); + foreach (var trackList in msgMemory.trackedMessages.Values) { + trackList.Clear(); + } + msgMemory.trackedMessages.Clear(); + } + } + + private static void ClearBasicMachine(BasicMachine machine) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + machine.OnChange = null; + if (machine.stateList != null) { + LogSpam($" clearing state machine, {machine.stateList.Count} states"); + foreach (var state in machine.stateList) { + state.CanEnter = null; + state.OnEnter = null; + state.OnExit = null; + } + } + } + + // TODO test this more thoroughly, i think it caused problems in the past... + private static void PostSerialization_Post(GameInstanceSave __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + //LogSpam($"{__instance.serializePassOne.ToString()}"); + //if (!__instance.serializePassOne) { + // TODO test this on a combat save (might break in that case) + LogSpam("setting references to an empty SerializableReferenceContainer"); + __instance.references = new SerializableReferenceContainer("the one and only"); + //} + } } } // vim: ts=4:sw=4 From d9f97f171b1feea4aae5786ed647efad952ddae1 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 15 Feb 2021 11:06:02 -0600 Subject: [PATCH 05/10] replace existing OnLanguageChanged fixes with a set of much simpler fixes (dont even create subs in the first place) --- source/MemoryLeakFix.cs | 677 ++++++---------------------------------- 1 file changed, 104 insertions(+), 573 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index 550461f..2edf885 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -36,77 +36,52 @@ public void Activate() { "EndSession".Transpile("Session_Transpile"); // fix 1.2: add a RemoveSubscriber() for a message type that never had one to begin with "OnSimGameInitializeComplete".Post(); - // fix 1.3.1: clear InterpolatedText objects that aren't supposed to live forever - "Destroy".Post(); - // fix 1.3.2: patch methods making an InterpolatedText object and doesn't store it anywhere - "RunMadLibs".Transpile("MadLib_Transpile"); - "RunMadLibsOnLanceDef".Transpile("MadLib_Transpile"); - "RunMadLib".Transpile("MadLib_Transpile"); - "UpdateTMPText".Transpile("ToTMP_Transpile"); + // fix 1.3: remove OnLanguageChanged subscriptions for these objects, which never unsub and therefore leak. + // b/c the user must drop back to main menu to change the language, there's no reason + // to use these in the first place (objects are created in-game and never on the main menu) + // Contract + var contractCtorTypes = new Type[]{typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), + typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), + typeof(bool), typeof(int), typeof(int), typeof(int)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(Contract), contractCtorTypes), + null, null, new HarmonyMethod(self, "Contract_ctor_Transpile")); + "PostDeserialize".Transpile(); + // ContractObjectiveOverride + Main.harmony.Patch(AccessTools.Constructor(typeof(ContractObjectiveOverride), new Type[]{}), + null, null, new HarmonyMethod(self, "ContractObjectiveOverride_ctor_Transpile")); + var cooCtorTypes = new Type[]{typeof(ContractObjectiveGameLogic)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(ContractObjectiveOverride), cooCtorTypes), + null, null, new HarmonyMethod(self, "ContractObjectiveOverride_ctor_cogl_Transpile")); + // ObjectiveOverride + Main.harmony.Patch(AccessTools.Constructor(typeof(ObjectiveOverride), new Type[]{}), + null, null, new HarmonyMethod(self, "ObjectiveOverride_ctor_Transpile")); + var ooCtorTypes = new Type[]{typeof(ObjectiveGameLogic)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(ObjectiveOverride), ooCtorTypes), + null, null, new HarmonyMethod(self, "ObjectiveOverride_ctor_ogl_Transpile")); + // DialogueContentOverride + Main.harmony.Patch(AccessTools.Constructor(typeof(DialogueContentOverride), new Type[]{}), + null, null, new HarmonyMethod(self, "DialogueContentOverride_ctor_Transpile")); + var dcoCtorTypes = new Type[]{typeof(DialogueContent)}; + Main.harmony.Patch(AccessTools.Constructor(typeof(DialogueContentOverride), dcoCtorTypes), + null, null, new HarmonyMethod(self, "DialogueContentOverride_ctor_dc_Transpile")); + // InterpolatedText + "Init".Transpile(); // these finalizers could never run to begin with, and they only did RemoveSubscriber; nop them - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - "Finalize".Transpile("Nop_Transpile"); - // fix 1.4: when a savefile is created, two copies of each contract are created and stored; - // when that savefile is loaded, one set of copies overwrites the other, and both sets register subs. - // this patch unregisters subs for the about-to-be-overwritten contracts - "Rehydrate".Pre("SS_Rehydrate_Pre"); - // fix 1.5: when the ContractOverrides are read from JSON, subs are created, but these COs get Copy()ed - // before getting attached to a Contract, and Copy also creates subs. the first set of subs - // are never seen by the user, so modify the JSON deserialization process to immediately unsub - "FullRehydrate".Post("CO_JSON_Post"); - "FromJSON".Post("CO_JSON_Post"); - // fix 1.6: when loading a campaign save, contracts & subs are created for all previously completed - // story missions (why? dunno) and then overwritten later on (ugh...) with the globalContracts - // defined in the file. completed contract subs were created and must be removed - "Rehydrate".Pre("SGS_Rehydrate_Pre"); - "Rehydrate".Post("SGS_Rehydrate_Post"); - "Rehydrate".Transpile("SGS_Rehydrate_Transpile"); + // FIXME? may need to nop out specifically the call to RemoveSubscriber (test this) + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); + "Finalize".Transpile("TranspileNopAll"); // fixes group 2: occurs on entering/exiting a contract - // fix 2.1: when a contract completes, remove its OnLanguageChanged subs - "ResolveCompleteContract".Pre(); - // fix 2.2: none of these classes need to store a CombatGameState + // fix 2.1: none of these classes need to store a CombatGameState "ContractInitialize".Post("DialogueContent_ContractInitialize_Post"); "ContractInitialize".Post("ConversationContent_ContractInitialize_Post"); "ContractInitialize".Post("DialogBucketDef_ContractInitialize_Post"); - // fixes group 3: occurs on transiting between star systems - // fix 3.1: when a star system removes its contracts, remove those contracts' OnLanguageChanged subs - "ResetContracts".Pre(); - // fix 3.2: see below - - // fixes group 4: occurs on accepting & completing a travel contract - // fix 4.1: don't let the Contract constructor make a copy of its given ContractOverride, - // let the caller handle it... - var paramTypes = new Type[]{ typeof(string), typeof(string), typeof(string), typeof(ContractTypeValue), - typeof(GameInstance), typeof(ContractOverride), typeof(GameContext), - typeof(bool), typeof(int), typeof(int), typeof(int?)}; - var ctor = AccessTools.Constructor(typeof(Contract), paramTypes); - var patch = new HarmonyMethod(AccessTools.Method(self, "Contract_Transpile")); - Main.harmony.Patch(ctor, null, null, patch); - // ...and perform a Copy() of the ObjectiveOverride in the one spot that needs it - // NOTE there's no clear way for Harmony to transpile an IEnumerator-returning method directly - // (something about having to patch *all* instances of IEnumerator.MoveNext() [lol no]), - // so instead just reimplement the method and patch its callers to use the reimplementation - "GeneratePotentialContracts".Transpile(); - - // fixes group 5: occurs when completing and/or cancelling a travel contract - // fix 5.1: when arriving at a travel contract's destination, - // remove the breadcrumb's ("pointer" to destination contract's) subs - "FinishCompleteBreadcrumbProcess".Pre(); - "FinishCompleteBreadcrumbProcess".Post(); - // fix 5.2.1: when backing out of a travel contract proper (ie not breadcrumb), remove its subs - "OnLanceConfigurationCancelled".Pre(); - // fix 5.2.2: same but for campaign mission - "CancelStoryOrConsecutiveLanceConfiguration".Pre(); - // fix 5.3: when cancelling a travel contract, remove its breadcrumb's subs - "FailBreadcrumb".Pre(); - - // fixes group 6: occurs on creating a new savefile - // fix 6.1: clean up the GameInstanceSave.references after serialization is complete + // fixes group 3: occurs on creating a new savefile + // fix 3.1: clean up the GameInstanceSave.references after serialization is complete "PostSerialization".Post(); } @@ -144,77 +119,87 @@ private static void OnSimGameInitializeComplete_Post(SimGameUXCreator __instance new ReceiveMessageCenterMessage(__instance.OnSimGameInitializeComplete)); } - private static void Destroy_Post(SimGameState __instance) + private static IEnumerable Contract_ctor_Transpile(IEnumerable ins) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance == null) { - LogSpam("SimGameState was null (ok if first load)"); - return; - } + return TranspileNopIndicesRange(ins, 125, 134); + } - var contracts = __instance.GetAllCurrentlySelectableContracts(); - LogSpam($"removing subscriptions for {contracts.Count} contracts"); - foreach (var contract in contracts) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } + private static IEnumerable PostDeserialize_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 21, 27); } - private static void RemoveContractSubscriptions(MessageCenter mc, Contract contract) + private static IEnumerable + ContractObjectiveOverride_ctor_Transpile(IEnumerable ins) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - LogSpam($"removing subs for contract {contract.GetHashCode()}"); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(contract.OnLanguageChanged)); + return TranspileNopIndicesRange(ins, 5, 14); + } - if (contract.Override != null) { - RemoveContractOverrideSubscriptions(mc, contract.Override); - } + private static IEnumerable + ContractObjectiveOverride_ctor_cogl_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 9, 18); } - private static void - RemoveContractOverrideSubscriptions(MessageCenter mc, ContractOverride contractOverride) + private static IEnumerable + ObjectiveOverride_ctor_Transpile(IEnumerable ins) { - LogSpam($"removing subs for contract override {contractOverride.GetHashCode()}"); - foreach (var dialogue in contractOverride.dialogueList) { - foreach (var content in dialogue.dialogueContent) { - RemoveTextSubscriber(mc, (InterpolatedText) content._Words); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(content.OnLanguageChanged)); - } - } + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 8, 17); + } - foreach (var objective in contractOverride.contractObjectiveList) { - RemoveTextSubscriber(mc, (InterpolatedText) objective._Title); - RemoveTextSubscriber(mc, (InterpolatedText) objective._Description); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); - } + private static IEnumerable + ObjectiveOverride_ctor_ogl_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 12, 21); + } - foreach (var objective in contractOverride.objectiveList) { - RemoveTextSubscriber(mc, (InterpolatedText) objective._Title); - RemoveTextSubscriber(mc, (InterpolatedText) objective._Description); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(objective.OnLanguageChanged)); - } + private static IEnumerable + DialogueContentOverride_ctor_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 23, 32); } - private static void RemoveTextSubscriber(MessageCenter mc, InterpolatedText text) { - if (text != null) { - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(text.OnLanguageChanged)); - } + private static IEnumerable + DialogueContentOverride_ctor_dc_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 60, 69); + } + + private static IEnumerable Init_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 3, 10); } - private static IEnumerable MadLib_Transpile(IEnumerable ins) + private static IEnumerable + TranspileNopIndicesRange(IEnumerable ins, int startIndex, int endIndex) { - var methString = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(string)}); - var methTagSet = AccessTools.Method(self, "_Contract_RunMadLib", - new Type[]{typeof(Contract), typeof(TagSet)}); - var firstPass = TranspileReplaceOverloadedCall(ins, typeof(Contract), "RunMadLib", - new Type[]{typeof(string)}, methString); - return TranspileReplaceOverloadedCall(firstPass, typeof(Contract), "RunMadLib", - new Type[]{typeof(TagSet)}, methTagSet); + LogDebug($"TranspileNopIndicesRange: nopping indices {startIndex}-{endIndex}"); + if (endIndex < startIndex || startIndex < 0) { + LogError($"TranspileNopIndicesRange: invalid use with startIndex = {startIndex}," + + $" endIndex = {endIndex} (transpiled method remains unmodified)"); + return ins; + } + + var code = ins.ToList(); + try { + for (int i = startIndex; i <= endIndex; i++) { + code[i].opcode = OpCodes.Nop; + code[i].operand = null; + } + return code.AsEnumerable(); + } catch (ArgumentOutOfRangeException ex) { + LogError($"TranspileNopIndicesRange: {ex.Message} (transpiled method remains unmodified)"); + return ins; + } } private static IEnumerable @@ -251,64 +236,7 @@ private static IEnumerable }); } - private static string _Contract_RunMadLib(Contract __instance, string text) - { - if (string.IsNullOrEmpty(text)) - { - return ""; - } - InterpolatedText iText = __instance.Interpolate(text); - text = iText.ToString(false); - __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); - return text; - } - - private static void _Contract_RunMadLib(Contract __instance, TagSet tagSet) - { - if (tagSet == null) - { - return; - } - string[] array = tagSet.ToArray(); - if (array == null) - { - return; - } - for (int i = 0; i < array.Length; i++) - { - string text = array[i]; - InterpolatedText iText = __instance.Interpolate(text); - text = iText.ToString(false); - __instance.messageCenter.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); - array[i] = text.ToLower(); - } - tagSet.Clear(); - tagSet.AddRange(array); - } - - private static IEnumerable ToTMP_Transpile(IEnumerable ins) - { - var originalTypes = new Type[]{typeof(GameContext), typeof(TextTooltipFormatOptions)}; - var replacementTypes = new Type[]{typeof(TextTooltipParser), typeof(GameContext), typeof(TextTooltipFormatOptions)}; - var meth = AccessTools.Method(self, "_TextTooltipParser_ToTMP", replacementTypes); - return TranspileReplaceOverloadedCall(ins, typeof(TextTooltipParser), "ToTMP", originalTypes, meth); - } - - private static Text - _TextTooltipParser_ToTMP(TextTooltipParser __instance, GameContext gameContext, TextTooltipFormatOptions formatOptions) - { - var mc = HBS.SceneSingletonBehavior.Instance.Game.MessageCenter; - return __instance.GenerateFinalString((TextTooltipData x) => { - InterpolatedText iText = x.ToTMP(gameContext, formatOptions); - mc.RemoveSubscriber(MessageCenterMessageType.OnLanguageChanged, - new ReceiveMessageCenterMessage(iText.OnLanguageChanged)); - return iText; - }); - } - - private static IEnumerable Nop_Transpile(IEnumerable ins) + private static IEnumerable TranspileNopAll(IEnumerable ins) { return ins.SelectMany(i => { i.opcode = OpCodes.Nop; @@ -317,138 +245,6 @@ private static IEnumerable Nop_Transpile(IEnumerable.Instance.Game.MessageCenter; - RemoveContractOverrideSubscriptions(mc, __instance); - } - - private static void SGS_Rehydrate_Pre(SimGameState __instance, ref List __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - __state = __instance.globalContracts; - } - - private static void SGS_Rehydrate_Post(SimGameState __instance, ref List __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - LogSpam($"{__state?.Count} contracts"); - foreach (var contract in __state) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } - } - - private static IEnumerable SGS_Rehydrate_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var types = new Type[]{typeof(SimGameState), typeof(SimGameState.AddContractData)}; - var meth = AccessTools.Method(self, "_AddContract", types); - return TranspileReplaceCall(ins, "AddContract", meth); - } - - private static Contract _AddContract(SimGameState __instance, SimGameState.AddContractData contractData) - { - StarSystem starSystem; - if (!string.IsNullOrEmpty(contractData.TargetSystem)) - { - string validatedSystemString = __instance.GetValidatedSystemString(contractData.TargetSystem); - if (!__instance.starDict.ContainsKey(validatedSystemString)) - { - return null; - } - starSystem = __instance.starDict[validatedSystemString]; - } - else - { - starSystem = __instance.CurSystem; - } - FactionValue factionValueFromString = __instance.GetFactionValueFromString(contractData.Target); - FactionValue factionValueFromString2 = __instance.GetFactionValueFromString(contractData.Employer); - FactionValue factionValue = __instance.GetFactionValueFromString(contractData.TargetAlly); - FactionValue factionValue2 = __instance.GetFactionValueFromString(contractData.EmployerAlly); - FactionValue factionValueFromString3 = __instance.GetFactionValueFromString(contractData.NeutralToAll); - FactionValue factionValueFromString4 = __instance.GetFactionValueFromString(contractData.HostileToAll); - if (factionValueFromString.IsInvalidUnset || factionValueFromString2.IsInvalidUnset) - { - return null; - } - factionValue = (factionValue.IsInvalidUnset ? factionValueFromString : factionValue); - factionValue2 = (factionValue2.IsInvalidUnset ? factionValueFromString2 : factionValue2); - ContractOverride contractOverride = __instance.DataManager.ContractOverrides.Get(contractData.ContractName).Copy(); - RemoveContractOverrideSubscriptions(__instance.MessageCenter, contractOverride); - ContractTypeValue contractTypeValue = contractOverride.ContractTypeValue; - if (contractTypeValue.IsTravelOnly) - { - return __instance.AddTravelContract(contractOverride, starSystem, factionValueFromString2); - } - List releasedMapsAndEncountersByContractTypeAndOwnership = MetadataDatabase.Instance.GetReleasedMapsAndEncountersByContractTypeAndOwnership(contractTypeValue.ID, false); - if (releasedMapsAndEncountersByContractTypeAndOwnership == null || releasedMapsAndEncountersByContractTypeAndOwnership.Count == 0) - { - UnityEngine.Debug.LogError(string.Format("There are no playable maps for __instance contract type[{0}]. Was your map published?", contractTypeValue.Name)); - } - MapAndEncounters mapAndEncounters = releasedMapsAndEncountersByContractTypeAndOwnership[0]; - List list = new List(); - foreach (EncounterLayer_MDD encounterLayer_MDD in mapAndEncounters.Encounters) - { - if (encounterLayer_MDD.ContractTypeRow.ContractTypeID == (long)contractTypeValue.ID) - { - list.Add(encounterLayer_MDD); - } - } - if (list.Count <= 0) - { - throw new Exception("Map does not contain any encounters of type: " + contractTypeValue.Name); - } - string encounterLayerGUID = list[__instance.NetworkRandom.Int(0, list.Count)].EncounterLayerGUID; - GameContext gameContext = new GameContext(__instance.Context); - gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, starSystem); - if (contractData.IsGlobal) - { - Contract contract = __instance.CreateTravelContract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, contractOverride, gameContext, factionValueFromString2, factionValueFromString, factionValue, factionValue2, factionValueFromString3, factionValueFromString4, contractData.IsGlobal, contractOverride.difficulty); - __instance.PrepContract(contract, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, starSystem); - __instance.GlobalContracts.Add(contract); - return contract; - } - Contract contract2 = new Contract(mapAndEncounters.Map.MapName, mapAndEncounters.Map.MapPath, encounterLayerGUID, contractTypeValue, __instance.BattleTechGame, contractOverride, gameContext, true, contractOverride.difficulty, 0, null); - if (!contractData.FromSave) - { - ContractData contractData2 = new ContractData(contractData.ContractName, contractData.Target, contractData.Employer, contractData.TargetSystem, contractData.TargetAlly, contractData.EmployerAlly); - contractData2.SetGuid(Guid.NewGuid().ToString()); - contract2.SetGuid(contractData2.GUID); - __instance.contractBits.Add(contractData2); - } - if (contractData.FromSave) - { - contract2.SetGuid(contractData.SaveGuid); - } - __instance.PrepContract(contract2, factionValueFromString2, factionValue2, factionValueFromString, factionValue, factionValueFromString3, factionValueFromString4, mapAndEncounters.Map.BiomeSkinEntry.BiomeSkin, contract2.Override.travelSeed, starSystem); - starSystem.SystemContracts.Add(contract2); - return contract2; - } - - private static void ResolveCompleteContract_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.CompletedContract != null) { - RemoveContractSubscriptions(__instance.MessageCenter, __instance.CompletedContract); - } - } - private static void DialogueContent_ContractInitialize_Post(DialogueContent __instance) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); @@ -467,275 +263,10 @@ private static void DialogBucketDef_ContractInitialize_Post(DialogBucketDef __in __instance.combat = null; } - private static void ResetContracts_Pre(StarSystem __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - foreach (var contract in __instance.activeSystemContracts) { - RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); - } - foreach (var contract in __instance.activeSystemBreadcrumbs) { - RemoveContractSubscriptions(__instance.Sim.MessageCenter, contract); - } - } - - private static IEnumerable Contract_Transpile(IEnumerable ins) - { - LogInfo($"Contract_Transpile: nopping call to Copy()"); - var meth = AccessTools.Method(typeof(ContractOverride), "Copy"); - CodeInstruction toBeNopped = new CodeInstruction(OpCodes.Callvirt, meth); - return ins.SelectMany(i => { - if (i.opcode == toBeNopped.opcode && i.operand == toBeNopped.operand) { - - i.opcode = OpCodes.Nop; - i.operand = null; - } - return Sequence(i); - }); - } - - private static IEnumerable - GeneratePotentialContracts_Transpile(IEnumerable ins) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - var types = new Type[]{typeof(SimGameState), typeof(bool), typeof(Action), typeof(StarSystem), typeof(bool)}; - var meth = AccessTools.Method(self, "_StartGeneratePotentialContractsRoutine", types); - return TranspileReplaceCall(ins, "StartGeneratePotentialContractsRoutine", meth); - } - - private static IEnumerator - _StartGeneratePotentialContractsRoutine(SimGameState __instance, bool clearExistingContracts, - Action onContractGenComplete, StarSystem systemOverride, bool useWait) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - int debugCount = 0; - bool usingBreadcrumbs = systemOverride != null; - if (useWait) - { - yield return new WaitForSeconds(0.2f); - } - StarSystem system; - List contractList; - int maxContracts; - if (usingBreadcrumbs) - { - system = systemOverride; - contractList = __instance.CurSystem.SystemBreadcrumbs; - maxContracts = __instance.CurSystem.CurMaxBreadcrumbs; - } - else - { - system = __instance.CurSystem; - contractList = __instance.CurSystem.SystemContracts; - maxContracts = Mathf.CeilToInt(system.CurMaxContracts); - } - if (clearExistingContracts) - { - // fix 3.2: when traveling to a new star system, remove the current system's contracts' OnLanguageChanged subs - LogSpam($"clearExistingContracts is set; removing subscriptions for {contractList.Count} contractList"); - foreach(var contract in contractList) { - RemoveContractSubscriptions(__instance.MessageCenter, contract); - } - contractList.Clear(); - } - SimGameState.ContractDifficultyRange difficultyRange = __instance.GetContractRangeDifficultyRange(system, __instance.SimGameMode, __instance.GlobalDifficulty); - Dictionary> potentialContracts = __instance.GetSinglePlayerProceduralContractOverrides(difficultyRange); - WeightedList playableMaps = SimGameState.GetSinglePlayerProceduralPlayableMaps(system); - Dictionary> validParticipants = __instance.GetValidParticipants(system); - if (!__instance.HasValidMaps(system, playableMaps) || !__instance.HasValidContracts(difficultyRange, potentialContracts) || !__instance.HasValidParticipants(system, validParticipants)) - { - if (onContractGenComplete != null) - { - onContractGenComplete(); - } - yield break; - } - __instance.ClearUsedBiomeFromDiscardPile(playableMaps); - while (contractList.Count < maxContracts && debugCount < 1000) - { - int num = debugCount; - debugCount = num + 1; - IEnumerable source = from map in playableMaps - select map.Map.Weight; - WeightedList weightedList = new WeightedList(WeightedListType.WeightedRandom, playableMaps.ToList(), source.ToList(), 0); - __instance.FilterActiveMaps(weightedList, contractList); - weightedList.Reset(false); - MapAndEncounters next = weightedList.GetNext(false); - SimGameState.MapEncounterContractData mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); - while (!mapEncounterContractData.HasContracts && weightedList.ActiveListCount > 0) - { - next = weightedList.GetNext(false); - mapEncounterContractData = __instance.FillMapEncounterContractData(system, difficultyRange, potentialContracts, validParticipants, next); - } - system.SetCurrentContractFactions(null, null); - if (mapEncounterContractData == null || mapEncounterContractData.Contracts.Count == 0) - { - if (__instance.mapDiscardPile.Count > 0) - { - __instance.mapDiscardPile.Clear(); - } - else - { - debugCount = 1000; - SimGameState.logger.LogError(string.Format("[CONTRACT] Unable to find any valid contracts for available map pool. Alert designers."/*, Array.Empty()*/)); - } - } - GameContext gameContext = new GameContext(__instance.Context); - gameContext.SetObject(GameContextObjectTagEnum.TargetStarSystem, system); - // see fix 4.1 - Contract item = _CreateProceduralContract(__instance, system, usingBreadcrumbs, next, mapEncounterContractData, gameContext); - contractList.Add(item); - if (useWait) - { - yield return new WaitForSeconds(0.2f); - } - } - if (debugCount >= 1000) - { - SimGameState.logger.LogError("Unable to fill contract list. Please inform AJ Immediately"); - } - if (onContractGenComplete != null) - { - onContractGenComplete(); - } - yield break; - } - - private static Contract - _CreateProceduralContract(SimGameState __instance, StarSystem system, bool usingBreadcrumbs, MapAndEncounters level, - SimGameState.MapEncounterContractData MapEncounterContractData, GameContext gameContext) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - WeightedList flatContracts = MapEncounterContractData.FlatContracts; - __instance.FilterContracts(flatContracts); - SimGameState.PotentialContract next = flatContracts.GetNext(true); - int id = next.contractOverride.ContractTypeValue.ID; - MapEncounterContractData.Encounters[id].Shuffle(); - string encounterLayerGUID = MapEncounterContractData.Encounters[id][0].EncounterLayerGUID; - ContractOverride contractOverride = next.contractOverride; - FactionValue employer = next.employer; - FactionValue target = next.target; - FactionValue employerAlly = next.employerAlly; - FactionValue targetAlly = next.targetAlly; - FactionValue neutralToAll = next.NeutralToAll; - FactionValue hostileToAll = next.HostileToAll; - int difficulty = next.difficulty; - Contract contract; - if (usingBreadcrumbs) { - contract = __instance.CreateTravelContract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, - next.contractOverride.ContractTypeValue, contractOverride, - gameContext, employer, target, targetAlly, employerAlly, - neutralToAll, hostileToAll, false, difficulty); - } else { - // see fix 4.1 - LogSpam("copying contractOverride"); - contractOverride = contractOverride.Copy(); - contract = new Contract(level.Map.MapName, level.Map.MapPath, encounterLayerGUID, - next.contractOverride.ContractTypeValue, __instance.BattleTechGame, - contractOverride, gameContext, true, difficulty, 0, null); - } - __instance.mapDiscardPile.Add(level.Map.MapID); - __instance.contractDiscardPile.Add(contractOverride.ID); - __instance.PrepContract(contract, employer, employerAlly, target, targetAlly, neutralToAll, - hostileToAll, level.Map.BiomeSkinEntry.BiomeSkin, contract.Override.travelSeed, system); - return contract; - } - - private static void FinishCompleteBreadcrumbProcess_Pre(SimGameState __instance, ref Contract __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.activeBreadcrumb != null) { - LogSpam($"activeBreadcrumb: {__instance.activeBreadcrumb.GetHashCode()}"); - } - if (__instance.pendingBreadcrumb != null) { - LogSpam($"pendingBreadcrumb: {__instance.pendingBreadcrumb.GetHashCode()}"); - } - __state = __instance.activeBreadcrumb; - } - - private static void FinishCompleteBreadcrumbProcess_Post(SimGameState __instance, ref Contract __state) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.pendingBreadcrumb == null && __state != null) { - LogSpam($"activeBreadcrumb and pendingBreadcrumb are both null, __state isn't null; contract was removed"); - RemoveContractSubscriptions(__instance.MessageCenter, __state); - } - } - - private static void OnLanceConfigurationCancelled_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.SelectedContract != null && !__instance.SelectedContract.IsPriorityContract && - __instance.pendingBreadcrumb != null && __instance.IsSelectedContractForced) { - if (__instance.CurSystem.SystemContracts.Contains(__instance.SelectedContract) || - __instance.GlobalContracts.Contains(__instance.SelectedContract)) { - RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); - } - } - } - - private static void CancelStoryOrConsecutiveLanceConfiguration_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.SelectedContract != null) { - LogSpam($"SelectedContract: {__instance.SelectedContract.GetHashCode()}"); - RemoveContractSubscriptions(__instance.MessageCenter, __instance.SelectedContract); - } - } - - private static void FailBreadcrumb_Pre(SimGameState __instance) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (__instance.activeBreadcrumb != null) { - RemoveContractSubscriptions(__instance.MessageCenter, __instance.activeBreadcrumb); - } - } - - private static void ClearMessageMemory(MessageMemory msgMemory) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - if (msgMemory.subscribedMessages != null) { - LogSpam($" {msgMemory.subscribedMessages.Count} subscribed "); - foreach (var key in msgMemory.subscribedMessages.Keys) { - foreach (var sub in msgMemory.subscribedMessages[key]) { - msgMemory.messageCenter.RemoveSubscriber(key, sub); - } - msgMemory.subscribedMessages[key].Clear(); - } - msgMemory.subscribedMessages.Clear(); - } - if (msgMemory.trackedMessages != null) { - LogSpam($" {msgMemory.trackedMessages.Count} tracked "); - foreach (var trackList in msgMemory.trackedMessages.Values) { - trackList.Clear(); - } - msgMemory.trackedMessages.Clear(); - } - } - - private static void ClearBasicMachine(BasicMachine machine) - { - LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - machine.OnChange = null; - if (machine.stateList != null) { - LogSpam($" clearing state machine, {machine.stateList.Count} states"); - foreach (var state in machine.stateList) { - state.CanEnter = null; - state.OnEnter = null; - state.OnExit = null; - } - } - } - - // TODO test this more thoroughly, i think it caused problems in the past... private static void PostSerialization_Post(GameInstanceSave __instance) { LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); - //LogSpam($"{__instance.serializePassOne.ToString()}"); - //if (!__instance.serializePassOne) { - // TODO test this on a combat save (might break in that case) - LogSpam("setting references to an empty SerializableReferenceContainer"); - __instance.references = new SerializableReferenceContainer("the one and only"); - //} + __instance.references = new SerializableReferenceContainer("the one and only"); } } } From d4d9971bcc2ca8a3cba8a3543c599fc4c52e8001 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 15 Feb 2021 11:33:26 -0600 Subject: [PATCH 06/10] cleanup --- source/MemoryLeakFix.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index 2edf885..c132066 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -9,19 +9,11 @@ using static BattletechPerformanceFix.Extensions; using BattleTech; using BattleTech.Analytics.Sim; -using BattleTech.Data; using BattleTech.Framework; -using BattleTech.Framework.Save; using BattleTech.Save; using BattleTech.Save.Test; using BattleTech.UI; -using BattleTech.UI.Tooltips; -using BattleTech.UI.TMProWrapper; using Localize; -using HBS.Collections; -using HBS.FSM; -using HBS.Util; -using UnityEngine; namespace BattletechPerformanceFix { From 3c43393be3b3732c3449ed7fdd8f39ae3f8f1e03 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Mon, 15 Feb 2021 13:19:05 -0600 Subject: [PATCH 07/10] cleanup --- source/MemoryLeakFix.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index c132066..cce5cb7 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -59,7 +59,6 @@ public void Activate() { // InterpolatedText "Init".Transpile(); // these finalizers could never run to begin with, and they only did RemoveSubscriber; nop them - // FIXME? may need to nop out specifically the call to RemoveSubscriber (test this) "Finalize".Transpile("TranspileNopAll"); "Finalize".Transpile("TranspileNopAll"); "Finalize".Transpile("TranspileNopAll"); From 2c80dc813c733829aa7217649bc0fd9dd7e2457a Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 21 Feb 2021 10:57:22 -0600 Subject: [PATCH 08/10] remove unused method --- source/MemoryLeakFix.cs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/source/MemoryLeakFix.cs b/source/MemoryLeakFix.cs index cce5cb7..6697165 100644 --- a/source/MemoryLeakFix.cs +++ b/source/MemoryLeakFix.cs @@ -207,26 +207,6 @@ private static IEnumerable }); } - private static IEnumerable - TranspileReplaceOverloadedCall(IEnumerable ins, Type originalMethodClass, - string originalMethodName, Type[] originalParamTypes, - MethodInfo replacementMethod) - { - LogInfo($"TranspileReplaceOverloadedCall: {originalMethodClass.ToString()}.{originalMethodName}" + - $"({String.Concat(originalParamTypes.Select(x => x.ToString()))}) -> {replacementMethod.ToString()}"); - return ins.SelectMany(i => { - var methInfo = i.operand as MethodInfo; - if (i.opcode == OpCodes.Callvirt && - methInfo.DeclaringType == originalMethodClass && - methInfo.Name.StartsWith(originalMethodName) && - Enumerable.SequenceEqual(methInfo.GetParameters().Select(x => x.ParameterType), originalParamTypes)) - { - i.operand = replacementMethod; - } - return Sequence(i); - }); - } - private static IEnumerable TranspileNopAll(IEnumerable ins) { return ins.SelectMany(i => { From eb61d7821fddee344daf66832102770420dda05b Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Sun, 21 Feb 2021 11:17:43 -0600 Subject: [PATCH 09/10] update readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5ce6626..19db7ff 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ - Make all simgame room transitions instant. - RemovedContractsFix - This fix removes invalid contracts allowing saves to load if a user created contract was removed from the mods in use. +- MemoryLeakFix + - Various fixes for memory leaks in the vanilla game. # Experimental patches - MDDB_TagsetQueryInChunks From e1425146533772ceb91225df83642571e7179fa7 Mon Sep 17 00:00:00 2001 From: Cole Thompson Date: Wed, 16 Dec 2020 12:46:25 -0600 Subject: [PATCH 10/10] Ignore vim .swp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aa4950f..4c47a94 100644 --- a/.gitignore +++ b/.gitignore @@ -267,3 +267,4 @@ __pycache__/ libs/ libs.fixed/ +*/*.swp