import java.lang.instrument.Instrumentation; import java.lang.reflect.*; import java.net.URLClassLoader; import java.util.*; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; /** * 第三版(点按钮没反应的原因已查明并修掉): * 1) 弹窗:编辑器自己的 `xl`(标题/正文/名字框/OK/Cancel)× ASM 生成的子类; * ⚠️ 实测:只调 `xl.D(dt)` 不会把窗口挂进 Stage(parent 一直 null,屏幕上什么都不出现), * 必须**显式 Stage.addActor(dialog)**(`dt.D(Ldl;)V`,字节码就是 root.addActor), * 再自己摆位置。中文在编辑器字体里是乱码,所以文字一律用英文。 * 2) 清掉上一版/实验残留的对话框(类名 com.spineview.live.AttNamePrompt 的那些)。 * 3) 名字捕获改成**每个对话框实例自己的字段**(不再用共享 static,避免多个等待线程互相串台)。 * 4) 建对象部分与第二版相同(已干跑验证):当前选中骨骼下 15 根骨骼 + 1 插槽 + 1 附件。 */ public final class AddButton10 { static Instrumentation INST; static ClassLoader L; static Class ARRAY, BONE, SLOT, ATT, GO, HI, SKELETON_C, UH_C, COLOR_C, IZ_C, XL, STAGE_C, ACTOR_C, GROUP_C, CELL_C, DIALOG_C, eX; static Object UH_INSTANCE, EDITOR, TREE_VIEW, STAGE, IZ_NORMAL, IZ_ADDITIVE; static Method ADD_SLOT, ADD_ATT, ADD_BONE, NAME_GEN, REFRESH, REFRESH2, UNDO_BEGIN, UNDO_COMMIT, ON_RENDER, ADD_ACTOR, SHOW; static Field DIALOG_PARENT_FIELD, DIALOG_NAME_FIELD, DIALOG_DONE_FIELD, DIALOG_STAGE_FIELD; static final int COUNT = 15; public static void agentmain(String args, Instrumentation inst) { INST = inst; try { run(); } catch (Throwable error) { log("失败: " + error); error.printStackTrace(System.err); } } static void log(String m) { System.err.println("[btn10] " + m); } static void run() throws Exception { for (Class c : INST.getAllLoadedClasses()) { if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; } } if (L == null) { log("找不到载荷类加载器"); return; } ARRAY = forName("fI"); BONE = forName("Gp"); SLOT = forName("GU"); ATT = forName("Gn"); GO = forName("GO"); HI = forName("Hi"); SKELETON_C = forName("GP"); UH_C = forName("UH"); COLOR_C = forName("com.badlogic.gdx.graphics.Color"); IZ_C = forName("iz"); XL = forName("xl"); STAGE_C = forName("dt"); ACTOR_C = forName("dl"); GROUP_C = forName("do"); CELL_C = forName("dR"); eX = forName("eX"); Class og = forName("OG"); Class undo = forName("un"); Class vz = forName("VZ"); UH_INSTANCE = staticFieldOfType(og, UH_C); EDITOR = staticFieldOfType(og, forName("com.esotericsoftware.spine.editor.Editor")); TREE_VIEW = staticFieldOfType(og, vz); STAGE = staticFieldOfType(og, STAGE_C); ADD_SLOT = find(UH_C, "D", new Class[] {SLOT, BONE, boolean.class, ARRAY}, SLOT); ADD_ATT = find(UH_C, "D", new Class[] {ATT, SLOT, boolean.class, boolean.class, ARRAY}, ATT); ADD_BONE = find(SKELETON_C, "D", new Class[] {BONE}, void.class); NAME_GEN = find(og, "D", new Class[] {String.class, ARRAY, SLOT}, String.class); REFRESH = find(vz, "D", new Class[] {SKELETON_C, boolean.class, boolean.class}, void.class); if (REFRESH == null && EDITOR != null) { REFRESH2 = find(vz, "D", new Class[] {EDITOR.getClass(), SKELETON_C, boolean.class, boolean.class}, void.class); } for (Method m : undo.getDeclaredMethods()) { if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue; if (m.getName().equals("D")) UNDO_BEGIN = m; if (m.getName().equals("B")) UNDO_COMMIT = m; } for (Method m : og.getDeclaredMethods()) { Class[] t = m.getParameterTypes(); if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) ON_RENDER = m; } ON_RENDER.setAccessible(true); // ⚠️ 实测:`STAGE_C.getMethod("D", ACTOR_C)` 拿到的那个调用后 parent 永远是 null, // 而"扫描 stage.getClass().getMethods() 里 1 个 Actor 参数 + void"的这个是真能挂上的。 for (Method m : STAGE.getClass().getMethods()) { Class[] t = m.getParameterTypes(); if (t.length != 1) continue; if (!ACTOR_C.isAssignableFrom(t[0])) continue; if (m.getReturnType() != void.class) continue; ADD_ACTOR = m; ADD_ACTOR.setAccessible(true); } log("addActor = " + ADD_ACTOR); for (Method m : XL.getMethods()) { Class[] t = m.getParameterTypes(); if (t.length == 1 && t[0] == STAGE_C) SHOW = m; } IZ_NORMAL = IZ_C.getField("D").get(null); IZ_ADDITIVE = IZ_C.getField("B").get(null); DIALOG_C = defineDialogClass(); DIALOG_NAME_FIELD = DIALOG_C.getField("NAME"); DIALOG_DONE_FIELD = DIALOG_C.getField("DONE"); DIALOG_PARENT_FIELD = parentFieldOf(ACTOR_C); DIALOG_STAGE_FIELD = stageFieldOf(ACTOR_C); if (ADD_SLOT == null || ADD_ATT == null || ADD_BONE == null || NAME_GEN == null || UNDO_BEGIN == null || UNDO_COMMIT == null) { log("API 没找齐,停"); return; } onRender(new Runnable() { @Override public void run() { cleanupOrphans(); } }); installButton(); } /** * 清掉前几版留下的对话框(含还留在屏幕上的实验弹窗)。 * 遍历方式照抄面板自己的 fillDialog:从 Stage 出发按**字段**广搜(按 actor 树走会漏,实测过)。 */ static void cleanupOrphans() { try { Class actorC = ACTOR_C; Method remove = null; for (Method m : actorC.getMethods()) { if (m.getName().equals("b_") && m.getParameterTypes().length == 0 && m.getReturnType() == boolean.class) remove = m; } remove.setAccessible(true); Deque queue = new ArrayDeque(); Set seen = Collections.newSetFromMap(new IdentityHashMap()); queue.add(STAGE); int visited = 0, removed = 0; while (!queue.isEmpty() && visited < 200000) { Object a = queue.poll(); if (a == null || !seen.add(a)) continue; visited++; String cn = a.getClass().getName(); if (cn.startsWith("com.spineview.live.")) { try { remove.invoke(a); removed++; } catch (Throwable ignored) { } continue; } try { if (cn.startsWith("java.") || cn.startsWith("javax.") || cn.startsWith("sun.")) continue; if (a instanceof Class || a instanceof String || a instanceof Number) continue; if (a.getClass().isArray()) { if (!a.getClass().getComponentType().isPrimitive()) { Object[] arr = (Object[]) a; for (Object item : arr) if (item != null) queue.add(item); } continue; } for (Class c = a.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || f.getType().isPrimitive()) continue; try { f.setAccessible(true); Object v = f.get(a); if (v != null) queue.add(v); } catch (Throwable ignored) { } } } } catch (Throwable t) { log("遍历 " + cn + " 出错: " + t); } } log("清理旧对话框 " + removed + " 个(遍历 " + visited + " 个对象)"); } catch (Throwable e) { log("清理旧对话框失败: " + e); } } /** * 删掉上一版跑出来那批骨骼。 * ⚠️ 判据必须精确,上一版我把"插槽有两个 Gn 字段"当判据,结果误伤了你原有的骨骼(已撤销恢复)。 * 这次的判据:**骨架附件表 GP.L 里,同一个插槽被登记了 >=2 个附件** —— 只有我上一版那批是这样 * (ADD_SLOT 带了模板插槽的附件 + 我又加了一个),原有插槽每个只有 1 个。 */ static void cleanupLastBatch() { try { java.util.List all = (java.util.List) Class.forName("com.spineview.SceneObjects", true, L) .getMethod("listAllNodes", ClassLoader.class).invoke(null, L); java.util.List bones = new ArrayList(); for (Object o : all) if (o != null && BONE.isInstance(o)) bones.add(o); if (bones.isEmpty()) return; Object skeleton = firstFieldOfType(BONE, bones.get(0), SKELETON_C); if (skeleton == null) return; Object slotsArr = arrayOfType(SKELETON_C, skeleton, SLOT); Object attsArr = arrayOfType(SKELETON_C, skeleton, ATT); // 统计每个插槽登记了几个附件 Map perSlot = new IdentityHashMap(); for (int i = 0; i < size(attsArr); i++) { Object a = get(attsArr, i); if (a == null) continue; Object slot = firstFieldOfType(ATT, a, SLOT); if (slot == null) continue; Integer c = perSlot.get(slot); perSlot.put(slot, Integer.valueOf(c == null ? 1 : c.intValue() + 1)); } Method del = Class.forName("com.spineview.SceneActions", true, L) .getMethod("delete", ClassLoader.class, Object.class); del.setAccessible(true); int killed = 0; Set killedSet = Collections.newSetFromMap(new IdentityHashMap()); for (Map.Entry e : perSlot.entrySet()) { if (e.getValue().intValue() < 2) continue; // 正常插槽只有 1 个附件 Object bone = firstFieldOfType(SLOT, e.getKey(), BONE); if (bone == null || !killedSet.add(bone)) continue; del.invoke(null, L, bone); killed++; } log("清理上一版残留骨骼 " + killed + " 根(判据:同插槽登记了 2 个以上附件)"); } catch (Throwable e) { log("清理上一版失败: " + e); } } static void installButton() throws Exception { Class panel = Class.forName("com.spineview.ObjectPanel"); Field lastF = panel.getDeclaredField("LAST_STATE"); lastF.setAccessible(true); Object state = lastF.get(null); if (state == null) { log("面板还没建"); return; } Object buttons = stateField(state, "buttons"); Object skin = stateField(state, "skin"); // 前几版一共往按钮区塞了 3 个按钮:这里把"第 11 个 TextButton 之后"的全摘掉,只留原来的 10 个 // ⚠️ Actor.remove() 是 `b_()Z`;`eX.b_()V` 是 invalidate()。按名字随便取会拿到 void 那个 → 删不掉。 Method remove = null; for (Method m : ACTOR_C.getMethods()) { if (m.getName().equals("b_") && m.getParameterTypes().length == 0 && m.getReturnType() == boolean.class) remove = m; } remove.setAccessible(true); Object kids = childrenOf(buttons); int removedOld = 0; for (int i = size(kids) - 1; i >= 0; i--) { Object child = get(kids, i); if (child == null || !child.getClass().getName().contains("TextButton")) continue; if (seenButtonsFromEnd(kids, i) > 10) { remove.invoke(child); removedOld++; } } log("移除前几版按钮 " + removedOld + " 个"); Class textButton = forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton"); Class skinClass = forName("com.badlogic.gdx.scenes.scene2d.ui.Skin"); Object button = textButton.getConstructor(String.class, skinClass) .newInstance("New 15 Bone+Slot+Att", skin); Method add = null, row = null, left = null; for (Method m : buttons.getClass().getMethods()) { Class[] t = m.getParameterTypes(); if (m.getName().equals("D") && t.length == 1 && t[0] == ACTOR_C && m.getReturnType() == CELL_C) add = m; if (m.getName().equals("I") && t.length == 0 && m.getReturnType() == CELL_C) row = m; } for (Method m : CELL_C.getMethods()) { if (m.getName().equals("C") && m.getParameterTypes().length == 0 && m.getReturnType() == CELL_C) left = m; } if (add == null) { log("找不到 Table.add(Actor)Cell"); return; } add.setAccessible(true); Object cell = add.invoke(buttons, button); if (left != null) { left.setAccessible(true); left.invoke(cell); } if (row != null) { row.setAccessible(true); row.invoke(buttons); } for (Method m : buttons.getClass().getMethods()) { if (m.getName().equals("b_") && m.getParameterTypes().length == 0 && m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(buttons); break; } } Method wire = panel.getDeclaredMethod("wire", ClassLoader.class, Object.class, Runnable.class); wire.setAccessible(true); wire.invoke(null, L, button, new Runnable() { @Override public void run() { Thread t = new Thread(new Runnable() { @Override public void run() { try { promptAndCreate(); } catch (Throwable e) { log("流程失败: " + e); e.printStackTrace(System.err); } } }, "spineview-prompt15-v3"); t.setDaemon(true); t.start(); } }); log("按钮已装好(第 " + size(childrenOf(buttons)) + " 行)并接上点击"); Thread verify = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(300L); Class.forName("AddButton10").getDeclaredMethod("promptAndCreate").invoke(null); } catch (Throwable e) { log("验证弹窗失败: " + e); } } }, "spineview-prompt15-verify"); verify.setDaemon(true); verify.start(); log("已触发一次弹窗验证(居中效果;Cancel 不会有任何改动)"); } /** 从前往后数,这个下标是第几个 TextButton(1 起) */ static int seenButtonsFromEnd(Object kids, int index) { int n = 0; for (int i = 0; i <= index; i++) { Object c = get(kids, i); if (c != null && c.getClass().getName().contains("TextButton")) n++; } return n; } static Object stateField(Object state, String name) throws Exception { Field f = state.getClass().getDeclaredField(name); f.setAccessible(true); return f.get(state); } /** 弹窗 → 等名字 → 建对象 */ static void promptAndCreate() throws Exception { final Object[] holder = new Object[1]; onRender(new Runnable() { @Override public void run() { try { holder[0] = showPrompt(); } catch (Throwable e) { log("弹面板失败: " + e); e.printStackTrace(System.err); } } }); Object dialog = holder[0]; if (dialog == null) { log("弹窗没建出来 → 结束"); return; } log("弹窗已显示:parent=" + (parentOf(dialog) != null) + " 位置=" + geom(dialog) + " Stage=" + fl(STAGE, "D") + "x" + fl(STAGE, "B") + " 在 Stage 孩子里=" + (indexOf(childrenOf(rootGroup()), dialog) >= 0)); String name = waitForName(dialog, 300000L); if (name == null || name.trim().isEmpty()) { log("没拿到名字(取消/超时)→ 结束"); removeQuietly(dialog); return; } final String finalName = name.trim(); log("输入的名字 = " + finalName); removeQuietly(dialog); onRender(new Runnable() { @Override public void run() { try { create15(finalName); } catch (Throwable e) { log("建对象失败: " + e); e.printStackTrace(System.err); } } }); } static Object showPrompt() throws Exception { Object dialog = DIALOG_C.getConstructor(String.class, String.class) .newInstance("New Bones + Slots + Attachment", "Enter an existing attachment name in this skeleton:"); // ① 先挂进 Stage(实测这一步必须用扫描出来的那个 addActor) ADD_ACTOR.invoke(STAGE, dialog); // ② 再走编辑器自己的显示流程(它会把键盘焦点给输入框) try { SHOW.invoke(dialog, STAGE); } catch (Throwable e) { log("xl.D(dt) 失败: " + e); } if (parentOf(dialog) == null) ADD_ACTOR.invoke(STAGE, dialog); // ③ 尺寸:默认窗口太小,正文/按钮会被裁。用编辑器自己的 pref 尺寸(拿不到就给下限) float prefW = fl(dialog, "StackMapTable"); // WidgetGroup.getPrefWidth float prefH = fl(dialog, "init"); // WidgetGroup.getPrefHeight float minW = fl(dialog, "LineNumberTable"); // getMinWidth float minH = fl(dialog, "String"); // getMinHeight float w = Math.max(Math.max(prefW, minW), 360f); float h = Math.max(Math.max(prefH, minH), 170f); setFloatField(dialog, "L", w); // Actor.width (dl.L:F) setFloatField(dialog, "I", h); // Actor.height (dl.I:F) for (Method m : eX.getMethods()) { // invalidate() 让内容按新尺寸重新布局 if (m.getName().equals("b_") && m.getParameterTypes().length == 0 && m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(dialog); break; } } log("窗口尺寸: pref=" + prefW + "x" + prefH + " min=" + minW + "x" + minH + " -> 设为 " + w + "x" + h); // ④ 立刻摆到正中心(同一次回调里做完,不会再从左上角/左下角闪一下) center(dialog); return dialog; } /** 等用户按 OK / Cancel:OK 时 L(String) 已把名字写进 NAME;Cancel 时 NAME 仍是 null */ static String waitForName(Object dialog, long timeoutMs) throws Exception { long end = System.currentTimeMillis() + timeoutMs; while (System.currentTimeMillis() < end) { Object done = DIALOG_DONE_FIELD.get(dialog); Object name = DIALOG_NAME_FIELD.get(dialog); if (name != null) return (String) name; if (Boolean.TRUE.equals(done)) return null; // 点了 Cancel if (stageField(dialog) == null) return null; // 窗口被关掉/移除 Thread.sleep(120L); } return null; } /** 读对话框自己的 stage 字段(dl.D:Ldt; = Actor.stage;remove() 之后会变 null) */ static Object stageField(Object actor) { if (DIALOG_STAGE_FIELD == null) return STAGE; try { return DIALOG_STAGE_FIELD.get(actor); } catch (Throwable e) { return STAGE; } } /** 按窗口真实尺寸把对话框摆到正中央(用 Actor.setPosition(float,float,int) 方法,不猜字段名) */ static void center(Object dialog) { try { float w = fl(dialog, "L"), h = fl(dialog, "I"); if (w <= 0f || w > 3000f) w = 300f; if (h <= 0f || h > 3000f) h = 180f; // setPosition(x,y,align) 在 align=0 时是"以 (x,y) 为中心",所以直接给 Stage 中心 float cx = fl(STAGE, "D") / 2f; float cy = fl(STAGE, "B") / 2f; Method setPos = null; for (Method m : ACTOR_C.getMethods()) { Class[] t = m.getParameterTypes(); if (t.length == 3 && t[0] == float.class && t[1] == float.class && t[2] == int.class) setPos = m; } if (setPos == null) { for (Method m : ACTOR_C.getMethods()) { Class[] t = m.getParameterTypes(); if (t.length == 2 && t[0] == float.class && t[1] == float.class && m.getReturnType() == void.class) setPos = m; } } if (setPos == null) { log("找不到 setPosition"); return; } setPos.setAccessible(true); if (setPos.getParameterTypes().length == 3) { setPos.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy), Integer.valueOf(0)); } else { setPos.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy)); } log("已居中(" + setPos.getName() + "):窗口 " + w + "x" + h + " -> (" + cx + "," + cy + ")" + " 实际位置=" + geom(dialog)); } catch (Throwable e) { log("居中失败: " + e); } } static Object rootGroup() { return firstFieldOfType(STAGE_C, STAGE, GROUP_C); } static void removeQuietly(Object dialog) { try { Method rm = dialog.getClass().getMethod("b_"); rm.setAccessible(true); rm.invoke(dialog); } catch (Throwable ignored) { } } /** 生成 xl 子类:OK 时把名字写进**自己的** NAME 字段并关窗 */ static Class defineDialogClass() throws Exception { final String internal = "com/spineview/live/AttPrompt8"; ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "xl", null); cw.visitField(Opcodes.ACC_PUBLIC, "NAME", "Ljava/lang/String;", null, null).visitEnd(); cw.visitField(Opcodes.ACC_PUBLIC, "DONE", "Z", null, null).visitEnd(); MethodVisitor c = cw.visitMethod(Opcodes.ACC_PUBLIC, "", "(Ljava/lang/String;Ljava/lang/String;)V", null, null); c.visitCode(); c.visitVarInsn(Opcodes.ALOAD, 0); c.visitVarInsn(Opcodes.ALOAD, 1); c.visitVarInsn(Opcodes.ALOAD, 2); c.visitInsn(Opcodes.ACONST_NULL); c.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "", "(Ljava/lang/String;Ljava/lang/String;LGe;)V", false); c.visitInsn(Opcodes.RETURN); c.visitMaxs(0, 0); c.visitEnd(); MethodVisitor h = cw.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/String;)V", null, null); h.visitCode(); h.visitVarInsn(Opcodes.ALOAD, 0); h.visitVarInsn(Opcodes.ALOAD, 1); h.visitFieldInsn(Opcodes.PUTFIELD, internal, "NAME", "Ljava/lang/String;"); h.visitVarInsn(Opcodes.ALOAD, 0); h.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "dl", "b_", "()Z", false); h.visitInsn(Opcodes.POP); h.visitInsn(Opcodes.RETURN); h.visitMaxs(0, 0); h.visitEnd(); // 覆盖 xl.L(Object)(OK 就是 Boolean.TRUE、Cancel 就是 FALSE):不管哪种,跑完就置 DONE MethodVisitor d = cw.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/Object;)V", null, null); d.visitCode(); d.visitVarInsn(Opcodes.ALOAD, 0); d.visitVarInsn(Opcodes.ALOAD, 1); d.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "L", "(Ljava/lang/Object;)V", false); d.visitVarInsn(Opcodes.ALOAD, 0); d.visitInsn(Opcodes.ICONST_1); d.visitFieldInsn(Opcodes.PUTFIELD, internal, "DONE", "Z"); d.visitInsn(Opcodes.RETURN); d.visitMaxs(0, 0); d.visitEnd(); cw.visitEnd(); final byte[] bytes = cw.toByteArray(); ClassLoader child = new ClassLoader(L) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals("com.spineview.live.AttPrompt8")) { Class found = findLoadedClass(name); if (found == null) found = defineClass(name, bytes, 0, bytes.length); if (resolve) resolveClass(found); return found; } return super.loadClass(name, resolve); } }; return Class.forName("com.spineview.live.AttPrompt8", true, child); } // ==================== 建 15 根骨骼 + 插槽 + 附件 ==================== static void create15(String name) throws Exception { Object selected = Class.forName("com.spineview.SceneObjects", true, L) .getMethod("selected", ClassLoader.class).invoke(null, L); if (selected == null) { log("当前没有选中对象 → 结束"); return; } if (!BONE.isInstance(selected)) { log("当前选中的不是骨骼(" + selected.getClass().getName() + ")→ 结束"); return; } Object parentBone = selected; Object skeleton = firstFieldOfType(BONE, parentBone, SKELETON_C); Object bones = arrayOfType(SKELETON_C, skeleton, BONE); Object slots = arrayOfType(SKELETON_C, skeleton, SLOT); Object attachments = arrayOfType(SKELETON_C, skeleton, ATT); if (bones == null || slots == null || attachments == null) { log("骨架数组没找齐 → 结束"); return; } Object templateSlot = size(slots) > 0 ? get(slots, 0) : null; Object template = null; for (int i = 0; i < size(attachments); i++) { Object a = get(attachments, i); if (a == null || !ATT.isInstance(a)) continue; if (!name.equals(nameOf(a))) continue; if (!HI.isInstance(a)) { template = a; break; } if (template == null) template = a; } if (template == null) { log("骨架里没有名为 \"" + name + "\" 的附件 → 结束"); return; } log("模板附件 = " + nameOf(template) + " (" + template.getClass().getName() + ") 模板插槽 = " + nameOf(templateSlot)); Random random = new Random(); Object empty = ARRAY.getConstructor().newInstance(); UNDO_BEGIN.setAccessible(true); ADD_BONE.setAccessible(true); UNDO_BEGIN.invoke(null); Constructor boneCtor = BONE.getConstructor(BONE, SKELETON_C, BONE); int made = 0, replaced = 0; for (int i = 0; i < COUNT; i++) { Object bone = boneCtor.newInstance(parentBone, skeleton, parentBone); set(bone, "D", String.class, NAME_GEN.invoke(null, "bone", bones, null)); setFloatField(bone, "Z", random.nextFloat() * 500f); // x setFloatField(bone, "V", random.nextFloat() * 500f); // y setFloatField(bone, "F", 0f); // rotation setFloatField(bone, "Code", 1f); // scaleX setFloatField(bone, "SourceFile", 1f); // scaleY setFloatField(bone, "StackMapTable", 0f); // shearX setFloatField(bone, "LineNumberTable", 0f); // shearY ADD_BONE.invoke(skeleton, bone); Object out = ARRAY.getConstructor().newInstance(); Object slot = ADD_SLOT.invoke(UH_INSTANCE, templateSlot, bone, Boolean.FALSE, out); if (slot == null) { log("第 " + i + " 个插槽没建出来"); continue; } // ⚠️ ADD_SLOT 会把"模板插槽"整个复制一份 —— 包括模板插槽自己挂着的那个附件 // (实测:新插槽上挂的是 mega_dg4 的副本,而不是我们要的"输入名字"的附件) Object inherited = readSlotAttachment(slot); float alpha = 0.5f + random.nextFloat() * 0.5f; Object color = COLOR_C.getConstructor(float.class, float.class, float.class, float.class) .newInstance(1f, 1f, 1f, alpha); set(slot, "B", COLOR_C, color); set(slot, "D", COLOR_C, color); set(slot, "D", IZ_C, random.nextBoolean() ? IZ_NORMAL : IZ_ADDITIVE); Object att = ADD_ATT.invoke(UH_INSTANCE, template, slot, Boolean.FALSE, Boolean.FALSE, empty); if (att == null) { log("第 " + i + " 个附件没建出来"); continue; } setFloatField(att, "D", 0f); setFloatField(att, "B", 0f); setFloatField(att, "S", 0f); setFloatField(att, "L", 1f); setFloatField(att, "I", 1f); String before = nameOf(att); if (!name.equals(before)) { set(att, "D", String.class, name); log(" 第 " + i + " 个附件名被编辑器改成过 \"" + before + "\",已改回"); } // 把插槽的当前附件换成**我们这份**(两个字段都写:setup=B / animate=D) set(slot, "B", ATT, att); set(slot, "D", ATT, att); // 删掉模板插槽带来的那份(保证"一根骨骼 = 1 插槽 + 1 附件") if (inherited != null && inherited != att) { deleteAttachment(inherited); replaced++; } made++; } UNDO_COMMIT.setAccessible(true); UNDO_COMMIT.invoke(null); if (REFRESH != null) { REFRESH.setAccessible(true); REFRESH.invoke(TREE_VIEW, skeleton, Boolean.TRUE, Boolean.TRUE); } else if (REFRESH2 != null) { REFRESH2.setAccessible(true); REFRESH2.invoke(TREE_VIEW, EDITOR, skeleton, Boolean.TRUE, Boolean.TRUE); } log("完成:在 " + nameOf(parentBone) + " 下建了 " + made + " 根骨骼;每根 1 插槽 1 附件 \"" + name + "\"(替换掉模板附件 " + replaced + " 个)"); } // ==================== 工具 ==================== /** 读插槽当前挂的附件(GU 上两个 Gn 字段随便哪个都指向同一份) */ static Object readSlotAttachment(Object slot) { for (Class c = slot.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || f.getType() != ATT) continue; f.setAccessible(true); try { Object v = f.get(slot); if (v != null) return v; } catch (Throwable ignored) { } } } return null; } /** Gn.L()V = 编辑器自己的"删掉这个附件"(字节码:skeleton.removeAttachment(this,true,true)) */ static void deleteAttachment(Object att) { try { Method del = find(ATT, "L", new Class[0], void.class); del.setAccessible(true); del.invoke(att); } catch (Throwable e) { log("删除模板附件失败: " + e); } } static void onRender(Runnable body) throws Exception { ON_RENDER.invoke(null, body); } static Class forName(String n) throws ClassNotFoundException { return Class.forName(n, true, L); } static String nameOf(Object o) { try { return (String) Class.forName("com.spineview.SceneObjects", true, L) .getMethod("nameOf", Object.class).invoke(null, o); } catch (Throwable t) { return "?"; } } static Field parentFieldOf(Class actorClass) { for (Field f : actorClass.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; if (f.getType() == GROUP_C) { f.setAccessible(true); return f; } } return null; } static Field stageFieldOf(Class actorClass) { for (Field f : actorClass.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; if (f.getType() == STAGE_C) { f.setAccessible(true); return f; } } return null; } static Object parentOf(Object actor) { try { return DIALOG_PARENT_FIELD == null ? null : DIALOG_PARENT_FIELD.get(actor); } catch (Throwable e) { return null; } } static Object staticFieldOfType(Class owner, Class type) { for (Field f : owner.getDeclaredFields()) { if (!Modifier.isStatic(f.getModifiers())) continue; if (f.getType() != type) continue; f.setAccessible(true); try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { } } return null; } static Object firstFieldOfType(Class owner, Object target, Class type) { if (target == null) return null; for (Class c = owner; c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue; f.setAccessible(true); try { Object v = f.get(target); if (v != null) return v; } catch (Throwable ignored) { } } } return null; } static Object arrayOfType(Class owner, Object target, Class elem) { for (Class c = owner; c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; if (!ARRAY.isAssignableFrom(f.getType())) continue; f.setAccessible(true); Object v; try { v = f.get(target); } catch (Throwable ignored) { continue; } if (v == null) continue; Class et = elemClass(v); if (et != null && elem.isAssignableFrom(et)) return v; } } 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 Object childrenOf(Object group) { if (group == null) return null; for (Field f : GROUP_C.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) continue; if (!ARRAY.isAssignableFrom(f.getType())) continue; f.setAccessible(true); try { return f.get(group); } catch (Throwable ignored) { } } return null; } static int indexOf(Object array, Object target) { for (int i = 0; i < size(array); i++) if (get(array, i) == target) return i; return -1; } static void set(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; } } } static void setFloatField(Object target, String name, float value) { try { set(target, name, float.class, Float.valueOf(value)); } catch (Throwable ignored) { // 该类型没这个字段就跳过 } } static Method find(Class owner, String name, Class[] params, Class ret) { for (Class c = owner; c != null && c != Object.class; c = c.getSuperclass()) { for (Method m : c.getDeclaredMethods()) { if (!m.getName().equals(name)) continue; if (!Arrays.equals(m.getParameterTypes(), params)) continue; if (ret != null && m.getReturnType() != ret) continue; m.setAccessible(true); return m; } } 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 c = ARRAY; c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.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 String geom(Object actor) { return "[x=" + fl(actor, "c_") + " y=" + fl(actor, "d_") + " w=" + fl(actor, "L") + " h=" + fl(actor, "I") + "]"; } }