ASM 核心 mod:向原版渲染类注入钩子

本 jar 既是普通 @Mod,又通过 IFMLLoadingPlugin 注册成 core mod,在类加载期用 ASM 改写两个原版客户端渲染类,把移动判定塞进去。

代码位置:src/main/scala/mrtjp/relocation/asm/RelocationPlugin.scala(19 行)+ Transformer.scala(106 行),被改写的落点在 src/main/scala/mrtjp/relocation/hacks.scala。

启用方式

gradle.properties 里一行:

coreModClass = asm.RelocationPlugin

这是 GTNH 约定构建(gtnh.settings.blowdryerTag = 0.2.0)读取的字段,相对 modGroup = mrtjp.relocation 解析为 mrtjp.relocation.asm.RelocationPlugin。同一份文件里 usesMixins = false / mixinPlugin =(空)——不走 Mixin 路线,纯 LaunchWrapper ASM。

RelocationPlugin

@IFMLLoadingPlugin.MCVersion("1.7.10")
@IFMLLoadingPlugin.TransformerExclusions(value = Array("mrtjp.relocation.asm.*", "scala"))
class RelocationPlugin extends IFMLLoadingPlugin {
  override def getASMTransformerClass = Array("mrtjp.relocation.asm.Transformer")
  override def getSetupClass = null
  override def getModContainerClass = null
  override def getAccessTransformerClass = null
  override def injectData(data: JMap[String, AnyRef]): Unit = {}
}
项 值 说明
getASMTransformerClass Array("mrtjp.relocation.asm.Transformer") 长度 1 的数组
getSetupClass null 无 premain / setup 阶段
getModContainerClass null 不替换 ModContainer
getAccessTransformerClass null 无访问转换器——仓库里也没有 META-INF/*_at.cfg
injectData 空实现 不读任何 core mod 数据
@MCVersion "1.7.10" 声明目标 MC 版本
@TransformerExclusions Array("mrtjp.relocation.asm.*", "scala") 排除自身 asm 包与整个 scala 包——防止改写自己的转换器和 Scala 库

转换器只有一个,不是有序枚举或列表。 getASMTransformerClass 返回的是长度为 1 的数组,jar 里也没有第二个 IClassTransformer 实现(见本页末尾 grep 证据)。

Transformer:数据驱动的转换表

Transformer extends IClassTransformer(net.minecraft.launchwrapper)。核心是一个 Map:

type MethodChecker = (String, MethodNode) => Boolean
type InsTransformer  = MethodNode => Unit

val classData = Map[String, (MethodChecker, MethodChecker, InsTransformer)](
  "net.minecraft.client.renderer.RenderBlocks" -> ((
    (_: String, m: MethodNode) => m.name == "renderBlockByRenderType",
    (n: String, m: MethodNode) => mapper.mapMethodName(n, m.name, m.desc) == "func_147805_b",
    transformBlockRender _
  )),
  "net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher" -> ((
    (_: String, m: MethodNode) => m.name == "renderTileEntity",
    (n: String, m: MethodNode) => mapper.mapMethodName(n, m.name, m.desc) == "func_147544_a",
    transformTERender _
  ))
)

共 2 个目标类。每个条目是三元组 (开发环境检查, 生产环境检查, 变换函数):

目标类 MCP 方法名 SRG 名 变换
net.minecraft.client.renderer.RenderBlocks renderBlockByRenderType func_147805_b transformBlockRender
net.minecraft.client.renderer.tileentity.TileEntityRendererDispatcher renderTileEntity func_147544_a transformTERender

开发/生产双名判定

lazy val deobfEnv = (Launch.blackboard get "fml.deobfuscatedEnvironment").asInstanceOf[Boolean]

从 LaunchWrapper 黑板读 fml.deobfuscatedEnvironment:开发环境为 true(MCP 映射名),生产为 false(SRG 混淆名)。判定时二选一:

for (m @ (_m: MethodNode) <- node.methods)
  if ((deobfEnv && ch1(name, m)) || (!deobfEnv && ch2(name, m))) {
    RelocationMod.log.info(s"$name $tName ${m.name} ${m.desc}")
    tr(m)
  }

开发环境用 m.name == "renderBlockByRenderType" 直接比字符串;生产环境用 FMLDeobfuscatingRemapper.INSTANCE.mapMethodName(n, m.name, m.desc) == "func_147805_b" 反混淆后比对。两者都通过时也会执行(||),但同一环境下只有一个为真。

每个类只改写一次

var matched = Set[String]()

override def transform(name: String, tName: String, data: Array[Byte]) = {
  if (classData.keys.contains(tName) && !matched.contains(tName)) {
    RelocationMod.log.info(s"transforming: $tName")
    val (ch1, ch2, tr) = classData(tName)

    val node   = new ClassNode
    val reader = new ClassReader(data)
    reader.accept(node, 0)          // flags = 0,不跳帧/不调试

    for (m @ (_m: MethodNode) <- node.methods) if (...) tr(m)

    matched += tName

    val writer = new ClassWriter(ClassWriter.COMPUTE_MAXS)
    node.accept(writer)
    writer.toByteArray
  } else data
}
  • matched 保证每个目标类只被改写一次(LaunchWrapper 可能多次回调)
  • 日志会打印 transforming: <类名> 以及每个命中方法的 <类> <目标类> <方法名> <描述符>,便于排查注入是否成功
  • 回写用 ClassWriter.COMPUTE_MAXS(只重算 max stack / max locals,不做帧计算)——插入的指令不改变控制流,因此不重算 frames

未命中的类直接 return data,零开销。

两处死代码

lazy val blockClass = mapper.unmap("net/minecraft/block/Block")
lazy val teClass    = mapper.unmap("net/minecraft/tileentity/TileEntity")

两个 lazy val 在 Transformer 里从未被引用(transformBlockRender / transformTERender 都没用)。mapper 是 FMLDeobfuscatingRemapper.INSTANCE 的别名(INSTANCE => mapper)。源码如此。

变换一:transformBlockRender

def transformBlockRender(m: MethodNode) {
  val old = m.instructions.toArray.collectFirst { case i: MethodInsnNode => i }.get
  val list = new InsnList
  list.add(new VarInsnNode(ILOAD, 2))
  list.add(new VarInsnNode(ILOAD, 3))
  list.add(new VarInsnNode(ILOAD, 4))
  list.add(new MethodInsnNode(INVOKESTATIC, "mrtjp/relocation/ASMHacks", "getRenderType",
    "(Lnet/minecraft/block/Block;III)I", false))
  m.instructions.insert(old, list)
  m.instructions.remove(old)
}

取方法里的第一个方法调用指令(collectFirst)——在 renderBlockByRenderType(Block, int, int, int) 中那正是 block.getRenderType()。原指令被删除,替换为:

  1. ILOAD 2 —— 局部变量 2 = x
  2. ILOAD 3 —— 局部变量 3 = y
  3. ILOAD 4 —— 局部变量 4 = z
  4. INVOKESTATIC mrtjp/relocation/ASMHacks.getRenderType(Block, int, int, int) : int

即"取渲染类型"这一步被换成走 ASMHacks,后者能返回 -1 跳过渲染。false = 非 itf(静态接口调用)。

变换二:transformTERender

def transformTERender(m: MethodNode) {
  val old = m.instructions.toArray.collect { case i: MethodInsnNode => i }.last
  m.instructions.insert(old, new InsnNode(POP))
  m.instructions.insert(old, new MethodInsnNode(INVOKESTATIC, "mrtjp/relocation/ASMHacks",
    "renderTileEntityAt", old.desc, false))
  m.instructions.remove(old)
}

取最后一个方法调用指令(collect{...}.last)——在 renderTileEntity 里是 TileEntityRendererDispatcher.instance.renderTileEntityAt(te, x, y, z, tick)。它被替换为:

  1. INVOKESTATIC ASMHacks.renderTileEntityAt —— 描述符直接沿用原调用(old.desc),所以签名严格匹配
  2. POP —— 吃掉原先作为实参压栈的那个引用

顺序是"先插 POP、再插 INVOKESTATIC"到同一位置 old 之前,因此最终指令流是 ... [INVOKESTATIC renderTileEntityAt] [POP] ...(后插的排在更前),POP 正好抵消 te 那个引用。落点实现见 客户端渲染。

被改写的两个原版类

原版类 改写的方法 注入的行为
RenderBlocks renderBlockByRenderType 渲染类型查询改为 ASMHacks.getRenderType(block, x, y, z);移动中的方块返回 -1 被跳过
TileEntityRendererDispatcher renderTileEntity 实际绘制改为 ASMHacks.renderTileEntityAt(te, x, y, z, tick);移动中的方块整体平移

两个目标类都是纯客户端类(net.minecraft.client.renderer.*),因此核心 mod 的改写只影响客户端;服务端完全不经过这段代码。相应地 ASMHacks 的两个方法都标了 @SideOnly(Side.CLIENT)。

本 mod 没有的东西(grep 证据)

在 /Users/evlos/a/mirror/ForgeRelocation/ 下对 src/ 全量检索:

断言 命令 结果
没有第二个 IClassTransformer 实现 grep -rn "IClassTransformer" src/ 2 命中,同一文件:asm/Transformer.scala:12 的 import 与 :19 的 class Transformer extends IClassTransformer。全 jar 唯一的转换器实现
没有 Access Transformer getAccessTransformerClass = null;grep -rn "accessTransformer" gradle.properties accessTransformersFile =(空);src/main/resources/META-INF/ 目录不存在
没有 Mixin gradle.properties 中 usesMixins = false、mixinPlugin =、mixinsPackage = 全为空 无 *.mixins.json、无 IMixinConfigPlugin 实现
没有 premain/setup getSetupClass = null 无
没有 per-mod 映射数据文件 `find src/main/resources -type f | grep -vE '.(png obj)$'`

相关条目