跳转至

匹配结果与捕获

唯一匹配

var damage = method.Match(damagePattern).Single();

damage.Transform(Hooks.ClampDamage)
      .Apply(VerifyOptions.Full);

Single() 只接受恰好一个结果:

  • 没找到:方法选错了、游戏更新了,或者 Pattern 和实际代码对不上;
  • 找到多处:Pattern 还不够具体;
  • 恰好一处:返回可改写的目标。

调试时可以先列出所有候选位置:

var candidates = method.Match(damagePattern);

foreach (var candidate in candidates)
    Console.WriteLine($"IL_{candidate.FirstInstruction.Offset:X4}");

匹配失败时,结果集里还带着诊断信息,解释为什么某个位置被跳过:

var candidates = method.Match(damagePattern);

if (candidates.Count != 1)
    Console.WriteLine(candidates.ExplainFailure());

诊断的存在不代表失败——它只在结果不符合预期时用来解释原因。

捕获内部的一段

根匹配可以直接改写。要改的是某个参数时,lambda 参数本身就是捕获,按参数名取回:

var pattern = Cil.Value((int baseDamage, int bonus) => baseDamage + bonus);

var match = method.Match(pattern).Single();

match.Arg("baseDamage").Transform(Hooks.ClampBaseDamage)
                       .Apply(VerifyOptions.Full);

要改的是复合子表达式时,把那一段声明成独立的 Cil.Value/Cil.Condition 片段直接写进表达式;局部变量用 Cil.Local。这些捕获按对象取回,返回类型由声明的对象决定:

索引 得到的内容
match.Arg("参数名") / match.This() lambda 参数对应的实参(ArgumentCapture
match.Local("参数名") CilLocal<T> 类型的 lambda 参数(LocalCapture
match[Cil.Any<T>() 对象] 一个可读取或改写的值(ValueCapture
match[Cil.Arg<T>() / Cil.This<T>() 对象] 捕获的参数(ArgumentCapture
match[Cil.Local<T>() 对象] 捕获的局部变量(LocalCapture
match[内嵌的 ValuePattern] 被选中的值片段(ValueCapture
match[内嵌的 ConditionPattern] 大条件中的一个子条件(ConditionCapture

拿错对象会立即抛 KeyNotFoundException——捕获身份就是对象身份,没有字符串名可写错。

编译器临时变量

Debug 和 Release 编译结果经常不同。同一段源码,某个中间值可能直接留在栈上,也可能先存进一个临时变量:

public static int DoubleDamage(int damage)
{
    var adjusted = damage + 1;
    return adjusted * 2;
}

MonoWeaver 默认会跟随来源唯一的临时变量,因此同一个 Pattern 往往能同时兼容两种编译结果:

var adjusted = Cil.Value((int damage) => damage + 1);

var pattern = Cil.Value(() => adjusted * 2);

它保持保守。如果一个读取位置可能来自多次赋值,或者变量被取过地址,它不会猜——这个位置就不会成为匹配。

明确指定来源

var tmp = Cil.Local(Cil.Value((int damage) => damage + 1));
var pattern = Cil.Value(() => tmp * 2);

var local = method.Match(pattern).Single()[tmp];

这表示 tmp * 2 中的 tmp 必须来自 damage + 1tmp 对象之后可以在多个 pattern 里复用,含义不变。

完全关闭跟随

var options = new PatternOptions
{
    TemporaryNormalization = TemporaryNormalization.None
};

var pattern = Cil.Value(
    () => P.Local<int>(0),
    options);

关闭后按局部变量读取本身来匹配。局部变量序号会随游戏版本和编译方式改变,一般不如写出它周围的计算稳定。

在某处之后匹配

结果集可以按 IL 位置筛选:After(x) 只留起点在 x 之后的匹配,Before(x) 只留终点在 x 之前的,Between(a, b) 两者都要。x 可以是一条指令,也可以是之前的匹配或捕获(以它覆盖的整段为界):

var sound = method.Match(Cil.Effect((Player player) => GameAudio.Play(player.HitSound))).Single();

// 只在播放音效之后找伤害计算;仍然要求唯一
var damage = method.Match(Cil.Value((int baseDamage, int bonus) => baseDamage + bonus))
    .After(sound)
    .Single();

这是 ILCursor "从这里往后找下一个"的对应写法,区别是 Single() 仍然要求筛选后唯一,不会静默取第一个。位置按 IL 顺序比较,不考虑控制流;锚点必须在当前方法体里——方法改写后旧锚点若已被删除会直接报错,重新匹配即可。

进阶:匹配位置

内置的改写操作已经会选择合适的插入点。只有做日志、调试器或自定义改写时才需要这些属性:

属性 可理解为
FirstInstruction 当前匹配从哪里开始
ResultInstruction 当前这次取值在哪里完成
DefinitionFirstInstruction 原值计算从哪里开始
DefinitionInstruction 原值最初在哪里产生
ConsumerInstruction 能确定时,下一步在哪里使用这个值
LastInstruction(行为/条件) 当前匹配覆盖到哪里结束
IsAddressBacked 该位置是否经由取地址指令到达

存在临时变量时,“原值产生位置”和“当前读取位置”会不同。

手动改完指令后要重新匹配

直接用 Cecil 改动方法体之后,之前拿到的匹配应视为过期。