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