// zlib/libpng License
//
// Copyright (c) 2018 Sinoa
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
// If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
// 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
// 3. This notice may not be removed or altered from any source distribution.
using System;
using System.Collections.Generic;
using System.Runtime.ExceptionServices;
using System.Threading;
namespace IceMilkTea.Core
{
///
/// ステートマシンの更新処理中に発生した、未処理の例外をどう振る舞うかを表現した列挙型です
///
public enum ImtStateMachineUnhandledExceptionMode
{
///
/// Update関数内で発生した例外をそのまま例外として発生させます。
///
ThrowException,
///
/// OnUnhandledException ハンドラに転送されます。
///
CatchException,
///
/// 現在動作中ステートの Error() に例外が転送されます。
/// ただし、現在動作中ステートが存在しない場合は ThrowException と同等の振る舞いになります。
///
CatchStateException,
}
#region 標準ステートマシン基底実装
///
/// コンテキストを持つことのできるステートマシンクラスです
///
/// このステートマシンが持つコンテキストの型
/// ステートマシンへ送信するイベントの型
public class ImtStateMachine
{
#region ステートクラス本体と特別ステートクラスの定義
///
/// ステートマシンが処理する状態を表現するステートクラスです。
///
public abstract class State
{
// メンバ変数定義
internal Dictionary transitionTable;
internal ImtStateMachine stateMachine;
///
/// このステートが所属するステートマシン
///
protected ImtStateMachine StateMachine => stateMachine;
///
/// このステートが所属するステートマシンが持っているコンテキスト
///
protected TContext Context => stateMachine.Context;
///
/// ステートに突入したときの処理を行います
///
protected internal virtual void Enter()
{
}
///
/// ステートを更新するときの処理を行います
///
protected internal virtual void Update()
{
}
///
/// ステートから脱出したときの処理を行います
///
protected internal virtual void Exit()
{
}
///
/// ステートマシンの未処理例外が発生した時の処理を行います。
/// ただし UnhandledExceptionMode が CatchStateException である必要があります。
///
///
/// もし、この関数が false を返した場合は、例外が結局未処理状態と判断されステートマシンの
/// Update() 関数が例外を送出することになります。
///
/// 発生した未処理の例外
/// 例外を処理した場合は true を、未処理の場合は false を返します
protected internal virtual bool Error(Exception exception)
{
// 通常は未処理として返す
return false;
}
///
/// ステートマシンがイベントを受ける時に、このステートがそのイベントをガードします
///
/// 渡されたイベントID
/// イベントの受付をガードする場合は true を、ガードせずイベントを受け付ける場合は false を返します
protected internal virtual bool GuardEvent(TEvent eventId)
{
// 通常はガードしない
return false;
}
///
/// ステートマシンがスタックしたステートをポップする前に、このステートがそのポップをガードします
///
/// ポップの動作をガードする場合は true を、ガードせずにポップ動作を続ける場合は false を返します
protected internal virtual bool GuardPop()
{
// 通常はガードしない
return false;
}
}
///
/// ステートマシンで "任意" を表現する特別なステートクラスです
///
public sealed class AnyState : State { }
#endregion
#region 列挙型定義
///
/// ステートマシンのUpdate状態を表現します
///
private enum UpdateState
{
///
/// アイドリング中です。つまり何もしていません
///
Idle,
///
/// ステートの突入処理中です
///
Enter,
///
/// ステートの更新処理中です
///
Update,
///
/// ステートの脱出処理中です
///
Exit,
}
#endregion
// メンバ変数定義
private UpdateState updateState;
private List stateList;
private State currentState;
private State nextState;
private Stack stateStack;
private HashSet> stateFactorySet;
///
/// ステートマシンの Update() 中に未処理の例外が発生した時のイベントハンドラです。
/// ただし UnhandledExceptionMode プロパティに CatchException が設定されている必要があります。
/// false が返されると、例外が未処理と判断され Update() 関数が例外を送出します。
///
public event Func UnhandledException;
///
/// ステートマシンが保持しているコンテキスト
///
public TContext Context { get; private set; }
///
/// ステートマシンが起動しているかどうか
///
public bool Running => currentState != null;
///
/// ステートマシンが、更新処理中かどうか。
/// Update 関数から抜けたと思っても、このプロパティが true を示す場合、
/// Update 中に例外などで不正な終了の仕方をしている場合が考えられます。
///
public bool Updating => (Running && updateState != UpdateState.Idle);
///
/// 現在のスタックしているステートの数
///
public int StackCount => stateStack.Count;
///
/// 現在のステートの名前を取得します。
/// まだステートマシンが起動していない場合は空文字列になります。
///
public string CurrentStateName => (Running ? currentState.GetType().Name : string.Empty);
///
/// SendEvent() 関数によって一度、遷移状態になった後に再び SendEvent() による遷移し直しを許可するかどうか
///
public bool AllowRetransition { get; set; }
///
/// 未処理の例外が発生した際の振る舞いの設定取得をします
///
public ImtStateMachineUnhandledExceptionMode UnhandledExceptionMode { get; set; }
///
/// このステートマシンを最後にUpdateしたスレッドID
///
public int LastUpdateThreadId { get; private set; }
///
/// このステートマシンが最後に受け付けたイベントID
///
public TEvent LastAcceptedEventID { get; private set; }
///
/// ImtStateMachine のインスタンスを初期化します
///
/// このステートマシンが持つコンテキスト
/// context が null です
/// ステートクラスのインスタンスの生成に失敗しました
public ImtStateMachine(TContext context)
{
// 渡されたコンテキストがnullなら
if (context == null)
{
// nullは許されない
throw new ArgumentNullException(nameof(context));
}
// メンバの初期化をする
Context = context;
stateList = new List();
stateStack = new Stack();
updateState = UpdateState.Idle;
AllowRetransition = false;
UnhandledExceptionMode = ImtStateMachineUnhandledExceptionMode.ThrowException;
stateFactorySet = new HashSet>();
}
#region 汎用ロジック系
///
/// 型からステートインスタンスを生成するファクトリ関数を登録します
///
/// 登録するファクトリ関数
/// stateFactory が null です
public void RegisterStateFactory(Func stateFactory)
{
// ハッシュセットに登録する
stateFactorySet.Add(stateFactory ?? throw new ArgumentNullException(nameof(stateFactory)));
}
///
/// 登録したファクトリ関数の解除をします
///
/// 解除するファクトリ関数
/// stateFactory が null です
public void UnregisterStateFactory(Func stateFactory)
{
// ハッシュセットから登録を解除する
stateFactorySet.Remove(stateFactory ?? throw new ArgumentNullException(nameof(stateFactory)));
}
#endregion
#region ステート遷移テーブル構築系
///
/// ステートの任意遷移構造を追加します。
///
///
/// この関数は、遷移元が任意の状態からの遷移を希望する場合に利用してください。
/// 任意の遷移は、通常の遷移(Any以外の遷移元)より優先度が低いことにも、注意をしてください。
/// また、ステートの遷移テーブル設定はステートマシンが起動する前に完了しなければなりません。
///
/// 任意状態から遷移する先になるステートの型
/// 遷移する条件となるイベントID
/// 既に同じ eventId が設定された遷移先ステートが存在します
/// ステートマシンは、既に起動中です
public void AddAnyTransition(TEvent eventId) where TNextState : State, new()
{
// 単純に遷移元がAnyStateなだけの単純な遷移追加関数を呼ぶ
AddTransition(eventId);
}
///
/// ステートの遷移構造を追加します。
/// また、ステートの遷移テーブル設定はステートマシンが起動する前に完了しなければなりません。
///
/// 遷移する元になるステートの型
/// 遷移する先になるステートの型
/// 遷移する条件となるイベントID
/// 既に同じ eventId が設定された遷移先ステートが存在します
/// ステートマシンは、既に起動中です
/// ステートクラスのインスタンスの生成に失敗しました
public void AddTransition(TEvent eventId) where TPrevState : State, new() where TNextState : State, new()
{
// ステートマシンが起動してしまっている場合は
if (Running)
{
// もう設定できないので例外を吐く
throw new InvalidOperationException("ステートマシンは、既に起動中です");
}
// 遷移元と遷移先のステートインスタンスを取得
var prevState = GetOrCreateState();
var nextState = GetOrCreateState();
// 遷移元ステートの遷移テーブルに既に同じイベントIDが存在していたら
if (prevState.transitionTable.ContainsKey(eventId))
{
// 上書き登録を許さないので例外を吐く
throw new ArgumentException($"ステート'{prevState.GetType().Name}'には、既にイベントID'{eventId}'の遷移が設定済みです");
}
// 遷移テーブルに遷移を設定する
prevState.transitionTable[eventId] = nextState;
}
///
/// ステートマシンが起動する時に、最初に開始するステートを設定します。
///
/// ステートマシンが起動時に開始するステートの型
/// ステートマシンは、既に起動中です
/// ステートクラスのインスタンスの生成に失敗しました
public void SetStartState() where TStartState : State, new()
{
// 既にステートマシンが起動してしまっている場合は
if (Running)
{
// 起動してしまったらこの関数の操作は許されない
throw new InvalidOperationException("ステートマシンは、既に起動中です");
}
// 次に処理するステートの設定をする
nextState = GetOrCreateState();
}
#endregion
#region ステートスタック操作系
///
/// 現在実行中のステートを、ステートスタックにプッシュします
///
/// ステートマシンは、まだ起動していません
public void PushState()
{
// そもそもまだ現在実行中のステートが存在していないなら例外を投げる
IfNotRunningThrowException();
// 現在のステートをスタックに積む
stateStack.Push(currentState);
}
///
/// ステートスタックに積まれているステートを取り出し、遷移の準備を行います。
///
///
/// この関数の挙動は、イベントIDを送ることのない点を除けば SendEvent 関数と非常に似ています。
/// 既に SendEvent によって次の遷移の準備ができている場合は、スタックからステートはポップされることはありません。
///
/// スタックからステートがポップされ次の遷移の準備が完了した場合は true を、ポップするステートがなかったり、ステートによりポップがガードされた場合は false を返します
/// ステートマシンは、まだ起動していません
public virtual bool PopState()
{
// そもそもまだ現在実行中のステートが存在していないなら例外を投げる
IfNotRunningThrowException();
// そもそもスタックが空であるか、次に遷移するステートが存在 かつ 再遷移が未許可か、ポップする前に現在のステートにガードされたのなら
if (stateStack.Count == 0 || (nextState != null && !AllowRetransition) || currentState.GuardPop())
{
// ポップ自体出来ないのでfalseを返す
return false;
}
// ステートをスタックから取り出して次のステートへ遷移するようにして成功を返す
nextState = stateStack.Pop();
return true;
}
///
/// ステートスタックに積まれているステートを取り出し、現在のステートとして直ちに直接設定します。
///
///
/// この関数の挙動は PopState() 関数と違い、ポップされたステートがそのまま現在処理中のステートとして直ちに設定するため、
/// 状態の遷移処理は行われず、ポップされたステートの Enter() は呼び出されずそのまま次回から Update() が呼び出されるようになります。
///
/// スタックからステートがポップされ、現在のステートとして設定出来た場合は true を、ポップするステートが無いか、ポップがガードされた場合は false を返します
/// ステートマシンは、まだ起動していません
public virtual bool PopAndDirectSetState()
{
// そもそもまだ現在実行中のステートが存在していないなら例外を投げる
IfNotRunningThrowException();
// そもそもスタックが空であるか、ポップする前に現在のステートにガードされたのなら
if (stateStack.Count == 0 || currentState.GuardPop())
{
// ポップ自体出来ないのでfalseを返す
return false;
}
// ステートをスタックから取り出して現在のステートとして設定して成功を返す
currentState = stateStack.Pop();
return true;
}
///
/// ステートスタックに積まれているステートを一つ取り出し、そのまま捨てます。
///
///
/// ステートスタックの一番上に積まれているステートをそのまま捨てたい時に利用します。
///
public void PopAndDropState()
{
// スタックが空なら
if (stateStack.Count == 0)
{
// 何もせず終了
return;
}
// スタックからステートを取り出して何もせずそのまま捨てる
stateStack.Pop();
}
///
/// ステートスタックに積まれているすべてのステートを捨てます。
///
public void ClearStack()
{
// スタックを空にする
stateStack.Clear();
}
#endregion
#region ステートマシン制御系
///
/// 現在実行中のステートが、指定されたステートかどうかを調べます。
///
/// 確認するステートの型
/// 指定されたステートの状態であれば true を、異なる場合は false を返します
/// ステートマシンは、まだ起動していません
public bool IsCurrentState() where TState : State
{
// そもそもまだ現在実行中のステートが存在していないなら例外を投げる
IfNotRunningThrowException();
// 現在のステートと型が一致するかの条件式の結果をそのまま返す
return currentState.GetType() == typeof(TState);
}
///
/// ステートマシンにイベントを送信して、ステート遷移の準備を行います。
///
///
/// ステートの遷移は直ちに行われず、次の Update が実行された時に遷移処理が行われます。
/// また、この関数によるイベント受付優先順位は、一番最初に遷移を受け入れたイベントのみであり Update によって遷移されるまで、後続のイベントはすべて失敗します。
/// ただし AllowRetransition プロパティに true が設定されている場合は、再遷移が許されます。
/// さらに、イベントはステートの Enter または Update 処理中でも受け付けることが可能で、ステートマシンの Update 中に
/// 何度も遷移をすることが可能ですが Exit 中でイベントを送ると、遷移中になるため例外が送出されます。
///
/// ステートマシンに送信するイベントID
/// ステートマシンが送信されたイベントを受け付けた場合は true を、イベントを拒否または、イベントの受付ができない場合は false を返します
/// ステートマシンは、まだ起動していません
/// ステートが Exit 処理中のためイベントを受け付けることが出来ません
public virtual bool SendEvent(TEvent eventId)
{
// そもそもまだ現在実行中のステートが存在していないなら例外を投げる
IfNotRunningThrowException();
// もし Exit 処理中なら
if (updateState == UpdateState.Exit)
{
// Exit 中の SendEvent は許されない
throw new InvalidOperationException("ステートが Exit 処理中のためイベントを受け付けることが出来ません");
}
// 既に遷移準備をしていて かつ 再遷移が許可されていないなら
if (nextState != null && !AllowRetransition)
{
// イベントの受付が出来なかったことを返す
return false;
}
// 現在のステートにイベントガードを呼び出して、ガードされたら
if (currentState.GuardEvent(eventId))
{
// ガードされて失敗したことを返す
return false;
}
// 次に遷移するステートを現在のステートから取り出すが見つけられなかったら
if (!currentState.transitionTable.TryGetValue(eventId, out nextState))
{
// 任意ステートからすらも遷移が出来なかったのなら
if (!GetOrCreateState().transitionTable.TryGetValue(eventId, out nextState))
{
// イベントの受付が出来なかった
return false;
}
}
// 最後に受け付けたイベントIDを覚えてイベントの受付をした事を返す
LastAcceptedEventID = eventId;
return true;
}
///
/// ステートマシンの状態を更新します。
///
///
/// ステートマシンの現在処理しているステートの更新を行いますが、まだ未起動の場合は SetStartState 関数によって設定されたステートが起動します。
/// また、ステートマシンが初回起動時の場合、ステートのUpdateは呼び出されず、次の更新処理が実行される時になります。
///
/// 現在のステートマシンは、別のスレッドによって更新処理を実行しています。[UpdaterThread={LastUpdateThreadId}, CurrentThread={currentThreadId}]
/// 現在のステートマシンは、既に更新処理を実行しています
/// 開始ステートが設定されていないため、ステートマシンの起動が出来ません
public virtual void Update()
{
// もしステートマシンの更新状態がアイドリング以外だったら
if (updateState != UpdateState.Idle)
{
// もし別スレッドからのUpdateによる多重Updateなら
int currentThreadId = Thread.CurrentThread.ManagedThreadId;
if (LastUpdateThreadId != currentThreadId)
{
// 別スレッドからの多重Updateであることを例外で吐く
throw new InvalidOperationException($"現在のステートマシンは、別のスレッドによって更新処理を実行しています。[UpdaterThread={LastUpdateThreadId}, CurrentThread={currentThreadId}]");
}
// 多重でUpdateが呼び出せない例外を吐く
throw new InvalidOperationException("現在のステートマシンは、既に更新処理を実行しています");
}
// Updateの起動スレッドIDを覚える
LastUpdateThreadId = Thread.CurrentThread.ManagedThreadId;
// まだ未起動なら
if (!Running)
{
// 次に処理するべきステート(つまり起動開始ステート)が未設定なら
if (nextState == null)
{
// 起動が出来ない例外を吐く
throw new InvalidOperationException("開始ステートが設定されていないため、ステートマシンの起動が出来ません");
}
// 現在処理中ステートとして設定する
currentState = nextState;
nextState = null;
try
{
// Enter処理中であることを設定してEnterを呼ぶ
updateState = UpdateState.Enter;
currentState.Enter();
}
catch (Exception exception)
{
// 起動時の復帰は現在のステートにnullが入っていないとまずいので遷移前の状態に戻す
nextState = currentState;
currentState = null;
// 更新状態をアイドリングにして、例外発生時のエラーハンドリングを行い終了する
updateState = UpdateState.Idle;
DoHandleException(exception);
return;
}
// 次に遷移するステートが無いなら
if (nextState == null)
{
// 起動処理は終わったので一旦終わる
updateState = UpdateState.Idle;
return;
}
}
try
{
// 次に遷移するステートが存在していないなら
if (nextState == null)
{
// Update処理中であることを設定してUpdateを呼ぶ
updateState = UpdateState.Update;
currentState.Update();
}
// 次に遷移するステートが存在している間ループ
while (nextState != null)
{
// Exit処理中であることを設定してExit処理を呼ぶ
updateState = UpdateState.Exit;
currentState.Exit();
// 次のステートに切り替える
currentState = nextState;
nextState = null;
// Enter処理中であることを設定してEnterを呼ぶ
updateState = UpdateState.Enter;
currentState.Enter();
}
// 更新処理が終わったらアイドリングに戻る
updateState = UpdateState.Idle;
}
catch (Exception exception)
{
// 更新状態をアイドリングにして、例外発生時のエラーハンドリングを行い終了する
updateState = UpdateState.Idle;
DoHandleException(exception);
return;
}
}
#endregion
#region 内部ロジック系
///
/// 発生した未処理の例外をハンドリングします
///
/// 発生した未処理の例外
/// exception が null です
private void DoHandleException(Exception exception)
{
// nullを渡されたら
if (exception == null)
{
// 何をハンドリングすればよいのか
throw new ArgumentNullException(nameof(exception));
}
// もし、例外を拾うモード かつ ハンドラが設定されているなら
if (UnhandledExceptionMode == ImtStateMachineUnhandledExceptionMode.CatchException && UnhandledException != null)
{
// イベントを呼び出して、正しくハンドリングされたのなら
if (UnhandledException(exception))
{
// そのまま終了
return;
}
}
// もし、例外を拾ってステートに任せるモード かつ 現在の実行ステートが設定されているのなら
if (UnhandledExceptionMode == ImtStateMachineUnhandledExceptionMode.CatchStateException && currentState != null)
{
// ステートに例外を投げて、正しくハンドリングされたのなら
if (currentState.Error(exception))
{
// そのまま終了
return;
}
}
// 上記のモード以外(つまり ThrowException)か、例外がハンドリングされなかった(false を返された)のなら例外をキャプチャして発生させる
ExceptionDispatchInfo.Capture(exception).Throw();
}
///
/// ステートマシンが未起動の場合に例外を送出します
///
/// ステートマシンは、まだ起動していません
protected void IfNotRunningThrowException()
{
// そもそもまだ現在実行中のステートが存在していないなら
if (!Running)
{
// まだ起動すらしていないので例外を吐く
throw new InvalidOperationException("ステートマシンは、まだ起動していません");
}
}
///
/// 指定されたステートの型のインスタンスを取得しますが、存在しない場合は生成してから取得します。
/// 生成されたインスタンスは、次回から取得されるようになります。
///
/// 取得、または生成するステートの型
/// 取得、または生成されたステートのインスタンスを返します
/// ステートクラスのインスタンスの生成に失敗しました
private TState GetOrCreateState() where TState : State, new()
{
// ステートの数分回る
var stateType = typeof(TState);
foreach (var state in stateList)
{
// もし該当のステートの型と一致するインスタンスなら
if (state.GetType() == stateType)
{
// そのインスタンスを返す
return (TState)state;
}
}
// ループから抜けたのなら、型一致するインスタンスが無いという事なのでインスタンスを生成してキャッシュする
var newState = CreateStateInstanceCore() ?? throw new InvalidOperationException("ステートクラスのインスタンスの生成に失敗しました");
stateList.Add(newState);
// 新しいステートに、自身の参照と遷移テーブルのインスタンスの初期化も行って返す
newState.stateMachine = this;
newState.transitionTable = new Dictionary();
return newState;
}
///
/// 指定されたステートの型のインスタンスを生成します。
///
/// 生成するべきステータスの型
/// 生成したインスタンスを返します
private TState CreateStateInstanceCore() where TState : State, new()
{
// 結果を受け取る変数を宣言
TState result;
// 登録されているファクトリ関数分回る
var stateType = typeof(TState);
foreach (var factory in stateFactorySet)
{
// 生成を試みてインスタンスが生成されたのなら
result = (TState)factory(stateType);
if (result != null)
{
// このインスタンスを返す
return result;
}
}
// ファクトリ関数でも駄目なら実装側生成関数に頼る
return CreateStateInstance();
}
///
/// 指定されたステートの型のインスタンスを生成します。
///
/// 生成するべきステータスの型
/// 生成したインスタンスを返します
protected virtual TState CreateStateInstance() where TState : State, new()
{
// 既定動作はジェネリックのnewをするのみで返す
return new TState();
}
#endregion
}
#endregion
#region 旧intイベント型ベースのステートマシン実装
///
/// コンテキストを持つことのできるステートマシンクラスです
///
/// このステートマシンが持つコンテキストの型
public class ImtStateMachine : ImtStateMachine
{
///
/// ImtStateMachine のインスタンスを初期化します
///
/// このステートマシンが持つコンテキスト
/// context が null です
public ImtStateMachine(TContext context) : base(context)
{
}
}
#endregion
}