使用Javassist通过CoreMod来轻松进行操作

首先

在修改Minecraft的Mod时,会遇到Mojan设置的陷阱规则限制。要避免这些限制,需要使用CoreMod来直接修改字节码,但使用asm来修改是相当困难的。所以,我们可以使用Javassist来简化这个过程。

抱歉

这篇文章不介绍Javassist的使用方法或者CoreMod本身的制作方法。关于这些内容,请参考Javassist的官方页面,Modding Wiki上的CoreMod,或者在Github上寻找其他人制作的Mod代码。

此外,这里使用的是我的兴趣语言Scala,Minecraft的版本是1.12.2。不过,我认为将其转换为Java应该很简单…应该吧。

另外,使用方法本身应该不依赖于Minecraft的版本,所以请根据您想要使用的版本进行代码的转换。

Javassist是什么?

这是一个库,可以在运行时编译Java源代码并生成字节码,还可以修改现有类。

公式网页是用英语编写的,但是有日语教程,所以我认为很容易上手。许可证有LGPL、MPL、Apache License三种选择,您可以根据需要进行选取。

请您以中文将以下内容进行重述,只需要提供一个选项:

お題

让我们通过使用打火石来尝试在代码中插入一些处理。大致上就是在这里插入。

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

用法

构建.gradle文件

让我们将javassist添加到dependencies中。

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

创建其他设置与创建核心模块时相同,没有问题。

IFMLLoadingPlugin: IFML加载插件

我会在TransformerExclusions中添加javassist。

@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()

如果从字节数组创建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

如果保持这样的话有点难看,我们把ExprEditor拆分出来吧。

method match {
  case Some(m) => // メソッドが見つかった場合
    // コードの注入
    m.instrument(ExprEditorPrintSuccess)
  case _ =>
}
// 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
      )
    }
  }
}

让我们在这里进行另一次处理,这次使用打火石时在聊天中显示文本。
为了在聊天中显示,我们将下面的方法切分出来。

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 _ =>
}
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在生產環境中運行時,Javassist會抱怨說「com.yukiaji.javassistsample.main.StatusBoneFireLit並不存在!」。這是由於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)
}

在这里生成的虚拟类/方法实际上不会作为字节码输出,所以只要方法内部等语法上没有问题,随意的内容都可以。

bannerAds