From 094c43a93b5408b0dd34623504bae7e23da6d7ef Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 24 Sep 2024 14:34:13 -0400 Subject: [PATCH] v7: Change the chainloader Unity Mono entrypoint method to be Harmony based (#994) * Change the Unity Mono entrypoint method to be Harmony based * Improve the error handling and logging * Log fatal + return in case of double StaticStart calls --- .../EntrypointPatcher.cs | 136 ------------------ .../UnityPreloader.cs | 71 ++++++++- .../Bootstrap/UnityChainloader.cs | 9 +- 3 files changed, 77 insertions(+), 139 deletions(-) delete mode 100644 Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs deleted file mode 100644 index fb2f834..0000000 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using BepInEx.Bootstrap; -using BepInEx.Configuration; -using BepInEx.Logging; -using BepInEx.Preloader.Core.Patching; -using BepInEx.Unity.Common; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace BepInEx.Unity.Mono.Preloader; - -[PatcherPluginInfo("io.bepinex.entrypointpatcher", "BepInEx Entrypoint", "1.0")] -internal class EntrypointPatcher : BasePatcher -{ - private static readonly ConfigEntry ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Assembly", - DefaultEntrypointAssembly, - "The local filename of the assembly to target."); - - private static readonly ConfigEntry ConfigEntrypointType = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Type", - DefaultEntrypointType, - "The name of the type in the entrypoint assembly to search for the entrypoint method."); - - private static readonly ConfigEntry ConfigEntrypointMethod = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Method", - ".cctor", - "The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from."); - - private static string DefaultEntrypointAssembly => - UnityInfo.Version.LessThan(2017) ? "UnityEngine.dll" : "UnityEngine.CoreModule.dll"; - - private static string DefaultEntrypointType => UnityInfo.Version.LessThan(5) ? "MonoBehaviour" : "Application"; - - private bool HasLoaded { get; set; } - - [TargetAssembly(TargetAssemblyAttribute.AllAssemblies)] - public bool PatchEntrypoint(ref AssemblyDefinition assembly, string filename) - { - if (HasLoaded || filename != ConfigEntrypointAssembly.Value) - return false; - - if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx"))) - throw new - Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!"); - - var entrypointType = ConfigEntrypointType.Value; - var entrypointMethod = ConfigEntrypointMethod.Value; - - Log.Log(LogLevel.Debug, $"Hooking chainloader into {entrypointType}::{entrypointMethod}"); - - var isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor"; - - - var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType); - - if (entryType == null) - throw new Exception("The entrypoint type is invalid! Please check your config/BepInEx.cfg file"); - - var chainloaderAssemblyPath = Path.Combine(Paths.BepInExAssemblyDirectory, "BepInEx.Unity.Mono.dll"); - - var readerParameters = new ReaderParameters - { - AssemblyResolver = TypeLoader.CecilResolver - }; - - using (var chainloaderAssemblyDefinition = - AssemblyDefinition.ReadAssembly(chainloaderAssemblyPath, readerParameters)) - { - var chainloaderType = - chainloaderAssemblyDefinition.MainModule.Types.First(x => x.Name == "UnityChainloader"); - - var originalStartMethod = chainloaderType.EnumerateAllMethods().First(x => x.Name == "StaticStart"); - - var startMethod = assembly.MainModule.ImportReference(originalStartMethod); - - var methods = new List(); - - if (isCctor) - { - var cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); - - if (cctor == null) - { - cctor = new MethodDefinition(".cctor", - MethodAttributes.Static | MethodAttributes.Private - | MethodAttributes.HideBySig | - MethodAttributes.SpecialName - | MethodAttributes.RTSpecialName, - assembly.MainModule.ImportReference(typeof(void))); - - entryType.Methods.Add(cctor); - var il = cctor.Body.GetILProcessor(); - il.Append(il.Create(OpCodes.Ret)); - } - - methods.Add(cctor); - } - else - { - methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod)); - } - - if (!methods.Any()) - throw new Exception("The entrypoint method is invalid! Please check your config.ini"); - - foreach (var method in methods) - { - var il = method.Body.GetILProcessor(); - - var ins = il.Body.Instructions.First(); - - il.InsertBefore(ins, - il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint) - - il.InsertBefore(ins, - il.Create(OpCodes.Call, - startMethod)); // UnityChainloader.StaticStart(string gameExePath) - } - } - - HasLoaded = true; - - return true; - } - - public override void Finalizer() - { - if (!HasLoaded) - Log.Log(LogLevel.Fatal, - $"Failed to patch BepInEx chainloader into assembly '{ConfigEntrypointAssembly.Value}', either due to error or not being able to find it. Is it spelled correctly?"); - } -} diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs index b6b2a42..7c750bf 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Linq; +using System.Reflection; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; @@ -11,6 +12,7 @@ using BepInEx.Preloader.RuntimeFixes; using BepInEx.Unity.Common; using BepInEx.Unity.Mono.Preloader.RuntimeFixes; using BepInEx.Unity.Mono.Preloader.Utils; +using HarmonyLib; namespace BepInEx.Unity.Mono.Preloader; @@ -35,6 +37,8 @@ internal static class UnityPreloader private static ManualLogSource Log => PreloaderLogger.Log; + private static readonly Harmony Harmony = new("BepInEx.Unity.Mono.Preloader"); + public static void Run() { try @@ -72,11 +76,14 @@ internal static class UnityPreloader Log.Log(LogLevel.Message, "Preloader started"); + // Set up the chainloader entrypoint which harmony patches the main Unity assembly as soon as possible and + // unpatch it in our hooking method before calling the chainloader init method + AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad; + TypeLoader.SearchDirectories.UnionWith(Paths.DllSearchPaths); using (var assemblyPatcher = new AssemblyPatcher(MonoAssemblyHelper.LoadFromMemory)) { - assemblyPatcher.AddPatchersFromDirectory(Paths.BepInExAssemblyDirectory); assemblyPatcher.AddPatchersFromProviders(); Log.Log(LogLevel.Info, @@ -130,6 +137,68 @@ internal static class UnityPreloader } } + // First step of the chainloader entrypoint: Harmony patch the main Unity assembly with a suitable hook + private static void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args) + { + const string sceneManagerTypeName = "UnityEngine.SceneManagement.SceneManager"; + const string displayTypeName = "UnityEngine.Display"; + + var assembly = args.LoadedAssembly; + var assemblyName = assembly.GetName().Name; + + if (assemblyName is not ("UnityEngine.CoreModule" or "UnityEngine")) + return; + + try + { + AppDomain.CurrentDomain.AssemblyLoad -= OnAssemblyLoad; + + var sceneManagerType = assembly.GetType(sceneManagerTypeName, false); + if (sceneManagerType != null) + { + var activeSceneChangedMethod = sceneManagerType.GetMethod("Internal_ActiveSceneChanged", + BindingFlags.NonPublic | BindingFlags.Static); + Harmony.Patch(activeSceneChangedMethod, prefix: new HarmonyMethod(typeof(UnityPreloader), nameof(Entrypoint))); + Log.LogInfo($"Hooked into {activeSceneChangedMethod.FullDescription()}"); + return; + } + + var displayType = assembly.GetType(displayTypeName, false); + if (displayType != null) + { + var recreateDisplayListMethod = displayType.GetMethod("RecreateDisplayList", BindingFlags.NonPublic | BindingFlags.Static); + Harmony.Patch(recreateDisplayListMethod, postfix: new HarmonyMethod(typeof(UnityPreloader), nameof(Entrypoint))); + Log.LogInfo($"Hooked into {recreateDisplayListMethod.FullDescription()}"); + return; + } + + Log.LogFatal($"Couldn't find a suitable chainloader entrypoint in the {assemblyName} assembly because " + + $"{sceneManagerTypeName} or {displayTypeName} do not exist in the assembly"); + } + catch (Exception e) + { + Log.LogFatal($"Unexpected error occured when trying to hook into {assemblyName}: {e}"); + } + } + + // Second step of the chainloader entrypoint: undo the Harmony patch and call the chainloader init method + private static void Entrypoint() + { + try + { + Harmony.UnpatchSelf(); + + Assembly.Load("BepInEx.Unity.Mono") + .GetType("BepInEx.Unity.Mono.Bootstrap.UnityChainloader") + .GetMethod("StaticStart", AccessTools.all)! + .Invoke(null, [null]); + } + catch (Exception e) + { + Log.LogFatal(e); + } + } + /// /// Allocates a console window for use by BepInEx safely. /// diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs index 19769e8..a320af3 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs @@ -33,7 +33,7 @@ public class UnityChainloader : BaseChainloader false, "Include unity log messages in log file output."); - private static readonly bool staticStartHasBeenCalled = false; + private static bool staticStartHasBeenCalled = false; private string _consoleTitle; @@ -69,8 +69,13 @@ public class UnityChainloader : BaseChainloader try { if (staticStartHasBeenCalled) - throw new InvalidOperationException("Cannot call StaticStart again"); + { + Logger.Log(LogLevel.Fatal, "StaticStart called more than once"); + return; + } + staticStartHasBeenCalled = true; + Logger.Log(LogLevel.Debug, "Entering chainloader StaticStart"); Instance = new UnityChainloader();