新增阶段五文档:面板接真实数据(增删改查/属性读写/Inspector 真名翻译/滚动焦点/统计)+ 14 条踩坑点 + 未完成清单
This commit is contained in:
1 parent
bb817b85e5
commit
7f06c6744a
43 files changed
+2942
-79
No files matched your search
@@ -0,0 +1,2 @@
|
||||
Agent-Class: BoneMaker
|
||||
Can-Retransform-Classes: false
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker2
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker3
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker4
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker5
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker6
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker7
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe1
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe2
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe3
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe4
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe5
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe6
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe7
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe8
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff1
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff2
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff3
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff4
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe3
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe4
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe5
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe6
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe7
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe9
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: ResetMaker1
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: ResetMaker2
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: SlotMaker1
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: SlotMaker2
|
||||
@@ -0,0 +1,257 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来,
|
||||
* 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。
|
||||
*
|
||||
* 依据(都不是猜的):
|
||||
* - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY,
|
||||
* scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float);
|
||||
* - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height;
|
||||
* - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、
|
||||
* `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。
|
||||
*/
|
||||
public final class GeomProbe1 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Object STATE;
|
||||
static Class<?> OG;
|
||||
|
||||
static final String[] FLOAT_NAMES = {
|
||||
"x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation"
|
||||
};
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; }
|
||||
}
|
||||
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||
Class<?> panel = Class.forName("com.spineview.ObjectPanel", true, L);
|
||||
Field last = panel.getDeclaredField("LAST_STATE");
|
||||
last.setAccessible(true);
|
||||
STATE = last.get(null);
|
||||
if (STATE == null) { log("LAST_STATE 还是 null(面板没建)"); return; }
|
||||
OG = Class.forName("OG", true, L);
|
||||
Method onRender = null;
|
||||
for (Method m : OG.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object buttons = state("buttons");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
Object status = state("status");
|
||||
log("==================== 面板控件几何 ====================");
|
||||
dump("root ", root);
|
||||
dump("buttons ", buttons);
|
||||
dump("listScroll ", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", status);
|
||||
log("---- root 的父链(往上看 4 层,看谁给了多少高度)----");
|
||||
Object p = parent(root);
|
||||
for (int i = 0; i < 4 && p != null; i++) {
|
||||
dump("parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- listScroll 的父链 ----");
|
||||
p = parent(scroll);
|
||||
for (int i = 0; i < 3 && p != null; i++) {
|
||||
dump("scroll.parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----");
|
||||
dumpScrollPaneFields(scroll);
|
||||
log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----");
|
||||
Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D");
|
||||
int n = size(children);
|
||||
log(" 子控件数 = " + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 2 && i < n - 2) continue;
|
||||
Object child = get(children, i);
|
||||
dump(" child[" + i + "] ", child);
|
||||
}
|
||||
log("---- 关键判定 ----");
|
||||
Object widget = findWidget(scroll);
|
||||
log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable)
|
||||
+ " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")");
|
||||
log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3)
|
||||
+ " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3));
|
||||
Object stage = callActor(root, "D"); // Actor.getStage()
|
||||
if (stage != null) {
|
||||
log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B"));
|
||||
}
|
||||
log("=====================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(label).append(' ').append(actor.getClass().getName());
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i;
|
||||
sb.append(' ').append(name).append('=').append(round(f.getFloat(actor)));
|
||||
i++;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (t.getSuperclass() == null) break;
|
||||
}
|
||||
sb.append(" |getHeight()=").append(round(floatOf(actor, "init")));
|
||||
Object parent = parent(actor);
|
||||
sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName());
|
||||
log(sb.toString());
|
||||
}
|
||||
|
||||
static void dumpScrollPaneFields(Object pane) {
|
||||
java.util.TreeSet<String> names = new java.util.TreeSet<String>();
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
Class<?> type = f.getType();
|
||||
if (type != float.class && type != int.class && type != boolean.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(pane);
|
||||
if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue()));
|
||||
names.add(t.getSimpleName() + "." + f.getName() + "=" + v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : names) sb.append(' ').append(s);
|
||||
log(" " + sb);
|
||||
}
|
||||
|
||||
static Object findWidget(Object pane) {
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType().getName().equals("dl")) {
|
||||
f.setAccessible(true);
|
||||
try { return f.get(pane); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object v = m.invoke(actor);
|
||||
if (v != null) return v;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object callActor(Object actor, String name) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(actor);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object call(Class<?> owner, Object target, String name) {
|
||||
try {
|
||||
for (Method m : owner.getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, int index) {
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
if (i == index) {
|
||||
f.setAccessible(true);
|
||||
try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method m : target.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
m.setAccessible(true);
|
||||
return ((Float) m.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != Object[].class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float round(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来,
|
||||
* 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。
|
||||
*
|
||||
* 依据(都不是猜的):
|
||||
* - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY,
|
||||
* scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float);
|
||||
* - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height;
|
||||
* - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、
|
||||
* `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。
|
||||
*/
|
||||
public final class GeomProbe2 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Object STATE;
|
||||
static Class<?> OG;
|
||||
|
||||
static final String[] FLOAT_NAMES = {
|
||||
"x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation"
|
||||
};
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom2] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; }
|
||||
}
|
||||
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||
Class<?> panel = null;
|
||||
int copies = 0;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
copies++;
|
||||
if (panel == null) panel = c;
|
||||
}
|
||||
log("已加载的 ObjectPanel 份数 = " + copies + " 用 " + panel
|
||||
+ " (loader=" + (panel == null ? "-" : panel.getClassLoader()) + ")");
|
||||
if (panel == null) { log("ObjectPanel 没加载"); return; }
|
||||
Field last = panel.getDeclaredField("LAST_STATE");
|
||||
last.setAccessible(true);
|
||||
STATE = last.get(null);
|
||||
log("LAST_STATE = " + STATE);
|
||||
if (STATE == null) { return; }
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getName().equals("OG")) { OG = c; break; }
|
||||
}
|
||||
if (OG == null) { log("OG 没加载"); return; }
|
||||
Method onRender = null;
|
||||
for (Method m : OG.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object buttons = state("buttons");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
Object status = state("status");
|
||||
log("==================== 面板控件几何 ====================");
|
||||
dump("root ", root);
|
||||
dump("buttons ", buttons);
|
||||
dump("listScroll ", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", status);
|
||||
log("---- root 的父链(往上看 4 层,看谁给了多少高度)----");
|
||||
Object p = parent(root);
|
||||
for (int i = 0; i < 4 && p != null; i++) {
|
||||
dump("parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- listScroll 的父链 ----");
|
||||
p = parent(scroll);
|
||||
for (int i = 0; i < 3 && p != null; i++) {
|
||||
dump("scroll.parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----");
|
||||
dumpScrollPaneFields(scroll);
|
||||
log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----");
|
||||
Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D");
|
||||
int n = size(children);
|
||||
log(" 子控件数 = " + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 2 && i < n - 2) continue;
|
||||
Object child = get(children, i);
|
||||
dump(" child[" + i + "] ", child);
|
||||
}
|
||||
log("---- 关键判定 ----");
|
||||
Object widget = findWidget(scroll);
|
||||
log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable)
|
||||
+ " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")");
|
||||
log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3)
|
||||
+ " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3));
|
||||
Object stage = callActor(root, "D"); // Actor.getStage()
|
||||
if (stage != null) {
|
||||
log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B"));
|
||||
}
|
||||
log("=====================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(label).append(' ').append(actor.getClass().getName());
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i;
|
||||
sb.append(' ').append(name).append('=').append(round(f.getFloat(actor)));
|
||||
i++;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (t.getSuperclass() == null) break;
|
||||
}
|
||||
sb.append(" |getHeight()=").append(round(floatOf(actor, "init")));
|
||||
Object parent = parent(actor);
|
||||
sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName());
|
||||
log(sb.toString());
|
||||
}
|
||||
|
||||
static void dumpScrollPaneFields(Object pane) {
|
||||
java.util.TreeSet<String> names = new java.util.TreeSet<String>();
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
Class<?> type = f.getType();
|
||||
if (type != float.class && type != int.class && type != boolean.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(pane);
|
||||
if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue()));
|
||||
names.add(t.getSimpleName() + "." + f.getName() + "=" + v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : names) sb.append(' ').append(s);
|
||||
log(" " + sb);
|
||||
}
|
||||
|
||||
static Object findWidget(Object pane) {
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType().getName().equals("dl")) {
|
||||
f.setAccessible(true);
|
||||
try { return f.get(pane); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object v = m.invoke(actor);
|
||||
if (v != null) return v;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object callActor(Object actor, String name) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(actor);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object call(Class<?> owner, Object target, String name) {
|
||||
try {
|
||||
for (Method m : owner.getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, int index) {
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
if (i == index) {
|
||||
f.setAccessible(true);
|
||||
try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method m : target.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
m.setAccessible(true);
|
||||
return ((Float) m.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != Object[].class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float round(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来,
|
||||
* 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。
|
||||
*
|
||||
* 依据(都不是猜的):
|
||||
* - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY,
|
||||
* scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float);
|
||||
* - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height;
|
||||
* - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、
|
||||
* `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。
|
||||
*/
|
||||
public final class GeomProbe3 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Object STATE;
|
||||
static Class<?> OG;
|
||||
|
||||
static final String[] FLOAT_NAMES = {
|
||||
"x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation"
|
||||
};
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom3] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; }
|
||||
}
|
||||
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||
Class<?> panel = null;
|
||||
int copies = 0;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
copies++;
|
||||
if (panel == null) panel = c;
|
||||
}
|
||||
log("已加载的 ObjectPanel 份数 = " + copies + " 用 " + panel
|
||||
+ " (loader=" + (panel == null ? "-" : panel.getClassLoader()) + ")");
|
||||
if (panel == null) { log("ObjectPanel 没加载"); return; }
|
||||
// 可能有两份拷贝(各自的静态字段互不相干)——逐个看谁的 LAST_STATE 不是 null
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
log(" 拷贝 " + c + " loader=" + c.getClassLoader() + " LAST_STATE=" + v);
|
||||
if (v != null && STATE == null) { STATE = v; panel = c; }
|
||||
} catch (Throwable e) { log(" 读 " + c + " 的 LAST_STATE 失败: " + e); }
|
||||
}
|
||||
if (panel != null) L = panel.getClassLoader() != null ? panel.getClassLoader() : L;
|
||||
log("选中的 STATE = " + STATE);
|
||||
if (STATE == null) { return; }
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getName().equals("OG")) { OG = c; break; }
|
||||
}
|
||||
if (OG == null) { log("OG 没加载"); return; }
|
||||
Method onRender = null;
|
||||
for (Method m : OG.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object buttons = state("buttons");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
Object status = state("status");
|
||||
log("==================== 面板控件几何 ====================");
|
||||
dump("root ", root);
|
||||
dump("buttons ", buttons);
|
||||
dump("listScroll ", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", status);
|
||||
log("---- root 的父链(往上看 4 层,看谁给了多少高度)----");
|
||||
Object p = parent(root);
|
||||
for (int i = 0; i < 4 && p != null; i++) {
|
||||
dump("parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- listScroll 的父链 ----");
|
||||
p = parent(scroll);
|
||||
for (int i = 0; i < 3 && p != null; i++) {
|
||||
dump("scroll.parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----");
|
||||
dumpScrollPaneFields(scroll);
|
||||
log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----");
|
||||
Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D");
|
||||
int n = size(children);
|
||||
log(" 子控件数 = " + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 2 && i < n - 2) continue;
|
||||
Object child = get(children, i);
|
||||
dump(" child[" + i + "] ", child);
|
||||
}
|
||||
log("---- 关键判定 ----");
|
||||
Object widget = findWidget(scroll);
|
||||
log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable)
|
||||
+ " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")");
|
||||
log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3)
|
||||
+ " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3));
|
||||
Object stage = callActor(root, "D"); // Actor.getStage()
|
||||
if (stage != null) {
|
||||
log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B"));
|
||||
}
|
||||
log("=====================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(label).append(' ').append(actor.getClass().getName());
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i;
|
||||
sb.append(' ').append(name).append('=').append(round(f.getFloat(actor)));
|
||||
i++;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (t.getSuperclass() == null) break;
|
||||
}
|
||||
sb.append(" |getHeight()=").append(round(floatOf(actor, "init")));
|
||||
Object parent = parent(actor);
|
||||
sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName());
|
||||
log(sb.toString());
|
||||
}
|
||||
|
||||
static void dumpScrollPaneFields(Object pane) {
|
||||
java.util.TreeSet<String> names = new java.util.TreeSet<String>();
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
Class<?> type = f.getType();
|
||||
if (type != float.class && type != int.class && type != boolean.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(pane);
|
||||
if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue()));
|
||||
names.add(t.getSimpleName() + "." + f.getName() + "=" + v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : names) sb.append(' ').append(s);
|
||||
log(" " + sb);
|
||||
}
|
||||
|
||||
static Object findWidget(Object pane) {
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType().getName().equals("dl")) {
|
||||
f.setAccessible(true);
|
||||
try { return f.get(pane); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object v = m.invoke(actor);
|
||||
if (v != null) return v;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object callActor(Object actor, String name) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(actor);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object call(Class<?> owner, Object target, String name) {
|
||||
try {
|
||||
for (Method m : owner.getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, int index) {
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
if (i == index) {
|
||||
f.setAccessible(true);
|
||||
try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method m : target.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
m.setAccessible(true);
|
||||
return ((Float) m.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != Object[].class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float round(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 只读探针 4:找"列表里的内容画到面板外面"是谁干的。
|
||||
*
|
||||
* 取值方法都来自 `javap -p -c dl`(Actor)的字节码,不是猜名字:
|
||||
* c_()F = getX (return 字段 D)
|
||||
* d_()F = getY (return 字段 B)
|
||||
* L()F = getWidth (return 字段 L)
|
||||
* I()F = getHeight (return 字段 I)
|
||||
* S()F = getTop (字段 B + 字段 I)
|
||||
* C()F = getRight (字段 D + 字段 L)
|
||||
* D()Ldo; = getParent;do.D()Lhq; = getChildren
|
||||
*/
|
||||
public final class GeomProbe4 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static Class<?> HQL;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom4] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
Class<?> panel = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) { STATE = v; panel = c; }
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("没找到活的 ObjectPanel.LAST_STATE"); return; }
|
||||
log("拿到面板 State: " + STATE + " (来自 " + panel + ")");
|
||||
HQL = Class.forName("hq", true, panel.getClassLoader());
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
Method onRender = null;
|
||||
for (Method m : og.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
log("========== 面板内三个关键控件(stage 坐标由父链累加得到)==========");
|
||||
dump("root ", root);
|
||||
dump("buttons ", state("buttons"));
|
||||
dump("listScroll", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", state("status"));
|
||||
log("========== listTable 有几行 / 每行落在哪 ==========");
|
||||
Object kids = childrenOf(listTable);
|
||||
int n = size(kids);
|
||||
log("行数 = " + n + " listTable 自身 " + geom(listTable));
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 3 && i < n - 3) continue;
|
||||
Object child = get(kids, i);
|
||||
log(" row[" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 可见=" + visible(child));
|
||||
}
|
||||
log("========== 从面板根往上爬,看每层给的高度 ==========");
|
||||
Object p = parent(root);
|
||||
int depth = 0;
|
||||
while (p != null && depth < 8) {
|
||||
log(" 上一级[" + depth + "] " + p.getClass().getName() + " " + geom(p));
|
||||
Object np = parent(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
depth++;
|
||||
}
|
||||
Object stageRoot = p;
|
||||
log("========== stage 根下挂了哪些顶格控件(这些都在窗口坐标里)==========");
|
||||
if (stageRoot != null) {
|
||||
log("stage 根 = " + stageRoot.getClass().getName() + " " + geom(stageRoot));
|
||||
Object top = childrenOf(stageRoot);
|
||||
int m = size(top);
|
||||
log("顶格控件数 = " + m);
|
||||
for (int i = 0; i < m; i++) {
|
||||
Object child = get(top, i);
|
||||
log(" [" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 子控件=" + size(childrenOf(child)) + " 可见=" + visible(child));
|
||||
// 名单里第 2 层也打一下,便于找到"画在左下角"的那一支
|
||||
Object grand = childrenOf(child);
|
||||
int gn = size(grand);
|
||||
for (int j = 0; j < gn && j < 12; j++) {
|
||||
Object g = get(grand, j);
|
||||
log(" └─ " + g.getClass().getName() + " " + geom(g)
|
||||
+ " 子控件=" + size(childrenOf(g)) + " 可见=" + visible(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I"))
|
||||
+ " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]";
|
||||
}
|
||||
|
||||
static String visible(Object actor) {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != boolean.class) continue;
|
||||
if (!m.getName().equals("V")) continue;
|
||||
try { m.setAccessible(true); return String.valueOf(m.invoke(actor)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
log(label + " " + actor.getClass().getName() + " " + geom(actor)
|
||||
+ " 子控件=" + size(childrenOf(actor)));
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (!m.getName().equals("D")) continue;
|
||||
if (!m.getReturnType().getName().equals("do")) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 只读探针 4:找"列表里的内容画到面板外面"是谁干的。
|
||||
*
|
||||
* 取值方法都来自 `javap -p -c dl`(Actor)的字节码,不是猜名字:
|
||||
* c_()F = getX (return 字段 D)
|
||||
* d_()F = getY (return 字段 B)
|
||||
* L()F = getWidth (return 字段 L)
|
||||
* I()F = getHeight (return 字段 I)
|
||||
* S()F = getTop (字段 B + 字段 I)
|
||||
* C()F = getRight (字段 D + 字段 L)
|
||||
* D()Ldo; = getParent;do.D()Lhq; = getChildren
|
||||
*/
|
||||
public final class GeomProbe5 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static Class<?> HQL;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom5] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
Class<?> panel = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) { STATE = v; panel = c; }
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("没找到活的 ObjectPanel.LAST_STATE"); return; }
|
||||
log("拿到面板 State: " + STATE + " (来自 " + panel + ")");
|
||||
// hq(快照数组)在载荷类加载器里,面板类本身在 AppClassLoader —— 不能混用
|
||||
ClassLoader payload = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { payload = c.getClassLoader(); break; }
|
||||
}
|
||||
log("载荷类加载器 = " + payload);
|
||||
HQL = Class.forName("hq", true, payload);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
Method onRender = null;
|
||||
for (Method m : og.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
log("========== 面板内三个关键控件(stage 坐标由父链累加得到)==========");
|
||||
dump("root ", root);
|
||||
dump("buttons ", state("buttons"));
|
||||
dump("listScroll", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", state("status"));
|
||||
log("========== listTable 有几行 / 每行落在哪 ==========");
|
||||
Object kids = childrenOf(listTable);
|
||||
int n = size(kids);
|
||||
log("行数 = " + n + " listTable 自身 " + geom(listTable));
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 3 && i < n - 3) continue;
|
||||
Object child = get(kids, i);
|
||||
log(" row[" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 可见=" + visible(child));
|
||||
}
|
||||
log("========== 从面板根往上爬,看每层给的高度 ==========");
|
||||
Object p = parent(root);
|
||||
int depth = 0;
|
||||
while (p != null && depth < 8) {
|
||||
log(" 上一级[" + depth + "] " + p.getClass().getName() + " " + geom(p));
|
||||
Object np = parent(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
depth++;
|
||||
}
|
||||
Object stageRoot = p;
|
||||
log("========== stage 根下挂了哪些顶格控件(这些都在窗口坐标里)==========");
|
||||
if (stageRoot != null) {
|
||||
log("stage 根 = " + stageRoot.getClass().getName() + " " + geom(stageRoot));
|
||||
Object top = childrenOf(stageRoot);
|
||||
int m = size(top);
|
||||
log("顶格控件数 = " + m);
|
||||
for (int i = 0; i < m; i++) {
|
||||
Object child = get(top, i);
|
||||
log(" [" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 子控件=" + size(childrenOf(child)) + " 可见=" + visible(child));
|
||||
// 名单里第 2 层也打一下,便于找到"画在左下角"的那一支
|
||||
Object grand = childrenOf(child);
|
||||
int gn = size(grand);
|
||||
for (int j = 0; j < gn && j < 12; j++) {
|
||||
Object g = get(grand, j);
|
||||
log(" └─ " + g.getClass().getName() + " " + geom(g)
|
||||
+ " 子控件=" + size(childrenOf(g)) + " 可见=" + visible(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I"))
|
||||
+ " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]";
|
||||
}
|
||||
|
||||
static String visible(Object actor) {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != boolean.class) continue;
|
||||
if (!m.getName().equals("V")) continue;
|
||||
try { m.setAccessible(true); return String.valueOf(m.invoke(actor)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
log(label + " " + actor.getClass().getName() + " " + geom(actor)
|
||||
+ " 子控件=" + size(childrenOf(actor)));
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (!m.getName().equals("D")) continue;
|
||||
if (!m.getReturnType().getName().equals("do")) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 只读探针 6:把面板的**整棵控件树**连坐标打出来(含 stage 坐标),
|
||||
* 用来定位"列表里的内容被画到面板外"。
|
||||
*
|
||||
* 取值方法来自字节码(javap -p -c):
|
||||
* dl.c_()F=getX dl.d_()F=getY dl.L()F=getWidth dl.I()F=getHeight
|
||||
* dl.D()Ldo; = getParent do.D()Lhq; = getChildren hq extends fI(Array),size 字段在 fI 里
|
||||
* ⚠️ et(Table) 自己也声明了 `int L()` / `int I()`(列数/行数),所以必须**按返回类型 float 过滤**。
|
||||
*/
|
||||
public final class GeomProbe6 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static Class<?> HQL, ARR, AO;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom6] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) STATE = v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("LAST_STATE 拿不到"); return; }
|
||||
ClassLoader payload = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { payload = c.getClassLoader(); break; }
|
||||
}
|
||||
HQL = Class.forName("hq", true, payload);
|
||||
ARR = Class.forName("fI", true, payload);
|
||||
AO = Class.forName("dl", true, payload);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
Method onRender = null;
|
||||
for (Method m : og.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(STATE); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work(Object state) throws Exception {
|
||||
Object root = state("root");
|
||||
log("=========== 面板控件树(缩进=层级;坐标为 stage 绝对坐标)===========");
|
||||
walk(root, 0, 0f, 0f, 6);
|
||||
log("=========== 面板根再往上(谁给它多大地方)===========");
|
||||
Object p = parent(root);
|
||||
float ax = 0f, ay = 0f;
|
||||
for (int i = 0; i < 6 && p != null; i++) {
|
||||
log(" 上级[" + i + "] " + p.getClass().getName() + " " + geom(p));
|
||||
Object np = parent(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
}
|
||||
log("=========== 关键对象定位 ===========");
|
||||
Object scroll = state("listScroll"), table = state("listTable");
|
||||
log(" listScroll " + geom(scroll) + " 子控件=" + size(childrenOf(scroll)));
|
||||
log(" listTable " + geom(table) + " 行数=" + size(childrenOf(table)));
|
||||
log(" listTable 的 stage 坐标 = " + stageX(table) + "," + stageY(table));
|
||||
log("=======================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void walk(Object actor, int depth, float px, float py, int maxDepth) {
|
||||
if (actor == null || depth > maxDepth) return;
|
||||
StringBuilder ind = new StringBuilder();
|
||||
for (int i = 0; i < depth; i++) ind.append(" ");
|
||||
float x = fl(actor, "c_"), y = fl(actor, "d_");
|
||||
Object kids = childrenOf(actor);
|
||||
int n = size(kids);
|
||||
log(ind + actor.getClass().getName() + " " + geom(actor)
|
||||
+ " stage=(" + r(px + x) + "," + r(py + y) + ") 子控件=" + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object child = get(kids, i);
|
||||
walk(child, depth + 1, px + x, py + y, maxDepth);
|
||||
}
|
||||
}
|
||||
|
||||
static float stageX(Object actor) {
|
||||
float x = 0f;
|
||||
Object a = actor;
|
||||
for (int i = 0; i < 8 && a != null; i++) { x += fl(a, "c_"); a = parent(a); }
|
||||
return r(x);
|
||||
}
|
||||
|
||||
static float stageY(Object actor) {
|
||||
float y = 0f;
|
||||
Object a = actor;
|
||||
for (int i = 0; i < 8 && a != null; i++) { y += fl(a, "d_"); a = parent(a); }
|
||||
return r(y);
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[local x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I"))
|
||||
+ " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]";
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (!m.getName().equals("D")) continue;
|
||||
if (!m.getReturnType().getName().equals("do")) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType() != HQL) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = ARR; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针 7:确认"列表的 ScrollPane 被搬出面板"这件事。
|
||||
* 1) 面板根表格现在挂了哪几个子控件(对比应该有的 buttons / ScrollPane / 日志 Label)
|
||||
* 2) ScrollPane 现在的父控件是谁、挂在哪一层、坐标多少
|
||||
* 3) Stage 上所有"1 个抢 Actor 参数 + 返回 void"的方法(grabScrollFocus 会逐个调用它们)
|
||||
* —— 特别标出**声明类不在白名单**的那些,它们就是可疑的"搬家"凶手
|
||||
*/
|
||||
public final class GeomProbe7 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static ClassLoader PAYLOAD;
|
||||
static Class<?> HQL, ARR, ACTOR_CLASS, GROUP_CLASS, WIDGETGROUP_CLASS;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom7] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) PAYLOAD = c.getClassLoader();
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) STATE = v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("LAST_STATE 拿不到"); return; }
|
||||
HQL = Class.forName("hq", true, PAYLOAD);
|
||||
ARR = Class.forName("fI", true, PAYLOAD);
|
||||
ACTOR_CLASS = Class.forName("dl", true, PAYLOAD);
|
||||
GROUP_CLASS = Class.forName("do", true, PAYLOAD);
|
||||
WIDGETGROUP_CLASS = Class.forName("eX", true, PAYLOAD);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
Method onRender = null;
|
||||
for (Method m : og.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object pane = state("listScroll");
|
||||
Object table = state("listTable");
|
||||
log("========== 1) 面板根表格现在挂了哪些子控件 ==========");
|
||||
Object kids = groupChildren(root);
|
||||
int n = size(kids);
|
||||
log("子控件数 = " + n + "(正常应该是 3:按钮块 / ScrollPane / 日志 Label)");
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object c = get(kids, i);
|
||||
log(" [" + i + "] " + c.getClass().getName() + " " + geom(c)
|
||||
+ " 是 listScroll 吗=" + (c == pane) + " 是 listTable 吗=" + (c == table));
|
||||
}
|
||||
log("========== 2) ScrollPane 现在挂在谁下面 ==========");
|
||||
Object p = pane;
|
||||
float ax = 0f, ay = 0f;
|
||||
for (int i = 0; i < 6 && p != null; i++) {
|
||||
log(" 层[" + i + "] " + p.getClass().getName() + " " + geom(p));
|
||||
ax += fl(p, "c_");
|
||||
ay += fl(p, "d_");
|
||||
Object np = parentOf(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
}
|
||||
log(" ScrollPane 的绝对坐标 ≈ (" + r(ax) + "," + r(ay) + ") ← 面板在 x≈2052 的位置");
|
||||
log("========== 3) Stage 上那些 1 个 Actor 参数 + void 的方法 ==========");
|
||||
Object stage = null;
|
||||
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, PAYLOAD);
|
||||
for (String name : new String[] {"uk", "dt"}) {
|
||||
try {
|
||||
stage = sceneObjects.getMethod("global", ClassLoader.class, String.class)
|
||||
.invoke(null, PAYLOAD, name);
|
||||
} catch (Throwable ignored) { }
|
||||
if (stage != null) { log(" (用 " + name + " 当 Stage)"); break; }
|
||||
}
|
||||
if (stage == null) { log(" 没拿到 Stage"); return; }
|
||||
String[] skip = {"dl", "do", "eX"};
|
||||
for (Method m : stage.getClass().getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1) continue;
|
||||
if (!t[0].isAssignableFrom(ACTOR_CLASS)) continue;
|
||||
if (m.getReturnType() != void.class) continue;
|
||||
String decl = m.getDeclaringClass().getName();
|
||||
boolean skipped = false;
|
||||
for (String s : skip) if (decl.equals(s)) skipped = true;
|
||||
log(" " + (skipped ? "(已排除)" : "★会被调用★") + " " + decl + "." + m.getName()
|
||||
+ "(" + t[0].getName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static Object groupChildren(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parentOf(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != GROUP_CLASS) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I")) + "]";
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = ARR; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针 7:确认"列表的 ScrollPane 被搬出面板"这件事。
|
||||
* 1) 面板根表格现在挂了哪几个子控件(对比应该有的 buttons / ScrollPane / 日志 Label)
|
||||
* 2) ScrollPane 现在的父控件是谁、挂在哪一层、坐标多少
|
||||
* 3) Stage 上所有"1 个抢 Actor 参数 + 返回 void"的方法(grabScrollFocus 会逐个调用它们)
|
||||
* —— 特别标出**声明类不在白名单**的那些,它们就是可疑的"搬家"凶手
|
||||
*/
|
||||
public final class GeomProbe8 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static ClassLoader PAYLOAD;
|
||||
static Class<?> HQL, ARR, ACTOR_CLASS, GROUP_CLASS, WIDGETGROUP_CLASS;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[geom8] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
java.util.LinkedHashSet<ClassLoader> loaders = new java.util.LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) loaders.add(c.getClassLoader());
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) STATE = v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("LAST_STATE 拿不到"); return; }
|
||||
// 可能有多个 URLClassLoader —— 逐个试,谁能加载 hq 谁就是载荷加载器
|
||||
for (ClassLoader cl : loaders) {
|
||||
try { HQL = Class.forName("hq", true, cl); PAYLOAD = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
log("载荷加载器 = " + PAYLOAD);
|
||||
if (HQL == null) { log("找不到能加载 hq 的加载器"); return; }
|
||||
HQL = Class.forName("hq", true, PAYLOAD);
|
||||
ARR = Class.forName("fI", true, PAYLOAD);
|
||||
ACTOR_CLASS = Class.forName("dl", true, PAYLOAD);
|
||||
GROUP_CLASS = Class.forName("do", true, PAYLOAD);
|
||||
WIDGETGROUP_CLASS = Class.forName("eX", true, PAYLOAD);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
Method onRender = null;
|
||||
for (Method m : og.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object pane = state("listScroll");
|
||||
Object table = state("listTable");
|
||||
log("========== 1) 面板根表格现在挂了哪些子控件 ==========");
|
||||
Object kids = groupChildren(root);
|
||||
int n = size(kids);
|
||||
log("子控件数 = " + n + "(正常应该是 3:按钮块 / ScrollPane / 日志 Label)");
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object c = get(kids, i);
|
||||
log(" [" + i + "] " + c.getClass().getName() + " " + geom(c)
|
||||
+ " 是 listScroll 吗=" + (c == pane) + " 是 listTable 吗=" + (c == table));
|
||||
}
|
||||
log("========== 2) ScrollPane 现在挂在谁下面 ==========");
|
||||
Object p = pane;
|
||||
float ax = 0f, ay = 0f;
|
||||
for (int i = 0; i < 6 && p != null; i++) {
|
||||
log(" 层[" + i + "] " + p.getClass().getName() + " " + geom(p));
|
||||
ax += fl(p, "c_");
|
||||
ay += fl(p, "d_");
|
||||
Object np = parentOf(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
}
|
||||
log(" ScrollPane 的绝对坐标 ≈ (" + r(ax) + "," + r(ay) + ") ← 面板在 x≈2052 的位置");
|
||||
log("========== 3) Stage 上那些 1 个 Actor 参数 + void 的方法 ==========");
|
||||
Object stage = null;
|
||||
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, PAYLOAD);
|
||||
for (String name : new String[] {"uk", "dt"}) {
|
||||
try {
|
||||
stage = sceneObjects.getMethod("global", ClassLoader.class, String.class)
|
||||
.invoke(null, PAYLOAD, name);
|
||||
} catch (Throwable ignored) { }
|
||||
if (stage != null) { log(" (用 " + name + " 当 Stage)"); break; }
|
||||
}
|
||||
if (stage == null) { log(" 没拿到 Stage"); return; }
|
||||
String[] skip = {"dl", "do", "eX"};
|
||||
for (Method m : stage.getClass().getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1) continue;
|
||||
if (!t[0].isAssignableFrom(ACTOR_CLASS)) continue;
|
||||
if (m.getReturnType() != void.class) continue;
|
||||
String decl = m.getDeclaringClass().getName();
|
||||
boolean skipped = false;
|
||||
for (String s : skip) if (decl.equals(s)) skipped = true;
|
||||
log(" " + (skipped ? "(已排除)" : "★会被调用★") + " " + decl + "." + m.getName()
|
||||
+ "(" + t[0].getName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static Object groupChildren(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parentOf(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != GROUP_CLASS) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I")) + "]";
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = ARR; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import java.io.FileWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 抽查验证用:直接调用编辑器自己的 JSON 导出器(载荷类 sB),做"字段 -> key"的差分实验。
|
||||
*
|
||||
* 步骤(全程在编辑器渲染线程里跑):
|
||||
* 1. 导出一次 -> /tmp/spinecheck-before.json
|
||||
* 2. 改若干个字段(骨骼 length、插槽 color/dark、图片附件 color)
|
||||
* 3. 再导出 -> /tmp/spinecheck-after.json
|
||||
* 4. 把字段改回原值,再导出 -> /tmp/spinecheck-restored.json
|
||||
* 之后对比三份 JSON:对应 key 的值如果跟着字段变、又跟着改回来,就说明"字段 ↔ key"是对的。
|
||||
*
|
||||
* 不改任何长期状态(值都还原)。
|
||||
*/
|
||||
public final class JsonDiff2 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Class<?> ARRAY, NODE, GP, GPC, GU, GN, GO, GY, GB, COLOR;
|
||||
static Object SKELETON;
|
||||
static Object BONES, SLOTS;
|
||||
static Object BONE, SLOT, SLOT_ATTACH;
|
||||
static Method SLOT_CURRENT;
|
||||
static Field F_BONE_LENGTH, F_SLOT_COLOR, F_SLOT_DARK, F_SLOT_HASDARK, F_GO_COLOR;
|
||||
static Object OLD_LEN, OLD_SLOT_COLOR, OLD_GO_COLOR, OLD_DARK;
|
||||
static boolean OLD_HASDARK;
|
||||
static Field GY_SETTINGS;
|
||||
static Object WRITER;
|
||||
static StringWriter SW;
|
||||
static Object SB_INSTANCE;
|
||||
static Method EXPORT;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[jsondiff2] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
// 谁**真的能加载载荷类 GP**,谁才是编辑器类加载器(重启后第一个 URLClassLoader 未必是它)
|
||||
Set<ClassLoader> candidates = new LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
ClassLoader cl = c.getClassLoader();
|
||||
if (cl instanceof URLClassLoader) candidates.add(cl);
|
||||
}
|
||||
for (ClassLoader cl : candidates) {
|
||||
try { Class.forName("GP", false, cl); L = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
if (L == null) { log("找不到能加载 GP 的编辑器类加载器,候选 " + candidates.size() + " 个"); return; }
|
||||
log("编辑器类加载器 = " + L);
|
||||
ARRAY = Class.forName("fI", true, L);
|
||||
NODE = Class.forName("eM", true, L);
|
||||
GP = Class.forName("GP", true, L);
|
||||
GPC = Class.forName("Gp", true, L);
|
||||
GU = Class.forName("GU", true, L);
|
||||
GN = Class.forName("Gn", true, L);
|
||||
GO = Class.forName("GO", true, L);
|
||||
GY = Class.forName("gy", true, L);
|
||||
GB = Class.forName("gB", true, L);
|
||||
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||
Class<?> OG = Class.forName("OG", true, L);
|
||||
Class<?> SB = Class.forName("sB", true, L);
|
||||
|
||||
SKELETON = firstOfType(rootNodes(globalStatic(OG, "Uq")), GP);
|
||||
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||
BONES = arrFieldOfType(GP, GPC);
|
||||
SLOTS = arrFieldOfType(GP, GU);
|
||||
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||
log("骨架 = " + nameOf(SKELETON) + ",骨骼 " + size(BONES) + ",插槽 " + size(SLOTS));
|
||||
|
||||
BONE = byName(BONES, "bone");
|
||||
if (BONE == null && size(BONES) > 1) BONE = get(BONES, 1);
|
||||
SLOT = byName(SLOTS, "mega_dg4");
|
||||
if (SLOT == null && size(SLOTS) > 0) SLOT = get(SLOTS, 0);
|
||||
SLOT_ATTACH = SLOT_CURRENT.invoke(SLOT);
|
||||
log("样本骨骼 = " + nameOf(BONE) + ",样本插槽 = " + nameOf(SLOT)
|
||||
+ ",该插槽附件 = " + (SLOT_ATTACH == null ? "null" : SLOT_ATTACH.getClass().getName()));
|
||||
|
||||
F_BONE_LENGTH = floatField(GPC, "C");
|
||||
F_SLOT_COLOR = typedField(GU, "B", COLOR);
|
||||
F_SLOT_DARK = typedField(GU, "I", COLOR);
|
||||
F_SLOT_HASDARK = typedField(GU, "I", boolean.class);
|
||||
F_GO_COLOR = SLOT_ATTACH == null ? null : typedField(GO, "D", COLOR);
|
||||
|
||||
// 导出器:sB.D(GP, g, gB, Z, Z, Z, StringBuilder)
|
||||
SB_INSTANCE = SB.getConstructor().newInstance();
|
||||
EXPORT = SB.getMethod("D", GP, Class.forName("g", true, L), GB, boolean.class, boolean.class,
|
||||
boolean.class, StringBuilder.class);
|
||||
EXPORT.setAccessible(true);
|
||||
GY_SETTINGS = GY.getField("D");
|
||||
|
||||
Method onRender = null;
|
||||
for (Method m : OG.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
String before = export("before");
|
||||
if (before == null || before.isEmpty()) { log("导出失败,实验终止"); return; }
|
||||
log("before.json 长度 = " + before.length());
|
||||
|
||||
// 1) 骨骼 length(Gp.C:F)
|
||||
OLD_LEN = F_BONE_LENGTH.get(BONE);
|
||||
F_BONE_LENGTH.set(BONE, 123.456f);
|
||||
// 2) 插槽 color(GU.B:Color)→ 用可辨识的紫
|
||||
OLD_SLOT_COLOR = F_SLOT_COLOR.get(SLOT);
|
||||
F_SLOT_COLOR.set(SLOT, newColor(0.25f, 0.5f, 0.75f, 1f));
|
||||
// 3) 插槽 dark(GU.I:Color + GU.I:boolean)
|
||||
if (F_SLOT_DARK != null) { OLD_DARK = F_SLOT_DARK.get(SLOT); F_SLOT_DARK.set(SLOT, newColor(1f, 0.2f, 0.1f, 1f)); }
|
||||
if (F_SLOT_HASDARK != null) { OLD_HASDARK = F_SLOT_HASDARK.getBoolean(SLOT); F_SLOT_HASDARK.setBoolean(SLOT, true); }
|
||||
// 4) 图片附件 color(GO.D:Color)
|
||||
if (F_GO_COLOR != null) { OLD_GO_COLOR = F_GO_COLOR.get(SLOT_ATTACH); F_GO_COLOR.set(SLOT_ATTACH, newColor(0.1f, 0.9f, 0.3f, 0.8f)); }
|
||||
log("已改:bone." + nameOf(BONE) + ".length=123.456,slot." + nameOf(SLOT) + ".color/dark,附件.color");
|
||||
export("after");
|
||||
|
||||
// 还原
|
||||
F_BONE_LENGTH.set(BONE, OLD_LEN);
|
||||
F_SLOT_COLOR.set(SLOT, OLD_SLOT_COLOR);
|
||||
if (F_SLOT_DARK != null && OLD_DARK != null) F_SLOT_DARK.set(SLOT, OLD_DARK);
|
||||
if (F_SLOT_HASDARK != null) F_SLOT_HASDARK.setBoolean(SLOT, OLD_HASDARK);
|
||||
if (F_GO_COLOR != null && OLD_GO_COLOR != null) F_GO_COLOR.set(SLOT_ATTACH, OLD_GO_COLOR);
|
||||
log("已还原原值");
|
||||
export("restored");
|
||||
}
|
||||
|
||||
static Object newColor(float r, float g, float b, float a) throws Exception {
|
||||
return COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(r, g, b, a);
|
||||
}
|
||||
|
||||
static String export(String tag) throws Exception {
|
||||
SW = new StringWriter();
|
||||
WRITER = GY.getConstructor(java.io.Writer.class).newInstance(SW);
|
||||
GY.getMethod("D").invoke(WRITER); // 初始化
|
||||
Object settings = null;
|
||||
Object[] values = (Object[]) GB.getMethod("values").invoke(null);
|
||||
for (int attempt = -1; attempt < values.length; attempt++) {
|
||||
settings = attempt < 0 ? null : values[attempt];
|
||||
GY_SETTINGS.set(WRITER, settings);
|
||||
try {
|
||||
EXPORT.invoke(SB_INSTANCE, SKELETON, null, settings, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE,
|
||||
new StringBuilder());
|
||||
if (SW.getBuffer().length() > 0) {
|
||||
log("导出(" + tag + ") 成功,settings=" + settings);
|
||||
write(tag, SW.toString());
|
||||
return SW.toString();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
if (attempt + 1 >= values.length) log("导出(" + tag + ") 各种 settings 都失败,最后一次: " + cause);
|
||||
}
|
||||
}
|
||||
return SW.getBuffer().toString();
|
||||
}
|
||||
|
||||
static void write(String tag, String text) throws Exception {
|
||||
FileWriter fw = new FileWriter("/tmp/spinecheck-" + tag + ".json");
|
||||
try { fw.write(text); } finally { fw.close(); }
|
||||
}
|
||||
|
||||
static Object byName(Object array, String name) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null && name.equals(nameOf(o))) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field floatField(Class<?> owner, String name) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == float.class) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field typedField(Class<?> owner, String name, Class<?> type) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == type) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrFieldOfType(Class<?> owner, Class<?> elem) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(SKELETON);
|
||||
if (v != null && size(v) > 0 && elem.isAssignableFrom(elemClass(v))) return v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object rootNodes(Object tree) {
|
||||
if (tree == null) return null;
|
||||
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != ARRAY) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(tree);
|
||||
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object firstOfType(Object nodes, Class<?> type) {
|
||||
if (nodes == null) return null;
|
||||
for (int i = 0; i < size(nodes); i++) {
|
||||
Object node = get(nodes, i);
|
||||
Object obj = field(node, NODE, Object.class);
|
||||
if (obj != null && type.isInstance(obj)) return obj;
|
||||
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||
if (target == null) return null;
|
||||
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(target); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object globalStatic(Class<?> holder, String typeName) {
|
||||
for (Class<?> t = holder; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (!f.getType().getName().equals(typeName)) continue;
|
||||
f.setAccessible(true);
|
||||
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String nameOf(Object o) {
|
||||
if (o == null) return "null";
|
||||
try {
|
||||
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||
} catch (Throwable t) { return "?"; }
|
||||
}
|
||||
|
||||
static int size(Object a) {
|
||||
if (a == null) return 0;
|
||||
try {
|
||||
for (Field f : ARRAY.getDeclaredFields()) {
|
||||
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object a, int i) {
|
||||
if (a == null) return null;
|
||||
try {
|
||||
for (Field f : ARRAY.getDeclaredFields()) {
|
||||
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Class<?> elemClass(Object array) {
|
||||
for (int i = 0; i < size(array); i++) { Object o = get(array, i); if (o != null) return o.getClass(); }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Method m : t.getDeclaredMethods()) {
|
||||
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||
if (ret != null && m.getReturnType() != ret) continue;
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import java.io.FileWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 抽查验证用:直接调用编辑器自己的 JSON 导出器(载荷类 sB),做"字段 -> key"的差分实验。
|
||||
*
|
||||
* 步骤(全程在编辑器渲染线程里跑):
|
||||
* 1. 导出一次 -> /tmp/spinecheck-before.json
|
||||
* 2. 改若干个字段(骨骼 length、插槽 color/dark、图片附件 color)
|
||||
* 3. 再导出 -> /tmp/spinecheck-after.json
|
||||
* 4. 把字段改回原值,再导出 -> /tmp/spinecheck-restored.json
|
||||
* 之后对比三份 JSON:对应 key 的值如果跟着字段变、又跟着改回来,就说明"字段 ↔ key"是对的。
|
||||
*
|
||||
* 不改任何长期状态(值都还原)。
|
||||
*/
|
||||
public final class JsonDiff3 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Class<?> ARRAY, NODE, GP, GPC, GU, GN, GO, GY, GB, COLOR;
|
||||
static Object SKELETON;
|
||||
static Object BONES, SLOTS;
|
||||
static Object BONE, SLOT, SLOT_ATTACH;
|
||||
static Method SLOT_CURRENT;
|
||||
static Field F_BONE_LENGTH, F_SLOT_COLOR, F_SLOT_DARK, F_SLOT_HASDARK, F_GO_COLOR;
|
||||
static Object OLD_LEN, OLD_SLOT_COLOR, OLD_GO_COLOR, OLD_DARK;
|
||||
static boolean OLD_HASDARK;
|
||||
static Field GY_SETTINGS;
|
||||
static Object WRITER;
|
||||
static StringWriter SW;
|
||||
static Object SB_INSTANCE;
|
||||
static Method EXPORT;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[jsondiff3] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
// 谁**真的能加载载荷类 GP**,谁才是编辑器类加载器(重启后第一个 URLClassLoader 未必是它)
|
||||
Set<ClassLoader> candidates = new LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
ClassLoader cl = c.getClassLoader();
|
||||
if (cl instanceof URLClassLoader) candidates.add(cl);
|
||||
}
|
||||
for (ClassLoader cl : candidates) {
|
||||
try { Class.forName("GP", false, cl); L = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
if (L == null) { log("找不到能加载 GP 的编辑器类加载器,候选 " + candidates.size() + " 个"); return; }
|
||||
log("编辑器类加载器 = " + L);
|
||||
ARRAY = Class.forName("fI", true, L);
|
||||
NODE = Class.forName("eM", true, L);
|
||||
GP = Class.forName("GP", true, L);
|
||||
GPC = Class.forName("Gp", true, L);
|
||||
GU = Class.forName("GU", true, L);
|
||||
GN = Class.forName("Gn", true, L);
|
||||
GO = Class.forName("GO", true, L);
|
||||
GY = Class.forName("gy", true, L);
|
||||
GB = Class.forName("gB", true, L);
|
||||
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||
Class<?> OG = Class.forName("OG", true, L);
|
||||
Class<?> SB = Class.forName("sB", true, L);
|
||||
|
||||
SKELETON = firstOfType(rootNodes(globalStatic(OG, "Uq")), GP);
|
||||
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||
BONES = arrFieldOfType(GP, GPC);
|
||||
SLOTS = arrFieldOfType(GP, GU);
|
||||
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||
log("骨架 = " + nameOf(SKELETON) + ",骨骼 " + size(BONES) + ",插槽 " + size(SLOTS));
|
||||
|
||||
BONE = byName(BONES, "bone");
|
||||
if (BONE == null && size(BONES) > 1) BONE = get(BONES, 1);
|
||||
SLOT = byName(SLOTS, "mega_dg4");
|
||||
if (SLOT == null && size(SLOTS) > 0) SLOT = get(SLOTS, 0);
|
||||
SLOT_ATTACH = SLOT_CURRENT.invoke(SLOT);
|
||||
log("样本骨骼 = " + nameOf(BONE) + ",样本插槽 = " + nameOf(SLOT)
|
||||
+ ",该插槽附件 = " + (SLOT_ATTACH == null ? "null" : SLOT_ATTACH.getClass().getName()));
|
||||
|
||||
F_BONE_LENGTH = floatField(GPC, "C");
|
||||
F_SLOT_COLOR = typedField(GU, "B", COLOR);
|
||||
F_SLOT_DARK = typedField(GU, "I", COLOR);
|
||||
F_SLOT_HASDARK = typedField(GU, "I", boolean.class);
|
||||
F_GO_COLOR = SLOT_ATTACH == null ? null : typedField(GO, "D", COLOR);
|
||||
|
||||
// 导出器:sB.D(GP, g, gB, Z, Z, Z, StringBuilder)
|
||||
SB_INSTANCE = SB.getConstructor().newInstance();
|
||||
EXPORT = SB.getMethod("D", GP, Class.forName("g", true, L), GB, boolean.class, boolean.class,
|
||||
boolean.class, StringBuilder.class);
|
||||
EXPORT.setAccessible(true);
|
||||
GY_SETTINGS = GY.getField("D");
|
||||
|
||||
Method onRender = null;
|
||||
for (Method m : OG.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
String before = export("before");
|
||||
if (before == null || before.isEmpty()) { log("导出失败,实验终止"); return; }
|
||||
log("before.json 长度 = " + before.length());
|
||||
|
||||
// 1) 骨骼 length(Gp.C:F)
|
||||
OLD_LEN = F_BONE_LENGTH.get(BONE);
|
||||
F_BONE_LENGTH.set(BONE, 123.456f);
|
||||
// 2) 插槽 color(GU.B:Color)→ 用可辨识的紫
|
||||
OLD_SLOT_COLOR = F_SLOT_COLOR.get(SLOT);
|
||||
F_SLOT_COLOR.set(SLOT, newColor(0.25f, 0.5f, 0.75f, 1f));
|
||||
// 3) 插槽 dark(GU.I:Color + GU.I:boolean)
|
||||
if (F_SLOT_DARK != null) { OLD_DARK = F_SLOT_DARK.get(SLOT); F_SLOT_DARK.set(SLOT, newColor(1f, 0.2f, 0.1f, 1f)); }
|
||||
if (F_SLOT_HASDARK != null) { OLD_HASDARK = F_SLOT_HASDARK.getBoolean(SLOT); F_SLOT_HASDARK.setBoolean(SLOT, true); }
|
||||
// 4) 图片附件 color(GO.D:Color)
|
||||
if (F_GO_COLOR != null) { OLD_GO_COLOR = F_GO_COLOR.get(SLOT_ATTACH); F_GO_COLOR.set(SLOT_ATTACH, newColor(0.1f, 0.9f, 0.3f, 0.8f)); }
|
||||
log("已改:bone." + nameOf(BONE) + ".length=123.456,slot." + nameOf(SLOT) + ".color/dark,附件.color");
|
||||
export("after");
|
||||
|
||||
// 还原
|
||||
F_BONE_LENGTH.set(BONE, OLD_LEN);
|
||||
F_SLOT_COLOR.set(SLOT, OLD_SLOT_COLOR);
|
||||
if (F_SLOT_DARK != null && OLD_DARK != null) F_SLOT_DARK.set(SLOT, OLD_DARK);
|
||||
if (F_SLOT_HASDARK != null) F_SLOT_HASDARK.setBoolean(SLOT, OLD_HASDARK);
|
||||
if (F_GO_COLOR != null && OLD_GO_COLOR != null) F_GO_COLOR.set(SLOT_ATTACH, OLD_GO_COLOR);
|
||||
log("已还原原值");
|
||||
export("restored");
|
||||
}
|
||||
|
||||
static Object newColor(float r, float g, float b, float a) throws Exception {
|
||||
return COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(r, g, b, a);
|
||||
}
|
||||
|
||||
static String export(String tag) throws Exception {
|
||||
SW = new StringWriter();
|
||||
WRITER = GY.getConstructor(java.io.Writer.class).newInstance(SW);
|
||||
GY.getMethod("D").invoke(WRITER); // 初始化
|
||||
Object settings = null;
|
||||
Object[] values = (Object[]) GB.getMethod("values").invoke(null);
|
||||
for (int attempt = -1; attempt < values.length; attempt++) {
|
||||
settings = attempt < 0 ? null : values[attempt];
|
||||
GY_SETTINGS.set(WRITER, settings);
|
||||
try {
|
||||
EXPORT.invoke(SB_INSTANCE, SKELETON, null, settings, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE,
|
||||
new StringBuilder());
|
||||
if (SW.getBuffer().length() > 0) {
|
||||
log("导出(" + tag + ") 成功,settings=" + settings);
|
||||
write(tag, SW.toString());
|
||||
return SW.toString();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
log("导出(" + tag + ") settings=" + settings + " 失败: " + cause);
|
||||
if (attempt < 0) cause.printStackTrace(System.err); // 第一发就打印完整栈,看清到底谁 null
|
||||
}
|
||||
}
|
||||
return SW.getBuffer().toString();
|
||||
}
|
||||
|
||||
static void write(String tag, String text) throws Exception {
|
||||
FileWriter fw = new FileWriter("/tmp/spinecheck-" + tag + ".json");
|
||||
try { fw.write(text); } finally { fw.close(); }
|
||||
}
|
||||
|
||||
static Object byName(Object array, String name) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null && name.equals(nameOf(o))) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field floatField(Class<?> owner, String name) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == float.class) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field typedField(Class<?> owner, String name, Class<?> type) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == type) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrFieldOfType(Class<?> owner, Class<?> elem) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(SKELETON);
|
||||
if (v != null && size(v) > 0 && elem.isAssignableFrom(elemClass(v))) return v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object rootNodes(Object tree) {
|
||||
if (tree == null) return null;
|
||||
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != ARRAY) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(tree);
|
||||
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object firstOfType(Object nodes, Class<?> type) {
|
||||
if (nodes == null) return null;
|
||||
for (int i = 0; i < size(nodes); i++) {
|
||||
Object node = get(nodes, i);
|
||||
Object obj = field(node, NODE, Object.class);
|
||||
if (obj != null && type.isInstance(obj)) return obj;
|
||||
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||
if (target == null) return null;
|
||||
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(target); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object globalStatic(Class<?> holder, String typeName) {
|
||||
for (Class<?> t = holder; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (!f.getType().getName().equals(typeName)) continue;
|
||||
f.setAccessible(true);
|
||||
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String nameOf(Object o) {
|
||||
if (o == null) return "null";
|
||||
try {
|
||||
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||
} catch (Throwable t) { return "?"; }
|
||||
}
|
||||
|
||||
static int size(Object a) {
|
||||
if (a == null) return 0;
|
||||
try {
|
||||
for (Field f : ARRAY.getDeclaredFields()) {
|
||||
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object a, int i) {
|
||||
if (a == null) return null;
|
||||
try {
|
||||
for (Field f : ARRAY.getDeclaredFields()) {
|
||||
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Class<?> elemClass(Object array) {
|
||||
for (int i = 0; i < size(array); i++) { Object o = get(array, i); if (o != null) return o.getClass(); }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Method m : t.getDeclaredMethods()) {
|
||||
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||
if (ret != null && m.getReturnType() != ret) continue;
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import java.io.FileWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 抽查验证用:直接调用编辑器自己的 JSON 导出器(载荷类 sB),做"字段 -> key"的差分实验。
|
||||
*
|
||||
* 步骤(全程在编辑器渲染线程里跑):
|
||||
* 1. 导出一次 -> /tmp/spinecheck-before.json
|
||||
* 2. 改若干个字段(骨骼 length、插槽 color/dark、图片附件 color)
|
||||
* 3. 再导出 -> /tmp/spinecheck-after.json
|
||||
* 4. 把字段改回原值,再导出 -> /tmp/spinecheck-restored.json
|
||||
* 之后对比三份 JSON:对应 key 的值如果跟着字段变、又跟着改回来,就说明"字段 ↔ key"是对的。
|
||||
*
|
||||
* 不改任何长期状态(值都还原)。
|
||||
*/
|
||||
public final class JsonDiff4 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Class<?> ARRAY, NODE, GP, GPC, GU, GN, GO, GY, GB, COLOR;
|
||||
static Object SKELETON;
|
||||
static Object BONES, SLOTS;
|
||||
static Object BONE, SLOT, SLOT_ATTACH;
|
||||
static Method SLOT_CURRENT;
|
||||
static Field F_BONE_LENGTH, F_SLOT_COLOR, F_SLOT_DARK, F_SLOT_HASDARK, F_GO_COLOR;
|
||||
static Object OLD_LEN, OLD_SLOT_COLOR, OLD_GO_COLOR, OLD_DARK;
|
||||
static boolean OLD_HASDARK;
|
||||
static Field GY_SETTINGS;
|
||||
static Object WRITER;
|
||||
static StringWriter SW;
|
||||
static Object SB_INSTANCE;
|
||||
static Method EXPORT;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[jsondiff4] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
// 谁**真的能加载载荷类 GP**,谁才是编辑器类加载器(重启后第一个 URLClassLoader 未必是它)
|
||||
Set<ClassLoader> candidates = new LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
ClassLoader cl = c.getClassLoader();
|
||||
if (cl instanceof URLClassLoader) candidates.add(cl);
|
||||
}
|
||||
for (ClassLoader cl : candidates) {
|
||||
try { Class.forName("GP", false, cl); L = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
if (L == null) { log("找不到能加载 GP 的编辑器类加载器,候选 " + candidates.size() + " 个"); return; }
|
||||
log("编辑器类加载器 = " + L);
|
||||
ARRAY = Class.forName("fI", true, L);
|
||||
NODE = Class.forName("eM", true, L);
|
||||
GP = Class.forName("GP", true, L);
|
||||
GPC = Class.forName("Gp", true, L);
|
||||
GU = Class.forName("GU", true, L);
|
||||
GN = Class.forName("Gn", true, L);
|
||||
GO = Class.forName("GO", true, L);
|
||||
GY = Class.forName("gy", true, L);
|
||||
GB = Class.forName("gB", true, L);
|
||||
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||
Class<?> OG = Class.forName("OG", true, L);
|
||||
Class<?> SB = Class.forName("sB", true, L);
|
||||
|
||||
SKELETON = firstOfType(rootNodes(globalStatic(OG, "Uq")), GP);
|
||||
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||
BONES = arrFieldOfType(GP, GPC);
|
||||
SLOTS = arrFieldOfType(GP, GU);
|
||||
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||
log("骨架 = " + nameOf(SKELETON) + ",骨骼 " + size(BONES) + ",插槽 " + size(SLOTS));
|
||||
|
||||
BONE = byName(BONES, "bone");
|
||||
if (BONE == null && size(BONES) > 1) BONE = get(BONES, 1);
|
||||
SLOT = byName(SLOTS, "mega_dg4");
|
||||
if (SLOT == null && size(SLOTS) > 0) SLOT = get(SLOTS, 0);
|
||||
SLOT_ATTACH = SLOT_CURRENT.invoke(SLOT);
|
||||
log("样本骨骼 = " + nameOf(BONE) + ",样本插槽 = " + nameOf(SLOT)
|
||||
+ ",该插槽附件 = " + (SLOT_ATTACH == null ? "null" : SLOT_ATTACH.getClass().getName()));
|
||||
|
||||
F_BONE_LENGTH = floatField(GPC, "C");
|
||||
F_SLOT_COLOR = typedField(GU, "B", COLOR);
|
||||
F_SLOT_DARK = typedField(GU, "I", COLOR);
|
||||
F_SLOT_HASDARK = typedField(GU, "I", boolean.class);
|
||||
F_GO_COLOR = SLOT_ATTACH == null ? null : typedField(GO, "D", COLOR);
|
||||
|
||||
// 导出器:sB.D(GP, g, gB, Z, Z, Z, StringBuilder)
|
||||
SB_INSTANCE = SB.getConstructor().newInstance();
|
||||
EXPORT = SB.getMethod("D", GP, Class.forName("g", true, L), GB, boolean.class, boolean.class,
|
||||
boolean.class, StringBuilder.class);
|
||||
EXPORT.setAccessible(true);
|
||||
GY_SETTINGS = GY.getField("D");
|
||||
|
||||
Method onRender = null;
|
||||
for (Method m : OG.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||
}
|
||||
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
String before = export("before");
|
||||
if (before == null || before.isEmpty()) { log("导出失败,实验终止"); return; }
|
||||
log("before.json 长度 = " + before.length());
|
||||
|
||||
// 1) 骨骼 length(Gp.C:F)
|
||||
OLD_LEN = F_BONE_LENGTH.get(BONE);
|
||||
F_BONE_LENGTH.set(BONE, 123.456f);
|
||||
// 2) 插槽 color(GU.B:Color)→ 用可辨识的紫
|
||||
OLD_SLOT_COLOR = F_SLOT_COLOR.get(SLOT);
|
||||
F_SLOT_COLOR.set(SLOT, newColor(0.25f, 0.5f, 0.75f, 1f));
|
||||
// 3) 插槽 dark(GU.I:Color + GU.I:boolean)
|
||||
if (F_SLOT_DARK != null) { OLD_DARK = F_SLOT_DARK.get(SLOT); F_SLOT_DARK.set(SLOT, newColor(1f, 0.2f, 0.1f, 1f)); }
|
||||
if (F_SLOT_HASDARK != null) { OLD_HASDARK = F_SLOT_HASDARK.getBoolean(SLOT); F_SLOT_HASDARK.setBoolean(SLOT, true); }
|
||||
// 4) 图片附件 color(GO.D:Color)
|
||||
if (F_GO_COLOR != null) { OLD_GO_COLOR = F_GO_COLOR.get(SLOT_ATTACH); F_GO_COLOR.set(SLOT_ATTACH, newColor(0.1f, 0.9f, 0.3f, 0.8f)); }
|
||||
log("已改:bone." + nameOf(BONE) + ".length=123.456,slot." + nameOf(SLOT) + ".color/dark,附件.color");
|
||||
export("after");
|
||||
|
||||
// 还原
|
||||
F_BONE_LENGTH.set(BONE, OLD_LEN);
|
||||
F_SLOT_COLOR.set(SLOT, OLD_SLOT_COLOR);
|
||||
if (F_SLOT_DARK != null && OLD_DARK != null) F_SLOT_DARK.set(SLOT, OLD_DARK);
|
||||
if (F_SLOT_HASDARK != null) F_SLOT_HASDARK.setBoolean(SLOT, OLD_HASDARK);
|
||||
if (F_GO_COLOR != null && OLD_GO_COLOR != null) F_GO_COLOR.set(SLOT_ATTACH, OLD_GO_COLOR);
|
||||
log("已还原原值");
|
||||
export("restored");
|
||||
}
|
||||
|
||||
static Object newColor(float r, float g, float b, float a) throws Exception {
|
||||
return COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(r, g, b, a);
|
||||
}
|
||||
|
||||
static String export(String tag) throws Exception {
|
||||
Object[] values = (Object[]) GB.getMethod("values").invoke(null);
|
||||
Object settings = null;
|
||||
for (int attempt = -1; attempt < values.length; attempt++) {
|
||||
settings = attempt < 0 ? null : values[attempt];
|
||||
SW = new StringWriter();
|
||||
WRITER = GY.getConstructor(java.io.Writer.class).newInstance(SW);
|
||||
GY_SETTINGS.set(WRITER, settings); // 先设 settings(照 sB 的字节码顺序)
|
||||
GY.getMethod("B").invoke(WRITER); // 再开根对象
|
||||
try {
|
||||
EXPORT.invoke(SB_INSTANCE, SKELETON, null, settings, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE,
|
||||
new StringBuilder());
|
||||
if (SW.getBuffer().length() > 0) {
|
||||
log("导出(" + tag + ") 成功,settings=" + settings);
|
||||
write(tag, SW.toString());
|
||||
return SW.toString();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
log("导出(" + tag + ") settings=" + settings + " 失败: " + cause);
|
||||
if (attempt < 0) cause.printStackTrace(System.err); // 第一发就打印完整栈,看清到底谁 null
|
||||
}
|
||||
}
|
||||
return SW.getBuffer().toString();
|
||||
}
|
||||
|
||||
static void write(String tag, String text) throws Exception {
|
||||
FileWriter fw = new FileWriter("/tmp/spinecheck-" + tag + ".json");
|
||||
try { fw.write(text); } finally { fw.close(); }
|
||||
}
|
||||
|
||||
static Object byName(Object array, String name) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null && name.equals(nameOf(o))) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field floatField(Class<?> owner, String name) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == float.class) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field typedField(Class<?> owner, String name, Class<?> type) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == type) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrFieldOfType(Class<?> owner, Class<?> elem) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(SKELETON);
|
||||
if (v != null && size(v) > 0 && elem.isAssignableFrom(elemClass(v))) return v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object rootNodes(Object tree) {
|
||||
if (tree == null) return null;
|
||||
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != ARRAY) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(tree);
|
||||
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object firstOfType(Object nodes, Class<?> type) {
|
||||
if (nodes == null) return null;
|
||||
for (int i = 0; i < size(nodes); i++) {
|
||||
Object node = get(nodes, i);
|
||||
Object obj = field(node, NODE, Object.class);
|
||||
if (obj != null && type.isInstance(obj)) return obj;
|
||||
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||
if (target == null) return null;
|
||||
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(target); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object globalStatic(Class<?> holder, String typeName) {
|
||||
for (Class<?> t = holder; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (!f.getType().getName().equals(typeName)) continue;
|
||||
f.setAccessible(true);
|
||||
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String nameOf(Object o) {
|
||||
if (o == null) return "null";
|
||||
try {
|
||||
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||
} catch (Throwable t) { return "?"; }
|
||||
}
|
||||
|
||||
static int size(Object a) {
|
||||
if (a == null) return 0;
|
||||
try {
|
||||
for (Field f : ARRAY.getDeclaredFields()) {
|
||||
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object a, int i) {
|
||||
if (a == null) return null;
|
||||
try {
|
||||
for (Field f : ARRAY.getDeclaredFields()) {
|
||||
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Class<?> elemClass(Object array) {
|
||||
for (int i = 0; i < size(array); i++) { Object o = get(array, i); if (o != null) return o.getClass(); }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Method m : t.getDeclaredMethods()) {
|
||||
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||
if (ret != null && m.getReturnType() != ret) continue;
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user