CoreModでJavassistを使って楽をする

# はじめに

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

# ごめんなさい

この記事はJavassistの使用方法や、CoreMod自体の作り方を解説するものではありません。そのあたりはJavassist公式ページ、CoreModは[Modding wiki](http://minecraftjp.info/modding/index.php/%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB%E4%B8%80%E8%A6%A7)、あるいは[他の方が作られたModのコードをgithubで探してみて](https://github.com/search?l=Java&q=extends+IFMLLoadingPlugin+minecraft&type=Code&utf8=%E2%9C%93)ください。
また、コードは筆者の趣味でScala、Minecraftのバージョンは1.12.2となっています。ただ、そこまでScalaScalaしてないはずなので、Javaに置き換えるのは簡単... なはずです。
また、使い方自体はMinecraftのバージョンにも依存しないはずなので、利用したいバージョンに合わせてコードを読み替えてください。

# Javassistって?

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

http://jboss-javassist.github.io/javassist/

公式ページは英語ですが、日本語のチュートリアルがあるので、簡単に使えると思います。
ライセンスは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を作ります

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

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

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

```scala
// 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

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

```scala
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をくくりだしてしまいましょう

```scala
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)
    }
  }
}
```

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

```scala
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から参照可能にしてあげます

```scala
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)
}
```

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