跳至正文

IL基础教程① 认识IL和IL补丁编写方式

大家好啊,我是人,你们也可以叫我龙舞。这个系列教程主要讲的是IL的应用。那么首先啊我们先来认识一下IL是什么。

介绍

IL是一种肥肠之弔的技术,通过IL编辑,你就可以用mod来魔改TR源代码,而不需要用TML提供的API(或者说是Hook 钩子)。IL是中间语言(Intermediate Language)的缩写,简单来说我们就是再按照需要编辑已经编译好的代码,这对于一些奇奇怪怪的弔需求很有用。注意,在进行IL编辑的同时,请各位亲爱的带朋友小朋友确保你们的代码兼容了其他mod可能会修改的同一处内容,不然会出申必bug。IL就是如此之弔,但是能用TML的API还是尽量用吧,那玩意兼容性比IL好多了。

P.S: 微软说过CLR(Common Language Runtime,通用语言运行平台)会在运行的时候内嵌短方法(short method)这些方法不能用IL编辑,但是微软没说啥算短方法,可能Properties算是一种,但是咱们也涉及不到这玩意。

工具


  • DnSpy – 这玩意是用来设计补丁的时候反编译TR源代码的
  • DE 如 VS 等等 虽然但是我在这打波广告 快用VScode下载我的插件
  • TR源码 – 主要是修改的时候找东西用

示例 – 蜂巢背包XSProMaxPlus++

首先啊我们来把原版的蜂巢背包给魔改一哈子,原版中玩家装备蜂巢背包的时候,player类的strongBees字段会被赋值为true,而且会让生成的蜜蜂概率变成大蜜蜂,就像这样:

我们的目标是修改代码让它有一定概率生成蜜蜂手雷。

首先我们先来看看原版中的方法是怎么写的,并构思一下我们要对IL代码进行哪些更改。用DnSpy打开Terraria.exe,依次在右侧的列表里面打开Terraria,Terraria.exe,Terraria和Player的选项卡,然后向下寻找并选中 beeType() 方法。这里有一点要说一下,我们在修改的时候怎么才能找到自己想要的方法呢?答案是猜,首先我们知道,目标是修改蜂巢背包使得蜜蜂武器射出蜜蜂手雷,那么关于饰品的效果代码开发者会放在哪里?当然是人物代码里面。所以我们便锁定了 Player 类,然后源码肯定不是一个人写出来的,如果想要让其他程序员看懂代码,要么这位编写者添加了注释,要么他便是直接起了关于蜜蜂的方法名,因此我们试着寻找bee这个单词,发现有 beeType() 这个方法,那么我们便可以开始细读这个方法的内容。在确定他就是原版执行蜂巢背包功能的方法后我们便成功了——找到了原版的方法。在需要修改其他的内容时也是如此,通过英文猜测大概的方法,之后细细读它的意思。

然后我们就会看到如下画面:

public int beeType()
{
    if(this.strongBees && Main.rand.Next(2) == 0)//这里源码使用了一个rand随机数来判断是否生成大蜜蜂
    {
        this.makeStrongBee = ture;//这里给一个叫makeStrongBee的字段赋了值,可能是后续判定用
        return 566;//返回大蜜蜂的ProjectileID
    }
    this.makeStrongBee = false;
    return 181;//返回小蜜蜂的ProjectileID
}

这就是原版方法的C#代码,如果你去查projectileID的话,你会发现566是大蜜蜂,181是蜜蜂,我们的目标是往里添加蜜蜂手雷的可能性,即让方法在一定情况下返回183也就是蜜蜂手雷的ProjectileID,如下图:(引用的是官方教程的代码,别问问就是懒得自己写)

public int beeType()
{
    if (this.strongBees && Main.rand.Next(2) == 0)
    {
        this.makeStrongBee = true;
        if(this.GetModPlayer<ExamplePlayer>().strongBeesUpgrade && Main.rand.NextBool(10))
        //这个ExamplePlayer换成你自己的modPlayer类(能看这个教程的应该都知道吧)
            return ProjectileID.Beenade;
        return 566;
    }
    this.makeStrongBee = false;
    return 181;
}

这就满足了我们的需求,但是应该怎么改呢?首先我们来利用DnSpy查看这个方法的IL代码。在菜单栏里面单击下拉组合框并切换到IL语言,我们会看到这样的东西。

// Token: 0x0600050A RID: 1290 RVA: 0x0023D2D0 File Offset: 0x0023B4D0
.method public hidebysig 
    instance int32 beeType () cil managed 
{
    // Header Size: 1 byte
    // Code Size: 47 (0x2F) bytes
    .maxstack 8

    /* 0x0023B4D1 02           */ IL_0000: ldarg.0
    /* 0x0023B4D2 7B280A0004   */ IL_0001: ldfld     bool Terraria.Player::strongBees
    /* 0x0023B4D7 2C1A         */ IL_0006: brfalse.s IL_0022

    /* 0x0023B4D9 7E19040004   */ IL_0008: ldsfld    class Terraria.Utilities.UnifiedRandom Terraria.Main::rand
    /* 0x0023B4DE 18           */ IL_000D: ldc.i4.2
    /* 0x0023B4DF 6F53090006   */ IL_000E: callvirt  instance int32 Terraria.Utilities.UnifiedRandom::Next(int32)
    /* 0x0023B4E4 2D0D         */ IL_0013: brtrue.s  IL_0022

    /* 0x0023B4E6 02           */ IL_0015: ldarg.0
    /* 0x0023B4E7 17           */ IL_0016: ldc.i4.1
    /* 0x0023B4E8 7D690B0004   */ IL_0017: stfld     bool Terraria.Player::makeStrongBee
    /* 0x0023B4ED 2036020000   */ IL_001C: ldc.i4    566
    /* 0x0023B4F2 2A           */ IL_0021: ret

    /* 0x0023B4F3 02           */ IL_0022: ldarg.0
    /* 0x0023B4F4 16           */ IL_0023: ldc.i4.0
    /* 0x0023B4F5 7D690B0004   */ IL_0024: stfld     bool Terraria.Player::makeStrongBee
    /* 0x0023B4FA 20B5000000   */ IL_0029: ldc.i4    181
    /* 0x0023B4FF 2A           */ IL_002E: ret
} // end of method Player::beeType
//哔了狗了,WordPress不支持IL

啊,这是一堆什么破玩意啊,压根看不懂

爷——不——写——了!!!!(不是)

莫急,我们要透过现象看本质,先瞅一眼主要的OpCode:

// ldarg.0 将第一个参数(参数0)压入栈
// 这个方法么的参数,马老师发生甚么事了
// 实际上,非静态方法是以当前实例(也就是this)作为第一个参数的
// 即使它这个this并不在方法中
IL_0000: ldarg.0
// 然后strongBees字段的值被压入栈
IL_0001: ldfld     bool Terraria.Player::strongBees
// 如果这个字段的值为false,则IL代码直接跳转到IL_0022那一行
IL_0006: brfalse.s IL_0022

// 将静态字段Main.rand压入栈
IL_0008: ldsfld    class Terraria.Utilities.UnifiedRandom Terraria.Main::rand
// 然后将2压入栈
IL_000D: ldc.i4.2
// 然后调用Main.rand.Next()方法,将返回值压入栈
IL_000E: callvirt  instance int32 Terraria.Utilities.UnifiedRandom::Next(int32)
// 如果栈顶元素(也就是刚刚压入栈的Next()方法的返回值)不是0,那么跳转到IL_0022那一行
IL_0013: brtrue.s  IL_0022

// 把player实例压入栈
IL_0015: ldarg.0
// 把1压入栈(记住,1就是true)
IL_0016: ldc.i4.1
// 然后把player实例的makeStrongBee这个字段的值赋为true
IL_0017: stfld     bool Terraria.Player::makeStrongBee
// 把566(大蜜蜂的ProjectileID)压入栈
IL_001C: ldc.i4    566
// 返回栈顶元素566
IL_0021: ret

// 把player实例压入栈
IL_0022: ldarg.0
// 把0压入栈(记住,0就是false)
IL_0023: ldc.i4.0
// 然后把player实例的makeStrongBee这个字段的值赋为false
IL_0024: stfld     bool Terraria.Player::makeStrongBee
// 把181(小蜜蜂的ProjectileID)压入栈
IL_0029: ldc.i4    181
// 返回栈顶元素181
IL_002E: ret

(如果还是看不懂就看后面的附录叭,或者你可以自己上网学习堆栈的相关知识)

现在我们已经了解了原始的方法,让我们用dnSpy来看看更改后的样子,首先我们右键 beeType() 方法并单击Edit Method(C#)…

在弹出的窗口中我们来更改原方法(注意这里要改代码的),然后单击编译

如果提示缺少程序集引用或者代码有错误,那么首先要将 using Terraria.ID;using ExampleMod; 添加到代码中(ExampleMod改成你自己的mod名)。然后为 ReLogicExamplePlayer (这个也改成你自己的modPlayer类)添加缺失的引用,在 \Documents\My Games\Terraria\ModLoader\references目录下你可以找到ReLogic.dll。然后如果你已经编译了你的mod,你可以在 \Documents\My Games\Terraria\ModLoader\references\mods\ 目录下找到你的mod.dll。然后通过添加程序集引用将这两个dll添加到dnSpy。

现在修复了错误,单击编译按钮便可以回到IL代码窗口。然后切换回C#视图,你可能会发现,你改的代码并没有完全保留在C#代码中。但是实际上逻辑是相同的,只是布局不太一样(估计是DnSpy自带优化,不太知道)。

public int beeType()
{
    if (!this.strongBees || Main.rand.Next(2) != 0)
    {
        this.makeStrongBee = false;
        return 181;
    }
    this.makeStrongBee = true;
    if (this.GetModPlayer<ExamplePlayer>().strongBeesUpgrade && Main.rand.NextBool(10))
    {
        return 183;
    }
    return 566;
}

然后我们切换回IL视图看看。发现代码已经增加了一部分,我们来注释一下。

// 把player实例压入栈
IL_0015: ldarg.0
// 把1(true)压入栈
IL_0016: ldc.i4.1
// 给player实例的makeStrongBee字段赋值
IL_0017: stfld     bool Terraria.Player::makeStrongBee

// 新的内容(从这行到IL_003C):把player实例压入栈
IL_001C: ldarg.0
// 调用GetModPlayer()方法
IL_001D: call      instance !!0 Terraria.Player::GetModPlayer<class [ExampleMod]ExampleMod.ExamplePlayer>()
// 把strongBeeUpdate()方法的返回值压入栈
IL_0022: ldfld     bool [ExampleMod]ExampleMod.ExamplePlayer::strongBeesUpgrade
// 如果返回值是false,则跳转到IL_003D那一行(这个行数是16进制)
IL_0027: brfalse.s IL_003D

// 把Main.rand类压入栈
IL_0029: ldsfld    class Terraria.Utilities.UnifiedRandom Terraria.Main::rand
// 把10压入栈
IL_002E: ldc.i4.s  10
// 调用Utils.NextBool()方法,注意是Utils,Utils类里面有NextBool扩展方法。
IL_0030: call      bool Terraria.Utils::NextBool(class Terraria.Utilities.UnifiedRandom, int32)
// 如果返回值是false,就跳转到IL_003D那一行
IL_0035: brfalse.s IL_003D

// 把183(蜜蜂手雷的ProjectileID)压入栈
IL_0037: ldc.i4    183
// 返回183
IL_003C: ret

// 将566压入栈
IL_003D: ldc.i4    566
// 返回566
IL_0042: ret

实际上到这补丁的编写部分就完成辣!由于这个IL编辑肥肠滴简单啊,下一篇我们将介绍把补丁打到游戏上的三种方法。

附录:IL代码解读


首先我们了解一下栈的含义,我们可以把栈理解为一个分层的桶,最先放入的数据在底部,后放入的数据在顶部,每次调用栈中的计算数据时调用的都是栈顶的元素,也就是调用之前最后入栈的元素,当然,出栈时,先出去的也肯定是最上面的数据,因此出栈操作会使栈顶被删除,使下一层的数据成为栈顶数据。

理解了栈的含义之后,我们来解读一下IL代码的含义。

首先我们来讲一下IL中那些看起来十分奇怪的代码格式,IL实际上是CLI规范中定义的一种LR(中间表示),(如果你学过编译原理这门课或者看过有关书籍,你可能会发现他跟汇编语言的助记符有些类似但是不完全相同),IL中很多代码都是它作用的缩写,基本掌握规律就可以猜出来一个大概。

然后我放一个表格在这里:

名称说明
Add将两个值相加并将结果压入栈
Sub将两个值相减并将结果压入栈
Mul将两个值相乘并将结果压入栈
Div将两个值相除并将结果以浮点(F)或整型(int32)压入栈
Rem将两个值相除并将余数压入栈
Call调用方法(也可以调用虚方法,不过是类形本身的虚方法) 不做null检测
Callvirtcall virtual 调用虚方法 不能调用静态方法 会做null检测,若实例是null会抛出NullReferenceException异常
ldstrload string 把一个string压入栈
ldfldload field 把一个字段的值压入栈
ldsfldload static field 把一个静态字段的值压入栈
ldarg.iload argument[i] 把argument数组里索引为i的参数压入栈
ldc把一个数字压入栈 后缀:.i(n) n表示字节数 即
.i1=int8(byte)
.i2=int16(short)
.i4=int32(int)
.i8=int64(long)
ldc.i(n).s把一个int8n数字作为int32压入栈
ldc.i(n).(m)把int8n数字m压入栈
st与ld相对应 store 赋值 用法与ld类似
b IL_XXXX条件判断满足就跳转到IL_XXXX那一行 如
beq即==
bge即>=
bgt即>
ble即<=
blt即<
bne即!=
brfalse / brtrue IL_XXXXbreak false / break true
若栈顶元素为false / true则跳转到IL_XXXX那一行
c比较
ceq 比较两个值,相等则将1(true)压入栈,否则将0(false)压入栈
cgt 比较两个值,第一个大于第二个则将1(true)压入栈,否则将0(false)压入栈
clt 比较两个值,第一个小于第二个则将1(true)压入栈,否则将0(false)压入栈
retreturn 返回栈顶元素

这个就是常用的IL代码,有不懂的可以在评论里问

课后作业

我跟U淫一样不留作业,大家玩得开心,我也要找U淫Van♂的♂开♂心♂去了

《IL基础教程① 认识IL和IL补丁编写方式》有3个想法

  1. Pingback: Tiger 的 IL 教程 - 裙中世界

发表回复