阶段四: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,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) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user