Files
Spine_lab/viewpanel/src/com/spineview/SceneReader.java
T
tianmo a0080428f9 场景对象增删改查调研:命令系统入口 + 新建对象的真实落点
- Commands.java:Editor -> pE(命令系统) -> void D(String) 按名字执行命令
- Launcher:新增 -Dspine.runCommand="<命令名>" 便于验证
- SceneReader:Tree/选中项探针(actor 树按 SnapshotArray 结构遍历)
- 混淆名对照表.md:补命令系统一节、可执行命令名单、skeleton/root 删除边界、
  以及 New X 最终落到静态 KP.B(target, 类型名) 的结论与待办
2026-10-16 14:20:45 +08:00

355 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) {
// 跳过
}
}
}
}
}