
本节内容为进阶内容,需要读者深入了解.Net,尤其是其中的IL Code和栈等相关知识,不会这些知识的可以忽略本节教程,使用之前教程的方法已经足够用了。
在dnSpy中,对着目标函数的目标行右键,选择编辑IL指令即可打开IL界面,当前行的IL代码会高亮。

编辑IL指令

IL界面
在图中我们已经看到了当前函数的IL代码,在Harmony中,我们只需要使用Transpiler即可对此函数的IL进行改动。
Transpiler的输入是IL代码,输出也是IL代码,我们只要在中间做修改即可。
最基本的Transpiler如下所示,这里没有任何修改直接返回了:

基本Transpiler
IL代码以IEnumerable的形式传入补丁函数,形参名必须为instructions,和前文一样,这是强制规定的。
我们将传入的IEnumerable转换为List,方便我们对它进行修改,大多数情况下,只需要修改opcode、operand,其他的用的较少,修改之后,再转换为IEnumerable进行返回。

修改IL代码
图中的30对应的是dnSpy中的序号,直接填目标行的序号就行。
对于不想要的行,我们可以直接操作List进行删除。另外也可以创建自己需要的IL代码插入到List中,不过你需要自己保证他们可以正常工作。通常情况下,你需要了解各种opcode的意义并以栈的结构压入,或者也可以通过反编译自己写的函数来查看IL再进行填写。
本文我们以《觅长生》作为范例,在这款游戏中,玩家的五行灵根按比例划分,每种灵根的比例 = 灵根权重/5种灵根权重总和。在游戏原本的逻辑中,每个灵根的初始权重为10,主灵根的权重为20,比如1灵根就是20/10/10/10/10,也就是主灵根占33.3%,其他灵根各占16.6%,5灵根的话就是各占20%。
用dnSpy查看游戏dll得到其灵根随机算法如下图:

《觅长生》灵根算法
我们今天的目标,就是写一个Transpiler,提高主灵根权重到22,降低其他灵根权重到8。也就是提高主灵根的占比。
在dnSpy中右键编辑IL指令,我们可以看到目标10的序号是6,目标20的序号是48。

目标的IL
我们直接根据序号修改目标的操作符。

修改IL
这里有一点需要注意,在整形数的赋值中,0-8是有单独的操作码的,假设目标的数字原本是3,那它的操作码会是ldc.i4.3,如果我们想把这个数字改成超过8的数字,比如12,那应该这样写
codes[6].opcode = OpCodes.Ldc_I4_S;
codes[6].operand = 12; 编译插件运行游戏测试,我们可以看到天灵根中主灵根的比例显示为40%,即22/(22+8*4)=0.407

游戏中的结果
在上一个示例中,我们提高了主灵根的比例,不过我们只是进行了最简单的IL修改操作,接下来我们稍微变化一下提高一点难度。我们这次的目标是让灵根不是分配固定数值,而是分别在10和20的左右3个范围内进行随机,也就是主灵根17-23随机,其他灵根7-13随机。
错误示范:可能会有人想到先使用Random随机一个数字,然后填入operand中,但是这样是错误的。
//错误示范
var roll = Random.Range(7, 14); //生成随机数
codes[6].operand = roll; //赋值 错误的原因是,Transpiler本身只执行一次,仅仅是进行方法体的修改,也就是说,在游戏运行后这个roll就已经确定并赋值了进去,并且不会再改变。
我们正确的方法是,通过IL来调用我们的函数。另外,因为我们要加入新的IL行,所以会导致索引变化,为了方便和安全,我们不再直接通过索引来修改IL。我们可以在IEnumerable迭代的过程中进行插入。
//获取随机函数
var methodInfo = AccessTools.Method(typeof(Random), nameof(Random.Range), new System.Type[] { typeof(int), typeof(int)});
var codes = instructions.ToList();
foreach(var code in codes)
{
if (code.opcode == OpCodes.Ldc_I4_S) //当迭代到OpCodes.Ldc_I4_S时判断operand
{
if ((sbyte)code.operand == 10) //注意,这里operand的类型为sbyte,不是int
{
yield return new CodeInstruction(OpCodes.Ldc_I4_7); //压入7
yield return new CodeInstruction(OpCodes.Ldc_I4_S, 14); //压入14
yield return new CodeInstruction(OpCodes.Call, methodInfo); //压入随机函数
}
if ((sbyte)code.operand == 20)
{
yield return new CodeInstruction(OpCodes.Ldc_I4_S, 17);
yield return new CodeInstruction(OpCodes.Ldc_I4_S, 24);
yield return new CodeInstruction(OpCodes.Call, methodInfo);
}
}
else
{
yield return code;
}
} 编译插件运行游戏测试,我们可以看到灵根不再是固定的比例,而是进行了变化。

随机的灵根
对于非常少量的操作,可以使用索引的方式进行IL操作,但是一旦数据稍微多一点,都建议使用yield return的方式,这样可以不用在意我们修改了第1个IL目标后,第2个目标到底在多少行 的问题。
这一节的内容大部分情况下都不会用到,因为不使用IL也能完成大部分我们想要的功能,在能用其他方法完成的情况下,我也不建议使用IL。只有在其他方法比较麻烦而IL比较简单的时候才推荐。
另外,对于基础不好的读者可能不太好理解IL,可以去网上查查相关知识再回来看。