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.

369 lines
13 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Threading.Tasks;
  7. using IPA.Config;
  8. using IPA.Logging;
  9. using IPA.Utilities;
  10. using Mono.Cecil;
  11. using Newtonsoft.Json;
  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. });
  26. /// <summary>
  27. /// A class which describes
  28. /// </summary>
  29. public class PluginMetadata
  30. {
  31. /// <summary>
  32. /// The assembly the plugin was loaded from.
  33. /// </summary>
  34. public Assembly Assembly { get; internal set; }
  35. /// <summary>
  36. /// The TypeDefinition for the main type of the plugin.
  37. /// </summary>
  38. public TypeDefinition PluginType { get; internal set; }
  39. /// <summary>
  40. /// The human readable name of the plugin.
  41. /// </summary>
  42. public string Name { get; internal set; }
  43. /// <summary>
  44. /// The ModSaber ID of the plugin, or null if it doesn't have one.
  45. /// </summary>
  46. public string Id { get; internal set; }
  47. /// <summary>
  48. /// The version of the plugin.
  49. /// </summary>
  50. public Version Version { get; internal set; }
  51. /// <summary>
  52. /// The file the plugin was loaded from.
  53. /// </summary>
  54. public FileInfo File { get; internal set; }
  55. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  56. /// <summary>
  57. /// The features this plugin requests.
  58. /// </summary>
  59. public string[] Features { get; internal set; }
  60. private PluginManifest manifest;
  61. internal PluginManifest Manifest
  62. {
  63. get => manifest;
  64. set
  65. {
  66. manifest = value;
  67. Name = value.Name;
  68. Version = value.Version;
  69. Id = value.Id;
  70. Features = value.Features;
  71. }
  72. }
  73. /// <inheritdoc />
  74. public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{LoneFunctions.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'";
  75. }
  76. /// <summary>
  77. /// A container object for all the data relating to a plugin.
  78. /// </summary>
  79. public class PluginInfo
  80. {
  81. internal IBeatSaberPlugin Plugin { get; set; }
  82. /// <summary>
  83. /// Metadata for the plugin.
  84. /// </summary>
  85. public PluginMetadata Metadata { get; internal set; } = new PluginMetadata();
  86. }
  87. internal static List<PluginMetadata> PluginsMetadata = new List<PluginMetadata>();
  88. internal static void LoadMetadata()
  89. {
  90. string[] plugins = Directory.GetFiles(BeatSaber.PluginsPath, "*.dll");
  91. try
  92. {
  93. var selfMeta = new PluginMetadata
  94. {
  95. Assembly = Assembly.GetExecutingAssembly(),
  96. File = new FileInfo(Path.Combine(BeatSaber.InstallPath, "IPA.exe")),
  97. PluginType = null
  98. };
  99. string manifest;
  100. using (var manifestReader =
  101. new StreamReader(
  102. selfMeta.Assembly.GetManifestResourceStream(typeof(PluginLoader), "manifest.json") ??
  103. throw new InvalidOperationException()))
  104. manifest = manifestReader.ReadToEnd();
  105. selfMeta.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  106. PluginsMetadata.Add(selfMeta);
  107. }
  108. catch (Exception e)
  109. {
  110. Logger.loader.Critical("Error loading own manifest");
  111. Logger.loader.Critical(e);
  112. }
  113. foreach (var plugin in plugins)
  114. {
  115. try
  116. {
  117. var metadata = new PluginMetadata
  118. {
  119. File = new FileInfo(Path.Combine(BeatSaber.PluginsPath, plugin))
  120. };
  121. var pluginModule = AssemblyDefinition.ReadAssembly(plugin, new ReaderParameters
  122. {
  123. ReadingMode = ReadingMode.Immediate,
  124. ReadWrite = false
  125. }).MainModule;
  126. var iBeatSaberPlugin = pluginModule.ImportReference(typeof(IBeatSaberPlugin));
  127. foreach (var type in pluginModule.Types)
  128. {
  129. foreach (var inter in type.Interfaces)
  130. {
  131. var ifType = inter.InterfaceType;
  132. if (iBeatSaberPlugin.FullName == ifType.FullName)
  133. {
  134. metadata.PluginType = type;
  135. break;
  136. }
  137. }
  138. if (metadata.PluginType != null) break;
  139. }
  140. if (metadata.PluginType == null)
  141. {
  142. Logger.loader.Warn($"Could not find plugin type for {Path.GetFileName(plugin)}");
  143. continue;
  144. }
  145. foreach (var resource in pluginModule.Resources)
  146. {
  147. if (!(resource is EmbeddedResource embedded) ||
  148. embedded.Name != $"{metadata.PluginType.Namespace}.manifest.json") continue;
  149. string manifest;
  150. using (var manifestReader = new StreamReader(embedded.GetResourceStream()))
  151. manifest = manifestReader.ReadToEnd();
  152. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  153. break;
  154. }
  155. PluginsMetadata.Add(metadata);
  156. }
  157. catch (Exception e)
  158. {
  159. Logger.loader.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  160. Logger.loader.Error(e);
  161. }
  162. }
  163. }
  164. internal static void Resolve()
  165. { // resolves duplicates and conflicts, etc
  166. PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version));
  167. var ids = new HashSet<string>();
  168. var ignore = new HashSet<PluginMetadata>();
  169. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  170. foreach (var meta in PluginsMetadata)
  171. {
  172. if (meta.Id != null)
  173. {
  174. if (ids.Contains(meta.Id))
  175. {
  176. Logger.loader.Warn($"Found duplicates of {meta.Id}, using newest");
  177. ignore.Add(meta);
  178. continue; // because of sorted order, hightest order will always be the first one
  179. }
  180. bool processedLater = false;
  181. foreach (var meta2 in PluginsMetadata)
  182. {
  183. if (ignore.Contains(meta2)) continue;
  184. if (meta == meta2)
  185. {
  186. processedLater = true;
  187. continue;
  188. }
  189. if (!meta2.Manifest.Conflicts.ContainsKey(meta.Id)) continue;
  190. var range = meta2.Manifest.Conflicts[meta.Id];
  191. if (!range.IsSatisfied(meta.Version)) continue;
  192. Logger.loader.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
  193. if (processedLater)
  194. {
  195. Logger.loader.Warn($"Ignoring {meta2.Name}");
  196. ignore.Add(meta2);
  197. }
  198. else
  199. {
  200. Logger.loader.Warn($"Ignoring {meta.Name}");
  201. ignore.Add(meta);
  202. break;
  203. }
  204. }
  205. }
  206. if (ignore.Contains(meta)) continue;
  207. if (meta.Id != null) ids.Add(meta.Id);
  208. resolved.Add(meta);
  209. }
  210. PluginsMetadata = resolved;
  211. }
  212. internal static void ComputeLoadOrder()
  213. {
  214. PluginsMetadata.Sort((a, b) =>
  215. {
  216. if (a.Id == b.Id) return 0;
  217. if (a.Id != null)
  218. {
  219. if (b.Manifest.Dependencies.ContainsKey(a.Id) || b.Manifest.LoadAfter.Contains(a.Id)) return -1;
  220. if (b.Manifest.LoadBefore.Contains(a.Id)) return 1;
  221. }
  222. if (b.Id != null)
  223. {
  224. if (a.Manifest.Dependencies.ContainsKey(b.Id) || a.Manifest.LoadAfter.Contains(b.Id)) return 1;
  225. if (a.Manifest.LoadBefore.Contains(b.Id)) return -1;
  226. }
  227. return 0;
  228. });
  229. var metadata = new List<PluginMetadata>();
  230. var pluginsToLoad = new Dictionary<string, Version>();
  231. foreach (var meta in PluginsMetadata)
  232. {
  233. bool load = true;
  234. foreach (var dep in meta.Manifest.Dependencies)
  235. {
  236. if (pluginsToLoad.ContainsKey(dep.Key) && dep.Value.IsSatisfied(pluginsToLoad[dep.Key])) continue;
  237. load = false;
  238. Logger.loader.Warn($"{meta.Name} is missing dependency {dep.Key}@{dep.Value}");
  239. }
  240. if (load)
  241. {
  242. metadata.Add(meta);
  243. if (meta.Id != null)
  244. pluginsToLoad.Add(meta.Id, meta.Version);
  245. }
  246. }
  247. PluginsMetadata = metadata;
  248. }
  249. internal static List<PluginInfo> LoadPlugins()
  250. {
  251. var list = PluginsMetadata.Select(LoadPlugin).Where(p => p != null).ToList();
  252. return list;
  253. }
  254. internal static PluginInfo LoadPlugin(PluginMetadata meta)
  255. {
  256. if (meta.PluginType == null)
  257. return new PluginInfo()
  258. {
  259. Metadata = meta,
  260. Plugin = null
  261. };
  262. var info = new PluginInfo();
  263. try
  264. {
  265. Logger.loader.Debug(meta.File.FullName);
  266. meta.Assembly = Assembly.LoadFrom(meta.File.FullName);
  267. var type = meta.Assembly.GetType(meta.PluginType.FullName);
  268. var instance = (IBeatSaberPlugin)Activator.CreateInstance(type);
  269. info.Metadata = meta;
  270. info.Plugin = instance;
  271. {
  272. var init = type.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  273. if (init != null)
  274. {
  275. var initArgs = new List<object>();
  276. var initParams = init.GetParameters();
  277. Logger modLogger = null;
  278. IModPrefs modPrefs = null;
  279. IConfigProvider cfgProvider = null;
  280. foreach (var param in initParams)
  281. {
  282. var paramType = param.ParameterType;
  283. if (paramType.IsAssignableFrom(typeof(Logger)))
  284. {
  285. if (modLogger == null) modLogger = new StandardLogger(meta.Name);
  286. initArgs.Add(modLogger);
  287. }
  288. else if (paramType.IsAssignableFrom(typeof(IModPrefs)))
  289. {
  290. if (modPrefs == null) modPrefs = new ModPrefs(instance);
  291. initArgs.Add(modPrefs);
  292. }
  293. else if (paramType.IsAssignableFrom(typeof(IConfigProvider)))
  294. {
  295. if (cfgProvider == null)
  296. {
  297. cfgProvider = Config.Config.GetProviderFor(Path.Combine("UserData", $"{meta.Name}"), param);
  298. }
  299. initArgs.Add(cfgProvider);
  300. }
  301. else
  302. initArgs.Add(paramType.GetDefault());
  303. }
  304. init.Invoke(instance, initArgs.ToArray());
  305. }
  306. }
  307. }
  308. catch (AmbiguousMatchException)
  309. {
  310. Logger.loader.Error($"Only one Init allowed per plugin (ambiguous match in {meta.Name})");
  311. return null;
  312. }
  313. catch (Exception e)
  314. {
  315. Logger.loader.Error($"Could not init plugin {meta.Name}: {e}");
  316. return null;
  317. }
  318. return info;
  319. }
  320. }
  321. }