跳至正文

魔改进阶——Mod联动


主流方法

我们说几个Mod之间有联动,一般是指Mod之间可以互相引用对方的内容。要实现这个功能,我们一般可以通过:

  1. 强弱引用/依赖:也就是在build.txt中标记依赖哪些Mod,然后通过引用这些Mod的程序集来修改内容。这种方法可以享受到代码高亮以及自动补全,是比较推荐的做法。
  2. 反射:利用.Net提供的反射方法去动态获取需要修改的属性,好处是比较自由,而且能访问第一种方法无法访问的一些属性。坏处就是代码量大,而且性能差,可读性差。
  3. Call接口:有些Mod会显式提供Call接口给其他Mod传参数,具体用法要看各个Mod的实现(比如BossChecklist)。优点是基本不会受到版本更新的影响,缺点是局限性大,没法自由的更改。

当然,也可以把几种方法结合起来,但是无论你用的是哪种方法,一定要先获取目标Mod的源码,你才能知道具体有哪些属性是你需要修改的。此外,在进行Mod联动的时候,一定要知道对方的Mod有没有被加载,如果没有加载那么你进行的操作其实是无效的,而且还会报错。

检测的方式就要用到ModLoader.GetMod这个函数,传入的参数是Mod的名字(不是DisplayName)。如果返回值不是null就说明这个Mod被加载了。值得注意的是这个函数不要写在Load重写函数里面,因为Load加载的时候并不能保证加载的顺序。也就是说,可能要引用的Mod在你的Mod之后加载,但是你却判定这个Mod没有被加载,这是不行的。


强弱引用/依赖区别

当一个Mod A强引用/依赖另外一些Mod B的时候,B所代表的所有的Mod必须全部加载,才能加载Mod A。也就是说,Mod A离开了B就不能活。此时因为有强引用关系,Mod A一定会在Mod B之后被加载,如果任何一个B中的Mod没有被加载,则A也无法被加载。比如四九落星对星虹集,合成图Mod对UIEditor都是这种关系。

弱引用/依赖是指,即使B一个都没有被加载,Mod A也能被加载,但是就会少一些联动内容。注意,由于B并不一定要被加载,所以这就需要程序员妥善的处理缺失一些Mod的情况,并保证在缺失Mod的情况下自己的Mod也能正确运行。如果没有处理好,就会导致游戏直接崩溃,并且有时没法显示报错。比如部分大型Mod对BossChecklist的支持。

如果只使用Call接口,那么可以不用引用别的Mod,因为Call接口实际上是由tModloader管理的,你并不需要知道别的Mod是如何运作的。

总而言之,强引用就像人和心脏的关系,离开了就不能活。而弱引用就像人和肥宅快乐水,虽然不能喝很难受,但是没有它你也不会死。

说句题外话,强引用可能会导致循环依赖,比如Mod A强依赖Mod B,然后Mod B依赖Mod C,然后Mod C依赖Mod A,这时候就会出现循环依赖,由于被依赖的Mod必须在依赖Mod之前被加载,所以请问,谁应该先被加载?所以使用强依赖的时候一定要注意不要引发循环依赖。


实现强弱依赖

首先我们要打开build.txt,然后在这个位置写下要依赖的Mod的内部名字,如果需要指定最低版本,可以在后面用@XXXX的方式指定版本。

注意这是强依赖,如果是弱依赖,我们需要加的是 weakReferences = XXX@XXX,加多个Mod可以用逗号分隔。 其实不加也可以,只要你不直接使用目标Mod的符号,但是在这里weakReferences有一个比较重要的作用就是限制版本,如果对方Mod的版本没有达到最低要求,那么就会报错,否则如果直接在代码里使用版本错误的Mod可能会导致游戏崩溃。

然后我们为了拥有自动补全和高亮,以及为了查看源码,我们可以把对面的Mod拆解出来一个dll程序集。具体做法就是在Mods里点进Mod界面,然后点击Extract(如果Mod有隐藏dll资源那就没办法了,虽然有暴力方法可以做到,但还是不推荐)

值得注意的是,拆解出来的文件夹名字就是其Mod内部名字

拆解完毕之后可以选择任何一个dll文件

然后在VS里面添加这个dll作为引用

然后我们就可以获得代码补全了!

然而,每次Mod更新你可能就需要重新拆解引用一下被依赖Mod的dll,如果你已经有被依赖的源码(或者你本来就是那个Mod的开发者),那么可以直接在VS编译出一个dll,就不用去拆解了。


合成表添加/物品引用

我们先从一个简单的添加合成表的例子讲起:

如果你想用TemplateMod2中的SkirtSword作为合成材料,那么如果是强引用情况下,我们不需要做任何检测,直接这么写

recipe.AddIngredient(ModContent.ItemType<SkirtSword>(), 1);

因为强引用保证这个Mod一定存在,就像是你的Mod和目标依赖Mod已经融为一体了。但是如果是弱引用,就比较麻烦了。

Mod templateMod2 = ModLoader.GetMod("TemplateMod2");
// 如果TeamplateMod2被加载了
if (templateMod2 != null) {
    recipe.AddIngredient(ModContent.ItemType<SkirtSword>(), 1);
    // 或者 recipe.AddIngredient(templateMod2.ItemType("SkirtSword"), 1);
    // 或者 recipe.AddIngredient(templateMod2, "SkirtSword", 1);
} else {
    // 如果不存在就用别的合成方式代替
}

两种替代的方法都是可以不用引用dll的,因为使用的是字符串而不是符号。但是之所以可以不用引用,是因为ItemType本身就是由tModloader管理的,而不是Mod独有的,而tModloader提供了直接用字符串访问类型的方法。同样,ProjectileType,BuffType等等都可以通过这个方式去访问。


Mod独有属性修改

我们以修改ThoriumMod的玩家属性为例,首先我们要用一些途径获取我们要修改的字段,然后引用Mod所对应的dll,最后在强引用模式下直接调用:

public override void UpdateEquips(ref bool wallSpeedBuff, ref bool tileSpeedBuff, ref bool tileRangeBuff) {
    player.GetModPlayer<ThoriumMod.ThoriumPlayer>().flatShadowDamage += 100;
}
if (ThoriumMod.ThoriumWorld.downedScout) {
    // 判断Mod时期
    shop.item[nextSlot].SetDefaults(ItemID.Gel);
    nextSlot++;
}

所以其实这件事情十分的简单,困难的在于你如何处理好弱引用的关系,以及如何获取引用的dll上。在弱引用模式下,由于目标Mod可能没有加载,所以代码里面的引用符号可能会无法被解析,导致游戏直接崩溃,比如上面这一行,当这段代码所在的类被调用的时候,会先去解析引用符号,确定ThoriumWorld.downedScout这个符号应该指向的地址,但是由于目标Mod没有加载,.NET的JIT系统无法识别ThoriumMod.ThoriumWorld.downedScout,这个字段,就会导致运行时错误。

那么如何解决这个问题呢?第一种方法是用反射去动态的加载,这样就不会解析不该解析的符号,第二种方法就是把这些修改放在一个单独的类里面,当目标依赖Mod存在的时候才会去调用这个类,这样JIT也不会解析没加载的符号。(一般不会有事,因为加载过的Mod都会被tModloader缓存下来)


反射应用

接下来我们就进入到不是那么舒适的阶段了,有些字段的访问等级被标记为internal或者private,或者你并没有目标Mod的dll引用程序集的时候,VS自动提示和编译器都无法解读这个符号,你就可能迫不得已得用反射来解决问题了。

我不是很推荐用反射去做联动内容,因为它会破坏面向对象的封装性和可维护性,但是有时候真的没办法就只能用用反射了。

比如我在TemplateMod2里面写了个私有字段和刷屏代码:

private string _text = "这是模板Mod";

public override void MidUpdatePlayerNPC() {
    Main.NewText(_text);
}

我们的任务是在另一个Mod里面修改_text这个字段,由于它是私有的,所以即使你找到了引用dll也无法直接使用它。这时候我们就可以用反射获取和修改私有字段。

public override void PostSetupContent() {
    // 注意,因为是弱引用,所以我写在了PostSetupContent里,确保所有该加载的Mod都已经加载
    Mod templateMod2 = ModLoader.GetMod("TemplateMod2");
    // 如果TeamplateMod2被加载了
    if (templateMod2 != null) {
        // 获取TemplateMod2._text这个私有成员字段的句柄
        // 后面的两个过滤器一定要打对,才能准确索引到这个字段
        var targetText = templateMod2.GetType().GetField("_text", BindingFlags.Instance | BindingFlags.NonPublic);

        // 接下来我们对TemplateMod2的实例进行修改
        targetText.SetValue(templateMod2, "你才不是模板Mod");
    }
}

进入游戏就能看到这个子段被改变了,与此同时,我没有使用任何引用符号,比如TemplateMod2.XXX,所以你甚至不需要引用dll,这就是反射的威力了。但是如果你用不好,反射会带来很大的麻烦,比如当目标Mod更新的时候,使用强弱引用会在编译时报错,但是反射却不会,它会在游戏里炸给你看,所以这是一把双刃剑。反射的详细教程我会在第四部分进行讲解。


如何使用Call接口

Call接口的使用非常自由,

public override object Call(params object[] args) {
    return base.Call(args);
}

观察它的形态我们会发现,本质上Call函数就是一个传入一堆object作为参数的函数,而如何解读这些object就要由Mod作者你来决定。

比如我想提供一个修改上面的_text的接口,我可以让第一个参数是个string类型,表示你要执行哪个函数,如果传进来的是”Text”那我们就知道对方想修改_text字段。然后我们把下一个参数赋值给_text字段。这样如果想要修改_text字段,对方需要Call(“Text”, “XXXXX”),于是我们可以这么写:

public override object Call(params object[] args) {
    string str = args[0].ToString();
    if (str.Equals("Text")) {
        _text = args[1].ToString();
    }
    return null;
}

然后在另一个Mod里面我们可以这么写:

Mod templateMod2 = ModLoader.GetMod("TemplateMod2");
// 如果TeamplateMod2被加载了
if (templateMod2 != null) {
    templateMod2.Call("Text", "我这是通过Call接口来修改你的值");
}

那么问题来了,我如何知道被依赖的Mod提供了哪些接口呢?最好的方式当然是去看看人家的官方文档,比如BossChecklist写的就很详细。

这样做的好处就是兼容性非常好,无论版本怎么更新接口不变就不用修改代码。

以上这些就是Mod联动的全部内容了,如果有疏漏之处欢迎补充。

《魔改进阶——Mod联动》有3个想法

  1. 我尝试给武器添加overhaul中的设定,不过就算直接复制overhaul源码中修改武器的部分也无法生效,是有什么其他的需要注意的点么

发表回复