UnityPreloader.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Diagnostics;
  5. using System.IO;
  6. using System.Linq;
  7. using BepInEx.Bootstrap;
  8. using BepInEx.Configuration;
  9. using BepInEx.Core.Logging;
  10. using BepInEx.Logging;
  11. using BepInEx.Preloader.Core;
  12. using BepInEx.Preloader.Core.Logging;
  13. using BepInEx.Preloader.RuntimeFixes;
  14. using Mono.Cecil;
  15. using Mono.Cecil.Cil;
  16. using MonoMod.Utils;
  17. using MethodAttributes = Mono.Cecil.MethodAttributes;
  18. namespace BepInEx.Preloader.Unity
  19. {
  20. /// <summary>
  21. /// The main entrypoint of BepInEx, and initializes all patchers and the chainloader.
  22. /// </summary>
  23. internal static class UnityPreloader
  24. {
  25. /// <summary>
  26. /// The log writer that is specific to the preloader.
  27. /// </summary>
  28. private static PreloaderConsoleListener PreloaderLog { get; set; }
  29. private static ManualLogSource Log => PreloaderLogger.Log;
  30. public static string ManagedPath { get; private set; } = Utility.CombinePaths(Paths.GameRootPath, $"{Paths.ProcessName}_Data", "Managed");
  31. public static bool IsPostUnity2017 { get; } = File.Exists(Path.Combine(ManagedPath, "UnityEngine.CoreModule.dll"));
  32. public static void Run(string managedDirectory)
  33. {
  34. try
  35. {
  36. InitializeHarmony();
  37. ConsoleManager.Initialize(false);
  38. AllocateConsole();
  39. if (managedDirectory != null)
  40. ManagedPath = managedDirectory;
  41. Utility.TryDo(() =>
  42. {
  43. if (ConfigApplyRuntimePatches.Value)
  44. UnityPatches.Apply();
  45. }, out var runtimePatchException);
  46. Logger.Sources.Add(TraceLogSource.CreateSource());
  47. Logger.Sources.Add(new HarmonyLogSource());
  48. Logger.Listeners.Add(new ConsoleLogListener());
  49. PreloaderLog = new PreloaderConsoleListener();
  50. Logger.Listeners.Add(PreloaderLog);
  51. ChainloaderLogHelper.PrintLogInfo(Log);
  52. Log.LogInfo($"Running under Unity v{GetUnityVersion()}");
  53. Log.LogInfo($"CLR runtime version: {Environment.Version}");
  54. Log.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}");
  55. Log.LogDebug($"Game executable path: {Paths.ExecutablePath}");
  56. Log.LogDebug($"Unity Managed directory: {ManagedPath}");
  57. Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}");
  58. if (runtimePatchException != null)
  59. Log.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}");
  60. Log.LogMessage("Preloader started");
  61. TypeLoader.SearchDirectories.Add(ManagedPath);
  62. using (var assemblyPatcher = new AssemblyPatcher())
  63. {
  64. assemblyPatcher.PatcherPlugins.Add(new PatcherPlugin
  65. {
  66. TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value },
  67. Patcher = PatchEntrypoint,
  68. TypeName = "BepInEx.Chainloader"
  69. });
  70. assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath);
  71. Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherPlugins.Count == 1 ? "" : "s")} loaded");
  72. assemblyPatcher.LoadAssemblyDirectory(ManagedPath);
  73. Log.LogInfo($"{assemblyPatcher.PatcherPlugins.Count} assemblies discovered");
  74. assemblyPatcher.PatchAndLoad();
  75. }
  76. Log.LogMessage("Preloader finished");
  77. Logger.Listeners.Remove(PreloaderLog);
  78. PreloaderLog.Dispose();
  79. Logger.Listeners.Add(new StdOutLogListener());
  80. }
  81. catch (Exception ex)
  82. {
  83. try
  84. {
  85. Log.LogFatal("Could not run preloader!");
  86. Log.LogFatal(ex);
  87. if (!ConsoleManager.ConsoleActive)
  88. {
  89. //if we've already attached the console, then the log will already be written to the console
  90. AllocateConsole();
  91. Console.Write(PreloaderLog);
  92. }
  93. }
  94. catch { }
  95. string log = string.Empty;
  96. try
  97. {
  98. // We could use platform-dependent newlines, however the developers use Windows so this will be easier to read :)
  99. log = string.Join("\r\n", PreloaderConsoleListener.LogEvents.Select(x => x.ToString()).ToArray());
  100. log += "\r\n";
  101. PreloaderLog?.Dispose();
  102. PreloaderLog = null;
  103. }
  104. catch { }
  105. File.WriteAllText(
  106. Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
  107. log + ex);
  108. }
  109. }
  110. /// <summary>
  111. /// Inserts BepInEx's own chainloader entrypoint into UnityEngine.
  112. /// </summary>
  113. /// <param name="assembly">The assembly that will be attempted to be patched.</param>
  114. public static void PatchEntrypoint(ref AssemblyDefinition assembly)
  115. {
  116. if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx")))
  117. throw new Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!");
  118. string entrypointType = ConfigEntrypointType.Value;
  119. string entrypointMethod = ConfigEntrypointMethod.Value;
  120. bool isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor";
  121. var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType);
  122. if (entryType == null)
  123. throw new Exception("The entrypoint type is invalid! Please check your config/BepInEx.cfg file");
  124. string chainloaderAssemblyPath = Path.Combine(Paths.BepInExAssemblyDirectory, "BepInEx.Unity.dll");
  125. var readerParameters = new ReaderParameters
  126. {
  127. AssemblyResolver = TypeLoader.CecilResolver
  128. };
  129. using (var chainloaderAssemblyDefinition = AssemblyDefinition.ReadAssembly(chainloaderAssemblyPath, readerParameters))
  130. {
  131. var chainloaderType = chainloaderAssemblyDefinition.MainModule.Types.First(x => x.Name == "UnityChainloader");
  132. var originalStartMethod = chainloaderType.EnumerateAllMethods().First(x => x.Name == "StaticStart");
  133. var startMethod = assembly.MainModule.ImportReference(originalStartMethod);
  134. var methods = new List<MethodDefinition>();
  135. if (isCctor)
  136. {
  137. var cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
  138. if (cctor == null)
  139. {
  140. cctor = new MethodDefinition(".cctor",
  141. MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig
  142. | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
  143. assembly.MainModule.ImportReference(typeof(void)));
  144. entryType.Methods.Add(cctor);
  145. var il = cctor.Body.GetILProcessor();
  146. il.Append(il.Create(OpCodes.Ret));
  147. }
  148. methods.Add(cctor);
  149. }
  150. else
  151. {
  152. methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod));
  153. }
  154. if (!methods.Any())
  155. throw new Exception("The entrypoint method is invalid! Please check your config.ini");
  156. foreach (var method in methods)
  157. {
  158. var il = method.Body.GetILProcessor();
  159. var ins = il.Body.Instructions.First();
  160. il.InsertBefore(ins,
  161. il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint
  162. il.InsertBefore(ins,
  163. il.Create(OpCodes.Call, startMethod)); // UnityChainloader.StaticStart(string gameExePath)
  164. }
  165. }
  166. }
  167. /// <summary>
  168. /// Allocates a console window for use by BepInEx safely.
  169. /// </summary>
  170. public static void AllocateConsole()
  171. {
  172. if (!ConsoleManager.ConfigConsoleEnabled.Value)
  173. return;
  174. try
  175. {
  176. ConsoleManager.CreateConsole();
  177. }
  178. catch (Exception ex)
  179. {
  180. Log.LogError("Failed to allocate console!");
  181. Log.LogError(ex);
  182. }
  183. }
  184. public static string GetUnityVersion()
  185. {
  186. if (PlatformHelper.Is(Platform.Windows))
  187. return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion;
  188. return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)";
  189. }
  190. private static void InitializeHarmony()
  191. {
  192. switch (ConfigHarmonyBackend.Value)
  193. {
  194. case MonoModBackend.auto:
  195. break;
  196. case MonoModBackend.dynamicmethod:
  197. case MonoModBackend.methodbuilder:
  198. case MonoModBackend.cecil:
  199. Environment.SetEnvironmentVariable("MONOMOD_DMD_TYPE", ConfigHarmonyBackend.Value.ToString());
  200. break;
  201. default:
  202. throw new ArgumentOutOfRangeException(nameof(ConfigHarmonyBackend), ConfigHarmonyBackend.Value, "Unknown backend");
  203. }
  204. }
  205. private enum MonoModBackend
  206. {
  207. // Enum names are important!
  208. [Description("Auto")] auto = 0,
  209. [Description("DynamicMethod")] dynamicmethod,
  210. [Description("MethodBuilder")] methodbuilder,
  211. [Description("Cecil")] cecil
  212. }
  213. #region Config
  214. private static readonly ConfigEntry<string> ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind(
  215. "Preloader.Entrypoint", "Assembly",
  216. IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll",
  217. "The local filename of the assembly to target.");
  218. private static readonly ConfigEntry<string> ConfigEntrypointType = ConfigFile.CoreConfig.Bind(
  219. "Preloader.Entrypoint", "Type",
  220. "Application",
  221. "The name of the type in the entrypoint assembly to search for the entrypoint method.");
  222. private static readonly ConfigEntry<string> ConfigEntrypointMethod = ConfigFile.CoreConfig.Bind(
  223. "Preloader.Entrypoint", "Method",
  224. ".cctor",
  225. "The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from.");
  226. internal static readonly ConfigEntry<bool> ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Bind(
  227. "Preloader", "ApplyRuntimePatches",
  228. true,
  229. "Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing.");
  230. private static readonly ConfigEntry<MonoModBackend> ConfigHarmonyBackend = ConfigFile.CoreConfig.Bind(
  231. "Preloader",
  232. "HarmonyBackend",
  233. MonoModBackend.auto,
  234. "Specifies which MonoMod backend to use for Harmony patches. Auto uses the best available backend.\nThis setting should only be used for development purposes (e.g. debugging in dnSpy). Other code might override this setting.");
  235. #endregion
  236. }
  237. }