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.

1029 lines
44 KiB

3 years ago
3 years ago
3 years ago
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. using Hive.Versioning;
  22. #if NET4
  23. using Task = System.Threading.Tasks.Task;
  24. using TaskEx = System.Threading.Tasks.Task;
  25. #endif
  26. #if NET3
  27. using Net3_Proxy;
  28. using Path = Net3_Proxy.Path;
  29. using File = Net3_Proxy.File;
  30. using Directory = Net3_Proxy.Directory;
  31. #endif
  32. namespace IPA.Loader
  33. {
  34. /// <summary>
  35. /// A type to manage the loading of plugins.
  36. /// </summary>
  37. internal partial class PluginLoader
  38. {
  39. internal static PluginMetadata SelfMeta = null!;
  40. internal static Task LoadTask() =>
  41. TaskEx.Run(() =>
  42. {
  43. YeetIfNeeded();
  44. var sw = Stopwatch.StartNew();
  45. LoadMetadata();
  46. sw.Stop();
  47. Logger.loader.Info($"Loading metadata took {sw.Elapsed}");
  48. sw.Reset();
  49. sw.Start();
  50. // Features contribute to load order considerations
  51. InitFeatures();
  52. DoOrderResolution();
  53. sw.Stop();
  54. Logger.loader.Info($"Calculating load order took {sw.Elapsed}");
  55. });
  56. internal static void YeetIfNeeded()
  57. {
  58. string pluginDir = UnityGame.PluginsPath;
  59. if (SelfConfig.YeetMods_ && UnityGame.IsGameVersionBoundary)
  60. {
  61. var oldPluginsName = Path.Combine(UnityGame.InstallPath, $"Old {UnityGame.OldVersion} Plugins");
  62. var newPluginsName = Path.Combine(UnityGame.InstallPath, $"Old {UnityGame.GameVersion} Plugins");
  63. if (Directory.Exists(oldPluginsName))
  64. Directory.Delete(oldPluginsName, true);
  65. Directory.Move(pluginDir, oldPluginsName);
  66. if (Directory.Exists(newPluginsName))
  67. Directory.Move(newPluginsName, pluginDir);
  68. else
  69. _ = Directory.CreateDirectory(pluginDir);
  70. }
  71. }
  72. internal static List<PluginMetadata> PluginsMetadata = new();
  73. internal static List<PluginMetadata> DisabledPlugins = new();
  74. private static readonly Regex embeddedTextDescriptionPattern = new(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  75. internal static void LoadMetadata()
  76. {
  77. string[] plugins = Directory.GetFiles(UnityGame.PluginsPath, "*.dll");
  78. try
  79. {
  80. var selfMeta = new PluginMetadata
  81. {
  82. Assembly = Assembly.GetExecutingAssembly(),
  83. File = new FileInfo(Path.Combine(UnityGame.InstallPath, "IPA.exe")),
  84. PluginType = null,
  85. IsSelf = true
  86. };
  87. string manifest;
  88. using (var manifestReader =
  89. new StreamReader(
  90. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  91. throw new InvalidOperationException()))
  92. manifest = manifestReader.ReadToEnd();
  93. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  94. PluginsMetadata.Add(selfMeta);
  95. SelfMeta = selfMeta;
  96. }
  97. catch (Exception e)
  98. {
  99. Logger.loader.Critical("Error loading own manifest");
  100. Logger.loader.Critical(e);
  101. }
  102. using var resolver = new CecilLibLoader();
  103. resolver.AddSearchDirectory(UnityGame.LibraryPath);
  104. resolver.AddSearchDirectory(UnityGame.PluginsPath);
  105. foreach (var plugin in plugins)
  106. {
  107. var metadata = new PluginMetadata
  108. {
  109. File = new FileInfo(Path.Combine(UnityGame.PluginsPath, plugin)),
  110. IsSelf = false
  111. };
  112. try
  113. {
  114. var scanResult = AntiMalwareEngine.Engine.ScanFile(metadata.File);
  115. if (scanResult is ScanResult.Detected)
  116. {
  117. Logger.loader.Warn($"Scan of {plugin} found malware; not loading");
  118. continue;
  119. }
  120. if (!SelfConfig.AntiMalware_.RunPartialThreatCode_ && scanResult is not ScanResult.KnownSafe and not ScanResult.NotDetected)
  121. {
  122. Logger.loader.Warn($"Scan of {plugin} found partial threat; not loading. To load this, set AntiMalware.RunPartialThreatCode in the config.");
  123. continue;
  124. }
  125. var pluginModule = AssemblyDefinition.ReadAssembly(metadata.File.FullName, new ReaderParameters
  126. {
  127. ReadingMode = ReadingMode.Immediate,
  128. ReadWrite = false,
  129. AssemblyResolver = resolver
  130. }).MainModule;
  131. string pluginNs = "";
  132. PluginManifest? pluginManifest = null;
  133. foreach (var resource in pluginModule.Resources)
  134. {
  135. const string manifestSuffix = ".manifest.json";
  136. if (resource is not EmbeddedResource embedded ||
  137. !embedded.Name.EndsWith(manifestSuffix, StringComparison.Ordinal)) continue;
  138. pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length);
  139. string manifest;
  140. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  141. manifest = manifestReader.ReadToEnd();
  142. pluginManifest = JsonConvert.DeserializeObject<PluginManifest?>(manifest);
  143. break;
  144. }
  145. if (pluginManifest == null)
  146. {
  147. #if DIRE_LOADER_WARNINGS
  148. Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}");
  149. #else
  150. Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}");
  151. #endif
  152. continue;
  153. }
  154. if (pluginManifest.Id == null)
  155. {
  156. Logger.loader.Warn($"Plugin '{pluginManifest.Name}' does not have a listed ID, using name");
  157. pluginManifest.Id = pluginManifest.Name;
  158. }
  159. metadata.Manifest = pluginManifest;
  160. bool TryPopulatePluginType(TypeDefinition type, PluginMetadata meta)
  161. {
  162. if (!type.HasCustomAttributes)
  163. return false;
  164. var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName);
  165. if (attr is null)
  166. return false;
  167. if (!attr.HasConstructorArguments)
  168. {
  169. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments");
  170. return false;
  171. }
  172. var args = attr.ConstructorArguments;
  173. if (args.Count != 1)
  174. {
  175. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments");
  176. return false;
  177. }
  178. var rtOptionsArg = args[0];
  179. if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName)
  180. {
  181. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}");
  182. return false;
  183. }
  184. var rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions
  185. meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt;
  186. meta.PluginType = type;
  187. return true;
  188. }
  189. void TryGetNamespacedPluginType(string ns, PluginMetadata meta)
  190. {
  191. foreach (var type in pluginModule.Types)
  192. {
  193. if (type.Namespace != ns) continue;
  194. if (TryPopulatePluginType(type, meta))
  195. return;
  196. }
  197. }
  198. var hint = metadata.Manifest.Misc?.PluginMainHint;
  199. if (hint != null)
  200. {
  201. var type = pluginModule.GetType(hint);
  202. if (type == null || !TryPopulatePluginType(type, metadata))
  203. TryGetNamespacedPluginType(hint, metadata);
  204. }
  205. if (metadata.PluginType == null)
  206. TryGetNamespacedPluginType(pluginNs, metadata);
  207. if (metadata.PluginType == null)
  208. {
  209. Logger.loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}");
  210. continue;
  211. }
  212. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  213. PluginsMetadata.Add(metadata);
  214. }
  215. catch (Exception e)
  216. {
  217. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  218. Logger.loader.Error(e);
  219. ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error)
  220. {
  221. ReasonText = "An error occurred loading the data",
  222. Error = e
  223. });
  224. }
  225. }
  226. IEnumerable<string> bareManifests = Directory.GetFiles(UnityGame.PluginsPath, "*.json");
  227. bareManifests = bareManifests.Concat(Directory.GetFiles(UnityGame.PluginsPath, "*.manifest"));
  228. foreach (var manifest in bareManifests)
  229. {
  230. try
  231. {
  232. var metadata = new PluginMetadata
  233. {
  234. File = new FileInfo(Path.Combine(UnityGame.PluginsPath, manifest)),
  235. IsSelf = false,
  236. IsBare = true,
  237. };
  238. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  239. if (metadata.Manifest.Files.Length < 1)
  240. Logger.loader.Warn($"Bare manifest {Path.GetFileName(manifest)} does not declare any files. " +
  241. $"Dependency resolution and verification cannot be completed.");
  242. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  243. PluginsMetadata.Add(metadata);
  244. }
  245. catch (Exception e)
  246. {
  247. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  248. Logger.loader.Error(e);
  249. }
  250. }
  251. foreach (var meta in PluginsMetadata)
  252. { // process description include
  253. var lines = meta.Manifest.Description.Split('\n');
  254. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  255. if (m.Success)
  256. {
  257. if (meta.IsBare)
  258. {
  259. Logger.loader.Warn($"Bare manifest cannot specify description file");
  260. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  261. continue;
  262. }
  263. var name = m.Groups[1].Value;
  264. string description;
  265. if (!meta.IsSelf)
  266. {
  267. // plugin type must be non-null for non-self plugins
  268. var resc = meta.PluginType!.Module.Resources.Select(r => r as EmbeddedResource)
  269. .NonNull()
  270. .FirstOrDefault(r => r.Name == name);
  271. if (resc == null)
  272. {
  273. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  274. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  275. continue;
  276. }
  277. using var reader = new StreamReader(resc.GetResourceStream());
  278. description = reader.ReadToEnd();
  279. }
  280. else
  281. {
  282. using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name));
  283. description = descriptionReader.ReadToEnd();
  284. }
  285. meta.Manifest.Description = description;
  286. }
  287. }
  288. }
  289. }
  290. #region Ignore stuff
  291. /// <summary>
  292. /// An enum that represents several categories of ignore reasons that the loader may encounter.
  293. /// </summary>
  294. /// <seealso cref="IgnoreReason"/>
  295. public enum Reason
  296. {
  297. /// <summary>
  298. /// An error was thrown either loading plugin information from disk, or when initializing the plugin.
  299. /// </summary>
  300. /// <remarks>
  301. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  302. /// <see cref="IgnoreReason.Error"/> will contain the thrown exception.
  303. /// </remarks>
  304. Error,
  305. /// <summary>
  306. /// The plugin this reason is associated with has the same ID as another plugin whose information was
  307. /// already loaded.
  308. /// </summary>
  309. /// <remarks>
  310. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  311. /// <see cref="IgnoreReason.RelatedTo"/> will contain the metadata of the already loaded plugin.
  312. /// </remarks>
  313. Duplicate,
  314. /// <summary>
  315. /// The plugin this reason is associated with conflicts with another already loaded plugin.
  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 plugin it conflicts with.
  320. /// </remarks>
  321. Conflict,
  322. /// <summary>
  323. /// The plugin this reason is associated with is missing a dependency.
  324. /// </summary>
  325. /// <remarks>
  326. /// Since this is only given when a dependency is missing, <see cref="IgnoreReason.RelatedTo"/> will
  327. /// not be set.
  328. /// </remarks>
  329. Dependency,
  330. /// <summary>
  331. /// The plugin this reason is associated with was released for a game update, but is still considered
  332. /// present for the purposes of updating.
  333. /// </summary>
  334. Released,
  335. /// <summary>
  336. /// The plugin this reason is associated with was denied from loading by a <see cref="Features.Feature"/>
  337. /// that it marks.
  338. /// </summary>
  339. Feature,
  340. /// <summary>
  341. /// The plugin this reason is associated with is unsupported.
  342. /// </summary>
  343. /// <remarks>
  344. /// Currently, there is no path in the loader that emits this <see cref="Reason"/>, however there may
  345. /// be in the future.
  346. /// </remarks>
  347. Unsupported,
  348. /// <summary>
  349. /// One of the files that a plugin declared in its manifest is missing.
  350. /// </summary>
  351. MissingFiles
  352. }
  353. /// <summary>
  354. /// A structure describing the reason that a plugin was ignored.
  355. /// </summary>
  356. public struct IgnoreReason : IEquatable<IgnoreReason>
  357. {
  358. /// <summary>
  359. /// Gets the ignore reason, as represented by the <see cref="Loader.Reason"/> enum.
  360. /// </summary>
  361. public Reason Reason { get; }
  362. /// <summary>
  363. /// Gets the textual description of the particular ignore reason. This will typically
  364. /// include details about why the plugin was ignored, if it is present.
  365. /// </summary>
  366. public string? ReasonText { get; internal set; }
  367. /// <summary>
  368. /// Gets the <see cref="Exception"/> that caused this plugin to be ignored, if any.
  369. /// </summary>
  370. public Exception? Error { get; internal set; }
  371. /// <summary>
  372. /// Gets the metadata of the plugin that this ignore was related to, if any.
  373. /// </summary>
  374. public PluginMetadata? RelatedTo { get; internal set; }
  375. /// <summary>
  376. /// Initializes an <see cref="IgnoreReason"/> with the provided data.
  377. /// </summary>
  378. /// <param name="reason">the <see cref="Loader.Reason"/> enum value that describes this reason</param>
  379. /// <param name="reasonText">the textual description of this ignore reason, if any</param>
  380. /// <param name="error">the <see cref="Exception"/> that caused this <see cref="IgnoreReason"/>, if any</param>
  381. /// <param name="relatedTo">the <see cref="PluginMetadata"/> this reason is related to, if any</param>
  382. public IgnoreReason(Reason reason, string? reasonText = null, Exception? error = null, PluginMetadata? relatedTo = null)
  383. {
  384. Reason = reason;
  385. ReasonText = reasonText;
  386. Error = error;
  387. RelatedTo = relatedTo;
  388. }
  389. /// <inheritdoc/>
  390. public override bool Equals(object obj)
  391. => obj is IgnoreReason ir && Equals(ir);
  392. /// <summary>
  393. /// Compares this <see cref="IgnoreReason"/> with <paramref name="other"/> for equality.
  394. /// </summary>
  395. /// <param name="other">the reason to compare to</param>
  396. /// <returns><see langword="true"/> if the two reasons compare equal, <see langword="false"/> otherwise</returns>
  397. public bool Equals(IgnoreReason other)
  398. => Reason == other.Reason && ReasonText == other.ReasonText
  399. && Error == other.Error && RelatedTo == other.RelatedTo;
  400. /// <inheritdoc/>
  401. public override int GetHashCode()
  402. {
  403. int hashCode = 778404373;
  404. hashCode = (hashCode * -1521134295) + Reason.GetHashCode();
  405. hashCode = (hashCode * -1521134295) + ReasonText?.GetHashCode() ?? 0;
  406. hashCode = (hashCode * -1521134295) + Error?.GetHashCode() ?? 0;
  407. hashCode = (hashCode * -1521134295) + RelatedTo?.GetHashCode() ?? 0;
  408. return hashCode;
  409. }
  410. /// <summary>
  411. /// Checks if two <see cref="IgnoreReason"/>s are equal.
  412. /// </summary>
  413. /// <param name="left">the first <see cref="IgnoreReason"/> to compare</param>
  414. /// <param name="right">the second <see cref="IgnoreReason"/> to compare</param>
  415. /// <returns><see langword="true"/> if the two reasons compare equal, <see langword="false"/> otherwise</returns>
  416. public static bool operator ==(IgnoreReason left, IgnoreReason right)
  417. => left.Equals(right);
  418. /// <summary>
  419. /// Checks if two <see cref="IgnoreReason"/>s are not 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 are not equal, <see langword="false"/> otherwise</returns>
  424. public static bool operator !=(IgnoreReason left, IgnoreReason right)
  425. => !(left == right);
  426. }
  427. #endregion
  428. internal partial class PluginLoader
  429. {
  430. // keep track of these for the updater; it should still be able to update mods not loaded
  431. // the thing -> the reason
  432. internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new();
  433. internal static void DoOrderResolution()
  434. {
  435. #if DEBUG
  436. // print starting order
  437. Logger.loader.Debug(string.Join(", ", PluginsMetadata.StrJP()));
  438. #endif
  439. PluginsMetadata.Sort((a, b) => b.HVersion.CompareTo(a.HVersion));
  440. #if DEBUG
  441. // print base resolution order
  442. Logger.loader.Debug(string.Join(", ", PluginsMetadata.StrJP()));
  443. #endif
  444. var metadataCache = new Dictionary<string, (PluginMetadata Meta, bool Enabled)>(PluginsMetadata.Count);
  445. var pluginsToProcess = new List<PluginMetadata>(PluginsMetadata.Count);
  446. var disabledIds = DisabledConfig.Instance.DisabledModIds;
  447. var disabledPlugins = new List<PluginMetadata>();
  448. // build metadata cache
  449. foreach (var meta in PluginsMetadata)
  450. {
  451. if (!metadataCache.TryGetValue(meta.Id, out var existing))
  452. {
  453. if (disabledIds.Contains(meta.Id))
  454. {
  455. metadataCache.Add(meta.Id, (meta, false));
  456. disabledPlugins.Add(meta);
  457. }
  458. else
  459. {
  460. metadataCache.Add(meta.Id, (meta, true));
  461. pluginsToProcess.Add(meta);
  462. }
  463. }
  464. else
  465. {
  466. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  467. ignoredPlugins.Add(meta, new(Reason.Duplicate)
  468. {
  469. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  470. RelatedTo = existing.Meta
  471. });
  472. }
  473. }
  474. // preprocess LoadBefore into LoadAfter
  475. foreach (var (_, (meta, _)) in metadataCache)
  476. { // we iterate the metadata cache because it contains both disabled and enabled plugins
  477. var loadBefore = meta.Manifest.LoadBefore;
  478. foreach (var id in loadBefore)
  479. {
  480. if (metadataCache.TryGetValue(id, out var plugin))
  481. {
  482. // if the id exists in our metadata cache, make sure it knows to load after the plugin in kvp
  483. _ = plugin.Meta.LoadsAfter.Add(meta);
  484. }
  485. }
  486. }
  487. // preprocess conflicts to be mutual
  488. foreach (var (_, (meta, _)) in metadataCache)
  489. {
  490. foreach (var (id, range) in meta.Manifest.Conflicts)
  491. {
  492. if (metadataCache.TryGetValue(id, out var plugin)
  493. && range.Matches(plugin.Meta.HVersion))
  494. {
  495. // make sure that there's a mutual dependency
  496. var targetRange = VersionRange.ForVersion(meta.HVersion);
  497. var targetConflicts = plugin.Meta.Manifest.Conflicts;
  498. if (!targetConflicts.TryGetValue(meta.Id, out var realRange))
  499. {
  500. // there's not already a listed conflict
  501. targetConflicts.Add(meta.Id, targetRange);
  502. }
  503. else if (!realRange.Matches(meta.HVersion))
  504. {
  505. // there is already a listed conflict that isn't mutual
  506. targetRange = realRange | targetRange;
  507. targetConflicts[meta.Id] = targetRange;
  508. }
  509. }
  510. }
  511. }
  512. var loadedPlugins = new Dictionary<string, (PluginMetadata Meta, bool Disabled, bool Ignored)>();
  513. var outputOrder = new List<PluginMetadata>(PluginsMetadata.Count);
  514. var isProcessing = new HashSet<PluginMetadata>();
  515. {
  516. bool TryResolveId(string id, [MaybeNullWhen(false)] out PluginMetadata meta, out bool disabled, out bool ignored, bool partial = false)
  517. {
  518. meta = null;
  519. disabled = false;
  520. ignored = true;
  521. Logger.loader.Trace($"Trying to resolve plugin '{id}' partial:{partial}");
  522. if (loadedPlugins.TryGetValue(id, out var foundMeta))
  523. {
  524. meta = foundMeta.Meta;
  525. disabled = foundMeta.Disabled;
  526. ignored = foundMeta.Ignored;
  527. Logger.loader.Trace($"- Found already processed");
  528. return true;
  529. }
  530. if (metadataCache!.TryGetValue(id, out var plugin))
  531. {
  532. Logger.loader.Trace($"- In metadata cache");
  533. if (partial)
  534. {
  535. Logger.loader.Trace($" - but requested in a partial lookup");
  536. return false;
  537. }
  538. disabled = !plugin.Enabled;
  539. meta = plugin.Meta;
  540. if (!disabled)
  541. {
  542. try
  543. {
  544. ignored = false;
  545. Resolve(plugin.Meta, ref disabled, out ignored);
  546. }
  547. catch (Exception e)
  548. {
  549. if (e is not DependencyResolutionLoopException)
  550. {
  551. Logger.loader.Error($"While performing load order resolution for {id}:");
  552. Logger.loader.Error(e);
  553. }
  554. if (!ignored)
  555. {
  556. ignoredPlugins.Add(plugin.Meta, new(Reason.Error)
  557. {
  558. Error = e
  559. });
  560. }
  561. ignored = true;
  562. }
  563. }
  564. if (!loadedPlugins.ContainsKey(id))
  565. {
  566. // this condition is specifically for when we fail resolution because of a graph loop
  567. Logger.loader.Trace($"- '{id}' resolved as ignored:{ignored},disabled:{disabled}");
  568. loadedPlugins.Add(id, (plugin.Meta, disabled, ignored));
  569. }
  570. return true;
  571. }
  572. Logger.loader.Trace($"- Not found");
  573. return false;
  574. }
  575. void Resolve(PluginMetadata plugin, ref bool disabled, out bool ignored)
  576. {
  577. Logger.loader.Trace($">Resolving '{plugin.Name}'");
  578. // first we need to check for loops in the resolution graph to prevent stack overflows
  579. if (isProcessing.Contains(plugin))
  580. {
  581. Logger.loader.Error($"Loop detected while processing '{plugin.Name}'; flagging as ignored");
  582. throw new DependencyResolutionLoopException();
  583. }
  584. isProcessing.Add(plugin);
  585. using var _removeProcessing = Utils.ScopeGuard(() => isProcessing.Remove(plugin));
  586. // if this method is being called, this is the first and only time that it has been called for this plugin.
  587. ignored = false;
  588. // perform file existence check before attempting to load dependencies
  589. foreach (var file in plugin.AssociatedFiles)
  590. {
  591. if (!file.Exists)
  592. {
  593. ignoredPlugins.Add(plugin, new IgnoreReason(Reason.MissingFiles)
  594. {
  595. ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} does not exist"
  596. });
  597. Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" +
  598. $" (declared by '{plugin.Name}') does not exist! Mod installation is incomplete, not loading it.");
  599. ignored = true;
  600. return;
  601. }
  602. }
  603. // first load dependencies
  604. var dependsOnSelf = false;
  605. foreach (var (id, range) in plugin.Manifest.Dependencies)
  606. {
  607. if (id == SelfMeta.Id)
  608. dependsOnSelf = true;
  609. if (!TryResolveId(id, out var depMeta, out var depDisabled, out var depIgnored)
  610. || !range.Matches(depMeta.HVersion))
  611. {
  612. Logger.loader.Warn($"'{plugin.Id}' is missing dependency '{id}@{range}'; ignoring");
  613. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  614. {
  615. ReasonText = $"Dependency '{id}@{range}' not found",
  616. });
  617. ignored = true;
  618. return;
  619. }
  620. // make a point to propagate ignored
  621. if (depIgnored)
  622. {
  623. Logger.loader.Warn($"Dependency '{id}' for '{plugin.Id}' previously ignored; ignoring '{plugin.Id}'");
  624. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  625. {
  626. ReasonText = $"Dependency '{id}' ignored",
  627. RelatedTo = depMeta
  628. });
  629. ignored = true;
  630. return;
  631. }
  632. // make a point to propagate disabled
  633. if (depDisabled)
  634. {
  635. Logger.loader.Warn($"Dependency '{id}' for '{plugin.Id}' disabled; disabling");
  636. disabledPlugins!.Add(plugin);
  637. _ = disabledIds!.Add(plugin.Id);
  638. disabled = true;
  639. }
  640. // we found our dep, lets save the metadata and keep going
  641. _ = plugin.Dependencies.Add(depMeta);
  642. }
  643. // make sure the plugin depends on the loader (assuming it actually needs to)
  644. if (!dependsOnSelf && !plugin.IsSelf && !plugin.IsBare)
  645. {
  646. Logger.loader.Warn($"Plugin '{plugin.Id}' does not depend on any particular loader version; assuming its incompatible");
  647. ignoredPlugins.Add(plugin, new(Reason.Dependency)
  648. {
  649. ReasonText = "Does not depend on any loader version, so it is assumed to be incompatible",
  650. RelatedTo = SelfMeta
  651. });
  652. ignored = true;
  653. return;
  654. }
  655. // exit early if we've decided we need to be disabled
  656. if (disabled)
  657. return;
  658. // handle LoadsAfter populated by Features processing
  659. foreach (var loadAfter in plugin.LoadsAfter)
  660. {
  661. if (TryResolveId(loadAfter.Id, out _, out _, out _))
  662. {
  663. // do nothing, because the plugin is already in the LoadsAfter set
  664. }
  665. }
  666. // then handle loadafters
  667. foreach (var id in plugin.Manifest.LoadAfter)
  668. {
  669. if (TryResolveId(id, out var meta, out var depDisabled, out var depIgnored))
  670. {
  671. // we only want to make sure to loadafter if its not ignored
  672. // if its disabled, we still wanna track it where possible
  673. _ = plugin.LoadsAfter.Add(meta);
  674. }
  675. }
  676. // after we handle dependencies and loadafters, then check conflicts
  677. foreach (var (id, range) in plugin.Manifest.Conflicts)
  678. {
  679. Logger.loader.Trace($">- Checking conflict '{id}' {range}");
  680. // this lookup must be partial to prevent loadBefore/conflictsWith from creating a recursion loop
  681. if (TryResolveId(id, out var meta, out var conflDisabled, out var conflIgnored, partial: true)
  682. && range.Matches(meta.HVersion)
  683. && !conflIgnored && !conflDisabled) // the conflict is only *actually* a problem if it is both not ignored and not disabled
  684. {
  685. Logger.loader.Warn($"Plugin '{plugin.Id}' conflicts with {meta.Id}@{meta.HVersion}; ignoring '{plugin.Id}'");
  686. ignoredPlugins.Add(plugin, new(Reason.Conflict)
  687. {
  688. ReasonText = $"Conflicts with {meta.Id}@{meta.HVersion}",
  689. RelatedTo = meta
  690. });
  691. ignored = true;
  692. return;
  693. }
  694. }
  695. // specifically check if some strange stuff happened (like graph loops) causing this to be ignored
  696. // from some other invocation
  697. if (!ignoredPlugins.ContainsKey(plugin))
  698. {
  699. // we can now load the current plugin
  700. Logger.loader.Trace($"->'{plugin.Name}' loads here");
  701. outputOrder!.Add(plugin);
  702. }
  703. // loadbefores have already been preprocessed into loadafters
  704. Logger.loader.Trace($">Processed '{plugin.Name}'");
  705. }
  706. // run TryResolveId over every plugin, which recursively calculates load order
  707. foreach (var plugin in pluginsToProcess)
  708. {
  709. _ = TryResolveId(plugin.Id, out _, out _, out _);
  710. }
  711. // by this point, outputOrder contains the full load order
  712. }
  713. DisabledConfig.Instance.Changed();
  714. DisabledPlugins = disabledPlugins;
  715. PluginsMetadata = outputOrder;
  716. }
  717. internal static void InitFeatures()
  718. {
  719. foreach (var meta in PluginsMetadata)
  720. {
  721. foreach (var feature in meta.Manifest.Features
  722. .SelectMany(f => f.Value.Select(o => (f.Key, o)))
  723. .Select(t => new Feature.Instance(meta, t.Key, t.o)))
  724. {
  725. if (feature.TryGetDefiningPlugin(out var plugin) && plugin == null)
  726. { // this is a DefineFeature, so we want to initialize it early
  727. if (!feature.TryCreate(out var inst))
  728. {
  729. Logger.features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}");
  730. }
  731. else
  732. {
  733. meta.InternalFeatures.Add(inst);
  734. }
  735. }
  736. else
  737. { // this is literally any other feature, so we want to delay its initialization
  738. _ = meta.UnloadedFeatures.Add(feature);
  739. }
  740. }
  741. }
  742. // at this point we have pre-initialized all features, so we can go ahead and use them to add stuff to the dep resolver
  743. foreach (var meta in PluginsMetadata)
  744. {
  745. foreach (var feature in meta.UnloadedFeatures)
  746. {
  747. if (feature.TryGetDefiningPlugin(out var plugin))
  748. {
  749. if (plugin != meta && plugin != null)
  750. { // if the feature is not applied to the defining feature
  751. _ = meta.LoadsAfter.Add(plugin);
  752. }
  753. if (plugin != null)
  754. {
  755. plugin.CreateFeaturesWhenLoaded.Add(feature);
  756. }
  757. }
  758. else
  759. {
  760. Logger.features.Warn($"No such feature {feature.Name}");
  761. }
  762. }
  763. }
  764. }
  765. internal static void ReleaseAll(bool full = false)
  766. {
  767. if (full)
  768. {
  769. ignoredPlugins = new();
  770. }
  771. else
  772. {
  773. foreach (var m in PluginsMetadata)
  774. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  775. foreach (var m in ignoredPlugins.Keys)
  776. { // clean them up so we can still use the metadata for updates
  777. m.InternalFeatures.Clear();
  778. m.PluginType = null;
  779. m.Assembly = null!;
  780. }
  781. }
  782. PluginsMetadata = new List<PluginMetadata>();
  783. DisabledPlugins = new List<PluginMetadata>();
  784. Feature.Reset();
  785. GC.Collect();
  786. GC.WaitForPendingFinalizers();
  787. }
  788. internal static void Load(PluginMetadata meta)
  789. {
  790. if (meta is { Assembly: null, PluginType: not null })
  791. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  792. }
  793. internal static PluginExecutor? InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  794. {
  795. if (meta.Manifest.GameVersion is { } gv && gv != UnityGame.GameVersion)
  796. Logger.loader.Warn($"Mod {meta.Name} developed for game version {gv}, so it may not work properly.");
  797. if (meta.IsSelf)
  798. return new PluginExecutor(meta, PluginExecutor.Special.Self);
  799. foreach (var dep in meta.Dependencies)
  800. {
  801. if (alreadyLoaded.Contains(dep)) continue;
  802. // otherwise...
  803. if (ignoredPlugins.TryGetValue(dep, out var reason))
  804. { // was added to the ignore list
  805. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  806. {
  807. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  808. RelatedTo = dep
  809. });
  810. }
  811. else
  812. { // was not added to ignore list
  813. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  814. {
  815. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  816. RelatedTo = dep
  817. });
  818. }
  819. return null;
  820. }
  821. if (meta.IsBare)
  822. return new PluginExecutor(meta, PluginExecutor.Special.Bare);
  823. Load(meta);
  824. PluginExecutor exec;
  825. try
  826. {
  827. exec = new PluginExecutor(meta);
  828. }
  829. catch (Exception e)
  830. {
  831. Logger.loader.Error($"Error creating executor for {meta.Name}");
  832. Logger.loader.Error(e);
  833. return null;
  834. }
  835. foreach (var feature in meta.Features)
  836. {
  837. try
  838. {
  839. feature.BeforeInit(meta);
  840. }
  841. catch (Exception e)
  842. {
  843. Logger.loader.Critical($"Feature errored in {nameof(Feature.BeforeInit)}:");
  844. Logger.loader.Critical(e);
  845. }
  846. }
  847. try
  848. {
  849. exec.Create();
  850. }
  851. catch (Exception e)
  852. {
  853. Logger.loader.Error($"Could not init plugin {meta.Name}");
  854. Logger.loader.Error(e);
  855. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  856. {
  857. ReasonText = "Error occurred while initializing",
  858. Error = e
  859. });
  860. return null;
  861. }
  862. // TODO: make this new features system behave better wrt DynamicInit plugins
  863. foreach (var feature in meta.CreateFeaturesWhenLoaded)
  864. {
  865. if (!feature.TryCreate(out var inst))
  866. {
  867. Logger.features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}");
  868. }
  869. else
  870. {
  871. feature.AppliedTo.InternalFeatures.Add(inst);
  872. _ = feature.AppliedTo.UnloadedFeatures.Remove(feature);
  873. }
  874. }
  875. meta.CreateFeaturesWhenLoaded.Clear(); // if a plugin is loaded twice, for the moment, we don't want to create the feature twice
  876. foreach (var feature in meta.Features)
  877. try
  878. {
  879. feature.AfterInit(meta, exec.Instance);
  880. }
  881. catch (Exception e)
  882. {
  883. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}:");
  884. Logger.loader.Critical(e);
  885. }
  886. return exec;
  887. }
  888. internal static bool IsFirstLoadComplete { get; private set; }
  889. internal static List<PluginExecutor> LoadPlugins()
  890. {
  891. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  892. var list = new List<PluginExecutor>();
  893. var loaded = new HashSet<PluginMetadata>();
  894. foreach (var meta in PluginsMetadata)
  895. {
  896. try
  897. {
  898. var exec = InitPlugin(meta, loaded);
  899. if (exec != null)
  900. {
  901. list.Add(exec);
  902. _ = loaded.Add(meta);
  903. }
  904. }
  905. catch (Exception e)
  906. {
  907. Logger.log.Critical($"Uncaught exception while loading plugin {meta.Name}:");
  908. Logger.log.Critical(e);
  909. }
  910. }
  911. // TODO: should this be somewhere else?
  912. IsFirstLoadComplete = true;
  913. return list;
  914. }
  915. }
  916. }