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.

257 lines
9.2 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.Logging;
  8. using IPA.Utilities;
  9. using Newtonsoft.Json;
  10. using Version = SemVer.Version;
  11. namespace IPA.Loader
  12. {
  13. /// <summary>
  14. /// A type to manage the loading of plugins.
  15. /// </summary>
  16. public class PluginLoader
  17. {
  18. /// <summary>
  19. /// The directory to load plugins from.
  20. /// </summary>
  21. public static string PluginsDirectory => Path.Combine(BeatSaber.InstallPath, "Plugins");
  22. internal static Task LoadTask() => Task.Run(() =>
  23. {
  24. LoadMetadata();
  25. Resolve();
  26. ComputeLoadOrder();
  27. });
  28. /// <summary>
  29. /// A class which describes
  30. /// </summary>
  31. public class PluginMetadata
  32. {
  33. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  34. /// <summary>
  35. /// The assembly the plugin was loaded from.
  36. /// </summary>
  37. public Assembly Assembly { get; internal set; }
  38. /// <summary>
  39. /// The Type that is the main type for the plugin.
  40. /// </summary>
  41. public Type PluginType { get; internal set; }
  42. /// <summary>
  43. /// The human readable name of the plugin.
  44. /// </summary>
  45. public string Name { get; internal set; }
  46. /// <summary>
  47. /// The ModSaber ID of the plugin, or null if it doesn't have one.
  48. /// </summary>
  49. public string Id { get; internal set; }
  50. /// <summary>
  51. /// The version of the plugin.
  52. /// </summary>
  53. public Version Version { get; internal set; }
  54. /// <summary>
  55. /// The file the plugin was loaded from.
  56. /// </summary>
  57. public FileInfo File { get; internal set; }
  58. // ReSharper disable once UnusedAutoPropertyAccessor.Global
  59. /// <summary>
  60. /// The features this plugin requests.
  61. /// </summary>
  62. public string[] Features { get; internal set; }
  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. Features = value.Features;
  74. }
  75. }
  76. /// <inheritdoc />
  77. public override string ToString() => $"{Name}({Id}@{Version})({PluginType.AssemblyQualifiedName}) from '{File.Name}'";
  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. internal string Filename { 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. internal static void LoadMetadata()
  93. {
  94. string[] plugins = Directory.GetFiles(PluginsDirectory, "*.dll");
  95. Assembly.ReflectionOnlyLoadFrom(Assembly.GetExecutingAssembly().Location); // load self as reflection only
  96. foreach (var plugin in plugins)
  97. { // should probably do patching first /shrug
  98. try
  99. {
  100. var metadata = new PluginMetadata();
  101. var assembly = Assembly.ReflectionOnlyLoadFrom(plugin);
  102. metadata.Assembly = assembly;
  103. metadata.File = new FileInfo(plugin);
  104. Type[] types;
  105. try
  106. {
  107. types = assembly.GetTypes();
  108. }
  109. catch (ReflectionTypeLoadException e)
  110. {
  111. types = e.Types;
  112. }
  113. foreach (var type in types)
  114. {
  115. if (type == null) continue;
  116. var iInterface = type.GetInterface(nameof(IBeatSaberPlugin));
  117. if (iInterface == null) continue;
  118. metadata.PluginType = type;
  119. break;
  120. }
  121. if (metadata.PluginType == null)
  122. {
  123. Logger.log.Warn($"Could not find plugin type for {Path.GetFileName(plugin)}");
  124. continue;
  125. }
  126. Stream metadataStream;
  127. try
  128. {
  129. metadataStream = assembly.GetManifestResourceStream(metadata.PluginType, "manifest.json");
  130. if (metadataStream == null)
  131. {
  132. Logger.log.Error($"manifest.json not found in plugin {Path.GetFileName(plugin)}");
  133. continue;
  134. }
  135. }
  136. catch (FileNotFoundException)
  137. {
  138. Logger.log.Error($"manifest.json not found in plugin {Path.GetFileName(plugin)}");
  139. continue;
  140. }
  141. string manifest;
  142. using (var manifestReader = new StreamReader(metadataStream))
  143. manifest = manifestReader.ReadToEnd();
  144. metadata.Manifest = JsonConvert.DeserializeObject<PluginManifest>(manifest);
  145. PluginsMetadata.Add(metadata);
  146. }
  147. catch (Exception e)
  148. {
  149. Logger.log.Error($"Could not load data for plugin {Path.GetFileName(plugin)}");
  150. Logger.log.Error(e);
  151. }
  152. }
  153. }
  154. internal static void Resolve()
  155. { // resolves duplicates and conflicts, etc
  156. PluginsMetadata.Sort((a, b) => a.Version.CompareTo(b.Version));
  157. var ids = new HashSet<string>();
  158. var ignore = new HashSet<PluginMetadata>();
  159. var resolved = new List<PluginMetadata>(PluginsMetadata.Count);
  160. foreach (var meta in PluginsMetadata)
  161. {
  162. if (meta.Id != null)
  163. {
  164. if (ids.Contains(meta.Id))
  165. {
  166. Logger.log.Warn($"Found duplicates of {meta.Id}, using newest");
  167. continue; // because of sorted order, hightest order will always be the first one
  168. }
  169. bool processedLater = false;
  170. foreach (var meta2 in PluginsMetadata)
  171. {
  172. if (ignore.Contains(meta2)) continue;
  173. if (meta == meta2)
  174. {
  175. processedLater = true;
  176. continue;
  177. }
  178. if (meta2.Manifest.Conflicts.ContainsKey(meta.Id))
  179. {
  180. var range = meta2.Manifest.Conflicts[meta.Id];
  181. if (range.IsSatisfied(meta.Version))
  182. {
  183. //TODO: actually choose the one most depended on
  184. Logger.log.Warn($"{meta.Id}@{meta.Version} conflicts with {meta2.Name}");
  185. if (processedLater)
  186. {
  187. Logger.log.Warn($"Ignoring {meta2.Name}");
  188. ignore.Add(meta2);
  189. }
  190. else
  191. {
  192. Logger.log.Warn($"Ignoring {meta.Name}");
  193. ignore.Add(meta);
  194. break;
  195. }
  196. }
  197. }
  198. }
  199. }
  200. if (ignore.Contains(meta)) continue;
  201. if (meta.Id != null) ids.Add(meta.Id);
  202. resolved.Add(meta);
  203. }
  204. PluginsMetadata = resolved;
  205. }
  206. internal static void ComputeLoadOrder()
  207. {
  208. PluginsMetadata.Sort((a, b) =>
  209. {
  210. if (a.Id == b.Id) return 0;
  211. if (a.Id != null)
  212. {
  213. if (b.Manifest.Dependencies.ContainsKey(a.Id) || b.Manifest.LoadAfter.Contains(a.Id)) return 1;
  214. if (b.Manifest.LoadBefore.Contains(a.Id)) return -1;
  215. }
  216. if (b.Id != null)
  217. {
  218. if (a.Manifest.Dependencies.ContainsKey(b.Id) || a.Manifest.LoadAfter.Contains(b.Id)) return -1;
  219. if (a.Manifest.LoadBefore.Contains(b.Id)) return 1;
  220. }
  221. return 0;
  222. });
  223. }
  224. internal static void LoadPlugins()
  225. {
  226. }
  227. }
  228. }