Files
Spine_lab/tools/patch_view/ViewPatcher.java
T
tianmo 2e7ab7c33c 阶段四:Spine 3.8.99 编辑器里挂上自建视图面板
目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示),
点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。

本轮打通:
1. 编辑器在我们自己的进程里完整启动
   - 类加载器必须是 URLClassLoader 本身(自检要求)
   - Editor.main 必须在 AWT EDT 上调用
   - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava
   - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身
2. 3.8.99 全量解密 → 可读源码
   - 字符串 17251/17461(伪造同名调用帧对齐 key)
   - 混淆动态派发还原 53871 处(调引导方法取 MemberName)
3. 自建视图
   - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检)
   - 面板 Actor 由外部反射填进 us.spineView.D
   - 复用已有图标做别名(不新建图片资源)
   最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开

另外:
- ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩
- DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成
- 阶段四-视图面板挂载.md:完整记录做法与踩过的坑

不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、
Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
2026-10-16 13:34:18 +08:00

197 lines
8.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
* <clinit> 里建好全部常量、组装 $VALUES(字段 B:[Lus;),再按 MV/ut 分成左右两组列表。
* - tD.<init> 给每个视图 new 一个 Actor 并挂到 us.X.D。
*
* 所以两处补丁:
* 1) us:<clinit> 开头造一个新常量 "spineView",并在 $VALUES 组装完之后把它追加进数组
* (必须在分组列表生成之前追加,否则 Views 菜单里看不到)。
* 2) tD:<init> 里最后一个 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<String, byte[]> entries = new LinkedHashMap<String, byte[]>();
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<String, byte[]> 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 (!"<clinit>".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, "<init>",
"(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.<clinit> 已插入新视图常量 " + VIEW_NAME);
}
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
node.accept(writer);
return writer.toByteArray();
}
/** 在 tD.<init> 里最后一个 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.<init> 已插入 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();
}
}