提供: Minecraft Modding Wiki
この編集を取り消せます。
下記の差分を確認して、本当に取り消していいか検証してください。よろしければ変更を保存して取り消しを完了してください。
最新版 | 編集中の文章 | ||
6行目: | 6行目: | ||
===Coremodとは=== | ===Coremodとは=== | ||
− | 通常 Minecraft.jar 内に上書きが必要な変更を施すような | + | 通常 Minecraft.jar 内に上書きが必要な変更を施すような mod(前提Modなど等)を、coremods フォルダに入れるだけでインストールできるようにする機能です。 |
ただし、既存書き換えのMODをそのまま coremod にすることは出来ません。 | ただし、既存書き換えのMODをそのまま coremod にすることは出来ません。 | ||
12行目: | 12行目: | ||
===仕組み=== | ===仕組み=== | ||
− | + | FMLでは、Java のデフォルトクラスローダーではなく、独自のクラスローダーを用いてクラスを読み込むように、Minecraftを改変します。通常コンパイル済みのClassファイルの内容は変更できませんが、クラスローダーを差し替えることで、クラスが読まれる直前に、そのバイトコードに対して変更を加えるポイントの追加を実現しています。 | |
− | + | この機能を利用するのが、このチュートリアルで解説する Coremod となります。 | |
− | + | Coremod を用いる事で、Minecraftの実行中に、初めてクラスがロードされた際に、クラスのバイトコードを置換、または部分的に書き換える事ができるようになります。動的に書き換えるため、Minecraft.jar 内(※Coremod部分以外のFMLコードも含む)および、mods 内の zip 等のClassファイルを、直接上書きして変更することなく改変することを可能とします。 | |
− | + | ====動的なクラス書き換えって、重くないの?==== | |
+ | クラスのロードは、基本的に最初にクラスの参照が要求された際に、1回だけ行われます。 | ||
− | + | 一度ロードされたクラスは、通常どおりコンパイルされたclassファイルとなんら違いはありません。 | |
− | |||
− | |||
− | |||
つまり、施した改変内容以上のパフォーマンスへの影響は、ほぼ無いと考えて差し支えありません。 | つまり、施した改変内容以上のパフォーマンスへの影響は、ほぼ無いと考えて差し支えありません。 | ||
58行目: | 56行目: | ||
# 任意の名前のクラス(ここではTutorialCorePlugin)を作成します。 | # 任意の名前のクラス(ここではTutorialCorePlugin)を作成します。 | ||
− | # | + | # IFMLLoadingPlugin、IFMLCallHook を実装します。 |
− | |||
− | |||
− | |||
− | |||
<source lang="java"> | <source lang="java"> | ||
75行目: | 69行目: | ||
import java.util.Map; | import java.util.Map; | ||
− | import | + | import cpw.mods.fml.relauncher.IFMLCallHook; |
− | import | + | import cpw.mods.fml.relauncher.IFMLLoadingPlugin; |
+ | import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions; | ||
− | // TransformerExclusions: | + | // TransformerExclusions.value: coremods でロードする際に参照されるため、パッケージ名と一致させてください。 |
− | // | + | // IFMLLoadingPlugin: Coremods の基礎インタフェース |
− | // | + | // IFMLCallHook: Coremods 内で、coremod 自身のパス等を取得する等の際に必要となります。 |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | @TransformerExclusions({"tutorial.asm"}) | + | @TransformerExclusions(value={"tutorial.asm"}) |
− | public class TutorialCorePlugin implements IFMLLoadingPlugin | + | public class TutorialCorePlugin implements IFMLLoadingPlugin, IFMLCallHook |
{ | { | ||
− | // coremod | + | // coremod 自身のファイルパスの保存用 |
− | + | public static File location; | |
− | + | ||
− | static File location; | + | // 今回は使用しません |
− | |||
− | |||
− | |||
− | // | ||
− | |||
@Override | @Override | ||
public String[] getLibraryRequestClass() | public String[] getLibraryRequestClass() | ||
106行目: | 90行目: | ||
} | } | ||
− | // Class | + | // Class の改変機能を実装したクラスの完全修飾名を返します。 |
− | |||
− | |||
@Override | @Override | ||
public String[] getASMTransformerClass() | public String[] getASMTransformerClass() | ||
115行目: | 97行目: | ||
} | } | ||
− | // coremod | + | // coremod の名前やバージョン情報の格納クラスの完全修飾名を返します。 |
− | |||
@Override | @Override | ||
public String getModContainerClass() | public String getModContainerClass() | ||
123行目: | 104行目: | ||
} | } | ||
− | // | + | // coremod 読み込みの基点クラスの完全修飾名を返します。 |
− | |||
− | |||
@Override | @Override | ||
public String getSetupClass() | public String getSetupClass() | ||
{ | { | ||
− | return | + | return "tutorial.asm.TutorialCorePlugin"; |
} | } | ||
− | // | + | // IFMLCallHook のメソッドです。 |
− | // 今回は coremod 自身の jar | + | // 今回は coremod 自身の jar ファイルパスを取得します。 |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
@Override | @Override | ||
public void injectData(Map<String, Object> data) | public void injectData(Map<String, Object> data) | ||
148行目: | 120行目: | ||
location = (File) data.get("coremodLocation"); | location = (File) data.get("coremodLocation"); | ||
} | } | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public Void call() throws Exception | ||
+ | { | ||
+ | return null; | ||
} | } | ||
} | } | ||
172行目: | 150行目: | ||
import com.google.common.eventbus.Subscribe; | import com.google.common.eventbus.Subscribe; | ||
− | import | + | import cpw.mods.fml.common.DummyModContainer; |
− | import | + | import cpw.mods.fml.common.LoadController; |
− | import | + | import cpw.mods.fml.common.ModMetadata; |
− | import | + | import cpw.mods.fml.common.event.FMLInitializationEvent; |
− | import | + | import cpw.mods.fml.common.versioning.ArtifactVersion; |
// 必ずしも DummyModContainer を継承している必要はありません。 | // 必ずしも DummyModContainer を継承している必要はありません。 | ||
− | // | + | // cpw.mods.fml.common.ModContainer さえ実装していれば、どんなクラスでも構いません。 |
public class TutorialModContainer extends DummyModContainer | public class TutorialModContainer extends DummyModContainer | ||
197行目: | 175行目: | ||
meta.url = ""; | meta.url = ""; | ||
meta.credits = ""; | meta.credits = ""; | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
} | } | ||
222行目: | 193行目: | ||
import java.util.zip.ZipFile; | 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 | + | import cpw.mods.fml.relauncher.FMLRelauncher; |
− | import | + | import cpw.mods.fml.relauncher.IClassTransformer; |
− | public class TutorialTransformer implements IClassTransformer | + | // Opcodes : インプリメントすると、ASMによるバイトコード定数にアクセスするのに便利です。 |
+ | // 必須ではありません。不用な場合は implements から削除してください。 | ||
+ | |||
+ | public class TutorialTransformer implements IClassTransformer, Opcodes | ||
{ | { | ||
// 改変対象のクラスの完全修飾名です。 | // 改変対象のクラスの完全修飾名です。 | ||
233行目: | 215行目: | ||
// クラスがロードされる際に呼び出されるメソッドです。 | // クラスがロードされる際に呼び出されるメソッドです。 | ||
− | public byte[] transform( | + | @Override |
− | + | public byte[] transform(String name, byte[] bytes) | |
+ | { | ||
// FMLRelauncher.side() : Client/Server どちらか一方のを対象とする場合や、 | // FMLRelauncher.side() : Client/Server どちらか一方のを対象とする場合や、 | ||
// 一つのMODで Client/Sever 両方に対応したMODで、この値を判定して処理を変える事ができます。 | // 一つのMODで Client/Sever 両方に対応したMODで、この値を判定して処理を変える事ができます。 | ||
241行目: | 224行目: | ||
// name : 現在ロードされようとしているクラス名が格納されています。 | // name : 現在ロードされようとしているクラス名が格納されています。 | ||
− | if ( | + | if (!FMLRelauncher.side().equals("CLIENT") || !name.equals(TARGET_CLASS_NAME)) |
{ | { | ||
// 処理対象外なので何もしない | // 処理対象外なので何もしない | ||
249行目: | 232行目: | ||
try | try | ||
{ | { | ||
− | // | + | // -------------------------------------------------------------- |
− | return | + | // クラスファイル丸ごと差し替える場合 |
+ | // -------------------------------------------------------------- | ||
+ | // return replaceClass(bytes); | ||
+ | |||
+ | // -------------------------------------------------------------- | ||
+ | // ASMを使用し、既存のクラスファイルに改変を施す場合。 | ||
+ | // -------------------------------------------------------------- | ||
+ | // return hookDoRenderLivingMethod(bytes); | ||
+ | |||
} | } | ||
catch (Exception e) | catch (Exception e) | ||
256行目: | 247行目: | ||
throw new RuntimeException("failed : TutorialTransformer loading", 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; | ||
} | } | ||
} | } | ||
</source> | </source> | ||
− | |||
− | |||
===META-INF/MANIFEST.MFファイルの作成=== | ===META-INF/MANIFEST.MFファイルの作成=== | ||
# META-INF/MANIFEST.MFファイルを作成します。 | # META-INF/MANIFEST.MFファイルを作成します。 | ||
− | # | + | # TutorialCorePlugin の完全修飾クラスの名(パッケージ名を含む名前)を指定します。 |
+ | |||
<source lang="java"> | <source lang="java"> | ||
Manifest-Version: 1.0 | Manifest-Version: 1.0 | ||
FMLCorePlugin: tutorial.asm.TutorialCorePlugin | FMLCorePlugin: tutorial.asm.TutorialCorePlugin | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
</source> | </source> | ||
291行目: | 363行目: | ||
**** TutorialModContainer.class | **** TutorialModContainer.class | ||
**** TutorialTransformer.class | **** TutorialTransformer.class | ||
− | ** ModifiedTargetClass.class (丸ごと差し替える場合に用いるクラス | + | ** ModifiedTargetClass.class (丸ごと差し替える場合に用いるクラス) |
通常の mod のように、zip で圧縮し、拡張子を .zip から .jar に変更すれば完成です。 | 通常の mod のように、zip で圧縮し、拡張子を .zip から .jar に変更すれば完成です。 | ||
※jarコマンドを用いて作成することも可能ですが、今回は割愛します。 | ※jarコマンドを用いて作成することも可能ですが、今回は割愛します。 | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
==テスト、デバッグ方法について== | ==テスト、デバッグ方法について== | ||
− | + | coremod 形式の mod は、Eclipse でのデバッグで動作させるのは手間がかかります。 | |
− | + | 機能の実装部分については、書き換え元のクラスを、直接改変して試したほうが簡単でしょう。 | |
− | + | coremodとしての動作確認は、実際の動作環境へ放り込んで行ってください。 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
==難読化への対抗手段の解説== | ==難読化への対抗手段の解説== | ||
− | |||
− | |||
− | |||
− | |||
− | + | ===準備=== | |
− | + | # 改変したいClassを書き換える。 | |
− | + | # recompile > reobfuscate し、難読化後のクラスファイルを作成する。 | |
− | |||
− | |||
− | |||
− | + | ===クラスを丸ごと差し替える場合=== | |
− | + | reobfに出力されたクラスファイル名を見てTutorialTransformerを適宜書き換えましょう。 | |
− | |||
− | + | ※置き変えてしまうので、同一のクラスを置き買えるものとは競合してしまいます。 | |
− | |||
− | |||
− | |||
− | + | ===ASMライブラリを用いたクラスの部分改変を行う場合=== | |
− | + | reobf/ に出力されたクラスファイルと、改変前のクラスファイルを、JBVD等の適当な ByteCodeViewer で開き、2つを比較して改変された部分を特定します。 | |
+ | それを元にASMのコードに置き変え、TutorialTransformer を適宜書き換えましょう。 | ||
− | + | サンプルのようなメソッドのフック処理程度であれば、Eclipse 等の ByteCodeOutline プラグインなどで、改変部分のコードを直接ASM形式のコードに変換し、難読化される部分を書きかえる程度の修正で、そのまま使える場合もあります。 | |
− | + | ※ ByteCode操作の場合、同一クラスにに複数の改変を施すことができるため、競合させずにそれぞれの改変を施すこともできます。 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
==雑記== | ==雑記== | ||
381行目: | 398行目: | ||
別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが<br /> | 別な処理に置き変えるなど、Javaでできることはほぼなんでも出来ますが<br /> | ||
ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。) | ASMライブラリの使用方法を別途調べる必要があります。(記述時点で、日本語解説はあまり多くありません。) | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
<comments /> | <comments /> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
[[Category:その他]] | [[Category:その他]] |