提供: Minecraft Modding Wiki
2013年1月13日 (日) 06:18時点における219.125.180.254 (トーク)による版 (仕組み)
移動先: 案内検索

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

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

概要

Coremodとは

通常 Minecraft.jar 内に上書きが必要な変更を施すような 前提MOD(API系)や既存書き換え系MODなどを、coremods フォルダに入れるだけでインストールできるようにする、FMLの機能です。

ただし、既存書き換えのMODをそのまま coremod にすることは出来ません。 Mod作者自身が coremod として作成する必要があります。

仕組み

FMLでは、Minecraftが起動した直後に処理を割り込ませ、現在のスレッドの ContextClassLoader に、cpw.mods.fml.relauncher.RelaunchClassLoader を設定しています。このクラスローダーでは、クラスロード時にバイトコードのクラスを編集するポイントが設けられており、Minecraftが実行中に読み込む殆どのクラス(※)を、ロード時に動的に改変する事が可能となっています。

※全てのクラスの変換が行えるわけではありません。IClassTransformer を通さないよう、変換対象から除外登録されている一部のパッケージ以下のクラスは、本来のクラスローダーであるシステムクラスローダーによりロードされてしまうため、動的な変換処理を行えません。またプラグイン側で TransformerExclusions アノテーションを用い除外設定されているパッケージやクラスも変換対象外となります。

詳細は RelaunchClassLoader の実装を確認するか、実際に試してみて判断ください。また、クラスローダーそのものの仕組みについては、Web検索で多くの情報を得る事が出来ますのでここでは割愛します。

当チュートリアルでは、主にこのクラス変換機能の実装方法を解説します。

この機能を用いることで、Minecraftの実行中に、初めてクラスがロードされた際に、クラスのバイトコードを置換、または部分的に書き換える事ができるようになります。動的に書き換えるため、Minecraft.jar 内(※Coremod部分以外のFMLコードも含む)および、mods 内の zip 等のClassファイルを、直接上書きして変更することなく改変することを可能とします。

動的なクラス書き換えって、重くないの?

クラスのロードは、基本的に最初にクラスの参照が要求された際に、1回だけ行われます。一度ロードされたクラスは、通常どおりコンパイルされたclassファイルとなんら違いはありません。

つまり、施した改変内容以上のパフォーマンスへの影響は、ほぼ無いと考えて差し支えありません。

ASMライブラリ

ASMライブラリとは、クラスのバイトコードに対し、動的に変更を施すことできるバイトコード操作ライブラリのことです。

FMLにバンドルされているので、別途ライブラリを添付することなく、FMLをインストールするだけで利用が可能です。

本チュートリアルでは、最低限の機能のみの実装のみを紹介するため、ASMライブラリ等の使い方については別途検索ください。

実装

今回作成するソースファイルは以下になります。

tutorial/asm/TutorialCorePlugin.java
coremods読み込みの基点となります。
tutorial/asm/TutorialModContainer.java
ModLoaderにおけるmod_XX.classのバージョン情報等のみを格納するものです。
FMLでは情報を格納するのにアノテーションや mcmod.info ファイルを使うこともできます
しかし、Coremodは読み込み方法が異なるので記載も別となっています。
tutorial/asm/TutorialTransformer.java
Classの改変機能を実装します。
META-INF/MANIFEST.MF
ソースファイルではありませんが、FMLがcoremodである事を認識するのに必要です。

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

TutorialCorePluginクラス作成

  1. 任意の名前のクラス(ここではTutorialCorePlugin)を作成します。
  2. IFMLLoadingPlugin、IFMLCallHook を実装します。

cpw.mods.fml.relauncher.IFMLCallHook は、このクラス自体に実装する必要はありません。 getSetupClass() メソッドで返される名前のクラスが、IFMLCallHookを実装している必要があります。 なお、本チュートリアルでは、コールフックを使用していないため、getSetupClass()メソッドの戻り値は null としています。

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

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

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

// TransformerExclusions: Transformerから除外するクラス名を設定するためのアノテーション
// 
//  値は文字列の配列で、複数指定も可能です。
//  指定した文字列と前方一致するクラス名は、後述のクラスの動的な変換処理から除外されます。
//  例えば、自身のクラスが変換されないように、自身のパッケージ以下を除外指定する、などが出来ます。
//  必須ではありません。必要に応じて設定してください。
//  本チュートリアルでは、参考として自身のパッケージを変換処理から除外しています。
// 
// IFMLLoadingPlugin: Coremods としてロードするために必要なインタフェース

@TransformerExclusions({"tutorial.asm"})
public class TutorialCorePlugin implements IFMLLoadingPlugin
{
    // coremod の jar ファイルのパス抽象表現を保持します。
    // Transformer 以外から呼ばれることは考慮しないため、デフォルトのアクセス指定子としています。

    static File location;

    // このプラグインが動作するために必要となるライブラリセットのクラス名の配列です。
    // 本チュートリアルでは使用しないため説明は割愛します。
    // インターフェイスの javadocや、FMLCorePlugin クラスの実装を参照してみてください。

    @Override
    public String[] getLibraryRequestClass()
    {
        return null;
    }

    // Class の改変機能を実装したクラスの完全修飾名の配列を返します。
    // 本チュートリアルの変換処理クラスは TutorialTransformer のみなので、一つだけを配列に詰め返却しています。

    @Override
    public String[] getASMTransformerClass()
    {
        return new String[]{"tutorial.asm.TutorialTransformer"};
    }

    // coremod の名前やバージョン情報を格納しているクラスの完全修飾名を返します。

    @Override
    public String getModContainerClass()
    {
        return "tutorial.asm.TutorialModContainer";
    }

    // IFMLCallHook を実装しているクラス名を返す必要があります。
    // 本チュートリアルでは、コールフックを用いないため、こちらの説明も割愛します。

    @Override
    public String getSetupClass()
    {
        return null;
    }

    // IFMLLoadingPlugin のメソッドです。(IFMLCallHook にも同じシグネチャーのメソッドがありますが、違います)
    // 今回は coremod 自身の jar ファイルパスを取得しています。これは後述のトランスフォーマークラスで、
    // jarから置換用クラスを取得しているためで、そのような処理を行わないのであれば何も実装しなくても構いません。
    // 
    // なお、IFMLLoadingPlugin のメソッドとして呼ばれた際は、"mcLocation"、"coremodList"、"coremodLocation" の3つ、
    // IFMLCallHook のメソッドとして呼ばれた際は、"classLoader" がマップに設定されています。(FML#511現在)
    // 
    // 渡されるマップの中身は、cpw.mods.fml.relauncher.RelaunchLibraryManager の実装からも確認する事が出来ます。

    @Override
    public void injectData(Map<String, Object> data)
    {
        if (data.containsKey("coremodLocation"))
        {
            location = (File) data.get("coremodLocation");
        }
    }
 }

TutorialModContainerクラス作成

  1. 任意の名前のクラス(ここではTutorialModContainer)を作成します。
  2. ModContainer を実装します。

Coremod 用のModでは、通常の mod読み込み処理より前にModContainerが要求されるため、MetaDataアノテーションや、mcmod.infoなど、MODのメタデータ設定処理を利用できません。

そのため、別の形式で記載することが必要になっています。

ID、名前、バージョン情報を変更できれば良いため、ここでは DummyModContainer クラスを継承して作成しています。

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());

        // 他のModと区別するための一意なIDやmodの名前など、MODのメタデータを設定します。
        ModMetadata meta = getMetadata();

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

TutorialTransformerクラス作成

  1. 任意の名前のクラス(ここではTutorialTransformer)を作成します。
  2. 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ファイルの作成

  1. META-INF/MANIFEST.MFファイルを作成します。
  2. 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
    • ModifiedTargetClass.class (丸ごと差し替える場合に用いるクラス)

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

※jarコマンドを用いて作成することも可能ですが、今回は割愛します。

テスト、デバッグ方法について

coremod 形式の mod は、Eclipse でのデバッグで動作させるのは手間がかかります。 機能の実装部分については、書き換え元のクラスを、直接改変して試したほうが簡単でしょう。

coremodとしての動作確認は、実際の動作環境へ放り込んで行ってください。

通常 coremods フォルダーにある場合のみ coremod として読み込まれるため、作成した coremod にただクラスパスを通すだけでは読み込まれません。しかし、FMLには環境変数 fml.coreMods.load を用いた coremod の読み込み機能が用意されています。これを用いることで、任意のプラグインを coremod として読み込ませる事が可能です。

なお、環境変数に指定するのは割と不便なので、JVM起動時の引数としてVMに環境変数を追加して指定するほうが便利です。

やり方はとても簡単で、デバッグ実行する際のJVMの引数に、以下を追加します。

-Dfml.coreMods.load=完全修飾クラス名CSV

※-Dオプションについての詳細は検索してください。

引数にはカンマ区切りで複数のクラス名を指定可能です。なお、両端トリムはされないため余計なスペースなどを含まないよう注意してください。(Ex: =com.example.mod.PluginAAA, com.example.mod.pluginBBB ; カンマの後にスペースを挟んでしまっている点が間違い。)

またこの際、coremod のクラスのある場所へのクラスパスを追加するのも忘れないでください。

  • Eclipseでの設定手順(MCP付属のワークスペースを用いており、既にClientのデバッグ構成があるものとします)
  1. デバッグの構成を開きます。
  2. Clientのデバッグ構成を選択し、引数タブを選択します。
  3. VM引数に、上記の引数を追加します。
  4. 必要に応じてプラグインへのクラスパスを追加します。

サーバーへのcoremod追加も手順は同じです。

正しくcoremodとして読み込まれた場合、コンソールに

yyyy-MM-dd hh:mm:ss [INFO] [ForgeModLoader] Found a command line coremod : プラグインの完全修飾名

が出力されます。(標準エラーを表示している場合)

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

準備

  1. 改変したいClassを書き換える。
  2. recompile > reobfuscate し、難読化後のクラスファイルを作成する。

クラスを丸ごと差し替える場合

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

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

ASMライブラリを用いたクラスの部分改変を行う場合

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

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

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

雑記

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


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


  • とてもいいチュートリアルなので、体裁などを修正させて頂きました。問題があるようでしたら差し戻してください。 --219.125.180.254 2013年1月7日 (月) 22:24 (JST)