package com.spineview; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * 把"按钮 + 文字显示"面板挂进 Spine 的 scene2d 场景树。 * * 编辑器的窗口是 jglfw 建的原生 NSWindow,不是 AWT 窗口,没有 Swing 面板可挂; * 界面本身是 libGDX scene2d 画出来的,只能加进它的 Stage。 * * 这套运行时把 libGDX 的方法名混淆了(类名保留): * Stage 的实现 = uk(继承 dt)、Group = do、Actor = dl、Table = et * 所以下面全部"按类型/签名找成员",不依赖混淆后的名字。 * * Stage 实例在 tD 里只是局部变量,任何静态字段都没有;但 libGDX 里每个 Actor * 都记着自己所在的 Stage,而 Fx.D 是一个静态 Actor —— 从这里就能把 Stage 捞回来。 */ public final class Scene2dPanel { private static final String STAGE = "dt"; private static final String GROUP = "do"; private static final String ACTOR = "dl"; private static final String SKIN = "com.badlogic.gdx.scenes.scene2d.ui.Skin"; private static final String LABEL = "com.badlogic.gdx.scenes.scene2d.ui.Label"; private static final String TEXT_BUTTON = "com.badlogic.gdx.scenes.scene2d.ui.TextButton"; private static final String WINDOW = "com.badlogic.gdx.scenes.scene2d.ui.Window"; private Scene2dPanel() { } public static void installWhenReady(final ClassLoader loader) { System.err.println("[panel] 开始等 Stage/Skin…"); if (System.getProperty("spine.dumpStatics") != null) { try { Thread.sleep(30000); } catch (InterruptedException ignored) { return; } dumpStatics(loader, paths((java.net.URLClassLoader) loader)); return; } Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 240; i++) { try { Object stage = stageFromActorAnchor(loader); Object skin = null; if (stage != null) { Object root = firstFieldOfType(stage, GROUP); if (root != null) skin = skinFromRoot(root); } if (i % 10 == 0) { System.err.println("[panel] 第 " + i + " 轮: stage=" + nameOf(stage) + " skin=" + nameOf(skin)); } if (stage != null && skin != null) { install(loader, stage, skin, i); return; } } catch (Throwable error) { System.err.println("[panel] 查找失败: " + error); error.printStackTrace(System.err); } try { Thread.sleep(500); } catch (InterruptedException ignored) { return; } } System.err.println("[panel] 等不到 Stage/Skin"); } }, "spineview-panel"); thread.setDaemon(true); thread.start(); } private static String nameOf(Object value) { return value == null ? "null" : value.getClass().getName(); } /** * 有了 Stage 实例之后挂面板。Skin 从 Stage 根节点的子节点上取 * (libGDX 的 Table 自己存着构造时用的 Skin)。 */ public static void installForStage(final Object stage, final ClassLoader loader) { // Stage 是先建出来的,UI 是随后才一层层加进去的,所以要等面板挂上来之后才拿得到 Skin。 Thread thread = new Thread(new Runnable() { @Override public void run() { try { for (int i = 0; i < 240; i++) { Object skin = findSkinDeep(stage, 6, 20000); if (skin != null) { System.err.println("[panel] 第 " + i + " 轮拿到 Skin: " + skin.getClass().getName()); install(loader, stage, skin, i); return; } if (i % 20 == 0) { System.err.println("[panel] 第 " + i + " 轮还没拿到 Skin(Stage " + nameOf(stage) + ")"); } Thread.sleep(500); } System.err.println("[panel] 一直没拿到 Skin"); } catch (Throwable error) { System.err.println("[panel] installForStage 失败"); error.printStackTrace(System.err); } } }, "spineview-skin"); thread.setDaemon(true); thread.start(); } private static void install(final ClassLoader loader, Object stage, Object skin, int waited) throws Exception { Class skinClass = Class.forName(SKIN, true, loader); Class buttonClass = Class.forName(TEXT_BUTTON, true, loader); Class labelClass = Class.forName(LABEL, true, loader); Class windowClass = Class.forName(WINDOW, true, loader); final Object button = buttonClass.getConstructor(String.class, skinClass) .newInstance("Read Scene Objects", skin); final Object label = labelClass.getConstructor(CharSequence.class, skinClass) .newInstance("Click the button to list bones / slots / attachments.", skin); // setBounds 必须各自在自己的类层次上找:Button 和 Label 只有共同的 Actor 祖先, // 拿 Button 上找到的方法去 invoke Label 会报"object is not an instance of declaring class"。 // 先挂在场景根节点上(视口左上角,避开编辑器自己的工具条)。 findInstanceMethod(button.getClass(), 4, float.class).invoke(button, 60f, 560f, 200f, 32f); findInstanceMethod(label.getClass(), 4, float.class).invoke(label, 60f, 300f, 540f, 250f); Object root = firstFieldOfType(stage, GROUP); addChild(root, button); addChild(root, label); System.err.println("[panel] 已把按钮和文字面板挂到 Stage(等了 " + waited + " 轮)"); wireButton(button, label, loader); } /** * 把"按钮点击 → 读取场景对象 → 更新文字"这条链路接上。 * * 按钮类的方法名被混淆了,没用名字去猜:Button 里有一对 setChecked(boolean)/isChecked(), * 我们把每个 setter 设成 true,再看哪个无参 boolean getter 跟着变 true,就标定了这一对。 * libGDX 的 Button 自带一个 ClickListener,点一下就会翻转 isChecked,所以轮询它就等于监听点击。 */ public static void wireButton(final Object button, final Object label, final ClassLoader loader) throws Exception { Class buttonClass = button.getClass(); Class labelClass = label.getClass(); // 优先用对照表里定好的名字:Button 上无参方法叫 "String" 的有两个(boolean 和 float), // 只按名字取会取错,必须连返回类型一起判。 Method isChecked = findPinned(buttonClass, Obf.BUTTON_IS_CHECKED, boolean.class); Method setChecked = findPinned(buttonClass, Obf.BUTTON_SET_CHECKED, void.class, boolean.class); if (isChecked == null || setChecked == null) { // 名字对不上(换版本)就退回运行期自校准 —— 只在 Button 自己这一层里试, // 不然会扫到继承来的方法,选出一对语义无关的 setter/getter(现象是点击被反复触发)。 System.err.println("[panel] 对照表里的 isChecked/setChecked 没找到,改用自校准"); Method[] pair = calibrateChecked(button, buttonClass); if (pair == null) { System.err.println("[panel] 没能标定 isChecked/setChecked"); return; } isChecked = pair[0]; setChecked = pair[1]; } if (isChecked == null) { System.err.println("[panel] 没能标定 isChecked/setChecked"); return; } System.err.println("[panel] isChecked=" + isChecked.getName() + " setChecked=" + setChecked.getName()); final Method readChecked = isChecked; final Method resetChecked = setChecked; // setChecked(boolean, boolean) 的第二个参数是"是否派发事件"。复位时用 false, // 否则把 isChecked 置回 false 这件事本身又会派发一次事件,表现为按一下读两次。 final Method resetQuietly = findPinned(buttonClass, Obf.BUTTON_SET_CHECKED_FIRE, void.class, boolean.class, boolean.class); // 必须连返回类型一起判:Label 上 `D(CharSequence)` 有 void(setText)和 boolean // (textEquals)两个,按参数个数找会挑中 textEquals,表现是文字永远不刷新。 final Method setText = findPinned(labelClass, Obf.LABEL_SET_TEXT, void.class, CharSequence.class); if (setText == null) { System.err.println("[panel] 没找到 Label.setText,面板文字不会更新"); return; } final ClassLoader classLoader = loader; Thread poll = new Thread(new Runnable() { @Override public void run() { while (true) { try { if (Boolean.TRUE.equals(readChecked.invoke(button))) { if (resetQuietly != null) { resetQuietly.invoke(button, Boolean.FALSE, Boolean.FALSE); } else { resetChecked.invoke(button, Boolean.FALSE); } System.err.println("[panel] 按钮被触发,开始读取场景对象"); // Actor 自己记着所在 Stage,直接问它就行 Object stage = firstFieldOfType(button, STAGE); String text = SceneReader.read(classLoader, stage); System.err.println("[panel] 读取完成,共 " + text.length() + " 字符"); setText.invoke(label, text); } Thread.sleep(80); } catch (Throwable error) { System.err.println("[panel] 轮询异常: " + error); return; } } } }, "spineview-poll"); poll.setDaemon(true); poll.start(); // 验证用:--Dspine.simulateClick=1 时自动把按钮"按"一次,方便无人值守地看结果。 if (System.getProperty("spine.simulateClick") != null) { Thread auto = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(4000); resetChecked.invoke(button, Boolean.TRUE); } catch (Throwable error) { System.err.println("[panel] 自动触发失败: " + error); } } }, "spineview-autoclick"); auto.setDaemon(true); auto.start(); } } /** 按"名字 + 参数类型 + 返回类型"找方法:混淆后同名方法成堆,少判一个条件就会挑错。 */ private static Method findPinned(Class owner, String name, Class returnType, Class... parameterTypes) { for (Method method : owner.getMethods()) { if (!method.getName().equals(name)) continue; if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue; if (returnType != null && method.getReturnType() != returnType) continue; return method; } return null; } /** * 运行期自校准:把每个 boolean setter 设成 true,看哪个无参 boolean getter 跟着变 true。 * 只在按钮类自己这一层(含父类)的方法里找 —— 扫到继承链上无关的方法会配出一对假组合, * 现象是"点一次按钮被反复触发"。 */ private static Method[] calibrateChecked(Object button, Class buttonClass) throws Exception { for (Class type = buttonClass; type != null && type != Object.class; type = type.getSuperclass()) { for (Method setter : type.getDeclaredMethods()) { if (setter.getParameterTypes().length != 1 || setter.getParameterTypes()[0] != boolean.class || setter.getReturnType() != void.class) continue; setter.setAccessible(true); setter.invoke(button, Boolean.TRUE); for (Method getter : type.getDeclaredMethods()) { if (getter.getParameterTypes().length != 0 || getter.getReturnType() != boolean.class) continue; getter.setAccessible(true); if (Boolean.TRUE.equals(getter.invoke(button))) { setter.invoke(button, Boolean.FALSE); return new Method[] {getter, setter}; } } setter.invoke(button, Boolean.FALSE); } } return null; } /** 从 Fx.D(静态 Actor)出发拿 Stage:libGDX 里每个 Actor 都记着自己所在的 Stage。 */ private static Object stageFromActorAnchor(ClassLoader loader) { try { Class holder = Class.forName("Fx", true, loader); Object anchor = null; for (Field field : holder.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers())) continue; if (!field.getType().getName().equals(ACTOR)) continue; field.setAccessible(true); anchor = field.get(null); if (anchor != null) break; } if (anchor == null) return null; return firstFieldOfType(anchor, STAGE); } catch (Throwable error) { return null; } } /** Stage 根节点是 do(Group);拿它第一个子节点身上的 Skin(Table 都带 Skin 字段)。 */ @SuppressWarnings("unused") private static Object skinFromRoot(Object root) throws Exception { Object children = firstArrayFieldOfType(root, "[L" + ACTOR + ";"); if (!(children instanceof Object[])) return null; for (Object child : (Object[]) children) { if (child == null) continue; Object skin = firstFieldOfType(child, SKIN); if (skin != null) return skin; Object deep = skinFromActor(child, 3); if (deep != null) return deep; } return null; } /** 顺着 actor 树往下找 Skin,最多 dig 层。 */ private static Object skinFromActor(Object actor, int dig) throws Exception { if (actor == null || dig < 0) return null; Object direct = firstFieldOfType(actor, SKIN); if (direct != null) return direct; if (dig == 0) return null; Object children = firstArrayFieldOfType(actor, "[L" + ACTOR + ";"); if (!(children instanceof Object[])) return null; for (Object child : (Object[]) children) { Object found = skinFromActor(child, dig - 1); if (found != null) return found; } return null; } private static Object firstFieldOfType(Object target, String typeName) throws Exception { for (Class type = target.getClass(); type != null; type = type.getSuperclass()) { for (Field field : type.getDeclaredFields()) { if (!field.getType().getName().equals(typeName)) continue; field.setAccessible(true); Object value = field.get(target); if (value != null) return value; } } return null; } /** 从任意对象出发做有界广度搜索,找第一个 Skin 实例。 */ private static Object findSkinDeep(Object start, int maxDepth, int maxNodes) { java.util.Set seen = java.util.Collections.newSetFromMap( new java.util.IdentityHashMap()); java.util.ArrayDeque queue = new java.util.ArrayDeque(); java.util.ArrayDeque depths = new java.util.ArrayDeque(); queue.add(start); depths.add(Integer.valueOf(0)); int nodes = 0; while (!queue.isEmpty() && nodes++ < maxNodes) { Object current = queue.poll(); int depth = depths.poll().intValue(); if (current == null || !seen.add(current)) continue; if (current.getClass().getName().equals(SKIN)) return current; if (depth >= maxDepth) continue; if (current.getClass().isArray()) { if (!current.getClass().getComponentType().isPrimitive()) { for (Object item : (Object[]) current) { if (item != null) { queue.add(item); depths.add(Integer.valueOf(depth + 1)); } } } continue; } String name = current.getClass().getName(); if (depth > 0 && (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.") || name.startsWith("org.lwjgl"))) continue; for (Class type = current.getClass(); type != null; type = type.getSuperclass()) { for (Field field : type.getDeclaredFields()) { if (Modifier.isStatic(field.getModifiers())) continue; if (field.getType().isPrimitive()) continue; try { field.setAccessible(true); Object value = field.get(current); if (value != null) { queue.add(value); depths.add(Integer.valueOf(depth + 1)); } } catch (Throwable ignored) { // 跳过读不到的 } } } } return null; } @SuppressWarnings("unused") private static Object firstFieldOfTypeOld(Object target, String typeName) throws Exception { for (Class type = target.getClass(); type != null; type = type.getSuperclass()) { for (Field field : type.getDeclaredFields()) { if (!field.getType().getName().equals(typeName)) continue; field.setAccessible(true); Object value = field.get(target); if (value != null) return value; } } return null; } private static Object firstArrayFieldOfType(Object target, String descriptor) throws Exception { for (Class type = target.getClass(); type != null; type = type.getSuperclass()) { for (Field field : type.getDeclaredFields()) { if (!field.getType().getName().equals(descriptor)) continue; field.setAccessible(true); Object value = field.get(target); if (value != null) return value; } } return null; } /** 按"参数个数 + 参数类型"找方法(名字被混淆了,只能这么找)。 */ /** Table.add(Actor):一个参数、参数能容纳该 actor、返回值非 void。 */ private static Method findAddMethod(Class tableClass, Class actorClass) throws NoSuchMethodException { for (Method method : tableClass.getMethods()) { if (Modifier.isStatic(method.getModifiers())) continue; Class[] types = method.getParameterTypes(); if (types.length != 1) continue; if (!types[0].isAssignableFrom(actorClass)) continue; if (method.getReturnType() == void.class) continue; return method; } throw new NoSuchMethodException("找不到 add"); } /** Table.row():无参、返回值就是表格自己。 */ private static Method findRowMethod(Class tableClass) throws NoSuchMethodException { for (Method method : tableClass.getMethods()) { if (Modifier.isStatic(method.getModifiers())) continue; if (method.getParameterTypes().length != 0) continue; // row() 声明在 Table 上、返回 Table;Window 继承来的,所以不能要求返回值正好是 Window Class returns = method.getReturnType(); if (returns != Object.class && returns.isAssignableFrom(tableClass)) return method; } throw new NoSuchMethodException("找不到 row"); } private static Method findInstanceMethod(Class owner, int parameterCount, Class parameterType) throws NoSuchMethodException { for (Method method : owner.getMethods()) { if (Modifier.isStatic(method.getModifiers())) continue; Class[] types = method.getParameterTypes(); if (types.length != parameterCount) continue; boolean match = true; for (Class type : types) { if (parameterType != null && type != parameterType) { match = false; break; } } if (match) return method; } throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount); } @SuppressWarnings("unused") private static Method findMethod(Class owner, int parameterCount, Class parameterType) throws NoSuchMethodException { for (Method method : owner.getMethods()) { Class[] types = method.getParameterTypes(); if (types.length != parameterCount) continue; boolean match = true; for (Class type : types) { if (parameterType != null && type != parameterType) { match = false; break; } } if (match) return method; } throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount); } /** Group 的 addActor(Actor):找一个只有一个参数、且参数能容纳该 actor 的 void 方法。 */ private static void addChild(Object group, Object actor) throws Exception { for (Method method : group.getClass().getMethods()) { Class[] types = method.getParameterTypes(); if (types.length != 1) continue; if (!types[0].isAssignableFrom(actor.getClass())) continue; if (method.getReturnType() != void.class) continue; method.invoke(group, actor); System.err.println("[panel] 用 " + method.getDeclaringClass().getSimpleName() + "." + method.getName() + " 挂上 " + actor.getClass().getSimpleName()); return; } throw new NoSuchMethodException("找不到接纳 " + actor.getClass().getName() + " 的单参方法"); } /** 全量扫描静态字段(用 Unsafe,不触发类初始化)。留作备用入口。 */ static Object scanStaticNoInit(ClassLoader loader, List paths, String typeName) { try { Object unsafe = unsafe(); Class unsafeClass = unsafe.getClass(); Method base = unsafeClass.getMethod("staticFieldBase", Field.class); Method offset = unsafeClass.getMethod("staticFieldOffset", Field.class); Method getObject = unsafeClass.getMethod("getObject", Object.class, long.class); for (String path : paths) { try (ZipFile zip = new ZipFile(path)) { Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (!name.endsWith(".class")) continue; Class type; try { type = Class.forName(name.substring(0, name.length() - 6).replace('/', '.'), false, loader); } catch (Throwable ignored) { continue; } for (Field field : type.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers())) continue; if (!field.getType().getName().equals(typeName)) continue; try { Object fieldBase = base.invoke(unsafe, field); long fieldOffset = ((Long) offset.invoke(unsafe, field)).longValue(); Object value = getObject.invoke(unsafe, fieldBase, Long.valueOf(fieldOffset)); if (value != null) return value; } catch (Throwable ignored) { // 读不到就跳过 } } } } catch (Throwable ignored) { // 不是 zip } } } catch (Throwable ignored) { // 忽略 } return null; } private static Object unsafe() throws Exception { Class unsafeClass = Class.forName("sun.misc.Unsafe"); Field field = unsafeClass.getDeclaredField("theUnsafe"); field.setAccessible(true); return field.get(null); } /** 把载荷里所有非空的静态字段倒出来(用 Unsafe,不触发类初始化)。 */ static void dumpStatics(ClassLoader loader, List paths) { try { Object unsafe = unsafe(); Class unsafeClass = unsafe.getClass(); Method base = unsafeClass.getMethod("staticFieldBase", Field.class); Method offset = unsafeClass.getMethod("staticFieldOffset", Field.class); Method getObject = unsafeClass.getMethod("getObject", Object.class, long.class); for (String path : paths) { try (ZipFile zip = new ZipFile(path)) { Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (!name.endsWith(".class")) continue; Class type; try { type = Class.forName(name.substring(0, name.length() - 6).replace('/', '.'), false, loader); } catch (Throwable ignored) { continue; } for (Field field : type.getDeclaredFields()) { if (!Modifier.isStatic(field.getModifiers())) continue; if (field.getType().isPrimitive()) continue; try { Object fieldBase = base.invoke(unsafe, field); long fieldOffset = ((Long) offset.invoke(unsafe, field)).longValue(); Object value = getObject.invoke(unsafe, fieldBase, Long.valueOf(fieldOffset)); if (value != null) { System.err.println("[statics] " + type.getName() + "." + field.getName() + " : " + field.getType().getName() + " = " + value.getClass().getName()); } } catch (Throwable ignored) { // 跳过 } } } } catch (Throwable ignored) { // 不是 zip } } } catch (Throwable error) { System.err.println("[statics] 失败: " + error); } } static List paths(java.net.URLClassLoader loader) { List paths = new ArrayList(); for (java.net.URL url : loader.getURLs()) { if (!"file".equals(url.getProtocol())) continue; try { paths.add(new java.io.File(url.toURI()).getAbsolutePath()); } catch (Throwable ignored) { // 跳过 } } return paths; } }