#nullable enable using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; #if NET3 using Net3_Proxy; #endif namespace IPA.Loader.Features { /// /// The root interface for a mod Feature. /// /// /// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature. /// public abstract class Feature { /// /// Initializes the feature with the data provided in the definition. /// /// /// This gets called AFTER your Init method, but BEFORE the target's Init method. If it is applied to the defining plugin, BeforeInit is not called. /// Returning does not prevent the plugin from being loaded. It simply prevents the feature from being used. /// /// the metadata of the plugin that is being prepared /// the data provided with the feature /// if the feature is valid for the plugin, otherwise protected abstract bool Initialize(PluginMetadata meta, JObject featureData); /// /// The message to be logged when the feature is not valid for a plugin. /// This should also be set whenever either returns false. /// /// the message to show when the feature is marked invalid public virtual string? InvalidMessage { get; protected set; } /// /// 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. /// /// the plugin to be initialized /// whether or not to call the Init method public virtual void BeforeInit(PluginMetadata plugin) { } /// /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. /// /// the plugin that was just initialized /// the instance of the plugin being initialized public virtual void AfterInit(PluginMetadata plugin, object pluginInstance) => AfterInit(plugin); /// /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception. /// /// the plugin that was just initialized public virtual void AfterInit(PluginMetadata plugin) { } /// /// Called after a plugin with this feature appplied is disabled. /// /// the plugin that was disabled public virtual void AfterDisable(PluginMetadata plugin) { } // TODO: rework features to take arguments as JSON objects [SuppressMessage("Nullability", "CS8618", Justification = "Reset sets those fields.")] static Feature() { Reset(); } internal static void Reset() { featureTypes = new() { { "IPA.DefineFeature", typeof(DefineFeature) } }; featureDelcarers = new() { { "IPA.DefineFeature", null } }; } private static Dictionary featureTypes; private static Dictionary featureDelcarers; internal static bool HasFeature(string name) => featureTypes.ContainsKey(name); 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)) throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type)); if (featureTypes.ContainsKey(name)) return false; if (featureDelcarers.TryGetValue(name, out var declarer)) { if (definingPlugin != declarer) return false; } else { featureDelcarers.Add(name, definingPlugin); } featureTypes.Add(name, type); return true; } private class EmptyFeature : Feature { protected override bool Initialize(PluginMetadata meta, JObject featureData) { throw new NotImplementedException(); } } internal string FeatureName = null!; internal class Instance { public readonly PluginMetadata AppliedTo; public readonly string Name; public readonly JObject Data; public Instance(PluginMetadata appliedTo, string name, JObject data) { AppliedTo = appliedTo; Name = name; 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) { if (type == null) { if (!featureTypes.TryGetValue(Name, out type)) { feature = new EmptyFeature() { InvalidMessage = "No such feature type found", FeatureName = Name }; 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(), FeatureName = Name }; } return result; } } } }