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.

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