この記事は"ForgeModLoader"を前提MODとしています。 |
ここでは、coremods用modのASM機能を使用するmod作成方法を紹介します。
目次
概要
- ASMとは、Java動的ByteCode操作ライブラリの一つです。
通常コンパイル済みのClassファイルの内容は変更できません。
ですが、JavaにClassファイルを読み込む機能ClassLoaderを乗っ取り、ASMにより動的にClassファイルに変更を施すことで
Minecraft.jar内(※Coremod部分以外のFMLコードも含む)および、mods内のzip等のClassファイルを直接上書きすることなく改変することを可能とします。
本チュートリアルでは、最低限の機能のみの実装のみを紹介するため、ASMライブラリ等の使い方については別途検索ください。
なお、別にASMをかならず使う必要は無く、読み込むファイルを丸ごと差し替える事も出来るため
今までMinecraft.jarに投入するタイプのModをcoremods形式にしてインストールし易くするということも出来ます。
- ※改変を施す部分のコードは、仮実装でありそのままでは動きません。
ソースコメントにしたがって必要な実装を施して書き換えましょう。
- ※パフォーマンスへの影響
この機能によってロードされたクラスは、ロードは原則1回しか行われませんし
完了すれば通常どおりコンパイルされたclassファイルとなんら違いはありません。
施した改変内容以上のパフォーマンスへの影響はほぼ無いです。
ソースの解説
作成するソース
- TutorialCorePlugin.java
coremods読み込みの基点となります。
- TutorialModContainer.java
ModLoaderにおけるmod_XX.classのバージョン情報等のみを格納するものです。
- TutorialTransformer.java
Classの改変機能を実装します。
- META-INF/MANIFEST.MF
coremod用jarにするために必要です。
TutorialCorePluginクラス作成
- CorePluginクラスを作成します。
mod_Tutorialソース
package tutorial.asm; //tutorial : 独自のパッケージ名を付けられます。 //主に他の開発者とファイル名の衝突を避けるために利用します(tutorial.abc.asm等) //asm : 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"; } //coremods読み込みの基点クラスの完全修飾名を返します。 @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; 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 flammpfeil.everybodysnametag.asm; import java.util.List; 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によるバイトコード定数にアクセスするのに便利です。 public class TutorialTransformer implements IClassTransformer , Opcodes { //クラスがロードされる際に呼び出されるメソッドです。 @Override public byte[] transform(String name, byte[] bytes) { try { //改変対象のクラスの完全修飾名です。 //後述でMinecraft.jar内の難読化されるファイルを対象とする場合の簡易な取得方法を紹介します。 String targetClassName = "net.minecraft.src.target"; //FMLRelauncher.side() : Client/Serverどちらか一方のを対象とする場合の判定などに使用できます。 //今回は"CLIENT"と比較し、Client側のファイルを対象としている例です。 //name : 現在ロードされようとしているクラス名が格納されています。 if(FMLRelauncher.side().equals("CLIENT") && name.equals(targetClassName)) { //-------------------------------------------------------------- //クラスファイル丸ごと差し替える場合 //-------------------------------------------------------------- /* ZipFile zf = null; InputStream zi = null; try{ zf = new ZipFile(TutorialCorePlugin.location); //差し替え後のファイルです。coremodのjar内のパスを指定します。 ZipEntry ze = zf.getEntry("target.class"); if(ze != null){ zi = zf.getInputStream(ze); bytes = new byte[(int) ze.getSize()]; zi.read(bytes); } }finally{ if(zi != null) zi.close(); if(zf != null) zf.close(); } */ //-------------------------------------------------------------- //ASMを使用し、既存のクラスファイルに改変を施す場合。 //今回のサンプルでは下記の想定で記述しています。 //EntityLiving.classのdoRenderLivingの先頭に //tutorial/test.classのpassTestRender(EntityLiving,double,double,double)メソッドの呼び出しを追加する //-------------------------------------------------------------- /* ClassNode cnode = new ClassNode(); ClassReader reader = new ClassReader(bytes); //ASMで、bytesに格納されたクラスファイルを解析します。 reader.accept(cnode, 0); MethodNode mnode = null; //改変対象のメソッド名です String targetMethodName = "doRenderLiving" ; //改変対象メソッドの戻り値型および、引数型をあらわします String targetMethoddesc = "(Lnet/minecraft/entity/EntityLiving;DDDFF)V"; //対象のメソッドを検索取得します。 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(); } */ } } catch(Exception e) { throw new RuntimeException("failed : TutorialTransformer loading",e); } return bytes; } }
META-INF/MANIFEST.MFファイルの作成
META-INF/MANIFEST.MFファイルを作成します。 TutorialCorePluginのクラス名を指定します。
Manifest-Version: 1.0 FMLCorePlugin: tutorial.asm.TutorialCorePlugin
jarパッケージにまとめる
前述で作成したclassを形式にまとめます。 coremods用のmodではjar形式でなければなりません。
今回の例では下記のようなファイル構成となります。
- tutorial.jar
- META-INF
- MANIFEST.MF
- tutorial
- asm
- TutorialCorePlugin.class
- TutorialModContainer.class
- TutorialTransformer.class
- asm
- target.class (丸ごと差し替え例用)
- META-INF
通常の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ライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。)
コメントの自動更新を有効化