- Commands.java:Editor -> pE(命令系统) -> void D(String) 按名字执行命令 - Launcher:新增 -Dspine.runCommand="<命令名>" 便于验证 - SceneReader:Tree/选中项探针(actor 树按 SnapshotArray 结构遍历) - 混淆名对照表.md:补命令系统一节、可执行命令名单、skeleton/root 删除边界、 以及 New X 最终落到静态 KP.B(target, 类型名) 的结论与待办
355 lines
14 KiB
Java
355 lines
14 KiB
Java
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();
|
||
// 找"当前选中项"的专用探针(-Dspine.probeTree=1 时只输出它,便于只看这一段)。
|
||
if (System.getProperty("spine.probeTree") != null) {
|
||
return probeTrees(stage);
|
||
}
|
||
// 面板里的文字全部用英文:编辑器这套运行时只装了 NotoSans 拉丁字形,
|
||
// 中文会渲染成一排小方块(tofu)。
|
||
out.append("=== Scene actors ===\n");
|
||
try {
|
||
listActors(stage, out, 0, 0);
|
||
} catch (Throwable error) {
|
||
out.append("read failed: ").append(error).append('\n');
|
||
}
|
||
out.append("=== Editor windows ===\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=== Objects reachable from Editor ===\n");
|
||
try {
|
||
Object editor = findEditor(editorLoader);
|
||
if (editor == null) {
|
||
out.append("(editor instance not ready yet)\n");
|
||
} else {
|
||
walk(editor, out);
|
||
}
|
||
} catch (Throwable error) {
|
||
out.append("read failed: ").append(error).append('\n');
|
||
}
|
||
return out.toString();
|
||
}
|
||
|
||
/**
|
||
* 探针:在 actor 树里找 `Tree` 控件(这个类名是保名的),把它的节点结构有界地 dump 出来。
|
||
*
|
||
* 目的:编辑器的层级树里挂着"骨骼 / 插槽 / 附件"的对象,找到它就能拿到
|
||
* **当前选中的那个对象**,这是面板要用的真正输入。
|
||
* 只做有界遍历(深度 + 元素个数都限死)—— 这套运行时对大范围反射很敏感,扫猛了会崩。
|
||
*/
|
||
static String probeTrees(Object stage) {
|
||
StringBuilder out = new StringBuilder();
|
||
out.append("=== Tree widgets (selection probe) ===\n");
|
||
int[] budget = new int[] {3};
|
||
int[] visited = new int[] {0};
|
||
try {
|
||
Object root = firstFieldOfType(stage, "do");
|
||
out.append("stage=").append(stage.getClass().getName())
|
||
.append(" root=").append(root == null ? "null" : root.getClass().getName()).append('\n');
|
||
findTrees(root, out, 0, budget, visited);
|
||
out.append("visited actors=").append(visited[0]).append('\n');
|
||
} catch (Throwable error) {
|
||
out.append("probe failed: ").append(error).append('\n');
|
||
}
|
||
System.err.println(out);
|
||
return out.toString();
|
||
}
|
||
|
||
/** Stage 的 root 是 Group(载荷里 do),它才是 UI 树真正的根。 */
|
||
private static Object firstFieldOfType(Object target, String typeName) {
|
||
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||
type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||
if (!field.getType().getName().equals(typeName)) continue;
|
||
try {
|
||
field.setAccessible(true);
|
||
Object value = field.get(target);
|
||
if (value != null) return value;
|
||
} catch (Throwable ignored) {
|
||
// 跳过
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static void findTrees(Object actor, StringBuilder out, int depth, int[] budget,
|
||
int[] visited) {
|
||
if (actor == null || depth > 20 || budget[0] <= 0 || visited[0] > 800) return;
|
||
visited[0]++;
|
||
if (visited[0] <= 120) {
|
||
out.append(indent(depth)).append(actor.getClass().getName()).append('\n');
|
||
}
|
||
if (actor.getClass().getName().startsWith("com.badlogic.gdx.scenes.scene2d.ui.Tree")) {
|
||
budget[0]--;
|
||
out.append("--- ").append(actor.getClass().getName()).append(" ---\n");
|
||
dumpFields(actor, out, 1, 3, 12);
|
||
}
|
||
Object children = children(actor);
|
||
if (children instanceof Object[]) {
|
||
for (Object child : (Object[]) children) {
|
||
findTrees(child, out, depth + 1, budget, visited);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 列出对象的字段:标量直接给值,数组/集合只列前几个元素的类名,最多往下 maxDepth 层。 */
|
||
private static void dumpFields(Object target, StringBuilder out, int depth, int maxDepth,
|
||
int maxItems) {
|
||
if (target == null || depth > maxDepth) return;
|
||
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||
type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||
Object value;
|
||
try {
|
||
field.setAccessible(true);
|
||
value = field.get(target);
|
||
} catch (Throwable error) {
|
||
continue;
|
||
}
|
||
out.append(indent(depth)).append(field.getName()).append(':')
|
||
.append(field.getType().getSimpleName()).append(" = ");
|
||
if (value == null) {
|
||
out.append("null\n");
|
||
} else if (value instanceof Number || value instanceof Boolean
|
||
|| value instanceof CharSequence) {
|
||
out.append(value).append('\n');
|
||
} else if (value.getClass().isArray() || arrayItems(value) != null) {
|
||
Object[] items = arrayItems(value);
|
||
out.append(value.getClass().getSimpleName()).append(" size=").append(items.length)
|
||
.append(" [");
|
||
for (int i = 0; i < Math.min(items.length, maxItems); i++) {
|
||
Object item = items[i];
|
||
out.append(i > 0 ? ", " : "")
|
||
.append(item == null ? "null" : item.getClass().getSimpleName());
|
||
}
|
||
out.append("]\n");
|
||
// 把前几个元素也展开一层:Tree 的节点对象就在里面。
|
||
for (int i = 0; i < Math.min(items.length, 2); i++) {
|
||
Object item = items[i];
|
||
if (item == null) continue;
|
||
String name = item.getClass().getName();
|
||
if (name.startsWith("com.esotericsoftware") || !name.contains(".")) {
|
||
out.append(indent(depth + 1)).append("[").append(i).append("] ")
|
||
.append(name).append('\n');
|
||
dumpFields(item, out, depth + 2, maxDepth, maxItems);
|
||
}
|
||
}
|
||
} else {
|
||
out.append(value.getClass().getName()).append('\n');
|
||
}
|
||
if (depth < maxDepth && value != null && !value.getClass().isArray()
|
||
&& !(Number.class.isAssignableFrom(value.getClass()))
|
||
&& !(value instanceof Boolean) && !(value instanceof CharSequence)) {
|
||
String name = value.getClass().getName();
|
||
// 只往"编辑器 / spine 自己的类"里钻,JDK 和 libGDX 内部对象就停住。
|
||
if (name.startsWith("com.esotericsoftware") || !name.contains(".")) {
|
||
dumpFields(value, out, depth + 1, maxDepth, maxItems);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 顺着 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;
|
||
}
|
||
|
||
/**
|
||
* 取 Actor 的子节点。
|
||
*
|
||
* libGDX 的 `Group` 用 `SnapshotArray`(载荷里叫 `hq`)装子节点,里面的 `Array`
|
||
* 又是"`Object[] items` + `int size`"两个字段 —— 而字段名被混淆成一堆同名,
|
||
* 所以这里**按结构找**:先找 `Object[]` 字段,再配一个 int 当 size。
|
||
*/
|
||
private static Object children(Object actor) {
|
||
for (Class<?> type = actor.getClass(); type != null && type != Object.class;
|
||
type = type.getSuperclass()) {
|
||
for (Field field : type.getDeclaredFields()) {
|
||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||
Object value;
|
||
try {
|
||
field.setAccessible(true);
|
||
value = field.get(actor);
|
||
} catch (Throwable ignored) {
|
||
continue;
|
||
}
|
||
Object[] items = arrayItems(value);
|
||
if (items != null && holdsActors(items)) return items;
|
||
}
|
||
// 只看 Group(do)这一层为止:再往上到 Actor(dl)会撞上 listeners 数组(也是 Object[]),
|
||
// 那个不是子节点。
|
||
if (type.getName().equals("do")) break;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** 子节点数组里装的应该是 Actor(载荷名 dl);不满足就说明这不是 children。 */
|
||
private static boolean holdsActors(Object[] items) {
|
||
for (Object item : items) {
|
||
if (isActor(item)) return true;
|
||
}
|
||
return items.length == 0;
|
||
}
|
||
|
||
private static boolean isActor(Object value) {
|
||
if (value == null) return false;
|
||
for (Class<?> type = value.getClass(); type != null && type != Object.class;
|
||
type = type.getSuperclass()) {
|
||
if (type.getName().equals("dl")) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** 把 `Object[]` 或"装着 Object[] 的容器"(Array/SnapshotArray)还原成元素数组。 */
|
||
private static Object[] arrayItems(Object value) {
|
||
if (value instanceof Object[]) return (Object[]) value;
|
||
if (value == null || value.getClass().isArray()) return null;
|
||
// libGDX 的 Array(载荷名 fI)才是"真正的表":`Object[] items` + `int size`。
|
||
// SnapshotArray(hq)继承它,自己另外还有 snapshot/recycled 两个数组和 snapshots 计数
|
||
// —— 所以必须只认 fI 这一层的字段,否则会拿错数组、size 也拿成 snapshots。
|
||
Class<?> arrayClass = null;
|
||
for (Class<?> type = value.getClass(); type != null && type != Object.class;
|
||
type = type.getSuperclass()) {
|
||
if (type.getName().equals("fI")) {
|
||
arrayClass = type;
|
||
break;
|
||
}
|
||
}
|
||
if (arrayClass == null) return null;
|
||
Object[] items = null;
|
||
int size = -1;
|
||
for (Field field : arrayClass.getDeclaredFields()) {
|
||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||
try {
|
||
field.setAccessible(true);
|
||
if (field.getType() == Object[].class) {
|
||
items = (Object[]) field.get(value);
|
||
} else if (field.getType() == int.class) {
|
||
size = field.getInt(value);
|
||
}
|
||
} catch (Throwable ignored) {
|
||
// 跳过
|
||
}
|
||
}
|
||
if (items == null || size < 0) return null;
|
||
int count = size >= 0 && size <= items.length ? size : items.length;
|
||
Object[] out = new Object[count];
|
||
System.arraycopy(items, 0, out, 0, count);
|
||
return out;
|
||
}
|
||
|
||
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(" children=").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(" fields=").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) {
|
||
// 跳过
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|