Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#13 Send and execute command upon state enter transition. #325

Merged
merged 1 commit into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,32 @@ public void SetUp() {
CreateTestData();
}

[Test, TestCaseSource(nameof(EnterTestArgs))]
public void ExecutionTestEnter(
Type choreType,
Func<object[]> createChoreArgsFunc,
Func<Dictionary<int, object?>> 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<FakeStatesManager>().TransitToState
);
}

[Test, TestCaseSource(nameof(ExitTestArgs))]
public void ExecutionTest(
public void ExecutionTestExit(
Type choreType,
Func<object[]> createChoreArgsFunc,
Func<Dictionary<int, object?>> stateTransitionArgsFunc,
Expand All @@ -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<FakeStatesManager>().WasCalled);
Assert.AreEqual(
config.StateToMonitorName,
Runtime.Instance.Dependencies.Get<FakeStatesManager>().TransitToState
);
}

[Test, TestCaseSource(nameof(EnterTestArgs))]
public void SerializationTestEnter(
Type choreType,
Func<object[]> createChoreArgsFunc,
Func<Dictionary<int, object?>> 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<object[]> createChoreArgsFunc,
Func<Dictionary<int, object?>> stateTransitionArgsFunc,
Expand All @@ -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;
Expand All @@ -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<int, object?> args) {
WasCalled = true;
TransitToState = targetState;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ public void ClientStateEnter_DisablesTransition(
StateTransitionConfig config
) {
var statesManager = Runtime.Instance.Dependencies.Get<FakeStatesManager>();
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))]
Expand All @@ -53,20 +55,26 @@ public void ClientStateExit_DisablesTransition(
StateTransitionConfig config
) {
var statesManager = Runtime.Instance.Dependencies.Get<FakeStatesManager>();
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<StateMachine.BaseState> CalledWithStates = new();
public readonly List<StateMachine.BaseState> WaitArgs = new();
public readonly List<StateMachine.BaseState> 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!;
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/MultiplayerMod.Test/Multiplayer/States/StatesManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ StateTransitionConfig config
Assert.AreEqual(expectedDictionary, waitHostState.ParametersArgs.Get(smi));
}

[Test]
public void AddContinuationState_CopiesArgumentsFromOriginal() {
var statesManager = Runtime.Instance.Dependencies.Get<StatesManager>();
var stateToBeSynced =
new GameStateMachine<AggressiveChore.States, AggressiveChore.StatesInstance, AggressiveChore, object>.State {
defaultState = new StateMachine.BaseState(),
enterActions = new List<StateMachine.Action> { new() },
exitActions = new List<StateMachine.Action> { new() },
updateActions = new List<StateMachine.UpdateAction> { new() },
events = new List<StateEvent> {
new GameStateMachine<AggressiveChore.States, AggressiveChore.StatesInstance, AggressiveChore, object>.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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,26 @@ public class TransitChoreToState : MultiplayerCommand {
public readonly string? TargetState;
public readonly Dictionary<int, object?> 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<int, object?> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@ private static IEnumerable<MethodBase> 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<StatesManager>();
var stateToBeSynced = stateTransitionConfig.GetMonitoredState(__instance);
Runtime.Instance.Dependencies.Get<StatesManager>().ReplaceWithWaitState(stateToBeSynced);
switch (stateTransitionConfig.TransitionType) {
case TransitionTypeEnum.Exit: {
statesManager.ReplaceWithWaitState(stateToBeSynced);
break;
}
case TransitionTypeEnum.Enter: {
statesManager.ReplaceWithWaitState(stateToBeSynced);
statesManager.AddContinuationState(stateToBeSynced);
break;
}
}
}
}
}
19 changes: 19 additions & 0 deletions src/MultiplayerMod/Multiplayer/States/ContinuationState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace MultiplayerMod.Multiplayer.States;

public class
ContinuationState<StateMachineType, StateMachineInstanceType, MasterType, DefType> : StateMachine<StateMachineType, StateMachineInstanceType, MasterType, DefType>.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 });
}
}
14 changes: 12 additions & 2 deletions src/MultiplayerMod/Multiplayer/States/StatesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, object?> args) {
var smi = GetSmi(chore);
Expand Down Expand Up @@ -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))
Expand All @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion src/MultiplayerMod/Multiplayer/States/WaitHostState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class
[UsedImplicitly] public WaitStateParam<Dictionary<int, object?>> 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<int, object?>());
Expand Down