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.

1167 lines
49 KiB

  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. 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)) 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
  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. var loadedPlugins = new Dictionary<string, (PluginMetadata Meta, bool Disabled, bool Ignored)>();
  683. var outputOrder = new List<PluginMetadata>(PluginsMetadata.Count);
  684. {
  685. bool TryResolveId(string id, [MaybeNullWhen(false)] out PluginMetadata meta, out bool disabled, out bool ignored)
  686. {
  687. meta = null;
  688. disabled = false;
  689. ignored = true;
  690. if (loadedPlugins.TryGetValue(id, out var foundMeta))
  691. {
  692. meta = foundMeta.Meta;
  693. disabled = foundMeta.Disabled;
  694. ignored = foundMeta.Ignored;
  695. return true;
  696. }
  697. if (metadataCache!.TryGetValue(id, out var plugin))
  698. {
  699. disabled = !plugin.Enabled;
  700. meta = plugin.Meta;
  701. if (!disabled)
  702. {
  703. Resolve(plugin.Meta, ref disabled, out ignored);
  704. }
  705. loadedPlugins.Add(id, (plugin.Meta, disabled, ignored));
  706. return true;
  707. }
  708. return false;
  709. }
  710. void Resolve(PluginMetadata plugin, ref bool disabled, out bool ignored)
  711. {
  712. // if this method is being called, this is the first and only time that it has been called for this plugin.
  713. ignored = false;
  714. // perform file existence check before attempting to load dependencies
  715. foreach (var file in plugin.AssociatedFiles)
  716. {
  717. if (!file.Exists)
  718. {
  719. ignoredPlugins.Add(plugin, new IgnoreReason(Reason.MissingFiles)
  720. {
  721. ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} does not exist"
  722. });
  723. Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" +
  724. $" (declared by {plugin.Name}) does not exist! Mod installation is incomplete, not loading it.");
  725. ignored = true;
  726. return;
  727. }
  728. }
  729. // first load dependencies
  730. var dependsOnSelf = false;
  731. foreach (var dep in plugin.Manifest.Dependencies)
  732. {
  733. if (dep.Key == SelfMeta.Id)
  734. dependsOnSelf = true;
  735. if (!TryResolveId(dep.Key, out var depMeta, out var depDisabled, out var depIgnored))
  736. {
  737. Logger.loader.Warn($"Dependency '{dep.Key}@{dep.Value}' for '{plugin.Id}' does not exist; ignoring '{plugin.Id}'");
  738. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  739. {
  740. ReasonText = $"Dependency '{dep.Key}@{dep.Value}' not found",
  741. });
  742. ignored = true;
  743. return;
  744. }
  745. // make a point to propagate ignored
  746. if (depIgnored)
  747. {
  748. Logger.loader.Warn($"Dependency '{dep.Key}' for '{plugin.Id}' previously ignored; ignoring '{plugin.Id}'");
  749. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  750. {
  751. ReasonText = $"Dependency '{dep.Key}' ignored",
  752. RelatedTo = depMeta
  753. });
  754. ignored = true;
  755. return;
  756. }
  757. // make a point to propagate disabled
  758. if (depDisabled)
  759. {
  760. Logger.loader.Warn($"Dependency '{dep.Key}' for '{plugin.Id}' disabled; disabling");
  761. disabledPlugins!.Add(plugin);
  762. _ = disabledIds!.Add(plugin.Id);
  763. disabled = true;
  764. }
  765. // we found our dep, lets save the metadata and keep going
  766. _ = plugin.Dependencies.Add(depMeta);
  767. }
  768. // make sure the plugin depends on the loader (assuming it actually needs to)
  769. if (!dependsOnSelf && !plugin.IsSelf && !plugin.IsBare)
  770. {
  771. Logger.loader.Warn($"Plugin '{plugin.Id}' does not depend on any particular loader version; assuming its incompatible");
  772. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  773. {
  774. ReasonText = "Does not depend on any loader version, so it is assumed to be incompatible",
  775. RelatedTo = SelfMeta
  776. });
  777. ignored = true;
  778. return;
  779. }
  780. // exit early if we've decided we need to be disabled
  781. if (disabled)
  782. return;
  783. // handle LoadsAfter populated by Features processing
  784. foreach (var loadAfter in plugin.LoadsAfter)
  785. {
  786. if (TryResolveId(loadAfter.Id, out _, out _, out _))
  787. {
  788. // do nothing, because the plugin is already in the LoadsAfter set
  789. }
  790. }
  791. // then handle loadafters
  792. foreach (var id in plugin.Manifest.LoadAfter)
  793. {
  794. if (TryResolveId(id, out var meta, out var depDisabled, out var depIgnored) && !depIgnored)
  795. {
  796. // we only want to make sure to loadafter if its not ignored
  797. // if its disabled, we still wanna track it where possible
  798. _ = plugin.LoadsAfter.Add(meta);
  799. }
  800. }
  801. // after we handle dependencies and loadafters, then check conflicts
  802. foreach (var conflict in plugin.Manifest.Conflicts)
  803. {
  804. if (TryResolveId(conflict.Key, out var meta, out var conflDisabled, out var conflIgnored) && !conflIgnored && !conflDisabled)
  805. {
  806. // the conflict is only *actually* a problem if it is both not ignored and not disabled
  807. Logger.loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.Version}; ignoring '{plugin.Id}'");
  808. ignoredPlugins.Add(plugin, new(Reason.Conflict)
  809. {
  810. ReasonText = $"Conflicts with {meta.Id}@{meta.Version}",
  811. RelatedTo = meta
  812. });
  813. ignored = true;
  814. return;
  815. }
  816. }
  817. // we can now load the current plugin
  818. outputOrder!.Add(plugin);
  819. // then we can handle loadbefores
  820. foreach (var id in plugin.Manifest.LoadBefore)
  821. {
  822. if (TryResolveId(id, out var meta, out var depDisabled, out var depIgnored) && !depIgnored)
  823. {
  824. // same logic as with loadafters
  825. // both loadafter and loadbefore get condensed to just LoadsAfter in memory, for simplicity's sake
  826. _ = meta.LoadsAfter.Add(plugin);
  827. }
  828. }
  829. }
  830. // run TryResolveId over every plugin, which recursively calculates load order
  831. foreach (var plugin in pluginsToProcess)
  832. {
  833. _ = TryResolveId(plugin.Id, out _, out _, out _);
  834. }
  835. // by this point, outputOrder contains the full load order
  836. }
  837. DisabledConfig.Instance.Changed();
  838. DisabledPlugins = disabledPlugins;
  839. PluginsMetadata = outputOrder;
  840. }
  841. internal static void InitFeatures()
  842. {
  843. foreach (var meta in PluginsMetadata)
  844. {
  845. foreach (var feature in meta.Manifest.Features.Select(f => new Feature.Instance(meta, f.Key, f.Value)))
  846. {
  847. if (feature.TryGetDefiningPlugin(out var plugin) && plugin == null)
  848. { // this is a DefineFeature, so we want to initialize it early
  849. if (!feature.TryCreate(out var inst))
  850. {
  851. Logger.features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}");
  852. }
  853. else
  854. {
  855. meta.InternalFeatures.Add(inst);
  856. }
  857. }
  858. else
  859. { // this is literally any other feature, so we want to delay its initialization
  860. _ = meta.UnloadedFeatures.Add(feature);
  861. }
  862. }
  863. }
  864. // at this point we have pre-initialized all features, so we can go ahead and use them to add stuff to the dep resolver
  865. foreach (var meta in PluginsMetadata)
  866. {
  867. foreach (var feature in meta.UnloadedFeatures)
  868. {
  869. if (feature.TryGetDefiningPlugin(out var plugin))
  870. {
  871. if (plugin != meta && plugin != null)
  872. { // if the feature is not applied to the defining feature
  873. _ = meta.LoadsAfter.Add(plugin);
  874. }
  875. if (plugin != null)
  876. {
  877. plugin.CreateFeaturesWhenLoaded.Add(feature);
  878. }
  879. }
  880. else
  881. {
  882. Logger.features.Warn($"No such feature {feature.Name}");
  883. }
  884. }
  885. }
  886. }
  887. internal static void ReleaseAll(bool full = false)
  888. {
  889. if (full)
  890. {
  891. ignoredPlugins = new();
  892. }
  893. else
  894. {
  895. foreach (var m in PluginsMetadata)
  896. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  897. foreach (var m in ignoredPlugins.Keys)
  898. { // clean them up so we can still use the metadata for updates
  899. m.InternalFeatures.Clear();
  900. m.PluginType = null;
  901. m.Assembly = null!;
  902. }
  903. }
  904. PluginsMetadata = new List<PluginMetadata>();
  905. DisabledPlugins = new List<PluginMetadata>();
  906. Feature.Reset();
  907. GC.Collect();
  908. GC.WaitForPendingFinalizers();
  909. }
  910. internal static void Load(PluginMetadata meta)
  911. {
  912. if (meta is { Assembly: null, PluginType: not null })
  913. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  914. }
  915. internal static PluginExecutor? InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  916. {
  917. if (meta.Manifest.GameVersion != UnityGame.GameVersion)
  918. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  919. if (meta.IsSelf)
  920. return new PluginExecutor(meta, PluginExecutor.Special.Self);
  921. foreach (var dep in meta.Dependencies)
  922. {
  923. if (alreadyLoaded.Contains(dep)) continue;
  924. // otherwise...
  925. if (ignoredPlugins.TryGetValue(dep, out var reason))
  926. { // was added to the ignore list
  927. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  928. {
  929. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  930. RelatedTo = dep
  931. });
  932. }
  933. else
  934. { // was not added to ignore list
  935. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  936. {
  937. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  938. RelatedTo = dep
  939. });
  940. }
  941. return null;
  942. }
  943. if (meta.IsBare)
  944. return new PluginExecutor(meta, PluginExecutor.Special.Bare);
  945. Load(meta);
  946. PluginExecutor exec;
  947. try
  948. {
  949. exec = new PluginExecutor(meta);
  950. }
  951. catch (Exception e)
  952. {
  953. Logger.loader.Error($"Error creating executor for {meta.Name}");
  954. Logger.loader.Error(e);
  955. return null;
  956. }
  957. foreach (var feature in meta.Features)
  958. {
  959. try
  960. {
  961. feature.BeforeInit(meta);
  962. }
  963. catch (Exception e)
  964. {
  965. Logger.loader.Critical($"Feature errored in {nameof(Feature.BeforeInit)}:");
  966. Logger.loader.Critical(e);
  967. }
  968. }
  969. try
  970. {
  971. exec.Create();
  972. }
  973. catch (Exception e)
  974. {
  975. Logger.loader.Error($"Could not init plugin {meta.Name}");
  976. Logger.loader.Error(e);
  977. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  978. {
  979. ReasonText = "Error ocurred while initializing",
  980. Error = e
  981. });
  982. return null;
  983. }
  984. // TODO: make this new features system behave better wrt DynamicInit plugins
  985. foreach (var feature in meta.CreateFeaturesWhenLoaded)
  986. {
  987. if (!feature.TryCreate(out var inst))
  988. {
  989. Logger.features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}");
  990. }
  991. else
  992. {
  993. feature.AppliedTo.InternalFeatures.Add(inst);
  994. _ = feature.AppliedTo.UnloadedFeatures.Remove(feature);
  995. }
  996. }
  997. meta.CreateFeaturesWhenLoaded.Clear(); // if a plugin is loaded twice, for the moment, we don't want to create the feature twice
  998. foreach (var feature in meta.Features)
  999. try
  1000. {
  1001. feature.AfterInit(meta, exec.Instance);
  1002. }
  1003. catch (Exception e)
  1004. {
  1005. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}:");
  1006. Logger.loader.Critical(e);
  1007. }
  1008. return exec;
  1009. }
  1010. internal static bool IsFirstLoadComplete { get; private set; } = false;
  1011. internal static List<PluginExecutor> LoadPlugins()
  1012. {
  1013. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  1014. var list = new List<PluginExecutor>();
  1015. var loaded = new HashSet<PluginMetadata>();
  1016. foreach (var meta in PluginsMetadata)
  1017. {
  1018. try
  1019. {
  1020. var exec = InitPlugin(meta, loaded);
  1021. if (exec != null)
  1022. {
  1023. list.Add(exec);
  1024. _ = loaded.Add(meta);
  1025. }
  1026. }
  1027. catch (Exception e)
  1028. {
  1029. Logger.log.Critical($"Uncaught exception while loading pluign {meta.Name}:");
  1030. Logger.log.Critical(e);
  1031. }
  1032. }
  1033. // TODO: should this be somewhere else?
  1034. IsFirstLoadComplete = true;
  1035. return list;
  1036. }
  1037. }
  1038. }