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.

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