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.

1183 lines
50 KiB

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