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.

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