Using Harmony to patch the real content of an async method for a Unity game

441 views Asked by At

Using dotPeek, I was able to see a method that I want to change a part of, specifically the 4 in CreateLobbyAsync to another integer.

public async void StartHost()
  {
    if (!(bool) (UnityEngine.Object) UnityEngine.Object.FindObjectOfType<MenuManager>())
    {
      Debug.Log((object) "Menu manager script is not present in scene; unable to start host");
    }
    else
    {
      if (GameNetworkManager.Instance.currentLobby.HasValue)
      {
        Debug.Log((object) "Tried starting host but currentLobby is not null! This should not happen. Leaving currentLobby and setting null.");
        this.LeaveCurrentSteamLobby();
      }
      if (!this.disableSteam)
      {
        GameNetworkManager gameNetworkManager = GameNetworkManager.Instance;
        gameNetworkManager.currentLobby = await SteamMatchmaking.CreateLobbyAsync(4);
        gameNetworkManager = (GameNetworkManager) null;
      }
      UnityEngine.Object.FindObjectOfType<MenuManager>().StartHosting();
      this.SubscribeToConnectionCallbacks();
      this.isHostingGame = true;
      this.connectedPlayers = 1;
    }
  }

However, The underlying IL shows that the actual functionality I want to change is not held within the accessible method, but in another automatically generated one.

Here is the method I wanted to change.

    StartHost() cil managed
  {
    .custom instance void [netstandard]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [netstandard]System.Type)
      = (
        01 00 23 47 61 6d 65 4e 65 74 77 6f 72 6b 4d 61 // ..#GameNetworkMa
        6e 61 67 65 72 2b 3c 53 74 61 72 74 48 6f 73 74 // nager+<StartHost
        3e 64 5f 5f 37 39 00 00                         // >d__79..
      )
      // type(valuetype GameNetworkManager/'<StartHost>d__79')
    .maxstack 2
    .locals init (
      [0] valuetype GameNetworkManager/'<StartHost>d__79' V_0
    )

    IL_0000: ldloca.s     V_0
    IL_0002: call         valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
    IL_0007: stfld        valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder GameNetworkManager/'<StartHost>d__79'::'<>t__builder'
    IL_000c: ldloca.s     V_0
    IL_000e: ldarg.0      // this
    IL_000f: stfld        class GameNetworkManager GameNetworkManager/'<StartHost>d__79'::'<>4__this'
    IL_0014: ldloca.s     V_0
    IL_0016: ldc.i4.m1
    IL_0017: stfld        int32 GameNetworkManager/'<StartHost>d__79'::'<>1__state'
    IL_001c: ldloca.s     V_0
    IL_001e: ldflda       valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder GameNetworkManager/'<StartHost>d__79'::'<>t__builder'
    IL_0023: ldloca.s     V_0
    IL_0025: call         instance void [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype GameNetworkManager/'<StartHost>d__79'>(!!0/*valuetype GameNetworkManager/'<StartHost>d__79'*/&)
    IL_002a: ret

  } // end of method GameNetworkManager::StartHost

Here is the Referenced real functionality that I have been unable to access

.class nested private sealed auto ansi beforefieldinit
    '<StartHost>d__79'
      extends [netstandard]System.ValueType
      implements [netstandard]System.Runtime.CompilerServices.IAsyncStateMachine
  {
    .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public int32 '<>1__state'

    .field public valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder '<>t__builder'

    .field public class GameNetworkManager '<>4__this'

    .field private class GameNetworkManager '<>7__wrap1'

    .field private valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>> '<>u__1'

    .method private final hidebysig virtual newslot instance void
      MoveNext() cil managed
    {
      .override method instance void [netstandard]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
      .maxstack 3
      .locals init (
        [0] int32 V_0,
        [1] class GameNetworkManager V_1,
        [2] valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby> V_2,
        [3] valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>> V_3,
        [4] class [netstandard]System.Exception V_4
      )

      IL_0000: ldarg.0      // this
      IL_0001: ldfld        int32 GameNetworkManager/'<StartHost>d__79'::'<>1__state'
      IL_0006: stloc.0      // V_0
      IL_0007: ldarg.0      // this
      IL_0008: ldfld        class GameNetworkManager GameNetworkManager/'<StartHost>d__79'::'<>4__this'
      IL_000d: stloc.1      // V_1
      .try
      {
        IL_000e: ldloc.0      // V_0
        IL_000f: brfalse      IL_009e

        // [654 5 - 654 89]
        IL_0014: call         !!0/*class MenuManager*/ [UnityEngine.CoreModule]UnityEngine.Object::FindObjectOfType<class MenuManager>()
        IL_0019: call         bool [UnityEngine.CoreModule]UnityEngine.Object::op_Implicit(class [UnityEngine.CoreModule]UnityEngine.Object)
        IL_001e: brtrue.s     IL_002f

        // [656 7 - 656 94]
        IL_0020: ldstr        "Menu manager script is not present in scene; unable to start host"
        IL_0025: call         void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)

        IL_002a: leave        IL_010e

        // [660 7 - 660 61]
        IL_002f: call         class GameNetworkManager GameNetworkManager::get_Instance()
        IL_0034: callvirt     instance valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby> GameNetworkManager::get_currentLobby()
        IL_0039: stloc.2      // V_2

        IL_003a: ldloca.s     V_2
        IL_003c: call         instance bool valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>::get_HasValue()
        IL_0041: brfalse.s    IL_0053

        // [662 9 - 662 143]
        IL_0043: ldstr        "Tried starting host but currentLobby is not null! This should not happen. Leaving currentLobby and setting null."
        IL_0048: call         void [UnityEngine.CoreModule]UnityEngine.Debug::Log(object)

        // [663 9 - 663 38]
        IL_004d: ldloc.1      // V_1
        IL_004e: call         instance void GameNetworkManager::LeaveCurrentSteamLobby()

        // [665 7 - 665 30]
        IL_0053: ldloc.1      // V_1
        IL_0054: ldfld        bool GameNetworkManager::disableSteam
        IL_0059: brtrue.s     IL_00d5

        // [667 9 - 667 76]
        IL_005b: ldarg.0      // this
        IL_005c: call         class GameNetworkManager GameNetworkManager::get_Instance()
        IL_0061: stfld        class GameNetworkManager GameNetworkManager/'<StartHost>d__79'::'<>7__wrap1'

        // [668 9 - 668 85]
        IL_0066: ldc.i4.4
        IL_0067: call         class [netstandard]System.Threading.Tasks.Task`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>> [Facepunch.Steamworks.Win64]Steamworks.SteamMatchmaking::CreateLobbyAsync(int32)
        IL_006c: callvirt     instance valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<!0/*valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>*/> class [netstandard]System.Threading.Tasks.Task`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>>::GetAwaiter()
        IL_0071: stloc.3      // V_3

        IL_0072: ldloca.s     V_3
        IL_0074: call         instance bool valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>>::get_IsCompleted()
        IL_0079: brtrue.s     IL_00ba
        IL_007b: ldarg.0      // this
        IL_007c: ldc.i4.0
        IL_007d: dup
        IL_007e: stloc.0      // V_0
        IL_007f: stfld        int32 GameNetworkManager/'<StartHost>d__79'::'<>1__state'
        IL_0084: ldarg.0      // this
        IL_0085: ldloc.3      // V_3
        IL_0086: stfld        valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>> GameNetworkManager/'<StartHost>d__79'::'<>u__1'
        IL_008b: ldarg.0      // this
        IL_008c: ldflda       valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder GameNetworkManager/'<StartHost>d__79'::'<>t__builder'
        IL_0091: ldloca.s     V_3
        IL_0093: ldarg.0      // this
        IL_0094: call         instance void [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted<valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>>, valuetype GameNetworkManager/'<StartHost>d__79'>(!!0/*valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>>*/&, !!1/*valuetype GameNetworkManager/'<StartHost>d__79'*/&)
        IL_0099: leave        IL_0121
        IL_009e: ldarg.0      // this
        IL_009f: ldfld        valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>> GameNetworkManager/'<StartHost>d__79'::'<>u__1'
        IL_00a4: stloc.3      // V_3
        IL_00a5: ldarg.0      // this
        IL_00a6: ldflda       valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>> GameNetworkManager/'<StartHost>d__79'::'<>u__1'
        IL_00ab: initobj      valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>>
        IL_00b1: ldarg.0      // this
        IL_00b2: ldc.i4.m1
        IL_00b3: dup
        IL_00b4: stloc.0      // V_0
        IL_00b5: stfld        int32 GameNetworkManager/'<StartHost>d__79'::'<>1__state'
        IL_00ba: ldloca.s     V_3
        IL_00bc: call         instance !0/*valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>*/ valuetype [netstandard]System.Runtime.CompilerServices.TaskAwaiter`1<valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>>::GetResult()
        IL_00c1: stloc.2      // V_2
        IL_00c2: ldarg.0      // this
        IL_00c3: ldfld        class GameNetworkManager GameNetworkManager/'<StartHost>d__79'::'<>7__wrap1'
        IL_00c8: ldloc.2      // V_2
        IL_00c9: callvirt     instance void GameNetworkManager::set_currentLobby(valuetype [netstandard]System.Nullable`1<valuetype [Facepunch.Steamworks.Win64]Steamworks.Data.Lobby>)

        // [669 9 - 669 55]
        IL_00ce: ldarg.0      // this
        IL_00cf: ldnull
        IL_00d0: stfld        class GameNetworkManager GameNetworkManager/'<StartHost>d__79'::'<>7__wrap1'

        // [671 7 - 671 72]
        IL_00d5: call         !!0/*class MenuManager*/ [UnityEngine.CoreModule]UnityEngine.Object::FindObjectOfType<class MenuManager>()
        IL_00da: callvirt     instance void MenuManager::StartHosting()

        // [672 7 - 672 44]
        IL_00df: ldloc.1      // V_1
        IL_00e0: call         instance void GameNetworkManager::SubscribeToConnectionCallbacks()

        // [673 7 - 673 32]
        IL_00e5: ldloc.1      // V_1
        IL_00e6: ldc.i4.1
        IL_00e7: stfld        bool GameNetworkManager::isHostingGame

        // [674 7 - 674 32]
        IL_00ec: ldloc.1      // V_1
        IL_00ed: ldc.i4.1
        IL_00ee: stfld        int32 GameNetworkManager::connectedPlayers

        IL_00f3: leave.s      IL_010e
      } // end of .try
      catch [netstandard]System.Exception
      {
        IL_00f5: stloc.s      V_4
        IL_00f7: ldarg.0      // this
        IL_00f8: ldc.i4.s     -2 // 0xfe
        IL_00fa: stfld        int32 GameNetworkManager/'<StartHost>d__79'::'<>1__state'
        IL_00ff: ldarg.0      // this
        IL_0100: ldflda       valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder GameNetworkManager/'<StartHost>d__79'::'<>t__builder'
        IL_0105: ldloc.s      V_4
        IL_0107: call         instance void [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetException(class [netstandard]System.Exception)
        IL_010c: leave.s      IL_0121
      } // end of catch
      IL_010e: ldarg.0      // this
      IL_010f: ldc.i4.s     -2 // 0xfe
      IL_0111: stfld        int32 GameNetworkManager/'<StartHost>d__79'::'<>1__state'
      IL_0116: ldarg.0      // this
      IL_0117: ldflda       valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder GameNetworkManager/'<StartHost>d__79'::'<>t__builder'
      IL_011c: call         instance void [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetResult()
      IL_0121: ret

    } // end of method '<StartHost>d__79'::MoveNext

    .method private final hidebysig virtual newslot instance void
      SetStateMachine(
        class [netstandard]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
      ) cil managed
    {
      .custom instance void [netstandard]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance void [netstandard]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [netstandard]System.Runtime.CompilerServices.IAsyncStateMachine)
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: ldflda       valuetype [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder GameNetworkManager/'<StartHost>d__79'::'<>t__builder'
      IL_0006: ldarg.1      // stateMachine
      IL_0007: call         instance void [netstandard]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetStateMachine(class [netstandard]System.Runtime.CompilerServices.IAsyncStateMachine)
      IL_000c: ret

    } // end of method '<StartHost>d__79'::SetStateMachine
  } // end of class '<StartHost>d__79'

I have been sofar unable to access the parts I actually want to change, the integer held within d__79 because I cannot access the automatically generated method using Harmony.

What I have so far, the patcher works and returns the original accessible but useless Method.

When I added MethodType.Enumerator to the HarmonyPatch(), it made it simply return a "The given key was not present in the dictionary." error. I assume because I am trying to access the GetNext of StartHost instead of d__79.

[HarmonyTranspiler]
[HarmonyPatch(typeof(GameNetworkManager), nameof(GameNetworkManager.StartHost)]
static IEnumerable<CodeInstruction> TranspileMoveNext(IEnumerable<CodeInstruction> instr)
{
    var codes = new List<CodeInstruction>(instr);
    for (int i = 0; i < codes.Count; i++)
    {
        Plugin.mls.LogError(codes[i].ToString());
        if (codes[i].opcode == OpCodes.Ldc_I4_4)
        {
            //Plugin.mls.LogError(codes[i+1].ToString());
            if (codes[i - 1].ToString() == "call int Steamworks.Data.Lobby::get_MemberCount()")
            {
                codes[i].opcode = OpCodes.Ldc_I4_8;
            }
        }
    }
    return codes.AsEnumerable();
}

Do I have any options here or is this a lost cause?

1

There are 1 answers

0
zonni On

I've stumbled upon this issue, and during my research, I read about your troubles. So, I think I'll leave a mark here after solving the issue. You can find the real implementation on my Github, as I am creating a mod for a game I like.

The thing about async void is that it uses a state machine to achieve asynchronous behavior, so you need to patch the state machine object.

But, let's understand it from the beginning. Let's create a simple class:

public class Test
{
    public async void AsyncVoid()
    {
        var foo = "bar";
    }
}

Now, let's review the cleaned IL of this class

.class public auto ansi beforefieldinit
  CSharp.Test extends [System.Runtime]System.Object
{

  .class nested private sealed auto ansi beforefieldinit
    '<AsyncVoid>d__0'
      extends [System.Runtime]System.Object
      implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  {
    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    { ... }

    .method private final hidebysig virtual newslot instance void
      MoveNext() cil managed
    { ... }

    .method private final hidebysig virtual newslot instance void
      SetStateMachine(
        class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
      ) cil managed
    { ... }
  }

  .method public hidebysig instance void
    AsyncVoid() cil managed
  { ... }

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  { ... }
}

By analyzing this code, you can determine that this method creates a hidden class that implements the IAsyncStateMachine interface. You should be able to see MoveNext and SetStateMachine methods. Let's jump to the documentation of IAsyncStateMachine to understand what these methods do.

Method Description
MoveNext() Moves the state machine to its next state.
SetStateMachine(IAsyncStateMachine) Configures the state machine with a heap-allocated replica.

What's left is to patch the method we need.

var targetMethod = typeof(Test).GetMethod("AsyncVoid", BindingFlags.Instance | BindingFlags.Public);
var stateMachineAttr = targetMethod.GetCustomAttribute<AsyncStateMachineAttribute>();
var moveNextMethod = stateMachineAttr.StateMachineType.GetMethod("MoveNext", BindingFlags.NonPublic | BindingFlags.Instance);

harmony.Patch(moveNextMethod, transpiler: harmonyTranspilerMethod, prefix: harmonyPrefixMethod);

VoilĂ ! Now you need to deal with handling async IL within the state machine