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()。原指令被删除,替换为:
ILOAD 2—— 局部变量 2 =xILOAD 3—— 局部变量 3 =yILOAD 4—— 局部变量 4 =zINVOKESTATIC 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)。它被替换为:
INVOKESTATIC ASMHacks.renderTileEntityAt—— 描述符直接沿用原调用(old.desc),所以签名严格匹配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)$'` |
相关条目
- 客户端渲染:移动中的方块如何画出来 ——
ASMHacks两个方法的具体行为与MovingRenderer的配合 - 运动模型(结构与行的分解) —— 被注入的
isMoving/getEnclosedStructure数据来源 - 配置文件 —— 资源文件完整清单