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.

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