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.

923 lines
38 KiB

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