#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
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 ();
private readonly HashSet toDisable = new ();
private bool stateChanged;
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()
|| (stateChanged && toEnable.Concat(toDisable).Any(m => m.RuntimeOptions != RuntimeOptions.DynamicInit));
///
/// Gets whether or not the current state has changed.
///
/// if the current state of the transaction is different from its construction, otherwise
/// if this object has been disposed
public bool HasStateChanged => ThrowIfDisposed() || stateChanged;
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>() ?? EnabledPluginsInternal;
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 (meta is null) throw new ArgumentNullException(nameof(meta));
if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
throw new ArgumentException("Plugin metadata does not represent a loadable plugin", nameof(meta));
disabledDeps = null;
if (IsEnabledInternal(meta)) return false;
var needsEnabled = meta.Dependencies.Where(m => DisabledPluginsInternal.Contains(m)).ToArray();
if (autoDeps)
{
foreach (var dep in needsEnabled)
{
var res = Enable(dep, out var failedDisabled, true);
if (failedDisabled == null) continue;
disabledDeps = failedDisabled;
return res;
}
}
else if (needsEnabled.Length > 0)
{
// there are currently enabled plugins that depend on this
disabledDeps = needsEnabled;
return false;
}
_ = toDisable.Remove(meta);
_ = toEnable.Add(meta);
stateChanged = true;
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 (meta is null) throw new ArgumentNullException(nameof(meta));
if (!currentlyEnabled.Contains(meta) && !currentlyDisabled.Contains(meta))
throw new ArgumentException("Plugin metadata does not represent a loadable plugin", nameof(meta));
enabledDependents = null;
if (IsDisabledInternal(meta)) return false;
var needsDisabled = EnabledPluginsInternal.Where(m => m.Dependencies.Contains(meta)).ToArray();
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.Length > 0)
{
// there are currently enabled plugins that depend on this
enabledDependents = needsDisabled;
return false;
}
_ = toDisable.Add(meta);
_ = toEnable.Remove(meta);
stateChanged = true;
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 .
///
///
/// 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.
/// Otherwise, the task returned represents both, and will not complete until Unity has done (possibly) several updates, depending on
/// the number of plugins being disabled, and the time they take.
///
///
/// 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);
///
/// Clones this transaction to be identical, but with unrelated underlying sets.
///
/// the new
/// if this object has been disposed
public StateTransitionTransaction Clone()
{
ThrowIfDisposed();
var copy = new StateTransitionTransaction(CurrentlyEnabled, CurrentlyDisabled);
foreach (var toEnable in ToEnable)
_ = copy.toEnable.Add(toEnable);
foreach (var toDisable in ToDisable)
_ = copy.toDisable.Add(toDisable);
copy.stateChanged = stateChanged;
return copy;
}
private void ThrowIfDisposed() => ThrowIfDisposed();
private T? ThrowIfDisposed()
{
return disposed ? throw new ObjectDisposedException(nameof(StateTransitionTransaction)) : default;
}
private bool disposed;
///
/// Disposes and discards this transaction without committing it.
///
public void Dispose()
=> disposed = true;
}
}