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.

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