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. 里 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 seen = Collections.newSetFromMap(new IdentityHashMap()); List queue = new ArrayList(); List depths = new ArrayList(); 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) { // 跳过 } } } } }