#nullable enable using System; using System.Runtime.CompilerServices; /// /// Base context for an FSM. /// MUST inherit from imm.Recorded or Timed in your concrete class. /// /// The concrete Context type. public abstract record class FsmContextBase : imm.Recorded where TSelf : FsmContextBase { // Required for 'with' expressions. protected FsmContextBase(imm.Recorded original) : base(original) { } protected FsmContextBase() { } } /// /// Base state for an FSM. /// MUST inherit from imm.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 : imm.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(imm.Recorded original) : base(original) { } protected FsmStateBase() { } } /// /// An immutable FSM base class. /// MUST inherit from imm.Recorded or Timed in your concrete class. /// /// The concrete FSM type. /// The concrete State type. /// The concrete Context type. public abstract record class FsmBase : imm.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(imm.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 imm.Recorded 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 'imm.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 ); } }