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.

798 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. // the thing -> the reason
  313. internal static Dictionary<PluginMetadata, IgnoreReason> ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  314. internal static void Resolve()
  315. { // resolves duplicates and conflicts, etc
  316. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  317. var ids = new HashSet<string>();
  318. var ignore = new Dictionary<PluginMetadata, IgnoreReason>();
  319. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  320. foreach (var meta in PluginsMetadata)
  321. {
  322. if (meta.Id != null)
  323. {
  324. if (ids.Contains(meta.Id))
  325. {
  326. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  327. var ireason = new IgnoreReason(Reason.Duplicate)
  328. {
  329. ReasonText = $"Duplicate entry of same ID ({meta.Id})",
  330. RelatedTo = resolved.First(p => p.Id == meta.Id)
  331. };
  332. ignore.Add(meta, ireason);
  333. ignoredPlugins.Add(meta, ireason);
  334. continue; // because of sorted order, hightest order will always be the first one
  335. }
  336. bool processedLater = false;
  337. foreach (var meta2 in PluginsMetadata)
  338. {
  339. if (ignore.ContainsKey(meta2)) continue;
  340. if (meta == meta2)
  341. {
  342. processedLater = true;
  343. continue;
  344. }
  345. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  346. var range = meta2.Manifest.Conflicts[meta.Id];
  347. if (!range.IsSatisfied(meta.Version)) continue;
  348. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Id}");
  349. if (processedLater)
  350. {
  351. Logger.loader.Warn($"Ignoring {meta2.Name}");
  352. ignore.Add(meta2, new IgnoreReason(Reason.Conflict)
  353. {
  354. ReasonText = $"{meta.Id}@{meta.Version} conflicts with {meta2.Id}",
  355. RelatedTo = meta
  356. });
  357. }
  358. else
  359. {
  360. Logger.loader.Warn($"Ignoring {meta.Name}");
  361. ignore.Add(meta, new IgnoreReason(Reason.Conflict)
  362. {
  363. ReasonText = $"{meta2.Id}@{meta2.Version} conflicts with {meta.Id}",
  364. RelatedTo = meta2
  365. });
  366. break;
  367. }
  368. }
  369. }
  370. if (ignore.TryGetValue(meta, out var reason))
  371. {
  372. ignoredPlugins.Add(meta, reason);
  373. continue;
  374. }
  375. if (meta.Id != null)
  376. ids.Add(meta.Id);
  377. resolved.Add(meta);
  378. }
  379. PluginsMetadata = resolved;
  380. }
  381. private static void FilterDisabled()
  382. {
  383. var enabled = new List<PluginMetadata>(PluginsMetadata.Count);
  384. var disabled = DisabledConfig.Instance.DisabledModIds;
  385. foreach (var meta in PluginsMetadata)
  386. {
  387. if (disabled.Contains(meta.Id ?? meta.Name))
  388. DisabledPlugins.Add(meta);
  389. else
  390. enabled.Add(meta);
  391. }
  392. PluginsMetadata = enabled;
  393. }
  394. internal static void ComputeLoadOrder()
  395. {
  396. #if DEBUG
  397. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  398. #endif
  399. static bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  400. { // this is slow, and hella recursive
  401. bool inserted = false;
  402. foreach (var sr in root)
  403. {
  404. inserted = inserted || InsertInto(sr.Dependencies, meta);
  405. if (meta.Id != null)
  406. if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id))
  407. inserted = inserted || sr.Dependencies.Add(meta);
  408. if (sr.Id != null)
  409. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  410. inserted = inserted || sr.Dependencies.Add(meta);
  411. }
  412. if (isRoot)
  413. {
  414. foreach (var sr in root)
  415. {
  416. InsertInto(meta.Dependencies, sr);
  417. if (sr.Id != null)
  418. if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id))
  419. meta.Dependencies.Add(sr);
  420. if (meta.Id != null)
  421. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  422. meta.Dependencies.Add(sr);
  423. }
  424. root.Add(meta);
  425. }
  426. return inserted;
  427. }
  428. var pluginTree = new HashSet<PluginMetadata>();
  429. foreach (var meta in PluginsMetadata)
  430. InsertInto(pluginTree, meta, true);
  431. static void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  432. {
  433. foreach (var st in tree)
  434. if (!into.Contains(st))
  435. {
  436. DeTree(into, st.Dependencies);
  437. into.Add(st);
  438. }
  439. }
  440. PluginsMetadata = new List<PluginMetadata>();
  441. DeTree(PluginsMetadata, pluginTree);
  442. #if DEBUG
  443. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString()).StrJP()));
  444. #endif
  445. }
  446. internal static void ResolveDependencies()
  447. {
  448. var metadata = new List<PluginMetadata>();
  449. var pluginsToLoad = new Dictionary<string, Version>();
  450. var disabledLookup = DisabledPlugins.NonNull(m => m.Id).ToDictionary(m => m.Id, m => m.Version);
  451. foreach (var meta in PluginsMetadata)
  452. {
  453. var missingDeps = new List<(string id, Range version, bool disabled)>();
  454. foreach (var dep in meta.Manifest.Dependencies)
  455. {
  456. #if DEBUG
  457. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  458. #endif
  459. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  460. continue;
  461. if (disabledLookup.ContainsKey(dep.Key) && dep.Value.IsSatisfied(disabledLookup[dep.Key]))
  462. {
  463. Logger.loader.Warn($"Dependency {dep.Key} was found, but disabled. Disabling {meta.Name} too.");
  464. missingDeps.Add((dep.Key, dep.Value, true));
  465. }
  466. else
  467. {
  468. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  469. missingDeps.Add((dep.Key, dep.Value, false));
  470. }
  471. }
  472. if (missingDeps.Count == 0)
  473. {
  474. metadata.Add(meta);
  475. if (meta.Id != null)
  476. pluginsToLoad.Add(meta.Id, meta.Version);
  477. }
  478. else if (missingDeps.Any(t => !t.disabled))
  479. { // missing deps
  480. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  481. {
  482. ReasonText = $"Missing dependencies {string.Join(", ", missingDeps.Where(t => !t.disabled).Select(t => $"{t.id}@{t.version}").StrJP())}"
  483. });
  484. }
  485. else
  486. {
  487. DisabledPlugins.Add(meta);
  488. DisabledConfig.Instance.DisabledModIds.Add(meta.Id ?? meta.Name);
  489. }
  490. }
  491. PluginsMetadata = metadata;
  492. }
  493. internal static void InitFeatures()
  494. {
  495. var parsedFeatures = PluginsMetadata.Select(m =>
  496. (metadata: m,
  497. features: m.Manifest.Features.Select(feature =>
  498. (feature, parsed: Ref.Create<Feature.FeatureParse?>(null))
  499. ).ToList()
  500. )
  501. ).ToList();
  502. while (DefineFeature.NewFeature)
  503. {
  504. DefineFeature.NewFeature = false;
  505. foreach (var (metadata, features) in parsedFeatures)
  506. for (var i = 0; i < features.Count; i++)
  507. {
  508. var feature = features[i];
  509. var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj,
  510. out var exception, out var valid, out var parsed, feature.parsed.Value);
  511. if (!success && !valid && featureObj == null && exception == null) // no feature of type found
  512. feature.parsed.Value = parsed;
  513. else if (success)
  514. {
  515. if (valid && featureObj.StoreOnPlugin)
  516. metadata.InternalFeatures.Add(featureObj);
  517. else if (!valid)
  518. Logger.features.Warn(
  519. $"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}");
  520. features.RemoveAt(i--);
  521. }
  522. else
  523. {
  524. Logger.features.Error($"Error parsing feature definition on {metadata.Name}");
  525. Logger.features.Error(exception);
  526. features.RemoveAt(i--);
  527. }
  528. }
  529. foreach (var plugin in PluginsMetadata)
  530. foreach (var feature in plugin.Features)
  531. feature.Evaluate();
  532. }
  533. foreach (var plugin in parsedFeatures)
  534. {
  535. if (plugin.features.Count <= 0) continue;
  536. Logger.features.Warn($"On plugin {plugin.metadata.Name}:");
  537. foreach (var feature in plugin.features)
  538. Logger.features.Warn($" Feature not found with name {feature.feature}");
  539. }
  540. }
  541. internal static void ReleaseAll(bool full = false)
  542. {
  543. if (full)
  544. ignoredPlugins = new Dictionary<PluginMetadata, IgnoreReason>();
  545. else
  546. {
  547. foreach (var m in PluginsMetadata)
  548. ignoredPlugins.Add(m, new IgnoreReason(Reason.Released));
  549. foreach (var m in ignoredPlugins.Keys)
  550. { // clean them up so we can still use the metadata for updates
  551. m.InternalFeatures.Clear();
  552. m.PluginType = null;
  553. m.Assembly = null;
  554. }
  555. }
  556. PluginsMetadata = new List<PluginMetadata>();
  557. DisabledPlugins = new List<PluginMetadata>();
  558. Feature.Reset();
  559. GC.Collect();
  560. }
  561. internal static void Load(PluginMetadata meta)
  562. {
  563. if (meta.Assembly == null && meta.PluginType != null)
  564. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  565. }
  566. internal static PluginInfo InitPlugin(PluginMetadata meta, IEnumerable<PluginMetadata> alreadyLoaded)
  567. {
  568. if (meta.PluginType == null)
  569. return new PluginInfo()
  570. {
  571. Metadata = meta,
  572. Plugin = null
  573. };
  574. var info = new PluginInfo();
  575. if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
  576. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  577. try
  578. {
  579. foreach (var dep in meta.Dependencies)
  580. {
  581. if (alreadyLoaded.Contains(dep)) continue;
  582. // otherwise...
  583. if (ignoredPlugins.TryGetValue(dep, out var reason))
  584. { // was added to the ignore list
  585. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  586. {
  587. ReasonText = $"Dependency was ignored at load time: {reason.ReasonText}",
  588. RelatedTo = dep
  589. });
  590. }
  591. else
  592. { // was not added to ignore list
  593. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Dependency)
  594. {
  595. ReasonText = $"Dependency was not already loaded at load time, but was also not ignored",
  596. RelatedTo = dep
  597. });
  598. }
  599. return null;
  600. }
  601. Load(meta);
  602. Feature denyingFeature = null;
  603. if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
  604. {
  605. Logger.loader.Warn(
  606. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
  607. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
  608. {
  609. ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}"
  610. });
  611. return null;
  612. }
  613. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  614. var instance = Activator.CreateInstance(type) as IPlugin;
  615. info.Metadata = meta;
  616. info.Plugin = instance;
  617. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  618. if (init != null)
  619. {
  620. denyingFeature = null;
  621. if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
  622. {
  623. Logger.loader.Warn(
  624. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
  625. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
  626. {
  627. ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {denyingFeature?.GetType()}:\n\t{denyingFeature?.InvalidMessage}"
  628. });
  629. return null;
  630. }
  631. PluginInitInjector.Inject(init, info);
  632. }
  633. foreach (var feature in meta.Features)
  634. try
  635. {
  636. feature.AfterInit(info, info.Plugin);
  637. }
  638. catch (Exception e)
  639. {
  640. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
  641. }
  642. /*try // TODO: move this out to after all plugins have been inited
  643. {
  644. instance.OnEnable();
  645. }
  646. catch (Exception e)
  647. {
  648. Logger.loader.Error($"Error occurred trying to enable {meta.Name}");
  649. Logger.loader.Error(e);
  650. return null; // is enable failure a full load failure?
  651. }*/
  652. }
  653. catch (AmbiguousMatchException)
  654. {
  655. Logger.loader.Critical($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  656. Logger.loader.Critical("@Developer: you *really* should fix this");
  657. // not adding to ignoredPlugins here because this should only happen in a development context
  658. // if someone fucks this up on release thats on them
  659. return null;
  660. }
  661. catch (Exception e)
  662. {
  663. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  664. ignoredPlugins.Add(meta, new IgnoreReason(Reason.Error)
  665. {
  666. ReasonText = "Error ocurred while initializing",
  667. Error = e
  668. });
  669. return null;
  670. }
  671. return info;
  672. }
  673. internal static List<PluginInfo> LoadPlugins()
  674. {
  675. InitFeatures();
  676. DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
  677. var list = new List<PluginInfo>();
  678. var loaded = new HashSet<PluginMetadata>();
  679. foreach (var meta in PluginsMetadata)
  680. {
  681. var info = InitPlugin(meta, loaded);
  682. if (info != null)
  683. {
  684. list.Add(info);
  685. loaded.Add(meta);
  686. }
  687. }
  688. return list;
  689. }
  690. }
  691. }