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.

176 lines
7.0 KiB

  1. #nullable enable
  2. using Newtonsoft.Json.Linq;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Diagnostics.CodeAnalysis;
  6. #if NET3
  7. using Net3_Proxy;
  8. #endif
  9. namespace IPA.Loader.Features
  10. {
  11. /// <summary>
  12. /// The root interface for a mod Feature.
  13. /// </summary>
  14. /// <remarks>
  15. /// Avoid storing any data in any subclasses. If you do, it may result in a failure to load the feature.
  16. /// </remarks>
  17. public abstract class Feature
  18. {
  19. /// <summary>
  20. /// Initializes the feature with the data provided in the definition.
  21. /// </summary>
  22. /// <remarks>
  23. /// <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>
  24. /// <para>Returning <see langword="false" /> does <i>not</i> prevent the plugin from being loaded. It simply prevents the feature from being used.</para>
  25. /// </remarks>
  26. /// <param name="meta">the metadata of the plugin that is being prepared</param>
  27. /// <param name="featureData">the data provided with the feature</param>
  28. /// <returns><see langword="true"/> if the feature is valid for the plugin, <see langword="false"/> otherwise</returns>
  29. protected abstract bool Initialize(PluginMetadata meta, JObject featureData);
  30. /// <summary>
  31. /// The message to be logged when the feature is not valid for a plugin.
  32. /// This should also be set whenever either <see cref="BeforeInit"/> returns false.
  33. /// </summary>
  34. /// <value>the message to show when the feature is marked invalid</value>
  35. public virtual string? InvalidMessage { get; protected set; }
  36. /// <summary>
  37. /// 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.
  38. /// </summary>
  39. /// <param name="plugin">the plugin to be initialized</param>
  40. /// <returns>whether or not to call the Init method</returns>
  41. public virtual void BeforeInit(PluginMetadata plugin) { }
  42. /// <summary>
  43. /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception.
  44. /// </summary>
  45. /// <param name="plugin">the plugin that was just initialized</param>
  46. /// <param name="pluginInstance">the instance of the plugin being initialized</param>
  47. public virtual void AfterInit(PluginMetadata plugin, object pluginInstance) => AfterInit(plugin);
  48. /// <summary>
  49. /// Called after a plugin has been fully initialized, whether or not there is an `Init` method. This should never throw an exception.
  50. /// </summary>
  51. /// <param name="plugin">the plugin that was just initialized</param>
  52. public virtual void AfterInit(PluginMetadata plugin) { }
  53. /// <summary>
  54. /// Called after a plugin with this feature appplied is disabled.
  55. /// </summary>
  56. /// <param name="plugin">the plugin that was disabled</param>
  57. public virtual void AfterDisable(PluginMetadata plugin) { }
  58. // TODO: rework features to take arguments as JSON objects
  59. [SuppressMessage("Nullability", "CS8618", Justification = "Reset sets those fields.")]
  60. static Feature()
  61. {
  62. Reset();
  63. }
  64. internal static void Reset()
  65. {
  66. featureTypes = new()
  67. {
  68. { "IPA.DefineFeature", typeof(DefineFeature) }
  69. };
  70. featureDelcarers = new()
  71. {
  72. { "IPA.DefineFeature", null }
  73. };
  74. }
  75. private static Dictionary<string, Type> featureTypes;
  76. private static Dictionary<string, PluginMetadata?> featureDelcarers;
  77. internal static bool HasFeature(string name) => featureTypes.ContainsKey(name);
  78. internal static bool PreregisterFeature(PluginMetadata defining, string name)
  79. {
  80. if (featureDelcarers.ContainsKey(name)) return false;
  81. featureDelcarers.Add(name, defining);
  82. return true;
  83. }
  84. internal static bool RegisterFeature(PluginMetadata definingPlugin, string name, Type type)
  85. {
  86. if (!typeof(Feature).IsAssignableFrom(type))
  87. throw new ArgumentException($"Feature type not subclass of {nameof(Feature)}", nameof(type));
  88. if (featureTypes.ContainsKey(name)) return false;
  89. if (featureDelcarers.TryGetValue(name, out var declarer))
  90. {
  91. if (definingPlugin != declarer)
  92. return false;
  93. }
  94. else
  95. {
  96. featureDelcarers.Add(name, definingPlugin);
  97. }
  98. featureTypes.Add(name, type);
  99. return true;
  100. }
  101. private class EmptyFeature : Feature
  102. {
  103. protected override bool Initialize(PluginMetadata meta, JObject featureData)
  104. {
  105. throw new NotImplementedException();
  106. }
  107. }
  108. internal string FeatureName = null!;
  109. internal class Instance
  110. {
  111. public readonly PluginMetadata AppliedTo;
  112. public readonly string Name;
  113. public readonly JObject Data;
  114. public Instance(PluginMetadata appliedTo, string name, JObject data)
  115. {
  116. AppliedTo = appliedTo;
  117. Name = name;
  118. Data = data;
  119. type = null;
  120. }
  121. private Type? type;
  122. public bool TryGetDefiningPlugin(out PluginMetadata? plugin)
  123. {
  124. return featureDelcarers.TryGetValue(Name, out plugin);
  125. }
  126. // returns whether or not Initialize returned true, feature is always set when the thing exists
  127. public bool TryCreate(out Feature feature)
  128. {
  129. if (type == null)
  130. {
  131. if (!featureTypes.TryGetValue(Name, out type))
  132. {
  133. feature = new EmptyFeature() { InvalidMessage = "No such feature type found", FeatureName = Name };
  134. return false;
  135. }
  136. }
  137. bool result;
  138. try
  139. {
  140. feature = (Feature)Activator.CreateInstance(type);
  141. feature.FeatureName = Name;
  142. result = feature.Initialize(AppliedTo, Data);
  143. }
  144. catch (Exception e)
  145. {
  146. result = false;
  147. feature = new EmptyFeature() { InvalidMessage = e.ToString(), FeatureName = Name };
  148. }
  149. return result;
  150. }
  151. }
  152. }
  153. }