Files
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

115 lines
4.6 KiB
Java
Raw Permalink 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.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* 在 Spine 3.8.99 的 launcher.jar 里插一条调用:
* com.esotericsoftware.spine.editor.launcher.Launcher.main(String[])
* 的第一行改成先调 com.spineview.SceneScan.main(String[]),
* 并把我们编译好的类一起打进 jar。
*
* 因为 3.8.99 是明文的,这么做之后我们的代码就和编辑器处在同一个类加载器里,
* 不再有"跨加载器看不到 TextButton"的问题。
*/
public final class Patcher {
/** 目标类 / 方法 / 描述符,从命令行传(args[3..5])。 */
private static String targetClass;
private static String targetEntry;
private static String hookMethod;
private static String hookDesc;
private static final String HOOK_OWNER = "com/spineview/SceneScan";
public static void main(String[] args) throws Exception {
Path source = Paths.get(args[0]);
Path target = Paths.get(args[1]);
Path classesDir = Paths.get(args[2]);
targetClass = args.length > 3 ? args[3] : "com/esotericsoftware/spine/editor/launcher/Launcher";
String methodName = args.length > 4 ? args[4] : "main";
hookMethod = args.length > 5 ? args[5] : "main";
hookDesc = args.length > 6 ? args[6] : "([Ljava/lang/String;)V";
targetEntry = targetClass + ".class";
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("原 jar 条目数 = " + entries.size());
byte[] original = entries.get(targetEntry);
if (original == null) throw new IllegalStateException("找不到 " + targetEntry);
entries.put(targetEntry, patch(original, methodName));
System.out.println("已给 " + targetClass + "." + methodName + " 插入了 " + HOOK_OWNER + "." + hookMethod);
int added = 0;
try (java.util.stream.Stream<Path> stream = Files.walk(classesDir)) {
for (Path path : (Iterable<Path>) stream::iterator) {
if (!Files.isRegularFile(path)) continue;
String relative = classesDir.relativize(path).toString();
if (!relative.endsWith(".class")) continue;
entries.put(relative.replace('\\', '/'), Files.readAllBytes(path));
added++;
}
}
System.out.println("加进 jar 的类 = " + added);
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
for (Map.Entry<String, byte[]> entry : entries.entrySet()) {
JarEntry jarEntry = new JarEntry(entry.getKey());
jarEntry.setTime(0L);
out.putNextEntry(jarEntry);
out.write(entry.getValue());
out.closeEntry();
}
}
System.out.println("已写出 " + target);
}
private static byte[] patch(byte[] original, final String methodName) {
ClassReader reader = new ClassReader(original);
ClassWriter writer = new ClassWriter(0);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor next = super.visitMethod(access, name, descriptor, signature, exceptions);
if (!methodName.equals(name) || !hookDesc.equals(descriptor)) return next;
return new MethodVisitor(Opcodes.ASM9, next) {
@Override
public void visitCode() {
super.visitCode();
// 我们的钩子是 ()V,不需要压任何参数;栈平衡保持不变。
visitMethodInsn(Opcodes.INVOKESTATIC, HOOK_OWNER, hookMethod, hookDesc, false);
}
};
}
};
reader.accept(visitor, 0);
return writer.toByteArray();
}
private static byte[] readAll(InputStream in) throws Exception {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[16384];
int read;
while ((read = in.read(chunk)) > 0) buffer.write(chunk, 0, read);
return buffer.toByteArray();
}
}