保留本次功能的最终实现源码(热更 agent 第 10 版)

内容:不重启、纯编辑器自身 API 加一个按钮 → 弹名称输入面板(编辑器自带 xl 对话框,ASM 现场生成子类拿名字)
→ 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼,每根 1 插槽 1 附件(输入的附件名):
- 骨骼 x/y 0~500 随机、rotation/scale/shear = 0/1/1/0/0
- 插槽 alpha 0.5~1 随机、blend normal/additive 随机
- 附件在插槽上 x/y/rotation=0、scaleX/scaleY=1(并换掉 ADD_SLOT 带来的模板附件)
- 弹窗按 pref 尺寸(399x170)撑开、同一次渲染回调里居中(不再先闪左下角)
同期其余临时件(build*/、*.jar、实验探针源码)已移出到 /tmp/spineview-tools-live-backup-1016-1933(可恢复)
This commit is contained in:
tianmo committed 2026-10-16 19:33:51 +08:00
1 parent 0a2da7f11b
commit 64361e389e
1 file changed
+803
+803
View File
@@ -0,0 +1,803 @@
import java.lang.instrument.Instrumentation;
import java.lang.reflect.*;
import java.net.URLClassLoader;
import java.util.*;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* 第三版(点按钮没反应的原因已查明并修掉):
* 1) 弹窗:编辑器自己的 `xl`(标题/正文/名字框/OK/Cancel)× ASM 生成的子类;
* ⚠️ 实测:只调 `xl.D(dt)` 不会把窗口挂进 Stage(parent 一直 null,屏幕上什么都不出现),
* 必须**显式 Stage.addActor(dialog)**(`dt.D(Ldl;)V`,字节码就是 root.addActor),
* 再自己摆位置。中文在编辑器字体里是乱码,所以文字一律用英文。
* 2) 清掉上一版/实验残留的对话框(类名 com.spineview.live.AttNamePrompt 的那些)。
* 3) 名字捕获改成**每个对话框实例自己的字段**(不再用共享 static,避免多个等待线程互相串台)。
* 4) 建对象部分与第二版相同(已干跑验证):当前选中骨骼下 15 根骨骼 + 1 插槽 + 1 附件。
*/
public final class AddButton10 {
static Instrumentation INST;
static ClassLoader L;
static Class<?> ARRAY, BONE, SLOT, ATT, GO, HI, SKELETON_C, UH_C, COLOR_C, IZ_C, XL, STAGE_C,
ACTOR_C, GROUP_C, CELL_C, DIALOG_C, eX;
static Object UH_INSTANCE, EDITOR, TREE_VIEW, STAGE, IZ_NORMAL, IZ_ADDITIVE;
static Method ADD_SLOT, ADD_ATT, ADD_BONE, NAME_GEN, REFRESH, REFRESH2, UNDO_BEGIN, UNDO_COMMIT,
ON_RENDER, ADD_ACTOR, SHOW;
static Field DIALOG_PARENT_FIELD, DIALOG_NAME_FIELD, DIALOG_DONE_FIELD,
DIALOG_STAGE_FIELD;
static final int COUNT = 15;
public static void agentmain(String args, Instrumentation inst) {
INST = inst;
try {
run();
} catch (Throwable error) {
log("失败: " + error);
error.printStackTrace(System.err);
}
}
static void log(String m) { System.err.println("[btn10] " + 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 = forName("fI");
BONE = forName("Gp");
SLOT = forName("GU");
ATT = forName("Gn");
GO = forName("GO");
HI = forName("Hi");
SKELETON_C = forName("GP");
UH_C = forName("UH");
COLOR_C = forName("com.badlogic.gdx.graphics.Color");
IZ_C = forName("iz");
XL = forName("xl");
STAGE_C = forName("dt");
ACTOR_C = forName("dl");
GROUP_C = forName("do");
CELL_C = forName("dR");
eX = forName("eX");
Class<?> og = forName("OG");
Class<?> undo = forName("un");
Class<?> vz = forName("VZ");
UH_INSTANCE = staticFieldOfType(og, UH_C);
EDITOR = staticFieldOfType(og, forName("com.esotericsoftware.spine.editor.Editor"));
TREE_VIEW = staticFieldOfType(og, vz);
STAGE = staticFieldOfType(og, STAGE_C);
ADD_SLOT = find(UH_C, "D", new Class<?>[] {SLOT, BONE, boolean.class, ARRAY}, SLOT);
ADD_ATT = find(UH_C, "D", new Class<?>[] {ATT, SLOT, boolean.class, boolean.class, ARRAY}, ATT);
ADD_BONE = find(SKELETON_C, "D", new Class<?>[] {BONE}, void.class);
NAME_GEN = find(og, "D", new Class<?>[] {String.class, ARRAY, SLOT}, String.class);
REFRESH = find(vz, "D", new Class<?>[] {SKELETON_C, boolean.class, boolean.class}, void.class);
if (REFRESH == null && EDITOR != null) {
REFRESH2 = find(vz, "D", new Class<?>[] {EDITOR.getClass(), SKELETON_C, boolean.class,
boolean.class}, void.class);
}
for (Method m : undo.getDeclaredMethods()) {
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
if (m.getName().equals("D")) UNDO_BEGIN = m;
if (m.getName().equals("B")) UNDO_COMMIT = m;
}
for (Method m : og.getDeclaredMethods()) {
Class<?>[] t = m.getParameterTypes();
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) ON_RENDER = m;
}
ON_RENDER.setAccessible(true);
// ⚠️ 实测:`STAGE_C.getMethod("D", ACTOR_C)` 拿到的那个调用后 parent 永远是 null,
// 而"扫描 stage.getClass().getMethods() 里 1 个 Actor 参数 + void"的这个是真能挂上的。
for (Method m : STAGE.getClass().getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (t.length != 1) continue;
if (!ACTOR_C.isAssignableFrom(t[0])) continue;
if (m.getReturnType() != void.class) continue;
ADD_ACTOR = m;
ADD_ACTOR.setAccessible(true);
}
log("addActor = " + ADD_ACTOR);
for (Method m : XL.getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (t.length == 1 && t[0] == STAGE_C) SHOW = m;
}
IZ_NORMAL = IZ_C.getField("D").get(null);
IZ_ADDITIVE = IZ_C.getField("B").get(null);
DIALOG_C = defineDialogClass();
DIALOG_NAME_FIELD = DIALOG_C.getField("NAME");
DIALOG_DONE_FIELD = DIALOG_C.getField("DONE");
DIALOG_PARENT_FIELD = parentFieldOf(ACTOR_C);
DIALOG_STAGE_FIELD = stageFieldOf(ACTOR_C);
if (ADD_SLOT == null || ADD_ATT == null || ADD_BONE == null || NAME_GEN == null
|| UNDO_BEGIN == null || UNDO_COMMIT == null) {
log("API 没找齐,停");
return;
}
onRender(new Runnable() {
@Override public void run() {
cleanupOrphans();
}
});
installButton();
}
/**
* 清掉前几版留下的对话框(含还留在屏幕上的实验弹窗)。
* 遍历方式照抄面板自己的 fillDialog:从 Stage 出发按**字段**广搜(按 actor 树走会漏,实测过)。
*/
static void cleanupOrphans() {
try {
Class<?> actorC = ACTOR_C;
Method remove = null;
for (Method m : actorC.getMethods()) {
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
&& m.getReturnType() == boolean.class) remove = m;
}
remove.setAccessible(true);
Deque<Object> queue = new ArrayDeque<Object>();
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
queue.add(STAGE);
int visited = 0, removed = 0;
while (!queue.isEmpty() && visited < 200000) {
Object a = queue.poll();
if (a == null || !seen.add(a)) continue;
visited++;
String cn = a.getClass().getName();
if (cn.startsWith("com.spineview.live.")) {
try { remove.invoke(a); removed++; } catch (Throwable ignored) { }
continue;
}
try {
if (cn.startsWith("java.") || cn.startsWith("javax.") || cn.startsWith("sun.")) continue;
if (a instanceof Class || a instanceof String || a instanceof Number) continue;
if (a.getClass().isArray()) {
if (!a.getClass().getComponentType().isPrimitive()) {
Object[] arr = (Object[]) a;
for (Object item : arr) if (item != null) queue.add(item);
}
continue;
}
for (Class<?> c = a.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers()) || f.getType().isPrimitive()) continue;
try {
f.setAccessible(true);
Object v = f.get(a);
if (v != null) queue.add(v);
} catch (Throwable ignored) { }
}
}
} catch (Throwable t) {
log("遍历 " + cn + " 出错: " + t);
}
}
log("清理旧对话框 " + removed + " 个(遍历 " + visited + " 个对象)");
} catch (Throwable e) {
log("清理旧对话框失败: " + e);
}
}
/**
* 删掉上一版跑出来那批骨骼。
* ⚠️ 判据必须精确,上一版我把"插槽有两个 Gn 字段"当判据,结果误伤了你原有的骨骼(已撤销恢复)。
* 这次的判据:**骨架附件表 GP.L 里,同一个插槽被登记了 >=2 个附件** —— 只有我上一版那批是这样
* (ADD_SLOT 带了模板插槽的附件 + 我又加了一个),原有插槽每个只有 1 个。
*/
static void cleanupLastBatch() {
try {
java.util.List<?> all = (java.util.List<?>) Class.forName("com.spineview.SceneObjects", true, L)
.getMethod("listAllNodes", ClassLoader.class).invoke(null, L);
java.util.List<Object> bones = new ArrayList<Object>();
for (Object o : all) if (o != null && BONE.isInstance(o)) bones.add(o);
if (bones.isEmpty()) return;
Object skeleton = firstFieldOfType(BONE, bones.get(0), SKELETON_C);
if (skeleton == null) return;
Object slotsArr = arrayOfType(SKELETON_C, skeleton, SLOT);
Object attsArr = arrayOfType(SKELETON_C, skeleton, ATT);
// 统计每个插槽登记了几个附件
Map<Object, Integer> perSlot = new IdentityHashMap<Object, Integer>();
for (int i = 0; i < size(attsArr); i++) {
Object a = get(attsArr, i);
if (a == null) continue;
Object slot = firstFieldOfType(ATT, a, SLOT);
if (slot == null) continue;
Integer c = perSlot.get(slot);
perSlot.put(slot, Integer.valueOf(c == null ? 1 : c.intValue() + 1));
}
Method del = Class.forName("com.spineview.SceneActions", true, L)
.getMethod("delete", ClassLoader.class, Object.class);
del.setAccessible(true);
int killed = 0;
Set<Object> killedSet = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
for (Map.Entry<Object, Integer> e : perSlot.entrySet()) {
if (e.getValue().intValue() < 2) continue; // 正常插槽只有 1 个附件
Object bone = firstFieldOfType(SLOT, e.getKey(), BONE);
if (bone == null || !killedSet.add(bone)) continue;
del.invoke(null, L, bone);
killed++;
}
log("清理上一版残留骨骼 " + killed + " 根(判据:同插槽登记了 2 个以上附件)");
} catch (Throwable e) {
log("清理上一版失败: " + e);
}
}
static void installButton() throws Exception {
Class<?> panel = Class.forName("com.spineview.ObjectPanel");
Field lastF = panel.getDeclaredField("LAST_STATE");
lastF.setAccessible(true);
Object state = lastF.get(null);
if (state == null) { log("面板还没建"); return; }
Object buttons = stateField(state, "buttons");
Object skin = stateField(state, "skin");
// 前几版一共往按钮区塞了 3 个按钮:这里把"第 11 个 TextButton 之后"的全摘掉,只留原来的 10 个
// ⚠️ Actor.remove() 是 `b_()Z`;`eX.b_()V` 是 invalidate()。按名字随便取会拿到 void 那个 → 删不掉。
Method remove = null;
for (Method m : ACTOR_C.getMethods()) {
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
&& m.getReturnType() == boolean.class) remove = m;
}
remove.setAccessible(true);
Object kids = childrenOf(buttons);
int removedOld = 0;
for (int i = size(kids) - 1; i >= 0; i--) {
Object child = get(kids, i);
if (child == null || !child.getClass().getName().contains("TextButton")) continue;
if (seenButtonsFromEnd(kids, i) > 10) {
remove.invoke(child);
removedOld++;
}
}
log("移除前几版按钮 " + removedOld + " 个");
Class<?> textButton = forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton");
Class<?> skinClass = forName("com.badlogic.gdx.scenes.scene2d.ui.Skin");
Object button = textButton.getConstructor(String.class, skinClass)
.newInstance("New 15 Bone+Slot+Att", skin);
Method add = null, row = null, left = null;
for (Method m : buttons.getClass().getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (m.getName().equals("D") && t.length == 1 && t[0] == ACTOR_C
&& m.getReturnType() == CELL_C) add = m;
if (m.getName().equals("I") && t.length == 0 && m.getReturnType() == CELL_C) row = m;
}
for (Method m : CELL_C.getMethods()) {
if (m.getName().equals("C") && m.getParameterTypes().length == 0
&& m.getReturnType() == CELL_C) left = m;
}
if (add == null) { log("找不到 Table.add(Actor)Cell"); return; }
add.setAccessible(true);
Object cell = add.invoke(buttons, button);
if (left != null) { left.setAccessible(true); left.invoke(cell); }
if (row != null) { row.setAccessible(true); row.invoke(buttons); }
for (Method m : buttons.getClass().getMethods()) {
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(buttons); break; }
}
Method wire = panel.getDeclaredMethod("wire", ClassLoader.class, Object.class, Runnable.class);
wire.setAccessible(true);
wire.invoke(null, L, button, new Runnable() {
@Override public void run() {
Thread t = new Thread(new Runnable() {
@Override public void run() {
try { promptAndCreate(); } catch (Throwable e) {
log("流程失败: " + e);
e.printStackTrace(System.err);
}
}
}, "spineview-prompt15-v3");
t.setDaemon(true);
t.start();
}
});
log("按钮已装好(第 " + size(childrenOf(buttons)) + " 行)并接上点击");
Thread verify = new Thread(new Runnable() {
@Override public void run() {
try {
Thread.sleep(300L);
Class.forName("AddButton10").getDeclaredMethod("promptAndCreate").invoke(null);
} catch (Throwable e) { log("验证弹窗失败: " + e); }
}
}, "spineview-prompt15-verify");
verify.setDaemon(true);
verify.start();
log("已触发一次弹窗验证(居中效果;Cancel 不会有任何改动)");
}
/** 从前往后数,这个下标是第几个 TextButton(1 起) */
static int seenButtonsFromEnd(Object kids, int index) {
int n = 0;
for (int i = 0; i <= index; i++) {
Object c = get(kids, i);
if (c != null && c.getClass().getName().contains("TextButton")) n++;
}
return n;
}
static Object stateField(Object state, String name) throws Exception {
Field f = state.getClass().getDeclaredField(name);
f.setAccessible(true);
return f.get(state);
}
/** 弹窗 → 等名字 → 建对象 */
static void promptAndCreate() throws Exception {
final Object[] holder = new Object[1];
onRender(new Runnable() {
@Override public void run() {
try {
holder[0] = showPrompt();
} catch (Throwable e) {
log("弹面板失败: " + e);
e.printStackTrace(System.err);
}
}
});
Object dialog = holder[0];
if (dialog == null) { log("弹窗没建出来 → 结束"); return; }
log("弹窗已显示:parent=" + (parentOf(dialog) != null) + " 位置=" + geom(dialog)
+ " Stage=" + fl(STAGE, "D") + "x" + fl(STAGE, "B")
+ " 在 Stage 孩子里=" + (indexOf(childrenOf(rootGroup()), dialog) >= 0));
String name = waitForName(dialog, 300000L);
if (name == null || name.trim().isEmpty()) {
log("没拿到名字(取消/超时)→ 结束");
removeQuietly(dialog);
return;
}
final String finalName = name.trim();
log("输入的名字 = " + finalName);
removeQuietly(dialog);
onRender(new Runnable() {
@Override public void run() {
try { create15(finalName); } catch (Throwable e) {
log("建对象失败: " + e);
e.printStackTrace(System.err);
}
}
});
}
static Object showPrompt() throws Exception {
Object dialog = DIALOG_C.getConstructor(String.class, String.class)
.newInstance("New Bones + Slots + Attachment",
"Enter an existing attachment name in this skeleton:");
// ① 先挂进 Stage(实测这一步必须用扫描出来的那个 addActor)
ADD_ACTOR.invoke(STAGE, dialog);
// ② 再走编辑器自己的显示流程(它会把键盘焦点给输入框)
try { SHOW.invoke(dialog, STAGE); } catch (Throwable e) { log("xl.D(dt) 失败: " + e); }
if (parentOf(dialog) == null) ADD_ACTOR.invoke(STAGE, dialog);
// ③ 尺寸:默认窗口太小,正文/按钮会被裁。用编辑器自己的 pref 尺寸(拿不到就给下限)
float prefW = fl(dialog, "StackMapTable"); // WidgetGroup.getPrefWidth
float prefH = fl(dialog, "init"); // WidgetGroup.getPrefHeight
float minW = fl(dialog, "LineNumberTable"); // getMinWidth
float minH = fl(dialog, "String"); // getMinHeight
float w = Math.max(Math.max(prefW, minW), 360f);
float h = Math.max(Math.max(prefH, minH), 170f);
setFloatField(dialog, "L", w); // Actor.width (dl.L:F)
setFloatField(dialog, "I", h); // Actor.height (dl.I:F)
for (Method m : eX.getMethods()) { // invalidate() 让内容按新尺寸重新布局
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(dialog); break; }
}
log("窗口尺寸: pref=" + prefW + "x" + prefH + " min=" + minW + "x" + minH
+ " -> 设为 " + w + "x" + h);
// ④ 立刻摆到正中心(同一次回调里做完,不会再从左上角/左下角闪一下)
center(dialog);
return dialog;
}
/** 等用户按 OK / Cancel:OK 时 L(String) 已把名字写进 NAME;Cancel 时 NAME 仍是 null */
static String waitForName(Object dialog, long timeoutMs) throws Exception {
long end = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < end) {
Object done = DIALOG_DONE_FIELD.get(dialog);
Object name = DIALOG_NAME_FIELD.get(dialog);
if (name != null) return (String) name;
if (Boolean.TRUE.equals(done)) return null; // 点了 Cancel
if (stageField(dialog) == null) return null; // 窗口被关掉/移除
Thread.sleep(120L);
}
return null;
}
/** 读对话框自己的 stage 字段(dl.D:Ldt; = Actor.stage;remove() 之后会变 null) */
static Object stageField(Object actor) {
if (DIALOG_STAGE_FIELD == null) return STAGE;
try { return DIALOG_STAGE_FIELD.get(actor); } catch (Throwable e) { return STAGE; }
}
/** 按窗口真实尺寸把对话框摆到正中央(用 Actor.setPosition(float,float,int) 方法,不猜字段名) */
static void center(Object dialog) {
try {
float w = fl(dialog, "L"), h = fl(dialog, "I");
if (w <= 0f || w > 3000f) w = 300f;
if (h <= 0f || h > 3000f) h = 180f;
// setPosition(x,y,align) 在 align=0 时是"以 (x,y) 为中心",所以直接给 Stage 中心
float cx = fl(STAGE, "D") / 2f;
float cy = fl(STAGE, "B") / 2f;
Method setPos = null;
for (Method m : ACTOR_C.getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (t.length == 3 && t[0] == float.class && t[1] == float.class && t[2] == int.class) setPos = m;
}
if (setPos == null) {
for (Method m : ACTOR_C.getMethods()) {
Class<?>[] t = m.getParameterTypes();
if (t.length == 2 && t[0] == float.class && t[1] == float.class
&& m.getReturnType() == void.class) setPos = m;
}
}
if (setPos == null) { log("找不到 setPosition"); return; }
setPos.setAccessible(true);
if (setPos.getParameterTypes().length == 3) {
setPos.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy), Integer.valueOf(0));
} else {
setPos.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy));
}
log("已居中(" + setPos.getName() + "):窗口 " + w + "x" + h + " -> (" + cx + "," + cy + ")"
+ " 实际位置=" + geom(dialog));
} catch (Throwable e) {
log("居中失败: " + e);
}
}
static Object rootGroup() { return firstFieldOfType(STAGE_C, STAGE, GROUP_C); }
static void removeQuietly(Object dialog) {
try {
Method rm = dialog.getClass().getMethod("b_");
rm.setAccessible(true);
rm.invoke(dialog);
} catch (Throwable ignored) { }
}
/** 生成 xl 子类:OK 时把名字写进**自己的** NAME 字段并关窗 */
static Class<?> defineDialogClass() throws Exception {
final String internal = "com/spineview/live/AttPrompt8";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "xl", null);
cw.visitField(Opcodes.ACC_PUBLIC, "NAME", "Ljava/lang/String;", null, null).visitEnd();
cw.visitField(Opcodes.ACC_PUBLIC, "DONE", "Z", null, null).visitEnd();
MethodVisitor c = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"(Ljava/lang/String;Ljava/lang/String;)V", null, null);
c.visitCode();
c.visitVarInsn(Opcodes.ALOAD, 0);
c.visitVarInsn(Opcodes.ALOAD, 1);
c.visitVarInsn(Opcodes.ALOAD, 2);
c.visitInsn(Opcodes.ACONST_NULL);
c.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "<init>",
"(Ljava/lang/String;Ljava/lang/String;LGe;)V", false);
c.visitInsn(Opcodes.RETURN);
c.visitMaxs(0, 0);
c.visitEnd();
MethodVisitor h = cw.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/String;)V", null, null);
h.visitCode();
h.visitVarInsn(Opcodes.ALOAD, 0);
h.visitVarInsn(Opcodes.ALOAD, 1);
h.visitFieldInsn(Opcodes.PUTFIELD, internal, "NAME", "Ljava/lang/String;");
h.visitVarInsn(Opcodes.ALOAD, 0);
h.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "dl", "b_", "()Z", false);
h.visitInsn(Opcodes.POP);
h.visitInsn(Opcodes.RETURN);
h.visitMaxs(0, 0);
h.visitEnd();
// 覆盖 xl.L(Object)(OK 就是 Boolean.TRUE、Cancel 就是 FALSE):不管哪种,跑完就置 DONE
MethodVisitor d = cw.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/Object;)V", null, null);
d.visitCode();
d.visitVarInsn(Opcodes.ALOAD, 0);
d.visitVarInsn(Opcodes.ALOAD, 1);
d.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "L", "(Ljava/lang/Object;)V", false);
d.visitVarInsn(Opcodes.ALOAD, 0);
d.visitInsn(Opcodes.ICONST_1);
d.visitFieldInsn(Opcodes.PUTFIELD, internal, "DONE", "Z");
d.visitInsn(Opcodes.RETURN);
d.visitMaxs(0, 0);
d.visitEnd();
cw.visitEnd();
final byte[] bytes = cw.toByteArray();
ClassLoader child = new ClassLoader(L) {
@Override protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (name.equals("com.spineview.live.AttPrompt8")) {
Class<?> found = findLoadedClass(name);
if (found == null) found = defineClass(name, bytes, 0, bytes.length);
if (resolve) resolveClass(found);
return found;
}
return super.loadClass(name, resolve);
}
};
return Class.forName("com.spineview.live.AttPrompt8", true, child);
}
// ==================== 建 15 根骨骼 + 插槽 + 附件 ====================
static void create15(String name) throws Exception {
Object selected = Class.forName("com.spineview.SceneObjects", true, L)
.getMethod("selected", ClassLoader.class).invoke(null, L);
if (selected == null) { log("当前没有选中对象 → 结束"); return; }
if (!BONE.isInstance(selected)) {
log("当前选中的不是骨骼(" + selected.getClass().getName() + ")→ 结束");
return;
}
Object parentBone = selected;
Object skeleton = firstFieldOfType(BONE, parentBone, SKELETON_C);
Object bones = arrayOfType(SKELETON_C, skeleton, BONE);
Object slots = arrayOfType(SKELETON_C, skeleton, SLOT);
Object attachments = arrayOfType(SKELETON_C, skeleton, ATT);
if (bones == null || slots == null || attachments == null) {
log("骨架数组没找齐 → 结束");
return;
}
Object templateSlot = size(slots) > 0 ? get(slots, 0) : null;
Object template = null;
for (int i = 0; i < size(attachments); i++) {
Object a = get(attachments, i);
if (a == null || !ATT.isInstance(a)) continue;
if (!name.equals(nameOf(a))) continue;
if (!HI.isInstance(a)) { template = a; break; }
if (template == null) template = a;
}
if (template == null) {
log("骨架里没有名为 \"" + name + "\" 的附件 → 结束");
return;
}
log("模板附件 = " + nameOf(template) + " (" + template.getClass().getName()
+ ") 模板插槽 = " + nameOf(templateSlot));
Random random = new Random();
Object empty = ARRAY.getConstructor().newInstance();
UNDO_BEGIN.setAccessible(true);
ADD_BONE.setAccessible(true);
UNDO_BEGIN.invoke(null);
Constructor<?> boneCtor = BONE.getConstructor(BONE, SKELETON_C, BONE);
int made = 0, replaced = 0;
for (int i = 0; i < COUNT; i++) {
Object bone = boneCtor.newInstance(parentBone, skeleton, parentBone);
set(bone, "D", String.class, NAME_GEN.invoke(null, "bone", bones, null));
setFloatField(bone, "Z", random.nextFloat() * 500f); // x
setFloatField(bone, "V", random.nextFloat() * 500f); // y
setFloatField(bone, "F", 0f); // rotation
setFloatField(bone, "Code", 1f); // scaleX
setFloatField(bone, "SourceFile", 1f); // scaleY
setFloatField(bone, "StackMapTable", 0f); // shearX
setFloatField(bone, "LineNumberTable", 0f); // shearY
ADD_BONE.invoke(skeleton, bone);
Object out = ARRAY.getConstructor().newInstance();
Object slot = ADD_SLOT.invoke(UH_INSTANCE, templateSlot, bone, Boolean.FALSE, out);
if (slot == null) { log("第 " + i + " 个插槽没建出来"); continue; }
// ⚠️ ADD_SLOT 会把"模板插槽"整个复制一份 —— 包括模板插槽自己挂着的那个附件
// (实测:新插槽上挂的是 mega_dg4 的副本,而不是我们要的"输入名字"的附件)
Object inherited = readSlotAttachment(slot);
float alpha = 0.5f + random.nextFloat() * 0.5f;
Object color = COLOR_C.getConstructor(float.class, float.class, float.class, float.class)
.newInstance(1f, 1f, 1f, alpha);
set(slot, "B", COLOR_C, color);
set(slot, "D", COLOR_C, color);
set(slot, "D", IZ_C, random.nextBoolean() ? IZ_NORMAL : IZ_ADDITIVE);
Object att = ADD_ATT.invoke(UH_INSTANCE, template, slot, Boolean.FALSE, Boolean.FALSE, empty);
if (att == null) { log("第 " + i + " 个附件没建出来"); continue; }
setFloatField(att, "D", 0f);
setFloatField(att, "B", 0f);
setFloatField(att, "S", 0f);
setFloatField(att, "L", 1f);
setFloatField(att, "I", 1f);
String before = nameOf(att);
if (!name.equals(before)) {
set(att, "D", String.class, name);
log(" 第 " + i + " 个附件名被编辑器改成过 \"" + before + "\",已改回");
}
// 把插槽的当前附件换成**我们这份**(两个字段都写:setup=B / animate=D)
set(slot, "B", ATT, att);
set(slot, "D", ATT, att);
// 删掉模板插槽带来的那份(保证"一根骨骼 = 1 插槽 + 1 附件")
if (inherited != null && inherited != att) {
deleteAttachment(inherited);
replaced++;
}
made++;
}
UNDO_COMMIT.setAccessible(true);
UNDO_COMMIT.invoke(null);
if (REFRESH != null) {
REFRESH.setAccessible(true);
REFRESH.invoke(TREE_VIEW, skeleton, Boolean.TRUE, Boolean.TRUE);
} else if (REFRESH2 != null) {
REFRESH2.setAccessible(true);
REFRESH2.invoke(TREE_VIEW, EDITOR, skeleton, Boolean.TRUE, Boolean.TRUE);
}
log("完成:在 " + nameOf(parentBone) + " 下建了 " + made + " 根骨骼;每根 1 插槽 1 附件 \"" + name
+ "\"(替换掉模板附件 " + replaced + " 个)");
}
// ==================== 工具 ====================
/** 读插槽当前挂的附件(GU 上两个 Gn 字段随便哪个都指向同一份) */
static Object readSlotAttachment(Object slot) {
for (Class<?> c = slot.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ATT) continue;
f.setAccessible(true);
try { Object v = f.get(slot); if (v != null) return v; } catch (Throwable ignored) { }
}
}
return null;
}
/** Gn.L()V = 编辑器自己的"删掉这个附件"(字节码:skeleton.removeAttachment(this,true,true)) */
static void deleteAttachment(Object att) {
try {
Method del = find(ATT, "L", new Class<?>[0], void.class);
del.setAccessible(true);
del.invoke(att);
} catch (Throwable e) {
log("删除模板附件失败: " + e);
}
}
static void onRender(Runnable body) throws Exception { ON_RENDER.invoke(null, body); }
static Class<?> forName(String n) throws ClassNotFoundException { return Class.forName(n, true, L); }
static String nameOf(Object o) {
try {
return (String) Class.forName("com.spineview.SceneObjects", true, L)
.getMethod("nameOf", Object.class).invoke(null, o);
} catch (Throwable t) { return "?"; }
}
static Field parentFieldOf(Class<?> actorClass) {
for (Field f : actorClass.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
if (f.getType() == GROUP_C) { f.setAccessible(true); return f; }
}
return null;
}
static Field stageFieldOf(Class<?> actorClass) {
for (Field f : actorClass.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
if (f.getType() == STAGE_C) { f.setAccessible(true); return f; }
}
return null;
}
static Object parentOf(Object actor) {
try { return DIALOG_PARENT_FIELD == null ? null : DIALOG_PARENT_FIELD.get(actor); }
catch (Throwable e) { return null; }
}
static Object staticFieldOfType(Class<?> owner, Class<?> type) {
for (Field f : owner.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers())) continue;
if (f.getType() != type) continue;
f.setAccessible(true);
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
}
return null;
}
static Object firstFieldOfType(Class<?> owner, Object target, Class<?> type) {
if (target == null) return null;
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
f.setAccessible(true);
try { Object v = f.get(target); if (v != null) return v; } catch (Throwable ignored) { }
}
}
return null;
}
static Object arrayOfType(Class<?> owner, Object target, Class<?> elem) {
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
if (!ARRAY.isAssignableFrom(f.getType())) continue;
f.setAccessible(true);
Object v;
try { v = f.get(target); } catch (Throwable ignored) { continue; }
if (v == null) continue;
Class<?> et = elemClass(v);
if (et != null && elem.isAssignableFrom(et)) return v;
}
}
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 Object childrenOf(Object group) {
if (group == null) return null;
for (Field f : GROUP_C.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) continue;
if (!ARRAY.isAssignableFrom(f.getType())) continue;
f.setAccessible(true);
try { return f.get(group); } catch (Throwable ignored) { }
}
return null;
}
static int indexOf(Object array, Object target) {
for (int i = 0; i < size(array); i++) if (get(array, i) == target) return i;
return -1;
}
static void set(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;
}
}
}
static void setFloatField(Object target, String name, float value) {
try {
set(target, name, float.class, Float.valueOf(value));
} catch (Throwable ignored) {
// 该类型没这个字段就跳过
}
}
static Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Method m : c.getDeclaredMethods()) {
if (!m.getName().equals(name)) continue;
if (!Arrays.equals(m.getParameterTypes(), params)) continue;
if (ret != null && m.getReturnType() != ret) continue;
m.setAccessible(true);
return m;
}
}
return null;
}
static float fl(Object actor, String name) {
if (actor == null) return -1f;
for (Method m : actor.getClass().getMethods()) {
if (!m.getName().equals(name)) continue;
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); }
catch (Throwable ignored) { }
}
return -1f;
}
static int size(Object array) {
if (array == null) return 0;
for (Class<?> c = ARRAY; c != null && c != Object.class; c = c.getSuperclass()) {
for (Field f : c.getDeclaredFields()) {
if (f.getType() != int.class) continue;
f.setAccessible(true);
try { return f.getInt(array); } catch (Throwable ignored) { }
}
}
return 0;
}
static Object get(Object array, int index) {
if (array == null) return null;
for (Method m : array.getClass().getMethods()) {
if (!m.getName().equals("D")) continue;
Class<?>[] t = m.getParameterTypes();
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); }
catch (Throwable ignored) { }
}
return null;
}
static String geom(Object actor) {
return "[x=" + fl(actor, "c_") + " y=" + fl(actor, "d_")
+ " w=" + fl(actor, "L") + " h=" + fl(actor, "I") + "]";
}
}