Skip to content

Commit

Permalink
#13 Fix npe during state syncs.
Browse files Browse the repository at this point in the history
Problem was undiscovered due to faulty tests which we successfully passing. Which has been resulted due to using exposed libraries while in game runtime libraries are not exposed.

Fixed by copying original libraries to test output directory. As result 69 tests has failed.

Applied fixes for those tests.
  • Loading branch information
zuev93 committed Jan 31, 2024
1 parent 1cb64d1 commit 3c3e03f
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 28 deletions.
4 changes: 4 additions & 0 deletions src/MultiplayerMod.Test/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />
<Target Name="AfterBuild">
<Copy SourceFiles="$(ManagedPath)\Assembly-CSharp.dll" DestinationFolder="$(OutputPath)"/>
<Copy SourceFiles="$(ManagedPath)\Assembly-CSharp-firstpass.dll" DestinationFolder="$(OutputPath)"/>
</Target>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using MultiplayerMod.Core.Dependency;
using MultiplayerMod.Core.Events;
using MultiplayerMod.Core.Reflection;
using MultiplayerMod.Game.Chores.States;
using MultiplayerMod.Game.Chores.Types;
using MultiplayerMod.ModRuntime;
Expand Down Expand Up @@ -54,7 +55,7 @@ StateTransitionConfig config

command.Execute(new MultiplayerCommandContext(null, new MultiplayerCommandRuntimeAccessor(Runtime.Instance)));

var target = sm.GetType().GetField("stateTarget")!.GetValue(sm);
var target = sm.GetFieldValue("stateTarget");
var navigator = (Navigator) target.GetType().GetMethod("Get")
.MakeGenericMethod(typeof(Navigator))
.Invoke(target, new object[] { smi });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using MultiplayerMod.Core.Dependency;
using MultiplayerMod.Core.Events;
using MultiplayerMod.Core.Reflection;
using MultiplayerMod.Game.Chores.States;
using MultiplayerMod.Game.Chores.Types;
using MultiplayerMod.ModRuntime;
Expand Down Expand Up @@ -49,7 +50,7 @@ StateTransitionConfig config
command.Execute(new MultiplayerCommandContext(null, new MultiplayerCommandRuntimeAccessor(Runtime.Instance)));

var sm = smi.stateMachine;
var target = sm.GetType().GetField("stateTarget")!.GetValue(sm);
var target = sm.GetFieldValue("stateTarget");
var navigator = (Navigator) target.GetType().GetMethod("Get")
.MakeGenericMethod(typeof(Navigator))
.Invoke(target, new object[] { smi });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,12 +223,12 @@ private class FakeStatesManager : StatesManager {
public readonly List<StateMachine.BaseState> WaitArgs = new();
public readonly List<StateMachine.BaseState> ContinuationArgs = new();

public override void AddAndTransitToWaiStateUponEnter(StateMachine.BaseState stateToBeSynced) {
WaitArgs.Add(stateToBeSynced);
public override void AddAndTransitToWaiStateUponEnter(StateMachine.BaseState state) {
WaitArgs.Add(state);
}

public override StateMachine.BaseState AddContinuationState(StateMachine.BaseState stateToBeSynced) {
ContinuationArgs.Add(stateToBeSynced);
public override StateMachine.BaseState AddContinuationState(StateMachine.BaseState state) {
ContinuationArgs.Add(state);
return null!;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/MultiplayerMod.Test/MultiplayerMod.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0"/>
<PackageReference Include="NUnit" Version="3.13.3"/>
<PackageReference Include="AssemblyExposer.MSBuild.Task" Version="0.1.5" Private="true" />
<PackageReference Include="AssemblyExposer.MSBuild.Task" Version="0.1.5" Private="true"/>
</ItemGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp" HintPath="$(ExposedLibrariesPath)\Assembly-CSharp.dll"/>
Expand Down
23 changes: 23 additions & 0 deletions src/MultiplayerMod/Core/Reflection/ReflectionExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Reflection;

namespace MultiplayerMod.Core.Reflection;

public static class ReflectionExtension {

public static object GetFieldValue(this object obj, string fieldName) {
return GetFieldValue<object>(obj, fieldName);
}

public static T GetFieldValue<T>(this object obj, string fieldName) {
var type = obj.GetType();
while (type != typeof(object)) {
var field = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (field != null) {
return (T) field.GetValue(obj);
}
type = type.BaseType;
}
throw new Exception($"Field {fieldName} not found in {obj.GetType()}");
}
}
23 changes: 15 additions & 8 deletions src/MultiplayerMod/Game/Chores/States/ChoreStateEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using HarmonyLib;
using JetBrains.Annotations;
using MultiplayerMod.Core.Reflection;
using MultiplayerMod.Game.Chores.Types;
using MultiplayerMod.ModRuntime.Context;
using MultiplayerMod.Multiplayer;
Expand Down Expand Up @@ -142,7 +143,7 @@ private static void UpdateHandlerCallback(StateTransitionConfig config, StateMac

private static void EventHandlerCallback(StateTransitionConfig config, StateMachine.Instance smi) {
var chore = (Chore) smi.GetMaster();
var goToStack = (Stack<StateMachine.BaseState>) smi.GetType().GetField("gotoStack").GetValue(smi);
var goToStack = GetGoToStack(smi);
var newState = goToStack.FirstOrDefault();
var args = config.ParameterName
.ToDictionary(
Expand All @@ -163,10 +164,9 @@ private static void EventHandlerCallback(StateTransitionConfig config, StateMach

private static void MoveEnterHandler(StateMachine.Instance smi) {
var chore = (Chore) smi.GetMaster();
var goToStack = (Stack<StateMachine.BaseState>) smi.GetType().GetField("gotoStack").GetValue(smi);
var goToStack = GetGoToStack(smi);
var newState = goToStack.FirstOrDefault();
var sm = smi.stateMachine;
var target = sm.GetType().GetField("stateTarget")!.GetValue(sm);
var target = GetStateTarget(smi);
var navigator = (Navigator) target.GetType().GetMethod("Get")
.MakeGenericMethod(typeof(Navigator))
.Invoke(target, new object[] { smi });
Expand All @@ -177,10 +177,9 @@ private static void MoveEnterHandler(StateMachine.Instance smi) {

private static void MoveUpdateHandler(StateMachine.Instance smi, float dt) {
var chore = (Chore) smi.GetMaster();
var goToStack = (Stack<StateMachine.BaseState>) smi.GetType().GetField("gotoStack").GetValue(smi);
var goToStack = GetGoToStack(smi);
var newState = goToStack.FirstOrDefault();
var sm = smi.stateMachine;
var target = sm.GetType().GetField("stateTarget")!.GetValue(sm);
var target = GetStateTarget(smi);
var navigator = (Navigator) target.GetType().GetMethod("Get")
.MakeGenericMethod(typeof(Navigator))
.Invoke(target, new object[] { smi });
Expand All @@ -191,7 +190,7 @@ private static void MoveUpdateHandler(StateMachine.Instance smi, float dt) {

private static void MoveExitHandler(StateMachine.Instance smi) {
var chore = (Chore) smi.GetMaster();
var goToStack = (Stack<StateMachine.BaseState>) smi.GetType().GetField("gotoStack").GetValue(smi);
var goToStack = GetGoToStack(smi);
var newState = goToStack.FirstOrDefault();

OnExitMoveTo?.Invoke(new MoveToArgs(chore, newState.name, 0, null!));
Expand All @@ -210,4 +209,12 @@ private static int GetParameterIndex(StateMachine.Instance smi, string parameter
var parameterContext = smi.parameterContexts[parameterIndex];
return parameterContext.GetType().GetField("value").GetValue(parameterContext);
}

private static object GetStateTarget(StateMachine.Instance smi) {
return smi.stateMachine.GetFieldValue("stateTarget");
}

private static Stack<StateMachine.BaseState> GetGoToStack(StateMachine.Instance smi) {
return smi.GetFieldValue<Stack<StateMachine.BaseState>>("gotoStack");
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using MultiplayerMod.Core.Reflection;
using MultiplayerMod.Game.Chores.States;
using MultiplayerMod.Multiplayer.Objects;
using MultiplayerMod.Multiplayer.States;
Expand Down Expand Up @@ -26,7 +27,7 @@ public override void Execute(MultiplayerCommandContext context) {
smi.GoTo("root." + TargetState);

var sm = smi.stateMachine;
var target = sm.GetType().GetField("stateTarget")!.GetValue(sm);
var target = sm.GetFieldValue("stateTarget");
var navigator = (Navigator) target.GetType().GetMethod("Get")
.MakeGenericMethod(typeof(Navigator))
.Invoke(target, new object[] { smi });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using MultiplayerMod.Core.Reflection;
using MultiplayerMod.Game.Chores.States;
using MultiplayerMod.Multiplayer.Objects;
using MultiplayerMod.Multiplayer.States;
Expand All @@ -20,7 +21,7 @@ public override void Execute(MultiplayerCommandContext context) {
var smi = context.Runtime.Dependencies.Get<StatesManager>().GetSmi(chore);

var sm = smi.stateMachine;
var target = sm.GetType().GetField("stateTarget")!.GetValue(sm);
var target = sm.GetFieldValue("stateTarget");
var navigator = (Navigator) target.GetType().GetMethod("Get")
.MakeGenericMethod(typeof(Navigator))
.Invoke(target, new object[] { smi });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Reflection;
using MultiplayerMod.Core.Reflection;
using MultiplayerMod.Multiplayer.Objects.Reference;

namespace MultiplayerMod.Multiplayer.Objects;
Expand All @@ -13,7 +13,5 @@ public static class StateMachineReferenceExtensions {
// `controller` field is defined in StateMachine<,,,>.GenericInstance. However cast is impossible due to unknown
// generic argument types. So reflection is the most handy way to get its value :(
private static StateMachineController GetStateMachineController(StateMachine.Instance instance) =>
(StateMachineController) instance.GetType()
.GetField("controller", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(instance);
instance.GetFieldValue<StateMachineController>("controller");
}
18 changes: 11 additions & 7 deletions src/MultiplayerMod/Multiplayer/States/StatesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Reflection;
using JetBrains.Annotations;
using MultiplayerMod.Core.Dependency;
using MultiplayerMod.Core.Reflection;

namespace MultiplayerMod.Multiplayer.States;

Expand All @@ -22,8 +23,8 @@ public virtual void AllowTransition(Chore chore, string? targetState, Dictionary
);
}

public virtual void AddAndTransitToWaiStateUponEnter(StateMachine.BaseState stateToBeSynced) {
var sm = (StateMachine) stateToBeSynced.GetType().GetField("sm").GetValue(stateToBeSynced);
public virtual void AddAndTransitToWaiStateUponEnter(StateMachine.BaseState state) {
var sm = GetSm(state);
InjectWaitHostState(sm);
var callbackType = typeof(StateMachine<,,,>)
.GetNestedType("State")
Expand All @@ -34,16 +35,15 @@ public virtual void AddAndTransitToWaiStateUponEnter(StateMachine.BaseState stat
BindingFlags.NonPublic | BindingFlags.Static
)!;
var callback = Delegate.CreateDelegate(callbackType, method);
stateToBeSynced.enterActions.Add(new StateMachine.Action("Transit to waiting state", callback));
state.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);

public virtual StateMachine.BaseState AddContinuationState(StateMachine.BaseState state) {
var sm = GetSm(state);
var genericType = typeof(ContinuationState<,,,>).MakeGenericType(
sm.GetType().BaseType.GetGenericArguments().Append(typeof(object))
);
return (StateMachine.BaseState) Activator.CreateInstance(genericType, sm, stateToBeSynced);
return (StateMachine.BaseState) Activator.CreateInstance(genericType, sm, state);
}

public void InjectWaitHostState(StateMachine sm) {
Expand All @@ -62,6 +62,10 @@ public StateMachine.Instance GetSmi(Chore chore) {
.GetValue(chore);
}

private static StateMachine GetSm(StateMachine.BaseState state) {
return state.GetFieldValue<StateMachine>("sm");
}

private static StateMachine.BaseState GetWaitHostState(StateMachine.Instance smi) =>
smi.stateMachine.GetState("root." + WaitStateName);

Expand Down

0 comments on commit 3c3e03f

Please sign in to comment.