@ -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" | |||
} | |||
} |