阶段四: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

@@ -0,0 +1,158 @@
package com.spineview;
import java.awt.Window;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
/**
* 点按钮时执行的读取动作。
*
* 现在只做"能拿到什么就先列什么":先把编辑器窗口里的真实组件树、以及编辑器进程里
* 自建的对象图列出来,确认通道打通;随后再把它换成真正的"骨骼 / 插槽 / 附件"场景对象。
*/
public final class SceneReader {
private SceneReader() {
}
public static String read(ClassLoader editorLoader, Object stage) {
StringBuilder out = new StringBuilder();
out.append("=== 场景树里的对象 ===\n");
try {
listActors(stage, out, 0, 0);
} catch (Throwable error) {
out.append("读取失败: ").append(error).append('\n');
}
out.append("=== 编辑器进程里的窗口 ===\n");
for (Window window : Window.getWindows()) {
if (window.isVisible()) {
out.append(" ").append(window.getClass().getName()).append(" ")
.append(window.getWidth()).append('x').append(window.getHeight()).append('\n');
if (window instanceof java.awt.Container) {
collectComponents((java.awt.Container) window, out, 1);
}
}
}
out.append("\n=== 编辑器实例可达的自建对象 ===\n");
try {
Object editor = findEditor(editorLoader);
if (editor == null) {
out.append("(还没等到编辑器实例)\n");
} else {
walk(editor, out);
}
} catch (Throwable error) {
out.append("读取失败: ").append(error).append('\n');
}
return out.toString();
}
/** 顺着 Stage 的 actor 树把对象列出来(scene2d 里的一切都是 Actor)。 */
private static int listActors(Object actor, StringBuilder out, int depth, int count) {
if (actor == null || depth > 3 || count > 80) return count;
StringBuilder pad = new StringBuilder();
for (int i = 0; i < depth; i++) pad.append(" ");
out.append(pad).append(actor.getClass().getName()).append('\n');
count++;
Object children = children(actor);
if (children instanceof Object[]) {
for (Object child : (Object[]) children) {
count = listActors(child, out, depth + 1, count);
}
}
return count;
}
private static Object children(Object actor) {
for (Class<?> type = actor.getClass(); type != null; type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
String name = field.getType().getName();
if (!name.startsWith("[L") || !name.endsWith("dl;")) continue;
try {
field.setAccessible(true);
Object value = field.get(actor);
if (value instanceof Object[]) return value;
} catch (Throwable ignored) {
// 跳过
}
}
}
return null;
}
private static void collectComponents(java.awt.Container container, StringBuilder out, int depth) {
if (container == null || depth > 3) return;
for (java.awt.Component child : container.getComponents()) {
out.append(indent(depth)).append(child.getClass().getName());
if (child instanceof java.awt.Container) {
out.append(" 子组件=").append(((java.awt.Container) child).getComponentCount());
}
out.append('\n');
if (child instanceof java.awt.Container) {
collectComponents((java.awt.Container) child, out, depth + 1);
}
}
}
private static String indent(int depth) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < depth; i++) builder.append(" ");
return builder.toString();
}
/** OG.D 是编辑器单例(Editor.<clinit> 里 new Editor() 挂上去的)。 */
static Object findEditor(ClassLoader loader) throws Exception {
Class<?> holder = Class.forName("OG", true, loader);
for (Field field : holder.getDeclaredFields()) {
if (!field.getName().equals("D") || !Modifier.isStatic(field.getModifiers())) continue;
field.setAccessible(true);
Object value = field.get(null);
if (value != null && value.getClass().getName()
.equals("com.esotericsoftware.spine.editor.Editor")) {
return value;
}
}
return null;
}
private static void walk(Object root, StringBuilder out) {
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
List<Object> queue = new ArrayList<Object>();
List<Integer> depths = new ArrayList<Integer>();
queue.add(root);
depths.add(Integer.valueOf(0));
int head = 0;
int printed = 0;
while (head < queue.size() && printed < 120) {
Object current = queue.get(head);
int depth = depths.get(head).intValue();
head++;
if (current == null || !seen.add(current) || current.getClass().isArray()) continue;
String name = current.getClass().getName();
if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.")
|| name.startsWith("org.lwjgl") || name.startsWith("com.badlogic")) continue;
Field[] fields = current.getClass().getDeclaredFields();
out.append(indent(depth)).append(name).append(" 字段数=").append(fields.length)
.append('\n');
printed++;
if (depth >= 2) continue;
for (Field field : fields) {
if (Modifier.isStatic(field.getModifiers())) continue;
try {
field.setAccessible(true);
Object value = field.get(current);
if (value == null) continue;
queue.add(value);
depths.add(Integer.valueOf(depth + 1));
} catch (Throwable ignored) {
// 跳过
}
}
}
}
}