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.

245 lines
13 KiB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace IPA.Loader
  7. {
  8. /// <summary>
  9. /// A class to represent a transaction for changing the state of loaded mods.
  10. /// </summary>
  11. public sealed class StateTransitionTransaction : IDisposable
  12. {
  13. private readonly HashSet<PluginMetadata> currentlyEnabled;
  14. private readonly HashSet<PluginMetadata> currentlyDisabled;
  15. private readonly HashSet<PluginMetadata> toEnable = new HashSet<PluginMetadata>();
  16. private readonly HashSet<PluginMetadata> toDisable = new HashSet<PluginMetadata>();
  17. internal StateTransitionTransaction(IEnumerable<PluginMetadata> enabled, IEnumerable<PluginMetadata> disabled)
  18. {
  19. currentlyEnabled = new HashSet<PluginMetadata>(enabled.ToArray());
  20. currentlyDisabled = new HashSet<PluginMetadata>(disabled.ToArray());
  21. }
  22. /// <summary>
  23. /// Gets whether or not a game restart will be necessary to fully apply this transaction.
  24. /// </summary>
  25. /// <value><see langword="true"/> if any mod who's state is changed cannot be changed at runtime, <see langword="false"/> otherwise</value>
  26. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  27. public bool WillNeedRestart
  28. => ThrowIfDisposed<bool>()
  29. || toEnable.Concat(toDisable).Any(m => m.RuntimeOptions != RuntimeOptions.DynamicInit);
  30. internal IEnumerable<PluginMetadata> CurrentlyEnabled => currentlyEnabled;
  31. internal IEnumerable<PluginMetadata> CurrentlyDisabled => currentlyDisabled;
  32. internal IEnumerable<PluginMetadata> ToEnable => toEnable;
  33. internal IEnumerable<PluginMetadata> ToDisable => toDisable;
  34. /// <summary>
  35. /// Gets a list of plugins that are enabled according to this transaction's current state.
  36. /// </summary>
  37. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  38. public IEnumerable<PluginMetadata> EnabledPlugins
  39. => ThrowIfDisposed<IEnumerable<PluginMetadata>>() ?? DisabledPluginsInternal;
  40. private IEnumerable<PluginMetadata> EnabledPluginsInternal => currentlyEnabled.Except(toDisable).Concat(toEnable);
  41. /// <summary>
  42. /// Gets a list of plugins that are disabled according to this transaction's current state.
  43. /// </summary>
  44. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  45. public IEnumerable<PluginMetadata> DisabledPlugins
  46. => ThrowIfDisposed<IEnumerable<PluginMetadata>>() ?? DisabledPluginsInternal;
  47. private IEnumerable<PluginMetadata> DisabledPluginsInternal => currentlyDisabled.Except(toEnable).Concat(toDisable);
  48. /// <summary>
  49. /// Checks if a plugin is enabled according to this transaction's current state.
  50. /// </summary>
  51. /// <remarks>
  52. /// <para>This should be roughly equivalent to <c>EnabledPlugins.Contains(meta)</c>, but more performant.</para>
  53. /// <para>This should also always return the inverse of <see cref="IsDisabled(PluginMetadata)"/> for valid plugins.</para>
  54. /// </remarks>
  55. /// <param name="meta">the plugin to check</param>
  56. /// <returns><see langword="true"/> if the plugin is enabled, <see langword="false"/> otherwise</returns>
  57. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  58. /// <seealso cref="EnabledPlugins"/>
  59. /// <seealso cref="IsDisabled(PluginMetadata)"/>
  60. public bool IsEnabled(PluginMetadata meta)
  61. => ThrowIfDisposed<bool>() || IsEnabledInternal(meta);
  62. private bool IsEnabledInternal(PluginMetadata meta)
  63. => (currentlyEnabled.Contains(meta) && !toDisable.Contains(meta))
  64. || toEnable.Contains(meta);
  65. /// <summary>
  66. /// Checks if a plugin is disabled according to this transaction's current state.
  67. /// </summary>
  68. /// <remarks>
  69. /// <para>This should be roughly equivalent to <c>DisabledPlugins.Contains(meta)</c>, but more performant.</para>
  70. /// <para>This should also always return the inverse of <see cref="IsEnabled(PluginMetadata)"/> for valid plugins.</para>
  71. /// </remarks>
  72. /// <param name="meta">the plugin to check</param>
  73. /// <returns><see langword="true"/> if the plugin is disabled, <see langword="false"/> otherwise</returns>
  74. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  75. /// <seealso cref="DisabledPlugins"/>
  76. /// <seealso cref="IsEnabled(PluginMetadata)"/>
  77. public bool IsDisabled(PluginMetadata meta)
  78. => ThrowIfDisposed<bool>() || IsDisabledInternal(meta);
  79. private bool IsDisabledInternal(PluginMetadata meta)
  80. => (currentlyDisabled.Contains(meta) && !toEnable.Contains(meta))
  81. || toDisable.Contains(meta);
  82. /// <summary>
  83. /// Enables a plugin in this transaction.
  84. /// </summary>
  85. /// <param name="meta">the plugin to enable</param>
  86. /// <param name="autoDeps">whether or not to automatically enable all dependencies of the plugin</param>
  87. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  88. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  89. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  90. /// <seealso cref="Enable(PluginMetadata, out IEnumerable{PluginMetadata}, bool)"/>
  91. public bool Enable(PluginMetadata meta, bool autoDeps = true)
  92. => Enable(meta, out var _, autoDeps);
  93. /// <summary>
  94. /// Enables a plugin in this transaction.
  95. /// </summary>
  96. /// <remarks>
  97. /// <paramref name="disabledDeps"/> will only be set when <paramref name="autoDeps"/> is <see langword="false"/>.
  98. /// </remarks>
  99. /// <param name="meta">the plugin to enable</param>
  100. /// <param name="disabledDeps"><see langword="null"/> if successful, otherwise a set of plugins that need to be enabled first</param>
  101. /// <param name="autoDeps">whether or not to automatically enable all dependencies</param>
  102. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  103. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  104. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  105. public bool Enable(PluginMetadata meta, out IEnumerable<PluginMetadata> disabledDeps, bool autoDeps = false)
  106. { // returns whether or not state was changed
  107. ThrowIfDisposed();
  108. if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
  109. throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin");
  110. disabledDeps = null;
  111. if (IsEnabledInternal(meta)) return false;
  112. var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m));
  113. if (autoDeps)
  114. {
  115. foreach (var dep in needsEnabled)
  116. {
  117. var res = Enable(dep, out var failedDisabled, true);
  118. if (failedDisabled == null) continue;
  119. disabledDeps = failedDisabled;
  120. return res;
  121. }
  122. }
  123. else if (needsEnabled.Any())
  124. {
  125. // there are currently enabled plugins that depend on this
  126. disabledDeps = needsEnabled;
  127. return false;
  128. }
  129. toDisable.Remove(meta);
  130. toEnable.Add(meta);
  131. return true;
  132. }
  133. /// <summary>
  134. /// Disables a plugin in this transaction.
  135. /// </summary>
  136. /// <param name="meta">the plugin to disable</param>
  137. /// <param name="autoDependents">whether or not to automatically disable all dependents of the plugin</param>
  138. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  139. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  140. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  141. /// <seealso cref="Disable(PluginMetadata, out IEnumerable{PluginMetadata}, bool)"/>
  142. public bool Disable(PluginMetadata meta, bool autoDependents = true)
  143. => Disable(meta, out var _, autoDependents);
  144. /// <summary>
  145. /// Disables a plugin in this transaction.
  146. /// </summary>
  147. /// <remarks>
  148. /// <paramref name="enabledDependents"/> will only be set when <paramref name="autoDependents"/> is <see langword="false"/>.
  149. /// </remarks>
  150. /// <param name="meta">the plugin to disable</param>
  151. /// <param name="enabledDependents"><see langword="null"/> if successful, otherwise a set of plugins that need to be disabled first</param>
  152. /// <param name="autoDependents">whether or not to automatically disable all dependents of the plugin</param>
  153. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  154. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  155. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  156. public bool Disable(PluginMetadata meta, out IEnumerable<PluginMetadata> enabledDependents, bool autoDependents = false)
  157. { // returns whether or not state was changed
  158. ThrowIfDisposed();
  159. if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
  160. throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin");
  161. enabledDependents = null;
  162. if (IsDisabledInternal(meta)) return false;
  163. var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta));
  164. if (autoDependents)
  165. {
  166. foreach (var dep in needsDisabled)
  167. {
  168. var res = Disable(dep, out var failedEnabled, true);
  169. if (failedEnabled == null) continue;
  170. enabledDependents = failedEnabled;
  171. return res;
  172. }
  173. }
  174. else if (needsDisabled.Any())
  175. {
  176. // there are currently enabled plugins that depend on this
  177. enabledDependents = needsDisabled;
  178. return false;
  179. }
  180. toDisable.Add(meta);
  181. toEnable.Remove(meta);
  182. return true;
  183. }
  184. /// <summary>
  185. /// Commits this transaction to actual state, enabling and disabling plugins as necessary.
  186. /// </summary>
  187. /// <remarks>
  188. /// <para>After this completes, this transaction will be disposed.</para>
  189. /// <para>
  190. /// The <see cref="Task"/> that is returned will error if <b>any</b> of the mods being <b>disabled</b>
  191. /// error. It is up to the caller to handle these in a sane way, like logging them. If nothing else, do something like this:
  192. /// <code lang="csharp">
  193. /// // get your transaction...
  194. /// var complete = transaction.Commit();
  195. /// await complete.ContinueWith(t => {
  196. /// if (t.IsFaulted)
  197. /// Logger.log.Error($"Error disabling plugins: {t.Exception}");
  198. /// });
  199. /// </code>
  200. /// If you are running in a coroutine, you can use <see cref="Utilities.Async.Coroutines.WaitForTask(Task)"/> instead of <see langword="await"/>.
  201. /// </para>
  202. /// <para>
  203. /// If you are running on the Unity main thread, this will block until all enabling is done, and will return a task representing the disables.
  204. /// Otherwise, the task returned represents both, and <i>will not complete</i> until Unity has done (possibly) several updates, depending on
  205. /// the number of plugins being disabled, and the time they take.
  206. /// </para>
  207. /// </remarks>
  208. /// <returns>a <see cref="Task"/> which completes whenever all disables complete</returns>
  209. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  210. /// <exception cref="InvalidOperationException">if the plugins' state no longer matches this transaction's original state</exception>
  211. public Task Commit() => ThrowIfDisposed<Task>() ?? PluginManager.CommitTransaction(this);
  212. private void ThrowIfDisposed() => ThrowIfDisposed<byte>();
  213. private T ThrowIfDisposed<T>()
  214. {
  215. if (disposed)
  216. throw new ObjectDisposedException(nameof(StateTransitionTransaction));
  217. return default;
  218. }
  219. private bool disposed = false;
  220. /// <summary>
  221. /// Disposes and discards this transaction without committing it.
  222. /// </summary>
  223. public void Dispose()
  224. => disposed = true;
  225. }
  226. }