mirror of
https://github.com/BepInEx/BepInEx.git
synced 2026-10-19 15:51:45 +00:00
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:
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();
|
||||
|
||||
Reference in new issue
Block a user