diff --git a/src/MultiplayerMod.Test/Game/Chores/States/ChoreStateEventsTest.cs b/src/MultiplayerMod.Test/Game/Chores/States/ChoreStateEventsTest.cs index 5eb2c8b5..88affb13 100644 --- a/src/MultiplayerMod.Test/Game/Chores/States/ChoreStateEventsTest.cs +++ b/src/MultiplayerMod.Test/Game/Chores/States/ChoreStateEventsTest.cs @@ -34,6 +34,7 @@ StateTransitionConfig config var chore = CreateChore(choreType, choreArgsFunc.Invoke()); var smi = (StateMachine.Instance) chore.GetType().GetProperty("smi").GetValue(chore); var state = config.GetMonitoredState(smi.stateMachine); + state.defaultState = null; var amountInstance = new AmountInstance(Db.Get().Amounts.Calories, Minion.gameObject); amountInstance.maxAttribute.Attribute.BaseValue = 50; Minion.gameObject.GetAmounts().ModifierList.Add(amountInstance); @@ -51,6 +52,10 @@ StateTransitionConfig config var expectedDictionary = expectedDictionaryFunc.Invoke(); Assert.NotNull(firedArgs); Assert.AreEqual(chore, firedArgs!.Chore); + Assert.AreEqual( + config.StateToMonitorName != "root" ? "root." + config.StateToMonitorName : config.StateToMonitorName, + firedArgs!.TargetState + ); Assert.AreEqual(expectedDictionary.Keys, firedArgs!.Args.Keys); Assert.AreEqual( expectedDictionary.Values.Select(it => it?.GetType()).ToArray(), diff --git a/src/MultiplayerMod.Test/Multiplayer/Commands/Chores/States/TransitChoreToStateTest.cs b/src/MultiplayerMod.Test/Multiplayer/Commands/Chores/States/TransitChoreToStateTest.cs index 5cf0cbf3..c19282ab 100644 --- a/src/MultiplayerMod.Test/Multiplayer/Commands/Chores/States/TransitChoreToStateTest.cs +++ b/src/MultiplayerMod.Test/Multiplayer/Commands/Chores/States/TransitChoreToStateTest.cs @@ -32,8 +32,32 @@ public void SetUp() { CreateTestData(); } + [Test, TestCaseSource(nameof(EnterTestArgs))] + public void ExecutionTestEnter( + Type choreType, + Func createChoreArgsFunc, + Func> stateTransitionArgsFunc, + StateTransitionConfig config + ) { + var chore = CreateChore(choreType, createChoreArgsFunc.Invoke()); + chore.Register(new MultiplayerId(Guid.NewGuid())); + var arg = new ChoreTransitStateArgs( + chore, + config.StateToMonitorName, + stateTransitionArgsFunc.Invoke() + ); + var command = TransitChoreToState.EnterTransition(arg); + + command.Execute(new MultiplayerCommandContext(null, new MultiplayerCommandRuntimeAccessor(Runtime.Instance))); + + Assert.AreEqual( + config.StateToMonitorName + "_" + StatesManager.ContinuationName, + Runtime.Instance.Dependencies.Get().TransitToState + ); + } + [Test, TestCaseSource(nameof(ExitTestArgs))] - public void ExecutionTest( + public void ExecutionTestExit( Type choreType, Func createChoreArgsFunc, Func> stateTransitionArgsFunc, @@ -46,15 +70,47 @@ StateTransitionConfig config config.StateToMonitorName, stateTransitionArgsFunc.Invoke() ); - var command = new TransitChoreToState(arg); + var command = TransitChoreToState.ExitTransition(arg); command.Execute(new MultiplayerCommandContext(null, new MultiplayerCommandRuntimeAccessor(Runtime.Instance))); - Assert.True(Runtime.Instance.Dependencies.Get().WasCalled); + Assert.AreEqual( + config.StateToMonitorName, + Runtime.Instance.Dependencies.Get().TransitToState + ); + } + + [Test, TestCaseSource(nameof(EnterTestArgs))] + public void SerializationTestEnter( + Type choreType, + Func createChoreArgsFunc, + Func> stateTransitionArgsFunc, + StateTransitionConfig config + ) { + var chore = CreateChore(choreType, createChoreArgsFunc.Invoke()); + chore.Register(new MultiplayerId(Guid.NewGuid())); + var arg = new ChoreTransitStateArgs( + chore, + config.StateToMonitorName, + stateTransitionArgsFunc.Invoke() + ); + var command = TransitChoreToState.EnterTransition(arg); + var messageFactory = new NetworkMessageFactory(); + var messageProcessor = new NetworkMessageProcessor(); + NetworkMessage? networkMessage = null; + + foreach (var messageHandle in messageFactory.Create(command, MultiplayerCommandOptions.SkipHost).ToArray()) { + networkMessage = messageProcessor.Process(1u, messageHandle); + } + + Assert.AreEqual(command.GetType(), networkMessage?.Command.GetType()); + Assert.AreEqual(command.ChoreId, ((TransitChoreToState) networkMessage!.Command).ChoreId); + Assert.AreEqual(command.TargetState, ((TransitChoreToState) networkMessage.Command).TargetState); + Assert.AreEqual(command.Args.Keys, ((TransitChoreToState) networkMessage.Command).Args.Keys); } [Test, TestCaseSource(nameof(ExitTestArgs))] - public void SerializationTest( + public void SerializationTestExit( Type choreType, Func createChoreArgsFunc, Func> stateTransitionArgsFunc, @@ -67,7 +123,7 @@ StateTransitionConfig config config.StateToMonitorName, stateTransitionArgsFunc.Invoke() ); - var command = new TransitChoreToState(arg); + var command = TransitChoreToState.ExitTransition(arg); var messageFactory = new NetworkMessageFactory(); var messageProcessor = new NetworkMessageProcessor(); NetworkMessage? networkMessage = null; @@ -83,10 +139,10 @@ StateTransitionConfig config } private class FakeStatesManager : StatesManager { - public bool WasCalled; + public string? TransitToState; public override void AllowTransition(Chore chore, string? targetState, Dictionary args) { - WasCalled = true; + TransitToState = targetState; } } } diff --git a/src/MultiplayerMod.Test/Multiplayer/Patches/Chores/States/DisableChoreStateTransitionTest.cs b/src/MultiplayerMod.Test/Multiplayer/Patches/Chores/States/DisableChoreStateTransitionTest.cs index 5f28a264..aab4ed8f 100644 --- a/src/MultiplayerMod.Test/Multiplayer/Patches/Chores/States/DisableChoreStateTransitionTest.cs +++ b/src/MultiplayerMod.Test/Multiplayer/Patches/Chores/States/DisableChoreStateTransitionTest.cs @@ -36,13 +36,15 @@ public void ClientStateEnter_DisablesTransition( StateTransitionConfig config ) { var statesManager = Runtime.Instance.Dependencies.Get(); - statesManager.CalledWithStates.Clear(); + statesManager.WaitArgs.Clear(); + statesManager.ContinuationArgs.Clear(); var chore = CreateChore(choreType, choreArgsFunc.Invoke()); var smi = statesManager.GetSmi(chore); - var expectedState = config.GetMonitoredState(smi.stateMachine); - Assert.Contains(expectedState, statesManager.CalledWithStates); + var expectedState = config.GetMonitoredState(smi.GetStateMachine()); + Assert.Contains(expectedState, statesManager.WaitArgs); + Assert.Contains(expectedState, statesManager.ContinuationArgs); } [Test, TestCaseSource(nameof(ExitTestArgs))] @@ -53,20 +55,26 @@ public void ClientStateExit_DisablesTransition( StateTransitionConfig config ) { var statesManager = Runtime.Instance.Dependencies.Get(); - statesManager.CalledWithStates.Clear(); + statesManager.WaitArgs.Clear(); var chore = CreateChore(choreType, choreArgsFunc.Invoke()); var smi = statesManager.GetSmi(chore); - var expectedState = config.GetMonitoredState(smi.stateMachine); - Assert.Contains(expectedState, statesManager.CalledWithStates); + var expectedState = config.GetMonitoredState(smi.GetStateMachine()); + Assert.Contains(expectedState, statesManager.WaitArgs); } private class FakeStatesManager : StatesManager { - public readonly List CalledWithStates = new(); + public readonly List WaitArgs = new(); + public readonly List ContinuationArgs = new(); public override void ReplaceWithWaitState(StateMachine.BaseState stateToBeSynced) { - CalledWithStates.Add(stateToBeSynced); + WaitArgs.Add(stateToBeSynced); + } + + public override StateMachine.BaseState AddContinuationState(StateMachine.BaseState stateToBeSynced) { + ContinuationArgs.Add(stateToBeSynced); + return null!; } } diff --git a/src/MultiplayerMod.Test/Multiplayer/States/StatesManagerTest.cs b/src/MultiplayerMod.Test/Multiplayer/States/StatesManagerTest.cs index 189a519f..028d45b6 100644 --- a/src/MultiplayerMod.Test/Multiplayer/States/StatesManagerTest.cs +++ b/src/MultiplayerMod.Test/Multiplayer/States/StatesManagerTest.cs @@ -55,6 +55,30 @@ StateTransitionConfig config Assert.AreEqual(expectedDictionary, waitHostState.ParametersArgs.Get(smi)); } + [Test] + public void AddContinuationState_CopiesArgumentsFromOriginal() { + var statesManager = Runtime.Instance.Dependencies.Get(); + var stateToBeSynced = + new GameStateMachine.State { + defaultState = new StateMachine.BaseState(), + enterActions = new List { new() }, + exitActions = new List { new() }, + updateActions = new List { new() }, + events = new List { + new GameStateMachine.GameEvent(GameHashes.Absorb, null, null, null) + }, + sm = new AggressiveChore.States() + }; + + var createdState = statesManager.AddContinuationState(stateToBeSynced); + + Assert.AreEqual(stateToBeSynced.defaultState, createdState.defaultState); + Assert.Null(createdState.enterActions); + Assert.AreEqual(stateToBeSynced.exitActions, createdState.exitActions); + Assert.Null(createdState.updateActions); + Assert.Null(createdState.events); + } + [Test, TestCaseSource(nameof(EnterTestArgs))] public void EnterArgs_DisableChoreStateTransition( Type choreType, diff --git a/src/MultiplayerMod/Game/Chores/Types/StateTransitionConfig.cs b/src/MultiplayerMod/Game/Chores/Types/StateTransitionConfig.cs index a379a6a5..f9bfe959 100644 --- a/src/MultiplayerMod/Game/Chores/Types/StateTransitionConfig.cs +++ b/src/MultiplayerMod/Game/Chores/Types/StateTransitionConfig.cs @@ -9,7 +9,6 @@ public record StateTransitionConfig( string[] ParameterName ) { - /// TODO: Execute command on the client public static StateTransitionConfig OnEnter(string stateName, params string[] parameterName) => new(TransitionTypeEnum.Enter, stateName, null, parameterName); diff --git a/src/MultiplayerMod/Multiplayer/Commands/Chores/States/TransitChoreToState.cs b/src/MultiplayerMod/Multiplayer/Commands/Chores/States/TransitChoreToState.cs index e41175b5..2e5a4c7a 100644 --- a/src/MultiplayerMod/Multiplayer/Commands/Chores/States/TransitChoreToState.cs +++ b/src/MultiplayerMod/Multiplayer/Commands/Chores/States/TransitChoreToState.cs @@ -14,12 +14,26 @@ public class TransitChoreToState : MultiplayerCommand { public readonly string? TargetState; public readonly Dictionary Args; - public TransitChoreToState(ChoreTransitStateArgs transitData) { - ChoreId = transitData.Chore.MultiplayerId(); - TargetState = transitData.TargetState; - Args = transitData.Args.ToDictionary(a => a.Key, a => ArgumentUtils.WrapObject(a.Value)); + private TransitChoreToState(MultiplayerId choreId, string? targetState, Dictionary args) { + ChoreId = choreId; + TargetState = targetState; + Args = args; } + public static TransitChoreToState EnterTransition(ChoreTransitStateArgs transitData) => + new( + transitData.Chore.MultiplayerId(), + transitData.TargetState! + "_" + StatesManager.ContinuationName, + transitData.Args.ToDictionary(a => a.Key, a => ArgumentUtils.WrapObject(a.Value)) + ); + + public static TransitChoreToState ExitTransition(ChoreTransitStateArgs transitData) => + new( + transitData.Chore.MultiplayerId(), + transitData.TargetState, + transitData.Args.ToDictionary(a => a.Key, a => ArgumentUtils.WrapObject(a.Value)) + ); + public override void Execute(MultiplayerCommandContext context) { var args = Args.ToDictionary(a => a.Key, a => ArgumentUtils.UnWrapObject(a.Value)); var chore = ChoreObjects.GetChore(ChoreId); diff --git a/src/MultiplayerMod/Multiplayer/CoreOperations/Binders/HostEventsBinder.cs b/src/MultiplayerMod/Multiplayer/CoreOperations/Binders/HostEventsBinder.cs index 336ae16c..90469b63 100644 --- a/src/MultiplayerMod/Multiplayer/CoreOperations/Binders/HostEventsBinder.cs +++ b/src/MultiplayerMod/Multiplayer/CoreOperations/Binders/HostEventsBinder.cs @@ -46,8 +46,12 @@ private void Bind() { new CreateHostChore(args), MultiplayerCommandOptions.SkipHost ); + ChoreStateEvents.OnStateEnter += args => server.Send( + TransitChoreToState.EnterTransition(args), + MultiplayerCommandOptions.SkipHost + ); ChoreStateEvents.OnStateExit += args => server.Send( - new TransitChoreToState(args), + TransitChoreToState.ExitTransition(args), MultiplayerCommandOptions.SkipHost ); ChoreStateEvents.OnStateUpdate += args => server.Send( diff --git a/src/MultiplayerMod/Multiplayer/Patches/Chores/States/DisableChoreStateTransition.cs b/src/MultiplayerMod/Multiplayer/Patches/Chores/States/DisableChoreStateTransition.cs index c292f409..ff00a2f6 100644 --- a/src/MultiplayerMod/Multiplayer/Patches/Chores/States/DisableChoreStateTransition.cs +++ b/src/MultiplayerMod/Multiplayer/Patches/Chores/States/DisableChoreStateTransition.cs @@ -30,11 +30,20 @@ private static IEnumerable TargetMethods() { public static void InitializeStatesPatch(StateMachine __instance) { var config = ChoreList.Config[__instance.GetType().DeclaringType].StatesTransitionSync; - foreach (var stateTransitionConfig in config.StateTransitionConfigs.Where( - it => it.TransitionType is TransitionTypeEnum.Exit or TransitionTypeEnum.Enter - )) { + foreach (var stateTransitionConfig in config.StateTransitionConfigs) { + var statesManager = Runtime.Instance.Dependencies.Get(); var stateToBeSynced = stateTransitionConfig.GetMonitoredState(__instance); - Runtime.Instance.Dependencies.Get().ReplaceWithWaitState(stateToBeSynced); + switch (stateTransitionConfig.TransitionType) { + case TransitionTypeEnum.Exit: { + statesManager.ReplaceWithWaitState(stateToBeSynced); + break; + } + case TransitionTypeEnum.Enter: { + statesManager.ReplaceWithWaitState(stateToBeSynced); + statesManager.AddContinuationState(stateToBeSynced); + break; + } + } } } } diff --git a/src/MultiplayerMod/Multiplayer/States/ContinuationState.cs b/src/MultiplayerMod/Multiplayer/States/ContinuationState.cs new file mode 100644 index 00000000..a2dcdb41 --- /dev/null +++ b/src/MultiplayerMod/Multiplayer/States/ContinuationState.cs @@ -0,0 +1,19 @@ +namespace MultiplayerMod.Multiplayer.States; + +public class + ContinuationState : StateMachine.State + where StateMachineInstanceType : StateMachine.Instance + where MasterType : IStateMachineTarget { + + public ContinuationState(StateMachine sm, StateMachine.BaseState original) { + name = StatesManager.ContinuationName; + + // enter and update actions must be synced differently. + exitActions = original.exitActions; + defaultState = original.defaultState; + + var stateMachineType = sm.GetType(); + var root = stateMachineType.GetField("root").GetValue(sm); + stateMachineType.GetMethod("BindState")!.Invoke(sm, new[] { root, this, name }); + } +} diff --git a/src/MultiplayerMod/Multiplayer/States/StatesManager.cs b/src/MultiplayerMod/Multiplayer/States/StatesManager.cs index e151b529..c5c20e06 100644 --- a/src/MultiplayerMod/Multiplayer/States/StatesManager.cs +++ b/src/MultiplayerMod/Multiplayer/States/StatesManager.cs @@ -9,7 +9,8 @@ namespace MultiplayerMod.Multiplayer.States; [Dependency, UsedImplicitly] public class StatesManager { - public const string StateName = "WaitHostState"; + public const string WaitStateName = "WaitHostState"; + public const string ContinuationName = "ContinuationState"; public virtual void AllowTransition(Chore chore, string? targetState, Dictionary args) { var smi = GetSmi(chore); @@ -38,6 +39,15 @@ public virtual void ReplaceWithWaitState(StateMachine.BaseState stateToBeSynced) stateToBeSynced.enterActions.Add(new StateMachine.Action("Transit to waiting state", callback)); } + public virtual StateMachine.BaseState AddContinuationState(StateMachine.BaseState stateToBeSynced) { + var sm = (StateMachine) stateToBeSynced.GetType().GetField("sm").GetValue(stateToBeSynced); + + var genericType = typeof(ContinuationState<,,,>).MakeGenericType( + sm.GetType().BaseType.GetGenericArguments().Append(typeof(object)) + ); + return (StateMachine.BaseState) Activator.CreateInstance(genericType, sm, stateToBeSynced); + } + public void InjectWaitHostState(StateMachine sm) { var genericType = typeof(WaitHostState<,,,>).MakeGenericType( sm.GetType().BaseType.GetGenericArguments().Append(typeof(object)) @@ -55,7 +65,7 @@ public StateMachine.Instance GetSmi(Chore chore) { } private static StateMachine.BaseState GetWaitHostState(StateMachine.Instance smi) => - smi.stateMachine.GetState("root." + StateName); + smi.stateMachine.GetState("root." + WaitStateName); private static void TransitToWaitState(StateMachine.Instance smi) { smi.GoTo(GetWaitHostState(smi)); diff --git a/src/MultiplayerMod/Multiplayer/States/WaitHostState.cs b/src/MultiplayerMod/Multiplayer/States/WaitHostState.cs index 6e09b578..8ae7942e 100644 --- a/src/MultiplayerMod/Multiplayer/States/WaitHostState.cs +++ b/src/MultiplayerMod/Multiplayer/States/WaitHostState.cs @@ -14,7 +14,7 @@ public class [UsedImplicitly] public WaitStateParam> ParametersArgs { get; } public WaitHostState(StateMachine sm) { - name = StatesManager.StateName; + name = StatesManager.WaitStateName; TransitionAllowed = InitParam(sm, false); TargetState = InitParam(sm, (string?) null); ParametersArgs = InitParam(sm, new Dictionary());