- 确认 GO 的字段/入口:x=D(float)、y=B(float)、rotation=S、scaleX=L、scaleY=I、width=C、height=Z; GO.D(FFFFF)V 就是编辑器自己的 setTransform(x,y,rotation,scaleX,scaleY)。 依据:载荷 JSON 写入器 sb_0.java:669-675 + javap -c GO(两条独立证据), width/height 与 mega_dg4.png 真实像素 157x154 对上。 - 确认删除入口:Gn.L()V => skeleton.removeAttachment(this,true,true);GP.L=附件表、GP.Z=插槽表; 占位附件是载荷类 Hi(hi_2.java 的 I() 返回 "placeholder")。 - 实机(JDK8 attach,进程 56177 全程没重启):20 个新建插槽的图片附件 x/y/rotation 归 0、scale 归 1; 20 个 Hi 占位附件删除;骨架附件表 43 -> 23;逐个核对删除占位没连带删掉图片附件。 - 顺带把 tools/live(attach 热更工具源码)入库,jar/build 加进 .gitignore。
344 lines
14 KiB
Java
344 lines
14 KiB
Java
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<Object> OUR_SLOTS = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||
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;
|
||
}
|
||
}
|