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.

598 lines
23 KiB

  1. using IPA.Loader.Features;
  2. using IPA.Logging;
  3. using IPA.Utilities;
  4. using Mono.Cecil;
  5. using Newtonsoft.Json;
  6. using System;
  7. using System.Collections.Generic;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Reflection;
  11. using System.Text.RegularExpressions;
  12. using System.Threading.Tasks;
  13. using Version = SemVer.Version;
  14. namespace IPA.Loader
  15. {
  16. /// <summary>
  17. /// A type to manage the loading of plugins.
  18. /// </summary>
  19. public class PluginLoader
  20. {
  21. internal static Task LoadTask() => Task.Run(() =>
  22. {
  23. LoadMetadata();
  24. Resolve();
  25. ComputeLoadOrder();
  26. });
  27. /// <summary>
  28. /// A class which describes a loaded plugin.
  29. /// </summary>
  30. public class PluginMetadata
  31. {
  32. /// <summary>
  33. /// The assembly the plugin was loaded from.
  34. /// </summary>
  35. public Assembly Assembly { get; internal set; }
  36. /// <summary>
  37. /// The TypeDefinition for the main type of the plugin.
  38. /// </summary>
  39. public TypeDefinition PluginType { get; internal set; }
  40. /// <summary>
  41. /// The human readable name of the plugin.
  42. /// </summary>
  43. public string Name { get; internal set; }
  44. /// <summary>
  45. /// The BeatMods ID of the plugin, or null if it doesn't have one.
  46. /// </summary>
  47. public string Id { get; internal set; }
  48. /// <summary>
  49. /// The version of the plugin.
  50. /// </summary>
  51. public Version Version { get; internal set; }
  52. /// <summary>
  53. /// The file the plugin was loaded from.
  54. /// </summary>
  55. public FileInfo File { get; internal set; }
  56. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  57. /// <summary>
  58. /// The features this plugin requests.
  59. /// </summary>
  60. public IReadOnlyList<Feature> Features => InternalFeatures;
  61. internal readonly List<Feature> InternalFeatures = new List<Feature>();
  62. internal bool IsSelf;
  63. internal bool IsBare;
  64. private PluginManifest manifest;
  65. internal HashSet<PluginMetadata> Dependencies { get; } = new HashSet<PluginMetadata>();
  66. internal PluginManifest Manifest
  67. {
  68. get => manifest;
  69. set
  70. {
  71. manifest = value;
  72. Name = value.Name;
  73. Version = value.Version;
  74. Id = value.Id;
  75. }
  76. }
  77. /// <inheritdoc />
  78. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
  79. }
  80. /// <summary>
  81. /// A container object for all the data relating to a plugin.
  82. /// </summary>
  83. public class PluginInfo
  84. {
  85. internal IBeatSaberPlugin Plugin { get; set; }
  86. /// <summary>
  87. /// Metadata for the plugin.
  88. /// </summary>
  89. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  90. }
  91. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  92. private static readonly Regex embeddedTextDescriptionPattern = new Regex(@"#!\[(.+)\]", RegexOptions.Compiled | RegexOptions.Singleline);
  93. internal static void LoadMetadata()
  94. {
  95. string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
  96. try
  97. {
  98. var selfMeta = new PluginMetadata
  99. {
  100. Assembly = Assembly.GetExecutingAssembly(),
  101. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  102. PluginType = null,
  103. IsSelf = true
  104. };
  105. string manifest;
  106. using (var manifestReader =
  107. new StreamReader(
  108. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  109. throw new InvalidOperationException()))
  110. manifest = manifestReader.ReadToEnd();
  111. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  112. PluginsMetadata.Add(selfMeta);
  113. }
  114. catch (Exception e)
  115. {
  116. Logger.loader.Critical("Error loading own manifest");
  117. Logger.loader.Critical(e);
  118. }
  119. foreach (var plugin in plugins)
  120. {
  121. try
  122. {
  123. var metadata = new PluginMetadata
  124. {
  125. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)),
  126. IsSelf = false
  127. };
  128. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  129. {
  130. ReadingMode = ReadingMode.Immediate,
  131. ReadWrite = false,
  132. AssemblyResolver = new CecilLibLoader()
  133. }).MainModule;
  134. var iBeatSaberPlugin = pluginModule.ImportReference(typeof(IBeatSaberPlugin));
  135. foreach (var type in pluginModule.Types)
  136. {
  137. foreach (var inter in type.Interfaces)
  138. {
  139. var ifType = inter.InterfaceType;
  140. if (iBeatSaberPlugin.FullName == ifType.FullName)
  141. {
  142. metadata.PluginType = type;
  143. break;
  144. }
  145. }
  146. if (metadata.PluginType != null) break;
  147. }
  148. if (metadata.PluginType == null)
  149. {
  150. Logger.loader.Notice(
  151. #if DIRE_LOADER_WARNINGS
  152. $"Could not find plugin type for {Path.GetFileName(plugin)}"
  153. #else
  154. $"New plugin type not present in {Path.GetFileName(plugin)}; maybe an old plugin?"
  155. #endif
  156. );
  157. continue;
  158. }
  159. foreach (var resource in pluginModule.Resources)
  160. {
  161. if (!(resource is EmbeddedResource embedded) ||
  162. embedded.Name != $"{metadata.PluginType.Namespace}.manifest.json") continue;
  163. string manifest;
  164. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  165. manifest = manifestReader.ReadToEnd();
  166. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  167. break;
  168. }
  169. if (metadata.Manifest == null)
  170. {
  171. Logger.loader.Error("Could not find manifest.json in namespace " +
  172. $"{metadata.PluginType.Namespace} for {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. }
  183. foreach (var meta in PluginsMetadata)
  184. { // process description include
  185. var lines = meta.Manifest.Description.Split('\n');
  186. var m = embeddedTextDescriptionPattern.Match(lines[0]);
  187. if (m.Success)
  188. {
  189. if (meta.IsBare)
  190. {
  191. Logger.loader.Warn($"Bare manifest cannot specify description file");
  192. meta.Manifest.Description = string.Join("\n", lines.Skip(1)); // ignore first line
  193. continue;
  194. }
  195. var name = m.Groups[1].Value;
  196. string description;
  197. if (!meta.IsSelf)
  198. {
  199. var resc = meta.PluginType.Module.Resources.Select(r => r as EmbeddedResource)
  200. .Where(r => r != null)
  201. .FirstOrDefault(r => r.Name == name);
  202. if (resc == null)
  203. {
  204. Logger.loader.Warn($"Could not find description file for plugin {meta.Name} ({name}); ignoring include");
  205. meta.Manifest.Description = string.Join("\n", lines.Skip(1)); // ignore first line
  206. continue;
  207. }
  208. using (var reader = new StreamReader(resc.GetResourceStream()))
  209. description = reader.ReadToEnd();
  210. }
  211. else
  212. {
  213. using (var descriptionReader =
  214. new StreamReader(
  215. meta.Assembly.GetManifestResourceStream(name) ??
  216. throw new InvalidOperationException()))
  217. description = descriptionReader.ReadToEnd();
  218. }
  219. meta.Manifest.Description = description;
  220. }
  221. }
  222. }
  223. IEnumerable<string> bareManifests = Directory.GetFiles(BeatSaber.PluginsPath, "*.json");
  224. bareManifests = bareManifests.Concat(Directory.GetFiles(BeatSaber.PluginsPath, "*.manifest"));
  225. foreach (var manifest in bareManifests)
  226. {
  227. try
  228. {
  229. var metadata = new PluginMetadata
  230. {
  231. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, manifest)),
  232. IsSelf = false,
  233. IsBare = true,
  234. };
  235. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(File.ReadAllText(manifest));
  236. Logger.loader.Debug($"Adding info for bare manifest {Path.GetFileName(manifest)}");
  237. PluginsMetadata.Add(metadata);
  238. }
  239. catch (Exception e)
  240. {
  241. Logger.loader.Error($"Could not load data for bare manifest {Path.GetFileName(manifest)}");
  242. Logger.loader.Error(e);
  243. }
  244. }
  245. }
  246. // keep track of these for the updater; it should still be able to update mods not loaded
  247. internal static HashSet<PluginMetadata> ignoredPlugins = new HashSet<PluginMetadata>();
  248. internal static void Resolve()
  249. { // resolves duplicates and conflicts, etc
  250. PluginsMetadata.Sort((a, b) => b.Version.CompareTo(a.Version));
  251. var ids = new HashSet<string>();
  252. var ignore = new HashSet<PluginMetadata>();
  253. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  254. foreach (var meta in PluginsMetadata)
  255. {
  256. if (meta.Id != null)
  257. {
  258. if (ids.Contains(meta.Id))
  259. {
  260. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  261. ignore.Add(meta);
  262. ignoredPlugins.Add(meta);
  263. continue; // because of sorted order, hightest order will always be the first one
  264. }
  265. bool processedLater = false;
  266. foreach (var meta2 in PluginsMetadata)
  267. {
  268. if (ignore.Contains(meta2)) continue;
  269. if (meta == meta2)
  270. {
  271. processedLater = true;
  272. continue;
  273. }
  274. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  275. var range = meta2.Manifest.Conflicts[meta.Id];
  276. if (!range.IsSatisfied(meta.Version)) continue;
  277. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
  278. if (processedLater)
  279. {
  280. Logger.loader.Warn($"Ignoring {meta2.Name}");
  281. ignore.Add(meta2);
  282. }
  283. else
  284. {
  285. Logger.loader.Warn($"Ignoring {meta.Name}");
  286. ignore.Add(meta);
  287. break;
  288. }
  289. }
  290. }
  291. if (ignore.Contains(meta))
  292. {
  293. ignoredPlugins.Add(meta);
  294. continue;
  295. }
  296. if (meta.Id != null)
  297. ids.Add(meta.Id);
  298. resolved.Add(meta);
  299. }
  300. PluginsMetadata = resolved;
  301. }
  302. internal static void ComputeLoadOrder()
  303. {
  304. #if DEBUG
  305. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString())));
  306. #endif
  307. bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  308. { // this is slow, and hella recursive
  309. bool inserted = false;
  310. foreach (var sr in root)
  311. {
  312. inserted = inserted || InsertInto(sr.Dependencies, meta);
  313. if (meta.Id != null)
  314. if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id))
  315. inserted = inserted || sr.Dependencies.Add(meta);
  316. if (sr.Id != null)
  317. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  318. inserted = inserted || sr.Dependencies.Add(meta);
  319. }
  320. if (isRoot)
  321. {
  322. foreach (var sr in root)
  323. {
  324. InsertInto(meta.Dependencies, sr);
  325. if (sr.Id != null)
  326. if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id))
  327. meta.Dependencies.Add(sr);
  328. if (meta.Id != null)
  329. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  330. meta.Dependencies.Add(sr);
  331. }
  332. root.Add(meta);
  333. }
  334. return inserted;
  335. }
  336. var pluginTree = new HashSet<PluginMetadata>();
  337. foreach (var meta in PluginsMetadata)
  338. InsertInto(pluginTree, meta, true);
  339. void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  340. {
  341. foreach (var st in tree)
  342. if (!into.Contains(st))
  343. {
  344. DeTree(into, st.Dependencies);
  345. into.Add(st);
  346. }
  347. }
  348. var deTreed = new List<PluginMetadata>();
  349. DeTree(deTreed, pluginTree);
  350. #if DEBUG
  351. Logger.loader.Debug(string.Join(", ", deTreed.Select(p => p.ToString())));
  352. #endif
  353. var metadata = new List<PluginMetadata>();
  354. var pluginsToLoad = new Dictionary<string, Version>();
  355. foreach (var meta in deTreed)
  356. {
  357. bool load = true;
  358. foreach (var dep in meta.Manifest.Dependencies)
  359. {
  360. #if DEBUG
  361. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  362. #endif
  363. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  364. continue;
  365. load = false;
  366. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  367. }
  368. if (load)
  369. {
  370. metadata.Add(meta);
  371. if (meta.Id != null)
  372. pluginsToLoad.Add(meta.Id, meta.Version);
  373. }
  374. else
  375. ignoredPlugins.Add(meta);
  376. }
  377. PluginsMetadata = metadata;
  378. }
  379. internal static void InitFeatures()
  380. {
  381. var parsedFeatures = PluginsMetadata.Select(m =>
  382. Tuple.Create(m,
  383. m.Manifest.Features.Select(f =>
  384. Tuple.Create(f, Ref.Create<Feature.FeatureParse?>(null))
  385. ).ToList()
  386. )
  387. ).ToList();
  388. while (DefineFeature.NewFeature)
  389. {
  390. DefineFeature.NewFeature = false;
  391. foreach (var plugin in parsedFeatures)
  392. for (var i = 0; i < plugin.Item2.Count; i++)
  393. {
  394. var feature = plugin.Item2[i];
  395. var success = Feature.TryParseFeature(feature.Item1, plugin.Item1, out var featureObj,
  396. out var exception, out var valid, out var parsed, feature.Item2.Value);
  397. if (!success && !valid && featureObj == null && exception == null) // no feature of type found
  398. feature.Item2.Value = parsed;
  399. else if (success)
  400. {
  401. if (valid && featureObj.StoreOnPlugin)
  402. plugin.Item1.InternalFeatures.Add(featureObj);
  403. else if (!valid)
  404. Logger.features.Warn(
  405. $"Feature not valid on {plugin.Item1.Name}: {featureObj.InvalidMessage}");
  406. plugin.Item2.RemoveAt(i--);
  407. }
  408. else
  409. {
  410. Logger.features.Error($"Error parsing feature definition on {plugin.Item1.Name}");
  411. Logger.features.Error(exception);
  412. plugin.Item2.RemoveAt(i--);
  413. }
  414. }
  415. foreach (var plugin in PluginsMetadata)
  416. foreach (var feature in plugin.Features)
  417. feature.Evaluate();
  418. }
  419. foreach (var plugin in parsedFeatures)
  420. {
  421. if (plugin.Item2.Count <= 0) continue;
  422. Logger.features.Warn($"On plugin {plugin.Item1.Name}:");
  423. foreach (var feature in plugin.Item2)
  424. Logger.features.Warn($" Feature not found with name {feature.Item1}");
  425. }
  426. }
  427. internal static void ReleaseAll()
  428. {
  429. ignoredPlugins = new HashSet<PluginMetadata>();
  430. PluginsMetadata = new List<PluginMetadata>();
  431. Feature.Reset();
  432. GC.Collect();
  433. }
  434. internal static void Load(PluginMetadata meta)
  435. {
  436. if (meta.Assembly == null && meta.PluginType != null)
  437. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  438. }
  439. internal static PluginInfo InitPlugin(PluginMetadata meta)
  440. {
  441. if (meta.PluginType == null)
  442. return new PluginInfo()
  443. {
  444. Metadata = meta,
  445. Plugin = null
  446. };
  447. var info = new PluginInfo();
  448. if (meta.Manifest.GameVersion != BeatSaber.GameVersion)
  449. Logger.loader.Warn($"Mod {meta.Name} developed for game version {meta.Manifest.GameVersion}, so it may not work properly.");
  450. try
  451. {
  452. Load(meta);
  453. Feature denyingFeature = null;
  454. if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
  455. {
  456. Logger.loader.Warn(
  457. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
  458. ignoredPlugins.Add(meta);
  459. return null;
  460. }
  461. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  462. var instance = (IBeatSaberPlugin)Activator.CreateInstance(type);
  463. info.Metadata = meta;
  464. info.Plugin = instance;
  465. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  466. if (init != null)
  467. {
  468. denyingFeature = null;
  469. if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
  470. {
  471. Logger.loader.Warn(
  472. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
  473. ignoredPlugins.Add(meta);
  474. return null;
  475. }
  476. PluginInitInjector.Inject(init, info);
  477. }
  478. foreach (var feature in meta.Features)
  479. try
  480. {
  481. feature.AfterInit(info);
  482. }
  483. catch (Exception e)
  484. {
  485. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
  486. }
  487. }
  488. catch (AmbiguousMatchException)
  489. {
  490. Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  491. // not adding to ignoredPlugins here because this should only happen in a development context
  492. // if someone fucks this up on release thats on them
  493. return null;
  494. }
  495. catch (Exception e)
  496. {
  497. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  498. ignoredPlugins.Add(meta);
  499. return null;
  500. }
  501. return info;
  502. }
  503. internal static List<PluginInfo> LoadPlugins()
  504. {
  505. InitFeatures();
  506. return PluginsMetadata.Select(InitPlugin).Where(p => p != null).ToList();
  507. }
  508. }
  509. }