You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1303 lines
55 KiB

3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
  1. #nullable enable
  2. using IPA.Config;
  3. using IPA.Loader.Features;
  4. using IPA.Logging;
  5. using IPA.Utilities;
  6. using Mono.Cecil;
  7. using Newtonsoft.Json;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Reflection;
  13. using System.Text.RegularExpressions;
  14. using System.Threading.Tasks;
  15. using Version = SemVer.Version;
  16. using SemVer;
  17. using System.Diagnostics.CodeAnalysis;
  18. using HarmonyLib;
  19. using System.Diagnostics;
  20. using IPA.AntiMalware;
  21. using Hive.Versioning;
  22. #if NET4
  23. using Task = System.Threading.Tasks.Task;
  24. using TaskEx = System.Threading.Tasks.Task;
  25. #endif
  26. #if NET3
  27. using Net3_Proxy;
  28. using Path = Net3_Proxy.Path;
  29. using File = Net3_Proxy.File;
  30. using Directory = Net3_Proxy.Directory;
  31. #endif
  32. namespace IPA.Loader
  33. {
  34. /// <summary>
  35. /// A type to manage the loading of plugins.
  36. /// </summary>
  37. internal partial class PluginLoader
  38. {
  39. internal static PluginMetadata SelfMeta = null!;
  40. internal static Task LoadTask() =>
  41. TaskEx.Run(() =>
  42. {
  43. YeetIfNeeded();
  44. var sw = Stopwatch.StartNew();
  45. LoadMetadata();
  46. sw.Stop();
  47. Logger.loader.Info($"Loading metadata took {sw.Elapsed}");
  48. sw.Reset();
  49. // old loader system
  50. #if false
  51. Resolve();
  52. InitFeatures();
  53. ComputeLoadOrder();
  54. FilterDisabled();
  55. FilterWithoutFiles();
  56. ResolveDependencies();
  57. #endif
  58. sw.Start();
  59. // Features contribute to load order considerations
  60. InitFeatures();
  61. DoOrderResolution();
  62. sw.Stop();
  63. Logger.loader.Info($"Calculating load order took {sw.Elapsed}");
  64. });
  65. internal static void YeetIfNeeded()
  66. {
  67. string pluginDir = UnityGame.PluginsPath;
  68. if (SelfConfig.YeetMods_ && UnityGame.IsGameVersionBoundary)
  69. {
  70. var oldPluginsName = Path.Combine(UnityGame.InstallPath, $"Old {UnityGame.OldVersion} Plugins");
  71. var newPluginsName = Path.Combine(UnityGame.InstallPath, $"Old {UnityGame.GameVersion} Plugins");
  72. if (Directory.Exists(oldPluginsName))
  73. Directory.Delete(oldPluginsName, true);
  74. Directory.Move(pluginDir, oldPluginsName);
  75. if (Directory.Exists(newPluginsName))
  76. Directory.Move(newPluginsName, pluginDir);
  77. else
  78. _ = Directory.CreateDirectory(pluginDir);
  79. }
  80. }
  81. internal static List<PluginMetadata> PluginsMetadata = new();
  82. internal static List<PluginMetadata> DisabledPlugins = new();
  83. private static readonly Regex embeddedTextDescriptionPattern = new(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  84. internal static void LoadMetadata()
  85. {
  86. string[] plugins = Directory.GetFiles(UnityGame.PluginsPath, "*.dll");
  87. try
  88. {
  89. var selfMeta = new PluginMetadata
  90. {
  91. Assembly = Assembly.GetExecutingAssembly(),
  92. File = new FileInfo(Path.Combine(UnityGame.InstallPath, "IPA.exe")),
  93. PluginType = null,
  94. IsSelf = true
  95. };
  96. string manifest;
  97. using (var manifestReader =
  98. new StreamReader(
  99. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  100. throw new InvalidOperationException()))
  101. manifest = manifestReader.ReadToEnd();
  102. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  103. PluginsMetadata.Add(selfMeta);
  104. SelfMeta = selfMeta;
  105. }
  106. catch (Exception e)
  107. {
  108. Logger.loader.Critical("Error loading own manifest");
  109. Logger.loader.Critical(e);
  110. }
  111. using var resolver = new CecilLibLoader();
  112. resolver.AddSearchDirectory(UnityGame.LibraryPath);
  113. resolver.AddSearchDirectory(UnityGame.PluginsPath);
  114. foreach (var plugin in plugins)
  115. {
  116. var metadata = new PluginMetadata
  117. {
  118. File = new FileInfo(Path.Combine(UnityGame.PluginsPath, plugin)),
  119. IsSelf = false
  120. };
  121. try
  122. {
  123. var scanResult = AntiMalwareEngine.Engine.ScanFile(metadata.File);
  124. if (scanResult is ScanResult.Detected)
  125. {
  126. Logger.loader.Warn($"Scan of {plugin} found malware; not loading");
  127. continue;
  128. }
  129. if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && scanResult is not ScanResult.KnownSafe and not ScanResult.NotDetected)
  130. {
  131. Logger.loader.Warn($"Scan of {plugin} found partial threat; not loading. To load this, set AntiMalware.RunPartialThreatCode in the config.");
  132. continue;
  133. }
  134. var pluginModule = AssemblyDefinition.ReadAssembly(metadata.File.FullName, new ReaderParameters
  135. {
  136. ReadingMode = ReadingMode.Immediate,
  137. ReadWrite = false,
  138. AssemblyResolver = resolver
  139. }).MainModule;
  140. string pluginNs = "";
  141. PluginManifest? pluginManifest = null;
  142. foreach (var resource in pluginModule.Resources)
  143. {
  144. const string manifestSuffix = ".manifest.json";
  145. if (resource is not EmbeddedResource embedded ||
  146. !embedded.Name.EndsWith(manifestSuffix, StringComparison.Ordinal)) continue;
  147. pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length);
  148. string manifest;
  149. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  150. manifest = manifestReader.ReadToEnd();
  151. pluginManifest = JsonConvert.DeserializeObject<PluginManifest?>(manifest);
  152. break;
  153. }
  154. if (pluginManifest == null)
  155. {
  156. #if DIRE_LOADER_WARNINGS
  157. Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}");
  158. #else
  159. Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}");
  160. #endif
  161. continue;
  162. }
  163. if (pluginManifest.Id == null)
  164. {
  165. Logger.loader.Warn($"Plugin '{pluginManifest.Name}' does not have a listed ID, using name");
  166. pluginManifest.Id = pluginManifest.Name;
  167. }
  168. metadata.Manifest = pluginManifest;
  169. bool TryPopulatePluginType(TypeDefinition type, PluginMetadata meta)
  170. {
  171. if (!type.HasCustomAttributes)
  172. return false;
  173. var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName);
  174. if (attr is null)
  175. return false;
  176. if (!attr.HasConstructorArguments)
  177. {
  178. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments");
  179. return false;
  180. }
  181. var args = attr.ConstructorArguments;
  182. if (args.Count != 1)
  183. {
  184. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments");
  185. return false;
  186. }
  187. var rtOptionsArg = args[0];
  188. if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName)
  189. {
  190. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}");
  191. return false;
  192. }
  193. var rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions
  194. meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt;
  195. meta.PluginType = type;
  196. return true;
  197. }
  198. void TryGetNamespacedPluginType(string ns, PluginMetadata meta)
  199. {
  200. foreach (var type in pluginModule.Types)
  201. {
  202. if (type.Namespace != ns) continue;
  203. if (TryPopulatePluginType(type, meta))
  204. return;
  205. }
  206. }
  207. var hint = metadata.Manifest.Misc?.PluginMainHint;
  208. if (hint != null)
  209. {
  210. var type = pluginModule.GetType(hint);
  211. if (type == null || !TryPopulatePluginType(type, metadata))
  212. TryGetNamespacedPluginType(hint, metadata);
  213. }
  214. if (metadata.PluginType == null)
  215. TryGetNamespacedPluginType(pluginNs, metadata);
  216. if (metadata.PluginType == null)
  217. {
  218. Logger.loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}");
  219. continue;
  220. }
  221. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  222. PluginsMetadata.Add(metadata);
  223. }
  224. catch (Exception e)
  225. {
  226. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  227. Logger.loader.Error(e);
  228. ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error)
  229. {
  230. ReasonText = "An error ocurred loading the data",
  231. Error = e
  232. });
  233. }
  234. }
  235. IEnumerable<string> bareManifests = Directory.GetFiles(UnityGame.PluginsPath, "*.json");
  236. bareManifests = bareManifests.Concat(Directory.GetFiles(UnityGame.PluginsPath, "*.manifest"));
  237. foreach (var manifest in bareManifests)
  238. {
  239. try
  240. {
  241. var metadata = new PluginMetadata
  242. {
  243. File = new FileInfo(Path.Combine(UnityGame.PluginsPath, manifest)),
  244. IsSelf = false,
  245. IsBare = true,
  246. };
  247. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  248. if (metadata.Manifest.Files.Length < 1)
  249. Logger.loader.Warn($"Bare manifest {Path.GetFileName(manifest)} does not declare any files. " +
  250. $"Dependency resolution and verification cannot be completed.");
  251. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  252. PluginsMetadata.Add(metadata);
  253. }
  254. catch (Exception e)
  255. {
  256. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  257. Logger.loader.Error(e);
  258. }
  259. }
  260. foreach (var meta in PluginsMetadata)
  261. { // process description include
  262. var lines = meta.Manifest.Description.Split('\n');
  263. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  264. if (m.Success)
  265. {
  266. if (meta.IsBare)
  267. {
  268. Logger.loader.Warn($"Bare manifest cannot specify description file");
  269. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  270. continue;
  271. }
  272. var name = m.Groups[1].Value;
  273. string description;
  274. if (!meta.IsSelf)
  275. {
  276. // plugin type must be non-null for non-self plugins
  277. var resc = meta.PluginType!.Module.Resources.Select(r => r as EmbeddedResource)
  278. .NonNull()
  279. .FirstOrDefault(r => r.Name == name);
  280. if (resc == null)
  281. {
  282. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  283. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  284. continue;
  285. }
  286. using var reader = new StreamReader(resc.GetResourceStream());
  287. description = reader.ReadToEnd();
  288. }
  289. else
  290. {
  291. using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name));
  292. description = descriptionReader.ReadToEnd();
  293. }
  294. meta.Manifest.Description = description;
  295. }
  296. }
  297. }
  298. }
  299. #region Ignore stuff
  300. /// <summary>
  301. /// An enum that represents several categories of ignore reasons that the loader may encounter.
  302. /// </summary>
  303. /// <seealso cref="IgnoreReason"/>
  304. public enum Reason
  305. {
  306. /// <summary>
  307. /// An error was thrown either loading plugin information from disk, or when initializing the plugin.
  308. /// </summary>
  309. /// <remarks>
  310. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  311. /// <see cref="IgnoreReason.Error"/> will contain the thrown exception.
  312. /// </remarks>
  313. Error,
  314. /// <summary>
  315. /// The plugin this reason is associated with has the same ID as another plugin whose information was
  316. /// already loaded.
  317. /// </summary>
  318. /// <remarks>
  319. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  320. /// <see cref="IgnoreReason.RelatedTo"/> will contain the metadata of the already loaded plugin.
  321. /// </remarks>
  322. Duplicate,
  323. /// <summary>
  324. /// The plugin this reason is associated with conflicts with another already loaded plugin.
  325. /// </summary>
  326. /// <remarks>
  327. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  328. /// <see cref="IgnoreReason.RelatedTo"/> will contain the metadata of the plugin it conflicts with.
  329. /// </remarks>
  330. Conflict,
  331. /// <summary>
  332. /// The plugin this reason is assiciated with is missing a dependency.
  333. /// </summary>
  334. /// <remarks>
  335. /// Since this is only given when a dependency is missing, <see cref="IgnoreReason.RelatedTo"/> will
  336. /// not be set.
  337. /// </remarks>
  338. Dependency,
  339. /// <summary>
  340. /// The plugin this reason is associated with was released for a game update, but is still considered
  341. /// present for the purposes of updating.
  342. /// </summary>
  343. Released,
  344. /// <summary>
  345. /// The plugin this reason is associated with was denied from loading by a <see cref="Features.Feature"/>
  346. /// that it marks.
  347. /// </summary>
  348. Feature,
  349. /// <summary>
  350. /// The plugin this reason is assoicated with is unsupported.
  351. /// </summary>
  352. /// <remarks>
  353. /// Currently, there is no path in the loader that emits this <see cref="Reason"/>, however there may
  354. /// be in the future.
  355. /// </remarks>
  356. Unsupported,
  357. /// <summary>
  358. /// One of the files that a plugin declared in its manifest is missing.
  359. /// </summary>
  360. MissingFiles
  361. }
  362. /// <summary>
  363. /// A structure describing the reason that a plugin was ignored.
  364. /// </summary>
  365. public struct IgnoreReason : IEquatable<IgnoreReason>
  366. {
  367. /// <summary>
  368. /// Gets the ignore reason, as represented by the <see cref="Loader.Reason"/> enum.
  369. /// </summary>
  370. public Reason Reason { get; }
  371. /// <summary>
  372. /// Gets the textual description of the particular ignore reason. This will typically
  373. /// include details about why the plugin was ignored, if it is present.
  374. /// </summary>
  375. public string? ReasonText { get; internal set; }
  376. /// <summary>
  377. /// Gets the <see cref="Exception"/> that caused this plugin to be ignored, if any.
  378. /// </summary>
  379. public Exception? Error { get; internal set; }
  380. /// <summary>
  381. /// Gets the metadata of the plugin that this ignore was related to, if any.
  382. /// </summary>
  383. public PluginMetadata? RelatedTo { get; internal set; }
  384. /// <summary>
  385. /// Initializes an <see cref="IgnoreReason"/> with the provided data.
  386. /// </summary>
  387. /// <param name="reason">the <see cref="Loader.Reason"/> enum value that describes this reason</param>
  388. /// <param name="reasonText">the textual description of this ignore reason, if any</param>
  389. /// <param name="error">the <see cref="Exception"/> that caused this <see cref="IgnoreReason"/>, if any</param>
  390. /// <param name="relatedTo">the <see cref="PluginMetadata"/> this reason is related to, if any</param>
  391. public IgnoreReason(Reason reason, string? reasonText = null, Exception? error = null, PluginMetadata? relatedTo = null)
  392. {
  393. Reason = reason;
  394. ReasonText = reasonText;
  395. Error = error;
  396. RelatedTo = relatedTo;
  397. }
  398. /// <inheritdoc/>
  399. public override bool Equals(object obj)
  400. => obj is IgnoreReason ir && Equals(ir);
  401. /// <summary>
  402. /// Compares this <see cref="IgnoreReason"/> with <paramref name="other"/> for equality.
  403. /// </summary>
  404. /// <param name="other">the reason to compare to</param>
  405. /// <returns><see langword="true"/> if the two reasons compare equal, <see langword="false"/> otherwise</returns>
  406. public bool Equals(IgnoreReason other)
  407. => Reason == other.Reason && ReasonText == other.ReasonText
  408. && Error == other.Error && RelatedTo == other.RelatedTo;
  409. /// <inheritdoc/>
  410. public override int GetHashCode()
  411. {
  412. int hashCode = 778404373;
  413. hashCode = (hashCode * -1521134295) + Reason.GetHashCode();
  414. hashCode = (hashCode * -1521134295) + ReasonText?.GetHashCode() ?? 0;
  415. hashCode = (hashCode * -1521134295) + Error?.GetHashCode() ?? 0;
  416. hashCode = (hashCode * -1521134295) + RelatedTo?.GetHashCode() ?? 0;
  417. return hashCode;
  418. }
  419. /// <summary>
  420. /// Checks if two <see cref="IgnoreReason"/>s are equal.
  421. /// </summary>
  422. /// <param name="left">the first <see cref="IgnoreReason"/> to compare</param>
  423. /// <param name="right">the second <see cref="IgnoreReason"/> to compare</param>
  424. /// <returns><see langword="true"/> if the two reasons compare equal, <see langword="false"/> otherwise</returns>
  425. public static bool operator ==(IgnoreReason left, IgnoreReason right)
  426. => left.Equals(right);
  427. /// <summary>
  428. /// Checks if two <see cref="IgnoreReason"/>s are not equal.
  429. /// </summary>
  430. /// <param name="left">the first <see cref="IgnoreReason"/> to compare</param>
  431. /// <param name="right">the second <see cref="IgnoreReason"/> to compare</param>
  432. /// <returns><see langword="true"/> if the two reasons are not equal, <see langword="false"/> otherwise</returns>
  433. public static bool operator !=(IgnoreReason left, IgnoreReason right)
  434. => !(left == right);
  435. }
  436. #endregion
  437. internal partial class PluginLoader
  438. {
  439. // keep track of these for the updater; it should still be able to update mods not loaded
  440. // the thing -> the reason
  441. internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new();
  442. #if false
  443. internal static void Resolve()
  444. { // resolves duplicates and conflicts, etc
  445. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  446. var ids = new HashSet<string>();
  447. var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
  448. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  449. foreach (var meta in PluginsMetadata)
  450. {
  451. if (meta.Id != null)
  452. {
  453. if (ids.Contains(meta.Id))
  454. {
  455. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  456. var ireason = new IgnoreReason(Reason.Duplicate)
  457. {
  458. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  459. RelatedTo = resolved.First(p => p.Id == meta.Id)
  460. };
  461. ignore.Add(meta, ireason);
  462. ignoredPlugins.Add(meta, ireason);
  463. continue; // because of sorted order, hightest order will always be the first one
  464. }
  465. bool processedLater = false;
  466. foreach (var meta2 in PluginsMetadata)
  467. {
  468. if (ignore.ContainsKey(meta2)) continue;
  469. if (meta == meta2)
  470. {
  471. processedLater = true;
  472. continue;
  473. }
  474. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  475. var range = meta2.Manifest.Conflicts[meta.Id];
  476. if (!range.IsSatisfied(meta.Version)) continue;
  477. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}");
  478. if (processedLater)
  479. {
  480. Logger.loader.Warn($"Ignoring {meta2.Name}");
  481. ignore.Add(meta2, new IgnoreReason(Reason.Conflict)
  482. {
  483. ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}",
  484. RelatedTo = meta
  485. });
  486. }
  487. else
  488. {
  489. Logger.loader.Warn($"Ignoring {meta.Name}");
  490. ignore.Add(meta, new IgnoreReason(Reason.Conflict)
  491. {
  492. ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}",
  493. RelatedTo = meta2
  494. });
  495. break;
  496. }
  497. }
  498. }
  499. if (ignore.TryGetValue(meta, out var reason))
  500. {
  501. ignoredPlugins.Add(meta, reason);
  502. continue;
  503. }
  504. if (meta.Id != null)
  505. ids.Add(meta.Id);
  506. resolved.Add(meta);
  507. }
  508. PluginsMetadata = resolved;
  509. }
  510. private static void FilterDisabled()
  511. {
  512. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  513. var disabled = DisabledConfig.Instance.DisabledModIds;
  514. foreach (var meta in PluginsMetadata)
  515. {
  516. if (disabled.Contains(meta.Id ?? meta.Name))
  517. DisabledPlugins.Add(meta);
  518. else
  519. enabled.Add(meta);
  520. }
  521. PluginsMetadata = enabled;
  522. }
  523. private static void FilterWithoutFiles()
  524. {
  525. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  526. foreach (var meta in PluginsMetadata)
  527. {
  528. var passed = true;
  529. foreach (var file in meta.AssociatedFiles)
  530. {
  531. if (!file.Exists)
  532. {
  533. passed = false;
  534. ignoredPlugins.Add(meta, new IgnoreReason(Reason.MissingFiles)
  535. {
  536. ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} (declared by {meta.Name}) does not exist"
  537. });
  538. Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" +
  539. $" (declared by {meta.Name}) does not exist! Mod installation is incomplete, not loading it.");
  540. break;
  541. }
  542. }
  543. if (passed)
  544. enabled.Add(meta);
  545. }
  546. PluginsMetadata = enabled;
  547. }
  548. internal static void ComputeLoadOrder()
  549. {
  550. #if DEBUG
  551. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  552. #endif
  553. static bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  554. { // this is slow, and hella recursive
  555. bool inserted = false;
  556. foreach (var sr in root)
  557. {
  558. inserted = inserted || InsertInto(sr.Dependencies, meta);
  559. if (meta.Id != null)
  560. {
  561. if (sr.Manifest.Dependencies.ContainsKey(meta.Id))
  562. inserted = inserted || sr.Dependencies.Add(meta);
  563. else if (sr.Manifest.LoadAfter.Contains(meta.Id))
  564. inserted = inserted || sr.LoadsAfter.Add(meta);
  565. }
  566. if (sr.Id != null)
  567. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  568. inserted = inserted || sr.LoadsAfter.Add(meta);
  569. }
  570. if (isRoot)
  571. {
  572. foreach (var sr in root)
  573. {
  574. InsertInto(meta.Dependencies, sr);
  575. if (sr.Id != null)
  576. {
  577. if (meta.Manifest.Dependencies.ContainsKey(sr.Id))
  578. meta.Dependencies.Add(sr);
  579. else if (meta.Manifest.LoadAfter.Contains(sr.Id))
  580. meta.LoadsAfter.Add(sr);
  581. }
  582. if (meta.Id != null)
  583. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  584. meta.LoadsAfter.Add(sr);
  585. }
  586. root.Add(meta);
  587. }
  588. return inserted;
  589. }
  590. var pluginTree = new HashSet<PluginMetadata>();
  591. foreach (var meta in PluginsMetadata)
  592. InsertInto(pluginTree, meta, true);
  593. static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  594. {
  595. foreach (var st in tree)
  596. if (!into.Contains(st))
  597. {
  598. DeTree(into, st.Dependencies);
  599. DeTree(into, st.LoadsAfter);
  600. into.Add(st);
  601. }
  602. }
  603. PluginsMetadata = new List<PluginMetadata>();
  604. DeTree(PluginsMetadata, pluginTree);
  605. #if DEBUG
  606. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  607. #endif
  608. }
  609. internal static void ResolveDependencies()
  610. {
  611. var metadata = new List<PluginMetadata>();
  612. var pluginsToLoad = new Dictionary<string, Version>();
  613. var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
  614. foreach (var meta in PluginsMetadata)
  615. {
  616. bool ignoreBcNoLoader = true;
  617. var missingDeps = new List<(string id, Range version, bool disabled)>();
  618. foreach (var dep in meta.Manifest.Dependencies)
  619. {
  620. #if DEBUG
  621. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  622. #endif
  623. if (dep.Key == SelfMeta.Id)
  624. ignoreBcNoLoader = false;
  625. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  626. continue;
  627. if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key]))
  628. {
  629. Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
  630. missingDeps.Add((dep.Key, dep.Value, true));
  631. }
  632. else
  633. {
  634. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  635. missingDeps.Add((dep.Key, dep.Value, false));
  636. }
  637. }
  638. if (meta.PluginType != null && !meta.IsSelf && !meta.IsBare && ignoreBcNoLoader)
  639. {
  640. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  641. {
  642. ReasonText = "BSIPA Plugin does not reference BSIPA!"
  643. });
  644. for (int i = 0; i < 20; i++)
  645. {
  646. Logger.loader.Warn($"HEY {meta.Id} YOU DEPEND ON BSIPA SO DEPEND ON BSIPA");
  647. }
  648. continue;
  649. }
  650. if (missingDeps.Count == 0)
  651. {
  652. metadata.Add(meta);
  653. if (meta.Id != null)
  654. pluginsToLoad.Add(meta.Id, meta.Version);
  655. }
  656. else if (missingDeps.Any(t => !t.disabled))
  657. { // missing deps
  658. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  659. {
  660. ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}"
  661. });
  662. }
  663. else
  664. {
  665. DisabledPlugins.Add(meta);
  666. DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
  667. }
  668. }
  669. DisabledConfig.Instance.Changed();
  670. PluginsMetadata = metadata;
  671. }
  672. #endif
  673. internal static void DoOrderResolution()
  674. {
  675. #if DEBUG
  676. // print starting order
  677. Logger.loader.Debug(string.Join(", ", PluginsMetadata.StrJP()));
  678. #endif
  679. PluginsMetadata.Sort((a, b) => b.HVersion.CompareTo(a.HVersion));
  680. #if DEBUG
  681. // print base resolution order
  682. Logger.loader.Debug(string.Join(", ", PluginsMetadata.StrJP()));
  683. #endif
  684. var metadataCache = new Dictionary<string, (PluginMetadata Meta, bool Enabled)>(PluginsMetadata.Count);
  685. var pluginsToProcess = new List<PluginMetadata>(PluginsMetadata.Count);
  686. var disabledIds = DisabledConfig.Instance.DisabledModIds;
  687. var disabledPlugins = new List<PluginMetadata>();
  688. // build metadata cache
  689. foreach (var meta in PluginsMetadata)
  690. {
  691. if (!metadataCache.TryGetValue(meta.Id, out var existing))
  692. {
  693. if (disabledIds.Contains(meta.Id))
  694. {
  695. metadataCache.Add(meta.Id, (meta, false));
  696. disabledPlugins.Add(meta);
  697. }
  698. else
  699. {
  700. metadataCache.Add(meta.Id, (meta, true));
  701. pluginsToProcess.Add(meta);
  702. }
  703. }
  704. else
  705. {
  706. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  707. ignoredPlugins.Add(meta, new(Reason.Duplicate)
  708. {
  709. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  710. RelatedTo = existing.Meta
  711. });
  712. }
  713. }
  714. // preprocess LoadBefore into LoadAfter
  715. foreach (var (_, (meta, _)) in metadataCache)
  716. { // we iterate the metadata cache because it contains both disabled and enabled plugins
  717. var loadBefore = meta.Manifest.LoadBefore;
  718. foreach (var id in loadBefore)
  719. {
  720. if (metadataCache.TryGetValue(id, out var plugin))
  721. {
  722. // if the id exists in our metadata cache, make sure it knows to load after the plugin in kvp
  723. _ = plugin.Meta.LoadsAfter.Add(meta);
  724. }
  725. }
  726. }
  727. // preprocess conflicts to be mutual
  728. foreach (var (_, (meta, _)) in metadataCache)
  729. {
  730. foreach (var (id, range) in meta.Manifest.Conflicts)
  731. {
  732. if (metadataCache.TryGetValue(id, out var plugin)
  733. && range.Matches(plugin.Meta.HVersion))
  734. {
  735. // make sure that there's a mutual dependency
  736. var targetRange = VersionRange.ForVersion(meta.HVersion);
  737. var targetConflicts = plugin.Meta.Manifest.Conflicts;
  738. if (!targetConflicts.TryGetValue(meta.Id, out var realRange))
  739. {
  740. // there's not already a listed conflict
  741. targetConflicts.Add(meta.Id, targetRange);
  742. }
  743. else if (!realRange.Matches(meta.HVersion))
  744. {
  745. // there is already a listed conflict that isn't mutual
  746. targetRange = realRange | targetRange;
  747. targetConflicts[meta.Id] = targetRange;
  748. }
  749. }
  750. }
  751. }
  752. var loadedPlugins = new Dictionary<string, (PluginMetadata Meta, bool Disabled, bool Ignored)>();
  753. var outputOrder = new List<PluginMetadata>(PluginsMetadata.Count);
  754. var isProcessing = new HashSet<PluginMetadata>();
  755. {
  756. bool TryResolveId(string id, [MaybeNullWhen(false)] out PluginMetadata meta, out bool disabled, out bool ignored, bool partial = false)
  757. {
  758. meta = null;
  759. disabled = false;
  760. ignored = true;
  761. Logger.loader.Trace($"Trying to resolve plugin '{id}' partial:{partial}");
  762. if (loadedPlugins.TryGetValue(id, out var foundMeta))
  763. {
  764. meta = foundMeta.Meta;
  765. disabled = foundMeta.Disabled;
  766. ignored = foundMeta.Ignored;
  767. Logger.loader.Trace($"- Found already processed");
  768. return true;
  769. }
  770. if (metadataCache!.TryGetValue(id, out var plugin))
  771. {
  772. Logger.loader.Trace($"- In metadata cache");
  773. if (partial)
  774. {
  775. Logger.loader.Trace($" - but requested in a partial lookup");
  776. return false;
  777. }
  778. disabled = !plugin.Enabled;
  779. meta = plugin.Meta;
  780. if (!disabled)
  781. {
  782. try
  783. {
  784. ignored = false;
  785. Resolve(plugin.Meta, ref disabled, out ignored);
  786. }
  787. catch (Exception e)
  788. {
  789. if (e is not DependencyResolutionLoopException)
  790. {
  791. Logger.loader.Error($"While performing load order resolution for {id}:");
  792. Logger.loader.Error(e);
  793. }
  794. if (!ignored)
  795. {
  796. ignoredPlugins.Add(plugin.Meta, new(Reason.Error)
  797. {
  798. Error = e
  799. });
  800. }
  801. ignored = true;
  802. }
  803. }
  804. if (!loadedPlugins.ContainsKey(id))
  805. {
  806. // this condition is specifically for when we fail resolution because of a graph loop
  807. Logger.loader.Trace($"- '{id}' resolved as ignored:{ignored},disabled:{disabled}");
  808. loadedPlugins.Add(id, (plugin.Meta, disabled, ignored));
  809. }
  810. return true;
  811. }
  812. Logger.loader.Trace($"- Not found");
  813. return false;
  814. }
  815. void Resolve(PluginMetadata plugin, ref bool disabled, out bool ignored)
  816. {
  817. Logger.loader.Trace($">Resolving '{plugin.Name}'");
  818. // first we need to check for loops in the resolution graph to prevent stack overflows
  819. if (isProcessing.Contains(plugin))
  820. {
  821. Logger.loader.Error($"Loop detected while processing '{plugin.Name}'; flagging as ignored");
  822. throw new DependencyResolutionLoopException();
  823. }
  824. isProcessing.Add(plugin);
  825. using var _removeProcessing = Utils.ScopeGuard(() => isProcessing.Remove(plugin));
  826. // if this method is being called, this is the first and only time that it has been called for this plugin.
  827. ignored = false;
  828. // perform file existence check before attempting to load dependencies
  829. foreach (var file in plugin.AssociatedFiles)
  830. {
  831. if (!file.Exists)
  832. {
  833. ignoredPlugins.Add(plugin, new IgnoreReason(Reason.MissingFiles)
  834. {
  835. ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} does not exist"
  836. });
  837. Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" +
  838. $" (declared by '{plugin.Name}') does not exist! Mod installation is incomplete, not loading it.");
  839. ignored = true;
  840. return;
  841. }
  842. }
  843. // first load dependencies
  844. var dependsOnSelf = false;
  845. foreach (var (id, range) in plugin.Manifest.Dependencies)
  846. {
  847. if (id == SelfMeta.Id)
  848. dependsOnSelf = true;
  849. if (!TryResolveId(id, out var depMeta, out var depDisabled, out var depIgnored)
  850. || !range.Matches(depMeta.HVersion))
  851. {
  852. Logger.loader.Warn($"Dependency '{id}@{range}' for '{plugin.Id}' does not exist; ignoring '{plugin.Id}'");
  853. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  854. {
  855. ReasonText = $"Dependency '{id}@{range}' not found",
  856. });
  857. ignored = true;
  858. return;
  859. }
  860. // make a point to propagate ignored
  861. if (depIgnored)
  862. {
  863. Logger.loader.Warn($"Dependency '{id}' for '{plugin.Id}' previously ignored; ignoring '{plugin.Id}'");
  864. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  865. {
  866. ReasonText = $"Dependency '{id}' ignored",
  867. RelatedTo = depMeta
  868. });
  869. ignored = true;
  870. return;
  871. }
  872. // make a point to propagate disabled
  873. if (depDisabled)
  874. {
  875. Logger.loader.Warn($"Dependency '{id}' for '{plugin.Id}' disabled; disabling");
  876. disabledPlugins!.Add(plugin);
  877. _ = disabledIds!.Add(plugin.Id);
  878. disabled = true;
  879. }
  880. // we found our dep, lets save the metadata and keep going
  881. _ = plugin.Dependencies.Add(depMeta);
  882. }
  883. // make sure the plugin depends on the loader (assuming it actually needs to)
  884. if (!dependsOnSelf && !plugin.IsSelf && !plugin.IsBare)
  885. {
  886. Logger.loader.Warn($"Plugin '{plugin.Id}' does not depend on any particular loader version; assuming its incompatible");
  887. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  888. {
  889. ReasonText = "Does not depend on any loader version, so it is assumed to be incompatible",
  890. RelatedTo = SelfMeta
  891. });
  892. ignored = true;
  893. return;
  894. }
  895. // exit early if we've decided we need to be disabled
  896. if (disabled)
  897. return;
  898. // handle LoadsAfter populated by Features processing
  899. foreach (var loadAfter in plugin.LoadsAfter)
  900. {
  901. if (TryResolveId(loadAfter.Id, out _, out _, out _))
  902. {
  903. // do nothing, because the plugin is already in the LoadsAfter set
  904. }
  905. }
  906. // then handle loadafters
  907. foreach (var id in plugin.Manifest.LoadAfter)
  908. {
  909. if (TryResolveId(id, out var meta, out var depDisabled, out var depIgnored))
  910. {
  911. // we only want to make sure to loadafter if its not ignored
  912. // if its disabled, we still wanna track it where possible
  913. _ = plugin.LoadsAfter.Add(meta);
  914. }
  915. }
  916. // after we handle dependencies and loadafters, then check conflicts
  917. foreach (var (id, range) in plugin.Manifest.Conflicts)
  918. {
  919. Logger.loader.Trace($">- Checking conflict '{id}' {range}");
  920. // this lookup must be partial to prevent loadBefore/conflictsWith from creating a recursion loop
  921. if (TryResolveId(id, out var meta, out var conflDisabled, out var conflIgnored, partial: true)
  922. && range.Matches(meta.HVersion)
  923. && !conflIgnored && !conflDisabled) // the conflict is only *actually* a problem if it is both not ignored and not disabled
  924. {
  925. Logger.loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.HVersion}; ignoring '{plugin.Id}'");
  926. ignoredPlugins.Add(plugin, new(Reason.Conflict)
  927. {
  928. ReasonText = $"Conflicts with {meta.Id}@{meta.HVersion}",
  929. RelatedTo = meta
  930. });
  931. ignored = true;
  932. return;
  933. }
  934. }
  935. // specifically check if some strange stuff happened (like graph loops) causing this to be ignored
  936. // from some other invocation
  937. if (!ignoredPlugins.ContainsKey(plugin))
  938. {
  939. // we can now load the current plugin
  940. Logger.loader.Trace($"->'{plugin.Name}' loads here");
  941. outputOrder!.Add(plugin);
  942. }
  943. // loadbefores have already been preprocessed into loadafters
  944. Logger.loader.Trace($">Processed '{plugin.Name}'");
  945. }
  946. // run TryResolveId over every plugin, which recursively calculates load order
  947. foreach (var plugin in pluginsToProcess)
  948. {
  949. _ = TryResolveId(plugin.Id, out _, out _, out _);
  950. }
  951. // by this point, outputOrder contains the full load order
  952. }
  953. DisabledConfig.Instance.Changed();
  954. DisabledPlugins = disabledPlugins;
  955. PluginsMetadata = outputOrder;
  956. }
  957. internal static void InitFeatures()
  958. {
  959. foreach (var meta in PluginsMetadata)
  960. {
  961. foreach (var feature in meta.Manifest.Features.Select(f => new Feature.Instance(meta, f.Key, f.Value)))
  962. {
  963. if (feature.TryGetDefiningPlugin(out var plugin) && plugin == null)
  964. { // this is a DefineFeature, so we want to initialize it early
  965. if (!feature.TryCreate(out var inst))
  966. {
  967. Logger.features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}");
  968. }
  969. else
  970. {
  971. meta.InternalFeatures.Add(inst);
  972. }
  973. }
  974. else
  975. { // this is literally any other feature, so we want to delay its initialization
  976. _ = meta.UnloadedFeatures.Add(feature);
  977. }
  978. }
  979. }
  980. // at this point we have pre-initialized all features, so we can go ahead and use them to add stuff to the dep resolver
  981. foreach (var meta in PluginsMetadata)
  982. {
  983. foreach (var feature in meta.UnloadedFeatures)
  984. {
  985. if (feature.TryGetDefiningPlugin(out var plugin))
  986. {
  987. if (plugin != meta && plugin != null)
  988. { // if the feature is not applied to the defining feature
  989. _ = meta.LoadsAfter.Add(plugin);
  990. }
  991. if (plugin != null)
  992. {
  993. plugin.CreateFeaturesWhenLoaded.Add(feature);
  994. }
  995. }
  996. else
  997. {
  998. Logger.features.Warn($"No such feature {feature.Name}");
  999. }
  1000. }
  1001. }
  1002. }
  1003. internal static void ReleaseAll(bool full = false)
  1004. {
  1005. if (full)
  1006. {
  1007. ignoredPlugins = new();
  1008. }
  1009. else
  1010. {
  1011. foreach (var m in PluginsMetadata)
  1012. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  1013. foreach (var m in ignoredPlugins.Keys)
  1014. { // clean them up so we can still use the metadata for updates
  1015. m.InternalFeatures.Clear();
  1016. m.PluginType = null;
  1017. m.Assembly = null!;
  1018. }
  1019. }
  1020. PluginsMetadata = new List<PluginMetadata>();
  1021. DisabledPlugins = new List<PluginMetadata>();
  1022. Feature.Reset();
  1023. GC.Collect();
  1024. GC.WaitForPendingFinalizers();
  1025. }
  1026. internal static void Load(PluginMetadata meta)
  1027. {
  1028. if (meta is { Assembly: null, PluginType: not null })
  1029. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  1030. }
  1031. internal static PluginExecutor? InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  1032. {
  1033. if (meta.Manifest.GameVersion != UnityGame.GameVersion)
  1034. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  1035. if (meta.IsSelf)
  1036. return new PluginExecutor(meta, PluginExecutor.Special.Self);
  1037. foreach (var dep in meta.Dependencies)
  1038. {
  1039. if (alreadyLoaded.Contains(dep)) continue;
  1040. // otherwise...
  1041. if (ignoredPlugins.TryGetValue(dep, out var reason))
  1042. { // was added to the ignore list
  1043. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  1044. {
  1045. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  1046. RelatedTo = dep
  1047. });
  1048. }
  1049. else
  1050. { // was not added to ignore list
  1051. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  1052. {
  1053. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  1054. RelatedTo = dep
  1055. });
  1056. }
  1057. return null;
  1058. }
  1059. if (meta.IsBare)
  1060. return new PluginExecutor(meta, PluginExecutor.Special.Bare);
  1061. Load(meta);
  1062. PluginExecutor exec;
  1063. try
  1064. {
  1065. exec = new PluginExecutor(meta);
  1066. }
  1067. catch (Exception e)
  1068. {
  1069. Logger.loader.Error($"Error creating executor for {meta.Name}");
  1070. Logger.loader.Error(e);
  1071. return null;
  1072. }
  1073. foreach (var feature in meta.Features)
  1074. {
  1075. try
  1076. {
  1077. feature.BeforeInit(meta);
  1078. }
  1079. catch (Exception e)
  1080. {
  1081. Logger.loader.Critical($"Feature errored in {nameof(Feature.BeforeInit)}:");
  1082. Logger.loader.Critical(e);
  1083. }
  1084. }
  1085. try
  1086. {
  1087. exec.Create();
  1088. }
  1089. catch (Exception e)
  1090. {
  1091. Logger.loader.Error($"Could not init plugin {meta.Name}");
  1092. Logger.loader.Error(e);
  1093. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  1094. {
  1095. ReasonText = "Error ocurred while initializing",
  1096. Error = e
  1097. });
  1098. return null;
  1099. }
  1100. // TODO: make this new features system behave better wrt DynamicInit plugins
  1101. foreach (var feature in meta.CreateFeaturesWhenLoaded)
  1102. {
  1103. if (!feature.TryCreate(out var inst))
  1104. {
  1105. Logger.features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}");
  1106. }
  1107. else
  1108. {
  1109. feature.AppliedTo.InternalFeatures.Add(inst);
  1110. _ = feature.AppliedTo.UnloadedFeatures.Remove(feature);
  1111. }
  1112. }
  1113. meta.CreateFeaturesWhenLoaded.Clear(); // if a plugin is loaded twice, for the moment, we don't want to create the feature twice
  1114. foreach (var feature in meta.Features)
  1115. try
  1116. {
  1117. feature.AfterInit(meta, exec.Instance);
  1118. }
  1119. catch (Exception e)
  1120. {
  1121. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}:");
  1122. Logger.loader.Critical(e);
  1123. }
  1124. return exec;
  1125. }
  1126. internal static bool IsFirstLoadComplete { get; private set; }
  1127. internal static List<PluginExecutor> LoadPlugins()
  1128. {
  1129. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  1130. var list = new List<PluginExecutor>();
  1131. var loaded = new HashSet<PluginMetadata>();
  1132. foreach (var meta in PluginsMetadata)
  1133. {
  1134. try
  1135. {
  1136. var exec = InitPlugin(meta, loaded);
  1137. if (exec != null)
  1138. {
  1139. list.Add(exec);
  1140. _ = loaded.Add(meta);
  1141. }
  1142. }
  1143. catch (Exception e)
  1144. {
  1145. Logger.log.Critical($"Uncaught exception while loading pluign {meta.Name}:");
  1146. Logger.log.Critical(e);
  1147. }
  1148. }
  1149. // TODO: should this be somewhere else?
  1150. IsFirstLoadComplete = true;
  1151. return list;
  1152. }
  1153. }
  1154. }