Browse Source

Implemented new Features system

pull/53/head
Anairkoen Schno 4 years ago
parent
commit
ce6e92ef5a
Signed by: DaNike GPG Key ID: BEFB74D5F3FC4387
12 changed files with 242 additions and 314 deletions
  1. +20
    -9
      IPA.Loader/Loader/Features/ConfigProviderFeature.cs
  2. +49
    -21
      IPA.Loader/Loader/Features/DefineFeature.cs
  3. +90
    -49
      IPA.Loader/Loader/Features/Feature.cs
  4. +0
    -105
      IPA.Loader/Loader/Features/InitInjectorFeature.cs
  5. +0
    -12
      IPA.Loader/Loader/Features/NoUpdateFeature.cs
  6. +0
    -32
      IPA.Loader/Loader/Features/PrintFeature.cs
  7. +48
    -57
      IPA.Loader/Loader/PluginLoader.cs
  8. +23
    -5
      IPA.Loader/Loader/PluginManager.cs
  9. +2
    -1
      IPA.Loader/Loader/PluginManifest.cs
  10. +4
    -0
      IPA.Loader/Loader/PluginMetadata.cs
  11. +6
    -8
      IPA.Loader/Loader/manifest.json
  12. +0
    -15
      IPA.Loader/Updating/BeatMods/Updater.cs

+ 20
- 9
IPA.Loader/Loader/Features/ConfigProviderFeature.cs View File

@ -1,28 +1,39 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO; using System.IO;
namespace IPA.Loader.Features namespace IPA.Loader.Features
{ {
internal class ConfigProviderFeature : Feature 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)
private class DataModel
{
[JsonProperty("type", Required = Required.Always)]
public string TypeName;
}
protected override bool Initialize(PluginMetadata meta, JObject featureData)
{
DataModel data;
try
{ {
InvalidMessage = "Incorrect number of parameters";
data = featureData.ToObject<DataModel>();
}
catch (Exception e)
{
InvalidMessage = $"Invalid data: {e}";
return false; return false;
} }
RequireLoaded(meta);
Type getType; Type getType;
try try
{ {
getType = meta.Assembly.GetType(parameters[0]);
getType = meta.Assembly.GetType(data.TypeName);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
InvalidMessage = $"Invalid type name {parameters[0]}";
InvalidMessage = $"Invalid type name {data.TypeName}";
return false; return false;
} }
catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException)


+ 49
- 21
IPA.Loader/Loader/Features/DefineFeature.cs View File

@ -1,4 +1,7 @@
using System;
using IPA.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO; using System.IO;
namespace IPA.Loader.Features namespace IPA.Loader.Features
@ -7,27 +10,49 @@ namespace IPA.Loader.Features
{ {
public static bool NewFeature = true; public static bool NewFeature = true;
protected internal override bool StoreOnPlugin => false;
private class DataModel
{
[JsonProperty("type", Required = Required.Always)]
public string TypeName;
[JsonProperty("name", Required = Required.DisallowNull)]
public string ActualName = null;
public override bool Initialize(PluginMetadata meta, string[] parameters)
{ // parameters should be (name, fully qualified type)
if (parameters.Length != 2)
public string Name => ActualName ?? TypeName;
}
private DataModel data;
protected override bool Initialize(PluginMetadata meta, JObject featureData)
{
Logger.features.Debug("Executing DefineFeature Init");
try
{ {
InvalidMessage = "Incorrect number of parameters";
data = featureData.ToObject<DataModel>();
}
catch (Exception e)
{
InvalidMessage = $"Invalid data: {e}";
return false; return false;
} }
RequireLoaded(meta);
InvalidMessage = $"Feature {data.Name} already exists";
return PreregisterFeature(meta, data.Name);
}
public override void AfterInit(PluginMetadata meta, object pluginInstance)
{
Logger.features.Debug("Executing DefineFeature AfterInit");
Type type; Type type;
try try
{ {
type = meta.Assembly.GetType(parameters[1]);
type = meta.Assembly.GetType(data.TypeName);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
InvalidMessage = $"Invalid type name {parameters[1]}";
return false;
Logger.features.Error($"Invalid type name {data.TypeName}");
return;
} }
catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException) catch (Exception e) when (e is FileNotFoundException || e is FileLoadException || e is BadImageFormatException)
{ {
@ -46,28 +71,31 @@ namespace IPA.Loader.Features
break; break;
} }
InvalidMessage = $"Could not find {filename} while loading type";
return false;
Logger.features.Error($"Could not find {filename} while loading type");
return;
} }
if (type == null) if (type == null)
{ {
InvalidMessage = $"Invalid type name {parameters[1]}";
return false;
Logger.features.Error($"Invalid type name {data.TypeName}");
return;
} }
try try
{ {
if (RegisterFeature(parameters[0], type)) return NewFeature = true;
InvalidMessage = $"Feature with name {parameters[0]} already exists";
return false;
if (RegisterFeature(meta, data.Name, type))
{
NewFeature = true;
return;
}
Logger.features.Error($"Feature with name {data.Name} already exists");
return;
} }
catch (ArgumentException) catch (ArgumentException)
{ {
InvalidMessage = $"{type.FullName} not a subclass of {nameof(Feature)}";
return false;
Logger.features.Error($"{type.FullName} not a subclass of {nameof(Feature)}");
return;
} }
} }
} }


+ 90
- 49
IPA.Loader/Loader/Features/Feature.cs View File

@ -1,5 +1,8 @@
using System;
using Mono.Cecil;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection;
using System.Text; using System.Text;
#if NET3 #if NET3
using Net3_Proxy; using Net3_Proxy;
@ -16,46 +19,24 @@ namespace IPA.Loader.Features
public abstract class Feature public abstract class Feature
{ {
/// <summary> /// <summary>
/// Initializes the feature with the parameters provided in the definition.
///
/// Note: When no parenthesis are provided, <paramref name="parameters"/> is an empty array.
/// Initializes the feature with the data provided in the definition.
/// </summary> /// </summary>
/// <remarks> /// <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.
/// <para>This gets called AFTER your <c>Init</c> method, but BEFORE the target's <c>Init</c> method. If it is applied to the defining plugin, <c>BeforeInit</c> is not called.</para>
/// <para>Returning <see langword="false" /> does <i>not</i> prevent the plugin from being loaded. It simply prevents the feature from being used.</para>
/// </remarks> /// </remarks>
/// <param name="meta">the metadata of the plugin that is being prepared</param> /// <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>
/// <param name="featureData">the data provided with the feature</param>
/// <returns><see langword="true"/> if the feature is valid for the plugin, <see langword="false"/> otherwise</returns> /// <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;
protected abstract bool Initialize(PluginMetadata meta, JObject featureData);
/// <summary> /// <summary>
/// The message to be logged when the feature is not valid for a plugin. /// 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.
/// This should also be set whenever either <see cref="BeforeInit"/> returns false.
/// </summary> /// </summary>
/// <value>the message to show when the feature is marked invalid</value> /// <value>the message to show when the feature is marked invalid</value>
public virtual string InvalidMessage { get; protected set; } 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> /// <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. /// 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> /// </summary>
@ -77,16 +58,10 @@ namespace IPA.Loader.Features
public virtual void AfterInit(PluginMetadata plugin) { } public virtual void AfterInit(PluginMetadata plugin) { }
/// <summary> /// <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.
/// Called after a plugin with this feature appplied is disabled.
/// </summary> /// </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;
/// <param name="plugin">the plugin that was disabled</param>
public virtual void AfterDisable(PluginMetadata plugin) { }
// TODO: rework features to take arguments as JSON objects // TODO: rework features to take arguments as JSON objects
@ -99,39 +74,105 @@ namespace IPA.Loader.Features
{ {
featureTypes = new Dictionary<string, Type> featureTypes = new Dictionary<string, Type>
{ {
{ "define-feature", typeof(DefineFeature) }
{ "IPA.DefineFeature", typeof(DefineFeature) }
};
featureDelcarers = new Dictionary<string, PluginMetadata>
{
{ "IPA.DefineFeature", null }
}; };
} }
private static Dictionary<string, Type> featureTypes; private static Dictionary<string, Type> featureTypes;
private static Dictionary<string, PluginMetadata> featureDelcarers;
internal static bool HasFeature(string name) => featureTypes.ContainsKey(name); internal static bool HasFeature(string name) => featureTypes.ContainsKey(name);
internal static bool RegisterFeature(string name, Type type)
internal static bool PreregisterFeature(PluginMetadata defining, string name)
{
if (featureDelcarers.ContainsKey(name)) return false;
featureDelcarers.Add(name, defining);
return true;
}
internal static bool RegisterFeature(PluginMetadata definingPlugin, string name, Type type)
{ {
if (!typeof(Feature).IsAssignableFrom(type)) if (!typeof(Feature).IsAssignableFrom(type))
throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type)); throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type));
if (featureTypes.ContainsKey(name)) return false; if (featureTypes.ContainsKey(name)) return false;
if (featureDelcarers.TryGetValue(name, out var declarer))
{
if (definingPlugin != declarer)
return false;
}
featureTypes.Add(name, type); featureTypes.Add(name, type);
return true; return true;
} }
internal struct FeatureParse
private class EmptyFeature : Feature
{ {
protected override bool Initialize(PluginMetadata meta, JObject featureData)
{
throw new NotImplementedException();
}
}
internal string FeatureName;
internal class Instance
{
public readonly PluginMetadata AppliedTo;
public readonly string Name; public readonly string Name;
public readonly string[] Parameters;
public readonly JObject Data;
public FeatureParse(string name, string[] parameters)
public Instance(PluginMetadata appliedTo, string name, JObject data)
{ {
AppliedTo = appliedTo;
Name = name; Name = name;
Parameters = parameters;
Data = data;
type = null;
}
private Type type;
public bool TryGetDefiningPlugin(out PluginMetadata plugin)
{
return featureDelcarers.TryGetValue(Name, out plugin);
}
// returns whether or not Initialize returned true, feature is always set when the thing exists
public bool TryCreate(out Feature feature)
{
feature = null;
if (type == null)
{
if (!featureTypes.TryGetValue(Name, out type))
return false;
}
bool result;
try
{
feature = (Feature)Activator.CreateInstance(type);
feature.FeatureName = Name;
result = feature.Initialize(AppliedTo, Data);
}
catch (Exception e)
{
result = false;
feature = new EmptyFeature() { InvalidMessage = e.ToString() };
}
return result;
} }
} }
/*
// returns false with both outs null for no such feature // returns false with both outs null for no such feature
internal static bool TryParseFeature(string featureString, PluginMetadata plugin, internal static bool TryParseFeature(string featureString, PluginMetadata plugin,
out Feature feature, out Exception failException, out bool featureValid, out FeatureParse parsed,
FeatureParse? preParsed = null)
out Feature feature, out Exception failException, out bool featureValid, out Instance parsed,
Instance? preParsed = null)
{ {
failException = null; failException = null;
feature = null; feature = null;
@ -190,7 +231,7 @@ namespace IPA.Loader.Features
if (name == null) if (name == null)
name = builder.ToString(); name = builder.ToString();
parsed = new FeatureParse(name, parameters.ToArray());
parsed = new Instance(name, parameters.ToArray());
if (parens != 0) if (parens != 0)
{ {
@ -212,7 +253,7 @@ namespace IPA.Loader.Features
return false; return false;
} }
featureValid = aFeature.Initialize(plugin, parsed.Parameters);
featureValid = aFeature.Initialize(plugin, TODO);
feature = aFeature; feature = aFeature;
return true; return true;
} }
@ -221,6 +262,6 @@ namespace IPA.Loader.Features
failException = e; failException = e;
return false; return false;
} }
}
}*/
} }
} }

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

@ -1,105 +0,0 @@
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
- 12
IPA.Loader/Loader/Features/NoUpdateFeature.cs View File

@ -1,12 +0,0 @@
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";
}
}

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

@ -1,32 +0,0 @@

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

+ 48
- 57
IPA.Loader/Loader/PluginLoader.cs View File

@ -39,6 +39,7 @@ namespace IPA.Loader
LoadMetadata(); LoadMetadata();
Resolve(); Resolve();
InitFeatures();
ComputeLoadOrder(); ComputeLoadOrder();
FilterDisabled(); FilterDisabled();
FilterWithoutFiles(); FilterWithoutFiles();
@ -687,57 +688,47 @@ namespace IPA.Loader
internal static void InitFeatures() internal static void InitFeatures()
{ {
var parsedFeatures = PluginsMetadata.Select(m =>
(metadata: m,
features: m.Manifest.Features.Select(feature =>
(feature, parsed: Ref.Create<Feature.FeatureParse?>(null))
).ToList()
)
).ToList();
while (DefineFeature.NewFeature)
foreach (var meta in PluginsMetadata)
{ {
DefineFeature.NewFeature = false;
foreach (var (metadata, features) in parsedFeatures)
for (var i = 0; i < features.Count; i++)
{
var feature = features[i];
var success = Feature.TryParseFeature(feature.feature, metadata, out var featureObj,
out var exception, out var valid, out var parsed, feature.parsed.Value);
if (!success && !valid && featureObj == null && exception == null) // no feature of type found
feature.parsed.Value = parsed;
else if (success)
foreach (var feature in meta.Manifest.Features.Select(f => new Feature.Instance(meta, f.Key, f.Value)))
{
if (feature.TryGetDefiningPlugin(out var plugin) && plugin == null)
{ // this is a DefineFeature, so we want to initialize it early
if (!feature.TryCreate(out var inst))
{ {
if (valid && featureObj.StoreOnPlugin)
metadata.InternalFeatures.Add(featureObj);
else if (!valid)
Logger.features.Warn(
$"Feature not valid on {metadata.Name}: {featureObj.InvalidMessage}");
features.RemoveAt(i--);
Logger.features.Error($"Error evaluating {feature.Name}: {inst.InvalidMessage}");
} }
else else
{ {
Logger.features.Error($"Error parsing feature definition on {metadata.Name}");
Logger.features.Error(exception);
features.RemoveAt(i--);
meta.InternalFeatures.Add(inst);
} }
} }
foreach (var plugin in PluginsMetadata)
foreach (var feature in plugin.Features)
feature.Evaluate();
else
{ // this is literally any other feature, so we want to delay its initialization
meta.UnloadedFeatures.Add(feature);
}
}
} }
foreach (var plugin in parsedFeatures)
// at this point we have pre-initialized all features, so we can go ahead and use them to add stuff to the dep resolver
foreach (var meta in PluginsMetadata)
{ {
if (plugin.features.Count <= 0) continue;
foreach (var feature in meta.UnloadedFeatures)
{
if (feature.TryGetDefiningPlugin(out var plugin))
{
if (plugin != meta)
{ // if the feature is not applied to the defining feature
meta.LoadsAfter.Add(plugin);
}
Logger.features.Warn($"On plugin {plugin.metadata.Name}:");
foreach (var feature in plugin.features)
Logger.features.Warn($" Feature not found with name {feature.feature}");
plugin.CreateFeaturesWhenLoaded.Add(feature);
}
else
{
Logger.features.Warn($"No such feature {feature.Name}");
}
}
} }
} }
@ -807,20 +798,6 @@ namespace IPA.Loader
Load(meta); Load(meta);
foreach (var feature in meta.Features)
{
if (!feature.BeforeLoad(meta))
{
Logger.loader.Warn(
$"Feature {feature?.GetType()} denied plugin {meta.Name} from loading! {feature?.InvalidMessage}");
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
{
ReasonText = $"Denied in {nameof(Feature.BeforeLoad)} of feature {feature?.GetType()}:\n\t{feature?.InvalidMessage}"
});
return null;
}
}
PluginExecutor exec; PluginExecutor exec;
try try
{ {
@ -838,10 +815,10 @@ namespace IPA.Loader
if (!feature.BeforeInit(meta)) if (!feature.BeforeInit(meta))
{ {
Logger.loader.Warn( Logger.loader.Warn(
$"Feature {feature?.GetType()} denied plugin {meta.Name} from initializing! {feature?.InvalidMessage}");
$"Feature {feature?.FeatureName} denied plugin {meta.Name} from initializing! {feature?.InvalidMessage}");
ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature) ignoredPlugins.Add(meta, new IgnoreReason(Reason.Feature)
{ {
ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {feature?.GetType()}:\n\t{feature?.InvalidMessage}"
ReasonText = $"Denied in {nameof(Feature.BeforeInit)} of feature {feature?.FeatureName}:\n\t{feature?.InvalidMessage}"
}); });
return null; return null;
} }
@ -863,6 +840,21 @@ namespace IPA.Loader
return null; return null;
} }
// TODO: make this new features system behave better wrt DynamicInit plugins
foreach (var feature in meta.CreateFeaturesWhenLoaded)
{
if (!feature.TryCreate(out var inst))
{
Logger.features.Warn($"Could not create instance of feature {feature.Name}: {inst.InvalidMessage}");
}
else
{
feature.AppliedTo.InternalFeatures.Add(inst);
feature.AppliedTo.UnloadedFeatures.Remove(feature);
}
}
meta.CreateFeaturesWhenLoaded.Clear(); // if a plugin is loaded twice, for the moment, we don't want to create the feature twice
foreach (var feature in meta.Features) foreach (var feature in meta.Features)
try try
{ {
@ -880,7 +872,6 @@ namespace IPA.Loader
internal static List<PluginExecutor> LoadPlugins() internal static List<PluginExecutor> LoadPlugins()
{ {
InitFeatures();
DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily DisabledPlugins.ForEach(Load); // make sure they get loaded into memory so their metadata and stuff can be read more easily
var list = new List<PluginExecutor>(); var list = new List<PluginExecutor>();


+ 23
- 5
IPA.Loader/Loader/PluginManager.cs View File

@ -14,6 +14,7 @@ using UnityEngine;
using Logger = IPA.Logging.Logger; using Logger = IPA.Logging.Logger;
using System.Threading.Tasks; using System.Threading.Tasks;
using IPA.Utilities.Async; using IPA.Utilities.Async;
using IPA.Loader.Features;
#if NET4 #if NET4
using TaskEx = System.Threading.Tasks.Task; using TaskEx = System.Threading.Tasks.Task;
using TaskEx6 = System.Threading.Tasks.Task; using TaskEx6 = System.Threading.Tasks.Task;
@ -189,16 +190,33 @@ namespace IPA.Loader
{ {
if (alreadyDisabled.TryGetValue(exec.Executor, out var task)) if (alreadyDisabled.TryGetValue(exec.Executor, out var task))
return task; return task;
else
else
{ {
if (exec.Executor.Metadata.RuntimeOptions != RuntimeOptions.DynamicInit) if (exec.Executor.Metadata.RuntimeOptions != RuntimeOptions.DynamicInit)
return TaskEx6.FromException(new CannotRuntimeDisableException(exec.Executor.Metadata)); return TaskEx6.FromException(new CannotRuntimeDisableException(exec.Executor.Metadata));
var res = TaskEx.WhenAll(exec.Dependents.Select(d => Disable(d, alreadyDisabled))) var res = TaskEx.WhenAll(exec.Dependents.Select(d => Disable(d, alreadyDisabled)))
.ContinueWith(t => t.IsFaulted
? TaskEx.WhenAll(t, TaskEx6.FromException(
new CannotRuntimeDisableException(exec.Executor.Metadata, "Dependents cannot be disabled for plugin")))
: exec.Executor.Disable(), UnityMainThreadTaskScheduler.Default).Unwrap();
.ContinueWith(t =>
{
if (t.IsFaulted) {
return TaskEx.WhenAll(t, TaskEx6.FromException(
new CannotRuntimeDisableException(exec.Executor.Metadata, "Dependents cannot be disabled for plugin")));
}
return exec.Executor.Disable()
.ContinueWith(t =>
{
foreach (var feature in exec.Executor.Metadata.Features)
{
try {
feature.AfterDisable(exec.Executor.Metadata);
}
catch (Exception e)
{
Logger.loader.Critical($"Feature errored in {nameof(Feature.AfterDisable)}: {e}");
}
}
}, UnityMainThreadTaskScheduler.Default);
}, UnityMainThreadTaskScheduler.Default).Unwrap();
// We do not want to call the disable method if a dependent couldn't be disabled // We do not want to call the disable method if a dependent couldn't be disabled
// By scheduling on a UnityMainThreadScheduler, we ensure that Disable() is always called on the Unity main thread // By scheduling on a UnityMainThreadScheduler, we ensure that Disable() is always called on the Unity main thread
alreadyDisabled.Add(exec.Executor, res); alreadyDisabled.Add(exec.Executor, res);


+ 2
- 1
IPA.Loader/Loader/PluginManifest.cs View File

@ -1,6 +1,7 @@
using IPA.JsonConverters; using IPA.JsonConverters;
using IPA.Utilities; using IPA.Utilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SemVer; using SemVer;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -40,7 +41,7 @@ namespace IPA.Loader
public Dictionary<string, Range> Conflicts = new Dictionary<string, Range>(); public Dictionary<string, Range> Conflicts = new Dictionary<string, Range>();
[JsonProperty("features", Required = Required.DisallowNull)] [JsonProperty("features", Required = Required.DisallowNull)]
public string[] Features = Array.Empty<string>();
public Dictionary<string, JObject> Features = new Dictionary<string, JObject>();
[JsonProperty("loadBefore", Required = Required.DisallowNull)] [JsonProperty("loadBefore", Required = Required.DisallowNull)]
public string[] LoadBefore = Array.Empty<string>(); public string[] LoadBefore = Array.Empty<string>();


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

@ -76,6 +76,10 @@ namespace IPA.Loader
internal readonly List<Feature> InternalFeatures = new List<Feature>(); internal readonly List<Feature> InternalFeatures = new List<Feature>();
internal readonly HashSet<Feature.Instance> UnloadedFeatures = new HashSet<Feature.Instance>();
internal readonly List<Feature.Instance> CreateFeaturesWhenLoaded = new List<Feature.Instance>();
/// <summary> /// <summary>
/// A list of files (that aren't <see cref="File"/>) that are associated with this plugin. /// A list of files (that aren't <see cref="File"/>) that are associated with this plugin.
/// </summary> /// </summary>


+ 6
- 8
IPA.Loader/Loader/manifest.json View File

@ -10,14 +10,12 @@
"name": "Beat Saber IPA", "name": "Beat Saber IPA",
"version": "4.0.6", "version": "4.0.6",
"icon": "IPA.icon_white.png", "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)"
],
"features": {
"IPA.DefineFeature": {
"type": "IPA.Loader.Features.ConfigProviderFeature",
"name": "IPA.ConfigProvider"
}
},
"links": { "links": {
"project-home": "https://beat-saber-modding-group.github.io/BeatSaber-IPA-Reloaded/index.html", "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", "project-source": "https://github.com/beat-saber-modding-group/BeatSaber-IPA-Reloaded",


+ 0
- 15
IPA.Loader/Updating/BeatMods/Updater.cs View File

@ -204,11 +204,6 @@ namespace IPA.Updating.BeatMods
LocalPluginMeta = msinfo LocalPluginMeta = msinfo
}; };
if (msinfo.Features.FirstOrDefault(f => f is NoUpdateFeature) != null)
{ // disable updating, by only matching self, so that dependencies can still be resolved
dep.Requirement = new Range(msinfo.Version.ToString());
}
depList.Value.Add(dep); depList.Value.Add(dep);
} }
} }
@ -225,11 +220,6 @@ namespace IPA.Updating.BeatMods
LocalPluginMeta = meta LocalPluginMeta = meta
}; };
if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null)
{ // disable updating, by only matching self
dep.Requirement = new Range(meta.Version.ToString());
}
depList.Value.Add(dep); depList.Value.Add(dep);
} }
} }
@ -246,11 +236,6 @@ namespace IPA.Updating.BeatMods
LocalPluginMeta = meta LocalPluginMeta = meta
}; };
if (meta.Features.FirstOrDefault(f => f is NoUpdateFeature) != null)
{ // disable updating, by only matching self
dep.Requirement = new Range(meta.Version.ToString());
}
depList.Value.Add(dep); depList.Value.Add(dep);
} }
} }


Loading…
Cancel
Save