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.

272 lines
15 KiB

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