Files
Spine_lab/tools/live/src/ResetMaker2.java
T
tianmo 7f519edc67 热更(不重启)整理新建附件:用编辑器自身 API 重置图片附件变换 + 删除占位(skin)附件
- 确认 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。
2026-10-16 18:26:40 +08:00

344 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}