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.

221 lines
12 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> ToEnable => toEnable;
  31. internal IEnumerable<PluginMetadata> ToDisable => toDisable;
  32. /// <summary>
  33. /// Gets a list of plugins that are enabled according to this transaction's current state.
  34. /// </summary>
  35. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  36. public IEnumerable<PluginMetadata> EnabledPlugins
  37. => ThrowIfDisposed<IEnumerable<PluginMetadata>>() ?? DisabledPluginsInternal;
  38. private IEnumerable<PluginMetadata> EnabledPluginsInternal => currentlyEnabled.Except(toDisable).Concat(toEnable);
  39. /// <summary>
  40. /// Gets a list of plugins that are disabled according to this transaction's current state.
  41. /// </summary>
  42. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  43. public IEnumerable<PluginMetadata> DisabledPlugins
  44. => ThrowIfDisposed<IEnumerable<PluginMetadata>>() ?? DisabledPluginsInternal;
  45. private IEnumerable<PluginMetadata> DisabledPluginsInternal => currentlyDisabled.Except(toEnable).Concat(toDisable);
  46. /// <summary>
  47. /// Checks if a plugin is enabled according to this transaction's current state.
  48. /// </summary>
  49. /// <remarks>
  50. /// <para>This should be roughly equivalent to <c>EnabledPlugins.Contains(meta)</c>, but more performant.</para>
  51. /// <para>This should also always return the inverse of <see cref="IsDisabled(PluginMetadata)"/> for valid plugins.</para>
  52. /// </remarks>
  53. /// <param name="meta">the plugin to check</param>
  54. /// <returns><see langword="true"/> if the plugin is enabled, <see langword="false"/> otherwise</returns>
  55. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  56. /// <seealso cref="EnabledPlugins"/>
  57. /// <seealso cref="IsDisabled(PluginMetadata)"/>
  58. public bool IsEnabled(PluginMetadata meta)
  59. => ThrowIfDisposed<bool>() || IsEnabledInternal(meta);
  60. private bool IsEnabledInternal(PluginMetadata meta)
  61. => (currentlyEnabled.Contains(meta) && !toDisable.Contains(meta))
  62. || toEnable.Contains(meta);
  63. /// <summary>
  64. /// Checks if a plugin is disabled according to this transaction's current state.
  65. /// </summary>
  66. /// <remarks>
  67. /// <para>This should be roughly equivalent to <c>DisabledPlugins.Contains(meta)</c>, but more performant.</para>
  68. /// <para>This should also always return the inverse of <see cref="IsEnabled(PluginMetadata)"/> for valid plugins.</para>
  69. /// </remarks>
  70. /// <param name="meta">the plugin to check</param>
  71. /// <returns><see langword="true"/> if the plugin is disabled, <see langword="false"/> otherwise</returns>
  72. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  73. /// <seealso cref="DisabledPlugins"/>
  74. /// <seealso cref="IsEnabled(PluginMetadata)"/>
  75. public bool IsDisabled(PluginMetadata meta)
  76. => ThrowIfDisposed<bool>() || IsDisabledInternal(meta);
  77. private bool IsDisabledInternal(PluginMetadata meta)
  78. => (currentlyDisabled.Contains(meta) && !toEnable.Contains(meta))
  79. || toDisable.Contains(meta);
  80. /// <summary>
  81. /// Enables a plugin in this transaction.
  82. /// </summary>
  83. /// <param name="meta">the plugin to enable</param>
  84. /// <param name="autoDeps">whether or not to automatically enable all dependencies of the plugin</param>
  85. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  86. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  87. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  88. /// <seealso cref="Enable(PluginMetadata, out IEnumerable{PluginMetadata}, bool)"/>
  89. public bool Enable(PluginMetadata meta, bool autoDeps = true)
  90. => Enable(meta, out var _, autoDeps);
  91. /// <summary>
  92. /// Enables a plugin in this transaction.
  93. /// </summary>
  94. /// <remarks>
  95. /// <paramref name="disabledDeps"/> will only be set when <paramref name="autoDeps"/> is <see langword="false"/>.
  96. /// </remarks>
  97. /// <param name="meta">the plugin to enable</param>
  98. /// <param name="disabledDeps"><see langword="null"/> if successful, otherwise a set of plugins that need to be enabled first</param>
  99. /// <param name="autoDeps">whether or not to automatically enable all dependencies</param>
  100. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  101. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  102. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  103. public bool Enable(PluginMetadata meta, out IEnumerable<PluginMetadata> disabledDeps, bool autoDeps = false)
  104. { // returns whether or not state was changed
  105. ThrowIfDisposed();
  106. if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
  107. throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin");
  108. disabledDeps = null;
  109. if (IsEnabledInternal(meta)) return false;
  110. var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m));
  111. if (autoDeps)
  112. {
  113. foreach (var dep in needsEnabled)
  114. {
  115. var res = Disable(dep, out var failedDisabled, true);
  116. if (failedDisabled == null) continue;
  117. disabledDeps = failedDisabled;
  118. return res;
  119. }
  120. }
  121. else if (needsEnabled.Any())
  122. {
  123. // there are currently enabled plugins that depend on this
  124. disabledDeps = needsEnabled;
  125. return false;
  126. }
  127. toDisable.Remove(meta);
  128. toEnable.Add(meta);
  129. return true;
  130. }
  131. /// <summary>
  132. /// Disables a plugin in this transaction.
  133. /// </summary>
  134. /// <param name="meta">the plugin to disable</param>
  135. /// <param name="autoDependents">whether or not to automatically disable all dependents of the plugin</param>
  136. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  137. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  138. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  139. /// <seealso cref="Disable(PluginMetadata, out IEnumerable{PluginMetadata}, bool)"/>
  140. public bool Disable(PluginMetadata meta, bool autoDependents = true)
  141. => Disable(meta, out var _, autoDependents);
  142. /// <summary>
  143. /// Disables a plugin in this transaction.
  144. /// </summary>
  145. /// <remarks>
  146. /// <paramref name="enabledDependents"/> will only be set when <paramref name="autoDependents"/> is <see langword="false"/>.
  147. /// </remarks>
  148. /// <param name="meta">the plugin to disable</param>
  149. /// <param name="enabledDependents"><see langword="null"/> if successful, otherwise a set of plugins that need to be disabled first</param>
  150. /// <param name="autoDependents">whether or not to automatically disable all dependents of the plugin</param>
  151. /// <returns><see langword="true"/> if the transaction's state was changed, <see langword="false"/> otherwise</returns>
  152. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  153. /// <exception cref="ArgumentException">if <paramref name="meta"/> is not loadable</exception>
  154. public bool Disable(PluginMetadata meta, out IEnumerable<PluginMetadata> enabledDependents, bool autoDependents = false)
  155. { // returns whether or not state was changed
  156. ThrowIfDisposed();
  157. if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
  158. throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin");
  159. enabledDependents = null;
  160. if (IsDisabledInternal(meta)) return false;
  161. var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta));
  162. if (autoDependents)
  163. {
  164. foreach (var dep in needsDisabled)
  165. {
  166. var res = Disable(dep, out var failedEnabled, true);
  167. if (failedEnabled == null) continue;
  168. enabledDependents = failedEnabled;
  169. return res;
  170. }
  171. }
  172. else if (needsDisabled.Any())
  173. {
  174. // there are currently enabled plugins that depend on this
  175. enabledDependents = needsDisabled;
  176. return false;
  177. }
  178. toDisable.Add(meta);
  179. toEnable.Remove(meta);
  180. return true;
  181. }
  182. /// <summary>
  183. /// Commits this transaction to actual state, enabling and disabling plugins as necessary.
  184. /// </summary>
  185. /// <returns>a <see cref="Task"/> which completes whenever all disables complete</returns>
  186. /// <exception cref="ObjectDisposedException">if this object has been disposed</exception>
  187. public Task Commit() => ThrowIfDisposed<Task>() ?? PluginManager.CommitTransaction(this);
  188. private void ThrowIfDisposed() => ThrowIfDisposed<byte>();
  189. private T ThrowIfDisposed<T>()
  190. {
  191. if (disposed)
  192. throw new ObjectDisposedException(nameof(StateTransitionTransaction));
  193. return default;
  194. }
  195. private bool disposed = false;
  196. /// <summary>
  197. /// Disposes and discards this transaction without committing it.
  198. /// </summary>
  199. public void Dispose()
  200. => disposed = true;
  201. }
  202. }