Files
Spine_lab/tools/live/src/BoneMaker2.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

196 lines
7.8 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.*;
/**
* 挂到**正在运行的**编辑器进程里,用编辑器自身 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);
}
}