Skip to content

拦截/注入 游戏函数实现高级操作

本章中的部分内容参考了:

通过前面的学习,想必大家已经对BepInEx的运行方式有个基本的认识了,现在,来讲解一下如何对游戏中的函数进行拦截和注入;

HarmonyPrefix

HarmonyPrefix是Harmony为我们提供的一个接口,它将在我们指定的函数前进行执行,并且我们可以返回一个bool值来控制是否继续继续执行游戏原函数;
需要配合HarmonyPatch一起使用

csharp
// 19 个重载
public HarmonyPatch();
public HarmonyPatch(Type declaringType);
public HarmonyPatch(MethodType methodType);
public HarmonyPatch(string methodName);
public HarmonyPatch(Type[] argumentTypes);
public HarmonyPatch(MethodType methodType, params Type[] argumentTypes);
public HarmonyPatch(string methodName, MethodType methodType);
public HarmonyPatch(string methodName, params Type[] argumentTypes);
public HarmonyPatch(Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, MethodType methodType);
public HarmonyPatch(Type declaringType, string methodName);
public HarmonyPatch(Type declaringType, Type[] argumentTypes);
public HarmonyPatch(Type declaringType, string methodName, MethodType methodType);
public HarmonyPatch(string methodName, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, string methodName, params Type[] argumentTypes);
public HarmonyPatch(MethodType methodType, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, MethodType methodType, params Type[] argumentTypes);
public HarmonyPatch(Type declaringType, string methodName, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(Type declaringType, MethodType methodType, Type[] argumentTypes, ArgumentType[] argumentVariations);
public HarmonyPatch(string assemblyQualifiedDeclaringType, string methodName, MethodType? methodType = null, Type[] argumentTypes = null, ArgumentType[] argumentVariations = null);

如:
我们想要对Mecha类下的SetForNewGame函数进行拦截,那么就是:

csharp
[HarmonyPrefix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static bool Mecha_SetForNewGame_Prefix()
{
    // 这里写入我们自己的内容            
    Debug.Log("这里的内容将会在游戏函数执行前进行执行");
    // 返回 true为继续执执行游戏原函数,返回 false为不执行游戏原函数,
    return true;
}

HarmonyPostfix

HarmonyPostfix一样也是Harmony为我们提供的一个接口,它将在我们指定的函数执行完毕后,再执行。
一样需要配合HarmonyPatch一起使用

如:

csharp
[HarmonyPostfix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static void Mecha_SetForNewGame_Postfix()
{
    // 这里写入我们自己的内容            
    Debug.Log("这里的内容需要等待游戏原函数执行完后才会执行");
}

注意:

  1. 我们的函数需要使用static静态函数,不然会报错;
  2. 函数名可以自定义,但尽量不要和游戏原有函数冲突;
  3. 两种拦截方式大同小异,希望大家举一反三。

this

在游戏原函数中难免会出现this参数,万能的Harmony当然也考虑到了这一点,针对于this,我们可以向函数中传递一个__instance。

游戏原函数内容:

csharp
public void SetForNewGame()
{
    ModeConfig freeMode = Configs.freeMode;
    this.coreEnergyCap = freeMode.mechaCoreEnergyCap;
    this.coreEnergy = this.coreEnergyCap;
    this.corePowerGen = freeMode.mechaCorePowerGen;
    this.reactorPowerGen = freeMode.mechaReactorPowerGen;
    this.reactorEnergy = 0.0;
    this.reactorItemId = 0;
}

我们可以这样写:

csharp
[HarmonyPostfix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static void Mecha_SetForNewGame_Postfix(Mecha __instance)
{
    ModeConfig freeMode = Configs.freeMode;
    __instance.coreEnergyCap = freeMode.mechaCoreEnergyCap;
    __instance.coreEnergy = __instance.coreEnergyCap;
    __instance.corePowerGen = freeMode.mechaCorePowerGen;
    __instance.reactorPowerGen = freeMode.mechaReactorPowerGen;
    __instance.reactorEnergy = 0.0;
    __instance.reactorItemId = 0;
}

注释:

  • Mecha __instance中,Mecha 为当前类的名称,__instance为Harmony的固有写法(有两个“_”);
  • 这种方法只限于操作公共public变量和函数;

游戏私有变量

刚刚提到,“__instance”只能获取游戏的公共变量和方法,如果我们要获取游戏中私有的变量和方法的话,就需要用到Traverse工具;
我们可以通过Traverse工具,方便访问游戏里所有公有,私有,受保护的变量,方法,以及属性,

如我们想获取游戏中的变量,那么在我们的插件中就可以这么写:

csharp
[HarmonyPostfix]
[HarmonyPatch(typeof(Mecha), "SetForNewGame")]
public static void Mecha_SetForNewGame_Postfix(Mecha __instance)
{
    // 获取 private float _dronesSpeed; 的值
    var _droneCount= Traverse.Create(__instance).Field("_droneCount").GetValue();
}

拓展知识

来自 https://bbs.3dmgame.com/thread-5870433-1-1.html关于Traverse的使用: Traverse是harmony类库下的一个工具类,也就是在一开始引用的using harmony;这条语句后,方便我们使用的一个类,首先我们要明白private和public还有protected三个关键词的区别,具体可以百度,我这里仅从结论讲明,除了public,其他的private和protected从外界是无法访问到的,但是用Traverse类不管它是public,private,protected,均可以强行访问,为什么不任何地方都使用Traverse去访问呢,因为性能问题,用Traverse要走映射,简单来说运行速度会有些许影响

Traverse的具体使用方法简单的来说明一下,Traverse.create(类的实例),表名我要将一个类的实例转为Traverse对象,简单来说就是附加功能,比如我们以前都是自己买菜,后来有了XX外卖,我们不需要亲力亲为了,XX外卖就等于Traverse对象了(这里就是将映射功能简单化了,不需要自己打代码了),这样我们就有一个可以访问类实例的Traverse对象了,在上面法宝的例子中,我是直接写为了
var itemID = Traverse.Create(__instance).Field("itemID").GetValue();
这是一种简化的写法的,下面我分开并且逐步注释一下
Traverse t = Traverse.Create(__instance);//根据__instance (ToilRefining类的实例) 创建 Traverse对象,并且用t表示
Traverse f = t.Field("itemID");//在ToilRefining实例里面有个itemID的字段,找到他并且创建一个Traverse对象,用f表示,这样可以强行访问 itemID,因为itemID是私有的没法直接访问
int itemID = f.GetValue();//将Traverse版本的itemID提取成可以直接访问的数值,因为Traverse并不知道原本itemID是什么类型的,所以我们要用标注这是个int类型了,从源代码中我们可以知道itemID的变量类型,对应修改即可 于是我们就访问到itemID了
既然有获取,自然就有修改,修改我们可以用f.SetValue(数值),这里就不需要指定了,因为你在输入数值的时候,他会自动把你输入的数据转成对应的类型

这里我说一下字段,属性,方法的意思,这是C#的基础,字段代表类变量,可以理解为类中的全局变量,可以再类中任意地方访问到

属性是字段的升级版,他在源代码中的样子是这样的

他跟字段的定义差不多,但是后面会有括号,里面还有set和get,这种样子的就是属性,我们不能通过Traverse.Field(字段名字),而是通过Traverse.Property(属性名字)来访问,如果定义中只有get,表名这个东西只能获取,不能更改(就是游戏开发者也不能),get和set都在就是可以获取也可以更改

最后就是方法,在C#中称为方法,C语言中称为函数,比如游戏源码中,MakeFaBao就是制作法宝的方法,他定义时后面跟随的是()这种括号,我们想要访问游戏private的方法可以用Traverse.Method(方法名字).GetValue()来运行,注意后面要加上.GetValue(),因为仅仅Traverse.Method(方法名字)是获取的方法的Traverse对象,而没有运行他

C#中有一个关键词是var,这个关键词是这个变量是智能根据你后面赋值来判断他的变量类型的
比如 var a = 6;//a是int类型
var b = "我是文字";//b就是string类型的
于是之前为了itemID那么多行的代码就可以省略为var itemID = Traverse.Create(__instance).Field("itemID").GetValue();一句话搞定
当然也可以var t = Traverse.Create(__instance);
var itemID = t.Field("itemID").GetValue();var XXXX = t.Field("XXXX").GetValue();
来多次获取