Preloader.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Text;
  8. using BepInEx.Logging;
  9. using Mono.Cecil;
  10. using Mono.Cecil.Cil;
  11. using UnityInjector.ConsoleUtil;
  12. using MethodAttributes = Mono.Cecil.MethodAttributes;
  13. namespace BepInEx.Bootstrap
  14. {
  15. /// <summary>
  16. /// The main entrypoint of BepInEx, and initializes all patchers and the chainloader.
  17. /// </summary>
  18. internal static class Preloader
  19. {
  20. /// <summary>
  21. /// The log writer that is specific to the preloader.
  22. /// </summary>
  23. public static PreloaderLogWriter PreloaderLog { get; private set; }
  24. public static void Run()
  25. {
  26. try
  27. {
  28. AllocateConsole();
  29. UnityPatches.Apply();
  30. PreloaderLog =
  31. new PreloaderLogWriter(Utility.SafeParseBool(Config.GetEntry("preloader-logconsole", "false", "BepInEx")));
  32. PreloaderLog.Enabled = true;
  33. string consoleTile =
  34. $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
  35. ConsoleWindow.Title = consoleTile;
  36. Logger.SetLogger(PreloaderLog);
  37. PreloaderLog.WriteLine(consoleTile);
  38. #if DEBUG
  39. object[] attributes = typeof(DebugInfoAttribute).Assembly.GetCustomAttributes(typeof(DebugInfoAttribute), false);
  40. if (attributes.Length > 0)
  41. {
  42. var attribute = (DebugInfoAttribute)attributes[0];
  43. PreloaderLog.WriteLine(attribute.Info);
  44. }
  45. #endif
  46. Logger.Log(LogLevel.Message, "Preloader started");
  47. string entrypointAssembly = Config.GetEntry("entrypoint-assembly", "UnityEngine.dll", "Preloader");
  48. AssemblyPatcher.AddPatcher(new PatcherPlugin { TargetDLLs = new []{ entrypointAssembly }, Patcher = PatchEntrypoint});
  49. AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods);
  50. AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
  51. AssemblyPatcher.DisposePatchers();
  52. }
  53. catch (Exception ex)
  54. {
  55. Logger.Log(LogLevel.Fatal, "Could not run preloader!");
  56. Logger.Log(LogLevel.Fatal, ex);
  57. PreloaderLog.Enabled = false;
  58. try
  59. {
  60. if (!ConsoleWindow.IsAttatched)
  61. {
  62. //if we've already attached the console, then the log will already be written to the console
  63. AllocateConsole();
  64. Console.Write(PreloaderLog);
  65. }
  66. }
  67. finally
  68. {
  69. File.WriteAllText(Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
  70. PreloaderLog.ToString());
  71. PreloaderLog.Dispose();
  72. }
  73. }
  74. }
  75. /// <summary>
  76. /// Scans the assembly for classes that use the patcher contract, and returns a list of valid patchers.
  77. /// </summary>
  78. /// <param name="assembly">The assembly to scan.</param>
  79. /// <returns>A list of assembly patchers that were found in the assembly.</returns>
  80. public static List<PatcherPlugin> GetPatcherMethods(Assembly assembly)
  81. {
  82. var patcherMethods = new List<PatcherPlugin>();
  83. var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase;
  84. foreach (var type in assembly.GetExportedTypes())
  85. try
  86. {
  87. if (type.IsInterface)
  88. continue;
  89. var targetsProperty = type.GetProperty("TargetDLLs",
  90. flags,
  91. null,
  92. typeof(IEnumerable<string>),
  93. Type.EmptyTypes,
  94. null);
  95. //first try get the ref patcher method
  96. var patcher = type.GetMethod("Patch",
  97. flags,
  98. null,
  99. CallingConventions.Any,
  100. new[] {typeof(AssemblyDefinition).MakeByRefType()},
  101. null);
  102. if (patcher == null) //otherwise try getting the non-ref patcher method
  103. patcher = type.GetMethod("Patch",
  104. flags,
  105. null,
  106. CallingConventions.Any,
  107. new[] {typeof(AssemblyDefinition)},
  108. null);
  109. if (targetsProperty == null || !targetsProperty.CanRead || patcher == null)
  110. continue;
  111. var assemblyPatcher = new PatcherPlugin();
  112. assemblyPatcher.Name = $"{assembly.GetName().Name}{type.FullName}";
  113. assemblyPatcher.Patcher = (ref AssemblyDefinition ass) =>
  114. {
  115. //we do the array fuckery here to get the ref result out
  116. object[] args = {ass};
  117. patcher.Invoke(null, args);
  118. ass = (AssemblyDefinition) args[0];
  119. };
  120. assemblyPatcher.TargetDLLs = (IEnumerable<string>) targetsProperty.GetValue(null, null);
  121. var initMethod = type.GetMethod("Initialize",
  122. flags,
  123. null,
  124. CallingConventions.Any,
  125. Type.EmptyTypes,
  126. null);
  127. if (initMethod != null)
  128. assemblyPatcher.Initializer = () => initMethod.Invoke(null, null);
  129. var finalizeMethod = type.GetMethod("Finish",
  130. flags,
  131. null,
  132. CallingConventions.Any,
  133. Type.EmptyTypes,
  134. null);
  135. if (finalizeMethod != null)
  136. assemblyPatcher.Finalizer = () => finalizeMethod.Invoke(null, null);
  137. }
  138. catch (Exception ex)
  139. {
  140. Logger.Log(LogLevel.Warning, $"Could not load patcher methods from {assembly.GetName().Name}");
  141. Logger.Log(LogLevel.Warning, $"{ex}");
  142. }
  143. Logger.Log(LogLevel.Info,
  144. $"Loaded {patcherMethods.Count} patcher methods from {assembly.GetName().Name}");
  145. return patcherMethods;
  146. }
  147. /// <summary>
  148. /// Inserts BepInEx's own chainloader entrypoint into UnityEngine.
  149. /// </summary>
  150. /// <param name="assembly">The assembly that will be attempted to be patched.</param>
  151. public static void PatchEntrypoint(ref AssemblyDefinition assembly)
  152. {
  153. if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx")))
  154. {
  155. throw new Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!");
  156. }
  157. string entrypointType = Config.GetEntry("entrypoint-type", "Application", "Preloader");
  158. string entrypointMethod = Config.GetEntry("entrypoint-method", ".cctor", "Preloader");
  159. bool isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor";
  160. var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType);
  161. if (entryType == null)
  162. {
  163. throw new Exception("The entrypoint type is invalid! Please check your config.ini");
  164. }
  165. using (var injected = AssemblyDefinition.ReadAssembly(Paths.BepInExAssemblyPath))
  166. {
  167. var originalInitMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods
  168. .First(x => x.Name == "Initialize");
  169. var originalStartMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods
  170. .First(x => x.Name == "Start");
  171. var initMethod = assembly.MainModule.ImportReference(originalInitMethod);
  172. var startMethod = assembly.MainModule.ImportReference(originalStartMethod);
  173. List<MethodDefinition> methods = new List<MethodDefinition>();
  174. if (isCctor)
  175. {
  176. MethodDefinition cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
  177. if (cctor == null)
  178. {
  179. cctor = new MethodDefinition(".cctor",
  180. MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig
  181. | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
  182. assembly.MainModule.ImportReference(typeof(void)));
  183. entryType.Methods.Add(cctor);
  184. ILProcessor il = cctor.Body.GetILProcessor();
  185. il.Append(il.Create(OpCodes.Ret));
  186. }
  187. methods.Add(cctor);
  188. }
  189. else
  190. {
  191. methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod));
  192. }
  193. if (!methods.Any())
  194. {
  195. throw new Exception("The entrypoint method is invalid! Please check your config.ini");
  196. }
  197. foreach (var method in methods)
  198. {
  199. var il = method.Body.GetILProcessor();
  200. Instruction ins = il.Body.Instructions.First();
  201. il.InsertBefore(ins, il.Create(OpCodes.Ldstr, Paths.ExecutablePath)); //containerExePath
  202. il.InsertBefore(ins, il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
  203. il.InsertBefore(ins, il.Create(OpCodes.Call, initMethod)); //Chainloader.Initialize(string containerExePath, bool startConsole = true)
  204. il.InsertBefore(ins, il.Create(OpCodes.Call, startMethod));
  205. }
  206. }
  207. }
  208. /// <summary>
  209. /// Allocates a console window for use by BepInEx safely.
  210. /// </summary>
  211. public static void AllocateConsole()
  212. {
  213. bool console = Utility.SafeParseBool(Config.GetEntry("console", "false", "BepInEx"));
  214. bool shiftjis = Utility.SafeParseBool(Config.GetEntry("console-shiftjis", "false", "BepInEx"));
  215. if (!console)
  216. return;
  217. try
  218. {
  219. ConsoleWindow.Attach();
  220. var encoding = (uint) Encoding.UTF8.CodePage;
  221. if (shiftjis)
  222. encoding = 932;
  223. ConsoleEncoding.ConsoleCodePage = encoding;
  224. Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
  225. }
  226. catch (Exception ex)
  227. {
  228. Logger.Log(LogLevel.Error, "Failed to allocate console!");
  229. Logger.Log(LogLevel.Error, ex);
  230. }
  231. }
  232. }
  233. }