-
Notifications
You must be signed in to change notification settings - Fork 43
Enumerator patches
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 IEnumerator
s 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.
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);
-
Enumerators are usually iterated using a foreach-loop, in which case
MoveNext
is run for each iteration. As such, any prefixes and postifxes onMoveNext
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
- Basic usage
-
HarmonyX extensions
1.1. Patching and unpatching
1.2. Prefixes are flowthrough
1.3. Targeting multiple methods with one patch
1.4. Patching enumerators
1.5. Transpiler helpers
1.6. ILManipulators
1.7. Extended patch targets
1.8. New patch attributes -
Extending HarmonyX
2.1. Custom patcher backends -
Misc
4.1. Patch parameters - Implementation differences from Harmony