import java.lang.instrument.Instrumentation; import java.lang.reflect.*; /** * 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。 * 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。 * * 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP): * new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null) * → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true) */ public final class BoneMaker { public static void agentmain(String args, Instrumentation inst) { int count = 10; try { if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim()); } catch (Throwable ignored) { // 默认 10 } try { run(inst, count); } catch (Throwable error) { System.err.println("[live] BoneMaker 失败: " + error); error.printStackTrace(System.err); } } private static void run(Instrumentation inst, int count) throws Exception { // 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的) // 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)), // 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。 ClassLoader loader = null; for (Class c : inst.getAllLoadedClasses()) { ClassLoader cl = c.getClassLoader(); if (cl instanceof java.net.URLClassLoader) { loader = cl; System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName()); break; } } if (loader == null) { System.err.println("[live] 找不到编辑器类加载器"); return; } System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName()); // 2) 当前选中对象(复用面板里已有的读取逻辑) Class sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader); Object parent = sceneObjects.getMethod("selected", ClassLoader.class) .invoke(null, loader); if (parent == null) { System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)"); return; } Object skeleton = sceneObjects.getMethod("skeletonOf", Object.class).invoke(null, parent); System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class) .invoke(null, parent) + ",骨架 = " + skeleton); // 3) 用编辑器自身 API 建 N 根 Class boneClass = Class.forName("Gp", true, loader); Class skeletonClass = Class.forName("GP", true, loader); Class arrayClass = Class.forName("fI", true, loader); Class slotClass = Class.forName("GU", true, loader); Class og = Class.forName("OG", true, loader); Class colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader); Class treeViewClass = Class.forName("VZ", true, loader); Constructor boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass); Method addBone = null; for (Method m : skeletonClass.getMethods()) { if (m.getName().equals("D") && m.getParameterTypes().length == 1 && m.getParameterTypes()[0] == boneClass) { addBone = m; break; } } Method nameGen = null; for (Method m : og.getMethods()) { Class[] t = m.getParameterTypes(); if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass && m.getReturnType() == String.class) { nameGen = m; break; } } Method refresh = null; for (Method m : treeViewClass.getMethods()) { Class[] t = m.getParameterTypes(); if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class && t[2] == boolean.class) { refresh = m; break; } } Object editor = null; for (Field f : og.getDeclaredFields()) { if (!Modifier.isStatic(f.getModifiers())) continue; if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue; f.setAccessible(true); editor = f.get(null); if (editor != null) break; } Object treeView = null; for (Field f : og.getDeclaredFields()) { if (!Modifier.isStatic(f.getModifiers())) continue; if (f.getType() != treeViewClass) continue; f.setAccessible(true); treeView = f.get(null); if (treeView != null) break; } Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class) .newInstance(1f, 0f, 0f, 1f); if (addBone == null || nameGen == null || treeView == null) { System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen + " treeView=" + treeView); return; } addBone.setAccessible(true); nameGen.setAccessible(true); // skeleton 的骨骼数组字段(fI) Object bones = null; for (Field f : skeletonClass.getDeclaredFields()) { if (f.getType() == arrayClass) { f.setAccessible(true); bones = f.get(skeleton); break; } } // 父骨骼的名字(String 字段) Object parentName = null; for (Field f : boneClass.getDeclaredFields()) { if (f.getType() == String.class) { f.setAccessible(true); parentName = f.get(parent); break; } } // 4) 撤销包裹(un.D()/un.B())—— 整批算一次 Class undo = Class.forName("un", true, loader); Method begin = null, 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); } int made = 0; for (int i = 0; i < count; i++) { Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父) Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成 setString(bone, boneClass, generated); // name addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y setField(bone, "B", colorClass, red); // color made++; } if (commit != null) { commit.setAccessible(true); commit.invoke(null); } if (refresh != null && editor != null) { refresh.setAccessible(true); refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次 System.err.println("[live] 已刷新树节点"); } System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼"); } private static void setField(Object target, String name, Class type, Object value) throws Exception { for (Class c = target.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (!f.getName().equals(name) || f.getType() != type) continue; f.setAccessible(true); f.set(target, value); return; } } } private static void setFloat(Object target, String name, float value) throws Exception { setField(target, name, float.class, Float.valueOf(value)); } private static void setString(Object target, Class boneClass, Object value) throws Exception { setField(target, "D", String.class, value); } }