import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.IntInsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; /** * 给 Spine 3.8.99 的编辑器载荷加一个"正式视图"。 * * 视图系统的情况(全部从字节码读出来的): * - us 是视图枚举:us("meshTools", 4, "Mesh Tools", MV.L, ut.B) * 里建好全部常量、组装 $VALUES(字段 B:[Lus;),再按 MV/ut 分成左右两组列表。 * - tD. 给每个视图 new 一个 Actor 并挂到 us.X.D。 * * 所以两处补丁: * 1) us: 开头造一个新常量 "spineView",并在 $VALUES 组装完之后把它追加进数组 * (必须在分组列表生成之前追加,否则 Views 菜单里看不到)。 * 2) tD: 里最后一个 us.X.D 赋值之后,插一条调用 * com.spineview.ViewInstaller.install(),由我们把面板 Actor 塞进 us.SPINEVIEW.D。 * * 用法:java -cp asm.jar:... ViewPatcher <源载荷> <目标载荷> */ public final class ViewPatcher { private static final String VIEW_ENUM = "us"; private static final String UI_CLASS = "tD"; private static final String VIEW_NAME = "spineView"; private static final String VIEW_TITLE = "Spine View"; private static final String VIEW_FIELD = "SPINEVIEW"; public static void main(String[] args) throws Exception { Path source = Paths.get(args[0]); Path target = Paths.get(args[1]); Map entries = new LinkedHashMap(); try (JarInputStream in = new JarInputStream(Files.newInputStream(source))) { JarEntry entry; while ((entry = in.getNextJarEntry()) != null) { if (entry.isDirectory()) continue; entries.put(entry.getName(), readAll(in)); } } System.out.println("载荷条目 " + entries.size()); entries.put(VIEW_ENUM + ".class", patchEnum(entries.get(VIEW_ENUM + ".class"))); entries.put(UI_CLASS + ".class", patchUi(entries.get(UI_CLASS + ".class"))); try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) { for (Map.Entry entry : entries.entrySet()) { out.putNextEntry(new JarEntry(entry.getKey())); out.write(entry.getValue()); out.closeEntry(); } } System.out.println("已写出 " + target); } /** 给 us 加一个新枚举常量。 */ private static byte[] patchEnum(byte[] data) { ClassNode node = new ClassNode(); new ClassReader(data).accept(node, 0); boolean hasField = false; for (FieldNode field : node.fields) { if (VIEW_FIELD.equals(field.name)) hasField = true; } if (!hasField) { node.fields.add(new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, VIEW_FIELD, "L" + VIEW_ENUM + ";", null, null)); } for (MethodNode method : node.methods) { if (!"".equals(method.name)) continue; InsnList head = new InsnList(); head.add(new TypeInsnNode(Opcodes.NEW, VIEW_ENUM)); head.add(new InsnNode(Opcodes.DUP)); head.add(new LdcInsnNode(VIEW_NAME)); head.add(new IntInsnNode(Opcodes.BIPUSH, 16)); head.add(new LdcInsnNode(VIEW_TITLE)); head.add(new FieldInsnNode(Opcodes.GETSTATIC, "MV", "L", "I")); head.add(new FieldInsnNode(Opcodes.GETSTATIC, "ut", "B", "Lut;")); head.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, VIEW_ENUM, "", "(Ljava/lang/String;ILjava/lang/String;ILut;)V", false)); head.add(new FieldInsnNode(Opcodes.PUTSTATIC, VIEW_ENUM, VIEW_FIELD, "L" + VIEW_ENUM + ";")); method.instructions.insert(head); // 找到 $VALUES 的组装点(PUTSTATIC us.B:[Lus;),在它后面把新常量追加进数组 AbstractInsnNode anchor = null; for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (insn instanceof FieldInsnNode) { FieldInsnNode field = (FieldInsnNode) insn; if (field.getOpcode() == Opcodes.PUTSTATIC && VIEW_ENUM.equals(field.owner) && "B".equals(field.name) && ("[L" + VIEW_ENUM + ";").equals(field.desc)) { anchor = insn; } } } if (anchor == null) throw new IllegalStateException("找不到 $VALUES 的组装点"); InsnList extend = new InsnList(); extend.add(new FieldInsnNode(Opcodes.GETSTATIC, VIEW_ENUM, "B", "[L" + VIEW_ENUM + ";")); extend.add(new VarInsnNode(Opcodes.ASTORE, 40)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ARRAYLENGTH)); extend.add(new InsnNode(Opcodes.ICONST_1)); extend.add(new InsnNode(Opcodes.IADD)); extend.add(new TypeInsnNode(Opcodes.ANEWARRAY, VIEW_ENUM)); extend.add(new VarInsnNode(Opcodes.ASTORE, 41)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ICONST_0)); extend.add(new VarInsnNode(Opcodes.ALOAD, 41)); extend.add(new InsnNode(Opcodes.ICONST_0)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ARRAYLENGTH)); extend.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false)); extend.add(new VarInsnNode(Opcodes.ALOAD, 41)); extend.add(new VarInsnNode(Opcodes.ALOAD, 40)); extend.add(new InsnNode(Opcodes.ARRAYLENGTH)); extend.add(new FieldInsnNode(Opcodes.GETSTATIC, VIEW_ENUM, VIEW_FIELD, "L" + VIEW_ENUM + ";")); extend.add(new InsnNode(Opcodes.AASTORE)); extend.add(new VarInsnNode(Opcodes.ALOAD, 41)); extend.add(new FieldInsnNode(Opcodes.PUTSTATIC, VIEW_ENUM, "B", "[L" + VIEW_ENUM + ";")); method.instructions.insert(anchor, extend); System.out.println("us. 已插入新视图常量 " + VIEW_NAME); } ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); node.accept(writer); return writer.toByteArray(); } /** 在 tD. 里最后一个 us.X.D 赋值之后,插一条调用去建我们视图的面板。 */ private static byte[] patchUi(byte[] data) { ClassNode node = new ClassNode(); new ClassReader(data).accept(node, 0); int inserted = 0; for (MethodNode method : node.methods) { AbstractInsnNode last = null; for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (insn instanceof FieldInsnNode) { FieldInsnNode field = (FieldInsnNode) insn; // 真正的写法是:getstatic us.StackMapTable:Lus; ... putfield us.D:Ldl; // 也就是"先取视图常量,再把面板 Actor 写进它的 D 字段"。 if (field.getOpcode() == Opcodes.PUTFIELD && VIEW_ENUM.equals(field.owner) && "Ldl;".equals(field.desc)) { last = insn; } } } if (last == null) continue; InsnList hook = new InsnList(); hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "com/spineview/ViewInstaller", "install", "()V", false)); method.instructions.insert(last, hook); inserted++; System.out.println("在 " + method.name + method.desc + " 里插入了调用"); } if (inserted == 0) throw new IllegalStateException("tD 里没找到挂视图的地方"); System.out.println("tD. 已插入 ViewInstaller.install()"); ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); node.accept(writer); return writer.toByteArray(); } private static byte[] readAll(InputStream in) throws java.io.IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] chunk = new byte[65536]; int n; while ((n = in.read(chunk)) > 0) buffer.write(chunk, 0, n); return buffer.toByteArray(); } }