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.

358 lines
16 KiB

  1. using IPA;
  2. using IPA.Config;
  3. using IPA.Config.ConfigProviders;
  4. using IPA.Logging;
  5. using IPA.Old;
  6. using IPA.Updating;
  7. using IPA.Utilities;
  8. using Mono.Cecil;
  9. using System;
  10. using System.Collections;
  11. using System.Collections.Generic;
  12. using System.Diagnostics;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Reflection;
  16. using System.Runtime.CompilerServices;
  17. using System.Runtime.InteropServices;
  18. using System.Text;
  19. using System.Threading.Tasks;
  20. namespace IPA.Loader
  21. {
  22. /// <summary>
  23. /// The manager class for all plugins.
  24. /// </summary>
  25. public static class PluginManager
  26. {
  27. #pragma warning disable CS0618 // Type or member is obsolete (IPlugin)
  28. /// <summary>
  29. /// A container object for all the data relating to a plugin.
  30. /// </summary>
  31. public class PluginInfo
  32. {
  33. internal IBeatSaberPlugin Plugin { get; set; }
  34. internal string Filename { get; set; }
  35. /// <summary>
  36. /// The Modsaber updating info for the mod, or null.
  37. /// </summary>
  38. public ModsaberModInfo ModsaberInfo { get; internal set; }
  39. }
  40. /// <summary>
  41. /// An <see cref="IEnumerable"/> of new Beat Saber plugins
  42. /// </summary>
  43. internal static IEnumerable<IBeatSaberPlugin> BSPlugins
  44. {
  45. get
  46. {
  47. if(_bsPlugins == null)
  48. {
  49. LoadPlugins();
  50. }
  51. return _bsPlugins.Select(p => p.Plugin);
  52. }
  53. }
  54. private static List<PluginInfo> _bsPlugins = null;
  55. internal static IEnumerable<PluginInfo> BSMetas
  56. {
  57. get
  58. {
  59. if (_bsPlugins == null)
  60. {
  61. LoadPlugins();
  62. }
  63. return _bsPlugins;
  64. }
  65. }
  66. /// <summary>
  67. /// Gets info about the plugin with the specified name.
  68. /// </summary>
  69. /// <param name="name">the name of the plugin to get (must be an exact match)</param>
  70. /// <returns>the plugin info for the requested plugin or null</returns>
  71. public static PluginInfo GetPlugin(string name)
  72. {
  73. return BSMetas.Where(p => p.Plugin.Name == name).FirstOrDefault();
  74. }
  75. /// <summary>
  76. /// Gets info about the plugin with the specified modsaber name.
  77. /// </summary>
  78. /// <param name="name">the modsaber name of the plugin to get (must be an exact match)</param>
  79. /// <returns>the plugin info for the requested plugin or null</returns>
  80. public static PluginInfo GetPluginFromModsaberName(string name)
  81. {
  82. return BSMetas.Where(p => p.ModsaberInfo.InternalName == name).FirstOrDefault();
  83. }
  84. /// <summary>
  85. /// An <see cref="IEnumerable"/> of old IPA plugins
  86. /// </summary>
  87. [Obsolete("I mean, IPlugin shouldn't be used, so why should this? Not renaming to extend support for old plugins.")]
  88. public static IEnumerable<IPlugin> Plugins
  89. {
  90. get
  91. {
  92. if (_ipaPlugins == null)
  93. {
  94. LoadPlugins();
  95. }
  96. return _ipaPlugins;
  97. }
  98. }
  99. private static List<IPlugin> _ipaPlugins = null;
  100. internal static IConfigProvider SelfConfigProvider { get; set; } = null;
  101. internal static List<KeyValuePair<IConfigProvider,Ref<DateTime>>> configProviders = new List<KeyValuePair<IConfigProvider, Ref<DateTime>>>();
  102. private static void LoadPlugins()
  103. {
  104. string pluginDirectory = Path.Combine(Environment.CurrentDirectory, "Plugins");
  105. // Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL,
  106. // so we need to resort to P/Invoke
  107. string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath);
  108. _bsPlugins = new List<PluginInfo>();
  109. _ipaPlugins = new List<IPlugin>();
  110. if (!Directory.Exists(pluginDirectory)) return;
  111. string cacheDir = Path.Combine(pluginDirectory, ".cache");
  112. if (!Directory.Exists(cacheDir))
  113. {
  114. Directory.CreateDirectory(cacheDir);
  115. }
  116. else
  117. {
  118. foreach (string plugin in Directory.GetFiles(cacheDir, "*"))
  119. {
  120. File.Delete(plugin);
  121. }
  122. }
  123. //Copy plugins to .cache
  124. string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll");
  125. foreach (string s in originalPlugins)
  126. {
  127. string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s));
  128. File.Copy(Path.Combine(pluginDirectory, s), pluginCopy);
  129. #region Fix assemblies for refactor
  130. var module = ModuleDefinition.ReadModule(Path.Combine(pluginDirectory, s));
  131. foreach (var @ref in module.AssemblyReferences)
  132. { // fix assembly references
  133. if (@ref.Name == "IllusionPlugin" || @ref.Name == "IllusionInjector")
  134. {
  135. @ref.Name = "IPA.Loader";
  136. }
  137. }
  138. foreach (var @ref in module.GetTypeReferences())
  139. { // fix type references
  140. if (@ref.FullName == "IllusionPlugin.IPlugin") @ref.Namespace = "IPA.Old"; //@ref.Name = "";
  141. if (@ref.FullName == "IllusionPlugin.IEnhancedPlugin") @ref.Namespace = "IPA.Old"; //@ref.Name = "";
  142. if (@ref.FullName == "IllusionPlugin.IBeatSaberPlugin") @ref.Namespace = "IPA"; //@ref.Name = "";
  143. if (@ref.FullName == "IllusionPlugin.IEnhancedBeatSaberPlugin") @ref.Namespace = "IPA"; //@ref.Name = "";
  144. if (@ref.FullName == "IllusionPlugin.BeatSaber.ModsaberModInfo") @ref.Namespace = "IPA"; //@ref.Name = "";
  145. if (@ref.FullName == "IllusionPlugin.IniFile") @ref.Namespace = "IPA.Config"; //@ref.Name = "";
  146. if (@ref.FullName == "IllusionPlugin.IModPrefs") @ref.Namespace = "IPA.Config"; //@ref.Name = "";
  147. if (@ref.FullName == "IllusionPlugin.ModPrefs") @ref.Namespace = "IPA.Config"; //@ref.Name = "";
  148. if (@ref.FullName == "IllusionPlugin.Utils.ReflectionUtil") @ref.Namespace = "IPA.Utilities"; //@ref.Name = "";
  149. if (@ref.FullName == "IllusionPlugin.Logging.Logger") @ref.Namespace = "IPA.Logging"; //@ref.Name = "";
  150. if (@ref.FullName == "IllusionPlugin.Logging.LogPrinter") @ref.Namespace = "IPA.Logging"; //@ref.Name = "";
  151. if (@ref.FullName == "IllusionInjector.PluginManager") @ref.Namespace = "IPA.Loader"; //@ref.Name = "";
  152. if (@ref.FullName == "IllusionInjector.PluginComponent") @ref.Namespace = "IPA.Loader"; //@ref.Name = "";
  153. if (@ref.FullName == "IllusionInjector.CompositeBSPlugin") @ref.Namespace = "IPA.Loader.Composite"; //@ref.Name = "";
  154. if (@ref.FullName == "IllusionInjector.CompositeIPAPlugin") @ref.Namespace = "IPA.Loader.Composite"; //@ref.Name = "";
  155. if (@ref.FullName == "IllusionInjector.Logging.UnityLogInterceptor") @ref.Namespace = "IPA.Logging"; //@ref.Name = "";
  156. if (@ref.FullName == "IllusionInjector.Logging.StandardLogger") @ref.Namespace = "IPA.Logging"; //@ref.Name = "";
  157. if (@ref.FullName == "IllusionInjector.Updating.SelfPlugin") @ref.Namespace = "IPA.Updating"; //@ref.Name = "";
  158. if (@ref.FullName == "IllusionInjector.Updating.Backup.BackupUnit") @ref.Namespace = "IPA.Updating.Backup"; //@ref.Name = "";
  159. if (@ref.Namespace == "IllusionInjector.Utilities") @ref.Namespace = "IPA.Utilities"; //@ref.Name = "";
  160. if (@ref.Namespace == "IllusionInjector.Logging.Printers") @ref.Namespace = "IPA.Logging.Printers"; //@ref.Name = "";
  161. if (@ref.Namespace == "IllusionInjector.Updating.ModsaberML") @ref.Namespace = "IPA.Updating.ModsaberML"; //@ref.Name = "";
  162. }
  163. module.Write(pluginCopy);
  164. #endregion
  165. }
  166. var selfPlugin = new PluginInfo
  167. {
  168. Filename = Path.Combine(Environment.CurrentDirectory, "IPA.exe"),
  169. Plugin = SelfPlugin.Instance
  170. };
  171. selfPlugin.ModsaberInfo = selfPlugin.Plugin.ModInfo;
  172. _bsPlugins.Add(selfPlugin);
  173. configProviders.Add(new KeyValuePair<IConfigProvider, Ref<DateTime>>(SelfConfigProvider = new JsonConfigProvider() { Filename = Path.Combine("UserData", SelfPlugin.IPA_Name) }, new Ref<DateTime>(SelfConfigProvider.LastModified)));
  174. SelfConfigProvider.Load();
  175. //Load copied plugins
  176. string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll");
  177. foreach (string s in copiedPlugins)
  178. {
  179. var result = LoadPluginsFromFile(s, exeName);
  180. _bsPlugins.AddRange(result.Item1);
  181. _ipaPlugins.AddRange(result.Item2);
  182. }
  183. Logger.log.Info(exeName);
  184. Logger.log.Info($"Running on Unity {UnityEngine.Application.unityVersion}");
  185. Logger.log.Info($"Game version {BeatSaber.GameVersion}");
  186. Logger.log.Info("-----------------------------");
  187. Logger.log.Info($"Loading plugins from {LoneFunctions.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}");
  188. Logger.log.Info("-----------------------------");
  189. foreach (var plugin in _bsPlugins)
  190. {
  191. Logger.log.Info($"{plugin.Plugin.Name}: {plugin.Plugin.Version}");
  192. }
  193. Logger.log.Info("-----------------------------");
  194. foreach (var plugin in _ipaPlugins)
  195. {
  196. Logger.log.Info($"{plugin.Name}: {plugin.Version}");
  197. }
  198. Logger.log.Info("-----------------------------");
  199. }
  200. private static Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>> LoadPluginsFromFile(string file, string exeName)
  201. {
  202. List<PluginInfo> bsPlugins = new List<PluginInfo>();
  203. List<IPlugin> ipaPlugins = new List<IPlugin>();
  204. if (!File.Exists(file) || !file.EndsWith(".dll", true, null))
  205. return new Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>>(bsPlugins, ipaPlugins);
  206. T OptionalGetPlugin<T>(Type t) where T : class
  207. {
  208. // use typeof() to allow for easier renaming (in an ideal world this compiles to a string, but ¯\_(ツ)_/¯)
  209. if (t.GetInterface(typeof(T).Name) != null)
  210. {
  211. try
  212. {
  213. T pluginInstance = Activator.CreateInstance(t) as T;
  214. string[] filter = null;
  215. if (pluginInstance is IGenericEnhancedPlugin)
  216. {
  217. filter = ((IGenericEnhancedPlugin)pluginInstance).Filter;
  218. }
  219. if (filter == null || filter.Contains(exeName, StringComparer.OrdinalIgnoreCase))
  220. return pluginInstance;
  221. }
  222. catch (Exception e)
  223. {
  224. Logger.loader.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}");
  225. }
  226. }
  227. return null;
  228. }
  229. try
  230. {
  231. Assembly assembly = Assembly.LoadFrom(file);
  232. foreach (Type t in assembly.GetTypes())
  233. {
  234. IBeatSaberPlugin bsPlugin = OptionalGetPlugin<IBeatSaberPlugin>(t);
  235. if (bsPlugin != null)
  236. {
  237. try
  238. {
  239. var init = t.GetMethod("Init", BindingFlags.Instance | BindingFlags.Public);
  240. if (init != null)
  241. {
  242. var initArgs = new List<object>();
  243. var initParams = init.GetParameters();
  244. Logger modLogger = null;
  245. IModPrefs modPrefs = null;
  246. IConfigProvider cfgProvider = null;
  247. foreach (var param in initParams)
  248. {
  249. var ptype = param.ParameterType;
  250. if (ptype.IsAssignableFrom(typeof(Logger))) {
  251. if (modLogger == null) modLogger = new StandardLogger(bsPlugin.Name);
  252. initArgs.Add(modLogger);
  253. }
  254. else if (ptype.IsAssignableFrom(typeof(IModPrefs)))
  255. {
  256. if (modPrefs == null) modPrefs = new ModPrefs(bsPlugin);
  257. initArgs.Add(modPrefs);
  258. }
  259. else if (ptype.IsAssignableFrom(typeof(IConfigProvider)))
  260. {
  261. if (cfgProvider == null)
  262. {
  263. cfgProvider = new JsonConfigProvider() { Filename = Path.Combine("UserData", $"{bsPlugin.Name}") };
  264. configProviders.Add(new KeyValuePair<IConfigProvider, Ref<DateTime>>(cfgProvider, new Ref<DateTime>(cfgProvider.LastModified)));
  265. cfgProvider.Load();
  266. }
  267. initArgs.Add(cfgProvider);
  268. }
  269. else
  270. initArgs.Add(ptype.GetDefault());
  271. }
  272. init.Invoke(bsPlugin, initArgs.ToArray());
  273. }
  274. bsPlugins.Add(new PluginInfo
  275. {
  276. Plugin = bsPlugin,
  277. Filename = file.Replace("\\.cache", ""), // quick and dirty fix
  278. ModsaberInfo = bsPlugin.ModInfo
  279. });
  280. }
  281. catch (AmbiguousMatchException)
  282. {
  283. Logger.loader.Error($"Only one Init allowed per plugin");
  284. }
  285. }
  286. else
  287. {
  288. IPlugin ipaPlugin = OptionalGetPlugin<IPlugin>(t);
  289. if (ipaPlugin != null)
  290. {
  291. ipaPlugins.Add(ipaPlugin);
  292. }
  293. }
  294. }
  295. }
  296. catch (Exception e)
  297. {
  298. Logger.loader.Error($"Could not load {Path.GetFileName(file)}! {e}");
  299. }
  300. return new Tuple<IEnumerable<PluginInfo>, IEnumerable<IPlugin>>(bsPlugins, ipaPlugins);
  301. }
  302. internal class AppInfo
  303. {
  304. [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = false)]
  305. private static extern int GetModuleFileName(HandleRef hModule, StringBuilder buffer, int length);
  306. private static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero);
  307. public static string StartupPath
  308. {
  309. get
  310. {
  311. StringBuilder stringBuilder = new StringBuilder(260);
  312. GetModuleFileName(NullHandleRef, stringBuilder, stringBuilder.Capacity);
  313. return stringBuilder.ToString();
  314. }
  315. }
  316. }
  317. #pragma warning restore CS0618 // Type or member is obsolete (IPlugin)
  318. }
  319. }