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.

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