保留本次功能的最终实现源码(热更 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:
1 parent
0a2da7f11b
commit
64361e389e
1 file changed
+803
@@ -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") + "]";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user