@ -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; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -1,224 +1,224 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
#if NET3 | |||||
using Net3_Proxy; | |||||
#endif | |||||
namespace IPA.Loader.Features | |||||
{ | |||||
/// <summary> | |||||
/// The root interface for a mod Feature. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. | |||||
/// </remarks> | |||||
public abstract class Feature | |||||
{ | |||||
/// <summary> | |||||
/// Initializes the feature with the parameters provided in the definition. | |||||
/// | |||||
/// Note: When no parenthesis are provided, <paramref name="parameters"/> is an empty array. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// This gets called BEFORE *your* `Init` method. | |||||
/// | |||||
/// Returning <see langword="false" /> does *not* prevent the plugin from being loaded. It simply prevents the feature from being used. | |||||
/// </remarks> | |||||
/// <param name="meta">the metadata of the plugin that is being prepared</param> | |||||
/// <param name="parameters">the parameters passed to the feature definition, or null</param> | |||||
/// <returns><see langword="true"/> if the feature is valid for the plugin, <see langword="false"/> otherwise</returns> | |||||
public abstract bool Initialize(PluginLoader.PluginMetadata meta, string[] parameters); | |||||
/// <summary> | |||||
/// 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 <see langword="true" /> from <see cref="Initialize"/> | |||||
/// </summary> | |||||
/// <returns>the truthiness of the Feature.</returns> | |||||
public virtual bool Evaluate() => true; | |||||
/// <summary> | |||||
/// The message to be logged when the feature is not valid for a plugin. | |||||
/// This should also be set whenever either <see cref="BeforeLoad"/> or <see cref="BeforeInit"/> returns false. | |||||
/// </summary> | |||||
/// <value>the message to show when the feature is marked invalid</value> | |||||
public virtual string InvalidMessage { get; protected set; } | |||||
/// <summary> | |||||
/// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// The assembly will still be loaded, but the plugin will not be constructed if this returns <see langword="false" />. | |||||
/// Any features it defines, for example, will still be loaded. | |||||
/// </remarks> | |||||
/// <param name="plugin">the plugin about to be loaded</param> | |||||
/// <returns>whether or not the plugin should be loaded</returns> | |||||
public virtual bool BeforeLoad(PluginLoader.PluginMetadata plugin) => true; | |||||
/// <summary> | |||||
/// 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. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to be initialized</param> | |||||
/// <returns>whether or not to call the Init method</returns> | |||||
public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; | |||||
/// <summary> | |||||
/// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was just initialized</param> | |||||
/// <param name="pluginInstance">the instance of the plugin being initialized</param> | |||||
public virtual void AfterInit(PluginLoader.PluginInfo plugin, IPlugin pluginInstance) => AfterInit(plugin); | |||||
/// <summary> | |||||
/// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was just initialized</param> | |||||
public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } | |||||
/// <summary> | |||||
/// Ensures a plugin's assembly is loaded. Do not use unless you need to. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to ensure is loaded.</param> | |||||
protected void RequireLoaded(PluginLoader.PluginMetadata plugin) => PluginLoader.Load(plugin); | |||||
/// <summary> | |||||
/// Defines whether or not this feature will be accessible from the plugin metadata once loaded. | |||||
/// </summary> | |||||
/// <value><see langword="true"/> if this <see cref="Feature"/> will be stored on the plugin metadata, <see langword="false"/> otherwise</value> | |||||
protected internal virtual bool StoreOnPlugin => true; | |||||
static Feature() | |||||
{ | |||||
Reset(); | |||||
} | |||||
internal static void Reset() | |||||
{ | |||||
featureTypes = new Dictionary<string, Type> | |||||
{ | |||||
{ "define-feature", typeof(DefineFeature) } | |||||
}; | |||||
} | |||||
private static Dictionary<string, Type> 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<string>(); | |||||
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 | |||||
{ | |||||
/// <summary> | |||||
/// The root interface for a mod Feature. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. | |||||
/// </remarks> | |||||
public abstract class Feature | |||||
{ | |||||
/// <summary> | |||||
/// Initializes the feature with the parameters provided in the definition. | |||||
/// | |||||
/// Note: When no parenthesis are provided, <paramref name="parameters"/> is an empty array. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// This gets called BEFORE *your* `Init` method. | |||||
/// | |||||
/// Returning <see langword="false" /> does *not* prevent the plugin from being loaded. It simply prevents the feature from being used. | |||||
/// </remarks> | |||||
/// <param name="meta">the metadata of the plugin that is being prepared</param> | |||||
/// <param name="parameters">the parameters passed to the feature definition, or null</param> | |||||
/// <returns><see langword="true"/> if the feature is valid for the plugin, <see langword="false"/> otherwise</returns> | |||||
public abstract bool Initialize(PluginMetadata meta, string[] parameters); | |||||
/// <summary> | |||||
/// 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 <see langword="true" /> from <see cref="Initialize"/> | |||||
/// </summary> | |||||
/// <returns>the truthiness of the Feature.</returns> | |||||
public virtual bool Evaluate() => true; | |||||
/// <summary> | |||||
/// The message to be logged when the feature is not valid for a plugin. | |||||
/// This should also be set whenever either <see cref="BeforeLoad"/> or <see cref="BeforeInit"/> returns false. | |||||
/// </summary> | |||||
/// <value>the message to show when the feature is marked invalid</value> | |||||
public virtual string InvalidMessage { get; protected set; } | |||||
/// <summary> | |||||
/// Called before a plugin is loaded. This should never throw an exception. An exception will abort the loading of the plugin with an error. | |||||
/// </summary> | |||||
/// <remarks> | |||||
/// The assembly will still be loaded, but the plugin will not be constructed if this returns <see langword="false" />. | |||||
/// Any features it defines, for example, will still be loaded. | |||||
/// </remarks> | |||||
/// <param name="plugin">the plugin about to be loaded</param> | |||||
/// <returns>whether or not the plugin should be loaded</returns> | |||||
public virtual bool BeforeLoad(PluginMetadata plugin) => true; | |||||
/// <summary> | |||||
/// 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. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to be initialized</param> | |||||
/// <returns>whether or not to call the Init method</returns> | |||||
public virtual bool BeforeInit(PluginLoader.PluginInfo plugin) => true; | |||||
/// <summary> | |||||
/// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was just initialized</param> | |||||
/// <param name="pluginInstance">the instance of the plugin being initialized</param> | |||||
public virtual void AfterInit(PluginLoader.PluginInfo plugin, IPlugin pluginInstance) => AfterInit(plugin); | |||||
/// <summary> | |||||
/// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was just initialized</param> | |||||
public virtual void AfterInit(PluginLoader.PluginInfo plugin) { } | |||||
/// <summary> | |||||
/// Ensures a plugin's assembly is loaded. Do not use unless you need to. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to ensure is loaded.</param> | |||||
protected void RequireLoaded(PluginMetadata plugin) => PluginLoader.Load(plugin); | |||||
/// <summary> | |||||
/// Defines whether or not this feature will be accessible from the plugin metadata once loaded. | |||||
/// </summary> | |||||
/// <value><see langword="true"/> if this <see cref="Feature"/> will be stored on the plugin metadata, <see langword="false"/> otherwise</value> | |||||
protected internal virtual bool StoreOnPlugin => true; | |||||
static Feature() | |||||
{ | |||||
Reset(); | |||||
} | |||||
internal static void Reset() | |||||
{ | |||||
featureTypes = new Dictionary<string, Type> | |||||
{ | |||||
{ "define-feature", typeof(DefineFeature) } | |||||
}; | |||||
} | |||||
private static Dictionary<string, Type> 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<string>(); | |||||
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; | |||||
} | |||||
} | |||||
} | |||||
} | } |
@ -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<ParameterModifier>()); | |||||
} | |||||
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; | |||||
} | |||||
} | |||||
} | |||||
} |
@ -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 { } | |||||
} | |||||
} | |||||
} |
@ -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"; | |||||
} | |||||
} |
@ -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; | |||||
} | |||||
} | |||||
} |
@ -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<PluginMetadata, object> CreatePlugin { get; set; } | |||||
private Action<object> LifecycleEnable { get; set; } | |||||
// disable may be async (#24) | |||||
private Func<object, Task> 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<PluginMetadata, object> 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<InitAttribute>())) | |||||
.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<InitAttribute>())) | |||||
.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<Func<PluginMetadata, object>>( | |||||
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<object> 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<IEdgeLifecycleAttribute>())) | |||||
.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<Action<object>>( | |||||
Expression.Block( | |||||
enableMethods | |||||
.Select(m => Expression.Call(instVar, m)) | |||||
.Prepend<Expression>(Expression.Assign(instVar, Expression.Convert(objParam, type)))), | |||||
objParam); | |||||
return createExpr.Compile(); | |||||
} | |||||
private static Func<object, Task> 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<IEdgeLifecycleAttribute>())) | |||||
.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<MethodInfo>(); | |||||
var nonTaskMethods = new List<MethodInfo>(); | |||||
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<Func<Task>> 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<Func<object, Task>>( | |||||
Expression.Block( | |||||
nonTaskMethods | |||||
.Select(m => Expression.Call(instVar, m)) | |||||
.Prepend<Expression>(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(); | |||||
} | |||||
} | |||||
} |
@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// The manager class for all plugins. | |||||
/// </summary> | |||||
public static class PluginManager | |||||
{ | |||||
#pragma warning disable CS0618 // Type or member is obsolete (IPlugin) | |||||
/// <summary> | |||||
/// An <see cref="IEnumerable"/> of new Beat Saber plugins | |||||
/// </summary> | |||||
internal static IEnumerable<IPlugin> BSPlugins => (_bsPlugins ?? throw new InvalidOperationException()).Select(p => p.Plugin); | |||||
private static List<PluginInfo> _bsPlugins; | |||||
internal static IEnumerable<PluginInfo> BSMetas => _bsPlugins; | |||||
/// <summary> | |||||
/// Gets info about the plugin with the specified name. | |||||
/// </summary> | |||||
/// <param name="name">the name of the plugin to get (must be an exact match)</param> | |||||
/// <returns>the plugin info for the requested plugin or null</returns> | |||||
public static PluginInfo GetPlugin(string name) | |||||
{ | |||||
return BSMetas.FirstOrDefault(p => p.Metadata.Name == name); | |||||
} | |||||
/// <summary> | |||||
/// Gets info about the plugin with the specified ModSaber name. | |||||
/// </summary> | |||||
/// <param name="name">the ModSaber name of the plugin to get (must be an exact match)</param> | |||||
/// <returns>the plugin info for the requested plugin or null</returns> | |||||
[Obsolete("Old name. Use GetPluginFromId instead.")] | |||||
public static PluginInfo GetPluginFromModSaberName(string name) => GetPluginFromId(name); | |||||
/// <summary> | |||||
/// Gets info about the plugin with the specified ID. | |||||
/// </summary> | |||||
/// <param name="name">the ID name of the plugin to get (must be an exact match)</param> | |||||
/// <returns>the plugin info for the requested plugin or null</returns> | |||||
public static PluginInfo GetPluginFromId(string name) | |||||
{ | |||||
return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); | |||||
} | |||||
/// <summary> | |||||
/// Gets a disabled plugin's metadata by its name. | |||||
/// </summary> | |||||
/// <param name="name">the name of the disabled plugin to get</param> | |||||
/// <returns>the metadata for the corresponding plugin</returns> | |||||
public static PluginMetadata GetDisabledPlugin(string name) => | |||||
DisabledPlugins.FirstOrDefault(p => p.Name == name); | |||||
/// <summary> | |||||
/// Gets a disabled plugin's metadata by its ID. | |||||
/// </summary> | |||||
/// <param name="name">the ID of the disabled plugin to get</param> | |||||
/// <returns>the metadata for the corresponding plugin</returns> | |||||
public static PluginMetadata GetDisabledPluginFromId(string name) => | |||||
DisabledPlugins.FirstOrDefault(p => p.Id == name); | |||||
/// <summary> | |||||
/// Disables a plugin, and all dependents. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to disable</param> | |||||
/// <returns>whether or not it needs a restart to enable</returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Disables a plugin, and all dependents. | |||||
/// </summary> | |||||
/// <param name="pluginId">the ID, or name if the ID is null, of the plugin to disable</param> | |||||
/// <returns>whether a restart is needed to activate</returns> | |||||
public static bool DisablePlugin(string pluginId) => DisablePlugin(GetPluginFromId(pluginId) ?? GetPlugin(pluginId)); | |||||
/// <summary> | |||||
/// Enables a plugin that had been previously disabled. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to enable</param> | |||||
/// <returns>whether a restart is needed to activate</returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Enables a plugin that had been previously disabled. | |||||
/// </summary> | |||||
/// <param name="pluginId">the ID, or name if the ID is null, of the plugin to enable</param> | |||||
/// <returns>whether a restart is needed to activate</returns> | |||||
public static bool EnablePlugin(string pluginId) => | |||||
EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId)); | |||||
/// <summary> | |||||
/// Checks if a given plugin is disabled. | |||||
/// </summary> | |||||
/// <param name="meta">the plugin to check</param> | |||||
/// <returns><see langword="true"/> if the plugin is disabled, <see langword="false"/> otherwise.</returns> | |||||
public static bool IsDisabled(PluginMetadata meta) => DisabledPlugins.Contains(meta); | |||||
/// <summary> | |||||
/// Checks if a given plugin is enabled. | |||||
/// </summary> | |||||
/// <param name="meta">the plugin to check</param> | |||||
/// <returns><see langword="true"/> if the plugin is enabled, <see langword="false"/> otherwise.</returns> | |||||
public static bool IsEnabled(PluginMetadata meta) => BSMetas.Any(p => p.Metadata == meta); | |||||
private static readonly List<PluginInfo> runtimeDisabled = new List<PluginInfo>(); | |||||
/// <summary> | |||||
/// Gets a list of disabled BSIPA plugins. | |||||
/// </summary> | |||||
/// <value>a collection of all disabled plugins as <see cref="PluginMetadata"/></value> | |||||
public static IEnumerable<PluginMetadata> DisabledPlugins => PluginLoader.DisabledPlugins.Concat(runtimeDisabled.Select(p => p.Metadata)); | |||||
/// <summary> | |||||
/// An invoker for the <see cref="PluginEnabled"/> event. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was enabled</param> | |||||
/// <param name="needsRestart">whether it needs a restart to take effect</param> | |||||
public delegate void PluginEnableDelegate(PluginInfo plugin, bool needsRestart); | |||||
/// <summary> | |||||
/// An invoker for the <see cref="PluginDisabled"/> event. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was disabled</param> | |||||
/// <param name="needsRestart">whether it needs a restart to take effect</param> | |||||
public delegate void PluginDisableDelegate(PluginMetadata plugin, bool needsRestart); | |||||
/// <summary> | |||||
/// Called whenever a plugin is enabled. | |||||
/// </summary> | |||||
public static event PluginEnableDelegate PluginEnabled; | |||||
/// <summary> | |||||
/// Called whenever a plugin is disabled. | |||||
/// </summary> | |||||
public static event PluginDisableDelegate PluginDisabled; | |||||
/// <summary> | |||||
/// Gets a list of all BSIPA plugins. | |||||
/// </summary> | |||||
/// <value>a collection of all enabled plugins as <see cref="PluginInfo"/>s</value> | |||||
public static IEnumerable<PluginInfo> AllPlugins => BSMetas; | |||||
/// <summary> | |||||
/// Converts a plugin's metadata to a <see cref="PluginInfo"/>. | |||||
/// </summary> | |||||
/// <param name="meta">the metadata</param> | |||||
/// <returns>the plugin info</returns> | |||||
public static PluginInfo InfoFromMetadata(PluginMetadata meta) | |||||
{ | |||||
if (IsDisabled(meta)) | |||||
return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta); | |||||
else | |||||
return AllPlugins.FirstOrDefault(p => p.Metadata == meta); | |||||
} | |||||
/// <summary> | |||||
/// An <see cref="IEnumerable"/> of old IPA plugins. | |||||
/// </summary> | |||||
/// <value>all legacy plugin instances</value> | |||||
[Obsolete("I mean, IPlugin shouldn't be used, so why should this? Not renaming to extend support for old plugins.")] | |||||
public static IEnumerable<Old.IPlugin> Plugins => _ipaPlugins; | |||||
private static List<Old.IPlugin> _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<PluginInfo>(); | |||||
_ipaPlugins = new List<Old.IPlugin>(); | |||||
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<Old.IPlugin> LoadPluginsFromFile(string file) | |||||
{ | |||||
var ipaPlugins = new List<Old.IPlugin>(); | |||||
if (!File.Exists(file) || !file.EndsWith(".dll", true, null)) | |||||
return ipaPlugins; | |||||
T OptionalGetPlugin<T>(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<Old.IPlugin>(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 | |||||
{ | |||||
/// <summary> | |||||
/// The manager class for all plugins. | |||||
/// </summary> | |||||
public static class PluginManager | |||||
{ | |||||
#pragma warning disable CS0618 // Type or member is obsolete (IPlugin) | |||||
/// <summary> | |||||
/// An <see cref="IEnumerable"/> of new Beat Saber plugins | |||||
/// </summary> | |||||
internal static IEnumerable<IPlugin> BSPlugins => (_bsPlugins ?? throw new InvalidOperationException()).Select(p => p.Plugin); | |||||
private static List<PluginInfo> _bsPlugins; | |||||
internal static IEnumerable<PluginInfo> BSMetas => _bsPlugins; | |||||
/// <summary> | |||||
/// Gets info about the plugin with the specified name. | |||||
/// </summary> | |||||
/// <param name="name">the name of the plugin to get (must be an exact match)</param> | |||||
/// <returns>the plugin info for the requested plugin or null</returns> | |||||
public static PluginInfo GetPlugin(string name) | |||||
{ | |||||
return BSMetas.FirstOrDefault(p => p.Metadata.Name == name); | |||||
} | |||||
/// <summary> | |||||
/// Gets info about the plugin with the specified ModSaber name. | |||||
/// </summary> | |||||
/// <param name="name">the ModSaber name of the plugin to get (must be an exact match)</param> | |||||
/// <returns>the plugin info for the requested plugin or null</returns> | |||||
[Obsolete("Old name. Use GetPluginFromId instead.")] | |||||
public static PluginInfo GetPluginFromModSaberName(string name) => GetPluginFromId(name); | |||||
/// <summary> | |||||
/// Gets info about the plugin with the specified ID. | |||||
/// </summary> | |||||
/// <param name="name">the ID name of the plugin to get (must be an exact match)</param> | |||||
/// <returns>the plugin info for the requested plugin or null</returns> | |||||
public static PluginInfo GetPluginFromId(string name) | |||||
{ | |||||
return BSMetas.FirstOrDefault(p => p.Metadata.Id == name); | |||||
} | |||||
/// <summary> | |||||
/// Gets a disabled plugin's metadata by its name. | |||||
/// </summary> | |||||
/// <param name="name">the name of the disabled plugin to get</param> | |||||
/// <returns>the metadata for the corresponding plugin</returns> | |||||
public static PluginMetadata GetDisabledPlugin(string name) => | |||||
DisabledPlugins.FirstOrDefault(p => p.Name == name); | |||||
/// <summary> | |||||
/// Gets a disabled plugin's metadata by its ID. | |||||
/// </summary> | |||||
/// <param name="name">the ID of the disabled plugin to get</param> | |||||
/// <returns>the metadata for the corresponding plugin</returns> | |||||
public static PluginMetadata GetDisabledPluginFromId(string name) => | |||||
DisabledPlugins.FirstOrDefault(p => p.Id == name); | |||||
/// <summary> | |||||
/// Disables a plugin, and all dependents. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to disable</param> | |||||
/// <returns>whether or not it needs a restart to enable</returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Disables a plugin, and all dependents. | |||||
/// </summary> | |||||
/// <param name="pluginId">the ID, or name if the ID is null, of the plugin to disable</param> | |||||
/// <returns>whether a restart is needed to activate</returns> | |||||
public static bool DisablePlugin(string pluginId) => DisablePlugin(GetPluginFromId(pluginId) ?? GetPlugin(pluginId)); | |||||
/// <summary> | |||||
/// Enables a plugin that had been previously disabled. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin to enable</param> | |||||
/// <returns>whether a restart is needed to activate</returns> | |||||
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; | |||||
} | |||||
/// <summary> | |||||
/// Enables a plugin that had been previously disabled. | |||||
/// </summary> | |||||
/// <param name="pluginId">the ID, or name if the ID is null, of the plugin to enable</param> | |||||
/// <returns>whether a restart is needed to activate</returns> | |||||
public static bool EnablePlugin(string pluginId) => | |||||
EnablePlugin(GetDisabledPluginFromId(pluginId) ?? GetDisabledPlugin(pluginId)); | |||||
/// <summary> | |||||
/// Checks if a given plugin is disabled. | |||||
/// </summary> | |||||
/// <param name="meta">the plugin to check</param> | |||||
/// <returns><see langword="true"/> if the plugin is disabled, <see langword="false"/> otherwise.</returns> | |||||
public static bool IsDisabled(PluginMetadata meta) => DisabledPlugins.Contains(meta); | |||||
/// <summary> | |||||
/// Checks if a given plugin is enabled. | |||||
/// </summary> | |||||
/// <param name="meta">the plugin to check</param> | |||||
/// <returns><see langword="true"/> if the plugin is enabled, <see langword="false"/> otherwise.</returns> | |||||
public static bool IsEnabled(PluginMetadata meta) => BSMetas.Any(p => p.Metadata == meta); | |||||
private static readonly List<PluginInfo> runtimeDisabled = new List<PluginInfo>(); | |||||
/// <summary> | |||||
/// Gets a list of disabled BSIPA plugins. | |||||
/// </summary> | |||||
/// <value>a collection of all disabled plugins as <see cref="PluginMetadata"/></value> | |||||
public static IEnumerable<PluginMetadata> DisabledPlugins => PluginLoader.DisabledPlugins.Concat(runtimeDisabled.Select(p => p.Metadata)); | |||||
/// <summary> | |||||
/// An invoker for the <see cref="PluginEnabled"/> event. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was enabled</param> | |||||
/// <param name="needsRestart">whether it needs a restart to take effect</param> | |||||
public delegate void PluginEnableDelegate(PluginInfo plugin, bool needsRestart); | |||||
/// <summary> | |||||
/// An invoker for the <see cref="PluginDisabled"/> event. | |||||
/// </summary> | |||||
/// <param name="plugin">the plugin that was disabled</param> | |||||
/// <param name="needsRestart">whether it needs a restart to take effect</param> | |||||
public delegate void PluginDisableDelegate(PluginMetadata plugin, bool needsRestart); | |||||
/// <summary> | |||||
/// Called whenever a plugin is enabled. | |||||
/// </summary> | |||||
public static event PluginEnableDelegate PluginEnabled; | |||||
/// <summary> | |||||
/// Called whenever a plugin is disabled. | |||||
/// </summary> | |||||
public static event PluginDisableDelegate PluginDisabled; | |||||
/// <summary> | |||||
/// Gets a list of all BSIPA plugins. | |||||
/// </summary> | |||||
/// <value>a collection of all enabled plugins as <see cref="PluginInfo"/>s</value> | |||||
public static IEnumerable<PluginInfo> AllPlugins => BSMetas; | |||||
/// <summary> | |||||
/// Converts a plugin's metadata to a <see cref="PluginInfo"/>. | |||||
/// </summary> | |||||
/// <param name="meta">the metadata</param> | |||||
/// <returns>the plugin info</returns> | |||||
public static PluginInfo InfoFromMetadata(PluginMetadata meta) | |||||
{ | |||||
if (IsDisabled(meta)) | |||||
return runtimeDisabled.FirstOrDefault(p => p.Metadata == meta); | |||||
else | |||||
return AllPlugins.FirstOrDefault(p => p.Metadata == meta); | |||||
} | |||||
/// <summary> | |||||
/// An <see cref="IEnumerable"/> of old IPA plugins. | |||||
/// </summary> | |||||
/// <value>all legacy plugin instances</value> | |||||
[Obsolete("I mean, IPlugin shouldn't be used, so why should this? Not renaming to extend support for old plugins.")] | |||||
public static IEnumerable<Old.IPlugin> Plugins => _ipaPlugins; | |||||
private static List<Old.IPlugin> _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<PluginInfo>(); | |||||
_ipaPlugins = new List<Old.IPlugin>(); | |||||
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<Old.IPlugin> LoadPluginsFromFile(string file) | |||||
{ | |||||
var ipaPlugins = new List<Old.IPlugin>(); | |||||
if (!File.Exists(file) || !file.EndsWith(".dll", true, null)) | |||||
return ipaPlugins; | |||||
T OptionalGetPlugin<T>(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<Old.IPlugin>(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) | |||||
} | |||||
} |
@ -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 | |||||
{ | |||||
/// <summary> | |||||
/// A class which describes a loaded plugin. | |||||
/// </summary> | |||||
public class PluginMetadata | |||||
{ | |||||
/// <summary> | |||||
/// The assembly the plugin was loaded from. | |||||
/// </summary> | |||||
/// <value>the loaded Assembly that contains the plugin main type</value> | |||||
public Assembly Assembly { get; internal set; } | |||||
/// <summary> | |||||
/// The TypeDefinition for the main type of the plugin. | |||||
/// </summary> | |||||
/// <value>the Cecil definition for the plugin main type</value> | |||||
public TypeDefinition PluginType { get; internal set; } | |||||
/// <summary> | |||||
/// The human readable name of the plugin. | |||||
/// </summary> | |||||
/// <value>the name of the plugin</value> | |||||
public string Name { get; internal set; } | |||||
/// <summary> | |||||
/// The BeatMods ID of the plugin, or null if it doesn't have one. | |||||
/// </summary> | |||||
/// <value>the updater ID of the plugin</value> | |||||
public string Id { get; internal set; } | |||||
/// <summary> | |||||
/// The version of the plugin. | |||||
/// </summary> | |||||
/// <value>the version of the plugin</value> | |||||
public Version Version { get; internal set; } | |||||
/// <summary> | |||||
/// The file the plugin was loaded from. | |||||
/// </summary> | |||||
/// <value>the file the plugin was loaded from</value> | |||||
public FileInfo File { get; internal set; } | |||||
// ReSharper disable once UnusedAutoPropertyAccessor.Global | |||||
/// <summary> | |||||
/// The features this plugin requests. | |||||
/// </summary> | |||||
/// <value>the list of features requested by the plugin</value> | |||||
public IReadOnlyList<Feature> Features => InternalFeatures; | |||||
internal readonly List<Feature> InternalFeatures = new List<Feature>(); | |||||
internal bool IsSelf; | |||||
/// <summary> | |||||
/// Whether or not this metadata object represents a bare manifest. | |||||
/// </summary> | |||||
/// <value><see langword="true"/> if it is bare, <see langword="false"/> otherwise</value> | |||||
public bool IsBare { get; internal set; } | |||||
private PluginManifest manifest; | |||||
internal HashSet<PluginMetadata> Dependencies { get; } = new HashSet<PluginMetadata>(); | |||||
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; | |||||
/// <summary> | |||||
/// Gets all of the metadata as a readable string. | |||||
/// </summary> | |||||
/// <returns>the readable printable metadata string</returns> | |||||
public override string ToString() => $"{Name}({Id}@{Version})({PluginType?.FullName}) from '{Utils.GetRelativePath(File?.FullName, BeatSaber.InstallPath)}'"; | |||||
} | |||||
} |
@ -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" | |||||
} | |||||
} | } |