大家好啊,我是人,你们也可以叫我龙舞。这个系列教程主要讲的是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名)。然后为 ReLogic
和 ExamplePlayer
(这个也改成你自己的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检测 |
Callvirt | call virtual 调用虚方法 不能调用静态方法 会做null检测,若实例是null会抛出NullReferenceException异常 |
ldstr | load string 把一个string压入栈 |
ldfld | load field 把一个字段的值压入栈 |
ldsfld | load static field 把一个静态字段的值压入栈 |
ldarg.i | load 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_XXXX | break false / break true 若栈顶元素为false / true则跳转到IL_XXXX那一行 |
c | 比较 ceq 比较两个值,相等则将1(true)压入栈,否则将0(false)压入栈 cgt 比较两个值,第一个大于第二个则将1(true)压入栈,否则将0(false)压入栈 clt 比较两个值,第一个小于第二个则将1(true)压入栈,否则将0(false)压入栈 |
ret | return 返回栈顶元素 |
这个就是常用的IL代码,有不懂的可以在评论里问
课后作业
我跟U淫一样不留作业,大家玩得开心,我也要找U淫Van♂的♂开♂心♂去了
钛库力
最后表格里cxx系列false写成2了, 应该是0
Pingback: Tiger 的 IL 教程 - 裙中世界