// // 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; } } }