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.

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