- Work on LuaCs system state machine.

This commit is contained in:
MapleWheels
2026-01-16 15:06:25 -05:00
committed by Maplewheels
parent 3ddaceb5ac
commit 6362a9c34f
5 changed files with 240 additions and 252 deletions
@@ -73,7 +73,7 @@ namespace Barotrauma
case MainMenuScreen: case MainMenuScreen:
case ModDownloadScreen: case ModDownloadScreen:
case ServerListScreen: case ServerListScreen:
SetRunState(RunState.Configuration); SetRunState(RunState.LoadedNoExec);
break; break;
// running lobby or editor states // running lobby or editor states
case CampaignEndScreen: case CampaignEndScreen:
@@ -17,14 +17,4 @@ partial class LuaCsSetup
SetRunState(RunState.Unloaded); SetRunState(RunState.Unloaded);
SetRunState(RunState.Running); SetRunState(RunState.Running);
} }
private partial bool ShouldRunCs() => IsCsEnabled.Value ||
(GetPackage(new SteamWorkshopId(CsForBarotraumaSteamId.Value), false, false) is { }
&& GameMain.Server.ServerPeer is LidgrenServerPeer);
// ReSharper disable once InconsistentNaming
private static readonly Lazy<bool> isRunningInsideWorkshop = new Lazy<bool>(() =>
Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()!.Location) !=
Directory.GetCurrentDirectory());
public static bool IsRunningInsideWorkshop => isRunningInsideWorkshop.Value;
} }
@@ -1,4 +1,4 @@
/*
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -8,9 +8,10 @@ using MoonSharp.Interpreter.Interop;
namespace Barotrauma namespace Barotrauma
{ {
partial class LuaUserData internal class LuaUserData
{ {
public static Type GetType(string typeName) => LuaCsSetup.GetType(typeName); [Obsolete("Use IPluginManagementService::GetTypesByName()")]
public static Type GetType(string typeName) => throw new NotImplementedException(); //LuaCsSetup.GetType(typeName);
public static IUserDataDescriptor RegisterType(string typeName) public static IUserDataDescriptor RegisterType(string typeName)
{ {
@@ -362,4 +363,4 @@ namespace Barotrauma
} }
} }
} }
*/
@@ -4,8 +4,10 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Configuration; using Barotrauma.LuaCs.Configuration;
using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs.Events; using Barotrauma.LuaCs.Events;
@@ -14,6 +16,7 @@ using Barotrauma.LuaCs.Services.Compatibility;
using Barotrauma.LuaCs.Services.Processing; using Barotrauma.LuaCs.Services.Processing;
using Barotrauma.LuaCs.Services.Safe; using Barotrauma.LuaCs.Services.Safe;
using Barotrauma.Networking; using Barotrauma.Networking;
using Barotrauma.Steam;
using FluentResults; using FluentResults;
using ImpromptuInterface; using ImpromptuInterface;
using Microsoft.Toolkit.Diagnostics; using Microsoft.Toolkit.Diagnostics;
@@ -24,54 +27,99 @@ namespace Barotrauma
internal delegate void LuaCsErrorHandler(Exception ex, LuaCsMessageOrigin origin); internal delegate void LuaCsErrorHandler(Exception ex, LuaCsMessageOrigin origin);
internal delegate void LuaCsExceptionHandler(Exception ex, LuaCsMessageOrigin origin); internal delegate void LuaCsExceptionHandler(Exception ex, LuaCsMessageOrigin origin);
partial class LuaCsSetup : IDisposable, IEventScreenSelected, IEventAllPackageListChanged, IEventEnabledPackageListChanged, IEventReloadAllPackages partial class LuaCsSetup : IDisposable, IEventScreenSelected, IEventAllPackageListChanged, IEventEnabledPackageListChanged,
IEventReloadAllPackages
{ {
public LuaCsSetup() public LuaCsSetup()
{ {
// == startup // == startup
_servicesProvider = new ServicesProvider(); _servicesProvider = new ServicesProvider();
RegisterServices(); RegisterServices(_servicesProvider);
ValidateLuaCsContent(); if (!ValidateLuaCsContent())
{
Logger.LogError($"{nameof(LuaCsSetup)}: ModConfigXml missing. Unable to continue.");
throw new ApplicationException($"{nameof(LuaCsSetup)}: Lua ModConfig.xml is missing. Unable to continue.");
}
SubscribeToLuaCsEvents(); SubscribeToLuaCsEvents();
_runStateMachine = SetupStateMachine();
//LoadLuaCsConfig();
return; return;
// == end // == end
// == sub processes StateMachine<RunState> SetupStateMachine()
void RegisterServices()
{ {
_servicesProvider.RegisterServiceType<ILoggerService, LoggerService>(ServiceLifetime.Singleton); return new StateMachine<RunState>(false, RunState.Unloaded, RunStateUnloaded_OnEnter, null)
_servicesProvider.RegisterServiceType<PerformanceCounterService, PerformanceCounterService>(ServiceLifetime.Singleton); .AddState(RunState.LoadedNoExec, RunStateLoadedNoExec_OnEnter, RunStateLoadedNoExec_OnExit)
_servicesProvider.RegisterServiceType<IStorageService, StorageService>(ServiceLifetime.Transient); .AddState(RunState.Running, RunStateRunning_OnEnter, RunStateRunning_OnExit);
_servicesProvider.RegisterServiceType<IEventService, EventService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<IPackageManagementService, PackageManagementService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<IPluginManagementService, PluginManagementService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<ILuaScriptManagementService, LuaScriptManagementService>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<ILuaScriptLoader, LuaScriptLoader>(ServiceLifetime.Transient);
_servicesProvider.RegisterServiceType<LuaGame, LuaGame>(ServiceLifetime.Singleton);
// TODO: IConfigService // ReSharper disable InconsistentNaming
// TODO: INetworkingService void RunStateUnloaded_OnEnter(State<RunState> currentState)
// TODO: [Resource Converter/Parser Services] {
_servicesProvider.RegisterServiceType<IModConfigService, ModConfigService>(ServiceLifetime.Transient); }
// service config data void RunStateLoadedNoExec_OnEnter(State<RunState> currentState)
_servicesProvider.RegisterServiceType<IStorageServiceConfig, StorageServiceConfig>(ServiceLifetime.Singleton); {
_servicesProvider.RegisterServiceType<ILuaScriptServicesConfig, LuaScriptServicesConfig>(ServiceLifetime.Singleton);
_servicesProvider.RegisterServiceType<IConfigServiceConfig, ConfigServiceConfig>(ServiceLifetime.Singleton);
// gen IL }
_servicesProvider.Compile();
void RunStateLoadedNoExec_OnExit(State<RunState> currentState)
{
}
void RunStateRunning_OnEnter(State<RunState> currentState)
{
}
void RunStateRunning_OnExit(State<RunState> currentState)
{
}
// ReSharper restore InconsistentNaming
} }
}
// Validates LuaCs assets in /Content are valid and ready to use. bool ValidateLuaCsContent()
void ValidateLuaCsContent() {
{ #if DEBUG
// check if /Content/Lua/ModConfig.xml exists // TODO: we just wanna boot for now
// if not, try to copy it from the Workshop Mod (ie. installation mode) return true;
// if that fails, throw an error and exit. #endif
} // check if /Content/Lua/ModConfig.xml exists
// if not, try to copy it from the Local Mods folder
// if not, try to copy it from the Workshop Mods folder
// if that fails, throw an error and exit.
throw new NotImplementedException();
}
void RegisterServices(IServicesProvider servicesProvider)
{
servicesProvider.RegisterServiceType<ILoggerService, LoggerService>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<PerformanceCounterService, PerformanceCounterService>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<IStorageService, StorageService>(ServiceLifetime.Transient);
servicesProvider.RegisterServiceType<IEventService, EventService>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<IPackageManagementService, PackageManagementService>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<IPluginManagementService, PluginManagementService>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<ILuaScriptManagementService, LuaScriptManagementService>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<ILuaScriptLoader, LuaScriptLoader>(ServiceLifetime.Transient);
servicesProvider.RegisterServiceType<LuaGame, LuaGame>(ServiceLifetime.Singleton);
// TODO: IConfigService
// TODO: INetworkingService
// TODO: [Resource Converter/Parser Services]
servicesProvider.RegisterServiceType<IModConfigService, ModConfigService>(ServiceLifetime.Transient);
// service config data
servicesProvider.RegisterServiceType<IStorageServiceConfig, StorageServiceConfig>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<ILuaScriptServicesConfig, LuaScriptServicesConfig>(ServiceLifetime.Singleton);
servicesProvider.RegisterServiceType<IConfigServiceConfig, ConfigServiceConfig>(ServiceLifetime.Singleton);
// gen IL
servicesProvider.Compile();
} }
void SubscribeToLuaCsEvents() void SubscribeToLuaCsEvents()
@@ -86,11 +134,10 @@ namespace Barotrauma
#if SERVER #if SERVER
public const bool IsServer = true; public const bool IsServer = true;
public const bool IsClient = false;
#else #else
public const bool IsServer = false; public const bool IsServer = false;
public const bool IsClient = true;
#endif #endif
public const bool IsClient = !IsServer;
#endregion #endregion
@@ -102,20 +149,20 @@ namespace Barotrauma
private readonly IServicesProvider _servicesProvider; private readonly IServicesProvider _servicesProvider;
public PerformanceCounterService PerformanceCounter => public PerformanceCounterService PerformanceCounter => _servicesProvider.GetService<PerformanceCounterService>();
_servicesProvider.GetService<PerformanceCounterService>();
public ILoggerService Logger => _servicesProvider.GetService<ILoggerService>(); public ILoggerService Logger => _servicesProvider.GetService<ILoggerService>();
public IConfigService ConfigService => _servicesProvider.GetService<IConfigService>(); public IConfigService ConfigService => _servicesProvider.GetService<IConfigService>();
public IPackageManagementService PackageManagementService => public IPackageManagementService PackageManagementService => _servicesProvider.GetService<IPackageManagementService>();
_servicesProvider.GetService<IPackageManagementService>(); public IPluginManagementService PluginManagementService => _servicesProvider.GetService<IPluginManagementService>();
public IPluginManagementService PluginManagementService => public ILuaScriptManagementService LuaScriptManagementService => _servicesProvider.GetService<ILuaScriptManagementService>();
_servicesProvider.GetService<IPluginManagementService>();
public ILuaScriptManagementService LuaScriptManagementService =>
_servicesProvider.GetService<ILuaScriptManagementService>();
public INetworkingService NetworkingService => _servicesProvider.GetService<INetworkingService>(); public INetworkingService NetworkingService => _servicesProvider.GetService<INetworkingService>();
public IEventService EventService => _servicesProvider.GetService<IEventService>(); public IEventService EventService => _servicesProvider.GetService<IEventService>();
public LuaGame Game => _servicesProvider.GetService<LuaGame>(); public LuaGame Game => _servicesProvider.GetService<LuaGame>();
#region LuaCsInternal
/* /*
* === Config Vars * === Config Vars
*/ */
@@ -123,67 +170,61 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Whether C# plugin code is enabled. /// Whether C# plugin code is enabled.
/// </summary> /// </summary>
public IConfigEntry<bool> IsCsEnabled { get; private set; } internal IConfigEntry<bool> IsCsEnabled { get; private set; }
/// <summary> /// <summary>
/// Whether mods marked as 'forced' or 'always load' should only be loaded if they're in the enabled mods list. /// Whether mods marked as 'forced' or 'always load' should only be loaded if they're in the enabled mods list.
/// </summary> /// </summary>
public IConfigEntry<bool> TreatForcedModsAsNormal { get; private set; } internal IConfigEntry<bool> TreatForcedModsAsNormal { get; private set; }
/// <summary> /// <summary>
/// Whether the lua script runner from Workshop package should be used over the in-built version. /// Whether the lua script runner from Workshop package should be used over the in-built version.
/// </summary> /// </summary>
public IConfigEntry<bool> PreferToUseWorkshopLuaSetup { get; private set; } internal IConfigEntry<bool> PreferToUseWorkshopLuaSetup { get; private set; }
/// <summary> /// <summary>
/// Whether the popup error GUI should be hidden/suppressed. /// Whether the popup error GUI should be hidden/suppressed.
/// </summary> /// </summary>
public IConfigEntry<bool> DisableErrorGUIOverlay { get; private set; } internal IConfigEntry<bool> DisableErrorGUIOverlay { get; private set; }
/// <summary> /// <summary>
/// Whether usernames are anonymized or show in logs. /// Whether usernames are anonymized or show in logs.
/// </summary> /// </summary>
public IConfigEntry<bool> HideUserNamesInLogs { get; private set; } internal IConfigEntry<bool> HideUserNamesInLogs { get; private set; }
/// <summary> /// <summary>
/// The SteamId of the Workshop LuaCs CPackage in use, if available. /// The SteamId of the Workshop LuaCs CPackage in use, if available.
/// </summary> /// </summary>
public IConfigEntry<ulong> LuaForBarotraumaSteamId { get; private set; } internal IConfigEntry<ulong> LuaForBarotraumaSteamId { get; private set; }
/// <summary>
/// The SteamId of the Workshop LuaCs CsForBarotrauma add-on, if available.
/// </summary>
public IConfigEntry<ulong> CsForBarotraumaSteamId { get; private set; }
/// <summary>
/// Whether to (re)load all package assets when a lobby starts/code session begins.
/// Intended for development use, or when packages are expected to change outside of External Updates (ie. Steam Workshop).
/// </summary>
public IConfigEntry<bool> ReloadPackagesOnLobbyStart { get; private set; }
/// <summary> /// <summary>
/// TODO: @evilfactory@users.noreply.github.com /// TODO: @evilfactory@users.noreply.github.com
/// </summary> /// </summary>
public IConfigEntry<bool> RestrictMessageSize { get; private set; } internal IConfigEntry<bool> RestrictMessageSize { get; private set; }
/// <summary> /// <summary>
/// The local save path for all local data storage for mods. /// The local save path for all local data storage for mods.
/// </summary> /// </summary>
public IConfigEntry<string> LocalDataSavePath { get; private set; } internal IConfigEntry<string> LocalDataSavePath { get; private set; }
#endregion
/** /**
* == Ops Vars * == Ops Vars
*/ */
private RunState _runState; private RunState _runState;
/// <summary> /// <summary>
/// The current run state of all services managed by LuaCs. /// The current run state of all services managed by LuaCs.
/// </summary> /// </summary>
public RunState CurrentRunState => _runState; public RunState CurrentRunState
{
get => _runState;
private set => _runState = value;
}
private bool CPacksParsed => CurrentRunState >= RunState.Parsed;
private bool IsStaticAssetsLoaded => CurrentRunState >= RunState.Configuration;
private bool IsCodeRunning => CurrentRunState >= RunState.Running;
private readonly StateMachine<RunState> _runStateMachine;
private readonly ConcurrentQueue<ContentPackage> _toLoad = new(); private readonly ConcurrentQueue<ContentPackage> _toLoad = new();
private readonly ConcurrentQueue<ContentPackage> _toUnload = new(); private readonly ConcurrentQueue<ContentPackage> _toUnload = new();
@@ -195,61 +236,6 @@ namespace Barotrauma
#endregion #endregion
private partial bool ShouldRunCs();
// TODO: Rework
[Obsolete("Use IPluginManagementService::GetTypesByName()")]
public static Type GetType(string typeName, bool throwOnError = false, bool ignoreCase = false)
{
throw new NotImplementedException();
//return AssemblyManager.GetTypesByName(typeName).FirstOrDefault((Type)null);
}
public static ContentPackage GetPackage(ContentPackageId id, bool fallbackToAll = true, bool useBackup = false)
{
foreach (ContentPackage package in ContentPackageManager.EnabledPackages.All)
{
if (package.UgcId.ValueEquals(id))
{
return package;
}
}
if (fallbackToAll)
{
foreach (ContentPackage package in ContentPackageManager.LocalPackages)
{
if (package.UgcId.ValueEquals(id))
{
return package;
}
}
foreach (ContentPackage package in ContentPackageManager.AllPackages)
{
if (package.UgcId.ValueEquals(id))
{
return package;
}
}
}
if (useBackup && ContentPackageManager.EnabledPackages.BackupPackages.Regular != null)
{
foreach (ContentPackage package in ContentPackageManager.EnabledPackages.BackupPackages.Regular.Value)
{
if (package.UgcId.ValueEquals(id))
{
return package;
}
}
}
return null;
}
public void Dispose() public void Dispose()
{ {
try try
@@ -334,108 +320,11 @@ namespace Barotrauma
//ProcessPackagesListDifferences(); //ProcessPackagesListDifferences();
} }
/*void ProcessPackagesListDifferences() void SetRunState(RunState newRunState)
{ {
if (IsCodeRunning) if (CurrentRunState == newRunState)
return; return;
// no reason to do anything if we're fully unloaded.
if (!CPacksParsed)
{
_toLoad.Clear();
_toUnload.Clear();
}
while (_toUnload.TryDequeue(out var cp))
{
}
var ls = new List<ContentPackage>();
while (_toLoad.TryDequeue(out var cp))
{
if (PackageManagementService.LoadPackageInfo(cp) is
{ IsFailed: true } failure)
{
Logger.LogError($"Failed to load package infos for {cp.Name}");
Logger.LogResults(failure);
continue;
}
ls.Add(cp);
}
if (ls.Any())
{
LoadStaticAssetsAsync(ls).GetAwaiter().GetResult();
}
}*/
void SetRunState(RunState runState)
{
if (CurrentRunState == runState)
return;
/*if (runState > CurrentRunState)
{
if (CurrentRunState <= RunState.Parsed)
{
LoadCurrentContentPackageInfos();
}
if (runState <= CurrentRunState)
{
return;
}
if (CurrentRunState <= RunState.Configuration)
{
LoadStaticAssets();
}
if (runState <= CurrentRunState)
{
return;
}
if (CurrentRunState <= RunState.Running)
{
RunScripts();
}
}
else if (runState < CurrentRunState)
{
if (CurrentRunState >= RunState.Running)
{
StopScripts();
ProcessPackagesListDifferences();
_runState = RunState.Configuration;
}
if (runState >= CurrentRunState)
{
return;
}
if (CurrentRunState == RunState.Configuration)
{
UnloadStaticAssets();
_runState = RunState.Parsed;
}
if (runState >= CurrentRunState)
{
return;
}
if (CurrentRunState == RunState.Parsed)
{
UnloadContentPackageInfos();
_runState = RunState.Unloaded;
}
// we should be unloaded completely now | RunState.Unloaded
}*/
} }
void LoadLuaCsConfig() void LoadLuaCsConfig()
@@ -450,12 +339,8 @@ namespace Barotrauma
: throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded."); : throw new NullReferenceException($"{nameof(HideUserNamesInLogs)} cannot be loaded.");
LuaForBarotraumaSteamId = ConfigService.TryGetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId", out var val5) ? val5 LuaForBarotraumaSteamId = ConfigService.TryGetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "LuaForBarotraumaSteamId", out var val5) ? val5
: throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded."); : throw new NullReferenceException($"{nameof(LuaForBarotraumaSteamId)} cannot be loaded.");
CsForBarotraumaSteamId = ConfigService.TryGetConfig<IConfigEntry<ulong>>(ContentPackageManager.VanillaCorePackage, "CsForBarotraumaSteamId", out var val6) ? val6
: throw new NullReferenceException($"{nameof(CsForBarotraumaSteamId)} cannot be loaded.");
RestrictMessageSize = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "RestrictMessageSize", out var val7) ? val7 RestrictMessageSize = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "RestrictMessageSize", out var val7) ? val7
: throw new NullReferenceException($"{nameof(RestrictMessageSize)} cannot be loaded."); : throw new NullReferenceException($"{nameof(RestrictMessageSize)} cannot be loaded.");
ReloadPackagesOnLobbyStart = ConfigService.TryGetConfig<IConfigEntry<bool>>(ContentPackageManager.VanillaCorePackage, "ReloadPackagesOnLobbyStart", out var val8) ? val8
: throw new NullReferenceException($"{nameof(ReloadPackagesOnLobbyStart)} cannot be loaded.");
} }
void DisposeLuaCsConfig() void DisposeLuaCsConfig()
@@ -465,12 +350,8 @@ namespace Barotrauma
DisableErrorGUIOverlay = null; DisableErrorGUIOverlay = null;
HideUserNamesInLogs = null; HideUserNamesInLogs = null;
LuaForBarotraumaSteamId = null; LuaForBarotraumaSteamId = null;
CsForBarotraumaSteamId = null;
RestrictMessageSize = null; RestrictMessageSize = null;
ReloadPackagesOnLobbyStart = null;
} }
} }
/// <summary> /// <summary>
@@ -479,9 +360,18 @@ namespace Barotrauma
/// </summary> /// </summary>
public enum RunState : byte public enum RunState : byte
{ {
Unloaded = 0, // No asset data loaded. /// <summary>
Parsed, // CPacks' ResourceInfos are parsed. /// No assets are loaded, code execution suspended.
Configuration, // localization and configuration assets loaded. /// </summary>
Running // all assets loaded, code running. Unloaded = 0,
/// <summary>
/// Loaded mod configs, settings and assets. No code execution.
/// </summary>
LoadedNoExec = 1,
/// <summary>
/// All assets loaded, code execution is active.
/// </summary>
Running = 2
} }
} }
@@ -0,0 +1,107 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Microsoft.Toolkit.Diagnostics;
namespace Barotrauma.LuaCs;
public class StateMachine<T> where T : Enum
{
private readonly ConcurrentDictionary<T, State<T>> _states;
private State<T> _currentState;
public T CurrentState => _currentState.StateId;
private bool _errorOnSameStateSelected;
private readonly AsyncReaderWriterLock _operationsLock = new();
public StateMachine(bool errorOnSameState, T defaultState, Action<State<T>> onEnter, Action<State<T>> onExit)
{
_errorOnSameStateSelected = errorOnSameState;
_states = new ConcurrentDictionary<T, State<T>>();
var defState = new State<T>(defaultState, onEnter, onExit);
_currentState = defState;
_states[defaultState] = defState;
}
public StateMachine<T> AddState(T stateId, Action<State<T>> onEnter, Action<State<T>> onExit)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (_states.TryGetValue(stateId, out _))
{
ThrowHelper.ThrowArgumentException($"State with id {stateId} already exists.");
}
_states[stateId] = new State<T>(stateId, onEnter, onExit);
return this;
}
public StateMachine<T> RemoveState(T stateId)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (EqualityComparer<T>.Default.Equals(stateId, CurrentState))
{
ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot remove.");
}
_states.TryRemove(stateId, out _);
return this;
}
public StateMachine<T> AddOrReplaceState(T oldStateId, T newStateId, Action<State<T>> onEnter, Action<State<T>> onExit)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (EqualityComparer<T>.Default.Equals(oldStateId, CurrentState))
{
ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot replace.");
}
_states[oldStateId] = new State<T>(newStateId, onEnter, onExit);
return this;
}
public StateMachine<T> GotoState(T stateId)
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (EqualityComparer<T>.Default.Equals(stateId, CurrentState))
{
if (_errorOnSameStateSelected)
{
ThrowHelper.ThrowInvalidOperationException($"State with id {stateId} is already selected.");
}
return this;
}
if (!_states.TryGetValue(stateId, out var newState))
{
ThrowHelper.ThrowArgumentNullException($"Target state with id {stateId} does not exist.");
}
_currentState.OnExit();
_currentState = newState;
_currentState.OnEnter();
return this;
}
}
public class State<T> where T : Enum
{
public T StateId;
private Action<State<T>> _onEnter, _onExit;
public State(T stateId, Action<State<T>> onExitState, Action<State<T>> onEnterState)
{
StateId = stateId;
_onEnter = onEnterState;
_onExit = onExitState;
}
public virtual void OnEnter()
{
_onEnter?.Invoke(this);
}
public virtual void OnExit()
{
_onExit?.Invoke(this);
}
}