热更(不重启)整理新建附件:用编辑器自身 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

+195
View File
@@ -0,0 +1,195 @@
import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
/**
* 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。
* 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。
*
* 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP):
* new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null)
* → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true)
*/
public final class BoneMaker2 {
public static void agentmain(String args, Instrumentation inst) {
int count = 10;
try {
if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim());
} catch (Throwable ignored) {
// 默认 10
}
try {
run(inst, count);
} catch (Throwable error) {
System.err.println("[live] BoneMaker 失败: " + error);
error.printStackTrace(System.err);
}
}
private static void run(Instrumentation inst, int count) throws Exception {
// 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的)
// 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)),
// 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。
ClassLoader loader = null;
for (Class<?> c : inst.getAllLoadedClasses()) {
ClassLoader cl = c.getClassLoader();
if (cl instanceof java.net.URLClassLoader) {
loader = cl;
System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName());
break;
}
}
if (loader == null) {
System.err.println("[live] 找不到编辑器类加载器");
return;
}
System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName());
// 2) 当前选中对象(复用面板里已有的读取逻辑)
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader);
Object parent = sceneObjects.getMethod("selected", ClassLoader.class)
.invoke(null, loader);
if (parent == null) {
System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)");
return;
}
Object skeleton = sceneObjects.getMethod("skeletonOf", Object.class).invoke(null, parent);
System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class)
.invoke(null, parent) + ",骨架 = " + skeleton);
// 3) 用编辑器自身 API 建 N 根
Class<?> boneClass = Class.forName("Gp", true, loader);
Class<?> skeletonClass = Class.forName("GP", true, loader);
Class<?> arrayClass = Class.forName("fI", true, loader);
Class<?> slotClass = Class.forName("GU", true, loader);
Class<?> og = Class.forName("OG", true, loader);
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
Class<?> treeViewClass = Class.forName("VZ", true, loader);
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
Method addBone = null;
for (Method m : skeletonClass.getMethods()) {
if (m.getName().equals("D") && m.getParameterTypes().length == 1
&& m.getParameterTypes()[0] == boneClass) {
addBone = m;
break;
}
}
Method nameGen = null;
for (Method m : og.getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass
&& m.getReturnType() == String.class) {
nameGen = m;
break;
}
}
Method refresh = null;
for (Method m : treeViewClass.getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class
&& t[2] == boolean.class) {
refresh = m;
break;
}
}
Object editor = null;
for (Field f : og.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())) continue;
if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue;
f.setAccessible(true);
editor = f.get(null);
if (editor != null) break;
}
Object treeView = null;
for (Field f : og.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())) continue;
if (f.getType() != treeViewClass) continue;
f.setAccessible(true);
treeView = f.get(null);
if (treeView != null) break;
}
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
.newInstance(1f, 0f, 0f, 1f);
if (addBone == null || nameGen == null || treeView == null) {
System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen
+ " treeView=" + treeView);
return;
}
addBone.setAccessible(true);
nameGen.setAccessible(true);
// skeleton 的骨骼数组字段(fI)
Object bones = null;
for (Field f : skeletonClass.getDeclaredFields()) {
if (f.getType() == arrayClass) {
f.setAccessible(true);
bones = f.get(skeleton);
break;
}
}
// 父骨骼的名字(String 字段)
Object parentName = null;
for (Field f : boneClass.getDeclaredFields()) {
if (f.getType() == String.class) {
f.setAccessible(true);
parentName = f.get(parent);
break;
}
}
// 4) 撤销包裹(un.D()/un.B())—— 整批算一次
Class<?> undo = Class.forName("un", true, loader);
Method begin = null, commit = null;
for (Method m : undo.getDeclaredMethods()) {
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
if (m.getName().equals("D")) begin = m;
if (m.getName().equals("B")) commit = m;
}
if (begin != null) {
begin.setAccessible(true);
begin.invoke(null);
}
int made = 0;
for (int i = 0; i < count; i++) {
Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父)
Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成
setString(bone, boneClass, generated); // name
addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone
setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x
setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y
setField(bone, "B", colorClass, red); // color
made++;
}
if (commit != null) {
commit.setAccessible(true);
commit.invoke(null);
}
if (refresh != null && editor != null) {
refresh.setAccessible(true);
refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次
System.err.println("[live] 已刷新树节点");
}
System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼");
}
private static void setField(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;
}
}
}
private static void setFloat(Object target, String name, float value) throws Exception {
setField(target, name, float.class, Float.valueOf(value));
}
private static void setString(Object target, Class<?> boneClass, Object value) throws Exception {
setField(target, "D", String.class, value);
}
}