You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

183 lines
7.5 KiB

using System;
using System.Collections.Generic;
using System.Text;
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>
/// 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>
/// 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>
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>
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);
private static readonly Dictionary<string, Type> featureTypes = new Dictionary<string, Type>
{
{ "define-feature", typeof(DefineFeature) }
};
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;
bool readingParams = false;
bool removeWhitespace = true;
foreach (var chr in featureString)
{
if (escape)
{
builder.Append(chr);
escape = false;
}
else
{
switch (chr)
{
case '\\':
escape = true;
break;
case '(' when !readingParams:
removeWhitespace = true;
readingParams = true;
name = builder.ToString();
builder.Clear();
break;
case ')' when readingParams:
readingParams = false;
goto case ',';
case ',':
parameters.Add(builder.ToString());
if (!readingParams) break;
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 (readingParams)
{
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;
}
}
}
}