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.

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