阶段四: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 副本、从载荷派生的批量产物。
This commit is contained in:
tianmo committed 2026-10-16 13:34:18 +08:00
commit 2e7ab7c33c
113 files changed
+25327

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ListIterator;
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.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
/**
* 3.8.99 载荷跑在自备 JDK8 上时,Editor.main 里有一道
* "请使用 Spine 自带的 Java 运行时" 的校验,不通过就 Runtime.halt(1)。
*
* 字节码里的形状是:
* iload 2
* ifne <成功分支>
* goto <报错 + halt>
*
* 这个 agent 把那段 iload 前面塞上 iconst_1 / istore 2,
* 让分支永远走成功那一条。只作用于内存中的类,不动磁盘文件。
*/
public final class ForceAgent {
private static final String TARGET = "com/esotericsoftware/spine/editor/Editor";
private static boolean patched;
private static boolean skipLicencePatch;
public static void premain(String arguments, Instrumentation instrumentation) {
skipLicencePatch = arguments != null && arguments.contains("wyridonly");
System.err.println("[force-agent] premain 已加载" + (skipLicencePatch ? "(只改 WYRiDdid)" : ""));
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> beingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!TARGET.equals(className)) return null;
try {
return patch(classfileBuffer);
} catch (Throwable error) {
System.err.println("[force-agent] 打补丁失败: " + error);
return null;
}
}
});
}
private static byte[] patch(byte[] data) {
ClassNode node = new ClassNode();
new ClassReader(data).accept(node, 0);
for (MethodNode method : node.methods) {
if (!"main".equals(method.name) || !"([Ljava/lang/String;)V".equals(method.desc)) continue;
InsnList list = method.instructions;
for (ListIterator<AbstractInsnNode> it = list.iterator(); it.hasNext();) {
AbstractInsnNode insn = it.next();
// 第二道门:invokedynamic WYRiDdid()Z 为 false 就直接 return。
// 直接把这次调用的结果换成常量 true —— 一对一替换,栈效果完全一样,
// 所以栈帧表和 max_stack 都不用动(改成跳转就会破坏栈帧)。
if (insn instanceof InvokeDynamicInsnNode
&& "WYRiDdid".equals(((InvokeDynamicInsnNode) insn).name)) {
list.set(insn, new InsnNode(Opcodes.ICONST_1));
System.err.println("[force-agent] 已把 WYRiDdid() 守卫改成永远通过");
}
if (insn.getOpcode() != Opcodes.ILOAD || ((VarInsnNode) insn).var != 2) continue;
if (skipLicencePatch) continue;
AbstractInsnNode next = insn.getNext();
if (next == null || next.getOpcode() != Opcodes.IFNE) continue;
AbstractInsnNode after = next.getNext();
if (after == null || after.getOpcode() != Opcodes.GOTO) continue;
InsnList force = new InsnList();
force.add(new InsnNode(Opcodes.ICONST_1));
force.add(new VarInsnNode(Opcodes.ISTORE, 2));
list.insertBefore(insn, force);
patched = true;
System.err.println("[force-agent] 已把 Editor.main 的运行时校验改成永远通过");
}
}
if (!patched) return null;
// 只插两条不入栈的指令,帧信息不变,用 COMPUTE_MAXS 就够了。
// 用 COMPUTE_FRAMES 的话 ASM 会在定义类的过程中反射加载同一个类,
// 触发 "attempted duplicate class definition"。
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
node.accept(writer);
return writer.toByteArray();
}
}