Chainloader.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. using BepInEx.Configuration;
  2. using BepInEx.Logging;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Reflection;
  8. using System.Runtime.CompilerServices;
  9. using System.Text.RegularExpressions;
  10. using Mono.Cecil;
  11. using MonoMod.Utils;
  12. using UnityEngine;
  13. using Logger = BepInEx.Logging.Logger;
  14. namespace BepInEx.Bootstrap
  15. {
  16. /// <summary>
  17. /// The manager and loader for all plugins, and the entry point for BepInEx plugin system.
  18. /// </summary>
  19. public static class Chainloader
  20. {
  21. /// <summary>
  22. /// The loaded and initialized list of plugins.
  23. /// </summary>
  24. public static Dictionary<string, PluginInfo> PluginInfos { get; } = new Dictionary<string, PluginInfo>();
  25. private static readonly List<BaseUnityPlugin> _plugins = new List<BaseUnityPlugin>();
  26. // In some rare cases calling Application.unityVersion seems to cause MissingMethodException
  27. // if a preloader patch applies Harmony patch to Chainloader.Initialize.
  28. // The issue could be related to BepInEx being compiled against Unity 5.6 version of UnityEngine.dll,
  29. // but the issue is apparently present with both official Harmony and HarmonyX
  30. // We specifically prevent inlining to prevent early resolving
  31. // TODO: Figure out better version obtaining mechanism (e.g. from globalmanagers)
  32. private static string UnityVersion
  33. {
  34. [MethodImpl(MethodImplOptions.NoInlining)]
  35. get => Application.unityVersion;
  36. }
  37. /// <summary>
  38. /// List of all <see cref="BepInPlugin"/> loaded via the chainloader.
  39. /// </summary>
  40. [Obsolete("Use PluginInfos instead")]
  41. public static List<BaseUnityPlugin> Plugins
  42. {
  43. get
  44. {
  45. lock (_plugins)
  46. {
  47. _plugins.RemoveAll(x => x == null);
  48. return _plugins.ToList();
  49. }
  50. }
  51. }
  52. /// <summary>
  53. /// Collection of error chainloader messages that occured during plugin loading.
  54. /// Contains information about what certain plugins were not loaded.
  55. /// </summary>
  56. public static List<string> DependencyErrors { get; } = new List<string>();
  57. /// <summary>
  58. /// The GameObject that all plugins are attached to as components.
  59. /// </summary>
  60. public static GameObject ManagerObject { get; private set; }
  61. private static bool _loaded = false;
  62. private static bool _initialized = false;
  63. /// <summary>
  64. /// Initializes BepInEx to be able to start the chainloader.
  65. /// </summary>
  66. public static void Initialize(string gameExePath, bool startConsole = true, ICollection<LogEventArgs> preloaderLogEvents = null)
  67. {
  68. if (_initialized)
  69. return;
  70. ThreadingHelper.Initialize();
  71. // Set vitals
  72. if (gameExePath != null)
  73. {
  74. // Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize
  75. // This is used by Preloader to use environment variables, for example
  76. Paths.SetExecutablePath(gameExePath);
  77. }
  78. // Start logging
  79. if (ConsoleManager.ConfigConsoleEnabled.Value && startConsole)
  80. {
  81. ConsoleManager.CreateConsole();
  82. Logger.Listeners.Add(new ConsoleLogListener());
  83. }
  84. Logger.InitializeInternalLoggers();
  85. if (ConfigDiskLogging.Value)
  86. Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskConsoleDisplayedLevel.Value, ConfigDiskAppend.Value, ConfigDiskWriteUnityLog.Value));
  87. if (!TraceLogSource.IsListening)
  88. Logger.Sources.Add(TraceLogSource.CreateSource());
  89. ReplayPreloaderLogs(preloaderLogEvents);
  90. // Add Unity log source only after replaying to prevent duplication in console
  91. if (ConfigUnityLogging.Value)
  92. Logger.Sources.Add(new UnityLogSource());
  93. Logger.Listeners.Add(new UnityLogListener());
  94. if (PlatformHelper.Is(Platform.Unix))
  95. {
  96. Logger.LogInfo($"Detected Unity version: v{UnityVersion}");
  97. }
  98. Logger.LogMessage("Chainloader ready");
  99. _initialized = true;
  100. }
  101. private static void ReplayPreloaderLogs(ICollection<LogEventArgs> preloaderLogEvents)
  102. {
  103. if (preloaderLogEvents == null)
  104. return;
  105. var unityLogger = new UnityLogListener();
  106. Logger.Listeners.Add(unityLogger);
  107. // Temporarily disable the console log listener (if there is one from preloader) as we replay the preloader logs
  108. var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
  109. if (logListener != null)
  110. Logger.Listeners.Remove(logListener);
  111. // Write preloader log events if there are any, including the original log source name
  112. var preloaderLogSource = Logger.CreateLogSource("Preloader");
  113. foreach (var preloaderLogEvent in preloaderLogEvents)
  114. Logger.InternalLogEvent(preloaderLogSource, preloaderLogEvent);
  115. Logger.Sources.Remove(preloaderLogSource);
  116. Logger.Listeners.Remove(unityLogger);
  117. if (logListener != null)
  118. Logger.Listeners.Add(logListener);
  119. }
  120. private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$");
  121. /// <summary>
  122. /// Analyzes the given type definition and attempts to convert it to a valid <see cref="PluginInfo"/>
  123. /// </summary>
  124. /// <param name="type">Type definition to analyze.</param>
  125. /// <returns>If the type represent a valid plugin, returns a <see cref="PluginInfo"/> instance. Otherwise, return null.</returns>
  126. public static PluginInfo ToPluginInfo(TypeDefinition type)
  127. {
  128. if (type.IsInterface || type.IsAbstract)
  129. return null;
  130. try
  131. {
  132. if (!type.IsSubtypeOf(typeof(BaseUnityPlugin)))
  133. return null;
  134. }
  135. catch (AssemblyResolutionException)
  136. {
  137. // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin.
  138. return null;
  139. }
  140. var metadata = BepInPlugin.FromCecilType(type);
  141. // Perform checks that will prevent the plugin from being loaded in ALL cases
  142. if (metadata == null)
  143. {
  144. Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified");
  145. return null;
  146. }
  147. if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID))
  148. {
  149. Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format.");
  150. return null;
  151. }
  152. if (metadata.Version == null)
  153. {
  154. Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid.");
  155. return null;
  156. }
  157. if (metadata.Name == null)
  158. {
  159. Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null.");
  160. return null;
  161. }
  162. var filters = BepInProcess.FromCecilType(type);
  163. var dependencies = BepInDependency.FromCecilType(type);
  164. var incompatibilities = BepInIncompatibility.FromCecilType(type);
  165. var bepinVersion = type.Module.AssemblyReferences.FirstOrDefault(reference => reference.Name == "BepInEx")?.Version ?? new Version();
  166. return new PluginInfo
  167. {
  168. Metadata = metadata,
  169. Processes = filters,
  170. Dependencies = dependencies,
  171. Incompatibilities = incompatibilities,
  172. TypeName = type.FullName,
  173. TargettedBepInExVersion = bepinVersion
  174. };
  175. }
  176. private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
  177. private static readonly Version CurrentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
  178. private static bool HasBepinPlugins(AssemblyDefinition ass)
  179. {
  180. if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName))
  181. return false;
  182. if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BaseUnityPlugin).FullName))
  183. return false;
  184. return true;
  185. }
  186. private static bool PluginTargetsWrongBepin(PluginInfo pluginInfo)
  187. {
  188. var pluginTarget = pluginInfo.TargettedBepInExVersion;
  189. // X.X.X.x - compare normally. x.x.x.X - nightly build number, ignore
  190. if (pluginTarget.Major != CurrentAssemblyVersion.Major) return true;
  191. if (pluginTarget.Minor > CurrentAssemblyVersion.Minor) return true;
  192. if (pluginTarget.Minor < CurrentAssemblyVersion.Minor) return false;
  193. return pluginTarget.Build > CurrentAssemblyVersion.Build;
  194. }
  195. /// <summary>
  196. /// The entrypoint for the BepInEx plugin system.
  197. /// </summary>
  198. public static void Start()
  199. {
  200. if (_loaded)
  201. return;
  202. if (!_initialized)
  203. throw new InvalidOperationException("BepInEx has not been initialized. Please call Chainloader.Initialize prior to starting the chainloader instance.");
  204. if (!Directory.Exists(Paths.PluginPath))
  205. Directory.CreateDirectory(Paths.PluginPath);
  206. if (!Directory.Exists(Paths.PatcherPluginPath))
  207. Directory.CreateDirectory(Paths.PatcherPluginPath);
  208. try
  209. {
  210. var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);
  211. if (ConsoleManager.ConsoleActive)
  212. ConsoleManager.SetConsoleTitle($"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Paths.ProcessName}");
  213. Logger.LogMessage("Chainloader started");
  214. ManagerObject = new GameObject("BepInEx_Manager");
  215. UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
  216. var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
  217. foreach (var keyValuePair in pluginsToLoad)
  218. foreach (var pluginInfo in keyValuePair.Value)
  219. pluginInfo.Location = keyValuePair.Key;
  220. var pluginInfos = pluginsToLoad.SelectMany(p => p.Value).ToList();
  221. var loadedAssemblies = new Dictionary<string, Assembly>();
  222. Logger.LogInfo($"{pluginInfos.Count} plugin{(PluginInfos.Count == 1 ? "" : "s")} to load");
  223. // We use a sorted dictionary to ensure consistent load order
  224. var dependencyDict = new SortedDictionary<string, IEnumerable<string>>(StringComparer.InvariantCultureIgnoreCase);
  225. var pluginsByGUID = new Dictionary<string, PluginInfo>();
  226. foreach (var pluginInfoGroup in pluginInfos.GroupBy(info => info.Metadata.GUID))
  227. {
  228. PluginInfo loadedVersion = null;
  229. foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
  230. {
  231. if (loadedVersion != null)
  232. {
  233. Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})");
  234. continue;
  235. }
  236. // Perform checks that will prevent loading plugins in this run
  237. var filters = pluginInfo.Processes.ToList();
  238. bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase));
  239. if (invalidProcessName)
  240. {
  241. Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})");
  242. continue;
  243. }
  244. loadedVersion = pluginInfo;
  245. dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID);
  246. pluginsByGUID[pluginInfo.Metadata.GUID] = pluginInfo;
  247. }
  248. }
  249. foreach (var pluginInfo in pluginsByGUID.Values.ToList())
  250. {
  251. if (pluginInfo.Incompatibilities.Any(incompatibility => pluginsByGUID.ContainsKey(incompatibility.IncompatibilityGUID)))
  252. {
  253. pluginsByGUID.Remove(pluginInfo.Metadata.GUID);
  254. dependencyDict.Remove(pluginInfo.Metadata.GUID);
  255. var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID).Where(x => pluginsByGUID.ContainsKey(x)).ToArray();
  256. string message = $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
  257. DependencyErrors.Add(message);
  258. Logger.LogError(message);
  259. }
  260. else if (PluginTargetsWrongBepin(pluginInfo))
  261. {
  262. string message = $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update";
  263. DependencyErrors.Add(message);
  264. Logger.LogWarning(message);
  265. }
  266. }
  267. var emptyDependencies = new string[0];
  268. // Sort plugins by their dependencies.
  269. // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list.
  270. var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys, x => dependencyDict.TryGetValue(x, out var deps) ? deps : emptyDependencies).ToList();
  271. var invalidPlugins = new HashSet<string>();
  272. var processedPlugins = new Dictionary<string, Version>();
  273. foreach (var pluginGUID in sortedPlugins)
  274. {
  275. // If the plugin is missing, don't process it
  276. if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginInfo))
  277. continue;
  278. var dependsOnInvalidPlugin = false;
  279. var missingDependencies = new List<BepInDependency>();
  280. foreach (var dependency in pluginInfo.Dependencies)
  281. {
  282. bool IsHardDependency(BepInDependency dep) => (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0;
  283. // If the dependency wasn't already processed, it's missing altogether
  284. bool dependencyExists = processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
  285. if (!dependencyExists || pluginVersion < dependency.MinimumVersion)
  286. {
  287. // If the dependency is hard, collect it into a list to show
  288. if (IsHardDependency(dependency))
  289. missingDependencies.Add(dependency);
  290. continue;
  291. }
  292. // If the dependency is invalid (e.g. has missing dependencies) and hard, report that to the user
  293. if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency))
  294. {
  295. dependsOnInvalidPlugin = true;
  296. break;
  297. }
  298. }
  299. processedPlugins.Add(pluginGUID, pluginInfo.Metadata.Version);
  300. if (dependsOnInvalidPlugin)
  301. {
  302. string message = $"Skipping [{pluginInfo}] because it has a dependency that was not loaded. See previous errors for details.";
  303. DependencyErrors.Add(message);
  304. Logger.LogWarning(message);
  305. continue;
  306. }
  307. if (missingDependencies.Count != 0)
  308. {
  309. bool IsEmptyVersion(Version v) => v.Major == 0 && v.Minor == 0 && v.Build <= 0 && v.Revision <= 0;
  310. string message = $@"Could not load [{pluginInfo}] because it has missing dependencies: {
  311. string.Join(", ", missingDependencies.Select(s => IsEmptyVersion(s.MinimumVersion) ? s.DependencyGUID : $"{s.DependencyGUID} (v{s.MinimumVersion} or newer)").ToArray())
  312. }";
  313. DependencyErrors.Add(message);
  314. Logger.LogError(message);
  315. invalidPlugins.Add(pluginGUID);
  316. continue;
  317. }
  318. try
  319. {
  320. Logger.LogInfo($"Loading [{pluginInfo}]");
  321. if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass))
  322. loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location);
  323. PluginInfos[pluginGUID] = pluginInfo;
  324. pluginInfo.Instance = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName));
  325. _plugins.Add(pluginInfo.Instance);
  326. }
  327. catch (Exception ex)
  328. {
  329. invalidPlugins.Add(pluginGUID);
  330. PluginInfos.Remove(pluginGUID);
  331. Logger.LogError($"Error loading [{pluginInfo}] : {ex.Message}");
  332. if (ex is ReflectionTypeLoadException re)
  333. Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
  334. else
  335. Logger.LogDebug(ex);
  336. }
  337. }
  338. }
  339. catch (Exception ex)
  340. {
  341. try
  342. {
  343. ConsoleManager.CreateConsole();
  344. }
  345. catch { }
  346. Logger.LogFatal("Error occurred starting the game");
  347. Logger.LogFatal(ex.ToString());
  348. }
  349. Logger.LogMessage("Chainloader startup complete");
  350. _loaded = true;
  351. }
  352. #region Config
  353. private static readonly ConfigEntry<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Bind(
  354. "Logging", "UnityLogListening",
  355. true,
  356. "Enables showing unity log messages in the BepInEx logging system.");
  357. private static readonly ConfigEntry<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind(
  358. "Logging.Disk", "WriteUnityLog",
  359. false,
  360. "Include unity log messages in log file output.");
  361. private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.Bind(
  362. "Logging.Disk", "AppendLog",
  363. false,
  364. "Appends to the log file instead of overwriting, on game startup.");
  365. private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.Bind(
  366. "Logging.Disk", "Enabled",
  367. true,
  368. "Enables writing log messages to disk.");
  369. private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
  370. "Logging.Disk", "LogLevels",
  371. LogLevel.Fatal | LogLevel.Error | LogLevel.Message | LogLevel.Info | LogLevel.Warning,
  372. "Which log leves are saved to the disk log output.");
  373. #endregion
  374. }
  375. }