diff --git a/.gitignore b/.gitignore index aa4950f..4c47a94 100644 --- a/.gitignore +++ b/.gitignore @@ -267,3 +267,4 @@ __pycache__/ libs/ libs.fixed/ +*/*.swp 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 diff --git a/source/BattletechPerformanceFix.csproj b/source/BattletechPerformanceFix.csproj index 4c46833..ea5767a 100644 --- a/source/BattletechPerformanceFix.csproj +++ b/source/BattletechPerformanceFix.csproj @@ -67,12 +67,13 @@ - + + @@ -120,4 +121,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 + 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..6697165 --- /dev/null +++ b/source/MemoryLeakFix.cs @@ -0,0 +1,244 @@ +using System; +using System.Diagnostics; +using System.Reflection; +using System.Reflection.Emit; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Harmony; +using static BattletechPerformanceFix.Extensions; +using BattleTech; +using BattleTech.Analytics.Sim; +using BattleTech.Framework; +using BattleTech.Save; +using BattleTech.Save.Test; +using BattleTech.UI; +using Localize; + +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("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(); + // 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("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: 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 creating a new savefile + // fix 3.1: clean up the GameInstanceSave.references after serialization is complete + "PostSerialization".Post(); + } + + private static IEnumerable Session_Transpile(IEnumerable ins) + { + var meth = AccessTools.Method(self, "_UpdateMessageSubscriptions"); + return TranspileReplaceCall(ins, "UpdateMessageSubscriptions", meth); + } + + private static void _UpdateMessageSubscriptions(BattleTechSimAnalytics __instance, bool subscribe) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} 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($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.sim.MessageCenter.RemoveSubscriber( + MessageCenterMessageType.OnSimGameInitialized, + new ReceiveMessageCenterMessage(__instance.OnSimGameInitializeComplete)); + } + + private static IEnumerable Contract_ctor_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 125, 134); + } + + private static IEnumerable PostDeserialize_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 21, 27); + } + + private static IEnumerable + ContractObjectiveOverride_ctor_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 5, 14); + } + + private static IEnumerable + ContractObjectiveOverride_ctor_cogl_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 9, 18); + } + + private static IEnumerable + ObjectiveOverride_ctor_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 8, 17); + } + + private static IEnumerable + ObjectiveOverride_ctor_ogl_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 12, 21); + } + + private static IEnumerable + DialogueContentOverride_ctor_Transpile(IEnumerable ins) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + return TranspileNopIndicesRange(ins, 23, 32); + } + + 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 + TranspileNopIndicesRange(IEnumerable ins, int startIndex, int endIndex) + { + 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 + 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 TranspileNopAll(IEnumerable ins) + { + return ins.SelectMany(i => { + i.opcode = OpCodes.Nop; + i.operand = null; + return Sequence(i); + }); + } + + 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 PostSerialization_Post(GameInstanceSave __instance) + { + LogSpam($"{new StackTrace().GetFrame(0).GetMethod()} called"); + __instance.references = new SerializableReferenceContainer("the one and only"); + } + } +} +// vim: ts=4:sw=4