import java.lang.instrument.Instrumentation; import java.lang.reflect.*; import java.net.URLClassLoader; import java.util.*; /** * 热更:把"刚刚创建的附件"整理干净 —— 全部走编辑器自身 API,不重启。 * * 1) 重置图片附件(GO)的变换:GO.D(FFFFF)V 就是编辑器自己的 setTransform, * 字节码里依次写 D=x、B=y、S=rotation、L=scaleX、I=scaleY(见 javap -c GO)。 * 图片附件在 3.8 没有 shear 字段(JSON 写入器 sb_0.java:669-675 也只写 x/y/scaleX/scaleY/rotation/width/height)。 * 2) 删除 skin/占位附件:Hi.I() 返回 "placeholder",实例方法 Gn.L() 内部是 * `slot.skeleton.removeAttachment(this,true,true)`(javap -c Gn 已确认)。 * * 目标对象 = 上一次热更里创建的 20 个插槽(骨骼是 bone6 的子骨骼)。 */ public final class ResetMaker2 { static Instrumentation INST; static ClassLoader L; static Class ARRAY, NODE, GN, GO, HI, GP, GU, GPC; static Method SLOT_CURRENT, SLOT_SETUP, GO_SET, ATTACH_DELETE, REFRESH, UNDO_BEGIN, UNDO_END, UNDO_RECORD; static Field GP_ATTACHMENTS, GP_SLOTS, BONE_PARENT; static Object SKELETON, TREE_VIEW; static Set OUR_SLOTS = Collections.newSetFromMap(new IdentityHashMap()); static int RESET, DELETED, FAILED; 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("[reset2] " + 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 = Class.forName("fI", true, L); NODE = Class.forName("eM", true, L); GN = Class.forName("Gn", true, L); GO = Class.forName("GO", true, L); HI = Class.forName("Hi", true, L); GP = Class.forName("GP", true, L); GU = Class.forName("GU", true, L); GPC = Class.forName("Gp", true, L); Class VZ = Class.forName("VZ", true, L); Class OG = Class.forName("OG", true, L); Class UN = Class.forName("un", 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); // 骨架:树根节点上的对象 TREE_VIEW = globalStatic(OG, VZ.getName()); Object roots = rootNodes(globalStatic(Class.forName("OG", true, L), "Uq")); SKELETON = firstOfType(roots, GP); if (SKELETON == null) { log("拿不到骨架"); return; } GP_ATTACHMENTS = firstArrayField(GP, GN); GP_SLOTS = firstArrayField(GP, GU); BONE_PARENT = firstTypeField(GPC, GPC); SLOT_CURRENT = method(GU, "D", new Class[] {}, GN); SLOT_SETUP = method(GU, "B", new Class[] {}, GN); GO_SET = method(GO, "D", new Class[] { float.class, float.class, float.class, float.class, float.class }, void.class); ATTACH_DELETE = method(GN, "L", new Class[] {}, void.class); REFRESH = method(VZ, "D", new Class[] { GP, boolean.class, boolean.class }, void.class); for (Method m : UN.getDeclaredMethods()) { if (m.getParameterTypes().length == 0 && m.getReturnType() == void.class) { if (m.getName().equals("D")) UNDO_BEGIN = m; if (m.getName().equals("B")) UNDO_END = m; } if (m.getParameterTypes().length == 1 && m.getParameterTypes()[0] == Object.class) UNDO_RECORD = m; } if (GO_SET == null || ATTACH_DELETE == null || SLOT_CURRENT == null) { log("API 没找齐"); return; } if (UNDO_BEGIN != null) UNDO_BEGIN.setAccessible(true); if (UNDO_END != null) UNDO_END.setAccessible(true); if (UNDO_RECORD != null) UNDO_RECORD.setAccessible(true); GO_SET.setAccessible(true); ATTACH_DELETE.setAccessible(true); collectOurSlots(); log("目标插槽 = " + OUR_SLOTS.size() + " 个"); onRender.invoke(null, new Runnable() { @Override public void run() { try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); } } }); log("完成:重置 " + RESET + " 个附件的变换,删除 " + DELETED + " 个占位(skin)附件,失败 " + FAILED); } /** 上一次热更建的 20 个插槽:骨骼是 bone6 的子骨骼。 */ static void collectOurSlots() throws Exception { // 扫描骨架上的所有 fI 字段,找出装着名字叫 bone6 的那个列表(不靠字段顺序猜) Object bone6 = null; for (Class t = GP; 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); Object v = f.get(SKELETON); if (v == null) continue; StringBuilder names = new StringBuilder(); for (int i = 0; i < size(v); i++) { Object o = get(v, i); if (o == null) continue; String n = nameOf(o); if (i < 4) names.append(n).append(','); if (GPC.isInstance(o) && "bone6".equals(n)) bone6 = o; } if (size(v) > 0) log(" 字段 GP." + f.getName() + " size=" + size(v) + " 元素=" + elemClass(v).getSimpleName() + " 前几个名字=" + names); } } if (bone6 == null) { log("找不到 bone6"); return; } for (int i = 0; i < size(slots()); i++) { Object slot = get(slots(), i); Object bone = field(slot, GU, GPC); if (bone != null && field(bone, GPC, GPC) == bone6) OUR_SLOTS.add(slot); } } static Object slots() throws Exception { return GP_SLOTS.get(SKELETON); } static void work() throws Exception { if (UNDO_BEGIN != null) UNDO_BEGIN.invoke(null); Object attachments = GP_ATTACHMENTS.get(SKELETON); Object slots = slots(); log("处理前:附件表 " + size(attachments) + " 项,插槽 " + size(slots) + " 个"); // 1) 重置图片附件变换 for (int i = 0; i < size(slots); i++) { Object slot = get(slots, i); if (!OUR_SLOTS.contains(slot)) continue; Object cur = SLOT_CURRENT.invoke(slot); Object setup = SLOT_SETUP.invoke(slot); if (cur != null && GO.isInstance(cur)) { log(" 重置前 " + nameOf(slot) + ":" + goValues(cur)); if (UNDO_RECORD != null) UNDO_RECORD.invoke(null, cur); GO_SET.invoke(cur, 0f, 0f, 0f, 1f, 1f); log(" 重置后 " + nameOf(slot) + ":" + goValues(cur)); RESET++; } else if (cur != null) { log(" " + nameOf(slot) + " 当前附件不是图片:" + cur.getClass().getName()); } if (setup != null && setup != cur && GO.isInstance(setup)) { if (UNDO_RECORD != null) UNDO_RECORD.invoke(null, setup); GO_SET.invoke(setup, 0f, 0f, 0f, 1f, 1f); RESET++; } } // 2) 删除占位(skin/placeholder)附件:Hi 实例,且它的 slot 属于我们建的那批 for (int i = size(attachments) - 1; i >= 0; i--) { Object a = get(attachments, i); if (a == null || !HI.isInstance(a)) continue; Object slot = field(a, GN, GU); if (slot == null || !OUR_SLOTS.contains(slot)) continue; String slotName = nameOf(slot); Object cur = SLOT_CURRENT.invoke(slot); String curDesc = cur == null ? "null" : cur.getClass().getName(); log(" 删除占位 " + a.getClass().getName() + " (slot=" + slotName + "),该插槽当前附件=" + curDesc); try { ATTACH_DELETE.invoke(a); DELETED++; } catch (Throwable e) { FAILED++; log(" 删除失败:" + e); } Object after = SLOT_CURRENT.invoke(slot); log(" 删除后该插槽当前附件=" + (after == null ? "null" : after.getClass().getName())); } if (UNDO_END != null) UNDO_END.invoke(null); if (REFRESH != null) { REFRESH.setAccessible(true); REFRESH.invoke(TREE_VIEW, SKELETON, Boolean.TRUE, Boolean.TRUE); log("已刷新树节点"); } // 3) 结果核对 Object after = GP_ATTACHMENTS.get(SKELETON); log("处理后:附件表 " + size(after) + " 项"); int withImage = 0, withPlaceholder = 0; for (int i = 0; i < size(after); i++) { Object a = get(after, i); if (a == null) continue; Object slot = field(a, GN, GU); if (slot == null || !OUR_SLOTS.contains(slot)) continue; if (GO.isInstance(a)) withImage++; if (HI.isInstance(a)) withPlaceholder++; } log("核对:属于目标插槽的图片附件 " + withImage + " 个,占位附件 " + withPlaceholder + " 个"); for (int i = 0; i < size(slots); i++) { Object slot = get(slots, i); if (!OUR_SLOTS.contains(slot)) continue; Object cur = SLOT_CURRENT.invoke(slot); log(" " + nameOf(slot) + " 当前附件=" + (cur == null ? "null" : nameOf(cur) + " " + goValues(cur))); } } static String goValues(Object go) { if (go == null) return "null"; String[] names = { "D", "B", "S", "L", "I", "C", "Z" }; String[] keys = { "x", "y", "rotation", "scaleX", "scaleY", "width", "height" }; StringBuilder sb = new StringBuilder(); for (int i = 0; i < keys.length; i++) { try { Field f = GO.getDeclaredField(names[i]); f.setAccessible(true); sb.append(keys[i]).append('=').append(f.getFloat(go)).append(' '); } catch (Throwable t) { sb.append(keys[i]).append("=? "); } } return sb.toString(); } static Field firstArrayField(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 f; } catch (Throwable ignored) { } } } return null; } static Field firstTypeField(Class owner, Class type) { for (Class t = owner; 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); return f; } } 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; } }