No Match, or Too Many¶
Read the diagnostics first¶
When matching fails, the result set carries an explanation:
var candidates = method.Match(damagePattern);
if (candidates.Count != 1)
Console.WriteLine(candidates.ExplainFailure());
It reports which IL in the method could not be modelled, which temporary-local passthrough was rejected, and why a local definition constraint (Cil.Local(definedBy)) was not satisfied. Read it before changing the pattern.
Nothing matched¶
Check in this order:
- Right match kind? Value, effect, and condition are three different things — see The Three Match Kinds. The test in
if (a && b)is a condition; the one inreturn a && b;is a value. - Right method? Check the
FullNamespelling, the overload, and the+separator for nested types. - Did the game update? Confirm the expression is still there and the argument order did not change.
- Right parameter name? A pattern-lambda parameter must have exactly the same name as the target parameter in metadata.
- Right constants and overloads?
Select("rare")will not matchSelect(1);1and1Lare different constants. - Right operand order?
a - bis notb - a, and the same goes for comparisons. - Did you capture a runtime object in the lambda? An outer variable becomes a closure field read and will never match a game value. Use a pattern-lambda parameter, a DSL placeholder, a literal, or a static field.
- Is a temporary in the way? Temporaries with an unambiguous definition are followed by default, but an ambiguous origin is never guessed. Pin the origin with
Cil.Local(definedBy)— see compiler temporaries.
Too many matched¶
Multiple matches are more dangerous than none: the pattern is not describing a unique piece of logic.
Do not take the first one. Add context instead:
- fold in the enclosing call: look for
GetScore() + 10, not justGetScore(); - fold in constants:
Level * 100is more specific thanLevel * x(wherexis aCil.Any<int>()); - fold in a field or property;
- declare the small part you actually want to change as an embedded fragment, and let the outer expression do the locating.
var score = Cil.Value((Player player) => player.GetScore());
var pattern = Cil.Value(() => score + 10);
This pattern only matches the call that feeds + 10, and the score mark then points at the inner GetScore().
If you really do need to edit several places, enumerate them explicitly and decide per candidate rather than relying on order:
var candidates = method.Match(damagePattern);
foreach (var candidate in candidates)
Console.WriteLine($"IL_{candidate.FirstInstruction.Offset:X4}");
Type scope too wide¶
The symbol form is exact by default, and Assignable() widens it to subclasses. When you get multiple matches, check whether Assignable() was applied more broadly than intended:
var game = CilSymbols.In("GameAssembly");
var enemy = game.Type("Game.Enemy");
var exact = P.Arg(0, enemy);
var allowDerived = P.Arg(0, enemy.Assignable());
Stale matches¶
Once a method is rewritten, previously obtained positions may no longer be valid.
- a
RewritePlancan only be applied successfully once; - to keep editing the same method,
Matchagain; - the same applies after any manual Cecil edit to the method body.
Quick reference¶
| Symptom | Check first |
|---|---|
No matching expression was found |
The method, whether the game updated, constants and overloads |
| More than one result | Add enclosing calls, fields, constants, or an embedded-fragment context |
| Wrong callback argument count | Transform/Observe already supply the original value as the first argument |
| Wrong callback return type | Transform must return something that can replace the original value; a condition must return bool |
match is stale |
Another edit changed the method — Match again |
| The check fails after applying | See Verification Failures |