Recipes and instruction edits
Use an Infix for prefixes, postfixes, or finalizers around a selected operation. To insert, replace, or remove instruction patterns, use a transpiler with CodeMatcher.
Transpilers run in their usual order. Infix selects from their finished output, before inserting its own code. Even Priority.Last transpilers run before Infix.
These examples are compiled and tested. Probe.Tick(), Before(), After(), Replacement(), Enter(), and Exit() are static, take no arguments, and return void. Each records its name. Recipes.Method(name) finds a Probe method; Recipes.Call(name) creates its call instruction.
Scope of these examples
These recipes reject exception regions and instruction prefixes such as constrained. and tail.:
static List<CodeInstruction> SimpleBody(IEnumerable<CodeInstruction> instructions)
{
var codes = instructions.Select(instruction => new CodeInstruction(instruction)).ToList();
if (codes.Any(instruction => instruction.blocks.Count != 0 || instruction.opcode.OpCodeType == OpCodeType.Prefix))
throw new InvalidOperationException("These examples require a body without exception regions or instruction prefixes.");
return codes;
}
Handling those bodies requires preserving exception regions, branch destinations, and prefixes. Moving every exception marker onto an inserted instruction is not enough. Use Infix for supported operations if you want Harmony to handle this.
Insert before and after a match
Surround the first Tick call:
public static IEnumerable<CodeInstruction> AroundFirst(IEnumerable<CodeInstruction> instructions)
{
var matcher = new CodeMatcher(SimpleBody(instructions))
.MatchStartForward(CodeMatch.Calls(Method(nameof(Probe.Tick))))
.ThrowIfInvalid("Expected a Tick call");
var before = Call(nameof(Probe.Before)).MoveLabelsFrom(matcher.Instruction);
matcher.InsertAndAdvance(before).InsertAfter(Call(nameof(Probe.After)));
return matcher.InstructionEnumeration();
}
Moving the call's labels to Before makes incoming branches run it too. After runs only on normal return. Branches to the next instruction still bypass the pair. The inserted void, zero-argument calls leave existing stack values alone.
Replace or remove an operation
Replace the first call with one that has the same stack behavior:
public static IEnumerable<CodeInstruction> ReplaceFirst(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(SimpleBody(instructions))
.MatchStartForward(CodeMatch.Calls(Method(nameof(Probe.Tick))))
.ThrowIfInvalid("Expected a Tick call")
.Set(OpCodes.Call, Method(nameof(Probe.Replacement)))
.InstructionEnumeration();
}
Set replaces the opcode and operand but keeps labels. To remove this call, use nop:
public static IEnumerable<CodeInstruction> DeleteFirst(IEnumerable<CodeInstruction> instructions)
{
return new CodeMatcher(SimpleBody(instructions))
.MatchStartForward(CodeMatch.Calls(Method(nameof(Probe.Tick))))
.ThrowIfInvalid("Expected a Tick call")
.Set(OpCodes.Nop, null)
.InstructionEnumeration();
}
This works for the void, zero-argument Tick(). Other calls need a replacement that accounts for their arguments and result. The nop keeps the old branch destination.
Require enough matches
Reject fewer than two Tick instructions, then replace every match:
public static IEnumerable<CodeInstruction> RequireTwoCalls(IEnumerable<CodeInstruction> instructions)
{
var codes = SimpleBody(instructions);
var target = Method(nameof(Probe.Tick));
if (codes.Count(instruction => instruction.Calls(target)) < 2)
throw new InvalidOperationException("Expected at least two Tick calls");
var matcher = new CodeMatcher(codes);
while (matcher.MatchStartForward(CodeMatch.Calls(target)).IsValid)
matcher.Set(OpCodes.Call, Method(nameof(Probe.Replacement))).Advance(1);
return matcher.InstructionEnumeration();
}
This counts instructions at this transpiler's turn, not final Infix matches. A call in a loop counts once; later transpilers can change the body. Enough matches does not prove they still mean the same thing after an update.
Process at most N matches
Change the first two matches and leave the rest:
public static IEnumerable<CodeInstruction> FirstTwoCalls(IEnumerable<CodeInstruction> instructions)
{
var matcher = new CodeMatcher(SimpleBody(instructions));
for (var count = 0; count < 2 && matcher.MatchStartForward(CodeMatch.Calls(Method(nameof(Probe.Tick)))).IsValid; count++)
matcher.Set(OpCodes.Call, Method(nameof(Probe.Replacement))).Advance(1);
return matcher.InstructionEnumeration();
}
Zero or one match is accepted. To reject more than two, count all matches before changing anything.
Insert at entry and normal exits
Usually, use an ordinary prefix/postfix for entry and exit. A transpiler can do it too:
public static IEnumerable<CodeInstruction> EntryAndExit(IEnumerable<CodeInstruction> instructions)
{
var codes = SimpleBody(instructions);
var result = new List<CodeInstruction> { Call(nameof(Probe.Enter)) };
foreach (var instruction in codes)
{
if (instruction.opcode == OpCodes.Ret)
result.Add(Call(nameof(Probe.Exit)).MoveLabelsFrom(instruction));
result.Add(instruction);
}
return result;
}
The entry call leaves labels on the original first instruction, so loops back to it do not repeat entry logic. Return labels move to the exit call so incoming branches run it. The void exit call preserves any return value on the stack.
This covers normal returns only, not exceptions or exception handlers. Use an ordinary finalizer for exception completion.
Install and remove a runtime group
Give each independently removable group its own Harmony owner ID:
public const string GroupId = "example.metrics";
public static void InstallGroup()
{
var harmony = new Harmony(GroupId);
foreach (var name in new[] { nameof(Probe.OneCall), nameof(Probe.ThreeCalls) })
{
var prefix = new HarmonyMethod(Method(nameof(Probe.Group)))
{
innerMethod = new InnerMethod(Method(nameof(Probe.Tick)))
};
harmony.CreateProcessor(Method(name)).AddInnerPrefix(prefix).Patch();
}
}
public static void RemoveGroup() => new Harmony(GroupId).UnpatchAll(GroupId);
RemoveGroup removes every patch with that owner, even on other methods, and leaves other owners alone. Repeated installation adds registrations. It does not toggle or replace the group. Updates are per method; a later failure does not undo earlier installations.