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.

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