From b22da6961244326963e19ed7bfc40f19d74909a7 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Sat, 4 Jan 2020 23:46:23 -0600 Subject: [PATCH] Refactored PluginMetadata and PluginExecutor out of PluginLoader --- IPA.Loader/Config/ModPrefs.cs | 2 +- IPA.Loader/IPA.Loader.csproj | 3 +- .../Loader/Features/ConfigProviderFeature.cs | 130 +-- IPA.Loader/Loader/Features/DefineFeature.cs | 142 +-- IPA.Loader/Loader/Features/Feature.cs | 446 ++++---- .../Loader/Features/InitInjectorFeature.cs | 207 ++-- .../Loader/Features/NoRuntimeEnableFeature.cs | 29 - IPA.Loader/Loader/Features/NoUpdateFeature.cs | 24 +- IPA.Loader/Loader/Features/PrintFeature.cs | 64 +- IPA.Loader/Loader/PluginExecutor.cs | 189 ++++ IPA.Loader/Loader/PluginInitInjector.cs | 10 +- IPA.Loader/Loader/PluginLoader.cs | 255 ----- IPA.Loader/Loader/PluginManager.cs | 953 +++++++++--------- IPA.Loader/Loader/PluginMetadata.cs | 94 ++ IPA.Loader/Loader/manifest.json | 51 +- 15 files changed, 1303 insertions(+), 1296 deletions(-) delete mode 100644 IPA.Loader/Loader/Features/NoRuntimeEnableFeature.cs create mode 100644 IPA.Loader/Loader/PluginExecutor.cs create mode 100644 IPA.Loader/Loader/PluginMetadata.cs diff --git a/IPA.Loader/Config/ModPrefs.cs b/IPA.Loader/Config/ModPrefs.cs index b9635c30..e1147395 100644 --- a/IPA.Loader/Config/ModPrefs.cs +++ b/IPA.Loader/Config/ModPrefs.cs @@ -103,7 +103,7 @@ namespace IPA.Config /// Constructs a ModPrefs object for the provide plugin. /// /// the plugin to get the preferences file for - public ModPrefs(PluginLoader.PluginMetadata plugin) { + public ModPrefs(PluginMetadata plugin) { _instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "ModPrefs", $"{plugin.Name}.ini")); } diff --git a/IPA.Loader/IPA.Loader.csproj b/IPA.Loader/IPA.Loader.csproj index c2bd030a..1687171b 100644 --- a/IPA.Loader/IPA.Loader.csproj +++ b/IPA.Loader/IPA.Loader.csproj @@ -112,15 +112,16 @@ - + + diff --git a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs index b22c71ee..caec1aad 100644 --- a/IPA.Loader/Loader/Features/ConfigProviderFeature.cs +++ b/IPA.Loader/Loader/Features/ConfigProviderFeature.cs @@ -1,65 +1,65 @@ -using System; -using System.IO; - -namespace IPA.Loader.Features -{ - internal class ConfigProviderFeature : Feature - { - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - {// parameters should be (fully qualified name of provider type) - if (parameters.Length != 1) - { - InvalidMessage = "Incorrect number of parameters"; - return false; - } - - RequireLoaded(meta); - - Type getType; - try - { - getType = meta.Assembly.GetType(parameters[0]); - } - catch (ArgumentException) - { - InvalidMessage = $"Invalid type name {parameters[0]}"; - return false; - } - catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) - { - string filename; - - switch (e) - { - case FileNotFoundException fn: - filename = fn.FileName; - goto hasFilename; - case FileLoadException fl: - filename = fl.FileName; - goto hasFilename; - case BadImageFormatException bi: - filename = bi.FileName; - hasFilename: - InvalidMessage = $"Could not find {filename} while loading type"; - break; - default: - InvalidMessage = $"Error while loading type: {e}"; - break; - } - - return false; - } - - try - { - Config.Config.Register(getType); - return true; - } - catch (Exception e) - { - InvalidMessage = $"Error generated while creating delegate: {e}"; - return false; - } - } - } -} +using System; +using System.IO; + +namespace IPA.Loader.Features +{ + internal class ConfigProviderFeature : Feature + { + public override bool Initialize(PluginMetadata meta, string[] parameters) + {// parameters should be (fully qualified name of provider type) + if (parameters.Length != 1) + { + InvalidMessage = "Incorrect number of parameters"; + return false; + } + + RequireLoaded(meta); + + Type getType; + try + { + getType = meta.Assembly.GetType(parameters[0]); + } + catch (ArgumentException) + { + InvalidMessage = $"Invalid type name {parameters[0]}"; + return false; + } + catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) + { + string filename; + + switch (e) + { + case FileNotFoundException fn: + filename = fn.FileName; + goto hasFilename; + case FileLoadException fl: + filename = fl.FileName; + goto hasFilename; + case BadImageFormatException bi: + filename = bi.FileName; + hasFilename: + InvalidMessage = $"Could not find {filename} while loading type"; + break; + default: + InvalidMessage = $"Error while loading type: {e}"; + break; + } + + return false; + } + + try + { + Config.Config.Register(getType); + return true; + } + catch (Exception e) + { + InvalidMessage = $"Error generated while creating delegate: {e}"; + return false; + } + } + } +} diff --git a/IPA.Loader/Loader/Features/DefineFeature.cs b/IPA.Loader/Loader/Features/DefineFeature.cs index 092c90e4..6835cc32 100644 --- a/IPA.Loader/Loader/Features/DefineFeature.cs +++ b/IPA.Loader/Loader/Features/DefineFeature.cs @@ -1,68 +1,74 @@ -using System; -using System.IO; - -namespace IPA.Loader.Features -{ - internal class DefineFeature : Feature - { - public static bool NewFeature = true; - - protected internal override bool StoreOnPlugin => false; - - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { // parameters should be (name, fully qualified type) - if (parameters.Length != 2) - { - InvalidMessage = "Incorrect number of parameters"; - return false; - } - - RequireLoaded(meta); - - Type type; - try - { - type = meta.Assembly.GetType(parameters[1]); - } - catch (ArgumentException) - { - InvalidMessage = $"Invalid type name {parameters[1]}"; - return false; - } - catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) - { - var filename = ""; - - switch (e) - { - case FileNotFoundException fn: - filename = fn.FileName; - break; - case FileLoadException fl: - filename = fl.FileName; - break; - case BadImageFormatException bi: - filename = bi.FileName; - break; - } - - InvalidMessage = $"Could not find {filename} while loading type"; - return false; - } - - try - { - if (RegisterFeature(parameters[0], type)) return NewFeature = true; - - InvalidMessage = $"Feature with name {parameters[0]} already exists"; - return false; - - } - catch (ArgumentException) - { - InvalidMessage = $"{type.FullName} not a subclass of {nameof(Feature)}"; - return false; - } - } - } -} +using System; +using System.IO; + +namespace IPA.Loader.Features +{ + internal class DefineFeature : Feature + { + public static bool NewFeature = true; + + protected internal override bool StoreOnPlugin => false; + + public override bool Initialize(PluginMetadata meta, string[] parameters) + { // parameters should be (name, fully qualified type) + if (parameters.Length != 2) + { + InvalidMessage = "Incorrect number of parameters"; + return false; + } + + RequireLoaded(meta); + + Type type; + try + { + type = meta.Assembly.GetType(parameters[1]); + } + catch (ArgumentException) + { + InvalidMessage = $"Invalid type name {parameters[1]}"; + return false; + } + catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) + { + var filename = ""; + + switch (e) + { + case FileNotFoundException fn: + filename = fn.FileName; + break; + case FileLoadException fl: + filename = fl.FileName; + break; + case BadImageFormatException bi: + filename = bi.FileName; + break; + } + + InvalidMessage = $"Could not find {filename} while loading type"; + return false; + } + + if (type == null) + { + InvalidMessage = $"Invalid type name {parameters[1]}"; + return false; + } + + try + { + if (RegisterFeature(parameters[0], type)) return NewFeature = true; + + InvalidMessage = $"Feature with name {parameters[0]} already exists"; + return false; + + } + catch (ArgumentException) + { + InvalidMessage = $"{type.FullName} not a subclass of {nameof(Feature)}"; + return false; + } + } + } +} diff --git a/IPA.Loader/Loader/Features/Feature.cs b/IPA.Loader/Loader/Features/Feature.cs index b790f2c0..039d3d3b 100644 --- a/IPA.Loader/Loader/Features/Feature.cs +++ b/IPA.Loader/Loader/Features/Feature.cs @@ -1,224 +1,224 @@ -using System; -using System.Collections.Generic; -using System.Text; -#if NET3 -using Net3_Proxy; -#endif - -namespace IPA.Loader.Features -{ - /// - /// The root interface for a mod Feature. - /// - /// - /// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. - /// - public abstract class Feature - { - /// - /// Initializes the feature with the parameters provided in the definition. - /// - /// Note: When no parenthesis are provided, is an empty array. - /// - /// - /// This gets called BEFORE *your* `Init` method. - /// - /// Returning does *not* prevent the plugin from being loaded. It simply prevents the feature from being used. - /// - /// the metadata of the plugin that is being prepared - /// the parameters passed to the feature definition, or null - /// if the feature is valid for the plugin, otherwise - public abstract bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters); - - /// - /// Evaluates the Feature for use in conditional meta-Features. This should be re-calculated on every call, unless it can be proven to not change. - /// - /// This will be called on every feature that returns from - /// - /// the truthiness of the Feature. - public virtual bool Evaluate() => true; - - /// - /// The message to be logged when the feature is not valid for a plugin. - /// This should also be set whenever either or returns false. - /// - /// the message to show when the feature is marked invalid - public virtual string InvalidMessage { get; protected set; } - - /// - /// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error. - /// - /// - /// The assembly will still be loaded, but the plugin will not be constructed if this returns . - /// Any features it defines, for example, will still be loaded. - /// - /// the plugin about to be loaded - /// whether or not the plugin should be loaded - public virtual bool BeforeLoad(PluginLoader.PluginMetadata plugin) => true; - - /// - /// Called before a plugin's `Init` method is called. This will not be called if there is no `Init` method. This should never throw an exception. An exception will abort the loading of the plugin with an error. - /// - /// the plugin to be initialized - /// whether or not to call the Init method - public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; - - /// - /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. - /// - /// the plugin that was just initialized - /// the instance of the plugin being initialized - public virtual void AfterInit(PluginLoader.PluginInfo plugin, IPlugin pluginInstance) => AfterInit(plugin); - - /// - /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. - /// - /// the plugin that was just initialized - public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } - - /// - /// Ensures a plugin's assembly is loaded. Do not use unless you need to. - /// - /// the plugin to ensure is loaded. - protected void RequireLoaded(PluginLoader.PluginMetadata plugin) => PluginLoader.Load(plugin); - - /// - /// Defines whether or not this feature will be accessible from the plugin metadata once loaded. - /// - /// if this will be stored on the plugin metadata, otherwise - protected internal virtual bool StoreOnPlugin => true; - - static Feature() - { - Reset(); - } - - internal static void Reset() - { - featureTypes = new Dictionary - { - { "define-feature", typeof(DefineFeature) } - }; - } - - private static Dictionary featureTypes; - - internal static bool HasFeature(string name) => featureTypes.ContainsKey(name); - - internal static bool RegisterFeature(string name, Type type) - { - if (!typeof(Feature).IsAssignableFrom(type)) - throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type)); - if (featureTypes.ContainsKey(name)) return false; - featureTypes.Add(name, type); - return true; - } - - internal struct FeatureParse - { - public readonly string Name; - public readonly string[] Parameters; - - public FeatureParse(string name, string[] parameters) - { - Name = name; - Parameters = parameters; - } - } - - // returns false with both outs null for no such feature - internal static bool TryParseFeature(string featureString, PluginLoader.PluginMetadata plugin, - out Feature feature, out Exception failException, out bool featureValid, out FeatureParse parsed, - FeatureParse? preParsed = null) - { - failException = null; - feature = null; - featureValid = false; - - if (preParsed == null) - { - var builder = new StringBuilder(); - string name = null; - var parameters = new List(); - - bool escape = false; - int parens = 0; - bool removeWhitespace = true; - foreach (var chr in featureString) - { - if (escape) - { - builder.Append(chr); - escape = false; - } - else - { - switch (chr) - { - case '\\': - escape = true; - break; - case '(': - parens++; - if (parens != 1) goto default; - removeWhitespace = true; - name = builder.ToString(); - builder.Clear(); - break; - case ')': - parens--; - if (parens != 0) goto default; - goto case ','; - case ',': - if (parens > 1) goto default; - parameters.Add(builder.ToString()); - builder.Clear(); - removeWhitespace = true; - break; - default: - if (removeWhitespace && !char.IsWhiteSpace(chr)) - removeWhitespace = false; - if (!removeWhitespace) - builder.Append(chr); - break; - } - } - } - - if (name == null) - name = builder.ToString(); - - parsed = new FeatureParse(name, parameters.ToArray()); - - if (parens != 0) - { - failException = new Exception("Malformed feature definition"); - return false; - } - } - else - parsed = preParsed.Value; - - if (!featureTypes.TryGetValue(parsed.Name, out var featureType)) - return false; - - try - { - if (!(Activator.CreateInstance(featureType) is Feature aFeature)) - { - failException = new InvalidCastException("Feature type not a subtype of Feature"); - return false; - } - - featureValid = aFeature.Initialize(plugin, parsed.Parameters); - feature = aFeature; - return true; - } - catch (Exception e) - { - failException = e; - return false; - } - } - } +using System; +using System.Collections.Generic; +using System.Text; +#if NET3 +using Net3_Proxy; +#endif + +namespace IPA.Loader.Features +{ + /// + /// The root interface for a mod Feature. + /// + /// + /// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. + /// + public abstract class Feature + { + /// + /// Initializes the feature with the parameters provided in the definition. + /// + /// Note: When no parenthesis are provided, is an empty array. + /// + /// + /// This gets called BEFORE *your* `Init` method. + /// + /// Returning does *not* prevent the plugin from being loaded. It simply prevents the feature from being used. + /// + /// the metadata of the plugin that is being prepared + /// the parameters passed to the feature definition, or null + /// if the feature is valid for the plugin, otherwise + public abstract bool Initialize(PluginMetadata meta, string[] parameters); + + /// + /// Evaluates the Feature for use in conditional meta-Features. This should be re-calculated on every call, unless it can be proven to not change. + /// + /// This will be called on every feature that returns from + /// + /// the truthiness of the Feature. + public virtual bool Evaluate() => true; + + /// + /// The message to be logged when the feature is not valid for a plugin. + /// This should also be set whenever either or returns false. + /// + /// the message to show when the feature is marked invalid + public virtual string InvalidMessage { get; protected set; } + + /// + /// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error. + /// + /// + /// The assembly will still be loaded, but the plugin will not be constructed if this returns . + /// Any features it defines, for example, will still be loaded. + /// + /// the plugin about to be loaded + /// whether or not the plugin should be loaded + public virtual bool BeforeLoad(PluginMetadata plugin) => true; + + /// + /// Called before a plugin's `Init` method is called. This will not be called if there is no `Init` method. This should never throw an exception. An exception will abort the loading of the plugin with an error. + /// + /// the plugin to be initialized + /// whether or not to call the Init method + public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; + + /// + /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. + /// + /// the plugin that was just initialized + /// the instance of the plugin being initialized + public virtual void AfterInit(PluginLoader.PluginInfo plugin, IPlugin pluginInstance) => AfterInit(plugin); + + /// + /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. + /// + /// the plugin that was just initialized + public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } + + /// + /// Ensures a plugin's assembly is loaded. Do not use unless you need to. + /// + /// the plugin to ensure is loaded. + protected void RequireLoaded(PluginMetadata plugin) => PluginLoader.Load(plugin); + + /// + /// Defines whether or not this feature will be accessible from the plugin metadata once loaded. + /// + /// if this will be stored on the plugin metadata, otherwise + protected internal virtual bool StoreOnPlugin => true; + + static Feature() + { + Reset(); + } + + internal static void Reset() + { + featureTypes = new Dictionary + { + { "define-feature", typeof(DefineFeature) } + }; + } + + private static Dictionary featureTypes; + + internal static bool HasFeature(string name) => featureTypes.ContainsKey(name); + + internal static bool RegisterFeature(string name, Type type) + { + if (!typeof(Feature).IsAssignableFrom(type)) + throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type)); + if (featureTypes.ContainsKey(name)) return false; + featureTypes.Add(name, type); + return true; + } + + internal struct FeatureParse + { + public readonly string Name; + public readonly string[] Parameters; + + public FeatureParse(string name, string[] parameters) + { + Name = name; + Parameters = parameters; + } + } + + // returns false with both outs null for no such feature + internal static bool TryParseFeature(string featureString, PluginMetadata plugin, + out Feature feature, out Exception failException, out bool featureValid, out FeatureParse parsed, + FeatureParse? preParsed = null) + { + failException = null; + feature = null; + featureValid = false; + + if (preParsed == null) + { + var builder = new StringBuilder(); + string name = null; + var parameters = new List(); + + bool escape = false; + int parens = 0; + bool removeWhitespace = true; + foreach (var chr in featureString) + { + if (escape) + { + builder.Append(chr); + escape = false; + } + else + { + switch (chr) + { + case '\\': + escape = true; + break; + case '(': + parens++; + if (parens != 1) goto default; + removeWhitespace = true; + name = builder.ToString(); + builder.Clear(); + break; + case ')': + parens--; + if (parens != 0) goto default; + goto case ','; + case ',': + if (parens > 1) goto default; + parameters.Add(builder.ToString()); + builder.Clear(); + removeWhitespace = true; + break; + default: + if (removeWhitespace && !char.IsWhiteSpace(chr)) + removeWhitespace = false; + if (!removeWhitespace) + builder.Append(chr); + break; + } + } + } + + if (name == null) + name = builder.ToString(); + + parsed = new FeatureParse(name, parameters.ToArray()); + + if (parens != 0) + { + failException = new Exception("Malformed feature definition"); + return false; + } + } + else + parsed = preParsed.Value; + + if (!featureTypes.TryGetValue(parsed.Name, out var featureType)) + return false; + + try + { + if (!(Activator.CreateInstance(featureType) is Feature aFeature)) + { + failException = new InvalidCastException("Feature type not a subtype of Feature"); + return false; + } + + featureValid = aFeature.Initialize(plugin, parsed.Parameters); + feature = aFeature; + return true; + } + catch (Exception e) + { + failException = e; + return false; + } + } + } } \ No newline at end of file diff --git a/IPA.Loader/Loader/Features/InitInjectorFeature.cs b/IPA.Loader/Loader/Features/InitInjectorFeature.cs index d4f069dc..af6487f9 100644 --- a/IPA.Loader/Loader/Features/InitInjectorFeature.cs +++ b/IPA.Loader/Loader/Features/InitInjectorFeature.cs @@ -1,102 +1,105 @@ -using System; -using System.IO; -using System.Reflection; - -namespace IPA.Loader.Features -{ - internal class InitInjectorFeature : Feature - { - protected internal override bool StoreOnPlugin => false; - - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { // parameters should be (assembly qualified lookup type, [fully qualified type]:[method name]) - // method should be static - if (parameters.Length != 2) - { - InvalidMessage = "Incorrect number of parameters"; - return false; - } - - RequireLoaded(meta); - - var methodParts = parameters[1].Split(':'); - - var type = Type.GetType(parameters[0], false); - if (type == null) - { - InvalidMessage = $"Could not find type {parameters[0]}"; - return false; - } - - Type getType; - try - { - getType = meta.Assembly.GetType(methodParts[0]); - } - catch (ArgumentException) - { - InvalidMessage = $"Invalid type name {methodParts[0]}"; - return false; - } - catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) - { - string filename; - - switch (e) - { - case FileNotFoundException fn: - filename = fn.FileName; - goto hasFilename; - case FileLoadException fl: - filename = fl.FileName; - goto hasFilename; - case BadImageFormatException bi: - filename = bi.FileName; - hasFilename: - InvalidMessage = $"Could not find {filename} while loading type"; - break; - default: - InvalidMessage = $"Error while loading type: {e}"; - break; - } - - return false; - } - - MethodInfo method; - try - { - method = getType.GetMethod(methodParts[1], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, - null, new[] - { - typeof(object), - typeof(ParameterInfo), - typeof(PluginLoader.PluginMetadata) - }, new ParameterModifier[0]); - } - catch (Exception e) - { - InvalidMessage = $"Error while loading type: {e}"; - return false; - } - - if (method == null) - { - InvalidMessage = $"Could not find method {methodParts[1]} in type {methodParts[0]}"; - return false; - } - - try - { - var del = (PluginInitInjector.InjectParameter)Delegate.CreateDelegate(typeof(PluginInitInjector.InjectParameter), null, method); - PluginInitInjector.AddInjector(type, del); - return true; - } - catch (Exception e) - { - InvalidMessage = $"Error generated while creating delegate: {e}"; - return false; - } - } - } -} +using System; +using System.IO; +using System.Reflection; +#if NET3 +using Array = Net3_Proxy.Array; +#endif + +namespace IPA.Loader.Features +{ + internal class InitInjectorFeature : Feature + { + protected internal override bool StoreOnPlugin => false; + + public override bool Initialize(PluginMetadata meta, string[] parameters) + { // parameters should be (assembly qualified lookup type, [fully qualified type]:[method name]) + // method should be static + if (parameters.Length != 2) + { + InvalidMessage = "Incorrect number of parameters"; + return false; + } + + RequireLoaded(meta); + + var methodParts = parameters[1].Split(':'); + + var type = Type.GetType(parameters[0], false); + if (type == null) + { + InvalidMessage = $"Could not find type {parameters[0]}"; + return false; + } + + Type getType; + try + { + getType = meta.Assembly.GetType(methodParts[0]); + } + catch (ArgumentException) + { + InvalidMessage = $"Invalid type name {methodParts[0]}"; + return false; + } + catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) + { + string filename; + + switch (e) + { + case FileNotFoundException fn: + filename = fn.FileName; + goto hasFilename; + case FileLoadException fl: + filename = fl.FileName; + goto hasFilename; + case BadImageFormatException bi: + filename = bi.FileName; + hasFilename: + InvalidMessage = $"Could not find {filename} while loading type"; + break; + default: + InvalidMessage = $"Error while loading type: {e}"; + break; + } + + return false; + } + + MethodInfo method; + try + { + method = getType.GetMethod(methodParts[1], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, + null, new[] + { + typeof(object), + typeof(ParameterInfo), + typeof(PluginMetadata) + }, Array.Empty()); + } + catch (Exception e) + { + InvalidMessage = $"Error while loading type: {e}"; + return false; + } + + if (method == null) + { + InvalidMessage = $"Could not find method {methodParts[1]} in type {methodParts[0]}"; + return false; + } + + try + { + var del = (PluginInitInjector.InjectParameter)Delegate.CreateDelegate(typeof(PluginInitInjector.InjectParameter), null, method); + PluginInitInjector.AddInjector(type, del); + return true; + } + catch (Exception e) + { + InvalidMessage = $"Error generated while creating delegate: {e}"; + return false; + } + } + } +} diff --git a/IPA.Loader/Loader/Features/NoRuntimeEnableFeature.cs b/IPA.Loader/Loader/Features/NoRuntimeEnableFeature.cs deleted file mode 100644 index 36fe6cfb..00000000 --- a/IPA.Loader/Loader/Features/NoRuntimeEnableFeature.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IPA.Loader.Features -{ - internal class NoRuntimeEnableFeature : Feature - { - internal static bool HaveLoadedPlugins = false; - - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { - return parameters.Length == 0; - } - - public override bool BeforeLoad(PluginLoader.PluginMetadata plugin) - { - return !HaveLoadedPlugins; - } - - public override string InvalidMessage - { - get => "Plugin requested to not be loaded after initial plugin load"; - protected set { } - } - } -} diff --git a/IPA.Loader/Loader/Features/NoUpdateFeature.cs b/IPA.Loader/Loader/Features/NoUpdateFeature.cs index ed5ebd97..afe38972 100644 --- a/IPA.Loader/Loader/Features/NoUpdateFeature.cs +++ b/IPA.Loader/Loader/Features/NoUpdateFeature.cs @@ -1,12 +1,12 @@ -namespace IPA.Loader.Features -{ - internal class NoUpdateFeature : Feature - { - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { - return meta.Id != null; - } - - public override string InvalidMessage { get; protected set; } = "No ID specified; cannot update anyway"; - } -} +namespace IPA.Loader.Features +{ + internal class NoUpdateFeature : Feature + { + public override bool Initialize(PluginMetadata meta, string[] parameters) + { + return meta.Id != null; + } + + public override string InvalidMessage { get; protected set; } = "No ID specified; cannot update anyway"; + } +} diff --git a/IPA.Loader/Loader/Features/PrintFeature.cs b/IPA.Loader/Loader/Features/PrintFeature.cs index 236aeb23..2acc48d8 100644 --- a/IPA.Loader/Loader/Features/PrintFeature.cs +++ b/IPA.Loader/Loader/Features/PrintFeature.cs @@ -1,32 +1,32 @@ - -using IPA.Logging; - -namespace IPA.Loader.Features -{ - internal class PrintFeature : Feature - { - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { - Logger.features.Info($"{meta.Name}: {string.Join(" ", parameters)}"); - return true; - } - } - - internal class DebugFeature : Feature - { - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { - Logger.features.Debug($"{meta.Name}: {string.Join(" ", parameters)}"); - return true; - } - } - - internal class WarnFeature : Feature - { - public override bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters) - { - Logger.features.Warn($"{meta.Name}: {string.Join(" ", parameters)}"); - return true; - } - } -} + +using IPA.Logging; + +namespace IPA.Loader.Features +{ + internal class PrintFeature : Feature + { + public override bool Initialize(PluginMetadata meta, string[] parameters) + { + Logger.features.Info($"{meta.Name}: {string.Join(" ", parameters)}"); + return true; + } + } + + internal class DebugFeature : Feature + { + public override bool Initialize(PluginMetadata meta, string[] parameters) + { + Logger.features.Debug($"{meta.Name}: {string.Join(" ", parameters)}"); + return true; + } + } + + internal class WarnFeature : Feature + { + public override bool Initialize(PluginMetadata meta, string[] parameters) + { + Logger.features.Warn($"{meta.Name}: {string.Join(" ", parameters)}"); + return true; + } + } +} diff --git a/IPA.Loader/Loader/PluginExecutor.cs b/IPA.Loader/Loader/PluginExecutor.cs new file mode 100644 index 00000000..f7a0bfc4 --- /dev/null +++ b/IPA.Loader/Loader/PluginExecutor.cs @@ -0,0 +1,189 @@ +using IPA.Logging; +using IPA.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Linq.Expressions; +#if NET4 +using Task = System.Threading.Tasks.Task; +#endif +#if NET3 +using Net3_Proxy; +using Path = Net3_Proxy.Path; +using File = Net3_Proxy.File; +using Directory = Net3_Proxy.Directory; +#endif + +namespace IPA.Loader +{ + internal class PluginExecutor + { + public PluginMetadata Metadata { get; } + public PluginExecutor(PluginMetadata meta) + { + Metadata = meta; + PrepareDelegates(); + } + + + private object pluginObject = null; + private Func CreatePlugin { get; set; } + private Action LifecycleEnable { get; set; } + // disable may be async (#24) + private Func LifecycleDisable { get; set; } + + public void Create() + { + if (pluginObject != null) return; + pluginObject = CreatePlugin(Metadata); + } + + public void Enable() => LifecycleEnable(pluginObject); + public Task Disable() => LifecycleDisable(pluginObject); + + + private void PrepareDelegates() + { // TODO: use custom exception types or something + PluginLoader.Load(Metadata); + var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName); + + CreatePlugin = MakeCreateFunc(type, Metadata.Name); + LifecycleEnable = MakeLifecycleEnableFunc(type, Metadata.Name); + LifecycleDisable = MakeLifecycleDisableFunc(type, Metadata.Name); + } + + private static Func MakeCreateFunc(Type type, string name) + { // TODO: what do i want the visibiliy of Init methods to be? + var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .Select(c => (c, attr: c.GetCustomAttribute())) + .NonNull(t => t.attr) + .OrderByDescending(t => t.c.GetParameters().Length) + .Select(t => t.c).ToArray(); + if (ctors.Length > 1) + Logger.loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters."); + + bool usingDefaultCtor = false; + var ctor = ctors.FirstOrDefault(); + if (ctor == null) + { // this is a normal case + usingDefaultCtor = true; + ctor = type.GetConstructor(Type.EmptyTypes); + if (ctor == null) + throw new InvalidOperationException($"{type.FullName} does not expose a public default constructor and has no constructors marked [Init]"); + } + + var initMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Select(m => (m, attr: m.GetCustomAttribute())) + .NonNull(t => t.attr).Select(t => t.m).ToArray(); + // verify that they don't have lifecycle attributes on them + foreach (var method in initMethods) + { + var attrs = method.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false); + if (attrs.Length != 0) + throw new InvalidOperationException($"Method {method} on {type.FullName} has both an [Init] attribute and a lifecycle attribute."); + } + + // TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3? + var metaParam = Expression.Parameter(typeof(PluginMetadata)); + var objVar = Expression.Variable(type); + var createExpr = Expression.Lambda>( + Expression.Block( + initMethods + .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es))) + .Prepend(Expression.Assign(objVar, + usingDefaultCtor + ? Expression.New(ctor) + : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, es => Expression.New(ctor, es)))) + .Append(Expression.Convert(objVar, typeof(object)))), + metaParam); + // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler + return createExpr.Compile(); + } + private static Action MakeLifecycleEnableFunc(Type type, string name) + { + var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) + .Select(t => (t.m, attrs: t.attrs.Cast())) + .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Enable)) + .Select(t => t.m).ToArray(); + if (enableMethods.Length == 0) + { + Logger.loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?"); + return o => { }; + } + + foreach (var m in enableMethods) + { + if (m.GetParameters().Length > 0) + throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters."); + if (m.ReturnType != typeof(void)) + Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored."); + } + + var objParam = Expression.Parameter(typeof(object)); + var instVar = Expression.Variable(type); + var createExpr = Expression.Lambda>( + Expression.Block( + enableMethods + .Select(m => Expression.Call(instVar, m)) + .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type)))), + objParam); + return createExpr.Compile(); + } + private static Func MakeLifecycleDisableFunc(Type type, string name) + { + var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) + .Select(t => (t.m, attrs: t.attrs.Cast())) + .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Disable)) + .Select(t => t.m).ToArray(); + if (disableMethods.Length == 0) + { + Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?"); + return o => Task.CompletedTask; + } + + var taskMethods = new List(); + var nonTaskMethods = new List(); + foreach (var m in disableMethods) + { + if (m.GetParameters().Length > 0) + throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and has parameters."); + if (m.ReturnType != typeof(void)) + { + if (typeof(Task).IsAssignableFrom(m.ReturnType)) + { + taskMethods.Add(m); + continue; + } + else + Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored."); + } + + nonTaskMethods.Add(m); + } + + Expression> completedTaskDel = () => Task.CompletedTask; + var getCompletedTask = completedTaskDel.Body; + var taskWhenAll = typeof(Task).GetMethod(nameof(Task.WhenAll), BindingFlags.Public | BindingFlags.Static); + + var objParam = Expression.Parameter(typeof(object)); + var instVar = Expression.Variable(type); + var createExpr = Expression.Lambda>( + Expression.Block( + nonTaskMethods + .Select(m => Expression.Call(instVar, m)) + .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type))) + .Append( + taskMethods.Count == 0 + ? getCompletedTask + : Expression.Call(taskWhenAll, + Expression.NewArrayInit(typeof(Task), + taskMethods.Select(m => + Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))), + objParam); + return createExpr.Compile(); + } + } +} \ No newline at end of file diff --git a/IPA.Loader/Loader/PluginInitInjector.cs b/IPA.Loader/Loader/PluginInitInjector.cs index d8d5d629..c88ca9f1 100644 --- a/IPA.Loader/Loader/PluginInitInjector.cs +++ b/IPA.Loader/Loader/PluginInitInjector.cs @@ -23,9 +23,9 @@ namespace IPA.Loader /// /// the previous return value of the function, or if never called for plugin. /// the of the parameter being injected. - /// the for the plugin being loaded. + /// the for the plugin being loaded. /// the value to inject into that parameter. - public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta); + public delegate object InjectParameter(object previous, ParameterInfo param, PluginMetadata meta); /// /// Adds an injector to be used when calling future plugins' Init methods. @@ -45,7 +45,7 @@ namespace IPA.Loader public TypedInjector(Type t, InjectParameter i) { Type = t; Injector = i; } - public object Inject(object prev, ParameterInfo info, PluginLoader.PluginMetadata meta) + public object Inject(object prev, ParameterInfo info, PluginMetadata meta) => Injector(prev, info, meta); public bool Equals(TypedInjector other) @@ -65,7 +65,7 @@ namespace IPA.Loader private static readonly List injectors = new List { new TypedInjector(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), - new TypedInjector(typeof(PluginLoader.PluginMetadata), (prev, param, meta) => prev ?? meta), + new TypedInjector(typeof(PluginMetadata), (prev, param, meta) => prev ?? meta), new TypedInjector(typeof(Config.Config), (prev, param, meta) => { if (prev != null) return prev; @@ -107,7 +107,7 @@ namespace IPA.Loader Expression.ArrayIndex(arr, Expression.Constant(i)), t)))); } - internal static object[] Inject(ParameterInfo[] initParams, PluginLoader.PluginMetadata meta) + internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta) { var initArgs = new List(); diff --git a/IPA.Loader/Loader/PluginLoader.cs b/IPA.Loader/Loader/PluginLoader.cs index d16550ed..ef7893e2 100644 --- a/IPA.Loader/Loader/PluginLoader.cs +++ b/IPA.Loader/Loader/PluginLoader.cs @@ -13,7 +13,6 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Version = SemVer.Version; using SemVer; -using System.Linq.Expressions; #if NET4 using Task = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task; @@ -45,260 +44,6 @@ namespace IPA.Loader ResolveDependencies(); }); - /// - /// A class which describes a loaded plugin. - /// - public class PluginMetadata - { - /// - /// The assembly the plugin was loaded from. - /// - /// the loaded Assembly that contains the plugin main type - public Assembly Assembly { get; internal set; } - - /// - /// The TypeDefinition for the main type of the plugin. - /// - /// the Cecil definition for the plugin main type - public TypeDefinition PluginType { get; internal set; } - - /// - /// The human readable name of the plugin. - /// - /// the name of the plugin - public string Name { get; internal set; } - - /// - /// The BeatMods ID of the plugin, or null if it doesn't have one. - /// - /// the updater ID of the plugin - public string Id { get; internal set; } - - /// - /// The version of the plugin. - /// - /// the version of the plugin - public Version Version { get; internal set; } - - /// - /// The file the plugin was loaded from. - /// - /// the file the plugin was loaded from - public FileInfo File { get; internal set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Global - /// - /// The features this plugin requests. - /// - /// the list of features requested by the plugin - public IReadOnlyList Features => InternalFeatures; - - internal readonly List InternalFeatures = new List(); - - internal bool IsSelf; - - /// - /// Whether or not this metadata object represents a bare manifest. - /// - /// if it is bare, otherwise - public bool IsBare { get; internal set; } - - private PluginManifest manifest; - - internal HashSet Dependencies { get; } = new HashSet(); - - internal PluginManifest Manifest - { - get => manifest; - set - { - manifest = value; - Name = value.Name; - Version = value.Version; - Id = value.Id; - } - } - - public RuntimeOptions RuntimeOptions { get; internal set; } - public bool IsAttributePlugin { get; internal set; } = false; - - /// - /// Gets all of the metadata as a readable string. - /// - /// the readable printable metadata string - public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; - } - - internal class PluginExecutor - { - public PluginMetadata Metadata { get; } - public PluginExecutor(PluginMetadata meta) - { - Metadata = meta; - PrepareDelegates(); - } - - - private object pluginObject = null; - private Func CreatePlugin { get; set; } - private Action LifecycleEnable { get; set; } - // disable may be async (#24) - private Func LifecycleDisable { get; set; } - - public void Create() - { - if (pluginObject != null) return; - pluginObject = CreatePlugin(Metadata); - } - - public void Enable() => LifecycleEnable(pluginObject); - public Task Disable() => LifecycleDisable(pluginObject); - - - private void PrepareDelegates() - { // TODO: use custom exception types or something - Load(Metadata); - var type = Metadata.Assembly.GetType(Metadata.PluginType.FullName); - - CreatePlugin = MakeCreateFunc(type, Metadata.Name); - LifecycleEnable = MakeLifecycleEnableFunc(type, Metadata.Name); - LifecycleDisable = MakeLifecycleDisableFunc(type, Metadata.Name); - } - - private static Func MakeCreateFunc(Type type, string name) - { // TODO: what do i want the visibiliy of Init methods to be? - var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) - .Select(c => (c, attr: c.GetCustomAttribute())) - .NonNull(t => t.attr) - .OrderByDescending(t => t.c.GetParameters().Length) - .Select(t => t.c).ToArray(); - if (ctors.Length > 1) - Logger.loader.Warn($"Plugin {name} has multiple [Init] constructors. Picking the one with the most parameters."); - - bool usingDefaultCtor = false; - var ctor = ctors.FirstOrDefault(); - if (ctor == null) - { // this is a normal case - usingDefaultCtor = true; - ctor = type.GetConstructor(Type.EmptyTypes); - if (ctor == null) - throw new InvalidOperationException($"{type.FullName} does not expose a public default constructor and has no constructors marked [Init]"); - } - - var initMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Select(m => (m, attr: m.GetCustomAttribute())) - .NonNull(t => t.attr).Select(t => t.m).ToArray(); - // verify that they don't have lifecycle attributes on them - foreach (var method in initMethods) - { - var attrs = method.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false); - if (attrs.Length != 0) - throw new InvalidOperationException($"Method {method} on {type.FullName} has both an [Init] attribute and a lifecycle attribute."); - } - - // TODO: how do I make this work for .NET 3? FEC.LightExpression but hacked to work on .NET 3? - var metaParam = Expression.Parameter(typeof(PluginMetadata)); - var objVar = Expression.Variable(type); - var createExpr = Expression.Lambda>( - Expression.Block( - initMethods - .Select(m => PluginInitInjector.InjectedCallExpr(m.GetParameters(), metaParam, es => Expression.Call(objVar, m, es))) - .Prepend(Expression.Assign(objVar, - usingDefaultCtor - ? Expression.New(ctor) - : PluginInitInjector.InjectedCallExpr(ctor.GetParameters(), metaParam, es => Expression.New(ctor, es)))) - .Append(Expression.Convert(objVar, typeof(object)))), - metaParam); - // TODO: since this new system will be doing a fuck load of compilation, maybe add FastExpressionCompiler - return createExpr.Compile(); - } - private static Action MakeLifecycleEnableFunc(Type type, string name) - { - var enableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) - .Select(t => (t.m, attrs: t.attrs.Cast())) - .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Enable)) - .Select(t => t.m).ToArray(); - if (enableMethods.Length == 0) - { - Logger.loader.Notice($"Plugin {name} has no methods marked [OnStart] or [OnEnable]. Is this intentional?"); - return o => { }; - } - - foreach (var m in enableMethods) - { - if (m.GetParameters().Length > 0) - throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and has parameters."); - if (m.ReturnType != typeof(void)) - Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnStart] or [OnEnable] and returns a value. It will be ignored."); - } - - var objParam = Expression.Parameter(typeof(object)); - var instVar = Expression.Variable(type); - var createExpr = Expression.Lambda>( - Expression.Block( - enableMethods - .Select(m => Expression.Call(instVar, m)) - .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type)))), - objParam); - return createExpr.Compile(); - } - private static Func MakeLifecycleDisableFunc(Type type, string name) - { - var disableMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance) - .Select(m => (m, attrs: m.GetCustomAttributes(typeof(IEdgeLifecycleAttribute), false))) - .Select(t => (t.m, attrs: t.attrs.Cast())) - .Where(t => t.attrs.Any(a => a.Type == EdgeLifecycleType.Disable)) - .Select(t => t.m).ToArray(); - if (disableMethods.Length == 0) - { - Logger.loader.Notice($"Plugin {name} has no methods marked [OnExit] or [OnDisable]. Is this intentional?"); - return o => Task.CompletedTask; - } - - var taskMethods = new List(); - var nonTaskMethods = new List(); - foreach (var m in disableMethods) - { - if (m.GetParameters().Length > 0) - throw new InvalidOperationException($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and has parameters."); - if (m.ReturnType != typeof(void)) - { - if (typeof(Task).IsAssignableFrom(m.ReturnType)) - { - taskMethods.Add(m); - continue; - } - else - Logger.loader.Warn($"Method {m} on {type.FullName} is marked [OnExit] or [OnDisable] and returns a non-Task value. It will be ignored."); - } - - nonTaskMethods.Add(m); - } - - Expression> completedTaskDel = () => Task.CompletedTask; - var getCompletedTask = completedTaskDel.Body; - var taskWhenAll = typeof(Task).GetMethod(nameof(Task.WhenAll), BindingFlags.Public | BindingFlags.Static); - - var objParam = Expression.Parameter(typeof(object)); - var instVar = Expression.Variable(type); - var createExpr = Expression.Lambda>( - Expression.Block( - nonTaskMethods - .Select(m => Expression.Call(instVar, m)) - .Prepend(Expression.Assign(instVar, Expression.Convert(objParam, type))) - .Append( - taskMethods.Count == 0 - ? getCompletedTask - : Expression.Call(taskWhenAll, - Expression.NewArrayInit(typeof(Task), - taskMethods.Select(m => - Expression.Convert(Expression.Call(instVar, m), typeof(Task))))))), - objParam); - return createExpr.Compile(); - } - } - /// /// A container object for all the data relating to a plugin. /// diff --git a/IPA.Loader/Loader/PluginManager.cs b/IPA.Loader/Loader/PluginManager.cs index 03d57dca..34d52def 100644 --- a/IPA.Loader/Loader/PluginManager.cs +++ b/IPA.Loader/Loader/PluginManager.cs @@ -1,477 +1,476 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Text; -using IPA.Config; -using IPA.Old; -using IPA.Utilities; -using Mono.Cecil; -using UnityEngine; -using Logger = IPA.Logging.Logger; -using static IPA.Loader.PluginLoader; -using IPA.Loader.Features; -#if NET3 -using Net3_Proxy; -using Path = Net3_Proxy.Path; -using File = Net3_Proxy.File; -using Directory = Net3_Proxy.Directory; -#endif - -namespace IPA.Loader -{ - /// - /// The manager class for all plugins. - /// - public static class PluginManager - { -#pragma warning disable CS0618 // Type or member is obsolete (IPlugin) - - /// - /// An of new Beat Saber plugins - /// - internal static IEnumerable BSPlugins => (_bsPlugins ?? throw new InvalidOperationException()).Select(p => p.Plugin); - private static List _bsPlugins; - internal static IEnumerable BSMetas => _bsPlugins; - - /// - /// Gets info about the plugin with the specified name. - /// - /// the name of the plugin to get (must be an exact match) - /// the plugin info for the requested plugin or null - public static PluginInfo GetPlugin(string name) - { - return BSMetas.FirstOrDefault(p => p.Metadata.Name == name); - } - - /// - /// Gets info about the plugin with the specified ModSaber name. - /// - /// the ModSaber name of the plugin to get (must be an exact match) - /// the plugin info for the requested plugin or null - [Obsolete("Old name. Use GetPluginFromId instead.")] - public static PluginInfo GetPluginFromModSaberName(string name) => GetPluginFromId(name); - - /// - /// Gets info about the plugin with the specified ID. - /// - /// the ID name of the plugin to get (must be an exact match) - /// the plugin info for the requested plugin or null - public static PluginInfo GetPluginFromId(string name) - { - return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); - } - - /// - /// Gets a disabled plugin's metadata by its name. - /// - /// the name of the disabled plugin to get - /// the metadata for the corresponding plugin - public static PluginMetadata GetDisabledPlugin(string name) => - DisabledPlugins.FirstOrDefault(p => p.Name == name); - - /// - /// Gets a disabled plugin's metadata by its ID. - /// - /// the ID of the disabled plugin to get - /// the metadata for the corresponding plugin - public static PluginMetadata GetDisabledPluginFromId(string name) => - DisabledPlugins.FirstOrDefault(p => p.Id == name); - - /// - /// Disables a plugin, and all dependents. - /// - /// the plugin to disable - /// whether or not it needs a restart to enable - public static bool DisablePlugin(PluginInfo plugin) - { - if (plugin == null) return false; - - if (plugin.Metadata.IsBare) - { - Logger.loader.Warn($"Trying to disable bare manifest"); - return false; - } - - if (IsDisabled(plugin.Metadata)) return false; - - var needsRestart = false; - - Logger.loader.Info($"Disabling {plugin.Metadata.Name}"); - - var dependents = BSMetas.Where(m => m.Metadata.Dependencies.Contains(plugin.Metadata)).ToList(); - needsRestart = dependents.Aggregate(needsRestart, (b, p) => DisablePlugin(p) || b); - - DisabledConfig.Instance.DisabledModIds.Add(plugin.Metadata.Id ?? plugin.Metadata.Name); - - if (!needsRestart && plugin.Plugin is IDisablablePlugin disable) - { - try - { - disable.OnDisable(); - } - catch (Exception e) - { - Logger.loader.Error($"Error occurred trying to disable {plugin.Metadata.Name}"); - Logger.loader.Error(e); - } - - if (needsRestart) - Logger.loader.Warn($"Disablable plugin has non-disablable dependents; some things may not work properly"); - } - else needsRestart = true; - - runtimeDisabled.Add(plugin); - _bsPlugins.Remove(plugin); - - try - { - PluginDisabled?.Invoke(plugin.Metadata, needsRestart); - } - catch (Exception e) - { - Logger.loader.Error($"Error occurred invoking disable event for {plugin.Metadata.Name}"); - Logger.loader.Error(e); - } - - return needsRestart; - } - - /// - /// Disables a plugin, and all dependents. - /// - /// the ID, or name if the ID is null, of the plugin to disable - /// whether a restart is needed to activate - public static bool DisablePlugin(string pluginId) => DisablePlugin(GetPluginFromId(pluginId) ?? GetPlugin(pluginId)); - - /// - /// Enables a plugin that had been previously disabled. - /// - /// the plugin to enable - /// whether a restart is needed to activate - public static bool EnablePlugin(PluginMetadata plugin) - { // TODO: fix some of this behaviour, by adding better support for runtime enable/disable of mods - if (plugin == null) return false; - - if (plugin.IsBare) - { - Logger.loader.Warn($"Trying to enable bare manifest"); - return false; - } - - if (!IsDisabled(plugin)) return false; - - Logger.loader.Info($"Enabling {plugin.Name}"); - - DisabledConfig.Instance.DisabledModIds.Remove(plugin.Id ?? plugin.Name); - - var needsRestart = true; - - var depsNeedRestart = plugin.Dependencies.Aggregate(false, (b, p) => EnablePlugin(p) || b); - - var runtimeInfo = runtimeDisabled.FirstOrDefault(p => p.Metadata == plugin); - if (runtimeInfo != null && runtimeInfo.Plugin is IPlugin newPlugin) - { - try - { - newPlugin.OnEnable(); - } - catch (Exception e) - { - Logger.loader.Error($"Error occurred trying to enable {plugin.Name}"); - Logger.loader.Error(e); - } - needsRestart = false; - } - else - { - PluginLoader.DisabledPlugins.Remove(plugin); - if (runtimeInfo == null) - { - runtimeInfo = InitPlugin(plugin, AllPlugins.Select(i => i.Metadata)); - needsRestart = false; - } - } - - if (runtimeInfo != null) - runtimeDisabled.Remove(runtimeInfo); - - _bsPlugins.Add(runtimeInfo); - - try - { - PluginEnabled?.Invoke(runtimeInfo, needsRestart || depsNeedRestart); - } - catch (Exception e) - { - Logger.loader.Error($"Error occurred invoking enable event for {plugin.Name}"); - Logger.loader.Error(e); - } - - return needsRestart || depsNeedRestart; - } - - /// - /// Enables a plugin that had been previously disabled. - /// - /// the ID, or name if the ID is null, of the plugin to enable - /// whether a restart is needed to activate - public static bool EnablePlugin(string pluginId) => - EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId)); - - /// - /// Checks if a given plugin is disabled. - /// - /// the plugin to check - /// if the plugin is disabled, otherwise. - public static bool IsDisabled(PluginMetadata meta) => DisabledPlugins.Contains(meta); - - /// - /// Checks if a given plugin is enabled. - /// - /// the plugin to check - /// if the plugin is enabled, otherwise. - public static bool IsEnabled(PluginMetadata meta) => BSMetas.Any(p => p.Metadata == meta); - - private static readonly List runtimeDisabled = new List(); - /// - /// Gets a list of disabled BSIPA plugins. - /// - /// a collection of all disabled plugins as - public static IEnumerable DisabledPlugins => PluginLoader.DisabledPlugins.Concat(runtimeDisabled.Select(p => p.Metadata)); - - /// - /// An invoker for the event. - /// - /// the plugin that was enabled - /// whether it needs a restart to take effect - public delegate void PluginEnableDelegate(PluginInfo plugin, bool needsRestart); - /// - /// An invoker for the event. - /// - /// the plugin that was disabled - /// whether it needs a restart to take effect - public delegate void PluginDisableDelegate(PluginMetadata plugin, bool needsRestart); - - /// - /// Called whenever a plugin is enabled. - /// - public static event PluginEnableDelegate PluginEnabled; - /// - /// Called whenever a plugin is disabled. - /// - public static event PluginDisableDelegate PluginDisabled; - - /// - /// Gets a list of all BSIPA plugins. - /// - /// a collection of all enabled plugins as s - public static IEnumerable AllPlugins => BSMetas; - - /// - /// Converts a plugin's metadata to a . - /// - /// the metadata - /// the plugin info - public static PluginInfo InfoFromMetadata(PluginMetadata meta) - { - if (IsDisabled(meta)) - return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta); - else - return AllPlugins.FirstOrDefault(p => p.Metadata == meta); - } - - /// - /// An of old IPA plugins. - /// - /// all legacy plugin instances - [Obsolete("I mean, IPlugin shouldn't be used, so why should this? Not renaming to extend support for old plugins.")] - public static IEnumerable Plugins => _ipaPlugins; - private static List _ipaPlugins; - - internal static IConfigProvider SelfConfigProvider { get; set; } - - internal static void Load() - { - string pluginDirectory = BeatSaber.PluginsPath; - - // Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL, - // so we need to resort to P/Invoke - string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath); - _bsPlugins = new List(); - _ipaPlugins = new List(); - - if (!Directory.Exists(pluginDirectory)) return; - - string cacheDir = Path.Combine(pluginDirectory, ".cache"); - - if (!Directory.Exists(cacheDir)) - { - Directory.CreateDirectory(cacheDir); - } - else - { - foreach (string plugin in Directory.GetFiles(cacheDir, "*")) - { - File.Delete(plugin); - } - } - - // initialize BSIPA plugins first - _bsPlugins.AddRange(PluginLoader.LoadPlugins()); - NoRuntimeEnableFeature.HaveLoadedPlugins = true; - - var metadataPaths = PluginsMetadata.Select(m => m.File.FullName).ToList(); - var ignoredPaths = ignoredPlugins.Select(m => m.Key.File.FullName).ToList(); - var disabledPaths = DisabledPlugins.Select(m => m.File.FullName).ToList(); - - //Copy plugins to .cache - string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll"); - foreach (string s in originalPlugins) - { - if (metadataPaths.Contains(s)) continue; - if (ignoredPaths.Contains(s)) continue; - if (disabledPaths.Contains(s)) continue; - string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s)); - - #region Fix assemblies for refactor - - var module = ModuleDefinition.ReadModule(Path.Combine(pluginDirectory, s)); - foreach (var @ref in module.AssemblyReferences) - { // fix assembly references - if (@ref.Name == "IllusionPlugin" || @ref.Name == "IllusionInjector") - { - @ref.Name = "IPA.Loader"; - } - } - - foreach (var @ref in module.GetTypeReferences()) - { // fix type references - if (@ref.FullName == "IllusionPlugin.IPlugin") @ref.Namespace = "IPA.Old"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.IEnhancedPlugin") @ref.Namespace = "IPA.Old"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.IBeatSaberPlugin") { @ref.Namespace = "IPA"; @ref.Name = nameof(IPlugin); } - if (@ref.FullName == "IllusionPlugin.IEnhancedBeatSaberPlugin") { @ref.Namespace = "IPA"; @ref.Name = nameof(IEnhancedPlugin); } - if (@ref.FullName == "IllusionPlugin.IniFile") @ref.Namespace = "IPA.Config"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.IModPrefs") @ref.Namespace = "IPA.Config"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.ModPrefs") @ref.Namespace = "IPA.Config"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.Utils.ReflectionUtil") @ref.Namespace = "IPA.Utilities"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.Logging.Logger") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; - if (@ref.FullName == "IllusionPlugin.Logging.LogPrinter") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.PluginManager") @ref.Namespace = "IPA.Loader"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.PluginComponent") @ref.Namespace = "IPA.Loader"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.CompositeBSPlugin") @ref.Namespace = "IPA.Loader.Composite"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.CompositeIPAPlugin") @ref.Namespace = "IPA.Loader.Composite"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.Logging.UnityLogInterceptor") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.Logging.StandardLogger") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.Updating.SelfPlugin") @ref.Namespace = "IPA.Updating"; //@ref.Name = ""; - if (@ref.FullName == "IllusionInjector.Updating.Backup.BackupUnit") @ref.Namespace = "IPA.Updating.Backup"; //@ref.Name = ""; - if (@ref.Namespace == "IllusionInjector.Utilities") @ref.Namespace = "IPA.Utilities"; //@ref.Name = ""; - if (@ref.Namespace == "IllusionInjector.Logging.Printers") @ref.Namespace = "IPA.Logging.Printers"; //@ref.Name = ""; - } - module.Write(pluginCopy); - - #endregion - } - - //Load copied plugins - string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll"); - foreach (string s in copiedPlugins) - { - var result = LoadPluginsFromFile(s); - if (result == null) continue; - _ipaPlugins.AddRange(result.NonNull()); - } - - Logger.log.Info(exeName); - Logger.log.Info($"Running on Unity {Application.unityVersion}"); - Logger.log.Info($"Game version {BeatSaber.GameVersion}"); - Logger.log.Info("-----------------------------"); - Logger.log.Info($"Loading plugins from {Utils.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); - Logger.log.Info("-----------------------------"); - foreach (var plugin in _bsPlugins) - { - Logger.log.Info($"{plugin.Metadata.Name} ({plugin.Metadata.Id}): {plugin.Metadata.Version}"); - } - Logger.log.Info("-----------------------------"); - foreach (var plugin in _ipaPlugins) - { - Logger.log.Info($"{plugin.Name}: {plugin.Version}"); - } - Logger.log.Info("-----------------------------"); - } - - private static IEnumerable LoadPluginsFromFile(string file) - { - var ipaPlugins = new List(); - - if (!File.Exists(file) || !file.EndsWith(".dll", true, null)) - return ipaPlugins; - - T OptionalGetPlugin(Type t) where T : class - { - if (t.FindInterfaces((t, o) => t == (o as Type), typeof(T)).Length > 0) - { - try - { - T pluginInstance = Activator.CreateInstance(t) as T; - return pluginInstance; - } - catch (Exception e) - { - Logger.loader.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}"); - } - } - - return null; - } - - try - { - Assembly assembly = Assembly.LoadFrom(file); - - foreach (Type t in assembly.GetTypes()) - { - - var ipaPlugin = OptionalGetPlugin(t); - if (ipaPlugin != null) - { - ipaPlugins.Add(ipaPlugin); - } - } - - } - catch (ReflectionTypeLoadException e) - { - Logger.loader.Error($"Could not load the following types from {Path.GetFileName(file)}:"); - Logger.loader.Error($" {string.Join("\n ", e.LoaderExceptions?.Select(e1 => e1?.Message).StrJP() ?? new string[0])}"); - } - catch (Exception e) - { - Logger.loader.Error($"Could not load {Path.GetFileName(file)}!"); - Logger.loader.Error(e); - } - - return ipaPlugins; - } - - internal static class AppInfo - { - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = false)] - private static extern int GetModuleFileName(HandleRef hModule, StringBuilder buffer, int length); - private static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); - public static string StartupPath - { - get - { - StringBuilder stringBuilder = new StringBuilder(260); - GetModuleFileName(NullHandleRef, stringBuilder, stringBuilder.Capacity); - return stringBuilder.ToString(); - } - } - } -#pragma warning restore CS0618 // Type or member is obsolete (IPlugin) - } -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using IPA.Config; +using IPA.Old; +using IPA.Utilities; +using Mono.Cecil; +using UnityEngine; +using Logger = IPA.Logging.Logger; +using static IPA.Loader.PluginLoader; +using IPA.Loader.Features; +#if NET3 +using Net3_Proxy; +using Path = Net3_Proxy.Path; +using File = Net3_Proxy.File; +using Directory = Net3_Proxy.Directory; +#endif + +namespace IPA.Loader +{ + /// + /// The manager class for all plugins. + /// + public static class PluginManager + { +#pragma warning disable CS0618 // Type or member is obsolete (IPlugin) + + /// + /// An of new Beat Saber plugins + /// + internal static IEnumerable BSPlugins => (_bsPlugins ?? throw new InvalidOperationException()).Select(p => p.Plugin); + private static List _bsPlugins; + internal static IEnumerable BSMetas => _bsPlugins; + + /// + /// Gets info about the plugin with the specified name. + /// + /// the name of the plugin to get (must be an exact match) + /// the plugin info for the requested plugin or null + public static PluginInfo GetPlugin(string name) + { + return BSMetas.FirstOrDefault(p => p.Metadata.Name == name); + } + + /// + /// Gets info about the plugin with the specified ModSaber name. + /// + /// the ModSaber name of the plugin to get (must be an exact match) + /// the plugin info for the requested plugin or null + [Obsolete("Old name. Use GetPluginFromId instead.")] + public static PluginInfo GetPluginFromModSaberName(string name) => GetPluginFromId(name); + + /// + /// Gets info about the plugin with the specified ID. + /// + /// the ID name of the plugin to get (must be an exact match) + /// the plugin info for the requested plugin or null + public static PluginInfo GetPluginFromId(string name) + { + return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); + } + + /// + /// Gets a disabled plugin's metadata by its name. + /// + /// the name of the disabled plugin to get + /// the metadata for the corresponding plugin + public static PluginMetadata GetDisabledPlugin(string name) => + DisabledPlugins.FirstOrDefault(p => p.Name == name); + + /// + /// Gets a disabled plugin's metadata by its ID. + /// + /// the ID of the disabled plugin to get + /// the metadata for the corresponding plugin + public static PluginMetadata GetDisabledPluginFromId(string name) => + DisabledPlugins.FirstOrDefault(p => p.Id == name); + + /// + /// Disables a plugin, and all dependents. + /// + /// the plugin to disable + /// whether or not it needs a restart to enable + public static bool DisablePlugin(PluginInfo plugin) + { + if (plugin == null) return false; + + if (plugin.Metadata.IsBare) + { + Logger.loader.Warn($"Trying to disable bare manifest"); + return false; + } + + if (IsDisabled(plugin.Metadata)) return false; + + var needsRestart = false; + + Logger.loader.Info($"Disabling {plugin.Metadata.Name}"); + + var dependents = BSMetas.Where(m => m.Metadata.Dependencies.Contains(plugin.Metadata)).ToList(); + needsRestart = dependents.Aggregate(needsRestart, (b, p) => DisablePlugin(p) || b); + + DisabledConfig.Instance.DisabledModIds.Add(plugin.Metadata.Id ?? plugin.Metadata.Name); + + if (!needsRestart && plugin.Plugin is IDisablablePlugin disable) + { + try + { + disable.OnDisable(); + } + catch (Exception e) + { + Logger.loader.Error($"Error occurred trying to disable {plugin.Metadata.Name}"); + Logger.loader.Error(e); + } + + if (needsRestart) + Logger.loader.Warn($"Disablable plugin has non-disablable dependents; some things may not work properly"); + } + else needsRestart = true; + + runtimeDisabled.Add(plugin); + _bsPlugins.Remove(plugin); + + try + { + PluginDisabled?.Invoke(plugin.Metadata, needsRestart); + } + catch (Exception e) + { + Logger.loader.Error($"Error occurred invoking disable event for {plugin.Metadata.Name}"); + Logger.loader.Error(e); + } + + return needsRestart; + } + + /// + /// Disables a plugin, and all dependents. + /// + /// the ID, or name if the ID is null, of the plugin to disable + /// whether a restart is needed to activate + public static bool DisablePlugin(string pluginId) => DisablePlugin(GetPluginFromId(pluginId) ?? GetPlugin(pluginId)); + + /// + /// Enables a plugin that had been previously disabled. + /// + /// the plugin to enable + /// whether a restart is needed to activate + public static bool EnablePlugin(PluginMetadata plugin) + { // TODO: fix some of this behaviour, by adding better support for runtime enable/disable of mods + if (plugin == null) return false; + + if (plugin.IsBare) + { + Logger.loader.Warn($"Trying to enable bare manifest"); + return false; + } + + if (!IsDisabled(plugin)) return false; + + Logger.loader.Info($"Enabling {plugin.Name}"); + + DisabledConfig.Instance.DisabledModIds.Remove(plugin.Id ?? plugin.Name); + + var needsRestart = true; + + var depsNeedRestart = plugin.Dependencies.Aggregate(false, (b, p) => EnablePlugin(p) || b); + + var runtimeInfo = runtimeDisabled.FirstOrDefault(p => p.Metadata == plugin); + if (runtimeInfo != null && runtimeInfo.Plugin is IPlugin newPlugin) + { + try + { + newPlugin.OnEnable(); + } + catch (Exception e) + { + Logger.loader.Error($"Error occurred trying to enable {plugin.Name}"); + Logger.loader.Error(e); + } + needsRestart = false; + } + else + { + PluginLoader.DisabledPlugins.Remove(plugin); + if (runtimeInfo == null) + { + runtimeInfo = InitPlugin(plugin, AllPlugins.Select(i => i.Metadata)); + needsRestart = false; + } + } + + if (runtimeInfo != null) + runtimeDisabled.Remove(runtimeInfo); + + _bsPlugins.Add(runtimeInfo); + + try + { + PluginEnabled?.Invoke(runtimeInfo, needsRestart || depsNeedRestart); + } + catch (Exception e) + { + Logger.loader.Error($"Error occurred invoking enable event for {plugin.Name}"); + Logger.loader.Error(e); + } + + return needsRestart || depsNeedRestart; + } + + /// + /// Enables a plugin that had been previously disabled. + /// + /// the ID, or name if the ID is null, of the plugin to enable + /// whether a restart is needed to activate + public static bool EnablePlugin(string pluginId) => + EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId)); + + /// + /// Checks if a given plugin is disabled. + /// + /// the plugin to check + /// if the plugin is disabled, otherwise. + public static bool IsDisabled(PluginMetadata meta) => DisabledPlugins.Contains(meta); + + /// + /// Checks if a given plugin is enabled. + /// + /// the plugin to check + /// if the plugin is enabled, otherwise. + public static bool IsEnabled(PluginMetadata meta) => BSMetas.Any(p => p.Metadata == meta); + + private static readonly List runtimeDisabled = new List(); + /// + /// Gets a list of disabled BSIPA plugins. + /// + /// a collection of all disabled plugins as + public static IEnumerable DisabledPlugins => PluginLoader.DisabledPlugins.Concat(runtimeDisabled.Select(p => p.Metadata)); + + /// + /// An invoker for the event. + /// + /// the plugin that was enabled + /// whether it needs a restart to take effect + public delegate void PluginEnableDelegate(PluginInfo plugin, bool needsRestart); + /// + /// An invoker for the event. + /// + /// the plugin that was disabled + /// whether it needs a restart to take effect + public delegate void PluginDisableDelegate(PluginMetadata plugin, bool needsRestart); + + /// + /// Called whenever a plugin is enabled. + /// + public static event PluginEnableDelegate PluginEnabled; + /// + /// Called whenever a plugin is disabled. + /// + public static event PluginDisableDelegate PluginDisabled; + + /// + /// Gets a list of all BSIPA plugins. + /// + /// a collection of all enabled plugins as s + public static IEnumerable AllPlugins => BSMetas; + + /// + /// Converts a plugin's metadata to a . + /// + /// the metadata + /// the plugin info + public static PluginInfo InfoFromMetadata(PluginMetadata meta) + { + if (IsDisabled(meta)) + return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta); + else + return AllPlugins.FirstOrDefault(p => p.Metadata == meta); + } + + /// + /// An of old IPA plugins. + /// + /// all legacy plugin instances + [Obsolete("I mean, IPlugin shouldn't be used, so why should this? Not renaming to extend support for old plugins.")] + public static IEnumerable Plugins => _ipaPlugins; + private static List _ipaPlugins; + + internal static IConfigProvider SelfConfigProvider { get; set; } + + internal static void Load() + { + string pluginDirectory = BeatSaber.PluginsPath; + + // Process.GetCurrentProcess().MainModule crashes the game and Assembly.GetEntryAssembly() is NULL, + // so we need to resort to P/Invoke + string exeName = Path.GetFileNameWithoutExtension(AppInfo.StartupPath); + _bsPlugins = new List(); + _ipaPlugins = new List(); + + if (!Directory.Exists(pluginDirectory)) return; + + string cacheDir = Path.Combine(pluginDirectory, ".cache"); + + if (!Directory.Exists(cacheDir)) + { + Directory.CreateDirectory(cacheDir); + } + else + { + foreach (string plugin in Directory.GetFiles(cacheDir, "*")) + { + File.Delete(plugin); + } + } + + // initialize BSIPA plugins first + _bsPlugins.AddRange(PluginLoader.LoadPlugins()); + + var metadataPaths = PluginsMetadata.Select(m => m.File.FullName).ToList(); + var ignoredPaths = ignoredPlugins.Select(m => m.Key.File.FullName).ToList(); + var disabledPaths = DisabledPlugins.Select(m => m.File.FullName).ToList(); + + //Copy plugins to .cache + string[] originalPlugins = Directory.GetFiles(pluginDirectory, "*.dll"); + foreach (string s in originalPlugins) + { + if (metadataPaths.Contains(s)) continue; + if (ignoredPaths.Contains(s)) continue; + if (disabledPaths.Contains(s)) continue; + string pluginCopy = Path.Combine(cacheDir, Path.GetFileName(s)); + + #region Fix assemblies for refactor + + var module = ModuleDefinition.ReadModule(Path.Combine(pluginDirectory, s)); + foreach (var @ref in module.AssemblyReferences) + { // fix assembly references + if (@ref.Name == "IllusionPlugin" || @ref.Name == "IllusionInjector") + { + @ref.Name = "IPA.Loader"; + } + } + + foreach (var @ref in module.GetTypeReferences()) + { // fix type references + if (@ref.FullName == "IllusionPlugin.IPlugin") @ref.Namespace = "IPA.Old"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.IEnhancedPlugin") @ref.Namespace = "IPA.Old"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.IBeatSaberPlugin") { @ref.Namespace = "IPA"; @ref.Name = nameof(IPlugin); } + if (@ref.FullName == "IllusionPlugin.IEnhancedBeatSaberPlugin") { @ref.Namespace = "IPA"; @ref.Name = nameof(IEnhancedPlugin); } + if (@ref.FullName == "IllusionPlugin.IniFile") @ref.Namespace = "IPA.Config"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.IModPrefs") @ref.Namespace = "IPA.Config"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.ModPrefs") @ref.Namespace = "IPA.Config"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.Utils.ReflectionUtil") @ref.Namespace = "IPA.Utilities"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.Logging.Logger") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; + if (@ref.FullName == "IllusionPlugin.Logging.LogPrinter") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.PluginManager") @ref.Namespace = "IPA.Loader"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.PluginComponent") @ref.Namespace = "IPA.Loader"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.CompositeBSPlugin") @ref.Namespace = "IPA.Loader.Composite"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.CompositeIPAPlugin") @ref.Namespace = "IPA.Loader.Composite"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.Logging.UnityLogInterceptor") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.Logging.StandardLogger") @ref.Namespace = "IPA.Logging"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.Updating.SelfPlugin") @ref.Namespace = "IPA.Updating"; //@ref.Name = ""; + if (@ref.FullName == "IllusionInjector.Updating.Backup.BackupUnit") @ref.Namespace = "IPA.Updating.Backup"; //@ref.Name = ""; + if (@ref.Namespace == "IllusionInjector.Utilities") @ref.Namespace = "IPA.Utilities"; //@ref.Name = ""; + if (@ref.Namespace == "IllusionInjector.Logging.Printers") @ref.Namespace = "IPA.Logging.Printers"; //@ref.Name = ""; + } + module.Write(pluginCopy); + + #endregion + } + + //Load copied plugins + string[] copiedPlugins = Directory.GetFiles(cacheDir, "*.dll"); + foreach (string s in copiedPlugins) + { + var result = LoadPluginsFromFile(s); + if (result == null) continue; + _ipaPlugins.AddRange(result.NonNull()); + } + + Logger.log.Info(exeName); + Logger.log.Info($"Running on Unity {Application.unityVersion}"); + Logger.log.Info($"Game version {BeatSaber.GameVersion}"); + Logger.log.Info("-----------------------------"); + Logger.log.Info($"Loading plugins from {Utils.GetRelativePath(pluginDirectory, Environment.CurrentDirectory)} and found {_bsPlugins.Count + _ipaPlugins.Count}"); + Logger.log.Info("-----------------------------"); + foreach (var plugin in _bsPlugins) + { + Logger.log.Info($"{plugin.Metadata.Name} ({plugin.Metadata.Id}): {plugin.Metadata.Version}"); + } + Logger.log.Info("-----------------------------"); + foreach (var plugin in _ipaPlugins) + { + Logger.log.Info($"{plugin.Name}: {plugin.Version}"); + } + Logger.log.Info("-----------------------------"); + } + + private static IEnumerable LoadPluginsFromFile(string file) + { + var ipaPlugins = new List(); + + if (!File.Exists(file) || !file.EndsWith(".dll", true, null)) + return ipaPlugins; + + T OptionalGetPlugin(Type t) where T : class + { + if (t.FindInterfaces((t, o) => t == (o as Type), typeof(T)).Length > 0) + { + try + { + T pluginInstance = Activator.CreateInstance(t) as T; + return pluginInstance; + } + catch (Exception e) + { + Logger.loader.Error($"Could not load plugin {t.FullName} in {Path.GetFileName(file)}! {e}"); + } + } + + return null; + } + + try + { + Assembly assembly = Assembly.LoadFrom(file); + + foreach (Type t in assembly.GetTypes()) + { + + var ipaPlugin = OptionalGetPlugin(t); + if (ipaPlugin != null) + { + ipaPlugins.Add(ipaPlugin); + } + } + + } + catch (ReflectionTypeLoadException e) + { + Logger.loader.Error($"Could not load the following types from {Path.GetFileName(file)}:"); + Logger.loader.Error($" {string.Join("\n ", e.LoaderExceptions?.Select(e1 => e1?.Message).StrJP() ?? new string[0])}"); + } + catch (Exception e) + { + Logger.loader.Error($"Could not load {Path.GetFileName(file)}!"); + Logger.loader.Error(e); + } + + return ipaPlugins; + } + + internal static class AppInfo + { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = false)] + private static extern int GetModuleFileName(HandleRef hModule, StringBuilder buffer, int length); + private static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); + public static string StartupPath + { + get + { + StringBuilder stringBuilder = new StringBuilder(260); + GetModuleFileName(NullHandleRef, stringBuilder, stringBuilder.Capacity); + return stringBuilder.ToString(); + } + } + } +#pragma warning restore CS0618 // Type or member is obsolete (IPlugin) + } +} diff --git a/IPA.Loader/Loader/PluginMetadata.cs b/IPA.Loader/Loader/PluginMetadata.cs new file mode 100644 index 00000000..71259d83 --- /dev/null +++ b/IPA.Loader/Loader/PluginMetadata.cs @@ -0,0 +1,94 @@ +using IPA.Loader.Features; +using IPA.Utilities; +using Mono.Cecil; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Version = SemVer.Version; + +namespace IPA.Loader +{ + /// + /// A class which describes a loaded plugin. + /// + public class PluginMetadata + { + /// + /// The assembly the plugin was loaded from. + /// + /// the loaded Assembly that contains the plugin main type + public Assembly Assembly { get; internal set; } + + /// + /// The TypeDefinition for the main type of the plugin. + /// + /// the Cecil definition for the plugin main type + public TypeDefinition PluginType { get; internal set; } + + /// + /// The human readable name of the plugin. + /// + /// the name of the plugin + public string Name { get; internal set; } + + /// + /// The BeatMods ID of the plugin, or null if it doesn't have one. + /// + /// the updater ID of the plugin + public string Id { get; internal set; } + + /// + /// The version of the plugin. + /// + /// the version of the plugin + public Version Version { get; internal set; } + + /// + /// The file the plugin was loaded from. + /// + /// the file the plugin was loaded from + public FileInfo File { get; internal set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Global + /// + /// The features this plugin requests. + /// + /// the list of features requested by the plugin + public IReadOnlyList Features => InternalFeatures; + + internal readonly List InternalFeatures = new List(); + + internal bool IsSelf; + + /// + /// Whether or not this metadata object represents a bare manifest. + /// + /// if it is bare, otherwise + public bool IsBare { get; internal set; } + + private PluginManifest manifest; + + internal HashSet Dependencies { get; } = new HashSet(); + + internal PluginManifest Manifest + { + get => manifest; + set + { + manifest = value; + Name = value.Name; + Version = value.Version; + Id = value.Id; + } + } + + public RuntimeOptions RuntimeOptions { get; internal set; } + public bool IsAttributePlugin { get; internal set; } = false; + + /// + /// Gets all of the metadata as a readable string. + /// + /// the readable printable metadata string + public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; + } +} \ No newline at end of file diff --git a/IPA.Loader/Loader/manifest.json b/IPA.Loader/Loader/manifest.json index c2fdcf86..3c4b7300 100644 --- a/IPA.Loader/Loader/manifest.json +++ b/IPA.Loader/Loader/manifest.json @@ -1,27 +1,26 @@ -{ - "$schema": "https://raw.githubusercontent.com/beat-saber-modding-group/BSIPA-MetadataFileSchema/master/Schema.json", - "author": "DaNike", - "description": [ - "#![IPA.Loader.description.md]", - "A mod loader specifically for Beat Saber." - ], - "gameVersion": "1.6.0", - "id": "BSIPA", - "name": "Beat Saber IPA", - "version": "4.0.0-beta.2", - "icon": "IPA.icon_white.png", - "features": [ - "define-feature(print, IPA.Loader.Features.PrintFeature)", - "define-feature(debug, IPA.Loader.Features.DebugFeature)", - "define-feature(warn, IPA.Loader.Features.WarnFeature)", - "define-feature(no-update, IPA.Loader.Features.NoUpdateFeature)", - "define-feature(no-runtime-enable, IPA.Loader.Features.NoRuntimeEnableFeature)", - "define-feature(init-injector, IPA.Loader.Features.InitInjectorFeature)", - "define-feature(config-provider, IPA.Loader.Features.ConfigProviderFeature)" - ], - "links": { - "project-home": "https://beat-saber-modding-group.github.io/BeatSaber-IPA-Reloaded/index.html", - "project-source": "https://github.com/beat-saber-modding-group/BeatSaber-IPA-Reloaded", - "donate": "https://ko-fi.com/danike" - } +{ + "$schema": "https://raw.githubusercontent.com/beat-saber-modding-group/BSIPA-MetadataFileSchema/master/Schema.json", + "author": "DaNike", + "description": [ + "#![IPA.Loader.description.md]", + "A mod loader specifically for Beat Saber." + ], + "gameVersion": "1.6.0", + "id": "BSIPA", + "name": "Beat Saber IPA", + "version": "4.0.0-beta.2", + "icon": "IPA.icon_white.png", + "features": [ + "define-feature(print, IPA.Loader.Features.PrintFeature)", + "define-feature(debug, IPA.Loader.Features.DebugFeature)", + "define-feature(warn, IPA.Loader.Features.WarnFeature)", + "define-feature(no-update, IPA.Loader.Features.NoUpdateFeature)", + "define-feature(init-injector, IPA.Loader.Features.InitInjectorFeature)", + "define-feature(config-provider, IPA.Loader.Features.ConfigProviderFeature)" + ], + "links": { + "project-home": "https://beat-saber-modding-group.github.io/BeatSaber-IPA-Reloaded/index.html", + "project-source": "https://github.com/beat-saber-modding-group/BeatSaber-IPA-Reloaded", + "donate": "https://ko-fi.com/danike" + } } \ No newline at end of file