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. 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. private bool stateChanged = false;
  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 (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
  116. throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin");
  117. disabledDeps = null;
  118. if (IsEnabledInternal(meta)) return false;
  119. var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m));
  120. if (autoDeps)
  121. {
  122. foreach (var dep in needsEnabled)
  123. {
  124. var res = Enable(dep, out var failedDisabled, true);
  125. if (failedDisabled == null) continue;
  126. disabledDeps = failedDisabled;
  127. return res;
  128. }
  129. }
  130. else if (needsEnabled.Any())
  131. {
  132. // there are currently enabled plugins that depend on this
  133. disabledDeps = needsEnabled;
  134. return false;
  135. }
  136. toDisable.Remove(meta);
  137. toEnable.Add(meta);
  138. stateChanged = true;
  139. return true;
  140. }
  141. /// <summary>
  142. /// Disables a plugin in this transaction.
  143. /// </summary>
  144. /// <param name="meta">the plugin to disable</param>
  145. /// <param name="autoDependents">whether or not to automatically disable all dependents of the plugin</param>
  146. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  147. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  148. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  149. /// <seealso cref="Disable(PluginMetadata, out IEnumerable{PluginMetadata}, bool)"/>
  150. public bool Disable(PluginMetadata meta, bool autoDependents = true)
  151. => Disable(meta, out var _, autoDependents);
  152. /// <summary>
  153. /// Disables a plugin in this transaction.
  154. /// </summary>
  155. /// <remarks>
  156. /// <paramref name="enabledDependents"/> will only be set when <paramref name="autoDependents"/> is <see langword="false"/>.
  157. /// </remarks>
  158. /// <param name="meta">the plugin to disable</param>
  159. /// <param name="enabledDependents"><see langword="null"/> if successful, otherwise a set of plugins that need to be disabled first</param>
  160. /// <param name="autoDependents">whether or not to automatically disable all dependents of the plugin</param>
  161. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  162. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  163. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  164. public bool Disable(PluginMetadata meta, out IEnumerable<PluginMetadata> enabledDependents, bool autoDependents = false)
  165. { // returns whether or not state was changed
  166. ThrowIfDisposed();
  167. if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
  168. throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin");
  169. enabledDependents = null;
  170. if (IsDisabledInternal(meta)) return false;
  171. var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta));
  172. if (autoDependents)
  173. {
  174. foreach (var dep in needsDisabled)
  175. {
  176. var res = Disable(dep, out var failedEnabled, true);
  177. if (failedEnabled == null) continue;
  178. enabledDependents = failedEnabled;
  179. return res;
  180. }
  181. }
  182. else if (needsDisabled.Any())
  183. {
  184. // there are currently enabled plugins that depend on this
  185. enabledDependents = needsDisabled;
  186. return false;
  187. }
  188. toDisable.Add(meta);
  189. toEnable.Remove(meta);
  190. stateChanged = true;
  191. return true;
  192. }
  193. /// <summary>
  194. /// Commits this transaction to actual state, enabling and disabling plugins as necessary.
  195. /// </summary>
  196. /// <remarks>
  197. /// <para>After this completes, this transaction will be disposed.</para>
  198. /// <para>
  199. /// The <see cref="Task"/> that is returned will error if <b>any</b> of the mods being <b>disabled</b>
  200. /// error. It is up to the caller to handle these in a sane way, like logging them. If nothing else, do something like this:
  201. /// <code lang="csharp">
  202. /// // get your transaction...
  203. /// var complete = transaction.Commit();
  204. /// await complete.ContinueWith(t => {
  205. /// if (t.IsFaulted)
  206. /// Logger.log.Error($"Error disabling plugins: {t.Exception}");
  207. /// });
  208. /// </code>
  209. /// If you are running in a coroutine, you can use <see cref="Utilities.Async.Coroutines.WaitForTask(Task)"/> instead of <see langword="await"/>.
  210. /// </para>
  211. /// <para>
  212. /// 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.
  213. /// Otherwise, the task returned represents both, and <i>will not complete</i> until Unity has done (possibly) several updates, depending on
  214. /// the number of plugins being disabled, and the time they take.
  215. /// </para>
  216. /// </remarks>
  217. /// <returns>a <see cref="Task"/> which completes whenever all disables complete</returns>
  218. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  219. /// <exception cref="InvalidOperationException">if the plugins' state no longer matches this transaction's original state</exception>
  220. public Task Commit() => ThrowIfDisposed<Task>() ?? PluginManager.CommitTransaction(this);
  221. /// <summary>
  222. /// Clones this transaction to be identical, but with unrelated underlying sets.
  223. /// </summary>
  224. /// <returns>the new <see cref="StateTransitionTransaction"/></returns>
  225. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  226. public StateTransitionTransaction Clone()
  227. {
  228. ThrowIfDisposed();
  229. var copy = new StateTransitionTransaction(CurrentlyEnabled, CurrentlyDisabled);
  230. foreach (var toEnable in ToEnable)
  231. copy.toEnable.Add(toEnable);
  232. foreach (var toDisable in ToDisable)
  233. copy.toDisable.Add(toDisable);
  234. copy.stateChanged = stateChanged;
  235. return copy;
  236. }
  237. private void ThrowIfDisposed() => ThrowIfDisposed<byte>();
  238. private T ThrowIfDisposed<T>()
  239. {
  240. if (disposed)
  241. throw new ObjectDisposedException(nameof(StateTransitionTransaction));
  242. return default;
  243. }
  244. private bool disposed = false;
  245. /// <summary>
  246. /// Disposes and discards this transaction without committing it.
  247. /// </summary>
  248. public void Dispose()
  249. => disposed = true;
  250. }
  251. }