MOD製作チュートリアル > クラス書き換えの利用

概要

Minecraft内のクラス書き換えインターフェイス、"IClassTransformer"を用いてバニラクラスの書き換えを行う。

このチュートリアルはとても難解な内容を含んでいます。
  • 中級編までをひとしきり出来るようになってから挑戦してください。
  • バニラクラスを書き換えるため競合に注意して行ってください。

事前準備

今回は事前の準備が幾つか必要である。
まず、クラス書き換え用の別ディレクトリをsrc/main/java内に作成する。今回はalcore.asmとする。
ディレクトリを分けずにやると正しく読み込めなくなるため必ずディレクトリを分ける。
次に、coremodとして働かせる(→クラス書き換え用に通常のMODより早く読み込む)MODとして、開発環境とビルド環境にそれぞれ読み込ませる。
今回はcoremodのクラスをalcore.asm.ALCorePluginとする。

ビルド環境

MODのjarファイル内に情報を埋め込めるよう、build.gradleファイルの末尾に以下のコードを追加する。
//manifestファイルの書き込みをビルドに追加する。これにより、coremodの読み込みが可能になる。
//なお、テスト起動時にはこのオプションが効かないためVMオプションに以下の引数を追加する。
//-Dfml.coreMods.load=alcore.asm.ALCorePlugin
jar {
    manifest {
        //coremodのパスを指定する。
        attributes FMLCorePlugin: 'alcore.asm.ALCorePlugin'
        //今回はAluminiummodが別で入っているためそれも読み込む。
        attributes FMLCorePluginContainsFMLMod : 'true'
    }
}
 

開発環境

開発環境では上記のものが効かないためVMオプションに以下の引数を追加する。
-Dfml.coreMods.load=alcore.asm.ALCorePlugin
 

ソースコード

今回は、ディレクトリ"tutorial.aluminiummod"でアイテムの追加レシピの追加及び鉱石の追加を参考にアルミニウムインゴット、アルミニウム鉱石とその精錬レシピまでを実装してある前提で解説する。
もしそこまでのやり方がわからない場合は、一旦上記三つのチュートリアルをよく読んでから戻ってきていただきたい。
  • ALCorePlugin.java
package alcore.asm;
 
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
import java.util.Map;
 
public class ALCorePlugin implements IFMLLoadingPlugin {
    //書き換え機能を実装したクラス一覧を渡す関数。書き方はパッケージ名+クラス名。
    @Override
    public String[] getASMTransformerClass() {
        return new String[]{"alcore.asm.ALCoreTransformer"};
    }
 
    //あとは今回は使わない為適当に。
    @Override
    public String getSetupClass() {
        return null;
    }
 
    @Override
    public void injectData(Map<String, Object> data) {
    }
 
    @Override
    public String getAccessTransformerClass() {
        return null;
    }
 
    @Override
    public String getModContainerClass() {
        return null;
    }
}
 

  • ALCoreTransformer.java
package alcore.asm;
 
import cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.*;
 
import static org.objectweb.asm.Opcodes.*;
 
public class ALCoreTransformer implements IClassTransformer {
    //IClassTransformerにより呼ばれる書き換え用のメソッド。
    @Override
    public byte[] transform(final String name, final String transformedName, byte[] bytes) {
        //対象クラス以外を除外する。対象は呼び出し元があるクラスである。
        if (!"net.minecraft.tileentity.TileEntityFurnace".equals(transformedName)) return bytes;
        ClassReader cr = new ClassReader(bytes);
        ClassWriter cw = new ClassWriter(1);
        ClassVisitor cv = new ClassVisitor(ASM4, cw) {
            //クラス内のメソッドを訪れる。
            @Override
            public MethodVisitor visitMethod(int access, String methodName, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, methodName, desc, signature, exceptions);
                //呼び出し元のメソッドを参照していることを確認する。
                String s1 = FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(name, methodName, desc);
                //C:\Users\<ユーザー名>\.gradle\caches\minecraft\net\minecraftforge\forge\1.7.10-10.13.4.1558-1.7.10\forge-1.7.10-10.13.4.1558-1.7.10-decomp.jar\より名称を検索、比較してメソッドの難読化名を探す。
                if (s1.equals("updateEntity") || s1.equals("func_145845_h") || methodName.equals("updateEntity") || methodName.equals("func_145845_h")) {
                    //もし対象だったらMethodVisitorを差し替える。
                    mv = new MethodVisitor(ASM4, mv) {
                        //呼び出す予定のメソッドを読み込む。
                        @Override
                        public void visitMethodInsn(int opcode, String owner, String methodName, String desc, boolean itf) {
                            //書き換え対象のメソッドであることを確認する。
                            String s2 = FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(name, methodName, desc);
                            if (s2.equals("isBurning") || s2.equals("func_145950_i") || methodName.equals("isBurning") || methodName.equals("func_145950_i")) {
                                //引数として次に渡す値にthisを指定する。
                                mv.visitVarInsn(ALOAD, 0);
                                //メソッドを読み込む。INVOKESTATICでstaticメソッドを呼び出す。
                                super.visitMethodInsn(INVOKESTATIC, "alcore/asm/ALCoreHook", "ALFurnaceHook", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType("net/minecraft/tileentity/TileEntity")), false);
                 //今回はフックを差し込むだけだが、ここで書き換えも出来る。
                            }
                            //今回は最後に元のクラスを読み込んでreturnする。
                            super.visitMethodInsn(opcode, owner, methodName, desc, itf);
                        }
                    };
                }
                return mv;
            }
        };
        cr.accept(cv, ClassReader.EXPAND_FRAMES);
        return cw.toByteArray();
    }
}
 

  • ALCoreHook.java
package alcore.asm;
 
import net.minecraft.entity.item.EntityItem;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityFurnace;
import tutorial.aluminiummod.AluminiumMod;
 
public class ALCoreHook {
    //差し込むフック。フック内の詳細は割愛。簡単に言うと、鉄鉱石とアルミニウムインゴットがある状態で火がついていたら爆発して鉄を撒き散らす。
    public static void ALFurnaceHook(TileEntity tileEntity) {
        if (tileEntity instanceof TileEntityFurnace) {
            TileEntityFurnace furnace = ((TileEntityFurnace) tileEntity);
            if (furnace.furnaceBurnTime > 0
                    && furnace.getStackInSlot(0) != null && furnace.getStackInSlot(0).getItem() == Item.getItemFromBlock(Blocks.iron_ore)
                    && furnace.getStackInSlot(2) != null && furnace.getStackInSlot(2).getItem() == AluminiumMod.aluminium) {
                furnace.setInventorySlotContents(0, null);
                furnace.setInventorySlotContents(1, null);
                furnace.setInventorySlotContents(2, null);
                if (!furnace.getWorldObj().isRemote) {
                    furnace.getWorldObj().newExplosion(null, furnace.xCoord, furnace.yCoord, furnace.zCoord, 15, true, true);
                    EntityItem entityItem = new EntityItem(furnace.getWorldObj(), furnace.xCoord, furnace.yCoord, furnace.zCoord,
                            new ItemStack(Items.iron_ingot, furnace.getWorldObj().rand.nextInt(32) + 32));
                    entityItem.fireResistance = Integer.MAX_VALUE;
                    furnace.getWorldObj().spawnEntityInWorld(entityItem);
                }
            }
        }
    }
}
 

解説

ALCorePlugin.java

public String[] getASMTransformerClass()

書き換えするコードを記述したIClassTransformerを実装してあるクラスを渡す。
配列形式であるため、トランスフォーマーはいくつも追加することが出来る。

public String getModContainerClass()

今回はModとして見えないように実装するため、nullを返してある。
もしModとして見えるようにする(CodeChickenCoreのように別MODとしてリリースする)ならば、DummyModContainerを継承したクラス名をStringで渡す。
ex. "alcore.asm.ALModContainer"

ALCoreTransformer.java

public byte[] transform(final String name, final String transformedName, byte[] bytes)

書き換えの実態を担うクラス。引数は クラス名、易読化クラス名、バイトコードである。
バイトコードはそのままでは読めない 読めたらすごい ため、
ClassReader(バイトコードをASMで読めるようにする)→ClassWriter(ClassReaderを書き出せるようにする)→ClassVisitor(内部クラスでクラスを書き換えられるようにする)
と手順を踏んで使えるようにする。
最終的にClassWriterをバイトコードに書き出して返す。
間違って別のクラス・メソッドを書き換えてしまってはいけないため、名称でのチェックが必要。

FMLDeobfuscatingRemapper

public String mapMethodName(String owner, String name, String desc)

クラス名、難読化されたメソッド名、引数記述Type(後述)の順で引数を与えて易読化メソッド名を返す……
はずなのだが、forge1.7.10だと何故かうまく動かない事がある。そのため、コメントの通り難読化されたソースコードと易読化されたものを比較し、難読化名も判定しておいたほうがいい。

ClassVisitor

public MethodVisitor visitMethod(int access, String methodName, String desc, String signature, String[] exceptions)

ここで、呼び出し元のメソッドをチェックする。今回は"onUpdateEntity"から呼ばれた"isBurning"に対して書き換えを施しているが、もし無差別にあるメソッドを書き換えるならif節は不要。

MethodVisitor

public void visitMethodInsn(int opcode, String owner, String methodName, String desc, boolean itf)

メソッド内での呼び出しされたメソッドを訪れる。
同様にintやstringなども訪れることが出来る(Methodvisitorのソース参照)。
ここで他のメソッドを呼び出してやることで、書き換えが実現できる。

ビルド・コンパイル

coremodが正しく読み込まれているならば以下が出力されているはずである。テストプレイ時はまずこれをチェックしてみてほしい。
Found a command line coremod : alcore.asm.ALCorePlugin
 

また、このデータをjarファイルにコンパイルしたらMinecraftのディレクトリにあるmodsフォルダに入れてテストプレイをしたほうが良い。
難読化の都合上環境を変えるとうまく動かないためである。
不具合がある場合はロガーやSystem.out.printInを使って引数や変数の内容を出力させてみるのも手である。

コメント

この項目に関する質問などをどうぞ。
名前: