一. 前言
通过上面的学习,其实我们已经可以完成一部分的mod,比如点击一下按钮,获得一堆金币,点一下按钮,让玩家直接死亡等等。但是,通常情况下,我们并不希望通过点击按钮来执行我们的逻辑,而是玩家安装mod后,什么都不用管,它自己就生效了,我们的界面只是用来修改设置的。这个时候,我们就需要用到Patch,也就是补丁。
Harmony可以满足我们的需求,UMM已经内置了这个库,它可以在函数的前面和后面打上补丁,让我们自己的函数在游戏的某个函数之前执行,或者等某个函数执行之后紧接着执行。也可以使用Transpiler通过修改IL代码的方式修改某个函数本身,不过IL代码相关的内容比较深入,本教程就不过多探讨,如果想了解,可以在github上查看Harmony作者的文章。 Transpiler教程
在本教程,我们只探讨前挂补丁和后挂补丁两种,这两种已经足够满足我们绝大多数的需求,并且相对来说更简单。
二. 补丁方式
1.自动补丁
打补丁的方式分为自动补丁和手动补丁,大多数情况下,自动补丁的方式更为方便并且足够满足需求。
HarmonyInstance.Create(mod.Info.Id).PatchAll(Assembly.GetExecutingAssembly());
显而易见,先根据Mod的Id创建实例,然后打上全部补丁,打补丁的对象是当前使用的程序集。
要让自动补丁被识别,我们需要使用C#特性,通过特性为我们的函数和类进行标记,然后Harmony就可以找到我们的补丁。它的样子看起来应该是类似这样的
[HarmonyPatch(typeof(XXXClass), "YYYMethod")]
class XXXClassPatch
{
public static void Postfix(XXXClass __instance)
{
if(mod.Enabled)
{
mod.Logger.Log(__instance.ToString());
}
}
}
我们需要创建一个类,名字随意,好认就可以,然后在类上使用HarmonyPatch特性,类里写上补丁函数。HarmonyPatch特性有多种重载,最常用的就是[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 Say()
{
Console.WriteLine("Hello");
}
public void Say(string content)
{
Console.WriteLine(content);
}
public void Say(int content)
{
Console.WriteLine(content);
}
}
我们需要这样写特性[HarmonyPatch(typeof(People), "Say", new Type[] { typeof(string) })]
如果要补丁第一个,就写[HarmonyPatch(typeof(People), "Say", new Type[] { })]
这样,我们就可以对重载的函数进行补丁了。
但是还有一部分需求无法满足,如果我要补丁的函数,是构造函数怎么办?如果我要补丁属性怎么办?这个时候,就需要使用MethodType参数了,举几个例子
[HarmonyPatch(typeof(People), "Name", MethodType.Getter] 补丁Name属性的get
[HarmonyPatch(typeof(People), "Age", MethodType.Setter] 补丁Age属性的set
[HarmonyPatch(typeof(People), MethodType.Constructor] 补丁People的构造函数
需要注意的是,补丁构造函数的时候不应该按照反编译器的提示写.ctor,而是应该直接省略函数名,使用MethodType或者类型列表和MethodType一起用。
2. 手动补丁
自动补丁已经基本满足我们的各种需求,并不需要麻烦的手动补丁,但是,确实还是有需要手动补丁的地方。比如,我想补丁一大堆相似的地方,使用我的一个补丁函数来补丁很多个方法,这个时候如果使用自动补丁,会造成篇幅很长,而且很多重复代码,这个时候我们就可以使用手动补丁。
var harmony = HarmonyInstance.Create(mod.Info.Id);
var original = typeof(TargetClass).GetMethod("TargetMethod");
var prefix = typeof(MyPatchClass1).GetMethod("SomeMethod");
var postfix = typeof(MyPatchClass2).GetMethod("SomeMethod");
harmony.Patch(original, new HarmonyMethod(prefix), new HarmonyMethod(postfix));
三. 补丁方法
补丁方法有多种,这里我们仅介绍两种最常用的,其他的补丁方法可以到Harmony的wiki查看。
1. 前置补丁 Prefix
bool Prefix(...)
我们有时候可能需要在一个函数执行之前做点什么,比如修改一下它传入的参数,对结果造成影响,或者我们不想让目标函数执行,整个拦截掉它,然后返回我们自己计算的结果,这个时候就应该使用Prefix了,它返回一个bool值,如果返回true,则代表原函数可以继续执行,如果返回false,则代表拦截原函数。
例:
[HarmonyPatch(typeof(People), "Say", new Type[] { typeof(string) })]
class PeopleSayPatch
{
public static bool Prefix(ref string content)
{
if(mod.Enabled)
{
content = "我要说的内容已被修改";
}
return true;
}
}
例2:
[HarmonyPatch(typeof(People), "Name", MethodType.Getter)]
class PeopleNamePatch
{
public static bool Prefix(ref string __result)
{
if(mod.Enabled)
{
__result = "张三";
return false; //拦截原方法,直接使用我们给出的结果
}
return true;
}
}
2. 后置补丁 Postfix
这应该是最常用的补丁方法了,使用很简单,一般情况下也不需要返回什么东西。
例:
[HarmonyPatch(typeof(People), "Say", new Type[] { typeof(string) })]
class PeopleSayPatch
{
public static void Postfix(ref string content)
{
if(mod.Enabled)
{
mod.Logger.Log("Say方法传入了 " + content);
}
}
}
四. 补丁参数
这里就是补丁最重要的部分之一了,如果我们想要随心所欲的修改,那这里的规则必须牢记并且正确使用。
- 补丁方法必须是静态方法
- Prefix需要返回void或者bool类型(void即不拦截)
- Postfix需要返回void类型,或者返回的类型要与第一个参数一致(直通模式)
- 如果原方法不是静态方法,则可以使用名为__instance(两个下划线)的参数来访问对象实例
- 可以使用名为__result(两个下划线)的参数来访问方法的返回值,如果是Prefix,则得到返回值的默认值
- 可以使用名为__state(两个下划线)的参数在Prefix补丁中存储任意类型的值,然后在Postfix中使用它,你有责任在Prefix中初始化它的值
- 可以使用与原方法中同名的参数来访问对应的参数,如果你要写入非引用类型,记得使用ref关键字
- 补丁使用的参数必须严格对应类型(或者使用object类型)和名字
- 我们的补丁只需要定义我们需要用到的参数,不用把所有参数都写上
- 要允许补丁重用,可以使用名为__originalMethod(两个下划线)的参数注入原始方法
Transpilers还有一些可选参数,我们这里不做探讨,想了解可以访问Harmony的wiki。