//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.GameLogic;
using System.ComponentModel;
using MUnique.OpenMU.PlugIns;
using Nito.AsyncEx;
///
/// A state machine.
///
public class StateMachine
{
///
/// The lock object for state transitions.
///
private readonly AsyncLock _asyncLock = new();
///
/// A cancel event args object, which is getting reused.
///
private readonly StateChangeEventArgs _cachedCancelEventArgs = new();
///
/// Initializes a new instance of the class.
///
/// The initial state.
public StateMachine(State initialState)
{
this.CurrentState = initialState;
}
///
/// Event that fires just before the state changes.
///
public event AsyncEventHandler? StateChanges;
///
/// Event that fires after the state have changed.
///
public event AsyncEventHandler? StateChanged;
///
/// Gets the current state.
///
public State CurrentState { get; private set; }
///
/// Gets a value indicating whether the state machine is in a finished state, that means that no further state changes are possible.
///
public bool Finished => this.CurrentState?.PossibleTransitions is null || this.CurrentState.PossibleTransitions.Count == 0;
///
/// Tries to advance the state to .
///
/// The state to advance to.
/// The success.
public async ValueTask TryAdvanceToAsync(State nextState)
{
if (this.CurrentState?.PossibleTransitions is null)
{
return false;
}
using var l = await this._asyncLock.LockAsync();
if (this.CurrentState?.PossibleTransitions is not { } possibleTransitions)
{
return false;
}
if (possibleTransitions.Contains(nextState) && await this.OnStateChangingAsync(nextState).ConfigureAwait(false))
{
var previousState = this.CurrentState;
this.CurrentState = nextState;
await this.OnStateChangedAsync(previousState, nextState).ConfigureAwait(false);
return true;
}
return false;
}
///
/// Tries to start a "transaction" to advance the state to .
///
/// The state to advance to.
/// The state change context. On disposal of this object, the state change is getting completed.
public async ValueTask TryBeginAdvanceToAsync(State nextState)
{
var lockRelease = await this._asyncLock.LockAsync().ConfigureAwait(false);
var context = new StateChangeContext(lockRelease, async () =>
{
var previousState = this.CurrentState;
this.CurrentState = nextState;
await this.OnStateChangedAsync(previousState, nextState).ConfigureAwait(false);
})
{
Allowed = (this.CurrentState.PossibleTransitions?.Contains(nextState) ?? false) && await this.OnStateChangingAsync(nextState).ConfigureAwait(false),
};
return context;
}
///
/// Calls the StateChanged-Event.
///
private async ValueTask OnStateChangedAsync(State previousState, State currentState)
{
await this.StateChanged.SafeInvokeAsync(new StateChangedEventArgs(previousState, currentState)).ConfigureAwait(false);
}
///
/// Calls the StateChanges-Event.
///
/// The next state.
/// True, if all event handlers did not set to true; Otherwise, false.
private async ValueTask OnStateChangingAsync(State nextState)
{
if (this.StateChanges != null)
{
this._cachedCancelEventArgs.Cancel = false;
this._cachedCancelEventArgs.NextState = nextState;
await this.StateChanges.SafeInvokeAsync(this._cachedCancelEventArgs).ConfigureAwait(false);
return !this._cachedCancelEventArgs.Cancel;
}
return true;
}
///
/// The state change context for more complex state changes.
/// On disposal of this object, the state change is getting completed.
///
public sealed class StateChangeContext : IAsyncDisposable
{
///
/// The lock release of the acquired lock of the state machine.
///
private readonly IDisposable _lockRelease;
///
/// The action which gets executed when the state change is completed.
///
private readonly Func _finishAction;
///
/// Initializes a new instance of the class.
///
/// The lock object of the state machine, which is in the locked state.
/// The action which should get executed when the state change is completed.
public StateChangeContext(IDisposable lockRelease, Func finishAction)
{
this._lockRelease = lockRelease;
this._finishAction = finishAction;
}
///
/// Gets a value indicating whether a state change is allowed.
///
public bool Allowed { get; internal set; }
///
public async ValueTask DisposeAsync()
{
try
{
if (this.Allowed)
{
await this._finishAction().ConfigureAwait(false);
}
}
finally
{
this._lockRelease.Dispose();
}
}
}
///
/// The state change event args, including the next state.
///
public class StateChangeEventArgs : CancelEventArgs
{
///
/// Gets or sets the next state.
///
public State? NextState { get; set; }
}
///
/// The event args for .
///
public class StateChangedEventArgs : EventArgs
{
///
/// Initializes a new instance of the class.
///
/// State of the previous.
/// State of the current state.
public StateChangedEventArgs(State previousState, State currentStateState)
{
this.PreviousState = previousState;
this.CurrentStateState = currentStateState;
}
///
/// Gets the state of the previous state.
///
public State PreviousState { get; }
///
/// Gets the state of the current state.
///
public State CurrentStateState { get; }
}
}