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.

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