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.

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