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.

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