前言
通过之前的教程,我们已经知道如何编写基本的插件,如果你有C#和Unity的基础,这个时候已经可以做出一些功能了,比如通过按键修改游戏数据之类的。但是,这有很大的局限性,因为通常情况下,我们并不想通过按键来调用我们的功能,我们想让大多数的功能都是加载之后就不需要管了,或者想做一些普通情况下比较难以操作的事情。这个时候,通过Harmony进行补丁可以解决我们绝大多数的需求。
Harmony的github链接 https://github.com/pardeike/Harmony 详细信息可以在github查看。
Harmony中使用最频繁的两个地方就是前置补丁和后置补丁,也是最简单的,本篇文章主要就讲这两种。一些特殊的需求需要修改函数本身也是可以的,Harmony支持修改函数的IL码,不过这个就不在基础的范畴了,以后有机会的话会放在进阶篇来讲。
HarmonyPatch特性
要对游戏中的方法进行补丁,首先我们需要确定一个目标,这里我准备了一个类,我们就以这个类为例子,对它进行补丁。
public class People
{
public string Name{ get; set; };
public int Age{ get; set; };
public People(string name, int age)
{
Name = name;
Age = age;
}
public void Sleep()
{
Console.WriteLine("睡觉");
}
public void Say()
{
Console.WriteLine("Hello");
}
public void Say(string content)
{
Console.WriteLine(content);
}
public void Say(int content)
{
Console.WriteLine(content);
}
}
这是一个简单的People类,有两个属性,分别是姓名和年龄,一个构造函数,一个Sleep方法,还有3个说话的方法,使用了3种重载。
以Sleep方法为例,我们写一个最简单的补丁。
[HarmonyPatch(typeof(People), "Sleep")]
class PeopleSleepPatch
{
public static void Postfix(People __instance)
{
Console.WriteLine(__instance.Name + "睡觉了");
}
}
这里的HarmonyPatch特性就用于确定补丁目标,这个特性的参数可以写在一排也可以分成几排写。例子中的两个参数分别是要补丁的类型,还有要补丁的方法的名字。这是最简单的情况,实际上我们还经常会遇到其他几种情况。比如,Say方法有3个重载,如何确定要补丁哪一个?属性要怎么补丁?我们再来看几个例子。
[HarmonyPatch(typeof(People), "Say", new Type[] { })]
[HarmonyPatch(typeof(People), "Say", new Type[] { typeof(string) })]
[HarmonyPatch(typeof(People), "Say", new Type[] { typeof(int) })]
如此,面对有重载的情况,我们只需要在添加一个参数,这个参数是一个Type数组,我们按顺序将参数类型填入即可。
[HarmonyPatch(typeof(People), "Name", MethodType.Getter]
[HarmonyPatch(typeof(People), "Age", MethodType.Setter]
[HarmonyPatch(typeof(People), MethodType.Constructor]
面对属性和构造函数,我们可以使用MethodType枚举来当作参数。需要注意的是,补丁构造函数时,函数名不能写dnSpy中看到的.ctor,而是应该直接省略不写函数名。
补丁方法
既然已经可以确定补丁目标了,接下来让我们了解一下最基础最常用的两种补丁方法,Prefix(前置补丁)、Postfix(后置补丁)。
先说后置补丁,这是最简单的,它在补丁目标运行结束之后运行,上面示例中的就是后置补丁,可以使用__result参数接收目标的返回值。
然后是前置补丁,顾名思义,它是在补丁目标运行之前运行的,这个相对复杂一点。因为我们可以选择是否执行原方法。我们来看两个例子。
[HarmonyPatch(typeof(People), "Say", new Type[] { typeof(string) })]
class PeopleSayPatch
{
public static bool Prefix(ref string content)
{
content = "要说的内容已被修改";
return true;
}
}
[HarmonyPatch(typeof(People), "Name", MethodType.Getter)]
class PeopleNamePatch
{
public static bool Prefix(ref string __result)
{
__result = "张三";
return false; //拦截原方法,直接使用我们给出的结果
}
}
第一个例子,我们将content的值修改为了我们自己想要的值,然后返回true表示让原函数继续执行。第二个例子,我们直接将最终结果修改,然后返回false,表示阻止原函数执行。如果你搞不明白IL代码,不知道如何修改函数本体,也可以通过前置补丁的方式自己计算结果然后修改。
补丁参数
在上面的例子中,我们有时候使用了__instance,有时候使用了__result,想必读者还留有疑问,为什么要这么写。其实,这是Harmony作者为我们定好的获取方法信息的方式。
大概情况如下:
补丁方法必须是静态方法
Prefix需要返回void或者bool类型(void即不拦截)
Postfix需要返回void类型,或者返回的类型要与第一个参数一致(直通模式)
如果原方法不是静态方法,则可以使用名为__instance(两个下划线)的参数来访问对象实例
可以使用名为__result(两个下划线)的参数来访问方法的返回值,如果是Prefix,则得到返回值的默认值
可以使用名为__state(两个下划线)的参数在Prefix补丁中存储任意类型的值,然后在Postfix中使用它,你有责任在Prefix中初始化它的值
可以使用与原方法中同名的参数来访问对应的参数,如果你要写入非引用类型,记得使用ref关键字
补丁使用的参数必须严格对应类型(或者使用object类型)和名字
我们的补丁只需要定义我们需要用到的参数,不用把所有参数都写上
要允许补丁重用,可以使用名为__originalMethod(两个下划线)的参数注入原始方法
Transpilers还有一些可选参数,我们这里不做探讨,想了解可以访问Harmony的wiki。
自动补丁
补丁的情况我们大体介绍完了,但是我们现在只是写了补丁,还没有对游戏进行补丁,其实很简单,我们只要在插件加载的时候,加上一句代码就好。
new Harmony("me.xiaoye97.plugin.Tutorial").PatchAll(); //以作者输入的字符串作为ID,对程序集中所有找到的补丁方法进行补丁。
或者
Harmony.CreateAndPatchAll(typeof(PluginTutorial)); //Harmony以类名为ID进行补丁,并且只补丁此类下的方法。
除了自动补丁之外,还可以进行手动补丁,可以更加细微的控制,就不在基础教程中说了,读者可以通过GitHub继续了解,以后的教程如果遇到需要手动补丁的情况我再继续讲解。