この記事は"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クラス作成
- 任意の名前のクラス(ここではTutorialCorePlugin)を作成します。
- IFMLLoadingPlugin、
IFMLCallHookを実装します。
cpw.mods.fml.relauncher.IFMLCallHook は、このクラス自体に実装する必要はありません。 getSetupClass() メソッドで返される名前のクラスが、IFMLCallHookを実装している必要があります。 なお、本チュートリアルでは、コールフックを使用していないため、getSetupClass()メソッドの戻り値は null としています。
1.7版
// パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。 // 例) 作者名、ドメイン など (一意性のあるものが好ましい) // // ここでは便宜上 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"); } } }
1.12.2版1.12.2版
// パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。 // 例) 作者名、ドメイン など (一意性のあるものが好ましい) // // ここでは便宜上 tutorial.asm パッケージとしています。 // asm は ASM機能を使うクラスを配置する場合の慣例ですが、解りやすくする以外の意味はなく、必ずこうしないといけないわけではありません。 package tutorial.asm; import java.io.File; import java.util.Map; import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; import net.minecraftforge.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現在) // // 渡されるマップの中身は、net.minecraftforge.fml.relauncher.RelaunchLibraryManager の実装からも確認する事が出来ます。 @Override public void injectData(Map<String, Object> data) { if (data.containsKey("coremodLocation")) { location = (File) data.get("coremodLocation"); } } }
TutorialModContainerクラス作成
- 任意の名前のクラス(ここではTutorialModContainer)を作成します。
- 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 = ""; this.setEnabledState(true); } @Override public boolean registerBus(EventBus bus, LoadController controller) { bus.register(this); return true; } }
TutorialTransformerクラス作成
- 任意の名前のクラス(ここではTutorialTransformer)を作成します。
- 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.*; import org.objectweb.asm.tree.*; import static org.objectweb.asm.Opcodes.*; import static org.objectweb.asm.tree.AbstractInsnNode.*; 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); int len = (int) ze.getSize(); bytes = new byte[len]; // ヒープサイズを超えないように、ストリームからファイルを1024ずつ読み込んで bytes に格納する int MAX_READ = 1024; int readed = 0, readsize, ret; while(readed < len) { readsize = MAX_READ; if (len - readed < MAX_READ ) { readsize = len - readed; } ret = zi.read(bytes, readed, readsize); if (ret == -1) break; readed += ret; } } 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"; // 改変対象メソッドの戻り値型および、引数型をあらわします ※1 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; } }
※1 詳しくはここを参照
META-INF/MANIFEST.MFファイルの作成
- META-INF/MANIFEST.MFファイルを作成します。
- FMLCorePlugin節にTutorialCorePlugin の完全修飾クラスの名(パッケージ名を含む名前)を指定します。
Manifest-Version: 1.0 FMLCorePlugin: tutorial.asm.TutorialCorePlugin
上記の場合、jarに通常のFML(@Mod)クラスやmcmod.infoを含めてもロードされません。 通常のFMLクラスを含めたい場合はFMLCorePluginContainsFMLMod節を追加します。値はなんでもいいです。 この場合、FMLでロードしたいクラス(@Modが付加されたクラス)はTransformerクラス(このチュートリアルではasm)等とpackageが異なる必要があります。
FMLCorePluginContainsFMLMod: *
jarパッケージへのまとめ方
前述で作成したclassをjar形式にまとめます。
coremods用のmodではjar形式でなければ読み込まれません。
今回の例では下記のようなファイル構成となります。
- tutorial.jar
- META-INF
- MANIFEST.MF
- tutorial
- asm
- TutorialCorePlugin.class
- TutorialModContainer.class
- TutorialTransformer.class
- asm
- ModifiedTargetClass.class (丸ごと差し替える場合に用いるクラス)
- META-INF
通常の 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のデバッグ構成があるものとします)
- デバッグの構成を開きます。
- Clientのデバッグ構成を選択し、引数タブを選択します。
- VM引数に、上記の引数を追加します。
- 必要に応じてプラグインへのクラスパスを追加します。
サーバーへのcoremod追加も手順は同じです。
正しくcoremodとして読み込まれた場合、コンソールに
yyyy-MM-dd hh:mm:ss [INFO] [ForgeModLoader] Found a command line coremod : プラグインの完全修飾名
が出力されます。(標準エラーを表示している場合)
難読化への対抗手段の解説
Minecraft1.5.0以降
- 改善された改変対象の比較検索方法
- IClassTransformer.transformメソッドに引数"transformedName"が追加されました。
- これは、易読化されたクラス名が渡されるため、開発時と同様のクラス名で比較することができるようになりました。
- また、Method名やMethodDescについても同様に、易読化する手段が提供されています。
- Method名の場合
- FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(class名,Method名,MethodDesc)
- MethodDescの場合
- FMLDeobfuscatingRemapper.INSTANCE.mapMethodDesc(MethodDesc)
- ※渡すのは、どちらも難読化されているもの 逆に難読化する方法については未調査です。
- ASMライブラリを用いたクラスの部分改変への改善
- 開発環境と同様とまでは行きませんが、deobf済みのclass名、method名、methodDescを用いて追加しても動作するようになりました。
- このため、Eclipse 等の ByteCodeOutline プラグインなどで得たコードをあるていどならば、ほぼそのまま利用できるようになりました。
Minecraft1.4.7まで
- 準備
- 改変したいClassを書き換える。
- recompile > reobfuscate し、難読化後のクラスファイルを作成する。
- クラスを丸ごと差し替える場合
- reobfに出力されたクラスファイル名を見てTutorialTransformerを適宜書き換えましょう。
- ※置き変えてしまうので、同一のクラスを置き買えるものとは競合してしまいます。
- ASMライブラリを用いたクラスの部分改変を行う場合
- reobf/ に出力されたクラスファイルと、改変前のクラスファイルを、JBVD等の適当な ByteCodeViewer で開き、2つを比較して改変された部分を特定します。
- それを元にASMのコードに置き変え、TutorialTransformer を適宜書き換えましょう。
- サンプルのようなメソッドのフック処理程度であれば、Eclipse 等の ByteCodeOutline プラグインなどで、改変部分のコードを直接ASM形式のコードに変換し、:難読化される部分を書きかえる程度の修正で、そのまま使える場合もあります。
- ※ ByteCode操作の場合、同一クラスにに複数の改変を施すことができるため、競合させずにそれぞれの改変を施すこともできます。
雑記
メソッドコールの追加は今回の例のとおりですが
別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが
ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。)
ASMライブラリの既知の不具合 ・関数外のstaticフィールドの初期化を行うコードがあるとClassWriterがNullPointerExceptionを引き起こします。
こめ
- MANIFEST.MF 内の FMLCorePluginContainsFMLMod について追記しました。 --118.22.179.98 2014年2月7日 (金) 08:58 (JST)
- 1.6 では FMLRelauncher.side() >> FMLLaunchHandler.side() と変更されているようです。パッケージもかなり移動しているようなので追記が必要かも知れません。 --122.251.192.117 2013年7月8日 (月) 00:44 (JST)
- とてもいいチュートリアルなので、体裁などを修正させて頂きました。問題があるようでしたら差し戻してください。 --219.125.180.254 2013年1月7日 (月) 22:24 (JST)
コメントの自動更新を有効化