热更(不重启)整理新建附件:用编辑器自身 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。
This commit is contained in:
tianmo committed 2026-10-16 18:26:40 +08:00
1 parent 0886ba647c
commit 7f519edc67
22 files changed
+3780

No files matched your search

+232
View File
@@ -0,0 +1,232 @@
import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
import java.net.URLClassLoader;
import java.util.*;
/**
* 只读探针 3:把骨架里所有 GO(图片附件)连同它的插槽、变换值列出;
* 再把皮肤 "animation" 的每个条目列出。目的是看清"多出来的那批附件"是什么。
*/
public final class Probe5 {
static Instrumentation INST;
static ClassLoader L;
static Class<?> ARRAY, NODE, GN;
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("[probe5] " + 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);
Class<?> GP = Class.forName("GP", true, L);
Class<?> GU = Class.forName("GU", true, L);
Class<?> GO = Class.forName("GO", true, L);
Class<?> GPC = Class.forName("Gp", true, L);
Class<?> GL = Class.forName("Gl", true, L);
Object roots = rootNodes(global("Uq"));
Object skeleton = firstOfType(roots, GP);
if (skeleton == null) { log("拿不到骨架"); return; }
log("骨架 = " + nameOf(skeleton));
// 插槽
Object slots = arrFieldOfType(GP, skeleton, GU);
for (int i = 0; i < size(slots); i++) {
Object s = get(slots, i);
log(" slot[" + i + "] name=" + nameOf(s) + " bone=" + nameOf(field(s, GU, GPC)));
}
// 所有 GO 附件
for (Field f : GP.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
f.setAccessible(true);
Object v = f.get(skeleton);
if (size(v) == 0) continue;
Class<?> et = elemClass(v);
if (et == null || !GO.isAssignableFrom(et)) continue;
log("GP." + f.getName() + ":" + size(v) + " 个 GO");
for (int i = 0; i < size(v); i++) {
Object go = get(v, i);
log(" GO[" + i + "] name=" + nameOf(go)
+ " slot=" + nameOf(field(go, GN, GU))
+ " x=" + fval(go, "x") + " y=" + fval(go, "y") + " rot=" + fval(go, "rot")
+ " sx=" + fval(go, "sx") + " sy=" + fval(go, "sy")
+ " w=" + fval(go, "w") + " h=" + fval(go, "h"));
}
}
// 皮肤条目
Object skins = arrFieldOfType(GP, skeleton, GL);
for (int k = 0; k < size(skins); k++) {
Object skin = get(skins, k);
log("皮肤[" + k + "] " + nameOf(skin));
for (int i = 0; i < size(slots); i++) {
Object s = get(slots, i);
Object entry = null;
for (Method m : GL.getDeclaredMethods()) {
Class<?>[] p = m.getParameterTypes();
if (p.length != 2 || p[0] != GPC || p[1] != GU) continue;
m.setAccessible(true);
try { entry = m.invoke(skin, field(s, GU, GPC), s); } catch (Throwable ignored) { }
if (entry != null) break;
}
if (entry != null) {
log(" 条目 slot=" + nameOf(s) + " -> " + describe(entry) + fields(entry));
}
}
}
}
/** GO 的字段语义来自编辑器自己的 JSON 写入器 sb_0.java:669-675。 */
static String fval(Object go, String which) {
String[] names = { "D", "B", "S", "L", "I", "C", "Z" };
String[] keys = { "x", "y", "rot", "sx", "sy", "w", "h" };
for (int i = 0; i < keys.length; i++) {
if (!keys[i].equals(which)) continue;
try {
Field f = go.getClass().getField(names[i]);
return String.valueOf(f.getFloat(go));
} catch (Throwable t) { return "?"; }
}
return "?";
}
static String fields(Object o) {
StringBuilder sb = new StringBuilder();
for (Class<?> t = o.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
for (Field f : t.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
f.setAccessible(true);
Object v;
try { v = f.get(o); } catch (Throwable e) { continue; }
if (v == null) continue;
String s = String.valueOf(v);
if (s.length() > 60) s = s.substring(0, 60) + "…";
sb.append(" ").append(f.getName()).append('=').append(s);
}
}
return sb.toString();
}
static Object arrFieldOfType(Class<?> owner, Object target, Class<?> elem) {
for (Field f : owner.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
f.setAccessible(true);
try {
Object v = f.get(target);
Class<?> et = elemClass(v);
if (et != null && elem.isAssignableFrom(et)) return v;
} catch (Throwable ignored) { }
}
return null;
}
static Object rootNodes(Object tree) {
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())) continue;
if (f.getType() != type) continue;
f.setAccessible(true);
try { return f.get(target); } catch (Throwable ignored) { }
}
}
return null;
}
static Object global(String typeName) {
try {
Class<?> og = Class.forName("OG", true, L);
for (Field f : og.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())) continue;
if (!f.getType().getName().equals(typeName)) continue;
f.setAccessible(true);
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 String describe(Object o) { return o == null ? "null" : o.getClass().getName(); }
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;
}
}