提供: Minecraft Modding Wiki
2013年1月7日 (月) 20:43時点における219.125.180.254 (トーク)による版 (文面を一部修正。サンプルコードのtabとスペースの混在の解消、コードフォーマットの修正、見やすくするためコメントアウト箇所をメソッドに分離。)
移動先: 案内検索

この記事は"ForgeModLoader"を前提MODとしています。

ここでは、coremodsフォルダに入れるタイプのmod作成方法を紹介します。

概要

  • coremodとは

通常 Minecraft.jar 内に上書きが必要な変更を施すような mod(前提Modなど等)を
coremods フォルダに入れるだけでインストールできるようにする機能です。 ただし、既存書き換えのMODをそのまま coremod にすることは出来ません。 Mod作者自身が coremod として作成する必要があります。

  • Classファイルの動的な変更

通常コンパイル済みのClassファイルの内容は変更できません。
ですがCoremodsでは、JavaにClassファイルを読み込む機能ClassLoaderを乗っ取る機能があります。
読み込む際に動的に変更を施す(後述のASMを使ったり、丸ごと別のクラスを読み込ませる)ことで
Minecraft.jar内(※Coremod部分以外のFMLコードも含む)および、mods内のzip等のClassファイルを直接上書きすることなく改変することを可能とします。

  • ASMライブラリ

ASMとは動的にClassファイルに変更を施すことできるバイトコード操作ライブラリのことです。
FMLをインストールするとバンドルされているので、別途ライブラリを添付することなく、そのまま利用が可能です。
本チュートリアルでは、最低限の機能のみの実装のみを紹介するため、ASMライブラリ等の使い方については別途検索ください。

※改変を施す部分のコードは、仮実装でありそのままでは動きません。
ソースコメントにしたがって必要な実装を施して書き換えましょう。

※パフォーマンスへの影響
この機能によってのロードは原則1回しか行われません
ロードされたクラスは、通常どおりコンパイルされたclassファイルとなんら違いはありません。
施した改変内容以上のパフォーマンスへの影響はほぼ無いです。

ソースの解説

作成するソース

  • TutorialCorePlugin.java

coremods読み込みの基点となります。

  • TutorialModContainer.java

ModLoaderにおけるmod_XX.classのバージョン情報等のみを格納するものです。
FMLでは情報を格納するのにアノテーションを使うこともできますが
Coremodは読み込み方法が異なるので記載も別となっています。

  • TutorialTransformer.java

Classの改変機能を実装します。

  • META-INF/MANIFEST.MF

FMLがcoremodである事を認識するのに必要です。

TutorialCorePluginクラス作成

    • CorePluginクラスを作成します。

mod_Tutorialソース

// パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。
// 例) 作者名、ドメイン など (一意性のあるものが好ましい)
// 
// ここでは便宜上 tutorial.asm パッケージとしています。
// asm は ASM機能を使うクラスを配置する場合の慣例ですが、解りやすくする以外の意味はなく、必ずこうしないといけないわけではありません。
package tutorial.asm;

import java.io.File;
import java.util.Map;

import cpw.mods.fml.relauncher.IFMLCallHook;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions;

// TransformerExclusions.value: coremods でロードする際に参照されるため、パッケージ名と一致させてください。
// IFMLLoadingPlugin: Coremods の基礎インタフェース
// IFMLCallHook: Coremods 内で、coremod 自身のパス等を取得する等の際に必要となります。

@TransformerExclusions(value={"tutorial.asm"})
public class TutorialCorePlugin implements IFMLLoadingPlugin, IFMLCallHook
{
    // coremod 自身のファイルパスの保存用
    public static File location;
    
    // 今回は使用しません
    @Override
    public String[] getLibraryRequestClass()
    {
        return null;
    }

    // Class の改変機能を実装したクラスの完全修飾名を返します。
    @Override
    public String[] getASMTransformerClass()
    {
        return new String[]{"tutorial.asm.TutorialTransformer"};
    }

    // coremod の名前やバージョン情報の格納クラスの完全修飾名を返します。
    @Override
    public String getModContainerClass()
    {
        return "tutorial.asm.TutorialModContainer";
    }

    // coremod 読み込みの基点クラスの完全修飾名を返します。
    @Override
    public String getSetupClass()
    {
        return "tutorial.asm.TutorialCorePlugin";
    }

    // IFMLCallHook のメソッドです。
    // 今回は coremod 自身の jar ファイルパスを取得します。
    @Override
    public void injectData(Map<String, Object> data)
    {
        if (data.containsKey("coremodLocation"))
        {
            location = (File) data.get("coremodLocation");
        }
    }

    @Override
    public Void call() throws Exception
    {
        return null;
    }
 }

TutorialModContainerクラス作成

    • ModContainerクラスを作成します。

バージョン情報のみで良いため、DummyModContainerクラスを継承して作成しています。 FML形式のModではアノテーションを利用して、情報を記載できますが
coremods用途の場合、通常のアノテーションやmod_XX.class形式のmod読み込み処理より前に取得されるため
別の形式で記載することが必要になっています。

package tutorial.asm;

import java.util.Arrays;
import java.util.List;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

import cpw.mods.fml.common.DummyModContainer;
import cpw.mods.fml.common.LoadController;
import cpw.mods.fml.common.ModMetadata;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.versioning.ArtifactVersion;

// 必ずしも DummyModContainer を継承している必要はありません。
// cpw.mods.fml.common.ModContainer さえ実装していれば、どんなクラスでも構いません。

public class TutorialModContainer extends DummyModContainer
{
    public TutorialModContainer()
    {
        super(new ModMetadata());
        getMetadata();
    }

    @Override
    public List<ArtifactVersion> getDependencies()
    {
        return super.getDependencies();
    }

    @Override
    public ModMetadata getMetadata()
    {
        // modの名前や、他のModと区別するための一意なID情報などを指定します。
        ModMetadata meta = super.getMetadata();

        meta.modId       = "transformertutorial";
        meta.name        = "TransformerTutorial";
        meta.version     = "1.0.0";
        meta.authorList  = Arrays.asList("Author");
        meta.description = "";
        meta.url         = "";
        meta.credits     = "";

        return meta;
    }

    @Override
    public boolean registerBus(EventBus bus, LoadController controller)
    {
        bus.register(this);
        return true;
    }

    @Subscribe
    public void init(FMLInitializationEvent event)
    {
    }
}

TutorialTransformerクラス作成

    • Transformerクラスを作成します。

IClassTransformerインタフェースを実装します。

package tutorial.asm;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;

import cpw.mods.fml.relauncher.FMLRelauncher;
import cpw.mods.fml.relauncher.IClassTransformer;

// Opcodes : インプリメントすると、ASMによるバイトコード定数にアクセスするのに便利です。
// 必須ではありません。不用な場合は implements から削除してください。

public class TutorialTransformer implements IClassTransformer, Opcodes
{
    // 改変対象のクラスの完全修飾名です。
    // 後述でMinecraft.jar内の難読化されるファイルを対象とする場合の簡易な取得方法を紹介します。
    private static final String TARGET_CLASS_NAME = "net.minecraft.src.TargetClass";

    // クラスがロードされる際に呼び出されるメソッドです。
    @Override
    public byte[] transform(String name, byte[] bytes)
    {
        // FMLRelauncher.side() : Client/Server どちらか一方のを対象とする場合や、
        // 一つのMODで Client/Sever 両方に対応したMODで、この値を判定して処理を変える事ができます。
        // 今回は"CLIENT"と比較し、Client側のファイルを対象としている例です。
        // Client側専用のMODとして公開するのであれば、判定は必須ではありません。

        // name : 現在ロードされようとしているクラス名が格納されています。
        if (!FMLRelauncher.side().equals("CLIENT") || !name.equals(TARGET_CLASS_NAME))
        {
            // 処理対象外なので何もしない
            return bytes;
        }

        try
        {
            // --------------------------------------------------------------
            // クラスファイル丸ごと差し替える場合
            // --------------------------------------------------------------
            // return replaceClass(bytes);

            // --------------------------------------------------------------
            // ASMを使用し、既存のクラスファイルに改変を施す場合。
            // --------------------------------------------------------------
            // return hookDoRenderLivingMethod(bytes);

        }
        catch (Exception e)
        {
            throw new RuntimeException("failed : TutorialTransformer loading", e);
        }
    }

    // 下記の想定で実装されています。
    // 対象クラスの bytes を ModifiedTargetClass.class ファイルに置き換える
    private byte[] replaceClass(byte[] bytes) throws IOException
    {
        ZipFile zf = null;
        InputStream zi = null;

        try
        {
            zf = new ZipFile(TutorialCorePlugin.location);

            // 差し替え後のファイルです。coremodのjar内のパスを指定します。
            ZipEntry ze = zf.getEntry("ModifiedTargetClass.class");

            if (ze != null)
            {
                zi = zf.getInputStream(ze);
                bytes = new byte[(int) ze.getSize()];
                zi.read(bytes);
            }
            return bytes;
        }
        finally
        {
            if (zi != null)
            {
                zi.close();
            }

            if (zf != null)
            {
                zf.close();
            }
        }
    }

    // 下記の想定で実装されています。
    // EntityLiving.class の doRenderLiving の先頭に
    // tutorial/test.class の passTestRender(EntityLiving, double, double, double)メソッドの呼び出しを追加する。
    private byte[] hookDoRenderLivingMethod(byte[] bytes)
    {
        // ASMで、bytesに格納されたクラスファイルを解析します。
        ClassNode cnode = new ClassNode();
        ClassReader reader = new ClassReader(bytes);
        reader.accept(cnode, 0);

        // 改変対象のメソッド名です
        String targetMethodName = "doRenderLiving";

        // 改変対象メソッドの戻り値型および、引数型をあらわします
        String targetMethoddesc = "(Lnet/minecraft/entity/EntityLiving;DDDFF)V";

        // 対象のメソッドを検索取得します。
        MethodNode mnode = null;
        for (MethodNode curMnode : (List<MethodNode>) cnode.methods)
        {
            if (targetMethodName.equals(curMnode.name) && targetMethoddesc.equals(curMnode.desc))
            {
                mnode = curMnode;
                break;
            }
        }

        if (mnode != null)
        {
            InsnList overrideList = new InsnList();

            // メソッドコールを、バイトコードであらわした例です。
            overrideList.add(new VarInsnNode(ALOAD, 1));
            overrideList.add(new VarInsnNode(DLOAD, 2));
            overrideList.add(new VarInsnNode(DLOAD, 4));
            overrideList.add(new VarInsnNode(DLOAD, 6));
            overrideList
                    .add(new MethodInsnNode(INVOKESTATIC, "tutorial/test", "passTestRender", "(LEntityLiving;DDD)V"));

            // mnode.instructions.get(1)で、対象のメソッドの先頭を取得
            // mnode.instructions.insertで、指定した位置にバイトコードを挿入します。
            mnode.instructions.insert(mnode.instructions.get(1), overrideList);

            // 改変したクラスファイルをバイト列に書き出します
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
            cnode.accept(cw);
            bytes = cw.toByteArray();
        }

        return bytes;
    }
}

META-INF/MANIFEST.MFファイルの作成

META-INF/MANIFEST.MFファイルを作成します。 TutorialCorePluginのクラス名を指定します。

Manifest-Version: 1.0
FMLCorePlugin: tutorial.asm.TutorialCorePlugin

jarパッケージにまとめる

前述で作成したclassをjar形式にまとめます。
coremods用のmodではjar形式でなければ読み込まれません。

今回の例では下記のようなファイル構成となります。

  • tutorial.jar
    • META-INF
      • MANIFEST.MF
    • tutorial
      • asm
        • TutorialCorePlugin.class
        • TutorialModContainer.class
        • TutorialTransformer.class
    • target.class (丸ごと差し替え例用)

通常のmodのように、zipで圧縮し、jarに拡張子を変更すれば完成です。

動作確認

coremod形式のmodは、基本的にはEclipseでのデバッグでは読み込みが行われません。 機能の実装部分については、元ファイルを直接改変するなどしましょう。

coremodとしての動作確認は、実際の動作環境へ放り込んで行うことになります。

難読化への対抗手段の解説

  • 準備

改変したいClassを通常通り書き換え。recompile>reobfuscateしてしまいます。

  • 丸ごと差し替える場合

reobfに出力されたクラスファイル名を見てTutorialTransformerを適宜書き換えましょう。

※置き変えてしまうので、同一のクラスを置き買えるものとは競合してしまいます。

  • ASMによる改変を行う場合

reobfに出力されたクラスファイルと、改変前のクラスファイルを
JBVD等の適当なByteCodeViewerで開き、2つを比較して改変された部分を特定します。
それを元にASMのコードに置き変えTutorialTransformerを適宜かきかえましょう。

サンプルのようなメソッドのフック処理程度であれば
Eclipse等のByteCodeOutlineプラグインなどで、改変部分のコードを直接ASM形式のコードに変換して
難読化される部分を書きかえる程度で、そのまま使える場合もあります。

※ByteCode操作のばあい、同一クラスにに複数の改変を施すことができるため、競合させずにそれぞれの改変を施すこともできます。

  • 雑記

メソッドコールの追加は今回の例のとおりですが
別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが
ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。)


自分のコメントを追加
Minecraft Modding Wikiはすべてのコメントを歓迎します。匿名で投稿したくない場合は、アカウント作成またはログインしてください。無料です。