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.

935 lines
38 KiB

  1. #nullable enable
  2. using IPA.Config;
  3. using IPA.Loader.Features;
  4. using IPA.Logging;
  5. using IPA.Utilities;
  6. using Mono.Cecil;
  7. using Newtonsoft.Json;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.IO;
  11. using System.Linq;
  12. using System.Reflection;
  13. using System.Text.RegularExpressions;
  14. using System.Threading.Tasks;
  15. using Version = SemVer.Version;
  16. using SemVer;
  17. #if NET4
  18. using Task = System.Threading.Tasks.Task;
  19. using TaskEx = System.Threading.Tasks.Task;
  20. #endif
  21. #if NET3
  22. using Net3_Proxy;
  23. using Path = Net3_Proxy.Path;
  24. using File = Net3_Proxy.File;
  25. using Directory = Net3_Proxy.Directory;
  26. #endif
  27. namespace IPA.Loader
  28. {
  29. /// <summary>
  30. /// A type to manage the loading of plugins.
  31. /// </summary>
  32. internal partial class PluginLoader
  33. {
  34. internal static PluginMetadata SelfMeta = null!;
  35. internal static Task LoadTask() =>
  36. TaskEx.Run(() =>
  37. {
  38. YeetIfNeeded();
  39. LoadMetadata();
  40. // old loader system
  41. #if false
  42. Resolve();
  43. InitFeatures();
  44. ComputeLoadOrder();
  45. FilterDisabled();
  46. FilterWithoutFiles();
  47. ResolveDependencies();
  48. #endif
  49. });
  50. internal static void YeetIfNeeded()
  51. {
  52. string pluginDir = UnityGame.PluginsPath;
  53. if (SelfConfig.YeetMods_ && UnityGame.IsGameVersionBoundary)
  54. {
  55. var oldPluginsName = Path.Combine(UnityGame.InstallPath, $"Old {UnityGame.OldVersion} Plugins");
  56. var newPluginsName = Path.Combine(UnityGame.InstallPath, $"Old {UnityGame.GameVersion} Plugins");
  57. if (Directory.Exists(oldPluginsName))
  58. Directory.Delete(oldPluginsName, true);
  59. Directory.Move(pluginDir, oldPluginsName);
  60. if (Directory.Exists(newPluginsName))
  61. Directory.Move(newPluginsName, pluginDir);
  62. else
  63. Directory.CreateDirectory(pluginDir);
  64. }
  65. }
  66. internal static List<PluginMetadata> PluginsMetadata = new();
  67. internal static List<PluginMetadata> DisabledPlugins = new();
  68. private static readonly Regex embeddedTextDescriptionPattern = new(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  69. internal static void LoadMetadata()
  70. {
  71. string[] plugins = Directory.GetFiles(UnityGame.PluginsPath, "*.dll");
  72. try
  73. {
  74. var selfMeta = new PluginMetadata
  75. {
  76. Assembly = Assembly.GetExecutingAssembly(),
  77. File = new FileInfo(Path.Combine(UnityGame.InstallPath, "IPA.exe")),
  78. PluginType = null,
  79. IsSelf = true
  80. };
  81. string manifest;
  82. using (var manifestReader =
  83. new StreamReader(
  84. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  85. throw new InvalidOperationException()))
  86. manifest = manifestReader.ReadToEnd();
  87. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  88. PluginsMetadata.Add(selfMeta);
  89. SelfMeta = selfMeta;
  90. }
  91. catch (Exception e)
  92. {
  93. Logger.loader.Critical("Error loading own manifest");
  94. Logger.loader.Critical(e);
  95. }
  96. var resolver = new CecilLibLoader();
  97. resolver.AddSearchDirectory(UnityGame.LibraryPath);
  98. resolver.AddSearchDirectory(UnityGame.PluginsPath);
  99. foreach (var plugin in plugins)
  100. {
  101. var metadata = new PluginMetadata
  102. {
  103. File = new FileInfo(Path.Combine(UnityGame.PluginsPath, plugin)),
  104. IsSelf = false
  105. };
  106. try
  107. {
  108. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  109. {
  110. ReadingMode = ReadingMode.Immediate,
  111. ReadWrite = false,
  112. AssemblyResolver = resolver
  113. }).MainModule;
  114. string pluginNs = "";
  115. foreach (var resource in pluginModule.Resources)
  116. {
  117. const string manifestSuffix = ".manifest.json";
  118. if (resource is not EmbeddedResource embedded ||
  119. !embedded.Name.EndsWith(manifestSuffix)) continue;
  120. pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length);
  121. string manifest;
  122. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  123. manifest = manifestReader.ReadToEnd();
  124. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  125. break;
  126. }
  127. if (metadata.Manifest == null)
  128. {
  129. #if DIRE_LOADER_WARNINGS
  130. Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}");
  131. #else
  132. Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}");
  133. #endif
  134. continue;
  135. }
  136. void TryGetNamespacedPluginType(string ns, PluginMetadata meta)
  137. {
  138. foreach (var type in pluginModule.Types)
  139. {
  140. if (type.Namespace != ns) continue;
  141. if (type.HasCustomAttributes)
  142. {
  143. var attr = type.CustomAttributes.FirstOrDefault(a => a.Constructor.DeclaringType.FullName == typeof(PluginAttribute).FullName);
  144. if (attr != null)
  145. {
  146. if (!attr.HasConstructorArguments)
  147. {
  148. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has no arguments");
  149. return;
  150. }
  151. var args = attr.ConstructorArguments;
  152. if (args.Count != 1)
  153. {
  154. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but attribute has unexpected number of arguments");
  155. return;
  156. }
  157. var rtOptionsArg = args[0];
  158. if (rtOptionsArg.Type.FullName != typeof(RuntimeOptions).FullName)
  159. {
  160. Logger.loader.Warn($"Attribute plugin found in {type.FullName}, but first argument is of unexpected type {rtOptionsArg.Type.FullName}");
  161. return;
  162. }
  163. var rtOptionsValInt = (int)rtOptionsArg.Value; // `int` is the underlying type of RuntimeOptions
  164. meta.RuntimeOptions = (RuntimeOptions)rtOptionsValInt;
  165. meta.PluginType = type;
  166. return;
  167. }
  168. }
  169. }
  170. }
  171. var hint = metadata.Manifest.Misc?.PluginMainHint;
  172. if (hint != null)
  173. {
  174. var type = pluginModule.GetType(hint);
  175. if (type != null)
  176. TryGetNamespacedPluginType(hint, metadata);
  177. }
  178. if (metadata.PluginType == null)
  179. TryGetNamespacedPluginType(pluginNs, metadata);
  180. if (metadata.PluginType == null)
  181. {
  182. Logger.loader.Error($"No plugin found in the manifest {(hint != null ? $"hint path ({hint}) or " : "")}namespace ({pluginNs}) in {Path.GetFileName(plugin)}");
  183. continue;
  184. }
  185. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  186. PluginsMetadata.Add(metadata);
  187. }
  188. catch (Exception e)
  189. {
  190. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  191. Logger.loader.Error(e);
  192. ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error)
  193. {
  194. ReasonText = "An error ocurred loading the data",
  195. Error = e
  196. });
  197. }
  198. }
  199. IEnumerable<string> bareManifests = Directory.GetFiles(UnityGame.PluginsPath, "*.json");
  200. bareManifests = bareManifests.Concat(Directory.GetFiles(UnityGame.PluginsPath, "*.manifest"));
  201. foreach (var manifest in bareManifests)
  202. {
  203. try
  204. {
  205. var metadata = new PluginMetadata
  206. {
  207. File = new FileInfo(Path.Combine(UnityGame.PluginsPath, manifest)),
  208. IsSelf = false,
  209. IsBare = true,
  210. };
  211. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  212. if (metadata.Manifest.Files.Length < 1)
  213. Logger.loader.Warn($"Bare manifest {Path.GetFileName(manifest)} does not declare any files. " +
  214. $"Dependency resolution and verification cannot be completed.");
  215. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  216. PluginsMetadata.Add(metadata);
  217. }
  218. catch (Exception e)
  219. {
  220. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  221. Logger.loader.Error(e);
  222. }
  223. }
  224. foreach (var meta in PluginsMetadata)
  225. { // process description include
  226. var lines = meta.Manifest.Description.Split('\n');
  227. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  228. if (m.Success)
  229. {
  230. if (meta.IsBare)
  231. {
  232. Logger.loader.Warn($"Bare manifest cannot specify description file");
  233. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  234. continue;
  235. }
  236. var name = m.Groups[1].Value;
  237. string description;
  238. if (!meta.IsSelf)
  239. {
  240. var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource)
  241. .NonNull()
  242. .FirstOrDefault(r => r.Name == name);
  243. if (resc == null)
  244. {
  245. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  246. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  247. continue;
  248. }
  249. using var reader = new StreamReader(resc.GetResourceStream());
  250. description = reader.ReadToEnd();
  251. }
  252. else
  253. {
  254. using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name));
  255. description = descriptionReader.ReadToEnd();
  256. }
  257. meta.Manifest.Description = description;
  258. }
  259. }
  260. }
  261. }
  262. #region Ignore stuff
  263. /// <summary>
  264. /// An enum that represents several categories of ignore reasons that the loader may encounter.
  265. /// </summary>
  266. /// <seealso cref="IgnoreReason"/>
  267. public enum Reason
  268. {
  269. /// <summary>
  270. /// An error was thrown either loading plugin information fomr disk, or when initializing the plugin.
  271. /// </summary>
  272. /// <remarks>
  273. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  274. /// <see cref="IgnoreReason.Error"/> will contain the thrown exception.
  275. /// </remarks>
  276. Error,
  277. /// <summary>
  278. /// The plugin this reason is associated with has the same ID as another plugin whose information was
  279. /// already loaded.
  280. /// </summary>
  281. /// <remarks>
  282. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  283. /// <see cref="IgnoreReason.RelatedTo"/> will contain the metadata of the already loaded plugin.
  284. /// </remarks>
  285. Duplicate,
  286. /// <summary>
  287. /// The plugin this reason is associated with conflicts with another already loaded plugin.
  288. /// </summary>
  289. /// <remarks>
  290. /// When this is the set <see cref="Reason"/> in an <see cref="IgnoreReason"/> structure, the member
  291. /// <see cref="IgnoreReason.RelatedTo"/> will contain the metadata of the plugin it conflicts with.
  292. /// </remarks>
  293. Conflict,
  294. /// <summary>
  295. /// The plugin this reason is assiciated with is missing a dependency.
  296. /// </summary>
  297. /// <remarks>
  298. /// Since this is only given when a dependency is missing, <see cref="IgnoreReason.RelatedTo"/> will
  299. /// not be set.
  300. /// </remarks>
  301. Dependency,
  302. /// <summary>
  303. /// The plugin this reason is associated with was released for a game update, but is still considered
  304. /// present for the purposes of updating.
  305. /// </summary>
  306. Released,
  307. /// <summary>
  308. /// The plugin this reason is associated with was denied from loading by a <see cref="Features.Feature"/>
  309. /// that it marks.
  310. /// </summary>
  311. Feature,
  312. /// <summary>
  313. /// The plugin this reason is assoicated with is unsupported.
  314. /// </summary>
  315. /// <remarks>
  316. /// Currently, there is no path in the loader that emits this <see cref="Reason"/>, however there may
  317. /// be in the future.
  318. /// </remarks>
  319. Unsupported,
  320. /// <summary>
  321. /// One of the files that a plugin declared in its manifest is missing.
  322. /// </summary>
  323. MissingFiles
  324. }
  325. /// <summary>
  326. /// A structure describing the reason that a plugin was ignored.
  327. /// </summary>
  328. public struct IgnoreReason
  329. {
  330. /// <summary>
  331. /// Gets the ignore reason, as represented by the <see cref="Loader.Reason"/> enum.
  332. /// </summary>
  333. public Reason Reason { get; }
  334. /// <summary>
  335. /// Gets the textual description of the particular ignore reason. This will typically
  336. /// include details about why the plugin was ignored, if it is present.
  337. /// </summary>
  338. public string? ReasonText { get; internal set; }
  339. /// <summary>
  340. /// Gets the <see cref="Exception"/> that caused this plugin to be ignored, if any.
  341. /// </summary>
  342. public Exception? Error { get; internal set; }
  343. /// <summary>
  344. /// Gets the metadata of the plugin that this ignore was related to, if any.
  345. /// </summary>
  346. public PluginMetadata? RelatedTo { get; internal set; }
  347. /// <summary>
  348. /// Initializes an <see cref="IgnoreReason"/> with the provided data.
  349. /// </summary>
  350. /// <param name="reason">the <see cref="Loader.Reason"/> enum value that describes this reason</param>
  351. /// <param name="reasonText">the textual description of this ignore reason, if any</param>
  352. /// <param name="error">the <see cref="Exception"/> that caused this <see cref="IgnoreReason"/>, if any</param>
  353. /// <param name="relatedTo">the <see cref="PluginMetadata"/> this reason is related to, if any</param>
  354. public IgnoreReason(Reason reason, string? reasonText = null, Exception? error = null, PluginMetadata? relatedTo = null)
  355. {
  356. Reason = reason;
  357. ReasonText = reasonText;
  358. Error = error;
  359. RelatedTo = relatedTo;
  360. }
  361. /// <inheritdoc/>
  362. public override bool Equals(object obj)
  363. => obj is IgnoreReason ir && Equals(ir);
  364. /// <summary>
  365. /// Compares this <see cref="IgnoreReason"/> with <paramref name="other"/> for equality.
  366. /// </summary>
  367. /// <param name="other">the reason to compare to</param>
  368. /// <returns><see langword="true"/> if the two reasons compare equal, <see langword="false"/> otherwise</returns>
  369. public bool Equals(IgnoreReason other)
  370. => Reason == other.Reason && ReasonText == other.ReasonText
  371. && Error == other.Error && RelatedTo == other.RelatedTo;
  372. /// <inheritdoc/>
  373. public override int GetHashCode()
  374. {
  375. int hashCode = 778404373;
  376. hashCode = (hashCode * -1521134295) + Reason.GetHashCode();
  377. hashCode = (hashCode * -1521134295) + ReasonText?.GetHashCode() ?? 0;
  378. hashCode = (hashCode * -1521134295) + Error?.GetHashCode() ?? 0;
  379. hashCode = (hashCode * -1521134295) + RelatedTo?.GetHashCode() ?? 0;
  380. return hashCode;
  381. }
  382. /// <summary>
  383. /// Checks if two <see cref="IgnoreReason"/>s are equal.
  384. /// </summary>
  385. /// <param name="left">the first <see cref="IgnoreReason"/> to compare</param>
  386. /// <param name="right">the second <see cref="IgnoreReason"/> to compare</param>
  387. /// <returns><see langword="true"/> if the two reasons compare equal, <see langword="false"/> otherwise</returns>
  388. public static bool operator ==(IgnoreReason left, IgnoreReason right)
  389. => left.Equals(right);
  390. /// <summary>
  391. /// Checks if two <see cref="IgnoreReason"/>s are not equal.
  392. /// </summary>
  393. /// <param name="left">the first <see cref="IgnoreReason"/> to compare</param>
  394. /// <param name="right">the second <see cref="IgnoreReason"/> to compare</param>
  395. /// <returns><see langword="true"/> if the two reasons are not equal, <see langword="false"/> otherwise</returns>
  396. public static bool operator !=(IgnoreReason left, IgnoreReason right)
  397. => !(left == right);
  398. }
  399. #endregion
  400. internal partial class PluginLoader
  401. {
  402. // keep track of these for the updater; it should still be able to update mods not loaded
  403. // the thing -> the reason
  404. internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new();
  405. #if false
  406. internal static void Resolve()
  407. { // resolves duplicates and conflicts, etc
  408. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  409. var ids = new HashSet<string>();
  410. var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
  411. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  412. foreach (var meta in PluginsMetadata)
  413. {
  414. if (meta.Id != null)
  415. {
  416. if (ids.Contains(meta.Id))
  417. {
  418. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  419. var ireason = new IgnoreReason(Reason.Duplicate)
  420. {
  421. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  422. RelatedTo = resolved.First(p => p.Id == meta.Id)
  423. };
  424. ignore.Add(meta, ireason);
  425. ignoredPlugins.Add(meta, ireason);
  426. continue; // because of sorted order, hightest order will always be the first one
  427. }
  428. bool processedLater = false;
  429. foreach (var meta2 in PluginsMetadata)
  430. {
  431. if (ignore.ContainsKey(meta2)) continue;
  432. if (meta == meta2)
  433. {
  434. processedLater = true;
  435. continue;
  436. }
  437. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  438. var range = meta2.Manifest.Conflicts[meta.Id];
  439. if (!range.IsSatisfied(meta.Version)) continue;
  440. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}");
  441. if (processedLater)
  442. {
  443. Logger.loader.Warn($"Ignoring {meta2.Name}");
  444. ignore.Add(meta2, new IgnoreReason(Reason.Conflict)
  445. {
  446. ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}",
  447. RelatedTo = meta
  448. });
  449. }
  450. else
  451. {
  452. Logger.loader.Warn($"Ignoring {meta.Name}");
  453. ignore.Add(meta, new IgnoreReason(Reason.Conflict)
  454. {
  455. ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}",
  456. RelatedTo = meta2
  457. });
  458. break;
  459. }
  460. }
  461. }
  462. if (ignore.TryGetValue(meta, out var reason))
  463. {
  464. ignoredPlugins.Add(meta, reason);
  465. continue;
  466. }
  467. if (meta.Id != null)
  468. ids.Add(meta.Id);
  469. resolved.Add(meta);
  470. }
  471. PluginsMetadata = resolved;
  472. }
  473. private static void FilterDisabled()
  474. {
  475. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  476. var disabled = DisabledConfig.Instance.DisabledModIds;
  477. foreach (var meta in PluginsMetadata)
  478. {
  479. if (disabled.Contains(meta.Id ?? meta.Name))
  480. DisabledPlugins.Add(meta);
  481. else
  482. enabled.Add(meta);
  483. }
  484. PluginsMetadata = enabled;
  485. }
  486. private static void FilterWithoutFiles()
  487. {
  488. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  489. foreach (var meta in PluginsMetadata)
  490. {
  491. var passed = true;
  492. foreach (var file in meta.AssociatedFiles)
  493. {
  494. if (!file.Exists)
  495. {
  496. passed = false;
  497. ignoredPlugins.Add(meta, new IgnoreReason(Reason.MissingFiles)
  498. {
  499. ReasonText = $"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)} (declared by {meta.Name}) does not exist"
  500. });
  501. Logger.loader.Warn($"File {Utils.GetRelativePath(file.FullName, UnityGame.InstallPath)}" +
  502. $" (declared by {meta.Name}) does not exist! Mod installation is incomplete, not loading it.");
  503. break;
  504. }
  505. }
  506. if (passed)
  507. enabled.Add(meta);
  508. }
  509. PluginsMetadata = enabled;
  510. }
  511. internal static void ComputeLoadOrder()
  512. {
  513. #if DEBUG
  514. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  515. #endif
  516. static bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  517. { // this is slow, and hella recursive
  518. bool inserted = false;
  519. foreach (var sr in root)
  520. {
  521. inserted = inserted || InsertInto(sr.Dependencies, meta);
  522. if (meta.Id != null)
  523. {
  524. if (sr.Manifest.Dependencies.ContainsKey(meta.Id))
  525. inserted = inserted || sr.Dependencies.Add(meta);
  526. else if (sr.Manifest.LoadAfter.Contains(meta.Id))
  527. inserted = inserted || sr.LoadsAfter.Add(meta);
  528. }
  529. if (sr.Id != null)
  530. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  531. inserted = inserted || sr.LoadsAfter.Add(meta);
  532. }
  533. if (isRoot)
  534. {
  535. foreach (var sr in root)
  536. {
  537. InsertInto(meta.Dependencies, sr);
  538. if (sr.Id != null)
  539. {
  540. if (meta.Manifest.Dependencies.ContainsKey(sr.Id))
  541. meta.Dependencies.Add(sr);
  542. else if (meta.Manifest.LoadAfter.Contains(sr.Id))
  543. meta.LoadsAfter.Add(sr);
  544. }
  545. if (meta.Id != null)
  546. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  547. meta.LoadsAfter.Add(sr);
  548. }
  549. root.Add(meta);
  550. }
  551. return inserted;
  552. }
  553. var pluginTree = new HashSet<PluginMetadata>();
  554. foreach (var meta in PluginsMetadata)
  555. InsertInto(pluginTree, meta, true);
  556. static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  557. {
  558. foreach (var st in tree)
  559. if (!into.Contains(st))
  560. {
  561. DeTree(into, st.Dependencies);
  562. DeTree(into, st.LoadsAfter);
  563. into.Add(st);
  564. }
  565. }
  566. PluginsMetadata = new List<PluginMetadata>();
  567. DeTree(PluginsMetadata, pluginTree);
  568. #if DEBUG
  569. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  570. #endif
  571. }
  572. internal static void ResolveDependencies()
  573. {
  574. var metadata = new List<PluginMetadata>();
  575. var pluginsToLoad = new Dictionary<string, Version>();
  576. var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
  577. foreach (var meta in PluginsMetadata)
  578. {
  579. bool ignoreBcNoLoader = true;
  580. var missingDeps = new List<(string id, Range version, bool disabled)>();
  581. foreach (var dep in meta.Manifest.Dependencies)
  582. {
  583. #if DEBUG
  584. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  585. #endif
  586. if (dep.Key == SelfMeta.Id)
  587. ignoreBcNoLoader = false;
  588. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  589. continue;
  590. if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key]))
  591. {
  592. Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
  593. missingDeps.Add((dep.Key, dep.Value, true));
  594. }
  595. else
  596. {
  597. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  598. missingDeps.Add((dep.Key, dep.Value, false));
  599. }
  600. }
  601. if (meta.PluginType != null && !meta.IsSelf && !meta.IsBare && ignoreBcNoLoader)
  602. {
  603. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  604. {
  605. ReasonText = "BSIPA Plugin does not reference BSIPA!"
  606. });
  607. for (int i = 0; i < 20; i++)
  608. {
  609. Logger.loader.Warn($"HEY {meta.Id} YOU DEPEND ON BSIPA SO DEPEND ON BSIPA");
  610. }
  611. continue;
  612. }
  613. if (missingDeps.Count == 0)
  614. {
  615. metadata.Add(meta);
  616. if (meta.Id != null)
  617. pluginsToLoad.Add(meta.Id, meta.Version);
  618. }
  619. else if (missingDeps.Any(t => !t.disabled))
  620. { // missing deps
  621. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  622. {
  623. ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}"
  624. });
  625. }
  626. else
  627. {
  628. DisabledPlugins.Add(meta);
  629. DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
  630. }
  631. }
  632. DisabledConfig.Instance.Changed();
  633. PluginsMetadata = metadata;
  634. }
  635. #endif
  636. internal static void InitFeatures()
  637. {
  638. foreach (var meta in PluginsMetadata)
  639. {
  640. foreach (var feature in meta.Manifest.Features.Select(f => new Feature.Instance(meta, f.Key, f.Value)))
  641. {
  642. if (feature.TryGetDefiningPlugin(out var plugin) && plugin == null)
  643. { // this is a DefineFeature, so we want to initialize it early
  644. if (!feature.TryCreate(out var inst))
  645. {
  646. Logger.features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}");
  647. }
  648. else
  649. {
  650. meta.InternalFeatures.Add(inst);
  651. }
  652. }
  653. else
  654. { // this is literally any other feature, so we want to delay its initialization
  655. _ = meta.UnloadedFeatures.Add(feature);
  656. }
  657. }
  658. }
  659. // at this point we have pre-initialized all features, so we can go ahead and use them to add stuff to the dep resolver
  660. foreach (var meta in PluginsMetadata)
  661. {
  662. foreach (var feature in meta.UnloadedFeatures)
  663. {
  664. if (feature.TryGetDefiningPlugin(out var plugin))
  665. {
  666. if (plugin != meta)
  667. { // if the feature is not applied to the defining feature
  668. _ = meta.LoadsAfter.Add(plugin);
  669. }
  670. if (plugin != null)
  671. {
  672. plugin.CreateFeaturesWhenLoaded.Add(feature);
  673. }
  674. }
  675. else
  676. {
  677. Logger.features.Warn($"No such feature {feature.Name}");
  678. }
  679. }
  680. }
  681. }
  682. internal static void ReleaseAll(bool full = false)
  683. {
  684. if (full)
  685. ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  686. else
  687. {
  688. foreach (var m in PluginsMetadata)
  689. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  690. foreach (var m in ignoredPlugins.Keys)
  691. { // clean them up so we can still use the metadata for updates
  692. m.InternalFeatures.Clear();
  693. m.PluginType = null;
  694. m.Assembly = null;
  695. }
  696. }
  697. PluginsMetadata = new List<PluginMetadata>();
  698. DisabledPlugins = new List<PluginMetadata>();
  699. Feature.Reset();
  700. GC.Collect();
  701. }
  702. internal static void Load(PluginMetadata meta)
  703. {
  704. if (meta is { Assembly: null, PluginType: not null })
  705. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  706. }
  707. internal static PluginExecutor? InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  708. {
  709. if (meta.Manifest.GameVersion != UnityGame.GameVersion)
  710. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  711. if (meta.IsSelf)
  712. return new PluginExecutor(meta, PluginExecutor.Special.Self);
  713. foreach (var dep in meta.Dependencies)
  714. {
  715. if (alreadyLoaded.Contains(dep)) continue;
  716. // otherwise...
  717. if (ignoredPlugins.TryGetValue(dep, out var reason))
  718. { // was added to the ignore list
  719. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  720. {
  721. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  722. RelatedTo = dep
  723. });
  724. }
  725. else
  726. { // was not added to ignore list
  727. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  728. {
  729. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  730. RelatedTo = dep
  731. });
  732. }
  733. return null;
  734. }
  735. if (meta.IsBare)
  736. return new PluginExecutor(meta, PluginExecutor.Special.Bare);
  737. Load(meta);
  738. PluginExecutor exec;
  739. try
  740. {
  741. exec = new PluginExecutor(meta);
  742. }
  743. catch (Exception e)
  744. {
  745. Logger.loader.Error($"Error creating executor for {meta.Name}");
  746. Logger.loader.Error(e);
  747. return null;
  748. }
  749. foreach (var feature in meta.Features)
  750. {
  751. try
  752. {
  753. feature.BeforeInit(meta);
  754. }
  755. catch (Exception e)
  756. {
  757. Logger.loader.Critical($"Feature errored in {nameof(Feature.BeforeInit)}:");
  758. Logger.loader.Critical(e);
  759. }
  760. }
  761. try
  762. {
  763. exec.Create();
  764. }
  765. catch (Exception e)
  766. {
  767. Logger.loader.Error($"Could not init plugin {meta.Name}");
  768. Logger.loader.Error(e);
  769. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  770. {
  771. ReasonText = "Error ocurred while initializing",
  772. Error = e
  773. });
  774. return null;
  775. }
  776. // TODO: make this new features system behave better wrt DynamicInit plugins
  777. foreach (var feature in meta.CreateFeaturesWhenLoaded)
  778. {
  779. if (!feature.TryCreate(out var inst))
  780. {
  781. Logger.features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}");
  782. }
  783. else
  784. {
  785. feature.AppliedTo.InternalFeatures.Add(inst);
  786. _ = feature.AppliedTo.UnloadedFeatures.Remove(feature);
  787. }
  788. }
  789. meta.CreateFeaturesWhenLoaded.Clear(); // if a plugin is loaded twice, for the moment, we don't want to create the feature twice
  790. foreach (var feature in meta.Features)
  791. try
  792. {
  793. feature.AfterInit(meta, exec.Instance);
  794. }
  795. catch (Exception e)
  796. {
  797. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}:");
  798. Logger.loader.Critical(e);
  799. }
  800. return exec;
  801. }
  802. internal static bool IsFirstLoadComplete { get; private set; } = false;
  803. internal static List<PluginExecutor> LoadPlugins()
  804. {
  805. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  806. var list = new List<PluginExecutor>();
  807. var loaded = new HashSet<PluginMetadata>();
  808. foreach (var meta in PluginsMetadata)
  809. {
  810. try
  811. {
  812. var exec = InitPlugin(meta, loaded);
  813. if (exec != null)
  814. {
  815. list.Add(exec);
  816. _ = loaded.Add(meta);
  817. }
  818. }
  819. catch (Exception e)
  820. {
  821. Logger.log.Critical($"Uncaught exception while loading pluign {meta.Name}:");
  822. Logger.log.Critical(e);
  823. }
  824. }
  825. // TODO: should this be somewhere else?
  826. IsFirstLoadComplete = true;
  827. return list;
  828. }
  829. }
  830. }