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
This commit is contained in:
aldelaro5 authored and GitHub committed 2024-09-24 20:34:13 +02:00
1 parent 50dd462e60
commit 094c43a93b
3 files changed
+77 -139

No files matched your search

@@ -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<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind(
"Preloader.Entrypoint", "Assembly",
DefaultEntrypointAssembly,
"The local filename of the assembly to target.");
private static readonly ConfigEntry<string> 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<string> 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<MethodDefinition>();
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?");
}
}
@@ -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);
}
}
/// <summary>
/// Allocates a console window for use by BepInEx safely.
/// </summary>
@@ -33,7 +33,7 @@ public class UnityChainloader : BaseChainloader<BaseUnityPlugin>
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<BaseUnityPlugin>
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();