提供: Minecraft Modding Wiki
この編集を取り消せます。
下記の差分を確認して、本当に取り消していいか検証してください。よろしければ変更を保存して取り消しを完了してください。
最新版 | 編集中の文章 | ||
12行目: | 12行目: | ||
===仕組み=== | ===仕組み=== | ||
− | FMLでは、Minecraftが起動した直後に処理を割り込ませ、現在のスレッドの ContextClassLoader | + | FMLでは、Minecraftが起動した直後に処理を割り込ませ、現在のスレッドの ContextClassLoader に、cpw.mods.fml.relauncher.RelaunchClassLoader を設定しています。このクラスローダーでは、クラスロード時にバイトコードのクラスを編集するポイントが設けられており、Minecraftが実行中に読み込む殆どのクラス(※)を、ロード時に動的に改変する事が可能となっています。 |
※全てのクラスの変換が行えるわけではありません。IClassTransformer を通さないよう、変換対象から除外登録されている一部のパッケージ以下のクラスは、本来のクラスローダーであるシステムクラスローダーによりロードされてしまうため、動的な変換処理を行えません。またプラグイン側で TransformerExclusions アノテーションを用い除外設定されているパッケージやクラスも変換対象外となります。 | ※全てのクラスの変換が行えるわけではありません。IClassTransformer を通さないよう、変換対象から除外登録されている一部のパッケージ以下のクラスは、本来のクラスローダーであるシステムクラスローダーによりロードされてしまうため、動的な変換処理を行えません。またプラグイン側で TransformerExclusions アノテーションを用い除外設定されているパッケージやクラスも変換対象外となります。 | ||
60行目: | 60行目: | ||
# IFMLLoadingPlugin、<strike>IFMLCallHook</strike> を実装します。 | # IFMLLoadingPlugin、<strike>IFMLCallHook</strike> を実装します。 | ||
− | + | cpw.mods.fml.relauncher.IFMLCallHook は、このクラス自体に実装する必要はありません。 | |
getSetupClass() メソッドで返される名前のクラスが、IFMLCallHookを実装している必要があります。 | getSetupClass() メソッドで返される名前のクラスが、IFMLCallHookを実装している必要があります。 | ||
なお、本チュートリアルでは、コールフックを使用していないため、getSetupClass()メソッドの戻り値は null としています。 | なお、本チュートリアルでは、コールフックを使用していないため、getSetupClass()メソッドの戻り値は null としています。 | ||
+ | '''1.7版''' | ||
+ | <source lang="java"> | ||
+ | // パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。 | ||
+ | // 例) 作者名、ドメイン など (一意性のあるものが好ましい) | ||
+ | // | ||
+ | // ここでは便宜上 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"); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | '''1.12.2版''' | ||
<source lang="java"> | <source lang="java"> | ||
// パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。 | // パッケージは、クラス(ファイル名)の衝突を回避するために、汎用的ではないユニークなパッケージ名を使用しましょう。 | ||
163行目: | 253行目: | ||
ID、名前、バージョン情報を変更できれば良いため、ここでは DummyModContainer クラスを継承して作成しています。 | ID、名前、バージョン情報を変更できれば良いため、ここでは DummyModContainer クラスを継承して作成しています。 | ||
+ | '''1.12.2版''' | ||
<source lang="java"> | <source lang="java"> | ||
package tutorial.asm; | package tutorial.asm; | ||
180行目: | 271行目: | ||
// 必ずしも DummyModContainer を継承している必要はありません。 | // 必ずしも DummyModContainer を継承している必要はありません。 | ||
// net.minecraftforge.fml.common.ModContainer さえ実装していれば、どんなクラスでも構いません。 | // net.minecraftforge.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; | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | '''1.7版''' | ||
+ | <source lang="java"> | ||
+ | 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 class TutorialModContainer extends DummyModContainer | ||
212行目: | 349行目: | ||
# 任意の名前のクラス(ここではTutorialTransformer)を作成します。 | # 任意の名前のクラス(ここではTutorialTransformer)を作成します。 | ||
# IClassTransformer インタフェースを実装します。 | # IClassTransformer インタフェースを実装します。 | ||
+ | |||
+ | 1.7版 | ||
+ | <source lang="java"> | ||
+ | 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; | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | |||
+ | 1.12.2版(ASMなし) | ||
<source lang="java"> | <source lang="java"> | ||
249行目: | 554行目: | ||
try | try | ||
{ | { | ||
− | // | + | // -------------------------------------------------------------- |
− | return | + | // クラスファイル丸ごと差し替える場合 |
+ | // -------------------------------------------------------------- | ||
+ | return replaceClass(bytes); | ||
+ | |||
+ | |||
+ | |||
} | } | ||
catch (Exception e) | catch (Exception e) | ||
257行目: | 567行目: | ||
} | } | ||
} | } | ||
+ | |||
+ | // 下記の想定で実装されています。 | ||
+ | // 対象クラスの 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(); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | |||
} | } | ||
</source> | </source> | ||
291行目: | 653行目: | ||
**** TutorialModContainer.class | **** TutorialModContainer.class | ||
**** TutorialTransformer.class | **** TutorialTransformer.class | ||
− | ** ModifiedTargetClass.class (丸ごと差し替える場合に用いるクラス | + | ** ModifiedTargetClass.class (丸ごと差し替える場合に用いるクラス) |
通常の mod のように、zip で圧縮し、拡張子を .zip から .jar に変更すれば完成です。 | 通常の mod のように、zip で圧縮し、拡張子を .zip から .jar に変更すれば完成です。 | ||
※jarコマンドを用いて作成することも可能ですが、今回は割愛します。 | ※jarコマンドを用いて作成することも可能ですが、今回は割愛します。 | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
==テスト、デバッグ方法について== | ==テスト、デバッグ方法について== | ||
381行目: | 729行目: | ||
別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが<br /> | 別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが<br /> | ||
ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。) | ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。) | ||
− | |||
ASMライブラリの既知の不具合 | ASMライブラリの既知の不具合 |