using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace IPA.Loader { /// /// A class to represent a transaction for changing the state of loaded mods. /// public sealed class StateTransitionTransaction : IDisposable { private readonly HashSet currentlyEnabled; private readonly HashSet currentlyDisabled; private readonly HashSet toEnable = new HashSet(); private readonly HashSet toDisable = new HashSet(); internal StateTransitionTransaction(IEnumerable enabled, IEnumerable disabled) { currentlyEnabled = new HashSet(enabled.ToArray()); currentlyDisabled = new HashSet(disabled.ToArray()); } /// /// Gets whether or not a game restart will be necessary to fully apply this transaction. /// /// if any mod who's state is changed cannot be changed at runtime, otherwise /// if this object has been disposed public bool WillNeedRestart => ThrowIfDisposed() || toEnable.Concat(toDisable).Any(m => m.RuntimeOptions != RuntimeOptions.DynamicInit); internal IEnumerable CurrentlyEnabled => currentlyEnabled; internal IEnumerable CurrentlyDisabled => currentlyDisabled; internal IEnumerable ToEnable => toEnable; internal IEnumerable ToDisable => toDisable; /// /// Gets a list of plugins that are enabled according to this transaction's current state. /// /// if this object has been disposed public IEnumerable EnabledPlugins => ThrowIfDisposed>() ?? DisabledPluginsInternal; private IEnumerable EnabledPluginsInternal => currentlyEnabled.Except(toDisable).Concat(toEnable); /// /// Gets a list of plugins that are disabled according to this transaction's current state. /// /// if this object has been disposed public IEnumerable DisabledPlugins => ThrowIfDisposed>() ?? DisabledPluginsInternal; private IEnumerable DisabledPluginsInternal => currentlyDisabled.Except(toEnable).Concat(toDisable); /// /// Checks if a plugin is enabled according to this transaction's current state. /// /// /// This should be roughly equivalent to EnabledPlugins.Contains(meta), but more performant. /// This should also always return the inverse of for valid plugins. /// /// the plugin to check /// if the plugin is enabled, otherwise /// if this object has been disposed /// /// public bool IsEnabled(PluginMetadata meta) => ThrowIfDisposed() || IsEnabledInternal(meta); private bool IsEnabledInternal(PluginMetadata meta) => (currentlyEnabled.Contains(meta) && !toDisable.Contains(meta)) || toEnable.Contains(meta); /// /// Checks if a plugin is disabled according to this transaction's current state. /// /// /// This should be roughly equivalent to DisabledPlugins.Contains(meta), but more performant. /// This should also always return the inverse of for valid plugins. /// /// the plugin to check /// if the plugin is disabled, otherwise /// if this object has been disposed /// /// public bool IsDisabled(PluginMetadata meta) => ThrowIfDisposed() || IsDisabledInternal(meta); private bool IsDisabledInternal(PluginMetadata meta) => (currentlyDisabled.Contains(meta) && !toEnable.Contains(meta)) || toDisable.Contains(meta); /// /// Enables a plugin in this transaction. /// /// the plugin to enable /// whether or not to automatically enable all dependencies of the plugin /// if the transaction's state was changed, otherwise /// if this object has been disposed /// if is not loadable /// public bool Enable(PluginMetadata meta, bool autoDeps = true) => Enable(meta, out var _, autoDeps); /// /// Enables a plugin in this transaction. /// /// /// will only be set when is . /// /// the plugin to enable /// if successful, otherwise a set of plugins that need to be enabled first /// whether or not to automatically enable all dependencies /// if the transaction's state was changed, otherwise /// if this object has been disposed /// if is not loadable public bool Enable(PluginMetadata meta, out IEnumerable disabledDeps, bool autoDeps = false) { // returns whether or not state was changed ThrowIfDisposed(); if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta)) throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin"); disabledDeps = null; if (IsEnabledInternal(meta)) return false; var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m)); if (autoDeps) { foreach (var dep in needsEnabled) { var res = Disable(dep, out var failedDisabled, true); if (failedDisabled == null) continue; disabledDeps = failedDisabled; return res; } } else if (needsEnabled.Any()) { // there are currently enabled plugins that depend on this disabledDeps = needsEnabled; return false; } toDisable.Remove(meta); toEnable.Add(meta); return true; } /// /// Disables a plugin in this transaction. /// /// the plugin to disable /// whether or not to automatically disable all dependents of the plugin /// if the transaction's state was changed, otherwise /// if this object has been disposed /// if is not loadable /// public bool Disable(PluginMetadata meta, bool autoDependents = true) => Disable(meta, out var _, autoDependents); /// /// Disables a plugin in this transaction. /// /// /// will only be set when is . /// /// the plugin to disable /// if successful, otherwise a set of plugins that need to be disabled first /// whether or not to automatically disable all dependents of the plugin /// if the transaction's state was changed, otherwise /// if this object has been disposed /// if is not loadable public bool Disable(PluginMetadata meta, out IEnumerable enabledDependents, bool autoDependents = false) { // returns whether or not state was changed ThrowIfDisposed(); if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta)) throw new ArgumentException(nameof(meta), "Plugin metadata does not represent a loadable plugin"); enabledDependents = null; if (IsDisabledInternal(meta)) return false; var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta)); if (autoDependents) { foreach (var dep in needsDisabled) { var res = Disable(dep, out var failedEnabled, true); if (failedEnabled == null) continue; enabledDependents = failedEnabled; return res; } } else if (needsDisabled.Any()) { // there are currently enabled plugins that depend on this enabledDependents = needsDisabled; return false; } toDisable.Add(meta); toEnable.Remove(meta); return true; } /// /// Commits this transaction to actual state, enabling and disabling plugins as necessary. /// /// /// After this completes, this transaction will be disposed. /// /// The that is returned will error if any of the mods being disabled /// error. It is up to the caller to handle these in a sane way, like logging them. If nothing else, do something like this: /// /// // get your transaction... /// var complete = transaction.Commit(); /// await complete.ContinueWith(t => { /// if (t.IsFaulted) /// Logger.log.Error($"Error disabling plugins: {t.Exception}"); /// }); /// /// If you are running in a coroutine, you can use instead of . /// /// /// a which completes whenever all disables complete /// if this object has been disposed /// if the plugins' state no longer matches this transaction's original state public Task Commit() => ThrowIfDisposed() ?? PluginManager.CommitTransaction(this); private void ThrowIfDisposed() => ThrowIfDisposed(); private T ThrowIfDisposed() { if (disposed) throw new ObjectDisposedException(nameof(StateTransitionTransaction)); return default; } private bool disposed = false; /// /// Disposes and discards this transaction without committing it. /// public void Dispose() => disposed = true; } }