diff --git a/tools/live/manifest.txt b/tools/live/manifest.txt new file mode 100644 index 0000000..bb27b10 --- /dev/null +++ b/tools/live/manifest.txt @@ -0,0 +1,2 @@ +Agent-Class: BoneMaker +Can-Retransform-Classes: false diff --git a/tools/live/manifest2.txt b/tools/live/manifest2.txt new file mode 100644 index 0000000..da700cb --- /dev/null +++ b/tools/live/manifest2.txt @@ -0,0 +1 @@ +Agent-Class: BoneMaker2 diff --git a/tools/live/manifest3.txt b/tools/live/manifest3.txt new file mode 100644 index 0000000..08dc636 --- /dev/null +++ b/tools/live/manifest3.txt @@ -0,0 +1 @@ +Agent-Class: BoneMaker3 diff --git a/tools/live/manifest4.txt b/tools/live/manifest4.txt new file mode 100644 index 0000000..fc846a6 --- /dev/null +++ b/tools/live/manifest4.txt @@ -0,0 +1 @@ +Agent-Class: BoneMaker4 diff --git a/tools/live/manifest5.txt b/tools/live/manifest5.txt new file mode 100644 index 0000000..59efa79 --- /dev/null +++ b/tools/live/manifest5.txt @@ -0,0 +1 @@ +Agent-Class: BoneMaker5 diff --git a/tools/live/manifest6.txt b/tools/live/manifest6.txt new file mode 100644 index 0000000..2af521c --- /dev/null +++ b/tools/live/manifest6.txt @@ -0,0 +1 @@ +Agent-Class: BoneMaker6 diff --git a/tools/live/manifest7.txt b/tools/live/manifest7.txt new file mode 100644 index 0000000..d186a3a --- /dev/null +++ b/tools/live/manifest7.txt @@ -0,0 +1 @@ +Agent-Class: BoneMaker7 diff --git a/tools/live/manifestG1.txt b/tools/live/manifestG1.txt new file mode 100644 index 0000000..38c197e --- /dev/null +++ b/tools/live/manifestG1.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe1 diff --git a/tools/live/manifestG2.txt b/tools/live/manifestG2.txt new file mode 100644 index 0000000..f59b43f --- /dev/null +++ b/tools/live/manifestG2.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe2 diff --git a/tools/live/manifestG3.txt b/tools/live/manifestG3.txt new file mode 100644 index 0000000..3fbbbac --- /dev/null +++ b/tools/live/manifestG3.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe3 diff --git a/tools/live/manifestG4.txt b/tools/live/manifestG4.txt new file mode 100644 index 0000000..93fbcae --- /dev/null +++ b/tools/live/manifestG4.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe4 diff --git a/tools/live/manifestG5.txt b/tools/live/manifestG5.txt new file mode 100644 index 0000000..f55515c --- /dev/null +++ b/tools/live/manifestG5.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe5 diff --git a/tools/live/manifestG6.txt b/tools/live/manifestG6.txt new file mode 100644 index 0000000..40c5b70 --- /dev/null +++ b/tools/live/manifestG6.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe6 diff --git a/tools/live/manifestG7.txt b/tools/live/manifestG7.txt new file mode 100644 index 0000000..3a0a620 --- /dev/null +++ b/tools/live/manifestG7.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe7 diff --git a/tools/live/manifestG8.txt b/tools/live/manifestG8.txt new file mode 100644 index 0000000..14f14c2 --- /dev/null +++ b/tools/live/manifestG8.txt @@ -0,0 +1 @@ +Agent-Class: GeomProbe8 diff --git a/tools/live/manifestJ1.txt b/tools/live/manifestJ1.txt new file mode 100644 index 0000000..d1de077 --- /dev/null +++ b/tools/live/manifestJ1.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: JsonDiff1 diff --git a/tools/live/manifestJ2.txt b/tools/live/manifestJ2.txt new file mode 100644 index 0000000..f3c6085 --- /dev/null +++ b/tools/live/manifestJ2.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: JsonDiff2 diff --git a/tools/live/manifestJ3.txt b/tools/live/manifestJ3.txt new file mode 100644 index 0000000..1b600f6 --- /dev/null +++ b/tools/live/manifestJ3.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: JsonDiff3 diff --git a/tools/live/manifestJ4.txt b/tools/live/manifestJ4.txt new file mode 100644 index 0000000..de4dba6 --- /dev/null +++ b/tools/live/manifestJ4.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: JsonDiff4 diff --git a/tools/live/manifestP3.txt b/tools/live/manifestP3.txt new file mode 100644 index 0000000..de35e0a --- /dev/null +++ b/tools/live/manifestP3.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: Probe3 diff --git a/tools/live/manifestP4.txt b/tools/live/manifestP4.txt new file mode 100644 index 0000000..2cf292c --- /dev/null +++ b/tools/live/manifestP4.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: Probe4 diff --git a/tools/live/manifestP5.txt b/tools/live/manifestP5.txt new file mode 100644 index 0000000..f4d7ac2 --- /dev/null +++ b/tools/live/manifestP5.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: Probe5 diff --git a/tools/live/manifestP6.txt b/tools/live/manifestP6.txt new file mode 100644 index 0000000..3d130d0 --- /dev/null +++ b/tools/live/manifestP6.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: Probe6 diff --git a/tools/live/manifestP7.txt b/tools/live/manifestP7.txt new file mode 100644 index 0000000..112de5b --- /dev/null +++ b/tools/live/manifestP7.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: Probe7 diff --git a/tools/live/manifestP9.txt b/tools/live/manifestP9.txt new file mode 100644 index 0000000..ef6f487 --- /dev/null +++ b/tools/live/manifestP9.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: Probe9 diff --git a/tools/live/manifestR1.txt b/tools/live/manifestR1.txt new file mode 100644 index 0000000..a875bec --- /dev/null +++ b/tools/live/manifestR1.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: ResetMaker1 diff --git a/tools/live/manifestR2.txt b/tools/live/manifestR2.txt new file mode 100644 index 0000000..8435c41 --- /dev/null +++ b/tools/live/manifestR2.txt @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Agent-Class: ResetMaker2 diff --git a/tools/live/manifestS1.txt b/tools/live/manifestS1.txt new file mode 100644 index 0000000..997fdbb --- /dev/null +++ b/tools/live/manifestS1.txt @@ -0,0 +1 @@ +Agent-Class: SlotMaker1 diff --git a/tools/live/manifestS2.txt b/tools/live/manifestS2.txt new file mode 100644 index 0000000..f2f78b0 --- /dev/null +++ b/tools/live/manifestS2.txt @@ -0,0 +1 @@ +Agent-Class: SlotMaker2 diff --git a/tools/live/src/GeomProbe1.java b/tools/live/src/GeomProbe1.java new file mode 100644 index 0000000..a07b2ac --- /dev/null +++ b/tools/live/src/GeomProbe1.java @@ -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 names = new java.util.TreeSet(); + 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; } +} diff --git a/tools/live/src/GeomProbe2.java b/tools/live/src/GeomProbe2.java new file mode 100644 index 0000000..86c13b4 --- /dev/null +++ b/tools/live/src/GeomProbe2.java @@ -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 names = new java.util.TreeSet(); + 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; } +} diff --git a/tools/live/src/GeomProbe3.java b/tools/live/src/GeomProbe3.java new file mode 100644 index 0000000..4e7b9bb --- /dev/null +++ b/tools/live/src/GeomProbe3.java @@ -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 names = new java.util.TreeSet(); + 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; } +} diff --git a/tools/live/src/GeomProbe4.java b/tools/live/src/GeomProbe4.java new file mode 100644 index 0000000..e343402 --- /dev/null +++ b/tools/live/src/GeomProbe4.java @@ -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; } +} diff --git a/tools/live/src/GeomProbe5.java b/tools/live/src/GeomProbe5.java new file mode 100644 index 0000000..0abe8ce --- /dev/null +++ b/tools/live/src/GeomProbe5.java @@ -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; } +} diff --git a/tools/live/src/GeomProbe6.java b/tools/live/src/GeomProbe6.java new file mode 100644 index 0000000..2d9d6aa --- /dev/null +++ b/tools/live/src/GeomProbe6.java @@ -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; } +} diff --git a/tools/live/src/GeomProbe7.java b/tools/live/src/GeomProbe7.java new file mode 100644 index 0000000..3202bc9 --- /dev/null +++ b/tools/live/src/GeomProbe7.java @@ -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; } +} diff --git a/tools/live/src/GeomProbe8.java b/tools/live/src/GeomProbe8.java new file mode 100644 index 0000000..47b18f3 --- /dev/null +++ b/tools/live/src/GeomProbe8.java @@ -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 loaders = new java.util.LinkedHashSet(); + 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; } +} diff --git a/tools/live/src/JsonDiff2.java b/tools/live/src/JsonDiff2.java new file mode 100644 index 0000000..171b256 --- /dev/null +++ b/tools/live/src/JsonDiff2.java @@ -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 candidates = new LinkedHashSet(); + 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; + } +} diff --git a/tools/live/src/JsonDiff3.java b/tools/live/src/JsonDiff3.java new file mode 100644 index 0000000..8c8ae03 --- /dev/null +++ b/tools/live/src/JsonDiff3.java @@ -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 candidates = new LinkedHashSet(); + 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; + } +} diff --git a/tools/live/src/JsonDiff4.java b/tools/live/src/JsonDiff4.java new file mode 100644 index 0000000..3c015f2 --- /dev/null +++ b/tools/live/src/JsonDiff4.java @@ -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 candidates = new LinkedHashSet(); + 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; + } +} diff --git a/viewpanel/src/com/spineview/ObjectPanel.java b/viewpanel/src/com/spineview/ObjectPanel.java index e97235f..0d05f69 100644 --- a/viewpanel/src/com/spineview/ObjectPanel.java +++ b/viewpanel/src/com/spineview/ObjectPanel.java @@ -636,45 +636,53 @@ public final class ObjectPanel { * 建用编辑器自己的 New Bone(带撤销),建完立刻改该骨骼的 x/y 与 color 字段。 */ private static void createTenRedBones(final State state) { - // 用已验证可行的做法:编辑器自己的 New Bone(一根一次)+ 每根之间等一帧;最后刷新一次。 - // (此前试过"直接调模型 API 连建 10 根",会把编辑器卡死,已放弃那条路。) + // 编辑器自己的 API 路径(不经 UI、不依赖下一帧):一次建 10 根 → 一次刷新 → 一次撤销 final Object parent = currentTarget(state); if (parent == null) { setStatus(state, "+10 Red Bones: select a parent bone first."); return; } - final int[] made = new int[1]; - final Object red; - try { - Class colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, state.loader); - red = colorClass.getConstructor(float.class, float.class, float.class, float.class) - .newInstance(Float.valueOf(1f), Float.valueOf(0f), Float.valueOf(0f), Float.valueOf(1f)); - } catch (Throwable error) { - setStatus(state, "+10 Red Bones: 造红色失败 " + error); + final Object skeleton = SceneActions.skeletonOf(parent); + if (skeleton == null) { + setStatus(state, "+10 Red Bones: 找不到所属骨架"); return; } - for (int i = 0; i < 10; i++) { - final float x = (float) (Math.random() * 400.0 - 200.0); - final float y = (float) (Math.random() * 400.0 - 200.0); - final java.util.List before = listQuietly(state); - SceneActions.onEditorThread(state.loader, new Runnable() { - @Override - public void run() { - SceneActions.create(state.loader, parent, "Bone"); - Object fresh = firstNew(before, listQuietly(state)); - if (fresh == null) return; - setBoneNumber(fresh, "x", x); - setBoneNumber(fresh, "y", y); - setBoneColor(fresh, red); - made[0]++; + final int[] made = new int[1]; + SceneActions.onEditorThread(state.loader, new Runnable() { + @Override + public void run() { + try { + Class colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, state.loader); + Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class) + .newInstance(Float.valueOf(1f), Float.valueOf(0f), Float.valueOf(0f), Float.valueOf(1f)); + Class undo = Class.forName("un", true, state.loader); + java.lang.reflect.Method begin = null; + java.lang.reflect.Method commit = null; + for (Method m : undo.getDeclaredMethods()) { + if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue; + if (m.getName().equals("D")) begin = m; + if (m.getName().equals("B")) commit = m; + } + if (begin != null) { + begin.setAccessible(true); + begin.invoke(null); + } + for (int i = 0; i < 10; i++) { + Object bone = SceneActions.createBone(state.loader, skeleton, parent, "red_" + (i + 1), + (float) (Math.random() * 400.0 - 200.0), (float) (Math.random() * 400.0 - 200.0), + red); + if (bone != null) made[0]++; + } + if (commit != null) { + commit.setAccessible(true); + commit.invoke(null); + } + SceneActions.refresh(state.loader, skeleton); + } catch (Throwable error) { + System.err.println("[panel] +10 Red Bones(API) 失败: " + error); } - }); - try { - Thread.sleep(180); // 等编辑器过一帧,下一根才会真的建出来 - } catch (InterruptedException ignored) { - break; } - } + }); setStatus(state, "made " + made[0] + " red bones under " + SceneObjects.nameOf(parent)); refreshList(state); } diff --git a/viewpanel/src/com/spineview/SceneActions.java b/viewpanel/src/com/spineview/SceneActions.java index 6415191..83f206e 100644 --- a/viewpanel/src/com/spineview/SceneActions.java +++ b/viewpanel/src/com/spineview/SceneActions.java @@ -909,67 +909,74 @@ public final class SceneActions { // → 骨架.StackMapTable() ← 重建索引 → Editor.D(骨骼, true) → VZ.D(骨架, true, true) 重建树 // 一次调用可以连建 N 根,最后只刷新一次;外层包 un.D()/un.B() 就是"一次撤销"。 - /** 直接建一根骨骼(返回新骨骼对象;失败返回 null)。 */ + /** + * 直接建一根骨骼(编辑器自己的序列,全部有字节码依据,见 `javap -c UH` 的 D(Gp,Gp,Z,gR,fI)): + * new Gp(模板, 父.skeleton, 父) ← 构造器 (Gp, GP, Gp) + * name = OG.D(父名字, skeleton.骨骼数组, null) ← 编辑器自己的不重名生成器 + * skeleton.addBone(bone) ← GP.D(Gp)(字节码:bone.B = size; array.add(bone)) + * 之后由调用方设 x/y/color 并统一刷新一次。 + */ public static Object createBone(ClassLoader loader, Object skeleton, Object parent, String name, float x, float y, Object color) { try { - System.err.println("[crud] createBone step1 new Gp " + skeleton + " parent=" + parent); Class boneClass = Class.forName("Gp", true, loader); - Object bone = boneClass.getConstructor().newInstance(); - setByType(bone, "GP", skeleton); - if (parent != null) setByType(bone, "Gp", parent); - setByNameType(bone, "D", String.class, name); - setByNameType(bone, "Z", float.class, Float.valueOf(x)); - setByNameType(bone, "V", float.class, Float.valueOf(y)); - if (color != null) setByNameType(bone, "B", - Class.forName("com.badlogic.gdx.graphics.Color", true, loader), color); - System.err.println("[crud] createBone step2 fields ok"); + Class skeletonClass = Class.forName("GP", true, loader); + Object bone = boneClass.getConstructor(boneClass, skeletonClass, boneClass) + .newInstance(parent, skeleton, parent); - // 骨架.D(骨骼):GP 上的 (LGp;)V - Method addBone = findMethod(skeleton.getClass(), "D", new Class[] {boneClass}, void.class); - if (addBone == null) throw new NoSuchMethodException("GP.D(LGp;)V addBone"); - addBone.setAccessible(true); - System.err.println("[crud] createBone step3 " + addBone.getDeclaringClass().getName() + "." - + addBone.getName() + "(Gp) 调用中"); - addBone.invoke(skeleton, bone); - System.err.println("[crud] createBone step3 ok"); - - // 骨架.StackMapTable():重建索引 - Method rebuild = null; - for (Method m : skeleton.getClass().getMethods()) { - if (m.getName().equals("StackMapTable") && m.getParameterTypes().length == 0 - && m.getReturnType() == void.class) { - rebuild = m; - break; - } - } - if (rebuild != null) { - rebuild.setAccessible(true); - rebuild.invoke(skeleton); - } - - // Editor.D(骨骼, true):通知编辑器(找不到就跳过,不影响模型) + // 名字:优先用编辑器自己的生成器(找不到就用调用方给的名字) + String finalName = name; try { - Class editorClass = Class.forName("com.esotericsoftware.spine.editor.Editor", true, loader); - Method notify = findMethod(editorClass, "D", new Class[] {boneClass, boolean.class}, - void.class); - Object editor = SceneObjects.global(loader, "com.esotericsoftware.spine.editor.Editor"); - if (notify != null && editor != null) { - notify.setAccessible(true); - notify.invoke(editor, bone, Boolean.TRUE); + Class og = Class.forName("OG", true, loader); + Class arrayClass = Class.forName("fI", true, loader); + Class slotClass = Class.forName("GU", true, loader); + Method gen = findMethod(og, "D", + new Class[] {String.class, arrayClass, slotClass}, String.class); + Object bones = null; + for (Field field : skeletonClass.getDeclaredFields()) { + if (field.getType() == arrayClass) { + field.setAccessible(true); + bones = field.get(skeleton); + break; + } + } + Object parentName = null; + for (Field field : boneClass.getDeclaredFields()) { + if (field.getType() == String.class) { + field.setAccessible(true); + parentName = field.get(parent); + break; + } + } + if (gen != null && bones != null) { + gen.setAccessible(true); + Object generated = gen.invoke(null, "bone", bones, null); + if (generated instanceof String && !((String) generated).isEmpty()) { + finalName = (String) generated; + } } } catch (Throwable error) { - System.err.println("[crud] Editor.D(bone,true) 跳过: " + error); + System.err.println("[crud] 名字生成器跳过: " + error); } + + Method addBone = findMethod(skeletonClass, "D", new Class[] {boneClass}, void.class); + if (addBone == null) throw new NoSuchMethodException("GP.D(Gp) addBone"); + addBone.setAccessible(true); + addBone.invoke(skeleton, bone); + + setByNameType(bone, "D", String.class, finalName); + setByNameType(bone, "Z", float.class, Float.valueOf(x)); + setByNameType(bone, "V", float.class, Float.valueOf(y)); + if (color != null) { + setByNameType(bone, "B", Class.forName("com.badlogic.gdx.graphics.Color", true, loader), + color); + } + System.err.println("[crud] createBone ok name=" + finalName); return bone; } catch (Throwable error) { System.err.println("[crud] createBone 失败: " + error); - Throwable cause = error; - while (cause != null) { - System.err.println("[crud] cause: " + cause); - cause.printStackTrace(System.err); - cause = cause.getCause(); - } + Throwable cause = error.getCause(); + if (cause != null) System.err.println("[crud] cause: " + cause); return null; } } diff --git a/阶段五-面板与实际数据联动.md b/阶段五-面板与实际数据联动.md new file mode 100644 index 0000000..adafae8 --- /dev/null +++ b/阶段五-面板与实际数据联动.md @@ -0,0 +1,128 @@ +# 阶段五:把面板接到"真实数据"上(对象增删改查 / 属性读取与改写 / 命名还原) + +本文记录阶段五**已经做成的**、**实测到的数字**、**踩过的坑**,以及**还没做完的**。 +详细的"混淆名 ⇒ 真名"对照在 [`混淆名对照表.md`](混淆名对照表.md),交互链路在 [`编辑器交互与数据API.md`](编辑器交互与数据API.md)。 + +--- + +## 一、阶段五做成了什么 + +### 1. 面板(Spine View)长这样(三段式) + +| 段 | 内容 | 高度 | +| --- | --- | --- | +| 上 | `Refresh / New Bone / New Slot / New BBox / New Path / Delete / Rename / Inspect / Set Value` + 名字输入框 | 由 `spineview-fit` 线程设置(当前按 600 的比例) | +| 中 | 列表 / Inspector(对象清单 或 选中对象的字段+方法) | 同上(1400 的比例) | +| 下 | 日志(最近 6 条按钮动作提示) | 同上(400 的比例) | + +面板文字**一律英文**(编辑器只装了拉丁字形,中文渲染成方块)。 + +### 2. 能做的事(都已实测跑通) + +| 能力 | 实现 | 证据 | +| --- | --- | --- | +| **列出**场景对象 | 走层级树的节点;`SceneObjects.listAllNodes()`(树里 28 个对象:skeleton/bone/slot/attachment + Constraints/Draw Order/… 固定根) | 面板列表与编辑器 Tree 一致 | +| **选中**对象 | `VZ.D(Tree.Node, boolean)`(`SceneObjects.select`) | 编辑器 Tree 里对应行被高亮(截图已确认) | +| **读**当前选择 | `VZ.B()` = `Selection.getLastSelected().object` | 面板 "selected: bone \"bone3\"" 与编辑器一致 | +| **增**对象 | 先选中父级 → 跑编辑器命令 `New Bone` / `New Slot` / `New BBox` / `New Path` | 新建出 `bone7`、`bone8`(骨骼自动编号)等 | +| **删**对象 | `un.D()` → `对象.L()` → `un.B()` → `VZ.D(骨架,true,true)` 重建树 | 删除后树与面板同步(父骨骼删除会连子骨骼一起删) | +| **改**名字 | 同上,`对象.D(新名字,true)` + 重建树 | `bone3` → `panel_test` 生效 | +| **改**属性(测试用) | Inspector 里点字段行 → 值读进输入框 → `Set Value` 直接写模型字段(带撤销) | 写回后值与编辑器属性面板同步 | +| **输入框自动化** | 有些 `New X` 会弹"输入名字"对话框:找到"带 TextField 且有 `L(Object)V` 回调"的对话框对象 → 填名字 → 直接调 `L(true)`(等于点 OK),不弹给用户 | `[crud] 已自动填写对话框并点 OK` | +| **跟随编辑器选择** | 后台线程 600ms 轮询 `VZ.B()`,变了就自动 Inspect | 日志 `[panel] Inspected slot "mega_dg6": 48 entries` | +| **鼠标滚轮** | 点面板按钮后把 Stage 的 scroll focus 抢到列表(`grabScrollFocus`) | 用户实测:"拖动/点列表后滚动条出现、滚轮可用" | + +### 3. 命名还原(把 `D/B/L` 变成有意义的名字) + +| 产物 | 内容 | +| --- | --- | +| `tools/deobf/Renamer.java` | ASM `Remapper`:按映射表重命名**成员**(不改类名 —— 载荷的 indy 靠加密串里的原名反射查找) | +| `tools/deobf/anchors.tsv` | 手动/实测锚点(含依据注释) | +| `tools/deobf/anchors-libgdx.tsv` | `MapMethods` 对上游 **gdx-1.11.0** 的**唯一命中**(`= `)条目 | +| `tools/deobf/anchors-libgdx-unconfirmed.tsv` | 只给了候选(`~n/m`)的条目 —— **不可信,别用** | +| `tools/deobf/names.tsv` | 合并结果(当前 **232 条**):确定 + 手动/实测 | +| `~/Desktop/spine398/src-named/` | 改名后的反编译源码(每个类单独反编译、`class_<载荷名>.java` 命名) | +| `tools/decrypt/DumpSites.java` | 逐 invokedynamic 站点解出"真实类.方法 **+ 描述符**"(带描述符是关键) | + +**面板 Inspector 现在会按映射表显示真名**,格式 `真名(混淆名)`,例如: + +``` +float scaleX(L) = 1.0 float width(C) = 166.0 +float rotation(S) = 0.0 float height(Z) = 87.0 +``` + +### 4. 本轮实测出来的字段语义(都写进了 `anchors.tsv`) + +| 对象 | 字段 | 含义 | 依据 | +| --- | --- | --- | --- | +| `Gp`(骨骼) | `Z/V` | 当前 x/y | 面板 Translate(-168,-116) ↔ Z=-168.00009/V=-116.0 | +| | `F` / `Code` / `SourceFile` | 当前 rotation / scaleX / scaleY | 面板 Rotate 341.456 ↔ F=33.676704;Scale(0.8799,0.572) ↔ Code/SourceFile | +| | `StackMapTable` / `LineNumberTable` | 当前 shearX / shearY | 面板 Shear(-52.221,-28.63) | +| | `D/B/L/I/S/C` | 静止姿态(setup) 的 x/y/rotation/scaleX/scaleY/shear(推断) | 默认值 0,0,0,1,1,0;未改动时与上面那组相等 | +| | `Class` / `Ljava` / `java` | length / 世界缩放 X / 世界缩放 Y | 面板 Length=0.0 ↔ Class=0.0;Ljava/java 与 Code/SourceFile 相等 | +| `GO`(附件) | `S/L/I/C/Z` | rotation / scaleX / scaleY / width / height | 面板 Region 49°/1.997/1.335/157/154 一一对应 | +| `GU`(插槽) | `String D` / `Gp D` / `Gn D` / `Color D` | 名字 / 所属骨骼 / 附件 / 颜色 | 面板 Slot 只有 Name+Color;树节点名一致 | +| 通用 | `Table` 上 `D(Ldl;)LdR;`=`add`、`I()LdR;`=`row`、`SourceFile()`=`clearChildren` | | 指纹 + 目录铺满/换行实测 | + +--- + +## 二、踩过的坑(按"代价从大到小") + +1. **`VZ.D(Object)` 不是"选中"** —— 它内部是 `Selection.L()`(去掉选中项)。真正"设成选中"的是 `VZ.D(Tree.Node, boolean)`。这让我前面所有增删改都"执行了但没生效"。 +2. **动模型的代码必须在编辑器渲染线程** —— `OG.I()` 有线程断言,非渲染线程直接抛 `RuntimeException`;要用编辑器的 `OG.B(Runnable)` 切过去。 +3. **不能在渲染线程里 sleep** —— 编辑器有些创建是**下一帧**做的;占着渲染线程睡 300ms,那一帧永远不来,对象就建不出来。 +4. **`MapMethods` 的候选(`~n/m`)不是结论** —— 我把它们混进 `names.tsv` 当确定名字用,结果"拿的方法不对"。现在拆成 `anchors-libgdx.tsv`(只放 `=` 唯一命中)与 `anchors-libgdx-unconfirmed.tsv`。 +5. **同名不同描述符的坑就在我们自己代码里** —— `Cell.fill()` 的载荷名是 `D`,但 Cell 上有多个无参 `D()`,`getMethod("D")` 会取错;`fillY="L"` 是"按形状猜"的,没验证过(很可能取错)。 +6. **`ScrollPane.B(ZZ)V` 是 `setScrollingDisabled`,不是 `setForceScroll`** —— 我传了 `(false,true)`,等于**把纵向滚动关掉了**(现象:列表滚不动)。`setForceScroll` 是 `I(ZZ)V`。 +7. **滚轮不生效的根因是焦点** —— 给列表 ScrollPane 挂事件监听后,滚轮事件收到 **0 条**:焦点在画布上。解法:点面板后 `Stage.setScrollFocus(面板)`;找这个方法必须用 `getMethods()`(它声明在接口 `LPt9` 里,`getDeclaredMethods` 扫不到)。 +8. **`run.sh` 漏了 `import os`** —— python 那句报 NameError,**编辑器根本起不来**;而我用 `>/dev/null` 把错误吞了,白排查了好几轮。 +9. **`nohup … &` 启动会被会话回收** —— 必须 `subprocess.Popen(..., start_new_session=True)`,否则我这边一次工具调用结束,编辑器就被收掉(你几次看到"进程没了"就是这个)。 +10. **面板根表格的高度被父容器限制** —— `fillParent` 是"复制父容器尺寸",而父容器又按我们的内容算 → 互相依赖;结果 panel 永远等于自己算出来的高度(实测 root≈1360,停靠框 1944)→ 底部固定留一条空带(你说的"区域1")。**从面板内部无解**,只能用 `expand` 或把停靠栏拖大。 +11. **热更(换容器重建面板)把面板搞成空白** —— 那版已 `git revert`;要做得换一种容器(能让 dock 正确布局的 WidgetGroup)。 +12. **macOS 文件系统大小写不敏感** —— 一次反编译整包时 `GP.java`/`Gp.java`、`Yr.java`/`yR.java` 互相覆盖,导致"有些类像没改名"。现在改成每个类单独反编译 + `class_<载荷名>.java`。 +13. **`Table.setDebug(true)` 的红/绿线是调试描边**,不是布局错误;它给每层表格的每个 cell 各画一圈(红=根表格三段,绿=内部格)。 +14. **我自作主张加过"分页"**(你没要求)—— 已回滚,并按你说的把纪律写成项目级技能 `.codex/skills/scope-discipline`。 + +--- + +## 三、API 规模统计(载荷里的"面") + +| 分组 | 类 | 方法 | public 方法 | +| --- | --- | --- | --- | +| LWJGL(保名) | 629 | 9564 | 5444 | +| Spine 编辑器(保名,`editor/**`) | 57 | 312 | 184 | +| libGDX(保名那部分) | 52 | 545 | 462 | +| jglfw + libimagequant | 7 | 169 | 150 | +| reflectasm + 其它第三方 | 13 | 98 | 55 | +| **默认包(类名被混淆)** | **3418** | **15782** | **9918** | +| 合计 | 4176 | 26470 | 16213 | + +默认包那 3418 个是**混合体**:用"方法描述符多重集 + 上游 gdx-1.11.0 指纹"能较有把握认出 **~55 类 / ~907 方法**属于 libGDX(你举的"设置文字/颜色"在这层),**其余 ~3363 类 / ~14875 方法**是编辑器自有业务代码(Tree 视图、模型、命令系统、属性面板、导出…)。这只是"正例识别",真实环境自带会比 907 略多。 + +另外:invokedynamic 调用点 **55637** 个(`~/Desktop/spine398/sites.tsv`,已解出真实目标+描述符);编辑器注册的菜单命令 100 多条(`hotkeys.txt`)。 + +--- + +## 四、怎么跑(当前唯一的正确入口) + +```bash +cd "/Users/tianmokeji/Desktop/临时/spine加功能" +JAVA_HOME=/tmp/jdk-17.0.20.1+1/Contents/Home ./viewpanel/build.sh # 编译 +./viewpanel/run.sh # 先杀旧实例再启动(约 20s) +# 或双击 viewpanel/重启编辑器.command / spine-lab/启动实验室Spine.command(后者已修好,内部同样调 run.sh) +tail -f /tmp/spineview.log # 日志([panel]/[view]/[crud] 前缀) +``` + +调试开关:`-Dspine.probe=objects|panels|select|crud|matrix|panel`(探针)、`-Dspine.runCommand="New Bone"`(跑一条编辑器命令)、`-Dpanel.h=<像素>`(写死面板高度,默认 auto=读 Stage)。 + +--- + +## 五、还没做完 / 没验证的(别当成已完成) + +1. **"列表控件铺满红框"**:要裁剪+滚动就不能铺满(ScrollPane 里 widget 按自己内容大小);要铺满就得去掉 ScrollPane(会溢出)。两条路都试过,**取舍未定**。 +2. **新确认的 `fillY="L"`、`CELL_FILL="D"`**:`D` 在映射表里是唯一命中的 `fill`,`L` 从没验证过 —— 列表没被纵向拉伸很可能就是这里取错了方法。 +3. **编辑器自有类(那 3363 个)的名字还原**:只做了 libGDX 层与少数手工锚点;业务层(`VZ`/`GP`/命令系统…)还没命名。 +4. **热更**:回滚了;要做就得换容器方案。 +5. **`GO/GU` 里还有一批未确认字段**(`boolean D/B/L/I/S/C/Z/V/F/Code`、`int D`、`Color B/L/I`…)。 +6. **调试描边常开**(`ObjectPanel` 里搜 `TABLE_SET_DEBUG` 可关),还没做开关。 +7. **导出链路**:现阶段未动(`Export` 菜单动作可用,见 `编辑器交互与数据API.md` §6)。