概述

在Minecraft中,体会到Mod之间的相互协作,正是Minecraft游戏的乐趣之一。此外,一些常见的Mod,如IndustrialCraft2(以下简称IC2)、ThermalExpansion等,都提供了相应的API用于让其他Mod调用,从而实现相互的兼容。IC2这一Mod由于其引入的科技自动化系统而经久不衰,其引入的能量单位,EnergyUnit(简称EU),大部分的Mod玩家更是耳熟能详。本部分将以IC2这一Mod为例,让我们在3.2节引入的金属熔炉引入EU系统,从而得以接入IC2的网络,并带给读者使用API的一般方法。

兼容和依赖

很多情况下,常常提到的兼容一个Mod,比如兼容IC2,往往指的是两种不同的情况:第一种情况指的是制作一个IC2的附属Mod,而第二种情况指的是仅仅是让自己的方块和物品兼容IC2的系统。本节教程着重介绍的是第二种情况。为方便区分,后面的部分,作者将把第一种情况描述为制作一个“依赖”IC2的Mod,而第二种情况描述为“兼容”IC2。

很明显,不管是兼容还是依赖,都需要做的一件事是:探测其兼容或依赖的Mod是否存在。Forge的net.minecraftforge.fml.common.Loader类为我们提供了查看一个Mod是否被我们加载的通用方法。例如,我们可以通过下面一行代码的返回值检查IC2这一Mod是否存在:

Loader.isModLoaded("IC2");

这一方法需要的参数自然就是modid,换句话说,如果有Mod想要检查我们正在编写的Mod是否存在,那么传入的参数应该是fmltutor

还有一个十分常见的需求,就是在一个Mod发现Forge没有提供其依赖的Mod时,Forge应该优雅地抛出一个异常,以提醒用户没有安装前置。

编写这样的规则十分简单,只需要在Mod主类中@Mod注解里添加一个名为dependencies的字段就可以了。该字段中的字符串为由分号分隔的若干片段,每个片段都是rule:modid@version的格式:

  • rule有四种:required-beforerequired-afterbefore、和after。前两种以require开头,代表Mod要求其加载顺序位于被要求的Mod前(required-before)或后(required-after),同时被要求的Mod必须存在,如果不存在则Forge会弹出报错。而后两种则代表Mod只是单纯地要求加载顺序,而不一定要求被要求的Mod必须存在
  • modid就是被要求的Mod的modid
  • version代表被要求的Mod的版本范围,具体规则和acceptedMinecraftVersions的约定相同,不熟悉的读者请参照1.2节的内容

@version部分有时可以被省略,即Mod对被要求的Mod的版本不做要求,此时为rule:modid的形式。

此外,还有两种极其特殊的形式,代表其应位于所有Mod前或后:before:*after:*required-beforerequired-after不能用在这里。

现在来举个例子吧!比如一个Mod如果要求IC2不得小于2.3.262版本,神秘时代5不得小于5.2.4版本,且应该加载在这两个Mod后,那么这个Mod的@Mod注解里应该有如下字段:

dependencies = "required-after:IC2@[2.3.262,);required-after:Thaumcraft@[5.2.4,)"

这个字段的另一个十分有用的特性是:它可以指定依赖Forge的最低版本。一般情况下,Mod可以使用的Forge的最低版本其实是它编译时所使用的Forge版本。现在使用的11.15.1.2318版本实在是太新了,那么我们可以通过修改这一字段让可以使用的Forge版本更多,比如让其兼容到11.15.1.1722的话,那么修改后的@Mod注解是这个样子的:

src/main/java/com/github/ustc_zzzz/fmltutor/FMLTutor.java(部分):

@Mod(modid = FMLTutor.MODID, name = FMLTutor.NAME, version = FMLTutor.VERSION, acceptedMinecraftVersions = "1.8.9", dependencies = "required-after:Forge@[11.15.1.1722,)")

引用其它API

一些知名Mod在发布时,会在其发布页面同时提供API的下载地址,比如你可以在IC2的官方构建站里找到最新的基于Minecraft不同版本的API,当然这里作者提供的是1.8.9的。

下载后请将API的Jar放置在工作环境根目录下的libs目录(如果没有,请新建一个)就可以了,当然,本教程的官方源代码仓库里也内置了这一个API。

下载并放置后API后,重新配置开发环境,你就可以引用API里的代码了。这里我们实现一下IC2的API中的ic2.api.energy.tile.IEnergySink接口,对TileEntityMetalFurnace类进行部分重写:

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

public class TileEntityMetalFurnace extends TileEntity implements ITickable, IEnergySink

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    protected double receivedEnergyUnit = 0;

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    @Override
    public void readFromNBT(NBTTagCompound compound)
    {
        super.readFromNBT(compound);
        this.upInventory.deserializeNBT(compound.getCompoundTag("UpInventory"));
        this.downInventory.deserializeNBT(compound.getCompoundTag("DownInventory"));
        this.receivedEnergyUnit = compound.getDouble("ReceivedEnergyUnit");
        this.burnTime = compound.getInteger("BurnTime");
    }

    @Override
    public void writeToNBT(NBTTagCompound compound)
    {
        super.writeToNBT(compound);
        compound.setTag("UpInventory", this.upInventory.serializeNBT());
        compound.setTag("DownInventory", this.downInventory.serializeNBT());
        compound.setDouble("ReceivedEnergyUnit", this.receivedEnergyUnit);
        compound.setInteger("BurnTime", this.burnTime);
    }

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    public double getRequiredEnergyPerTick()
    {
        return 4.5;
    }

    public double getEnergyCapacity()
    {
        return 4096;
    }

    @Override
    public double getDemandedEnergy()
    {
        return Math.max(0, this.getEnergyCapacity() - this.receivedEnergyUnit);
    }

    @Override
    public int getSinkTier()
    {
        return 2;
    }

    @Override
    public double injectEnergy(EnumFacing directionFrom, double amount, double voltage)
    {
        this.receivedEnergyUnit += amount;
        return 0;
    }

    @Override
    public boolean acceptsEnergyFrom(IEnergyEmitter iEnergyEmitter, EnumFacing enumFacing)
    {
        return true;
    }

具体如何实现请参见IC2的API文档,在此略过不提。

同时IC2要求其接入网络的机器应该在启用和停用时触发两个事件,按照其API文档,我们对相关方法也做一下修改:

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    private boolean updated = false;

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    @Override
    public void invalidate()
    {
        super.invalidate();
        if (!this.worldObj.isRemote && Loader.isModLoaded("IC2"))
        {
            this.onIC2MachineUnloaded();
        }
    }

    @Override
    public void update()
    {
        if (!this.worldObj.isRemote)
        {
            if (!this.updated && Loader.isModLoaded("IC2"))
            {
                this.onIC2MachineLoaded();
                this.updated = true;
            }

            ItemStack itemStack = upInventory.extractItem(0, 1, true);
            IBlockState state = this.worldObj.getBlockState(pos);

            if (itemStack != null)
            {
                ItemStack furnaceRecipeResult = FurnaceRecipes.instance().getSmeltingResult(itemStack);
                if (furnaceRecipeResult != null && downInventory.insertItem(0, furnaceRecipeResult, true) == null)
                {
                    double requiredEnergyPerTick = this.getRequiredEnergyPerTick();
                    if (this.receivedEnergyUnit >= requiredEnergyPerTick)
                    {
                        this.receivedEnergyUnit -= requiredEnergyPerTick;

                        this.worldObj.setBlockState(pos, state.withProperty(BlockMetalFurnace.BURNING, Boolean.TRUE));

                        int burnTotalTime = this.getTotalBurnTime();

                        if (++this.burnTime >= burnTotalTime)
                        {
                            this.burnTime = 0;
                            itemStack = upInventory.extractItem(0, 1, false);
                            furnaceRecipeResult = FurnaceRecipes.instance().getSmeltingResult(itemStack).copy();
                            downInventory.insertItem(0, furnaceRecipeResult, false);
                            this.markDirty();
                        }
                    }
                }
            }
            else
            {
                this.burnTime = 0;
                this.worldObj.setBlockState(pos, state.withProperty(BlockMetalFurnace.BURNING, Boolean.FALSE));
            }
        }
        else
        {
            IBlockState blockState = this.worldObj.getBlockState(this.pos);
            boolean burning = blockState.getProperties().containsKey(BlockMetalFurnace.BURNING)
                    && blockState.getValue(BlockMetalFurnace.BURNING).booleanValue();
            if (burning || this.rotationDegree > 0)
            {
                this.rotationDegree += 11.25;
                if (this.rotationDegree >= 360.0)
                {
                    this.rotationDegree -= 360.0;
                }
                this.worldObj.markBlockRangeForRenderUpdate(this.pos, this.pos);
            }
        }
    }

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    private void onIC2MachineLoaded()
    {
        MinecraftForge.EVENT_BUS.post(new EnergyTileLoadEvent(this));
    }

    private void onIC2MachineUnloaded()
    {
        MinecraftForge.EVENT_BUS.post(new EnergyTileUnloadEvent(this));
    }

现在这一个熔炉,应该可以投入IC2的EU网络使用了。

在运行时加入其他Mod

现在我们面临另一个问题:我们不管是运行runClient命令,还是在IDE中运行我们的Minecraft,目前的情况是都没有IC2这个Mod添加到其中。看起来解决这个问题十分简单——把IC2的Mod本体下载下来加到run/mods目录下不就可以了?实际上并非如此。因此Minecraft的代码是混淆过的,而MCP采取了一种极其特殊的混淆与反混淆策略(对此不了解的读者可以参见附录),在开发环境中运行时使用的是MCP Name,而实际运行时,也就是下载到的IC2的Mod本体使用的是Srg Name,也就是说我们需要放置在run/mods目录下的Mod也要是MCP Name,否则是运行不了的。

部分Mod的作者在下载源中同时也会提供一个dev版本(而非universal版本)的Jar(实际上我们在构建Mod时也会生成这样一个文件),通常情况下,将其下载并放置在对应目录就可以正常运行了。不过有的时候Mod作者并不会提供,而有的时候Mod作者编译生成的dev版本所使用的MCP映射表和我们在开发Mod时使用的不同,因此,有的时候,我们需要将universal版本的Mod手动反混淆成适用于自己开发工作的dev版本。

这里需要向读者介绍一个名为simpledeobf的工具,这个工具可以根据基于指定的映射表把一个Jar里的变量方法名等替换成对应的名称。在GitHub下载后,运行下面的命令,就可以把industrialcraft-2-2.3.263-ex18.jar反混淆到industrialcraft-2-2.3.263-ex18-dev.jar这一文件:

java -jar simpledeobf-0.6.jar --input industrialcraft-2-2.3.263-ex18.jar --output industrialcraft-2-2.3.263-ex18-dev.jar --mapFile .gradle/caches/minecraft/de/oceanlabs/mcp/mcp_stable/20/srgs/srg-mcp.srg

--mapFile后指定的是映射表的位置,请读者根据自己的开发环境自行选择。

最后记得打开Jar,并把META-INF目录下MANIFEST.MF之外的文件删除,以跳过Forge对Jar的验证。

处理后的Mod就是我们想要的dev版本了,直接扔进run/mods目录下运行,就一点问题都没有了。

现在运行游戏,在地图中添加一个能量源(比如一块充满电的MFSU),然后再把自己的熔炉连上去,应该就可以注意到熔炉工作的同时,能量源的能量也在不断减少了。

应对没有API的情况

很明显,这样的一个API,只有IC2这一Mod存在时才会有提供,而如果IC2不存在,那么加载在Minecraft中的Java字节码,很可能就不会包含有IC2 API了。那么我们引用的Mod就会出错,因为找不到IC2的API相关的类,也就是包名以ic2.api开头的类。

很多Mod对此的做法是把一个API的相关代码或二进制文件内置(又称Shade)进自己的Mod。虽然Forge会注意到多个Mod都Shade了同一个API,Forge也会在其中选择一个合适的加载进JVM,不过在这里,我们不推荐这么做,因为这样做的缺点有以下两个:

  • Mod的API可能随时都会更新,而保证运行时使用的是最新版本的API的工作应由相应的Mod完成
  • 把API的相关代码或二进制文件Shade进Mod的做法会使Mod增加不必要的文件大小

解决方法自然是有的。Forge为我们提供了若干注解,可以用于在对应Mod不存在时动态移除相应的实现。相关的所有注解都位于net.minecraftforge.fml.common.Optional类下,共有三种:

  • Optional.Method用于注解一个方法,标识该方法将在相应Mod不存在时移除
  • Optional.Interface用于注解一个类,标识该类将在相应Mod不存在时放弃实现特定接口
  • Optional.InterfaceList用于注解一个类,其存储一个接口的列表,每个接口的情况和Optional.Interface相同

  • Optional.MethodOptional.Interface分别有一个名为modid的字段,用于标识相应的方法或接口兼容的Mod是什么

  • Optional.Interface多出来了一个名为iface的字段,请输入相应的接口全名(带包名的)
  • Optional.Interface还有一个名为stripref的字段,其为一布尔值,默认为false,如果为true,代表Forge会在相应Mod不存在时把实现该接口时实现的方法一并抹去,本教程里不会用到这一字段,读者可以考虑情况自行采用

那什么时候需要添加这些注解呢?其实判定的规则很简单:

  • 所有用到Mod的特定API类的方法(包括方法参数、返回值、方法体内部)都应该加上Optional.Method注解
  • 所有实现Mod的特定API接口的类都应该加上Optional.InterfaceOptional.InterfaceList注解,只包含一个元素的Optional.InterfaceList注解和Optional.Interface注解等价

那么如果用到Mod的特定API类的方法十分重要,不管相应的Mod是否存在都要执行呢,比如下面这样一个方法:

    public void foo()
    {
        if (Loader.isModLoaded("IC2"))
        {
            this.a.doSomeThing();
            this.b.doSomeThingElse();
        }
        // something also important while IC2 does not exist
    }

一个常用的做法是把只会在相应Mod存在时的逻辑分离出来,单独形成一个方法,就像下面这样:

    public void foo()
    {
        if (Loader.isModLoaded("IC2"))
        {
            this.bar();
        }
        // something also important while IC2 does not exist
    }

    @Optional.Method(modid = "IC2")
    public void bar()
    {
        this.a.doSomeThing();
        this.b.doSomeThingElse();
    }

根据JVM规范,类字节码中相关类的合法性在相应对象创建时便已检查完成,但方法引用的合法性只会在试图执行该方法时才会检查。所以如果IC2这一Mod不存在,那么相应的条件分支便不会执行,相应方法的合法性也便不会检查。这一特性其实也可以用在其它的场合,比如@SideOnly等。

实际上,之前的代码中,作者把onIC2MachineLoadedonIC2MachineUnloaded两个方法从updateinvalidate这两个在TileEntity的生命周期中十分重要的方法中分离出来,便也是出于这样的考虑。

那么我们现在把这一特性添加到我们的代码上:

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

@Optional.Interface(iface = "ic2.api.energy.tile.IEnergySink", modid = "IC2")
public class TileEntityMetalFurnace extends TileEntity implements ITickable, IEnergySink

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    @Override
    @Optional.Method(modid = "IC2")
    public boolean acceptsEnergyFrom(IEnergyEmitter iEnergyEmitter, EnumFacing enumFacing)
    {
        return true;
    }

src/main/java/com/github/ustc_zzzz/fmltutor/tileentity/TileEntityMetalFurnace.java(部分):

    @Optional.Method(modid = "IC2")
    private void onIC2MachineLoaded()
    {
        MinecraftForge.EVENT_BUS.post(new EnergyTileLoadEvent(this));
    }

    @Optional.Method(modid = "IC2")
    private void onIC2MachineUnloaded()
    {
        MinecraftForge.EVENT_BUS.post(new EnergyTileUnloadEvent(this));
    }

很好,现在我们的Mod就算没有IC2的API,也可以正常运行了。

results matching ""

    No results matching ""