Browse Source

Refactored PluginMetadata and PluginExecutor out of PluginLoader

4.0.0-beta
Anairkoen Schno 4 years ago
parent
commit
b22da69612
15 changed files with 1303 additions and 1296 deletions
  1. +1
    -1
      IPA.Loader/Config/ModPrefs.cs
  2. +2
    -1
      IPA.Loader/IPA.Loader.csproj
  3. +65
    -65
      IPA.Loader/Loader/Features/ConfigProviderFeature.cs
  4. +74
    -68
      IPA.Loader/Loader/Features/DefineFeature.cs
  5. +223
    -223
      IPA.Loader/Loader/Features/Feature.cs
  6. +105
    -102
      IPA.Loader/Loader/Features/InitInjectorFeature.cs
  7. +0
    -29
      IPA.Loader/Loader/Features/NoRuntimeEnableFeature.cs
  8. +12
    -12
      IPA.Loader/Loader/Features/NoUpdateFeature.cs
  9. +32
    -32
      IPA.Loader/Loader/Features/PrintFeature.cs
  10. +189
    -0
      IPA.Loader/Loader/PluginExecutor.cs
  11. +5
    -5
      IPA.Loader/Loader/PluginInitInjector.cs
  12. +0
    -255
      IPA.Loader/Loader/PluginLoader.cs
  13. +476
    -477
      IPA.Loader/Loader/PluginManager.cs
  14. +94
    -0
      IPA.Loader/Loader/PluginMetadata.cs
  15. +25
    -26
      IPA.Loader/Loader/manifest.json

+ 1
- 1
IPA.Loader/Config/ModPrefs.cs View File

@ -103,7 +103,7 @@ namespace IPA.Config
/// Constructs a ModPrefs object for the provide plugin. /// Constructs a ModPrefs object for the provide plugin.
/// </summary> /// </summary>
/// <param name="plugin">the plugin to get the preferences file for</param> /// <param name="plugin">the plugin to get the preferences file for</param>
public ModPrefs(PluginLoader.PluginMetadata plugin) {
public ModPrefs(PluginMetadata plugin) {
_instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "ModPrefs", _instance = new IniFile(Path.Combine(Environment.CurrentDirectory, "UserData", "ModPrefs",
$"{plugin.Name}.ini")); $"{plugin.Name}.ini"));
} }


+ 2
- 1
IPA.Loader/IPA.Loader.csproj View File

@ -112,15 +112,16 @@
<Compile Include="Loader\Features\ConfigProviderFeature.cs" /> <Compile Include="Loader\Features\ConfigProviderFeature.cs" />
<Compile Include="Loader\Features\DefineFeature.cs" /> <Compile Include="Loader\Features\DefineFeature.cs" />
<Compile Include="Loader\Features\InitInjectorFeature.cs" /> <Compile Include="Loader\Features\InitInjectorFeature.cs" />
<Compile Include="Loader\Features\NoRuntimeEnableFeature.cs" />
<Compile Include="Loader\Features\NoUpdateFeature.cs" /> <Compile Include="Loader\Features\NoUpdateFeature.cs" />
<Compile Include="Loader\Features\PrintFeature.cs" /> <Compile Include="Loader\Features\PrintFeature.cs" />
<Compile Include="Loader\HarmonyProtector.cs" /> <Compile Include="Loader\HarmonyProtector.cs" />
<Compile Include="Loader\PluginExecutor.cs" />
<Compile Include="Loader\PluginInitInjector.cs" /> <Compile Include="Loader\PluginInitInjector.cs" />
<Compile Include="Loader\LibLoader.cs" /> <Compile Include="Loader\LibLoader.cs" />
<Compile Include="Loader\Features\Feature.cs" /> <Compile Include="Loader\Features\Feature.cs" />
<Compile Include="Loader\PluginLoader.cs" /> <Compile Include="Loader\PluginLoader.cs" />
<Compile Include="Loader\PluginManifest.cs" /> <Compile Include="Loader\PluginManifest.cs" />
<Compile Include="Loader\PluginMetadata.cs" />
<Compile Include="Logging\ConsoleWindow.cs" /> <Compile Include="Logging\ConsoleWindow.cs" />
<Compile Include="Logging\Printers\ColorlessConsolePrinter.cs" /> <Compile Include="Logging\Printers\ColorlessConsolePrinter.cs" />
<Compile Include="Logging\Printers\PluginSubLogPrinter.cs" /> <Compile Include="Logging\Printers\PluginSubLogPrinter.cs" />


+ 65
- 65
IPA.Loader/Loader/Features/ConfigProviderFeature.cs View File

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

+ 74
- 68
IPA.Loader/Loader/Features/DefineFeature.cs View File

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

+ 223
- 223
IPA.Loader/Loader/Features/Feature.cs View File

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

+ 105
- 102
IPA.Loader/Loader/Features/InitInjectorFeature.cs View File

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

+ 0
- 29
IPA.Loader/Loader/Features/NoRuntimeEnableFeature.cs View File

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

+ 12
- 12
IPA.Loader/Loader/Features/NoUpdateFeature.cs View File

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

+ 32
- 32
IPA.Loader/Loader/Features/PrintFeature.cs View File

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

+ 189
- 0
IPA.Loader/Loader/PluginExecutor.cs View File

@ -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();
}
}
}

+ 5
- 5
IPA.Loader/Loader/PluginInitInjector.cs View File

@ -23,9 +23,9 @@ namespace IPA.Loader
/// </summary> /// </summary>
/// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param> /// <param name="previous">the previous return value of the function, or <see langword="null"/> if never called for plugin.</param>
/// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param> /// <param name="param">the <see cref="ParameterInfo"/> of the parameter being injected.</param>
/// <param name="meta">the <see cref="PluginLoader.PluginMetadata"/> for the plugin being loaded.</param>
/// <param name="meta">the <see cref="PluginMetadata"/> for the plugin being loaded.</param>
/// <returns>the value to inject into that parameter.</returns> /// <returns>the value to inject into that parameter.</returns>
public delegate object InjectParameter(object previous, ParameterInfo param, PluginLoader.PluginMetadata meta);
public delegate object InjectParameter(object previous, ParameterInfo param, PluginMetadata meta);
/// <summary> /// <summary>
/// Adds an injector to be used when calling future plugins' Init methods. /// Adds an injector to be used when calling future plugins' Init methods.
@ -45,7 +45,7 @@ namespace IPA.Loader
public TypedInjector(Type t, InjectParameter i) public TypedInjector(Type t, InjectParameter i)
{ Type = t; Injector = i; } { Type = t; Injector = i; }
public object Inject(object prev, ParameterInfo info, PluginLoader.PluginMetadata meta)
public object Inject(object prev, ParameterInfo info, PluginMetadata meta)
=> Injector(prev, info, meta); => Injector(prev, info, meta);
public bool Equals(TypedInjector other) public bool Equals(TypedInjector other)
@ -65,7 +65,7 @@ namespace IPA.Loader
private static readonly List<TypedInjector> injectors = new List<TypedInjector> private static readonly List<TypedInjector> injectors = new List<TypedInjector>
{ {
new TypedInjector(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)), new TypedInjector(typeof(Logger), (prev, param, meta) => prev ?? new StandardLogger(meta.Name)),
new TypedInjector(typeof(PluginLoader.PluginMetadata), (prev, param, meta) => prev ?? meta),
new TypedInjector(typeof(PluginMetadata), (prev, param, meta) => prev ?? meta),
new TypedInjector(typeof(Config.Config), (prev, param, meta) => new TypedInjector(typeof(Config.Config), (prev, param, meta) =>
{ {
if (prev != null) return prev; if (prev != null) return prev;
@ -107,7 +107,7 @@ namespace IPA.Loader
Expression.ArrayIndex(arr, Expression.Constant(i)), t)))); Expression.ArrayIndex(arr, Expression.Constant(i)), t))));
} }
internal static object[] Inject(ParameterInfo[] initParams, PluginLoader.PluginMetadata meta)
internal static object[] Inject(ParameterInfo[] initParams, PluginMetadata meta)
{ {
var initArgs = new List<object>(); var initArgs = new List<object>();


+ 0
- 255
IPA.Loader/Loader/PluginLoader.cs View File

@ -13,7 +13,6 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Version = SemVer.Version; using Version = SemVer.Version;
using SemVer; using SemVer;
using System.Linq.Expressions;
#if NET4 #if NET4
using Task = System.Threading.Tasks.Task; using Task = System.Threading.Tasks.Task;
using TaskEx = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task;
@ -45,260 +44,6 @@ namespace IPA.Loader
ResolveDependencies(); ResolveDependencies();
}); });
/// <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)}'";
}
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
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();
}
}
/// <summary> /// <summary>
/// A container object for all the data relating to a plugin. /// A container object for all the data relating to a plugin.
/// </summary> /// </summary>


+ 476
- 477
IPA.Loader/Loader/PluginManager.cs View File

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

+ 94
- 0
IPA.Loader/Loader/PluginMetadata.cs View File

@ -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)}'";
}
}

+ 25
- 26
IPA.Loader/Loader/manifest.json View File

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

Loading…
Cancel
Save