阶段四: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:
commit
2e7ab7c33c
113 files changed
+25327
No files matched your search
@@ -0,0 +1,114 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user