Rewrite Operations¶
Everything below is pick one. Do not chain several of them onto the same old match. After each Apply, Match again if you want to keep editing.
Value¶
Transform: read the old value, return a new one¶
damage.Transform(Hooks.ClampDamage)
.Apply(VerifyOptions.Full);
// static int ClampDamage(int original)
The original value arrives as the callback's first parameter, and the return value goes back to the game's own logic. The return type has to fit where the original value went.
Observe: look, do not change¶
For logging, statistics, and raising mod events. The original value still reaches the game. If the callback returns something, it is discarded by default.
Replace: skip the original computation¶
The original expression is not executed, and the callback does not automatically receive the old value. Every argument it needs must come from args.
Before / After: add behaviour around it¶
damage.Before(Hooks.OnDamageCalculationStarted)
.Apply(VerifyOptions.Full);
damage.After(Hooks.OnDamageCalculated)
.Apply(VerifyOptions.Full);
After only means "call once the computation is finished". It does not pass the original value to the callback — use Observe for that.
Effect¶
var effect = method.Match(soundPattern).Single();
effect.Before(Hooks.BeforeSound)
.Apply(VerifyOptions.Full);
effect.After(Hooks.AfterSound)
.Apply(VerifyOptions.Full);
effect.Replace(Hooks.PlayCustomSound)
.Apply(VerifyOptions.Full);
effect.Remove()
.Apply(VerifyOptions.Full);
Four independent choices: insert before, insert after, replace wholesale, delete.
Condition¶
Conditions also support Before, but not After: a condition can finish at different places, so there is no single "line after it".
Check first that a rewrite is possible at all:
What a callback can be¶
| Source | Good for |
|---|---|
A strongly typed delegate such as Func<int, int> |
Runtime mods |
CilMethodSpec |
Describing a static callback by signature |
A Cecil MethodReference |
Offline patchers that already hold a method reference |
A static delegate is lowered to a direct call to that static method. Instance delegates, closures, and multicast delegates are runtime-only.
Pass the method group directly: damage.Transform(Hooks.ClampDamage). Only on C# 9 and below (a net48 project without LangVersion defaults to 7.3) does a callback on a capture need an explicit delegate type, e.g. match.Arg("x").Transform((Func<int, int>)Hooks.ClampDamage) — method groups had no natural type before C# 10. A root ValueMatch<T> never needs it, because T is already known.
Extra callback arguments¶
Transform and Observe put the original value in the first parameter automatically. The rest come from args => ..., in order:
damage.Transform(
Hooks.AdjustDamage,
args => args
.Arg(0)
.Constant(999))
.Apply(VerifyOptions.Full);
// static int AdjustDamage(int original, int firstMethodArg, int limit)
| Call | What the callback receives |
|---|---|
args.This() |
The current object |
args.Arg(0) |
The target method's first explicit parameter |
args.Arg(argumentCapture) |
A captured parameter |
args.Local(localCapture) |
The current value of a captured local |
args.Capture(valueCapture) |
The value of a captured expression |
args.Constant(100) |
A constant; overloads exist for every primitive |
args.ConstantI4(value, nominalType) |
An integer constant passed under a specific nominal type |
args.Null(type) |
A null reference |
MonoWeaver checks argument count, order, and types. If a captured value is not yet available at the callback site, it refuses before touching the method.
Where the callback result goes¶
See The Rewrite Plan. The short version:
Transformand valueReplacereturn values go straight back to the game; nothing to configure;- a non-
voidObserveresult is discarded by default; - a non-
voidBefore/Afterresult is left on the stack by default, so a normal hook should callDiscard()orStore(...)explicitly.