Files
Spine_lab/tools/live/src/AddButton10.java
T
tianmo 64361e389e 保留本次功能的最终实现源码(热更 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(可恢复)
2026-10-16 19:33:51 +08:00

804 lines
34 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.*;
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") + "]";
}
}