目标:在 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 副本、从载荷派生的批量产物。
115 lines
4.6 KiB
Java
115 lines
4.6 KiB
Java
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();
|
||
}
|
||
}
|