Preloader.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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.Common;
  9. using BepInEx.Logging;
  10. using Mono.Cecil;
  11. using Mono.Cecil.Cil;
  12. using UnityInjector.ConsoleUtil;
  13. using MethodAttributes = Mono.Cecil.MethodAttributes;
  14. namespace BepInEx.Bootstrap
  15. {
  16. public static class Preloader
  17. {
  18. #region Path Properties
  19. public static string ExecutablePath { get; private set; }
  20. public static string CurrentExecutingAssemblyPath => Assembly.GetExecutingAssembly().CodeBase.Replace("file:///", "").Replace('/', '\\');
  21. public static string CurrentExecutingAssemblyDirectoryPath => Path.GetDirectoryName(CurrentExecutingAssemblyPath);
  22. public static string GameName => Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName);
  23. public static string GameRootPath => Path.GetDirectoryName(ExecutablePath);
  24. public static string ManagedPath => Utility.CombinePaths(GameRootPath, $"{GameName}_Data", "Managed");
  25. public static string PluginPath => Utility.CombinePaths(GameRootPath, "BepInEx");
  26. public static string PatcherPluginPath => Utility.CombinePaths(GameRootPath, "BepInEx", "patchers");
  27. #endregion
  28. public static PreloaderLogWriter PreloaderLog { get; private set; }
  29. public static Dictionary<AssemblyPatcherDelegate, IEnumerable<string>> PatcherDictionary { get; } = new Dictionary<AssemblyPatcherDelegate, IEnumerable<string>>();
  30. public static List<Action> Initializers { get; } = new List<Action>();
  31. public static List<Action> Finalizers { get; } = new List<Action>();
  32. public static void AddPatcher(IEnumerable<string> dllNames, AssemblyPatcherDelegate patcher)
  33. {
  34. PatcherDictionary[patcher] = dllNames;
  35. }
  36. private static bool SafeGetConfigBool(string key, string defaultValue)
  37. {
  38. try
  39. {
  40. string result = Config.GetEntry(key, defaultValue);
  41. return bool.Parse(result);
  42. }
  43. catch
  44. {
  45. return false;
  46. }
  47. }
  48. internal static void AllocateConsole()
  49. {
  50. bool console = SafeGetConfigBool("console", "false");
  51. bool shiftjis = SafeGetConfigBool("console-shiftjis", "false");
  52. if (console)
  53. {
  54. try
  55. {
  56. ConsoleWindow.Attach();
  57. uint encoding = (uint) Encoding.UTF8.CodePage;
  58. if (shiftjis)
  59. encoding = 932;
  60. ConsoleEncoding.ConsoleCodePage = encoding;
  61. Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
  62. }
  63. catch (Exception ex)
  64. {
  65. Logger.Log(LogLevel.Error, "Failed to allocate console!");
  66. Logger.Log(LogLevel.Error, ex);
  67. }
  68. }
  69. }
  70. public static void Main(string[] args)
  71. {
  72. try
  73. {
  74. AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
  75. ExecutablePath = args[0];
  76. AllocateConsole();
  77. PreloaderLog = new PreloaderLogWriter(SafeGetConfigBool("preloader-logconsole", "false"));
  78. PreloaderLog.Enabled = true;
  79. string consoleTile = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
  80. ConsoleWindow.Title = consoleTile;
  81. Logger.SetLogger(PreloaderLog);
  82. PreloaderLog.WriteLine(consoleTile);
  83. Logger.Log(LogLevel.Message, "Preloader started");
  84. AddPatcher(new [] { "UnityEngine.dll" }, PatchEntrypoint);
  85. if (Directory.Exists(PatcherPluginPath))
  86. foreach (string assemblyPath in Directory.GetFiles(PatcherPluginPath, "*.dll"))
  87. {
  88. try
  89. {
  90. var assembly = Assembly.LoadFrom(assemblyPath);
  91. foreach (var kv in GetPatcherMethods(assembly))
  92. AddPatcher(kv.Value, kv.Key);
  93. }
  94. catch (BadImageFormatException) { } //unmanaged DLL
  95. catch (ReflectionTypeLoadException) { } //invalid references
  96. }
  97. AssemblyPatcher.PatchAll(ManagedPath, PatcherDictionary);
  98. }
  99. catch (Exception ex)
  100. {
  101. Logger.Log(LogLevel.Fatal, "Could not run preloader!");
  102. Logger.Log(LogLevel.Fatal, ex);
  103. PreloaderLog.Enabled = false;
  104. try
  105. {
  106. UnityInjector.ConsoleUtil.ConsoleWindow.Attach();
  107. Console.Write(PreloaderLog);
  108. }
  109. finally
  110. {
  111. File.WriteAllText(Path.Combine(GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
  112. PreloaderLog.ToString());
  113. PreloaderLog.Dispose();
  114. }
  115. }
  116. }
  117. internal static Dictionary<AssemblyPatcherDelegate, IEnumerable<string>> GetPatcherMethods(Assembly assembly)
  118. {
  119. var patcherMethods = new Dictionary<AssemblyPatcherDelegate, IEnumerable<string>>();
  120. foreach (var type in assembly.GetExportedTypes())
  121. {
  122. try
  123. {
  124. if (type.IsInterface)
  125. continue;
  126. PropertyInfo targetsProperty = type.GetProperty(
  127. "TargetDLLs",
  128. BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
  129. null,
  130. typeof(IEnumerable<string>),
  131. Type.EmptyTypes,
  132. null);
  133. //first try get the ref patcher method
  134. MethodInfo patcher = type.GetMethod(
  135. "Patch",
  136. BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
  137. null,
  138. CallingConventions.Any,
  139. new[] {typeof(AssemblyDefinition).MakeByRefType()},
  140. null);
  141. if (patcher == null) //otherwise try getting the non-ref patcher method
  142. {
  143. patcher = type.GetMethod(
  144. "Patch",
  145. BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
  146. null,
  147. CallingConventions.Any,
  148. new[] {typeof(AssemblyDefinition)},
  149. null);
  150. }
  151. if (targetsProperty == null || !targetsProperty.CanRead || patcher == null)
  152. continue;
  153. AssemblyPatcherDelegate patchDelegate = (ref AssemblyDefinition ass) =>
  154. {
  155. //we do the array fuckery here to get the ref result out
  156. object[] args = { ass };
  157. patcher.Invoke(null, args);
  158. ass = (AssemblyDefinition)args[0];
  159. };
  160. IEnumerable<string> targets = (IEnumerable<string>)targetsProperty.GetValue(null, null);
  161. patcherMethods[patchDelegate] = targets;
  162. MethodInfo initMethod = type.GetMethod(
  163. "Initialize",
  164. BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
  165. null,
  166. CallingConventions.Any,
  167. Type.EmptyTypes,
  168. null);
  169. if (initMethod != null)
  170. {
  171. Initializers.Add(() => initMethod.Invoke(null, null));
  172. }
  173. MethodInfo finalizeMethod = type.GetMethod(
  174. "Finish",
  175. BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase,
  176. null,
  177. CallingConventions.Any,
  178. Type.EmptyTypes,
  179. null);
  180. if (finalizeMethod != null)
  181. {
  182. Finalizers.Add(() => finalizeMethod.Invoke(null, null));
  183. }
  184. }
  185. catch (Exception ex)
  186. {
  187. Logger.Log(LogLevel.Warning, $"Could not load patcher methods from {assembly.GetName().Name}");
  188. Logger.Log(LogLevel.Warning, $"{ex}");
  189. }
  190. }
  191. Logger.Log(LogLevel.Info, $"Loaded {patcherMethods.Select(x => x.Key).Distinct().Count()} patcher methods from {assembly.GetName().Name}");
  192. return patcherMethods;
  193. }
  194. internal static void PatchEntrypoint(ref AssemblyDefinition assembly)
  195. {
  196. if (assembly.Name.Name == "UnityEngine")
  197. {
  198. #if CECIL_10
  199. using (AssemblyDefinition injected = AssemblyDefinition.ReadAssembly(CurrentExecutingAssemblyPath))
  200. #elif CECIL_9
  201. AssemblyDefinition injected = AssemblyDefinition.ReadAssembly(CurrentExecutingAssemblyPath);
  202. #endif
  203. {
  204. var originalInjectMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader")
  205. .Methods.First(x => x.Name == "Initialize");
  206. var injectMethod = assembly.MainModule.ImportReference(originalInjectMethod);
  207. var sceneManager = assembly.MainModule.Types.First(x => x.Name == "Application");
  208. var voidType = assembly.MainModule.ImportReference(typeof(void));
  209. var cctor = new MethodDefinition(".cctor",
  210. MethodAttributes.Static
  211. | MethodAttributes.Private
  212. | MethodAttributes.HideBySig
  213. | MethodAttributes.SpecialName
  214. | MethodAttributes.RTSpecialName,
  215. voidType);
  216. var ilp = cctor.Body.GetILProcessor();
  217. ilp.Append(ilp.Create(OpCodes.Call, injectMethod));
  218. ilp.Append(ilp.Create(OpCodes.Ret));
  219. sceneManager.Methods.Add(cctor);
  220. }
  221. }
  222. }
  223. internal static Assembly LocalResolve(object sender, ResolveEventArgs args)
  224. {
  225. AssemblyName assemblyName = new AssemblyName(args.Name);
  226. var foundAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == assemblyName.Name);
  227. if (foundAssembly != null)
  228. return foundAssembly;
  229. if (Utility.TryResolveDllAssembly(assemblyName, CurrentExecutingAssemblyDirectoryPath, out foundAssembly) ||
  230. Utility.TryResolveDllAssembly(assemblyName, PatcherPluginPath, out foundAssembly) ||
  231. Utility.TryResolveDllAssembly(assemblyName, PluginPath, out foundAssembly))
  232. return foundAssembly;
  233. return null;
  234. }
  235. }
  236. }