面板增删改查 + Inspector + 可识别源码版(src-named)
面板(viewpanel/src/com/spineview/) - ObjectPanel:Refresh / New Bone / New Slot / New BBox / New Path / Delete / Rename / Inspect + 名字输入框 + 对象列表(点行=选中)+ 状态行,全英文。 - SceneObjects:查(Tree 控件按层级/扁平列全部对象)、选中(VZ.D(Tree.Node,boolean) —— VZ.D(Object) 内部只是 Selection.L(),不真选中)、读当前选择(VZ.B())。 - SceneActions:增/删/改 + 撤销包装(un.D()/un.B())+ 树节点重建(VZ.D(GP,true,true)); 统一在编辑器渲染线程执行(OG.B(Runnable);OG.I() 有线程断言,非渲染线程会抛 RuntimeException); 输入名字对话框自动化:找到『有 TextField 字段 + L(Object)V 回调』的对话框对象 → 填名字 → 调 L(true)。 - 实测:骨骼新建自动编号(bone7/bone8);模型层允许重名、按名字查只返回第一个;删父骨骼连子节点一起删; 不能在渲染线程里 sleep(有些创建是下一帧做的)。 - 探针:-Dspine.probe=objects|panels|select|crud|matrix|panel。 可识别源码版(工具) - tools/deobf/Renamer.java:ASM Remapper 只改成员名不改类名(indy 运行时要用加密串里的类名反射查)。 - tools/deobf/anchors.tsv(手动 14 条有证据的锚点)+ anchors-libgdx.tsv(MapMethods 对 gdx-1.11.0 指纹自动生成 178 条)→ names.tsv 192 条。 - 产物 ~/Desktop/spine398/src-named(4176 类全量,原 src-readable 不动);已验证 Table.add/clearChildren、SelectBox.getSelected/setSelected/setSelectedIndex、Gp.name。 - 文档:混淆名对照表.md 新增第六节(工具链 + 已验证 + 已知限制)。
This commit is contained in:
1 parent
7fbb042aa6
commit
d9ea449b64
10 files changed
+2225
-43
No files matched your search
@@ -0,0 +1,650 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* "Spine View" 面板的内容:**场景对象的增删改查**。
|
||||
*
|
||||
* 布局(都是英文,编辑器缺中文字形):
|
||||
* ```
|
||||
* [Refresh] [New Bone]
|
||||
* [New Slot] [Delete] [Rename]
|
||||
* [新名字 TextField]
|
||||
* ┌ 对象列表(每个对象一行,点一下就选中它)┐
|
||||
* │ skeleton skeleton │
|
||||
* │ bone root │
|
||||
* │ ... │
|
||||
* └─────────────────────────────────────────┘
|
||||
* 状态行
|
||||
* ```
|
||||
*
|
||||
* 两条硬约束(都在 `编辑器交互与数据API.md` 里):
|
||||
* 1. **凡是要动编辑器模型/控件的代码,必须在编辑器的渲染线程上跑** ——
|
||||
* 统一走 {@link SceneActions#onEditorThread}(它用编辑器的 `OG.B(Runnable)`)。
|
||||
* 2. **所有动作都作用在"当前选择"上** —— 新建/删除/改名之前先 {@link SceneActions#select}。
|
||||
*/
|
||||
public final class ObjectPanel {
|
||||
|
||||
private ObjectPanel() {
|
||||
}
|
||||
|
||||
/** 面板状态(静态:按钮回调是另一条线程进来的,靠它记住"当前对着谁操作")。 */
|
||||
private static final class State {
|
||||
ClassLoader loader;
|
||||
Object skin;
|
||||
Object listTable;
|
||||
Object status;
|
||||
Object nameField;
|
||||
List<Object> objects = new ArrayList<Object>();
|
||||
Object target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 建面板(由 {@link ViewInstaller#buildPanel} 调用)。返回的 Table 会被塞进 us.spineView。
|
||||
*/
|
||||
public static Object build(ClassLoader loader, Object skin) throws Exception {
|
||||
final State state = new State();
|
||||
state.loader = loader;
|
||||
state.skin = skin;
|
||||
|
||||
Class<?> tableClass = Class.forName(Obf.TABLE, true, loader);
|
||||
Class<?> cellClass = Class.forName(Obf.CELL, true, loader);
|
||||
Class<?> actorClass = Class.forName(Obf.ACTOR, true, loader);
|
||||
Class<?> scrollClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.ScrollPane", true, loader);
|
||||
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
|
||||
Class<?> fieldClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextField", true, loader);
|
||||
|
||||
Object root = tableClass.getConstructor().newInstance();
|
||||
Object rows = tableClass.getConstructor().newInstance();
|
||||
state.listTable = tableClass.getConstructor().newInstance();
|
||||
Object scroll = scrollClass.getConstructor(actorClass, skinClass).newInstance(state.listTable, skin);
|
||||
state.status = label(loader, skin, "Press Refresh to list scene objects.");
|
||||
state.nameField = fieldClass.getConstructor(String.class, skinClass)
|
||||
.newInstance("particle_bone", skin);
|
||||
|
||||
// 让面板铺满停靠框(WidgetGroup.setFillParent)。
|
||||
call(root, Obf.TABLE_SET_FILL_PARENT, new Class<?>[] {boolean.class}, void.class, Boolean.TRUE);
|
||||
|
||||
Object refresh = button(loader, skin, "Refresh");
|
||||
Object newBone = button(loader, skin, "New Bone");
|
||||
Object newSlot = button(loader, skin, "New Slot");
|
||||
Object newBBox = button(loader, skin, "New BBox");
|
||||
Object newPath = button(loader, skin, "New Path");
|
||||
Object delete = button(loader, skin, "Delete");
|
||||
Object rename = button(loader, skin, "Rename");
|
||||
Object inspect = button(loader, skin, "Inspect");
|
||||
|
||||
Object cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, refresh);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newBone);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newSlot);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, delete);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, rename);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, inspect);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newBBox);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, newPath);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
cell = call(rows, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
|
||||
layoutCell(cell, Obf.CELL_LEFT, Obf.CELL_EXPAND);
|
||||
call(rows, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
|
||||
// 第 1 行:按钮区;第 2 行:对象列表(撑满);第 3 行:状态行。
|
||||
cell = call(root, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, rows);
|
||||
layoutCell(cell, Obf.CELL_LEFT, Obf.CELL_FILL);
|
||||
call(root, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
cell = call(root, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, scroll);
|
||||
layoutCell(cell, Obf.CELL_EXPAND, Obf.CELL_FILL, Obf.CELL_TOP);
|
||||
call(root, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
cell = call(root, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.status);
|
||||
layoutCell(cell, Obf.CELL_LEFT, Obf.CELL_FILL);
|
||||
|
||||
wire(loader, refresh, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshList(state);
|
||||
}
|
||||
});
|
||||
wire(loader, newBone, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
create(state, "Bone");
|
||||
}
|
||||
});
|
||||
wire(loader, newSlot, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
create(state, "Slot");
|
||||
}
|
||||
});
|
||||
wire(loader, newBBox, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
create(state, "Bounding Box");
|
||||
}
|
||||
});
|
||||
wire(loader, newPath, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
create(state, "Path");
|
||||
}
|
||||
});
|
||||
wire(loader, inspect, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
inspect(state);
|
||||
}
|
||||
});
|
||||
wire(loader, delete, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (state.target == null) {
|
||||
setStatus(state, "Delete: select an object first.");
|
||||
return;
|
||||
}
|
||||
Object victim = state.target;
|
||||
String what = SceneObjects.kind(victim) + " " + SceneObjects.nameOf(victim);
|
||||
state.target = null;
|
||||
boolean ok = SceneActions.delete(state.loader, victim);
|
||||
setStatus(state, (ok ? "Deleted " : "Delete failed: ") + what);
|
||||
refreshList(state);
|
||||
}
|
||||
});
|
||||
wire(loader, rename, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (state.target == null) {
|
||||
setStatus(state, "Rename: select an object first.");
|
||||
return;
|
||||
}
|
||||
String name = textFieldText(state.nameField);
|
||||
boolean ok = SceneActions.rename(state.loader, state.target, name);
|
||||
setStatus(state, (ok ? "Renamed to " : "Rename failed: ") + name);
|
||||
refreshList(state);
|
||||
}
|
||||
});
|
||||
|
||||
System.err.println("[panel] 增删改查面板已建好");
|
||||
refreshList(state);
|
||||
return root;
|
||||
}
|
||||
|
||||
/** 重新读一遍树、重建对象列表(全部在编辑器渲染线程上做)。 */
|
||||
private static void refreshList(final State state) {
|
||||
SceneActions.onEditorThread(state.loader, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<Object> objects;
|
||||
try {
|
||||
objects = SceneObjects.listAllNodes(state.loader);
|
||||
} catch (Throwable error) {
|
||||
setStatus(state, "List failed: " + error);
|
||||
return;
|
||||
}
|
||||
state.objects = objects;
|
||||
try {
|
||||
// Table.clearChildren()(载荷名 `SourceFile()V`):把列表里的行全清掉重建。
|
||||
call(state.listTable, Obf.TABLE_CLEAR_CHILDREN, new Class<?>[0], void.class);
|
||||
Class<?> cellClass = Class.forName(Obf.CELL, true, state.loader);
|
||||
Class<?> actorClass = Class.forName(Obf.ACTOR, true, state.loader);
|
||||
for (int i = 0; i < objects.size() && i < 300; i++) {
|
||||
final Object object = objects.get(i);
|
||||
Object row = button(state.loader, state.skin,
|
||||
SceneObjects.kind(object) + " " + SceneObjects.nameOf(object));
|
||||
Object cell = call(state.listTable, Obf.TABLE_ADD,
|
||||
new Class<?>[] {actorClass}, cellClass, row);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
call(state.listTable, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
wire(state.loader, row, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
selectTarget(state, object);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
setStatus(state, "List build failed: " + error);
|
||||
return;
|
||||
}
|
||||
setStatus(state, objects.size() + " scene objects"
|
||||
+ (state.target == null ? "" : "; target = " + SceneObjects.kind(state.target)
|
||||
+ " " + SceneObjects.nameOf(state.target)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 点列表里的某一行:在编辑器层级树里选中它,并把它记成"当前操作对象"。 */
|
||||
private static void selectTarget(final State state, final Object object) {
|
||||
state.target = object;
|
||||
boolean ok = SceneActions.select(state.loader, object);
|
||||
setStatus(state, (ok ? "Selected " : "Select failed: ")
|
||||
+ SceneObjects.kind(object) + " " + SceneObjects.nameOf(object));
|
||||
}
|
||||
|
||||
/** 新建:以当前对象为父级跑编辑器的 `New <type>`。 */
|
||||
/**
|
||||
* 新建:以当前对象为父级跑编辑器的 `New <type>`。
|
||||
* 名称输入框里**有字就用它**,没字就让编辑器那套自动化自己生成(`New Slot` 会弹输入框,
|
||||
* 面板会自动把名字填进去并点 OK,见 SceneActions#fillDialogWhenReady)。
|
||||
*/
|
||||
private static void create(final State state, final String type) {
|
||||
if (state.target == null) {
|
||||
setStatus(state, "New " + type + ": select a parent object first.");
|
||||
return;
|
||||
}
|
||||
Object parent = state.target;
|
||||
String typed = textFieldText(state.nameField);
|
||||
SceneActions.nextName(typed == null ? "" : typed.trim());
|
||||
boolean ok = SceneActions.create(state.loader, parent, type);
|
||||
String used = SceneActions.lastName();
|
||||
setStatus(state, (ok ? "New " + type + " under " : "New " + type + " failed: ")
|
||||
+ SceneObjects.kind(parent) + " " + SceneObjects.nameOf(parent)
|
||||
+ (used == null ? "" : " name=" + used));
|
||||
refreshList(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspector:把**编辑器里当前选中对象**的字段与方法列到列表区。
|
||||
* 字段名是被混淆的(一堆 D/B/L),所以每行都带上类型;对象引用只显示类名。
|
||||
*/
|
||||
private static void inspect(final State state) {
|
||||
SceneActions.onEditorThread(state.loader, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Object object;
|
||||
try {
|
||||
object = SceneObjects.selected(state.loader);
|
||||
} catch (Throwable error) {
|
||||
setStatus(state, "Inspect failed: " + error);
|
||||
return;
|
||||
}
|
||||
if (object == null) {
|
||||
setStatus(state, "Inspect: nothing selected (click a row, or select in the editor tree).");
|
||||
return;
|
||||
}
|
||||
List<String> lines = new ArrayList<String>();
|
||||
lines.add("selected: " + SceneObjects.kind(object) + " \"" + SceneObjects.nameOf(object)
|
||||
+ "\" (" + object.getClass().getName() + ")");
|
||||
lines.add("--- fields (" + countFields(object) + ") ---");
|
||||
for (Class<?> type = object.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (java.lang.reflect.Field field : type.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||||
String value;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object raw = field.get(object);
|
||||
value = raw == null ? "null"
|
||||
: (raw instanceof Number || raw instanceof Boolean || raw instanceof String)
|
||||
? String.valueOf(raw) : raw.getClass().getName();
|
||||
} catch (Throwable error) {
|
||||
value = "<" + error.getClass().getSimpleName() + ">";
|
||||
}
|
||||
lines.add(" " + shortType(field.getType().getName()) + " " + field.getName()
|
||||
+ " = " + value + " @" + type.getName());
|
||||
}
|
||||
}
|
||||
lines.add("--- methods ---");
|
||||
for (Method method : object.getClass().getMethods()) {
|
||||
if (method.getDeclaringClass() == Object.class) continue;
|
||||
StringBuilder params = new StringBuilder();
|
||||
for (Class<?> parameter : method.getParameterTypes()) {
|
||||
if (params.length() > 0) params.append(", ");
|
||||
params.append(shortType(parameter.getName()));
|
||||
}
|
||||
lines.add(" " + shortType(method.getReturnType().getName()) + " " + method.getName()
|
||||
+ "(" + params + ")");
|
||||
}
|
||||
fillTextRows(state, lines);
|
||||
setStatus(state, "Inspected " + SceneObjects.kind(object) + " \""
|
||||
+ SceneObjects.nameOf(object) + "\": " + (lines.size() - 2) + " entries");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int countFields(Object object) {
|
||||
int count = 0;
|
||||
for (Class<?> type = object.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (java.lang.reflect.Field field : type.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static String shortType(String name) {
|
||||
int index = name.lastIndexOf('.');
|
||||
String simple = index < 0 ? name : name.substring(index + 1);
|
||||
if (simple.length() == 1) return name; // D/B/L 这种混淆名:带上包名才分得清
|
||||
return simple;
|
||||
}
|
||||
|
||||
/** 用纯文本行填列表区(行不可点)。 */
|
||||
private static void fillTextRows(final State state, List<String> lines) {
|
||||
try {
|
||||
call(state.listTable, Obf.TABLE_CLEAR_CHILDREN, new Class<?>[0], void.class);
|
||||
Class<?> cellClass = Class.forName(Obf.CELL, true, state.loader);
|
||||
Class<?> actorClass = Class.forName(Obf.ACTOR, true, state.loader);
|
||||
for (String line : lines) {
|
||||
Object row = button(state.loader, state.skin, line);
|
||||
Object cell = call(state.listTable, Obf.TABLE_ADD, new Class<?>[] {actorClass},
|
||||
cellClass, row);
|
||||
layoutCell(cell, Obf.CELL_LEFT);
|
||||
call(state.listTable, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] inspector 填充失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setStatus(final State state, final String text) {
|
||||
if (state.status == null) return;
|
||||
SceneActions.onEditorThread(state.loader, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
call(state.status, Obf.LABEL_SET_TEXT, new Class<?>[] {CharSequence.class},
|
||||
void.class, text);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 状态行更新失败: " + error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** TextField 里的文字(名字被混淆了,按"无参 + 返回 String"取)。 */
|
||||
private static String textFieldText(Object field) {
|
||||
for (Method method : field.getClass().getMethods()) {
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
if (method.getReturnType() != String.class) continue;
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
Object value = method.invoke(field);
|
||||
if (value instanceof String) return (String) value;
|
||||
} catch (Throwable ignored) {
|
||||
// 换下一个
|
||||
}
|
||||
}
|
||||
return "particle_bone";
|
||||
}
|
||||
|
||||
private static Object button(ClassLoader loader, Object skin, String text) throws Exception {
|
||||
Class<?> buttonClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
|
||||
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
|
||||
return buttonClass.getConstructor(String.class, skinClass).newInstance(text, skin);
|
||||
}
|
||||
|
||||
private static Object label(ClassLoader loader, Object skin, String text) throws Exception {
|
||||
Class<?> labelClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Label", true, loader);
|
||||
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
|
||||
return labelClass.getConstructor(CharSequence.class, skinClass).newInstance(text, skin);
|
||||
}
|
||||
|
||||
// ==================== 按钮点击:一个轮询线程 + 一张路由表 ====================
|
||||
//
|
||||
// libGDX 的 Button 自己带 ClickListener,点一下会翻转 isChecked,所以轮询 isChecked 就等于监听点击。
|
||||
// 用轮询而不是 addListener,是因为监听器接口的实例没法用反射 new 出来(Scene2dPanel 里已经踩过)。
|
||||
// 一条路由 = { button, action, isCheckedMethod, setCheckedQuietMethod }。
|
||||
private static final List<Object[]> ROUTES = new ArrayList<Object[]>();
|
||||
private static volatile boolean routerStarted;
|
||||
|
||||
/** 验证用:记住面板上的按钮(标签 → 按钮),探针可以按标签"点"它们。 */
|
||||
private static final java.util.Map<String, Object> BUTTONS =
|
||||
new java.util.LinkedHashMap<String, Object>();
|
||||
|
||||
/** 建面板的顺序(验证用):0 Refresh 1 New Bone 2 New Slot 3 New BBox 4 New Path 5 Delete 6 Rename。 */
|
||||
private static final List<Object> ORDER = new ArrayList<Object>();
|
||||
|
||||
static Object buttonAt(int index) {
|
||||
synchronized (ORDER) {
|
||||
return index >= 0 && index < ORDER.size() ? ORDER.get(index) : null;
|
||||
}
|
||||
}
|
||||
|
||||
static void remember(String label, Object button) {
|
||||
synchronized (BUTTONS) {
|
||||
BUTTONS.put(label, button);
|
||||
ORDER.add(button);
|
||||
if (ROUTES.size() > 100000) BUTTONS.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static Object panelButton(String label) {
|
||||
synchronized (BUTTONS) {
|
||||
return BUTTONS.get(label);
|
||||
}
|
||||
}
|
||||
|
||||
static void wire(ClassLoader loader, Object button, Runnable action) {
|
||||
try {
|
||||
Method isChecked = null;
|
||||
Method resetQuiet = null;
|
||||
Class<?> buttonClass = button.getClass();
|
||||
for (Method method : buttonClass.getMethods()) {
|
||||
if (!method.getName().equals(Obf.BUTTON_IS_CHECKED)) continue;
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
if (method.getReturnType() != boolean.class) continue;
|
||||
isChecked = method;
|
||||
break;
|
||||
}
|
||||
for (Method method : buttonClass.getMethods()) {
|
||||
if (!method.getName().equals(Obf.BUTTON_SET_CHECKED_FIRE)) continue;
|
||||
if (method.getParameterTypes().length != 2) continue;
|
||||
if (method.getParameterTypes()[0] != boolean.class
|
||||
|| method.getParameterTypes()[1] != boolean.class) continue;
|
||||
resetQuiet = method;
|
||||
break;
|
||||
}
|
||||
if (isChecked == null) {
|
||||
System.err.println("[panel] 找不到 isChecked,按钮不响应: " + button);
|
||||
return;
|
||||
}
|
||||
isChecked.setAccessible(true);
|
||||
if (resetQuiet != null) resetQuiet.setAccessible(true);
|
||||
synchronized (ROUTES) {
|
||||
ROUTES.add(new Object[] {button, action, isChecked, resetQuiet});
|
||||
}
|
||||
remember(buttonLabel(button), button);
|
||||
startRouter();
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 接按钮失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 按钮上的文字(TextButton 自己的 getText;不取 Actor.getName)。 */
|
||||
static String buttonLabel(Object button) {
|
||||
for (Class<?> type = button.getClass(); type != null
|
||||
&& !type.getName().equals(Obf.ACTOR); type = type.getSuperclass()) {
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
if (method.getReturnType() != String.class) continue;
|
||||
// 跳过 Object/Actor 那一批(toString 会返回 "TextButton: Refresh" 这种调试串)
|
||||
if (method.getName().equals("toString")) continue;
|
||||
if (method.getName().equals("hashCode")) continue;
|
||||
if (method.getName().equals("equals")) continue;
|
||||
if (method.getName().equals("getName")) continue;
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
Object value = method.invoke(button);
|
||||
if (value instanceof String) return (String) value;
|
||||
} catch (Throwable ignored) {
|
||||
// 换下一个
|
||||
}
|
||||
}
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
private static void startRouter() {
|
||||
if (routerStarted) return;
|
||||
synchronized (ObjectPanel.class) {
|
||||
if (routerStarted) return;
|
||||
routerStarted = true;
|
||||
}
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
List<Object[]> snapshot;
|
||||
synchronized (ROUTES) {
|
||||
snapshot = new ArrayList<Object[]>(ROUTES);
|
||||
}
|
||||
for (Object[] route : snapshot) {
|
||||
if (!Boolean.TRUE.equals(((Method) route[2]).invoke(route[0]))) continue;
|
||||
Method resetQuiet = (Method) route[3];
|
||||
if (resetQuiet != null) {
|
||||
resetQuiet.invoke(route[0], Boolean.FALSE, Boolean.FALSE);
|
||||
}
|
||||
try {
|
||||
((Runnable) route[1]).run();
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 动作失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
Thread.sleep(80);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 轮询异常: " + error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "spineview-crud-router");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
// ==================== 反射小工具(名字被混淆,一律"名字 + 参数 + 返回类型"三判) ====================
|
||||
|
||||
/**
|
||||
* 验证用探针:按顺序"点"面板上的按钮,然后把每一步的结果打出来。
|
||||
* 点击 = 调 `setChecked(true, true)`(与真人点击效果一致:路由器看到 isChecked 就执行动作)。
|
||||
*/
|
||||
public static String probePanel(ClassLoader loader) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append("buttons: ").append(buttonLabels()).append('\n');
|
||||
press(0, out); // Refresh
|
||||
out.append("after Refresh: ").append(rows(loader)).append('\n');
|
||||
// 列表是 Refresh 之后才建出来的,找到 "bone bone3" 那一行并按它
|
||||
press(panelButton("bone bone3"), "row bone bone3", out);
|
||||
out.append("after select: ").append(rows(loader)).append('\n');
|
||||
press(2, out); // New Slot(会弹输入框,看面板能不能自己填+点 OK)
|
||||
sleepQuiet(3000);
|
||||
out.append("after New Slot: ").append(rows(loader)).append('\n');
|
||||
press(3, out); // New BBox
|
||||
sleepQuiet(3000);
|
||||
out.append("after New BBox: ").append(rows(loader)).append('\n');
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static void press(String label, StringBuilder out) {
|
||||
press(panelButton(label), label, out);
|
||||
}
|
||||
|
||||
private static void press(int index, StringBuilder out) {
|
||||
press(buttonAt(index), "#" + index, out);
|
||||
}
|
||||
|
||||
private static void press(Object button, String label, StringBuilder out) {
|
||||
if (button == null) {
|
||||
out.append("press ").append(label).append(" -> button not found\n");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Method setChecked = null;
|
||||
for (Method method : button.getClass().getMethods()) {
|
||||
if (!method.getName().equals(Obf.BUTTON_SET_CHECKED_FIRE)) continue;
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 2 || types[0] != boolean.class || types[1] != boolean.class) continue;
|
||||
setChecked = method;
|
||||
break;
|
||||
}
|
||||
if (setChecked == null) {
|
||||
out.append("press ").append(label).append(" -> no setChecked(boolean,boolean)\n");
|
||||
return;
|
||||
}
|
||||
setChecked.setAccessible(true);
|
||||
setChecked.invoke(button, Boolean.TRUE, Boolean.TRUE);
|
||||
out.append("press ").append(label).append(" ok\n");
|
||||
sleepQuiet(600);
|
||||
} catch (Throwable error) {
|
||||
out.append("press ").append(label).append(" failed: ").append(error).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
private static void sleepQuiet(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private static String buttonLabels() {
|
||||
StringBuilder out = new StringBuilder();
|
||||
synchronized (BUTTONS) {
|
||||
for (String label : BUTTONS.keySet()) {
|
||||
if (out.length() > 0) out.append(", ");
|
||||
out.append('"').append(label).append('"');
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static String rows(ClassLoader loader) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
try {
|
||||
for (Object item : SceneObjects.listAllNodes(loader)) {
|
||||
out.append(SceneObjects.kind(item)).append(' ').append(SceneObjects.nameOf(item))
|
||||
.append(" | ");
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 换下面那条路
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
static Object call(Object target, String name, Class<?>[] parameterTypes, Class<?> returnType,
|
||||
Object... arguments) throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (Method method : type.getMethods()) {
|
||||
if (!method.getName().equals(name)) continue;
|
||||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||||
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||||
if (returnType != null && method.getReturnType() != returnType) continue;
|
||||
method.setAccessible(true);
|
||||
return method.invoke(target, arguments);
|
||||
}
|
||||
}
|
||||
throw new NoSuchMethodException(target.getClass().getName() + "." + name
|
||||
+ java.util.Arrays.toString(parameterTypes));
|
||||
}
|
||||
|
||||
static void layoutCell(Object cell, String... names) {
|
||||
for (String name : names) {
|
||||
try {
|
||||
Method method = cell.getClass().getMethod(name);
|
||||
method.setAccessible(true);
|
||||
method.invoke(cell);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 单元格布局 " + name + " 失败: " + error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user