编辑器交互/数据 API 调研完成:解出 KP=SelectBox、选中链路、增删改真实落点与撤销栈
- 新工具 tools/decrypt/DumpSites.java:逐 indy 站点解出『类.方法 + 描述符』(带描述符, 解决 rewrite_source.py 丢描述符导致的重载混淆);ResolveSites.java 走运行时 CallSite 交叉验证。 - 结论改正:KP = libGDX SelectBox,KP.B(String)V = setSelected;XW/acW/acK/Yr 是 『属性面板控件』而不是场景数据;场景对象是树节点上的 GP/Gp/GU/GO。 - 补齐:当前选择怎么设(VZ.D(Object))、谁刷新界面(VZ.D(GP,ZZ) / rW.B / fC.C)、 撤销栈(载荷类 un 的 D()/B() 成对包一层)、删除边界(root 的删除按钮本来就是 disabled)、 查/选/改名/删除/导出的真实实现与出处。 - 新增运行时探针 -Dspine.probe=objects(SceneObjects.java),实测打印整棵场景对象树。 - 文档:编辑器交互与数据API.md 重写;混淆名对照表.md 增补类/方法对照与大小写同名类坑。
This commit is contained in:
1 parent
2b23ce59db
commit
7fbb042aa6
6 files changed
+990
-126
No files matched your search
@@ -128,6 +128,33 @@ public final class Launcher {
|
||||
final ClassLoader commandLoader = viewLoader;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (ViewInstaller.installForLoader(viewLoader)) {
|
||||
// 验证用:-Dspine.probe=objects 会在界面稳定后把"树里的场景对象"整份打到日志里。
|
||||
final String probe = System.getProperty("spine.probe");
|
||||
if (probe != null) {
|
||||
Thread prober = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(6000);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
System.err.println("[probe] ==== " + probe + " ====");
|
||||
if ("objects".equals(probe)) {
|
||||
System.err.println(SceneObjects.describe(viewLoader));
|
||||
} else {
|
||||
System.err.println("[probe] 未知探针: " + probe);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[probe] 失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}, "spineview-probe");
|
||||
prober.setDaemon(true);
|
||||
prober.start();
|
||||
}
|
||||
// 验证用:-Dspine.runCommand="New Bone" 会在界面稳定后执行这条编辑器命令
|
||||
// (名字取自 hotkeys.txt / 菜单项,链路见 Commands)。
|
||||
final String command = System.getProperty("spine.runCommand");
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 场景对象(骨骼 / 插槽 / 附件…)的读取入口。
|
||||
*
|
||||
* 关键事实(都是从载荷字节码里读出来的,见 `混淆名对照表.md`):
|
||||
*
|
||||
* - `OG` 是编辑器的"全局静态持有者",它上面有一串**同名不同描述符**的 `D` 字段:
|
||||
* `D:LEditor;`(编辑器实例)、`D:LVZ;`(Tree 视图 Actor)、`D:LUq;`(libGDX Tree 控件)、
|
||||
* `D:LSkin;`、`D:LPrefs;`、`D:LStage;` … 所以取字段必须**按类型名挑**,不能只看名字。
|
||||
* - 层级树的节点是 libGDX `Tree.Node`(载荷名 `eM`,编辑器子类 `Uo`),
|
||||
* 节点上挂的"场景对象"是 `Gj` 的子类(载荷名 `GP` / `Gp` / `GU` / `GT` …)。
|
||||
* - `VZ.D(fI)fI` 会把树里所有节点的对象收集进一个 `Array`(载荷名 `fI`)里返回,
|
||||
* 这是"列出全部场景对象"的最省事入口。
|
||||
*/
|
||||
public final class SceneObjects {
|
||||
/** com.esotericsoftware.spine.editor.Editor(保名类) */
|
||||
static final String EDITOR = "com.esotericsoftware.spine.editor.Editor";
|
||||
/** Tree 视图的 Actor(载荷名 VZ,extends Table) */
|
||||
static final String TREE_VIEW = "VZ";
|
||||
/** libGDX Tree 控件(载荷名 Uq,extends com.badlogic.gdx.scenes.scene2d.ui.Tree) */
|
||||
static final String TREE = "Uq";
|
||||
/** libGDX Tree.Node(载荷名 eM;编辑器子类 Uo) */
|
||||
static final String NODE = "eM";
|
||||
/** libGDX Array(载荷名 fI) */
|
||||
static final String ARRAY = "fI";
|
||||
/** 场景对象基类(载荷名 Gj) */
|
||||
static final String OBJECT_BASE = "Gj";
|
||||
|
||||
private SceneObjects() {
|
||||
}
|
||||
|
||||
/** 从 OG 的静态字段里按**类型**取一个实例(OG 上一堆同名字段,只能靠类型区分)。 */
|
||||
static Object global(ClassLoader loader, String typeName) throws Exception {
|
||||
Class<?> holder = Class.forName("OG", true, loader);
|
||||
for (Class<?> type = holder; 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;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value != null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** OG 上可能有多个同类型字段(例如 Skin),这里全部收一遍。 */
|
||||
static List<Object> globals(ClassLoader loader, String typeName) throws Exception {
|
||||
List<Object> out = new ArrayList<Object>();
|
||||
Class<?> holder = Class.forName("OG", true, loader);
|
||||
for (Field field : holder.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (!field.getType().getName().equals(typeName)) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value != null) out.add(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出树里全部节点对象 —— `VZ.D(fI)fI`(把每个 node 的对象 add 进传进去的 Array)。
|
||||
*
|
||||
* 注意:这个方法名 `D` 在 VZ 上有很多重载,必须按 (fI)->fI 这个描述符挑。
|
||||
*/
|
||||
public static List<Object> listAll(ClassLoader loader) throws Exception {
|
||||
Object treeView = global(loader, TREE_VIEW);
|
||||
if (treeView == null) throw new IllegalStateException("拿不到 Tree 视图(OG 上没有 VZ 字段)");
|
||||
Class<?> arrayClass = Class.forName(ARRAY, true, loader);
|
||||
Object array = arrayClass.getDeclaredConstructor().newInstance();
|
||||
Method collect = null;
|
||||
for (Class<?> type = treeView.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
Class<?>[] parameters = method.getParameterTypes();
|
||||
if (parameters.length != 1) continue;
|
||||
if (parameters[0] != arrayClass || method.getReturnType() != arrayClass) continue;
|
||||
collect = method;
|
||||
break;
|
||||
}
|
||||
if (collect != null) break;
|
||||
}
|
||||
if (collect == null) throw new IllegalStateException("VZ 上没有 (fI)->fI 的收集方法");
|
||||
collect.setAccessible(true);
|
||||
Object filled = collect.invoke(treeView, array);
|
||||
return new ArrayList<Object>(java.util.Arrays.asList(arrayItems(filled)));
|
||||
}
|
||||
|
||||
/** 当前选中的场景对象(`VZ.B()Object` = 树上 Selection 的 lastSelected 的 object)。 */
|
||||
public static Object selected(ClassLoader loader) throws Exception {
|
||||
Object treeView = global(loader, TREE_VIEW);
|
||||
if (treeView == null) return null;
|
||||
for (Method method : treeView.getClass().getDeclaredMethods()) {
|
||||
if (!method.getName().equals("B")) continue;
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
if (method.getReturnType() != Object.class) continue;
|
||||
method.setAccessible(true);
|
||||
return method.invoke(treeView);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 选中某个场景对象(`VZ.D(Object)Uo` = Tree.findNode + 选中)。返回是否选中成功。 */
|
||||
public static boolean select(ClassLoader loader, Object sceneObject) throws Exception {
|
||||
Object treeView = global(loader, TREE_VIEW);
|
||||
if (treeView == null || sceneObject == null) return false;
|
||||
Class<?> nodeClass = Class.forName(NODE, true, loader);
|
||||
for (Method method : treeView.getClass().getDeclaredMethods()) {
|
||||
Class<?>[] parameters = method.getParameterTypes();
|
||||
if (parameters.length != 1 || parameters[0] != Object.class) continue;
|
||||
if (method.getReturnType() != nodeClass) continue;
|
||||
method.setAccessible(true);
|
||||
return method.invoke(treeView, sceneObject) != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 场景对象的中文类型不在需求里(面板一律英文),这里给英文缩写归类。 */
|
||||
public static String kind(Object sceneObject) {
|
||||
if (sceneObject == null) return "null";
|
||||
String name = sceneObject.getClass().getName();
|
||||
if (name.equals("GP")) return "skeleton";
|
||||
if (name.equals("Gp")) return "bone";
|
||||
if (name.equals("GU")) return "slot";
|
||||
if (name.equals("GT")) return "attachment";
|
||||
return name;
|
||||
}
|
||||
|
||||
/** 对象的显示名(`Gj` 子类的 toString() 就是名字)。 */
|
||||
public static String nameOf(Object sceneObject) {
|
||||
if (sceneObject == null) return "(none)";
|
||||
try {
|
||||
return String.valueOf(sceneObject.toString());
|
||||
} catch (Throwable error) {
|
||||
return sceneObject.getClass().getName();
|
||||
}
|
||||
}
|
||||
|
||||
/** 探针:把树里所有对象 + 层级列出来,用于确认"查"的链路。 */
|
||||
public static String describe(ClassLoader loader) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
try {
|
||||
Object tree = global(loader, TREE);
|
||||
out.append("tree widget = ").append(tree == null ? "null" : tree.getClass().getName())
|
||||
.append('\n');
|
||||
List<Object> all = listAll(loader);
|
||||
out.append("scene objects = ").append(all.size()).append('\n');
|
||||
for (int i = 0; i < all.size() && i < 100; i++) {
|
||||
Object item = all.get(i);
|
||||
out.append(" [").append(i).append("] ").append(item.getClass().getName())
|
||||
.append(" ").append(kind(item)).append(" name=").append(nameOf(item))
|
||||
.append('\n');
|
||||
}
|
||||
Object current = selected(loader);
|
||||
out.append("selected = ").append(current == null ? "null" : nameOf(current))
|
||||
.append('\n');
|
||||
out.append("--- node tree ---\n");
|
||||
dumpNodes(loader, out);
|
||||
} catch (Throwable error) {
|
||||
out.append("describe failed: ").append(error).append('\n');
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/** 顺着 libGDX Tree 控件的 root 节点把层级打印出来(节点上挂着场景对象)。 */
|
||||
private static void dumpNodes(ClassLoader loader, StringBuilder out) throws Exception {
|
||||
Object tree = global(loader, TREE);
|
||||
if (tree == null) return;
|
||||
Class<?> nodeClass = Class.forName(NODE, true, loader);
|
||||
Class<?> arrayClass = Class.forName(ARRAY, true, loader);
|
||||
List<Object> roots = new ArrayList<Object>();
|
||||
for (Class<?> type = tree.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (field.getType() != arrayClass) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(tree);
|
||||
if (value == null) continue;
|
||||
Object[] items = arrayItems(value);
|
||||
for (Object item : items) {
|
||||
if (item != null && nodeClass.isInstance(item)) roots.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Object> seen = java.util.Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||
for (Object root : roots) walkNode(root, out, 0, seen);
|
||||
}
|
||||
|
||||
private static void walkNode(Object node, StringBuilder out, int depth, Set<Object> seen) {
|
||||
if (node == null || depth > 8 || !seen.add(node)) return;
|
||||
Object object = nodeObject(node);
|
||||
if (depth <= 6) {
|
||||
out.append(indent(depth)).append(node.getClass().getName()).append(" -> ")
|
||||
.append(object == null ? "null" : object.getClass().getName())
|
||||
.append(" ").append(kind(object)).append(" name=").append(nameOf(object))
|
||||
.append('\n');
|
||||
}
|
||||
for (Object child : nodeChildren(node)) walkNode(child, out, depth + 1, seen);
|
||||
}
|
||||
|
||||
/** Tree.Node 上的"场景对象"字段(声明在载荷类 eM 上、类型 Object 的那个 D)。 */
|
||||
static Object nodeObject(Object node) {
|
||||
for (Class<?> type = node.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
if (!type.getName().equals(NODE)) continue;
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (field.getType() != Object.class) continue;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
return field.get(node);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Tree.Node 上的 children 数组(声明在 eM 上、类型 fI 的那个 D)。 */
|
||||
static List<Object> nodeChildren(Object node) {
|
||||
List<Object> out = new ArrayList<Object>();
|
||||
for (Class<?> type = node.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
if (!type.getName().equals(NODE)) continue;
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals(ARRAY)) continue;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object[] items = arrayItems(field.get(node));
|
||||
for (Object item : items) if (item != null) out.add(item);
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 把 libGDX Array(载荷名 fI:Object[] items + int size)还原成元素数组。 */
|
||||
static Object[] arrayItems(Object value) {
|
||||
if (value instanceof Object[]) return (Object[]) value;
|
||||
if (value == null || value.getClass().isArray()) return new Object[0];
|
||||
Object[] items = null;
|
||||
int size = -1;
|
||||
for (Class<?> type = value.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
if (!type.getName().equals(ARRAY)) continue;
|
||||
for (Field field : type.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) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (items == null) return new Object[0];
|
||||
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 String indent(int depth) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < depth; i++) builder.append(" ");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user