LoginSignup
3
3

More than 5 years have passed since last update.

CoreModでJavassistを使って楽をする

Last updated at Posted at 2018-03-03

はじめに

MinecraftのModをいじる上で、立ちはだかるMojanの罠仕様の壁。これを回避するにはCoreModを使って直接バイトコードを書き換える必要がありますが、asmを使って書き換えるのはなかなか厳しいものがあります。
そこで、Javassistを使って楽をしてしまいましょう

ごめんなさい

この記事はJavassistの使用方法や、CoreMod自体の作り方を解説するものではありません。そのあたりはJavassist公式ページ、CoreModはModding wiki、あるいは他の方が作られたModのコードをgithubで探してみてください。
また、コードは筆者の趣味でScala、Minecraftのバージョンは1.12.2となっています。ただ、そこまでScalaScalaしてないはずなので、Javaに置き換えるのは簡単... なはずです。
また、使い方自体はMinecraftのバージョンにも依存しないはずなので、利用したいバージョンに合わせてコードを読み替えてください。

Javassistって?

Javaのソースコードを実行時にコンパイル・バイトコードを生成、既存のクラスを書き換えたりすることができるライブラリです

公式ページは英語ですが、日本語のチュートリアルがあるので、簡単に使えると思います。
ライセンスはLGPLとMPL、Apache Licenseのトリプルライセンスなので、必要なものを選択しましょう。

お題

今回は火打ち石を使ったときの処理に色々差し込んでみましょう。コードとしてはこのあたりに差し込む感じです

ItemFlintAndSteel.java
// 46行目あたり
if (player instanceof EntityPlayerMP) // ←ここのifの判定のあたりを書き換える
{
    CriteriaTriggers.PLACED_BLOCK.trigger((EntityPlayerMP)player, pos, itemstack);
}

使い方

build.gradle

dependenciesにjavassistを加えましょう

build.gradle
dependencies {
    compile group: 'org.javassist', name: 'javassist', version: '3.22.0-GA'
}

他の設定はCoreModを作るときと同じで問題ありません

IFMLLoadingPlugin

TransformerExclusionsにjavassistを追加しておきます

JavassistSampleCore.scala
@IFMLLoadingPlugin.TransformerExclusions(Array("java", "scala", "javassist", "com.yukiaji.javassistsample.asm"))
@IFMLLoadingPlugin.MCVersion("1.12.2")
class JavassistSampleCore extends IFMLLoadingPlugin with IFMLCallHook {
    // snip
}

IClassTransformer

前準備

対象のクラスを見つけたら、CtClassを作ります

val pool = ClassPool.getDefault
val ctClass = pool.makeClass(new ByteArrayInputStream(basicClass))
ctClass.defrost()

byte配列からCtClassを作った場合、できたCtClassは変更不可な状態となっているため、defrostを呼び出して変更できるようにします。

次に、読み込んだCtClassに定義されているメソッドの中で、目的のものを探します。今回はOnItemUseです

// TargetMethodNameはOnItemUse、TargetMethodSrgNameはOnItemUseのsrg名、TargetMethodSigは(Lnet.minecraft.entity.player.EntityPlayer;L...)L...みたいな引数と返値の型を表す文字列
val method = ctClass.getDeclaredMethods.find(method => {
  val methodName = FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(ctClass.getName, method.getName, method.getSignature)
  val methodSig = FMLDeobfuscatingRemapper.INSTANCE.mapMethodDesc(method.getSignature)
  // srg名でない場合もシグネチャの比較はすべきだがオーバーロードされていない前提で省略
  methodName == TargetMethodName || (methodName == TargetMethodSrgName && methodSig == TargetMethodSig)
})

ここまでが前準備です。次は実際にコードを書き換えてみます

コードの書き換え1

見つけたメソッドの目的の箇所を検索、コードを注入します

method match {
  case Some(m) => // メソッドが見つかった場合
    // コードの注入
    m.instrument(new ExprEditor() {
      val TargetInstanceOfClassName = FMLDeobfuscatingRemapper.INSTANCE.unmap("net/minecraft/entity/player/EntityPlayerMP").replaceAll("\\/", ".")

      override def edit(i: Instanceof): Unit = {
        // edit内で取れるクラス名やメソッド名は難読化後の名前で取れるため注意
        if (i.getType.getName == TargetInstanceOfClassName) {
          // コードの書き換え
          i.replace(
            """
              |System.out.println("success fire"); // 注入するコード
              |$_ = $proceed($$); // 元のコード
            """.stripMargin
          )
        }
      }
    })
  case _ =>
}

これで実行すると、火打ち石を使ったときにコンソールにsuccess fireと出力されると思います。おそらく2回出力されると思いますが、ClientとServer両方で実行されるので正常な動作です

コードの書き換え2

このままだとちょっと見にくいので、ExprEditorをくくりだしてしまいましょう

method match {
  case Some(m) => // メソッドが見つかった場合
    // コードの注入
    m.instrument(ExprEditorPrintSuccess)
  case _ =>
}
ExprEditorPrintSuccess.scala
// objectとは: https://dwango.github.io/scala_text/object.html
object ExprEditorPrintSuccess extends ExprEditor {
  val TargetInstanceOfClassName = ClassName("net.minecraft.entity.player.EntityPlayerMP") // ClassNameは元のクラス名から難読化済みのクラスを作ったりするための独自case class

  override def edit(i: Instanceof): Unit = {
    // edit内で取れるクラス名やメソッド名は難読化後の名前で取れるため、比較前に適宜map/unmapする
    if (i.getType.getName == TargetInstanceOfClassName.unmappedName) {
      i.replace(
        """
          |System.out.println("success fire");
          |$_ = $proceed($$);
        """.stripMargin
      )
    }
  }
}

これにもう1処理、今度は火打ち石を使ったときにチャットにテキストを表示するようにしてみましょう。
チャットに表示するためのメソッドは下のように切り出しておきます

StatusBoneFireLit.scala
object StatusBoneFireLit {
  def send(player: EntityPlayer): Unit = {
    if (player.isInstanceOf[EntityPlayerMP]) {
      player.sendStatusMessage(new TextComponentString("§eBONEFIRE LIT"), true)
    }
  }
}

では、これを先ほどと同じ位置で呼び出すようにコードを書き換えてみます

method match {
  case Some(m) =>
    // コードの注入
    m.instrument(ExprEditorStatusMessage)
  case _ =>
}
ExprEditorStatusMessage.scala
object ExprEditorStatusMessage extends ExprEditor {
  val TargetInstanceOfClassName = ClassName("net.minecraft.entity.player.EntityPlayerMP")

  val PlayerClassName = ClassName("net.minecraft.entity.player.EntityPlayer")

  override def edit(i: Instanceof): Unit = {
    // edit内で取れるクラス名やメソッド名は難読化後の名前で取れるため、比較前に適宜map/unmapする
    if (i.getType.getName == TargetInstanceOfClassName.unmappedName) {
      definePlaceholder("com.yukiaji.javassistsample.main", "StatusBoneFireLit", "send", List(ClassName("net.minecraft.entity.player.EntityPlayer")), ClassName.Void, Modifier.PUBLIC | Modifier.STATIC)
      // ↑ ?

      i.replace(
        """
          |com.yukiaji.javassistsample.main.StatusBoneFireLit.send((%s)$1);
          |$_ = $proceed($$);
        """.stripMargin.format(PlayerClassName.unmappedName)
      )
    }
  }
}

さて、サンプルコードに謎のメソッドdefinePlaceholderが現れました。
scalaだけなのか、読み込み順序の問題か、はたまたおま環なのかはわかりませんが、開発環境での実行はここをコメントアウトしても問題なく動作するものの、実際にjarに固めて本番環境で実行すると、「com.yukiaji.javassistsample.main.StatusBoneFireLitなんてないぞ!」とJavassistに怒られてしまいます。これは、JavassistからStatusBoneFireLitが参照できないために起きるエラーで、コードの書き換え前にJavassistにこのクラス、あるいはメソッドの存在を教えないといけません。なので、ダミーでクラスとメソッドを定義し、Javassistから参照可能にしてあげます

private def definePlaceholder(packageName: String, className: String, methodName: String, argTypes: List[ClassName], returnType: ClassName, modifiers: Int): Unit = {
  val pool = ClassPool.getDefault
  pool.makePackage(getClass.getClassLoader, packageName)
  val placeholder = pool.makeClass(packageName + "." + className)
  // 生成するメソッドで参照するクラスはすべてmapされる前のクラス名を使用する
  var argString = argTypes.zipWithIndex.map({ case (t, i) => t.unmappedName + " arg" + i }).mkString(", ") // type arg1, type arg2, ...
  val placeholderMethod = CtMethod.make(
    s"""
       |${returnType.unmappedName} $methodName($argString) {
       |    System.out.println("WARNING: call placeholder method");
       |    ${if (returnType != ClassName.Void) "return null;" else ""}
       |}""".stripMargin,
    placeholder
  )
  placeholderMethod.setModifiers(modifiers)
  placeholder.addMethod(placeholderMethod)
}

ここで生成するダミーのクラス/メソッドは実際にバイトコードとして出力はされないので、メソッドの中身などは構文的に問題なければ適当で大丈夫です

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3