(誤字を勝手に修正。) |
(文面を一部修正。サンプルコードのtabとスペースの混在の解消、コードフォーマットの修正、見やすくするためコメントアウト箇所をメソッドに分離。) |
||
6行目: | 6行目: | ||
==概要== | ==概要== | ||
*coremodとは<br /> | *coremodとは<br /> | ||
− | + | 通常 Minecraft.jar 内に上書きが必要な変更を施すような mod(前提Modなど等)を<br /> | |
− | + | coremods フォルダに入れるだけでインストールできるようにする機能です。 | |
+ | ただし、既存書き換えのMODをそのまま coremod にすることは出来ません。 | ||
+ | Mod作者自身が coremod として作成する必要があります。 | ||
*Classファイルの動的な変更<br /> | *Classファイルの動的な変更<br /> | ||
通常コンパイル済みのClassファイルの内容は変更できません。<br /> | 通常コンパイル済みのClassファイルの内容は変更できません。<br /> | ||
ですがCoremodsでは、JavaにClassファイルを読み込む機能ClassLoaderを乗っ取る機能があります。<br /> | ですがCoremodsでは、JavaにClassファイルを読み込む機能ClassLoaderを乗っ取る機能があります。<br /> | ||
− | + | 読み込む際に動的に変更を施す(後述のASMを使ったり、丸ごと別のクラスを読み込ませる)ことで<br /> | |
− | Minecraft.jar内( | + | Minecraft.jar内(※Coremod部分以外のFMLコードも含む)および、mods内のzip等のClassファイルを直接上書きすることなく改変することを可能とします。 |
*ASMライブラリ<br /> | *ASMライブラリ<br /> | ||
47行目: | 49行目: | ||
mod_Tutorialソース | mod_Tutorialソース | ||
<source lang="java"> | <source lang="java"> | ||
− | + | // パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。 | |
− | + | // 例) 作者名、ドメイン など (一意性のあるものが好ましい) | |
− | + | // | |
− | + | // ここでは便宜上 tutorial.asm パッケージとしています。 | |
+ | // asm は ASM機能を使うクラスを配置する場合の慣例ですが、解りやすくする以外の意味はなく、必ずこうしないといけないわけではありません。 | ||
+ | package tutorial.asm; | ||
import java.io.File; | import java.io.File; | ||
import java.util.Map; | import java.util.Map; | ||
− | |||
import cpw.mods.fml.relauncher.IFMLCallHook; | import cpw.mods.fml.relauncher.IFMLCallHook; | ||
60行目: | 63行目: | ||
import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions; | import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions; | ||
− | + | // TransformerExclusions.value: coremods でロードする際に参照されるため、パッケージ名と一致させてください。 | |
− | + | // IFMLLoadingPlugin: Coremods の基礎インタフェース | |
− | + | // IFMLCallHook: Coremods 内で、coremod 自身のパス等を取得する等の際に必要となります。 | |
+ | |||
@TransformerExclusions(value={"tutorial.asm"}) | @TransformerExclusions(value={"tutorial.asm"}) | ||
public class TutorialCorePlugin implements IFMLLoadingPlugin, IFMLCallHook | public class TutorialCorePlugin implements IFMLLoadingPlugin, IFMLCallHook | ||
− | + | { | |
− | // | + | // coremod 自身のファイルパスの保存用 |
− | + | public static File location; | |
− | //今回は使用しません | + | // 今回は使用しません |
− | + | @Override | |
− | + | public String[] getLibraryRequestClass() | |
− | + | { | |
return null; | 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"; | 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; | |
− | + | } | |
} | } | ||
</source> | </source> | ||
118行目: | 123行目: | ||
**ModContainerクラスを作成します。 | **ModContainerクラスを作成します。 | ||
− | + | バージョン情報のみで良いため、DummyModContainerクラスを継承して作成しています。 | |
FML形式のModではアノテーションを利用して、情報を記載できますが<br /> | FML形式のModではアノテーションを利用して、情報を記載できますが<br /> | ||
coremods用途の場合、通常のアノテーションやmod_XX.class形式のmod読み込み処理より前に取得されるため<br /> | coremods用途の場合、通常のアノテーションやmod_XX.class形式のmod読み込み処理より前に取得されるため<br /> | ||
124行目: | 129行目: | ||
<source lang="java"> | <source lang="java"> | ||
− | + | package tutorial.asm; | |
− | |||
import java.util.Arrays; | import java.util.Arrays; | ||
138行目: | 142行目: | ||
import cpw.mods.fml.common.event.FMLInitializationEvent; | import cpw.mods.fml.common.event.FMLInitializationEvent; | ||
import cpw.mods.fml.common.versioning.ArtifactVersion; | import cpw.mods.fml.common.versioning.ArtifactVersion; | ||
+ | |||
+ | // 必ずしも DummyModContainer を継承している必要はありません。 | ||
+ | // cpw.mods.fml.common.ModContainer さえ実装していれば、どんなクラスでも構いません。 | ||
+ | |||
public class TutorialModContainer extends DummyModContainer | public class TutorialModContainer extends DummyModContainer | ||
{ | { | ||
− | + | public TutorialModContainer() | |
{ | { | ||
super(new ModMetadata()); | super(new ModMetadata()); | ||
146行目: | 154行目: | ||
} | } | ||
− | + | @Override | |
− | + | public List<ArtifactVersion> getDependencies() | |
− | + | { | |
− | + | return super.getDependencies(); | |
− | + | } | |
− | + | @Override | |
− | + | public ModMetadata getMetadata() | |
− | + | { | |
− | + | // modの名前や、他のModと区別するための一意なID情報などを指定します。 | |
− | + | ModMetadata meta = super.getMetadata(); | |
meta.modId = "transformertutorial"; | meta.modId = "transformertutorial"; | ||
167行目: | 175行目: | ||
return meta; | return meta; | ||
− | + | } | |
− | + | @Override | |
− | + | public boolean registerBus(EventBus bus, LoadController controller) | |
− | + | { | |
− | + | bus.register(this); | |
− | + | return true; | |
− | + | } | |
− | + | @Subscribe | |
public void init(FMLInitializationEvent event) | public void init(FMLInitializationEvent event) | ||
{ | { | ||
189行目: | 197行目: | ||
<source lang="java"> | <source lang="java"> | ||
− | package | + | package tutorial.asm; |
− | |||
− | |||
+ | import java.io.IOException; | ||
import java.io.InputStream; | import java.io.InputStream; | ||
import java.util.List; | import java.util.List; | ||
210行目: | 217行目: | ||
import cpw.mods.fml.relauncher.IClassTransformer; | import cpw.mods.fml.relauncher.IClassTransformer; | ||
− | //Opcodes : インプリメントすると、ASMによるバイトコード定数にアクセスするのに便利です。 | + | // Opcodes : インプリメントすると、ASMによるバイトコード定数にアクセスするのに便利です。 |
− | public class TutorialTransformer implements IClassTransformer , Opcodes | + | // 必須ではありません。不用な場合は implements から削除してください。 |
+ | |||
+ | public class TutorialTransformer implements IClassTransformer, Opcodes | ||
{ | { | ||
+ | // 改変対象のクラスの完全修飾名です。 | ||
+ | // 後述でMinecraft.jar内の難読化されるファイルを対象とする場合の簡易な取得方法を紹介します。 | ||
+ | private static final String TARGET_CLASS_NAME = "net.minecraft.src.TargetClass"; | ||
− | //クラスがロードされる際に呼び出されるメソッドです。 | + | // クラスがロードされる際に呼び出されるメソッドです。 |
@Override | @Override | ||
public byte[] transform(String name, byte[] bytes) | 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 | 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); | zi = zf.getInputStream(ze); | ||
bytes = new byte[(int) ze.getSize()]; | bytes = new byte[(int) ze.getSize()]; | ||
zi.read(bytes); | zi.read(bytes); | ||
− | + | } | |
− | + | return bytes; | |
− | + | } | |
+ | finally | ||
+ | { | ||
+ | if (zi != null) | ||
+ | { | ||
zi.close(); | zi.close(); | ||
− | + | } | |
+ | |||
+ | if (zf != null) | ||
+ | { | ||
zf.close(); | 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(); | ||
} | } | ||
314行目: | 349行目: | ||
} | } | ||
} | } | ||
− | |||
</source> | </source> | ||
340行目: | 374行目: | ||
****TutorialModContainer.class | ****TutorialModContainer.class | ||
****TutorialTransformer.class | ****TutorialTransformer.class | ||
− | **target.class | + | **target.class (丸ごと差し替え例用) |
通常のmodのように、zipで圧縮し、jarに拡張子を変更すれば完成です。 | 通常のmodのように、zipで圧縮し、jarに拡張子を変更すれば完成です。 | ||
373行目: | 407行目: | ||
メソッドコールの追加は今回の例のとおりですが<br /> | メソッドコールの追加は今回の例のとおりですが<br /> | ||
別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが<br /> | 別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが<br /> | ||
− | + | ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。) | |
<comments /> | <comments /> | ||
[[Category:その他]] | [[Category:その他]] |
2013年1月7日 (月) 20:43時点における版
この記事は"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
- 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ライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。)
コメントの自動更新を有効化