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.

864 lines
35 KiB

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