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.

841 lines
34 KiB

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