BepInex Plugin开发教程 05 Harmony补丁基础入门

一、介绍

通过之前的教程,我们已经知道如何编写基本的插件,如果你有C#和Unity的基础,这个时候已经可以做出一些功能了,比如通过按键修改游戏数据之类的。但是,这有很大的局限性,因为通常情况下,我们并不想通过按键来调用我们的功能,我们想让大多数的功能都是加载之后就不需要管了,或者想做一些普通情况下比较难以操作的事情。这个时候,通过Harmony进行补丁可以解决我们绝大多数的需求。

Harmony的github链接 https://github.com/pardeike/Harmony 详细信息可以在github查看。

Harmony中使用最频繁的两个地方就是前置补丁和后置补丁,也是最简单的,本篇文章主要就将这两种。一些特殊的需求需要修改函数本身也是可以的,Harmony支持修改函数的IL码,不过这个就不在基础教程的范畴了,有兴趣的话之后可以在Harmony的github查看。

二、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.T05").PatchAll();

这里的参数使用插件的id即可,使用PatchAll的方法时,它会自动收集我们程序集中所有的HarmonyPatch特性,然后根据特性进行补丁。

除了自动补丁之外,还可以进行手动补丁,可以更加细微的控制,就不在基础教程中说了,读者可以通过GitHub继续了解。

六、后话

对于新接触BepInexPlugin或者UnityModManager Mod开发的人来说,Harmony可能比较陌生,如果看了本篇教程之后还是感觉不太好下手,可以使用Dnspy反编译其他人制作的插件,或者如果他们有开源的话,可以直接查看开源的代码进行学习模仿。我在我的Github上也放了几个小插件,大家可以查看学习。

发表评论

电子邮件地址不会被公开。必填项已用 * 标注