#nullable enable using System; using System.Runtime.CompilerServices; /// /// Base context for an FSM. /// MUST inherit from io.Recorded or Timed in your concrete class. /// /// The concrete Context type. public abstract record class FsmContextBase : io.Recorded where TSelf : FsmContextBase { // Required for 'with' expressions. protected FsmContextBase( io.Recorded original ) : base( original ) { } protected FsmContextBase() { } } /// /// Base state for an FSM. /// MUST inherit from io.Recorded or Timed in your concrete class. /// /// The concrete State type. /// The concrete Context type (must be based on FsmContextBase). public abstract record class FsmStateBase : io.Recorded where TSelf : FsmStateBase where TCtx : FsmContextBase { /// /// Called when entering this state. /// public virtual (TCtx Context, TSelf State) OnEnter( TCtx context, FsmStateBase oldState ) { return (context, (TSelf)this); } /// /// Called when exiting this state. /// public virtual (TCtx Context, TSelf State) OnExit( TCtx context, FsmStateBase newState ) { return (context, (TSelf)this); } // Required for 'with' expressions. protected FsmStateBase( io.Recorded original ) : base( original ) { } protected FsmStateBase() { } } /// /// An immutable FSM base class. /// MUST inherit from io.Recorded or Timed in your concrete class. /// /// The concrete FSM type. /// The concrete State type. /// The concrete Context type. public abstract record class FsmBase : io.Recorded where TSelf : FsmBase where TState : FsmStateBase where TCtx : FsmContextBase { public TCtx Context { get; init; } public TState State { get; init; } protected FsmBase( TCtx initialContext, TState initialState ) { Context = initialContext; State = initialState; } // Required for 'with' expressions. protected FsmBase( io.Recorded original ) : base( original ) { var o = original as FsmBase; Context = o!.Context; State = o!.State; } /// /// Transitions the FSM. It automatically uses the 'Process' /// method appropriate for io.ecorded or Timed, thanks to virtual overrides. /// public TSelf Transition( TState newState, string reason, [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0, [CallerArgumentExpression( "newState" )] string expression = "" ) { Console.WriteLine( $"[FSM] Transition: {State.GetType().Name} -> {newState.GetType().Name}. Reason: {reason}" ); var (ctxAfterExit, stateAfterExit) = State.OnExit( Context, newState ); var (ctxAfterEnter, stateAfterEnter) = newState.OnEnter( ctxAfterExit, stateAfterExit ); // Since 'this' is at least 'io.Recorded', we can call the // detailed 'Process'. If 'this' is actually 'Timed', C#'s // virtual dispatch will call the 'Timed' override automatically. return Process( fsm => (TSelf)fsm with { Context = ctxAfterEnter, State = stateAfterEnter }, $"Transition to {newState.GetType().Name}: {reason}", memberName, filePath, lineNumber, expression ); } }