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.

804 lines
31 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. public class PluginLoader
  32. {
  33. internal static Task LoadTask() =>
  34. TaskEx.Run(() =>
  35. {
  36. YeetIfNeeded();
  37. LoadMetadata();
  38. Resolve();
  39. ComputeLoadOrder();
  40. FilterDisabled();
  41. ResolveDependencies();
  42. });
  43. /// <summary>
  44. /// A class which describes a loaded plugin.
  45. /// </summary>
  46. public class PluginMetadata
  47. {
  48. /// <summary>
  49. /// The assembly the plugin was loaded from.
  50. /// </summary>
  51. /// <value>the loaded Assembly that contains the plugin main type</value>
  52. public Assembly Assembly { get; internal set; }
  53. /// <summary>
  54. /// The TypeDefinition for the main type of the plugin.
  55. /// </summary>
  56. /// <value>the Cecil definition for the plugin main type</value>
  57. public TypeDefinition PluginType { get; internal set; }
  58. /// <summary>
  59. /// The human readable name of the plugin.
  60. /// </summary>
  61. /// <value>the name of the plugin</value>
  62. public string Name { get; internal set; }
  63. /// <summary>
  64. /// The BeatMods ID of the plugin, or null if it doesn't have one.
  65. /// </summary>
  66. /// <value>the updater ID of the plugin</value>
  67. public string Id { get; internal set; }
  68. /// <summary>
  69. /// The version of the plugin.
  70. /// </summary>
  71. /// <value>the version of the plugin</value>
  72. public Version Version { get; internal set; }
  73. /// <summary>
  74. /// The file the plugin was loaded from.
  75. /// </summary>
  76. /// <value>the file the plugin was loaded from</value>
  77. public FileInfo File { get; internal set; }
  78. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  79. /// <summary>
  80. /// The features this plugin requests.
  81. /// </summary>
  82. /// <value>the list of features requested by the plugin</value>
  83. public IReadOnlyList<Feature> Features => InternalFeatures;
  84. internal readonly List<Feature> InternalFeatures = new List<Feature>();
  85. internal bool IsSelf;
  86. /// <summary>
  87. /// Whether or not this metadata object represents a bare manifest.
  88. /// </summary>
  89. /// <value><see langword="true"/> if it is bare, <see langword="false"/> otherwise</value>
  90. public bool IsBare { get; internal set; }
  91. private PluginManifest manifest;
  92. internal HashSet<PluginMetadata> Dependencies { get; } = new HashSet<PluginMetadata>();
  93. internal PluginManifest Manifest
  94. {
  95. get => manifest;
  96. set
  97. {
  98. manifest = value;
  99. Name = value.Name;
  100. Version = value.Version;
  101. Id = value.Id;
  102. }
  103. }
  104. /// <summary>
  105. /// Gets all of the metadata as a readable string.
  106. /// </summary>
  107. /// <returns>the readable printable metadata string</returns>
  108. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
  109. }
  110. /// <summary>
  111. /// A container object for all the data relating to a plugin.
  112. /// </summary>
  113. public class PluginInfo
  114. {
  115. internal IPlugin Plugin { get; set; }
  116. /// <summary>
  117. /// Metadata for the plugin.
  118. /// </summary>
  119. /// <value>the metadata for this plugin</value>
  120. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  121. }
  122. internal static void YeetIfNeeded()
  123. {
  124. string pluginDir = BeatSaber.PluginsPath;
  125. var gameVer = BeatSaber.GameVersion;
  126. var lastVerS = SelfConfig.LastGameVersion_;
  127. var lastVer = lastVerS != null ? new AlmostVersion(lastVerS, gameVer) : null;
  128. if (SelfConfig.YeetMods_ && lastVer != null && gameVer != lastVer)
  129. {
  130. var oldPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {lastVer} Plugins");
  131. var newPluginsName = Path.Combine(BeatSaber.InstallPath, $"Old {gameVer} Plugins");
  132. if (Directory.Exists(oldPluginsName))
  133. Directory.Delete(oldPluginsName, true);
  134. Directory.Move(pluginDir, oldPluginsName);
  135. if (Directory.Exists(newPluginsName))
  136. Directory.Move(newPluginsName, pluginDir);
  137. else
  138. Directory.CreateDirectory(pluginDir);
  139. }
  140. SelfConfig.Instance.LastGameVersion = gameVer.ToString();
  141. }
  142. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  143. internal static List<PluginMetadata> DisabledPlugins = new List<PluginMetadata>();
  144. private static readonly Regex embeddedTextDescriptionPattern = new Regex(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  145. internal static void LoadMetadata()
  146. {
  147. string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
  148. try
  149. {
  150. var selfMeta = new PluginMetadata
  151. {
  152. Assembly = Assembly.GetExecutingAssembly(),
  153. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  154. PluginType = null,
  155. IsSelf = true
  156. };
  157. string manifest;
  158. using (var manifestReader =
  159. new StreamReader(
  160. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  161. throw new InvalidOperationException()))
  162. manifest = manifestReader.ReadToEnd();
  163. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  164. PluginsMetadata.Add(selfMeta);
  165. }
  166. catch (Exception e)
  167. {
  168. Logger.loader.Critical("Error loading own manifest");
  169. Logger.loader.Critical(e);
  170. }
  171. foreach (var plugin in plugins)
  172. {
  173. var metadata = new PluginMetadata
  174. {
  175. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)),
  176. IsSelf = false
  177. };
  178. try
  179. {
  180. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  181. {
  182. ReadingMode = ReadingMode.Immediate,
  183. ReadWrite = false,
  184. AssemblyResolver = new CecilLibLoader()
  185. }).MainModule;
  186. string pluginNs = "";
  187. foreach (var resource in pluginModule.Resources)
  188. {
  189. const string manifestSuffix = ".manifest.json";
  190. if (!(resource is EmbeddedResource embedded) ||
  191. !embedded.Name.EndsWith(manifestSuffix)) continue;
  192. pluginNs = embedded.Name.Substring(0, embedded.Name.Length - manifestSuffix.Length);
  193. string manifest;
  194. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  195. manifest = manifestReader.ReadToEnd();
  196. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  197. break;
  198. }
  199. if (metadata.Manifest == null)
  200. {
  201. #if DIRE_LOADER_WARNINGS
  202. Logger.loader.Error($"Could not find manifest.json for {Path.GetFileName(plugin)}");
  203. #else
  204. Logger.loader.Notice($"No manifest.json in {Path.GetFileName(plugin)}");
  205. #endif
  206. continue;
  207. }
  208. foreach (var type in pluginModule.Types)
  209. {
  210. if (type.Namespace != pluginNs) continue;
  211. if (type.HasInterface(typeof(IPlugin).FullName))
  212. {
  213. metadata.PluginType = type;
  214. break;
  215. }
  216. }
  217. if (metadata.PluginType == null)
  218. {
  219. Logger.loader.Error($"No plugin found in the manifest namespace ({pluginNs}) in {Path.GetFileName(plugin)}");
  220. continue;
  221. }
  222. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  223. PluginsMetadata.Add(metadata);
  224. }
  225. catch (Exception e)
  226. {
  227. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  228. Logger.loader.Error(e);
  229. ignoredPlugins.Add(metadata, new IgnoreReason(Reason.Error)
  230. {
  231. ReasonText = "An error ocurred loading the data",
  232. Error = e
  233. });
  234. }
  235. }
  236. IEnumerable<string> bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json");
  237. bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest"));
  238. foreach (var manifest in bareManifests)
  239. { // TODO: maybe find a way to allow a bare manifest to specify an associated file
  240. try
  241. {
  242. var metadata = new PluginMetadata
  243. {
  244. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, manifest)),
  245. IsSelf = false,
  246. IsBare = true,
  247. };
  248. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  249. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  250. PluginsMetadata.Add(metadata);
  251. }
  252. catch (Exception e)
  253. {
  254. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  255. Logger.loader.Error(e);
  256. }
  257. }
  258. foreach (var meta in PluginsMetadata)
  259. { // process description include
  260. var lines = meta.Manifest.Description.Split('\n');
  261. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  262. if (m.Success)
  263. {
  264. if (meta.IsBare)
  265. {
  266. Logger.loader.Warn($"Bare manifest cannot specify description file");
  267. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  268. continue;
  269. }
  270. var name = m.Groups[1].Value;
  271. string description;
  272. if (!meta.IsSelf)
  273. {
  274. var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource)
  275. .NonNull()
  276. .FirstOrDefault(r => r.Name == name);
  277. if (resc == null)
  278. {
  279. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  280. meta.Manifest.Description = string.Join("\n", lines.Skip(1).StrJP()); // ignore first line
  281. continue;
  282. }
  283. using var reader = new StreamReader(resc.GetResourceStream());
  284. description = reader.ReadToEnd();
  285. }
  286. else
  287. {
  288. using var descriptionReader = new StreamReader(meta.Assembly.GetManifestResourceStream(name));
  289. description = descriptionReader.ReadToEnd();
  290. }
  291. meta.Manifest.Description = description;
  292. }
  293. }
  294. }
  295. internal enum Reason
  296. {
  297. Error, Duplicate, Conflict, Dependency,
  298. Released,
  299. Feature
  300. }
  301. internal struct IgnoreReason
  302. {
  303. public Reason Reason { get; }
  304. public string ReasonText { get; set; }
  305. public Exception Error { get; set; }
  306. public PluginMetadata RelatedTo { get; set; }
  307. public IgnoreReason(Reason reason)
  308. {
  309. Reason = reason;
  310. ReasonText = null;
  311. Error = null;
  312. RelatedTo = null;
  313. }
  314. }
  315. // keep track of these for the updater; it should still be able to update mods not loaded
  316. // TODO: add ignore reason
  317. // the thing -> the reason
  318. internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  319. internal static void Resolve()
  320. { // resolves duplicates and conflicts, etc
  321. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  322. var ids = new HashSet<string>();
  323. var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
  324. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  325. foreach (var meta in PluginsMetadata)
  326. {
  327. if (meta.Id != null)
  328. {
  329. if (ids.Contains(meta.Id))
  330. {
  331. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  332. var ireason = new IgnoreReason(Reason.Duplicate)
  333. {
  334. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  335. RelatedTo = resolved.First(p => p.Id == meta.Id)
  336. };
  337. ignore.Add(meta, ireason);
  338. ignoredPlugins.Add(meta, ireason);
  339. continue; // because of sorted order, hightest order will always be the first one
  340. }
  341. bool processedLater = false;
  342. foreach (var meta2 in PluginsMetadata)
  343. {
  344. if (ignore.ContainsKey(meta2)) continue;
  345. if (meta == meta2)
  346. {
  347. processedLater = true;
  348. continue;
  349. }
  350. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  351. var range = meta2.Manifest.Conflicts[meta.Id];
  352. if (!range.IsSatisfied(meta.Version)) continue;
  353. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}");
  354. if (processedLater)
  355. {
  356. Logger.loader.Warn($"Ignoring {meta2.Name}");
  357. ignore.Add(meta2, new IgnoreReason(Reason.Conflict)
  358. {
  359. ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}",
  360. RelatedTo = meta
  361. });
  362. }
  363. else
  364. {
  365. Logger.loader.Warn($"Ignoring {meta.Name}");
  366. ignore.Add(meta, new IgnoreReason(Reason.Conflict)
  367. {
  368. ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}",
  369. RelatedTo = meta2
  370. });
  371. break;
  372. }
  373. }
  374. }
  375. if (ignore.TryGetValue(meta, out var reason))
  376. {
  377. ignoredPlugins.Add(meta, reason);
  378. continue;
  379. }
  380. if (meta.Id != null)
  381. ids.Add(meta.Id);
  382. resolved.Add(meta);
  383. }
  384. PluginsMetadata = resolved;
  385. }
  386. private static void FilterDisabled()
  387. {
  388. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  389. var disabled = DisabledConfig.Instance.DisabledModIds;
  390. foreach (var meta in PluginsMetadata)
  391. {
  392. if (disabled.Contains(meta.Id ?? meta.Name))
  393. DisabledPlugins.Add(meta);
  394. else
  395. enabled.Add(meta);
  396. }
  397. PluginsMetadata = enabled;
  398. }
  399. internal static void ComputeLoadOrder()
  400. {
  401. #if DEBUG
  402. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  403. #endif
  404. static bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  405. { // this is slow, and hella recursive
  406. bool inserted = false;
  407. foreach (var sr in root)
  408. {
  409. inserted = inserted || InsertInto(sr.Dependencies, meta);
  410. if (meta.Id != null)
  411. if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id))
  412. inserted = inserted || sr.Dependencies.Add(meta);
  413. if (sr.Id != null)
  414. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  415. inserted = inserted || sr.Dependencies.Add(meta);
  416. }
  417. if (isRoot)
  418. {
  419. foreach (var sr in root)
  420. {
  421. InsertInto(meta.Dependencies, sr);
  422. if (sr.Id != null)
  423. if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id))
  424. meta.Dependencies.Add(sr);
  425. if (meta.Id != null)
  426. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  427. meta.Dependencies.Add(sr);
  428. }
  429. root.Add(meta);
  430. }
  431. return inserted;
  432. }
  433. var pluginTree = new HashSet<PluginMetadata>();
  434. foreach (var meta in PluginsMetadata)
  435. InsertInto(pluginTree, meta, true);
  436. static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  437. {
  438. foreach (var st in tree)
  439. if (!into.Contains(st))
  440. {
  441. DeTree(into, st.Dependencies);
  442. into.Add(st);
  443. }
  444. }
  445. PluginsMetadata = new List<PluginMetadata>();
  446. DeTree(PluginsMetadata, pluginTree);
  447. #if DEBUG
  448. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  449. #endif
  450. }
  451. internal static void ResolveDependencies()
  452. {
  453. var metadata = new List<PluginMetadata>();
  454. var pluginsToLoad = new Dictionary<string, Version>();
  455. var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
  456. foreach (var meta in PluginsMetadata)
  457. {
  458. var missingDeps = new List<(string id, Range version, bool disabled)>();
  459. foreach (var dep in meta.Manifest.Dependencies)
  460. {
  461. #if DEBUG
  462. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  463. #endif
  464. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  465. continue;
  466. if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key]))
  467. {
  468. Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
  469. missingDeps.Add((dep.Key, dep.Value, true));
  470. }
  471. else
  472. {
  473. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  474. missingDeps.Add((dep.Key, dep.Value, false));
  475. }
  476. }
  477. if (missingDeps.Count == 0)
  478. {
  479. metadata.Add(meta);
  480. if (meta.Id != null)
  481. pluginsToLoad.Add(meta.Id, meta.Version);
  482. }
  483. else if (missingDeps.Any(t => !t.disabled))
  484. { // missing deps
  485. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  486. {
  487. ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}"
  488. });
  489. }
  490. else
  491. {
  492. DisabledPlugins.Add(meta);
  493. DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
  494. }
  495. }
  496. PluginsMetadata = metadata;
  497. }
  498. internal static void InitFeatures()
  499. {
  500. var parsedFeatures = PluginsMetadata.Select(m =>
  501. (metadata: m,
  502. features: m.Manifest.Features.Select(feature =>
  503. (feature, parsed: Ref.Create<Feature.FeatureParse?>(null))
  504. ).ToList()
  505. )
  506. ).ToList();
  507. while (DefineFeature.NewFeature)
  508. {
  509. DefineFeature.NewFeature = false;
  510. foreach (var (metadata, features) in parsedFeatures)
  511. for (var i = 0; i < features.Count; i++)
  512. {
  513. var feature = features[i];
  514. var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj,
  515. out var exception, out var valid, out var parsed, feature.parsed.Value);
  516. if (!success && !valid && featureObj == null && exception == null) // no feature of type found
  517. feature.parsed.Value = parsed;
  518. else if (success)
  519. {
  520. if (valid && featureObj.StoreOnPlugin)
  521. metadata.InternalFeatures.Add(featureObj);
  522. else if (!valid)
  523. Logger.features.Warn(
  524. $"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}");
  525. features.RemoveAt(i--);
  526. }
  527. else
  528. {
  529. Logger.features.Error($"Error parsing feature definition on {metadata.Name}");
  530. Logger.features.Error(exception);
  531. features.RemoveAt(i--);
  532. }
  533. }
  534. foreach (var plugin in PluginsMetadata)
  535. foreach (var feature in plugin.Features)
  536. feature.Evaluate();
  537. }
  538. foreach (var plugin in parsedFeatures)
  539. {
  540. if (plugin.features.Count <= 0) continue;
  541. Logger.features.Warn($"On plugin {plugin.metadata.Name}:");
  542. foreach (var feature in plugin.features)
  543. Logger.features.Warn($" Feature not found with name {feature.feature}");
  544. }
  545. }
  546. internal static void ReleaseAll(bool full = false)
  547. {
  548. if (full)
  549. ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  550. else
  551. {
  552. foreach (var m in PluginsMetadata)
  553. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  554. foreach (var m in ignoredPlugins.Keys)
  555. { // clean them up so we can still use the metadata for updates
  556. m.InternalFeatures.Clear();
  557. m.PluginType = null;
  558. m.Assembly = null;
  559. }
  560. }
  561. PluginsMetadata = new List<PluginMetadata>();
  562. DisabledPlugins = new List<PluginMetadata>();
  563. Feature.Reset();
  564. GC.Collect();
  565. }
  566. internal static void Load(PluginMetadata meta)
  567. {
  568. if (meta.Assembly == null && meta.PluginType != null)
  569. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  570. }
  571. internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  572. {
  573. if (meta.PluginType == null)
  574. return new PluginInfo()
  575. {
  576. Metadata = meta,
  577. Plugin = null
  578. };
  579. var info = new PluginInfo();
  580. if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
  581. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  582. try
  583. {
  584. foreach (var dep in meta.Dependencies)
  585. {
  586. if (alreadyLoaded.Contains(dep)) continue;
  587. // otherwise...
  588. if (ignoredPlugins.TryGetValue(dep, out var reason))
  589. { // was added to the ignore list
  590. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  591. {
  592. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  593. RelatedTo = dep
  594. });
  595. }
  596. else
  597. { // was not added to ignore list
  598. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  599. {
  600. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  601. RelatedTo = dep
  602. });
  603. }
  604. return null;
  605. }
  606. Load(meta);
  607. Feature denyingFeature = null;
  608. if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
  609. {
  610. Logger.loader.Warn(
  611. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
  612. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
  613. {
  614. ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}"
  615. });
  616. return null;
  617. }
  618. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  619. var instance = Activator.CreateInstance(type) as IPlugin;
  620. info.Metadata = meta;
  621. info.Plugin = instance;
  622. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  623. if (init != null)
  624. {
  625. denyingFeature = null;
  626. if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
  627. {
  628. Logger.loader.Warn(
  629. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
  630. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
  631. {
  632. ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}"
  633. });
  634. return null;
  635. }
  636. PluginInitInjector.Inject(init, info);
  637. }
  638. foreach (var feature in meta.Features)
  639. try
  640. {
  641. feature.AfterInit(info, info.Plugin);
  642. }
  643. catch (Exception e)
  644. {
  645. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
  646. }
  647. try // TODO: move this out to after all plugins have been inited
  648. {
  649. instance.OnEnable();
  650. }
  651. catch (Exception e)
  652. {
  653. Logger.loader.Error($"Error occurred trying to enable {meta.Name}");
  654. Logger.loader.Error(e);
  655. return null; // is enable failure a full load failure?
  656. }
  657. }
  658. catch (AmbiguousMatchException)
  659. {
  660. Logger.loader.Critical($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  661. Logger.loader.Critical("@Developer: you *really* should fix this");
  662. // not adding to ignoredPlugins here because this should only happen in a development context
  663. // if someone fucks this up on release thats on them
  664. return null;
  665. }
  666. catch (Exception e)
  667. {
  668. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  669. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  670. {
  671. ReasonText = "Error ocurred while initializing",
  672. Error = e
  673. });
  674. return null;
  675. }
  676. return info;
  677. }
  678. internal static List<PluginInfo> LoadPlugins()
  679. {
  680. InitFeatures();
  681. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  682. var list = new List<PluginInfo>();
  683. var loaded = new HashSet<PluginMetadata>();
  684. foreach (var meta in PluginsMetadata)
  685. {
  686. var info = InitPlugin(meta, loaded);
  687. if (info != null)
  688. {
  689. list.Add(info);
  690. loaded.Add(meta);
  691. }
  692. }
  693. return list;
  694. }
  695. }
  696. }