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.

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