- 确认 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。
196 lines
7.8 KiB
Java
196 lines
7.8 KiB
Java
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 BoneMaker {
|
||
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);
|
||
}
|
||
}
|