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.

511 lines
19 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.Threading.Tasks;
  12. using Version = SemVer.Version;
  13. namespace IPA.Loader
  14. {
  15. /// <summary>
  16. /// A type to manage the loading of plugins.
  17. /// </summary>
  18. public class PluginLoader
  19. {
  20. internal static Task LoadTask() => Task.Run(() =>
  21. {
  22. LoadMetadata();
  23. Resolve();
  24. ComputeLoadOrder();
  25. InitFeatures();
  26. });
  27. /// <summary>
  28. /// A class which describes
  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 ModSaber 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. private PluginManifest manifest;
  64. internal HashSet<PluginMetadata> Dependencies { get; } = new HashSet<PluginMetadata>();
  65. internal PluginManifest Manifest
  66. {
  67. get => manifest;
  68. set
  69. {
  70. manifest = value;
  71. Name = value.Name;
  72. Version = value.Version;
  73. Id = value.Id;
  74. }
  75. }
  76. /// <inheritdoc />
  77. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
  78. }
  79. /// <summary>
  80. /// A container object for all the data relating to a plugin.
  81. /// </summary>
  82. public class PluginInfo
  83. {
  84. internal IBeatSaberPlugin Plugin { get; set; }
  85. /// <summary>
  86. /// Metadata for the plugin.
  87. /// </summary>
  88. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  89. }
  90. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  91. internal static void LoadMetadata()
  92. {
  93. string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
  94. try
  95. {
  96. var selfMeta = new PluginMetadata
  97. {
  98. Assembly = Assembly.GetExecutingAssembly(),
  99. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  100. PluginType = null,
  101. IsSelf = true
  102. };
  103. string manifest;
  104. using (var manifestReader =
  105. new StreamReader(
  106. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  107. throw new InvalidOperationException()))
  108. manifest = manifestReader.ReadToEnd();
  109. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  110. PluginsMetadata.Add(selfMeta);
  111. }
  112. catch (Exception e)
  113. {
  114. Logger.loader.Critical("Error loading own manifest");
  115. Logger.loader.Critical(e);
  116. }
  117. foreach (var plugin in plugins)
  118. {
  119. try
  120. {
  121. var metadata = new PluginMetadata
  122. {
  123. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin)),
  124. IsSelf = false
  125. };
  126. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  127. {
  128. ReadingMode = ReadingMode.Immediate,
  129. ReadWrite = false,
  130. AssemblyResolver = new CecilLibLoader()
  131. }).MainModule;
  132. var iBeatSaberPlugin = pluginModule.ImportReference(typeof(IBeatSaberPlugin));
  133. foreach (var type in pluginModule.Types)
  134. {
  135. foreach (var inter in type.Interfaces)
  136. {
  137. var ifType = inter.InterfaceType;
  138. if (iBeatSaberPlugin.FullName == ifType.FullName)
  139. {
  140. metadata.PluginType = type;
  141. break;
  142. }
  143. }
  144. if (metadata.PluginType != null) break;
  145. }
  146. if (metadata.PluginType == null)
  147. {
  148. Logger.loader.Notice(
  149. #if DIRE_LOADER_WARNINGS
  150. $"Could not find plugin type for {Path.GetFileName(plugin)}"
  151. #else
  152. $"New plugin type not present in {Path.GetFileName(plugin)}; maybe an old plugin?"
  153. #endif
  154. );
  155. continue;
  156. }
  157. foreach (var resource in pluginModule.Resources)
  158. {
  159. if (!(resource is EmbeddedResource embedded) ||
  160. embedded.Name != $"{metadata.PluginType.Namespace}.manifest.json") continue;
  161. string manifest;
  162. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  163. manifest = manifestReader.ReadToEnd();
  164. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  165. break;
  166. }
  167. if (metadata.Manifest == null)
  168. {
  169. Logger.loader.Error("Could not find manifest.json in namespace " +
  170. $"{metadata.PluginType.Namespace} for {Path.GetFileName(plugin)}");
  171. continue;
  172. }
  173. Logger.loader.Debug($"Adding info for {Path.GetFileName(plugin)}");
  174. PluginsMetadata.Add(metadata);
  175. }
  176. catch (Exception e)
  177. {
  178. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  179. Logger.loader.Error(e);
  180. }
  181. }
  182. }
  183. // keep track of these for the updater; it should still be able to update mods not loaded
  184. internal static HashSet<PluginMetadata> ignoredPlugins = new HashSet<PluginMetadata>();
  185. internal static void Resolve()
  186. { // resolves duplicates and conflicts, etc
  187. PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version));
  188. var ids = new HashSet<string>();
  189. var ignore = new HashSet<PluginMetadata>();
  190. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  191. foreach (var meta in PluginsMetadata)
  192. {
  193. if (meta.Id != null)
  194. {
  195. if (ids.Contains(meta.Id))
  196. {
  197. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  198. ignore.Add(meta);
  199. ignoredPlugins.Add(meta);
  200. continue; // because of sorted order, hightest order will always be the first one
  201. }
  202. bool processedLater = false;
  203. foreach (var meta2 in PluginsMetadata)
  204. {
  205. if (ignore.Contains(meta2)) continue;
  206. if (meta == meta2)
  207. {
  208. processedLater = true;
  209. continue;
  210. }
  211. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  212. var range = meta2.Manifest.Conflicts[meta.Id];
  213. if (!range.IsSatisfied(meta.Version)) continue;
  214. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
  215. if (processedLater)
  216. {
  217. Logger.loader.Warn($"Ignoring {meta2.Name}");
  218. ignore.Add(meta2);
  219. }
  220. else
  221. {
  222. Logger.loader.Warn($"Ignoring {meta.Name}");
  223. ignore.Add(meta);
  224. break;
  225. }
  226. }
  227. }
  228. if (ignore.Contains(meta))
  229. {
  230. ignoredPlugins.Add(meta);
  231. continue;
  232. }
  233. if (meta.Id != null)
  234. ids.Add(meta.Id);
  235. resolved.Add(meta);
  236. }
  237. PluginsMetadata = resolved;
  238. }
  239. internal static void ComputeLoadOrder()
  240. {
  241. #if DEBUG
  242. Logger.loader.Debug(string.Join(", ", PluginsMetadata.Select(p => p.ToString())));
  243. #endif
  244. bool InsertInto(HashSet<PluginMetadata> root, PluginMetadata meta, bool isRoot = false)
  245. { // this is slow, and hella recursive
  246. bool inserted = false;
  247. foreach (var sr in root)
  248. {
  249. inserted = inserted || InsertInto(sr.Dependencies, meta);
  250. if (meta.Id != null)
  251. if (sr.Manifest.Dependencies.ContainsKey(meta.Id) || sr.Manifest.LoadAfter.Contains(meta.Id))
  252. inserted = inserted || sr.Dependencies.Add(meta);
  253. if (sr.Id != null)
  254. if (meta.Manifest.LoadBefore.Contains(sr.Id))
  255. inserted = inserted || sr.Dependencies.Add(meta);
  256. }
  257. if (isRoot)
  258. {
  259. foreach (var sr in root)
  260. {
  261. InsertInto(meta.Dependencies, sr);
  262. if (sr.Id != null)
  263. if (meta.Manifest.Dependencies.ContainsKey(sr.Id) || meta.Manifest.LoadAfter.Contains(sr.Id))
  264. meta.Dependencies.Add(sr);
  265. if (meta.Id != null)
  266. if (sr.Manifest.LoadBefore.Contains(meta.Id))
  267. meta.Dependencies.Add(sr);
  268. }
  269. root.Add(meta);
  270. }
  271. return inserted;
  272. }
  273. var pluginTree = new HashSet<PluginMetadata>();
  274. foreach (var meta in PluginsMetadata)
  275. InsertInto(pluginTree, meta, true);
  276. void DeTree(List<PluginMetadata> into, HashSet<PluginMetadata> tree)
  277. {
  278. foreach (var st in tree)
  279. if (!into.Contains(st))
  280. {
  281. DeTree(into, st.Dependencies);
  282. into.Add(st);
  283. }
  284. }
  285. var deTreed = new List<PluginMetadata>();
  286. DeTree(deTreed, pluginTree);
  287. #if DEBUG
  288. Logger.loader.Debug(string.Join(", ", deTreed.Select(p => p.ToString())));
  289. #endif
  290. var metadata = new List<PluginMetadata>();
  291. var pluginsToLoad = new Dictionary<string, Version>();
  292. foreach (var meta in deTreed)
  293. {
  294. bool load = true;
  295. foreach (var dep in meta.Manifest.Dependencies)
  296. {
  297. #if DEBUG
  298. Logger.loader.Debug($"Looking for dependency {dep.Key} with version range {dep.Value.Intersect(new SemVer.Range("*.*.*"))}");
  299. #endif
  300. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key]))
  301. continue;
  302. load = false;
  303. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  304. }
  305. if (load)
  306. {
  307. metadata.Add(meta);
  308. if (meta.Id != null)
  309. pluginsToLoad.Add(meta.Id, meta.Version);
  310. }
  311. else
  312. ignoredPlugins.Add(meta);
  313. }
  314. PluginsMetadata = metadata;
  315. }
  316. internal static void InitFeatures()
  317. {
  318. var parsedFeatures = PluginsMetadata.Select(m =>
  319. Tuple.Create(m,
  320. m.Manifest.Features.Select(f =>
  321. Tuple.Create(f, Ref.Create<Feature.FeatureParse?>(null))
  322. ).ToList()
  323. )
  324. ).ToList();
  325. while (DefineFeature.NewFeature)
  326. {
  327. DefineFeature.NewFeature = false;
  328. foreach (var plugin in parsedFeatures)
  329. for (var i = 0; i < plugin.Item2.Count; i++)
  330. {
  331. var feature = plugin.Item2[i];
  332. var success = Feature.TryParseFeature(feature.Item1, plugin.Item1, out var featureObj,
  333. out var exception, out var valid, out var parsed, feature.Item2.Value);
  334. if (!success && !valid && featureObj == null && exception == null) // no feature of type found
  335. feature.Item2.Value = parsed;
  336. else if (success)
  337. {
  338. if (valid && featureObj.StoreOnPlugin)
  339. plugin.Item1.InternalFeatures.Add(featureObj);
  340. else if (!valid)
  341. Logger.features.Warn(
  342. $"Feature not valid on {plugin.Item1.Name}: {featureObj.InvalidMessage}");
  343. plugin.Item2.RemoveAt(i--);
  344. }
  345. else
  346. {
  347. Logger.features.Error($"Error parsing feature definition on {plugin.Item1.Name}");
  348. Logger.features.Error(exception);
  349. plugin.Item2.RemoveAt(i--);
  350. }
  351. }
  352. foreach (var plugin in PluginsMetadata)
  353. foreach (var feature in plugin.Features)
  354. feature.Evaluate();
  355. }
  356. foreach (var plugin in parsedFeatures)
  357. {
  358. if (plugin.Item2.Count <= 0) continue;
  359. Logger.features.Warn($"On plugin {plugin.Item1.Name}:");
  360. foreach (var feature in plugin.Item2)
  361. Logger.features.Warn($" Feature not found with name {feature.Item1}");
  362. }
  363. }
  364. internal static void Load(PluginMetadata meta)
  365. {
  366. if (meta.Assembly == null)
  367. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  368. }
  369. internal static PluginInfo InitPlugin(PluginMetadata meta)
  370. {
  371. if (meta.PluginType == null)
  372. return new PluginInfo()
  373. {
  374. Metadata = meta,
  375. Plugin = null
  376. };
  377. var info = new PluginInfo();
  378. try
  379. {
  380. Load(meta);
  381. Feature denyingFeature = null;
  382. if (!meta.Features.All(f => (denyingFeature = f).BeforeLoad(meta)))
  383. {
  384. Logger.loader.Warn(
  385. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from loading! {denyingFeature?.InvalidMessage}");
  386. ignoredPlugins.Add(meta);
  387. return null;
  388. }
  389. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  390. var instance = (IBeatSaberPlugin)Activator.CreateInstance(type);
  391. info.Metadata = meta;
  392. info.Plugin = instance;
  393. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  394. if (init != null)
  395. {
  396. denyingFeature = null;
  397. if (!meta.Features.All(f => (denyingFeature = f).BeforeInit(info)))
  398. {
  399. Logger.loader.Warn(
  400. $"Feature {denyingFeature?.GetType()} denied plugin {meta.Name} from initializing! {denyingFeature?.InvalidMessage}");
  401. ignoredPlugins.Add(meta);
  402. return null;
  403. }
  404. PluginInitInjector.Inject(init, info);
  405. }
  406. foreach (var feature in meta.Features)
  407. try
  408. {
  409. feature.AfterInit(info);
  410. }
  411. catch (Exception e)
  412. {
  413. Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterInit)}: {e}");
  414. }
  415. }
  416. catch (AmbiguousMatchException)
  417. {
  418. Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  419. // not adding to ignoredPlugins here because this should only happen in a development context
  420. // if someone fucks this up on release thats on them
  421. return null;
  422. }
  423. catch (Exception e)
  424. {
  425. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  426. ignoredPlugins.Add(meta);
  427. return null;
  428. }
  429. return info;
  430. }
  431. internal static List<PluginInfo> LoadPlugins() => PluginsMetadata.Select(InitPlugin).Where(p => p != null).ToList();
  432. }
  433. }