diff --git a/Build/Release/RogueTechPerfFix/RogueTechPerfFixes.dll b/Build/Release/RogueTechPerfFix/RogueTechPerfFixes.dll index 8ed8440..223c02f 100644 Binary files a/Build/Release/RogueTechPerfFix/RogueTechPerfFixes.dll and b/Build/Release/RogueTechPerfFix/RogueTechPerfFixes.dll differ diff --git a/CustomComponentPerfFix/DataManager/AsyncJsonLoadRequest.cs b/CustomComponentPerfFix/DataManager/AsyncJsonLoadRequest.cs new file mode 100644 index 0000000..238744a --- /dev/null +++ b/CustomComponentPerfFix/DataManager/AsyncJsonLoadRequest.cs @@ -0,0 +1,53 @@ +using BattleTech; +using HBS.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RogueTechPerfFixes.DataManager +{ + public static class AsyncJsonLoadRequest + { + private static readonly ILog logger = Logger.GetLogger("DataLoader", LogLevel.Log); + + public static void LogLoadRequest(BattleTechResourceType resourceType, string identifier, bool allowRequestStacking) + { + logger.Log($"LOAD REQUEST for type: {resourceType} id: {identifier} with allowStacking: {allowRequestStacking}"); + StackTrace st = new StackTrace(); + logger.Log($" ST: {st}"); + } + + public static async Task LoadResource(string path, Action handler) + { + try + { + using (FileStream arg = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + StreamReader sr = new StreamReader(arg); + //logger.Log($"READ file at path: {path}"); + + StackTrace st = new StackTrace(); + //logger.Log($" ST: {st}"); + + string content = await sr.ReadToEndAsync(); + + // TODO: Add DataLoader.Entry references here, so file update monitoring can happen -or- replicate with our own + //logger.Log($"Handling file at path: {path} with content: {content}"); + //logger.Log($"HANDLE file at path: {path}"); + handler(content); + } + } + catch (Exception exception) + { + string message = $"LoadResource() - Caught exception while loading [{path}]"; + logger.LogError(message); + logger.LogException(exception); + handler(null); + } + } + } +} diff --git a/CustomComponentPerfFix/RogueTechPerfFixes.csproj b/CustomComponentPerfFix/RogueTechPerfFixes.csproj index c0bf066..aad2162 100644 --- a/CustomComponentPerfFix/RogueTechPerfFixes.csproj +++ b/CustomComponentPerfFix/RogueTechPerfFixes.csproj @@ -41,27 +41,27 @@ - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\0Harmony.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\0Harmony.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\Assembly-CSharp.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\Assembly-CSharp.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\CustomActivatableEquipment\CustomActivatableEquipment.dll + E:\steam\SteamApps\common\BATTLETECH\Mods\CustomActivatableEquipment\CustomActivatableEquipment.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\CustomAmmoCategories\CustomAmmoCategories.dll + E:\steam\SteamApps\common\BATTLETECH\Mods\CustomAmmoCategories\CustomAmmoCategories.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\CustomComponents\CustomComponents.dll + E:\steam\SteamApps\common\BATTLETECH\Mods\CustomComponents\CustomComponents.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\CustomUnits\CustomUnits.dll + E:\steam\SteamApps\common\BATTLETECH\Mods\CustomUnits\CustomUnits.dll False @@ -69,17 +69,17 @@ False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\DOTweenPro.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\DOTweenPro.dll False False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\LowVisibility\LowVisibility.dll + E:\steam\SteamApps\common\BATTLETECH\Mods_BTA_Shared_Old\LowVisibility\LowVisibility.dll False False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\Newtonsoft.Json.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\Newtonsoft.Json.dll False @@ -91,19 +91,20 @@ - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.CoreModule.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.CoreModule.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.PhysicsModule.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.PhysicsModule.dll False + diff --git a/Injection/Injection.csproj b/Injection/Injection.csproj index ef21f3f..d2f76aa 100644 --- a/Injection/Injection.csproj +++ b/Injection/Injection.csproj @@ -41,7 +41,7 @@ - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\Assembly-CSharp.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\Assembly-CSharp.dll False @@ -49,17 +49,17 @@ False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\DOTweenPro.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\DOTweenPro.dll False False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\ModTek\Mono.Cecil.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\MonoMod\Mono.Cecil.dll True False - G:\SteamLibrary\steamapps\common\BATTLETECH\Mods\ModTek\Mono.Cecil.Rocks.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\MonoMod\Mono.Cecil.Rocks.dll True @@ -71,11 +71,11 @@ - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.dll False - G:\SteamLibrary\steamapps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.CoreModule.dll + E:\steam\SteamApps\common\BATTLETECH\BattleTech_Data\Managed\UnityEngine.CoreModule.dll False @@ -83,6 +83,8 @@ + + diff --git a/Injection/Injection/CecilManager.cs b/Injection/Injection/CecilManager.cs index 34faff9..7e10240 100644 --- a/Injection/Injection/CecilManager.cs +++ b/Injection/Injection/CecilManager.cs @@ -112,7 +112,6 @@ static CecilManager() nameof(RTPFVersion) + Mod.Version.ToString().Replace('.', '_') , FieldAttributes.Private , _assembly.MainModule.ImportReference(typeof(string))); - TypeDefinition targetType = null; foreach (TypeDefinition type in _assembly.MainModule.Types) { @@ -157,11 +156,14 @@ public static void Init() try { //Injectors.Add(new I_DesiredAuraReceptionState()); - Injectors.Add(new I_CombatAuraReticle()); Injectors.Add(new I_BTLight()); Injectors.Add(new I_BTLightController()); + // DataManager fixes + Injectors.Add(new I_StringDataLoadRequest()); + //Injectors.Add(new I_DataManager()); + //Injectors.Add(new I_DOTweenAnimation()); //Injectors.Add(new I_ElementManager()); //Injectors.Add(new I_SortMoveCandidatesByInfMapNode()); diff --git a/Injection/Injection/I_DataManager.cs b/Injection/Injection/I_DataManager.cs new file mode 100644 index 0000000..246c007 --- /dev/null +++ b/Injection/Injection/I_DataManager.cs @@ -0,0 +1,99 @@ +using BattleTech; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using RogueTechPerfFixes; +using RogueTechPerfFixes.DataManager; +using RogueTechPerfFixes.Injection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Injection.Injection +{ + // Injector that logs the DataManager load requests by type + class I_DataManager : IInjector + { + private const string _targetType = "BattleTech.Data.DataManager"; + + #region Implementation of IInjector + + public void Inject(Dictionary typeTable, ModuleDefinition module) + { + if (!Mod.Settings.Patch.Vanilla) + return; + + if (typeTable.TryGetValue(_targetType, out TypeDefinition type)) + { + CecilManager.WriteError($"Injecting IL for targetType: {_targetType}"); + InjectIL(type, module); + } + else + { + CecilManager.WriteError($"Can't find target type: {_targetType}"); + } + } + + #endregion + private static void InjectIL(TypeDefinition type, ModuleDefinition module) + { + // internal DataManager.FileLoadRequest CreateFileRequest(BattleTechResourceType resourceType, string identifier, PrewarmRequest prewarm, bool allowRequestStacking) + const string targetMethod = "CreateFileRequest"; + + // From class -> JsonLoadRequest -> StringDataLoadRequest + MethodDefinition method = + type.GetMethods().FirstOrDefault(m => m.Name == targetMethod); + + if (method == null) + { + CecilManager.WriteError($"Can't find method: {targetMethod}\n"); + return; + } + + ILProcessor ilProcessor = method.Body.GetILProcessor(); + Instruction methodStart = method.Body.Instructions[0]; + + List newInstructions = CreateInstructions(ilProcessor, methodStart, module); + newInstructions.Reverse(); + + foreach (Instruction instruction in newInstructions) + { + ilProcessor.InsertBefore(method.Body.Instructions[0], instruction); + } + } + + private static List CreateInstructions(ILProcessor ilProcessor, Instruction branchTarget, ModuleDefinition module) + { + // Create an importable reference to our logger + TypeReference ajlr_TR = module.ImportReference(typeof(AsyncJsonLoadRequest)); + TypeReference void_TR = module.ImportReference(typeof(void)); + + MethodReference ajlr_lr_MR = new MethodReference("LogLoadRequest", void_TR, ajlr_TR); + + TypeReference battleTechResourceType_TR = module.ImportReference(typeof(BattleTechResourceType)); + ajlr_lr_MR.Parameters.Add(new ParameterDefinition(battleTechResourceType_TR)); + + TypeReference string_TR = module.ImportReference(typeof(string)); + ajlr_lr_MR.Parameters.Add(new ParameterDefinition(string_TR)); + + TypeReference bool_TR = module.ImportReference(typeof(bool)); + ajlr_lr_MR.Parameters.Add(new ParameterDefinition(bool_TR)); + + MethodReference ajlr_lr_Imported_MR = module.ImportReference(ajlr_lr_MR); + + List instructions = new List() + { + // Add all params to stack + ilProcessor.Create(OpCodes.Ldarg, 1), + ilProcessor.Create(OpCodes.Ldarg, 2), + ilProcessor.Create(OpCodes.Ldarg, 4), + ilProcessor.Create(OpCodes.Call, ajlr_lr_Imported_MR) + }; + + return instructions; + } + + } +} diff --git a/Injection/Injection/I_StringDataLoadRequest.cs b/Injection/Injection/I_StringDataLoadRequest.cs new file mode 100644 index 0000000..2e268b4 --- /dev/null +++ b/Injection/Injection/I_StringDataLoadRequest.cs @@ -0,0 +1,121 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using RogueTechPerfFixes; +using RogueTechPerfFixes.DataManager; +using RogueTechPerfFixes.Injection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Injection.Injection +{ + // Changes DataManager.LoadResource(string path, Action handler) to support async loads of files. + // This significantly improves the load time as IO is waits together instead of blocking. + // We target AmmunitionDefLoadRequest as it's the first name that inherits from StringDataLoadRequest + class I_StringDataLoadRequest : IInjector + { + private const string _baseType = "BattleTech.Data.DataManager"; + private const string _targetType = "BattleTech.Data.DataManager/AmmunitionDefLoadRequest"; + + #region Implementation of IInjector + + public void Inject(Dictionary typeTable, ModuleDefinition module) + { + if (!Mod.Settings.Patch.Vanilla) + return; + + if (typeTable.TryGetValue(_baseType, out TypeDefinition type)) + { + CecilManager.WriteLog($"Found baseType: {_baseType}"); + + foreach (TypeDefinition nestedType in type.NestedTypes) + { + if (_targetType.Equals(nestedType.FullName, StringComparison.InvariantCultureIgnoreCase)) + { + CecilManager.WriteLog($"Found target nestedType: {nestedType.FullName}"); + InjectIL(nestedType, module); + } + } + } + else + { + CecilManager.WriteError($"Can't find target type: {_targetType}"); + } + } + + #endregion + private static void InjectIL(TypeDefinition type, ModuleDefinition module) + { + const string targetMethod = "Load"; + + // From class -> JsonLoadRequest -> StringDataLoadRequest + TypeDefinition baseType = type.BaseType.Resolve().BaseType.Resolve(); + MethodDefinition method = + baseType.GetMethods().FirstOrDefault(m => m.Name == targetMethod); + + if (method == null) + { + CecilManager.WriteError($"Can't find method: {targetMethod}\n"); + return; + } + + ILProcessor ilProcessor = method.Body.GetILProcessor(); + + // Add a enew reference to an importable method call for AsyncJsonLoadRequest.Load() + TypeReference ajlr_TR = module.ImportReference(typeof(AsyncJsonLoadRequest)); + TypeReference taskTR = module.ImportReference(typeof(Task)); + + MethodReference ajlr_lr_MR = new MethodReference("LoadResource", taskTR, ajlr_TR); + TypeReference stringTR = module.ImportReference(typeof(string)); + ajlr_lr_MR.Parameters.Add(new ParameterDefinition(stringTR)); + + TypeReference actionStringTR = module.ImportReference(typeof(Action)); + ajlr_lr_MR.Parameters.Add(new ParameterDefinition(actionStringTR)); + ajlr_lr_MR.ReturnType = taskTR; + + MethodReference ajlr_lr_Imported_MR = module.ImportReference(ajlr_lr_MR); + + // Walk the instructions to find the target. Don't mutate as we go, so we can use insertAfter later. + int targetIdx = -1; + for (int i = 0; i < method.Body.Instructions.Count - 1; i++) + { + Instruction instruction = method.Body.Instructions[i]; + if (instruction.OpCode == OpCodes.Callvirt && + instruction.Operand is MethodDefinition methodDef) + { + //CecilManager.WriteLog($"Found methodDef: {methodDef.FullName}"); + + if (methodDef.FullName.StartsWith("System.Void HBS.Data.DataLoader::LoadResource")) + { + CecilManager.WriteLog($"Found injection point: {methodDef.FullName}\n"); + targetIdx = i; + } + } + } + if (targetIdx != -1) + { + // Replace callvirt for dataManager.dataLoader.LoadResource with call to AsyncJsonLoadRequest + method.Body.Instructions[targetIdx] = ilProcessor.Create(OpCodes.Call, ajlr_lr_Imported_MR); + + // Elminate references to dataLoader (no longer used) + method.Body.Instructions[targetIdx - 9].OpCode = OpCodes.Nop; + method.Body.Instructions[targetIdx - 9].Operand = null; + + method.Body.Instructions[targetIdx - 8].OpCode = OpCodes.Nop; + method.Body.Instructions[targetIdx - 8].Operand = null; + + method.Body.Instructions[targetIdx - 7].OpCode = OpCodes.Nop; + method.Body.Instructions[targetIdx - 7].Operand = null; + + // Add a pop to remove the async Task (to eliminate dnSpy decompile err) + Instruction popInst = ilProcessor.Create(OpCodes.Pop); + ilProcessor.InsertAfter(method.Body.Instructions[targetIdx], popInst); + } + + } + + } +} diff --git a/QuickStart/IInjector.cs b/QuickStart/IInjector.cs new file mode 100644 index 0000000..699bef5 --- /dev/null +++ b/QuickStart/IInjector.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Mono.Cecil; + +namespace RogueTechPerfFixes.Injection +{ + public interface IInjector + { + void Inject(Dictionary typeTable, ModuleDefinition module); + } +}