#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.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 '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
);
}
}