Skip to content

Enumerator patches

ManlyMarco edited this page Jan 11, 2024 · 4 revisions

Sometimes you might need to patch enumerators like these

IEnumerator<int> SomeEnumerator()
{
    yield return 0; // Return 0 and "pause" the method
    
    Console.WriteLine("Some thing");

    yield return 1;
    yield return 2;
}

These enumerators are compiled into IEnumerators which are simple state machines. For example, the previous method is roughly compiled into the following code:

IEnumerator<int> SomeEnumerator()
{
    return new SomeEnumerator_Impl();
}

class SomeEnumerator_Impl: IEnumerator<int>
{
    int state;
    int current;

    public int Current => current;

    public bool MoveNext()
    {
        switch (state)
        {
           case 0:
               state = 1;
               current = 0;
               return true;
           case 1:
               Console.WriteLine("Some thing");
               state = 2;
               current = 1;
               return true;
           case 2:
               state = 3;
               current = 2;
               return true;
           case 3:
               state = -1;
               return false;
           default:
               state = -1;
               return false;
        }
    }

    public void Reset() => throw new NotImplementedException();
}

In other words, if you wanted to change the code inside the enumerator, adding a transpiler directly into SomeEnumerable will not work because all the actual code was moved to a hidden compiler-generated class. This behaviour applies to IEnumerator, IEnumerator<T>, IEnumerable and IEnumerable<T> enumerators.

Enumerators are used heavily in Unity coroutines and as such is useful to be able to patch them. While adding prefixes works with normal Harmony easily and postfixes are possible by simply wrapping the returned enumerator with your own, writing transpilers is complex. HarmonyX allows to directly get the needed MoveNext method with a fitting MethodType value.

Example syntax for patching MoveNext of enumerators

You can use a normal HarmonyPatch attribute and specify MethodType.Enmerator as the method's type:

// This will transpile MoveNext of `TargetClass.SomeEnumerator`
[HarmonyTranspiler]
[HarmonyPatch(typeof(TargetClass), "SomeEnumerator", MethodType.Enumerator)]
static IEnumerable<CodeInstruction> TranspileMoveNext(IEnumerable<CodeInstruction>);


// NOTE THE DIFFERENCE: This will transpile `TargetClass.SomeEnumerator` which only has `new SomeEnumerator_Impl()`!
[HarmonyTranspiler]
[HarmonyPatch(typeof(TargetClass), "SomeEnumerator")]
static IEnumerable<CodeInstruction> TranspileMoveNext(IEnumerable<CodeInstruction>);

If you prefer manual patching, use AccessTools.EnumeratorMoveNext(MethodBase) to obtain the MoveNext method reference:

// Get reference to SomeEnumerator
var enumeratorMethod = AccessTools.Method(typeof(TargetClass), "SomeEnumerator");
// Resolve MoveNext from the enumerator
var moveNext = AccessTools.EnumeratorMoveNext(enumeratorMethod);

Notes on targeting MoveNext

  • Enumerators are usually iterated using a foreach-loop, in which case MoveNext is run for each iteration. As such, any prefixes and postifxes on MoveNext will be run on every iteration

  • If you need to add a postfix to the end of the enumerator, it's easier to insert a postfix into SomeEnumerator and wrap the returned enumerator into your own:

    [HarmonyPostfix]
    [HarmonyPatch(typeof(TargetClass), "SomeEnumerator")]
    static IEnumerator MyWrapper(IEnumerator result)
    {
        // Run original enumerator code
        while (result.MoveNext())
            yield return result.Current;
        
        // Run your postfix
    }

    In this case the compiler will generate the correct postfix that wraps the original method's enumerator in your own.

  • When writing transpilers, it might be useful to view the automatically generated enumerators. If you're using dnSpy, you can disable decompiling yields and enable viewing compiler generated types