本文链接:杀戮尖塔Mod中RawPatch的探索
在做杀戮尖塔Mod的时候,Patch是个常用工具,用来在原版游戏里面各处插入代码。不过最经常用的都是PrefixPatch、InsertPatch、PostfixPatch这种直接在特定地方插入代码,最多用用InstrumentPatch,像RawPatch几乎不会用到。但如果要做一些复杂的功能,只插入一些代码完成不了的时候,就需要使用RawPatch了。
使用方法
和其他Patch类似,RawPatch也需要用SpirePatch定义需要修改的方法,代码如下:
1 2 3 4 5 6 7 |
@SpirePatch(clz = CardCrawlGame.class, method = "render") public static class CardCrawlGameRendertPatch { @SpireRawPatch public static void Raw(CtBehavior method) throws Exception { // modify method } } |
作为参数传入的CtBehavior是Javassist提供的类,可以直接改这个方法的任意代码。但RawPatch的功能不只如此,通过CtBehavior其实可以拿到任意一个类的任意方法并修改。所以如果你想对不特定的类加Patch,可以用RawPatch作为入口来读取。具体方法是通过method拿到ClassPool,再从ClassPool中取到对应的类:
1 2 |
ClassPool pool = method.getDeclaringClass().getClassPool(); CtClass theClass = pool.get(AbstractMonster.class.getName()); |
修改代码
一般来说,你不需要从字节码层面去修改代码。从CtBehavior可以用insertAt、insertAfter、insertBefore来在特定位置插入代码,也可以用instrument来做和InstrumentPatch类似的事情。但如果这些不能满足你,就需要修改字节码了。
要修改字节码,需要以下几个东西:
1 2 3 4 |
MethodInfo mi = method.getMethodInfo(); CodeAttribute ca = mi.getCodeAttribute(); ConstPool cp = mi.getConstPool(); CodeIterator ci = ca.iterator(); |
CodeIterator是用来读写字节码的,ConstPool是常量池,有些指令会引用常量池中的内容。
字节码的功能可以参考以下文档:
- https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions
- https://docs.oracle.com/javase/specs/jvms/se6/html/Instructions2.doc.html
以下的代码可以打印出这个方法所有的字节码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public static void printCode(CodeAttribute ca) throws BadBytecode { int lastIndex = 0; int lastOp = -1; CodeIterator ci = ca.iterator(); ConstPool cp = ca.getConstPool(); while (ci.hasNext()) { int index = ci.next(); int op = ci.byteAt(index); if (lastOp == Opcode.INVOKESTATIC || lastOp == Opcode.INVOKEVIRTUAL || lastOp == Opcode.INVOKESPECIAL || lastOp == Opcode.INVOKEINTERFACE) { int argument = ci.u16bitAt(lastIndex); System.out.printf(" %s.%s %s", cp.getMethodrefClassName(argument), cp.getMethodrefName(argument), cp.getMethodrefType(argument)); } else if (lastOp == Opcode.GETSTATIC || lastOp == Opcode.GETFIELD) { int argument = ci.u16bitAt(lastIndex); System.out.printf(" %s.%s", cp.getFieldrefClassName(argument), cp.getFieldrefName(argument)); } else { for (int i = lastIndex; i < index; i++) { System.out.printf(" %02X", ci.byteAt(i)); } } System.out.println(); System.out.printf("%04X: %20s", index, Mnemonic.OPCODE[op]); lastIndex = index + 1; lastOp = op; } System.out.println(); } |
要插入代码,可以用Bytecode对象生成代码,再用CodeIterator插入。Bytecode提供了一系列方法来插入字节码,对于没支持的方法,也可以用addOpcode或者add来直接插入字节码。
1 2 3 4 |
Bytecode bytecode = new Bytecode(cp); bytecode.addAload(1); bytecode.addInvokestatic("MyClass", "MyMethod", "(Ljava/lang/String;)V"); ci.insert(0, bytecode.get()); |
注意事项
插入代码后,这个方法最大栈深度和局部变量数可能会不同。需要估算后更新进去,下面只是个示例,没考虑到插入位置对栈深度和局部变量数的影响。
1 2 3 4 5 6 7 8 |
int maxStack = bytecode.getMaxStack(); int maxLocals = bytecode.getMaxLocals(); if (ca.getMaxStack() < maxStack) { ca.setMaxStack(maxStack); } if (ca.getMaxLocals() < maxLocals) { ca.setMaxLocals(maxLocals); } |
如果你插入了任何跳转指令并产生了新的跳转落点,或者加入了局部变量,则需要更新StackMapTable:
1 |
mi.rebuildStackMap(classPool); |
总结
虽说RawPatch提供了强大的功能,不过能不直接改字节码还是别折腾了吧,又费事又容易出bug。