using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Text; using BepInEx.Logging; using Mono.Cecil; using Mono.Cecil.Cil; using UnityInjector.ConsoleUtil; using MethodAttributes = Mono.Cecil.MethodAttributes; namespace BepInEx.Bootstrap { /// /// The main entrypoint of BepInEx, and initializes all patchers and the chainloader. /// internal static class Preloader { /// /// The list of finalizers that were loaded from the patcher contract. /// public static List Finalizers { get; } = new List(); /// /// The list of initializers that were loaded from the patcher contract. /// public static List Initializers { get; } = new List(); /// /// The dictionary of currently loaded patchers. The key is the patcher delegate that will be used to patch, and the /// value is a list of filenames of assemblies that the patcher is targeting. /// public static Dictionary> PatcherDictionary { get; } = new Dictionary>(); /// /// The log writer that is specific to the preloader. /// public static PreloaderLogWriter PreloaderLog { get; private set; } public static void Run() { try { AllocateConsole(); PreloaderLog = new PreloaderLogWriter(Utility.SafeParseBool(Config.GetEntry("preloader-logconsole", "false", "BepInEx"))); PreloaderLog.Enabled = true; string consoleTile = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {Process.GetCurrentProcess().ProcessName}"; ConsoleWindow.Title = consoleTile; Logger.SetLogger(PreloaderLog); PreloaderLog.WriteLine(consoleTile); #if DEBUG object[] attributes = typeof(DebugInfoAttribute).Assembly.GetCustomAttributes(typeof(DebugInfoAttribute), false); if (attributes.Length > 0) { var attribute = (DebugInfoAttribute)attributes[0]; PreloaderLog.WriteLine(attribute.Info); } #endif Logger.Log(LogLevel.Message, "Preloader started"); string entrypointAssembly = Config.GetEntry("entrypoint-assembly", "UnityEngine.dll", "Preloader"); AddPatcher(new[] {entrypointAssembly}, PatchEntrypoint); if (Directory.Exists(Paths.PatcherPluginPath)) { var sortedPatchers = new SortedDictionary>>(); foreach (string assemblyPath in Directory.GetFiles(Paths.PatcherPluginPath, "*.dll")) try { var assembly = Assembly.LoadFrom(assemblyPath); foreach (KeyValuePair> kv in GetPatcherMethods(assembly)) sortedPatchers.Add(assembly.GetName().Name, kv); } catch (BadImageFormatException) { } //unmanaged DLL catch (ReflectionTypeLoadException) { } //invalid references foreach (KeyValuePair>> kv in sortedPatchers) AddPatcher(kv.Value.Value, kv.Value.Key); } AssemblyPatcher.PatchAll(Paths.ManagedPath, PatcherDictionary, Initializers, Finalizers); } catch (Exception ex) { Logger.Log(LogLevel.Fatal, "Could not run preloader!"); Logger.Log(LogLevel.Fatal, ex); PreloaderLog.Enabled = false; try { if (!ConsoleWindow.IsAttatched) { //if we've already attached the console, then the log will already be written to the console AllocateConsole(); Console.Write(PreloaderLog); } } finally { File.WriteAllText(Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"), PreloaderLog.ToString()); PreloaderLog.Dispose(); } } } /// /// Scans the assembly for classes that use the patcher contract, and returns a dictionary of the patch methods. /// /// The assembly to scan. /// A dictionary of delegates which will be used to patch the targeted assemblies. public static Dictionary> GetPatcherMethods(Assembly assembly) { var patcherMethods = new Dictionary>(); foreach (var type in assembly.GetExportedTypes()) try { if (type.IsInterface) continue; var targetsProperty = type.GetProperty("TargetDLLs", BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase, null, typeof(IEnumerable), Type.EmptyTypes, null); //first try get the ref patcher method var patcher = type.GetMethod("Patch", BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase, null, CallingConventions.Any, new[] {typeof(AssemblyDefinition).MakeByRefType()}, null); if (patcher == null) //otherwise try getting the non-ref patcher method patcher = type.GetMethod("Patch", BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase, null, CallingConventions.Any, new[] {typeof(AssemblyDefinition)}, null); if (targetsProperty == null || !targetsProperty.CanRead || patcher == null) continue; AssemblyPatcherDelegate patchDelegate = (ref AssemblyDefinition ass) => { //we do the array fuckery here to get the ref result out object[] args = {ass}; patcher.Invoke(null, args); ass = (AssemblyDefinition) args[0]; }; var targets = (IEnumerable) targetsProperty.GetValue(null, null); patcherMethods[patchDelegate] = targets; var initMethod = type.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase, null, CallingConventions.Any, Type.EmptyTypes, null); if (initMethod != null) Initializers.Add(() => initMethod.Invoke(null, null)); var finalizeMethod = type.GetMethod("Finish", BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase, null, CallingConventions.Any, Type.EmptyTypes, null); if (finalizeMethod != null) Finalizers.Add(() => finalizeMethod.Invoke(null, null)); } catch (Exception ex) { Logger.Log(LogLevel.Warning, $"Could not load patcher methods from {assembly.GetName().Name}"); Logger.Log(LogLevel.Warning, $"{ex}"); } Logger.Log(LogLevel.Info, $"Loaded {patcherMethods.Select(x => x.Key).Distinct().Count()} patcher methods from {assembly.GetName().Name}"); return patcherMethods; } /// /// Inserts BepInEx's own chainloader entrypoint into UnityEngine. /// /// The assembly that will be attempted to be patched. public static void PatchEntrypoint(ref AssemblyDefinition assembly) { 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!"); } string entrypointType = Config.GetEntry("entrypoint-type", "Application", "Preloader"); string entrypointMethod = Config.GetEntry("entrypoint-method", ".cctor", "Preloader"); bool 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.ini"); } using (var injected = AssemblyDefinition.ReadAssembly(Paths.BepInExAssemblyPath)) { var originalInitMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods .First(x => x.Name == "Initialize"); var originalStartMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods .First(x => x.Name == "Start"); var initMethod = assembly.MainModule.ImportReference(originalInitMethod); var startMethod = assembly.MainModule.ImportReference(originalStartMethod); List methods = new List(); if (isCctor) { MethodDefinition 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); ILProcessor 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(); Instruction ins = il.Body.Instructions.First(); il.InsertBefore(ins, il.Create(OpCodes.Ldstr, Paths.ExecutablePath)); //containerExePath il.InsertBefore(ins, il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader) il.InsertBefore(ins, il.Create(OpCodes.Call, initMethod)); //Chainloader.Initialize(string containerExePath, bool startConsole = true) il.InsertBefore(ins, il.Create(OpCodes.Call, startMethod)); } } } /// /// Allocates a console window for use by BepInEx safely. /// public static void AllocateConsole() { bool console = Utility.SafeParseBool(Config.GetEntry("console", "false", "BepInEx")); bool shiftjis = Utility.SafeParseBool(Config.GetEntry("console-shiftjis", "false", "BepInEx")); if (console) try { ConsoleWindow.Attach(); var encoding = (uint) Encoding.UTF8.CodePage; if (shiftjis) encoding = 932; ConsoleEncoding.ConsoleCodePage = encoding; Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding); } catch (Exception ex) { Logger.Log(LogLevel.Error, "Failed to allocate console!"); Logger.Log(LogLevel.Error, ex); } } /// /// Adds the patcher to the patcher dictionary. /// /// The list of DLL filenames to be patched. /// The method that will perform the patching. public static void AddPatcher(IEnumerable dllNames, AssemblyPatcherDelegate patcher) { PatcherDictionary[patcher] = dllNames; } } }