An IL hook edits a method’s instruction list while MonoMod builds a detour. Use one when the required insertion point is inside the method and the method’s arguments, fields, and return value do not expose that point.
For example, On.Player.Update can run code before or after the whole update. It cannot distinguish two calls to the same helper inside the original body. IL.Player.Update can target one of those calls by matching the surrounding instructions. Hook lifecycle covers subscription timing and removal. The choice here is about the location of the change.
| Requirement | Surface |
|---|---|
Read final player state after Player.Update | On.Player.Update after orig |
| Adjust an argument before the whole method runs | On.Player.Update before orig |
| Change values at one internal call site | IL.Player.Update |
| Replace the method’s overall result | Usually On, when its generated signature exposes the result |
Start with On when either surface can express the same behavior. The continuation and C# control flow remain visible in a review. An IL patch takes on additional obligations: finding the intended instructions, preserving the evaluation stack, and handling a changed method body.
HookGen’s IL surface
In the hook assembly for build 22785462, IL.Player.Update is an event whose handlers are MonoMod.Cil.ILContext.Manipulator delegates. Adding a handler routes through HookEndpointManager.Modify. The manipulator runs when the detour is prepared. It does not run once for every player update.
ILContext supplies the method body. ILCursor navigates and emits instructions. The installed MonoMod.Utils exposes TryGotoNext, GotoNext, Index, Emit, and EmitDelegate<T>.
This API fragment inserts an empty delegate before a return. It belongs inside a plugin with a logger and the System and MonoMod.Cil namespaces imported:
private static void Player_UpdateIL(ILContext il)
{
ILCursor cursor = new ILCursor(il);
if (!cursor.TryGotoNext(MoveType.Before, instruction => instruction.MatchRet()))
{
log.LogError("Player.Update IL patch skipped: no return instruction matched.");
return;
}
cursor.EmitDelegate<Action>(RecordPatchedReturn);
}
private static void RecordPatchedReturn()
{
}Starting from a new cursor, this example selects the first matching ret. Any later returns remain unmodified. The checked match and skip path demonstrate the API, but MatchRet() alone is too broad for an operational patch. Use it to learn the cursor operation, then replace it with a sequence that identifies the behavior being changed.
Match a behavior
A useful matcher names a called member and enough nearby loads or stores to identify its role. A lone ret, ldfld, or callvirt often appears more than once. MonoMod matching helpers such as MatchCall(...) and MatchRet() express member and opcode intent without relying on instruction offsets that move after compilation.
Before publishing a matcher, record:
- The complete signature of each member used by the predicates.
- The expected nearby instruction sequence and insertion direction.
- The number of matching candidates in the inspected method.
- The rule used to select one candidate when several are valid.
Use TryGotoNext when a changed target should disable one feature and leave the rest of the plugin available. Log the target method, game build, expected pattern, and skip result. GotoNext throws on a miss. That can shorten local development when the patch is required for the test, but it changes an ordinary compatibility miss into an initialization error.
Preserve the evaluation stack
At each cursor position, list the values currently on the evaluation stack and the values expected by the next original instruction. EmitDelegate<T> consumes its parameters from that stack and pushes its return value. The zero argument Action above has a stack effect of 0 -> 0. It leaves any preexisting stack values unchanged, so it has no general empty stack requirement. Other delegate signatures must consume and produce the exact values required by the surrounding instructions. Every insertion must also preserve branch targets and control flow.
Branch targets, exception regions, and multiple returns make this accounting more involved. Avoid deleting instructions or moving labels until the exact change has run on the target Mono runtime. An InvalidProgramException during method execution points toward invalid emitted IL. A TypeLoadException or missing reference error points toward the assembly set or a signature mismatch.
Checks still required in game
The API names and signatures were checked in the recorded Rain World, HookGen, MonoMod.Utils, and Mono.Cecil assemblies. No patched method was executed in the game.
Test the real matcher first with only its plugin enabled. Record whether it matched, then trigger the target method. Repeat with each known IL mod added separately. Preserve the full exception and identify whether failure occurred during plugin loading, detour application, or method execution. Debugging and performance gives the report fields and controlled comparison. Runtime reference identifies the assemblies used for this baseline.
Sources
- MonoMod
ILHooksource at commit89825332 - MonoMod HookGen usage at commit
89825332 IL.Player.Update,MonoMod.Cil.ILContext, andMonoMod.Cil.ILCursorin the assemblies recorded by Runtime reference.