把 New 15 Bone+Slot+Att 按钮移植进面板源码(重启后仍在)
- 新类 com.spineview.NewFifteen:按钮 → 编辑器自带名称面板(xl + ASM 子类拿名字) → 按名字匹配附件 → 当前选中骨骼下建 15 根骨骼,每根 1 插槽 1 附件(该名字的副本) 骨骼 x/y 0~500 随机、rotation/scale/shear=0/1/0;插槽 alpha 0.5~1、blend normal/additive 随机; 附件 x/y/rotation=0、scaleX/scaleY=1(并替换掉 ADD_SLOT 带过来的模板附件) 弹窗按 pref 尺寸(>=360x170)撑开、同一次渲染回调里居中 - ObjectPanel.build 里挂一行按钮(复用 ObjectPanel.wire 的点击轮询) - build.sh 的 javac 加 -cp ../tools/asm/asm.jar(NewFifteen 现场生成 xl 子类需要 ASM) 依据:tools/deobf/anchors.tsv + 混淆名对照表.md(全部字节码/实测确认)
This commit is contained in:
1 parent
64361e389e
commit
8df990a058
3 files changed
+687
-1
No files matched your search
+3
-1
@@ -20,7 +20,9 @@ mkdir -p build/classes
|
||||
|
||||
echo "== 编译 Java 侧(javac $("$JAVA_HOME/bin/javac" -version 2>&1))"
|
||||
find src -name '*.class' -delete 2>/dev/null || true
|
||||
"$JAVA_HOME/bin/javac" --release 8 -encoding UTF-8 -d build/classes $(find src -name '*.java')
|
||||
# 面板里有一处用了 ASM(NewFifteen 现场生成编辑器对话框子类),编译期也要能看到 asm.jar
|
||||
"$JAVA_HOME/bin/javac" --release 8 -encoding UTF-8 -cp "../tools/asm/asm.jar" \
|
||||
-d build/classes $(find src -name '*.java')
|
||||
|
||||
# 保名类清单:这套运行时把 libGDX 基类混淆了,但 ui/* 和 editor/* 保名。
|
||||
# 旧版本的明文载荷里能直接列出这些类名,用它们做静态字段扫描的候选。
|
||||
|
||||
@@ -0,0 +1,682 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
/**
|
||||
* “New 15 Bone+Slot+Att” 按钮(正式版,重启后仍在)。
|
||||
*
|
||||
* 流程:点一下 → 弹编辑器自带的名称输入面板 → 用输入的名字匹配骨架里的附件
|
||||
* → 名字不对/找不到就结束 → 否则在**当前选中骨骼**下建 15 根骨骼,
|
||||
* 每根 1 个插槽 + 1 个附件(该名字的附件副本)。
|
||||
*
|
||||
* 参数要求(全部按已确认的字段语义写,见 混淆名对照表.md):
|
||||
* 骨骼:x/y 各 0~500 随机;rotation=0;scaleX=scaleY=1;shearX=shearY=0
|
||||
* 插槽:颜色 alpha 0.5~1 随机;blend = normal / additive 随机
|
||||
* 附件(挂在插槽上):x=y=rotation=0;scaleX=scaleY=1
|
||||
*
|
||||
* 关键 API(都在 tools/deobf/anchors.tsv 里,附字节码依据):
|
||||
* UH.D(GU,Gp,Z,fI)GU addSlot ⚠️ 会连模板插槽的附件一起复制
|
||||
* UH.D(Gn,GU,Z,Z,fI)Gn addAttachmentCopy(内部 Gn.D(GP) 复制模板附件)
|
||||
* GP.D(Gp)V addBone
|
||||
* OG.D(String,fI,GU)String uniqueName
|
||||
* VZ.D(GP,Z,Z)V refresh
|
||||
* un.D()/un.B() undoBegin/undoCommit
|
||||
* Gn.L()V delete
|
||||
* dt.D(dl)V Stage.addActor
|
||||
* xl.D(dt) / xl.L(Object) / xl.L(String) 编辑器名称面板:显示 / OK-Cancel 回调 / 拿到名字(抽象)
|
||||
*/
|
||||
final class NewFifteen {
|
||||
private static final int COUNT = 15;
|
||||
private static final String BUTTON_TEXT = "New 15 Bone+Slot+Att";
|
||||
|
||||
/** 生成的对话框子类(ASM 现生成:xl 是抽象类,且必须在载荷类加载器那边实现)。 */
|
||||
private static Class<?> dialogClass;
|
||||
private static ClassLoader dialogLoader;
|
||||
private static boolean installed;
|
||||
|
||||
private NewFifteen() {
|
||||
}
|
||||
|
||||
/** 由 ObjectPanel.build 调用:往按钮表格里加一行按钮并接上点击。 */
|
||||
static void install(final ClassLoader loader, Object skin, Object buttonsTable,
|
||||
Class<?> tableClass, Class<?> cellClass, Class<?> actorClass) {
|
||||
if (installed) return;
|
||||
try {
|
||||
Class<?> textButton = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
|
||||
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
|
||||
final Object button = textButton.getConstructor(String.class, skinClass)
|
||||
.newInstance(BUTTON_TEXT, skin);
|
||||
Object cell = call(buttonsTable, "D", new Class<?>[] {actorClass}, cellClass, button);
|
||||
call(cell, "C", new Class<?>[0], cellClass); // Cell.left()
|
||||
call(buttonsTable, "I", new Class<?>[0], cellClass); // Table.row()
|
||||
ObjectPanel.wire(loader, button, new Runnable() {
|
||||
@Override public void run() {
|
||||
// 面板路由线程里:只负责开线程走流程,别把它堵住(要等用户输入)
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
promptThenCreate(loader);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[new15] 流程失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}, "spineview-new15");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
});
|
||||
installed = true;
|
||||
System.err.println("[new15] 按钮已加入: " + BUTTON_TEXT);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[new15] 加按钮失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 弹面板 → 等名字 → 建对象 ====================
|
||||
|
||||
private static void promptThenCreate(final ClassLoader loader) throws Exception {
|
||||
final Object[] holder = new Object[1];
|
||||
SceneActions.onEditorThread(loader, new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
holder[0] = showPrompt(loader);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[new15] 弹面板失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
final Object dialog = holder[0];
|
||||
if (dialog == null) {
|
||||
System.err.println("[new15] 面板没建出来");
|
||||
return;
|
||||
}
|
||||
String name = waitForName(loader, dialog, 300000L);
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
System.err.println("[new15] 没拿到名字(取消/超时)→ 结束");
|
||||
removeQuietly(dialog);
|
||||
return;
|
||||
}
|
||||
final String finalName = name.trim();
|
||||
System.err.println("[new15] 输入的名字 = " + finalName);
|
||||
removeQuietly(dialog);
|
||||
SceneActions.onEditorThread(loader, new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
create(loader, finalName);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[new15] 建对象失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 在渲染线程里:建对话框 → 挂进 Stage → 按 pref 撑开 → 居中(都在同一帧里完成,不会先闪别处)。 */
|
||||
private static Object showPrompt(ClassLoader loader) throws Exception {
|
||||
Class<?> xl = Class.forName("xl", true, loader);
|
||||
Class<?> stageClass = Class.forName("dt", true, loader);
|
||||
Object stage = global(loader, stageClass);
|
||||
if (stage == null) throw new IllegalStateException("拿不到 Stage");
|
||||
Class<?> dialog = dialogClass(loader);
|
||||
Object dialogObject = dialog.getConstructor(String.class, String.class)
|
||||
.newInstance("New Bones + Slots + Attachment",
|
||||
"Enter an existing attachment name in this skeleton:");
|
||||
// ① 挂进 Stage(实测 xl.D(dt) 不会挂;Stage.addActor 用"扫描 getMethods"那个才生效)
|
||||
addActor(stage, dialogObject);
|
||||
// ② 走编辑器自己的显示流程(把键盘焦点给名字框)
|
||||
Method show = null;
|
||||
for (Method method : xl.getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length == 1 && types[0] == stageClass) show = method;
|
||||
}
|
||||
if (show != null) {
|
||||
try {
|
||||
show.setAccessible(true);
|
||||
show.invoke(dialogObject, stage);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[new15] xl.D(dt) 失败: " + error);
|
||||
}
|
||||
}
|
||||
// ③ 尺寸:默认 150x150 会把正文/按钮裁掉 → 用 pref(下限 360x170)
|
||||
float prefW = floatOf(dialogObject, "StackMapTable"); // getPrefWidth
|
||||
float prefH = floatOf(dialogObject, "init"); // getPrefHeight
|
||||
float minW = floatOf(dialogObject, "LineNumberTable"); // getMinWidth
|
||||
float minH = floatOf(dialogObject, "String"); // getMinHeight
|
||||
float width = Math.max(Math.max(prefW, minW), 360f);
|
||||
float height = Math.max(Math.max(prefH, minH), 170f);
|
||||
setFloatField(loader, dialogObject, "L", width); // Actor.width
|
||||
setFloatField(loader, dialogObject, "I", height); // Actor.height
|
||||
invalidate(loader, dialogObject);
|
||||
// ④ 居中:setPosition(x,y,align) 在 align=0 时是"以 (x,y) 为中心"
|
||||
center(stage, dialogObject);
|
||||
return dialogObject;
|
||||
}
|
||||
|
||||
/** 轮询用户操作:OK 时 NAME 有值;Cancel 时 DONE=true 且 NAME 为空;窗口被移除时 stage 字段变 null。 */
|
||||
private static String waitForName(ClassLoader loader, Object dialog, long timeoutMs)
|
||||
throws Exception {
|
||||
Field nameField = dialog.getClass().getField("NAME");
|
||||
Field doneField = dialog.getClass().getField("DONE");
|
||||
Field stageField = fieldOfType(Class.forName("dl", true, loader),
|
||||
Class.forName("dt", true, loader));
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
Object name = nameField.get(dialog);
|
||||
if (name != null) return (String) name;
|
||||
if (Boolean.TRUE.equals(doneField.get(dialog))) return null;
|
||||
if (stageField != null && stageField.get(dialog) == null) return null;
|
||||
Thread.sleep(120L);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== 建 15 组对象 ====================
|
||||
|
||||
private static void create(ClassLoader loader, String name) throws Exception {
|
||||
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||
Class<?> attClass = Class.forName("Gn", true, loader);
|
||||
Class<?> hiClass = Class.forName("Hi", true, loader);
|
||||
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||
Class<?> blendClass = Class.forName("iz", true, loader);
|
||||
Class<?> uhClass = Class.forName("UH", true, loader);
|
||||
Class<?> og = Class.forName("OG", true, loader);
|
||||
Class<?> undoClass = Class.forName("un", true, loader);
|
||||
Class<?> treeClass = Class.forName("VZ", true, loader);
|
||||
|
||||
Object selected = SceneObjects.selected(loader);
|
||||
if (selected == null || !boneClass.isInstance(selected)) {
|
||||
System.err.println("[new15] 当前选中的不是骨骼 → 结束");
|
||||
return;
|
||||
}
|
||||
Object skeleton = firstFieldOfType(boneClass, selected, skeletonClass);
|
||||
if (skeleton == null) {
|
||||
System.err.println("[new15] 拿不到骨架 → 结束");
|
||||
return;
|
||||
}
|
||||
Object bones = arrayOfType(skeletonClass, skeleton, arrayClass, boneClass);
|
||||
Object slots = arrayOfType(skeletonClass, skeleton, arrayClass, slotClass);
|
||||
Object attachments = arrayOfType(skeletonClass, skeleton, arrayClass, attClass);
|
||||
if (bones == null || slots == null || attachments == null) {
|
||||
System.err.println("[new15] 骨架的骨骼/插槽/附件表没找齐 → 结束");
|
||||
return;
|
||||
}
|
||||
// 按名字找附件(优先非 Hi 占位)
|
||||
Object template = null;
|
||||
for (int i = 0; i < size(arrayClass, attachments); i++) {
|
||||
Object candidate = get(arrayClass, attachments, i);
|
||||
if (candidate == null || !attClass.isInstance(candidate)) continue;
|
||||
if (!name.equals(SceneObjects.nameOf(candidate))) continue;
|
||||
if (!hiClass.isInstance(candidate)) {
|
||||
template = candidate;
|
||||
break;
|
||||
}
|
||||
if (template == null) template = candidate;
|
||||
}
|
||||
if (template == null) {
|
||||
System.err.println("[new15] 骨架里没有名为 \"" + name + "\" 的附件 → 结束");
|
||||
return;
|
||||
}
|
||||
Object templateSlot = size(arrayClass, slots) > 0 ? get(arrayClass, slots, 0) : null;
|
||||
|
||||
Object uh = staticField(og, uhClass);
|
||||
if (uh == null) {
|
||||
System.err.println("[new15] 拿不到编辑器模型助手 UH → 结束");
|
||||
return;
|
||||
}
|
||||
Method addBone = find(skeletonClass, "D", new Class<?>[] {boneClass}, void.class);
|
||||
Method addSlot = find(uhClass, "D", new Class<?>[] {slotClass, boneClass, boolean.class,
|
||||
arrayClass}, slotClass);
|
||||
Method addAttachment = find(uhClass, "D", new Class<?>[] {attClass, slotClass, boolean.class,
|
||||
boolean.class, arrayClass}, attClass);
|
||||
Method nameGen = find(og, "D", new Class<?>[] {String.class, arrayClass, slotClass}, String.class);
|
||||
Method refresh = find(treeClass, "D", new Class<?>[] {skeletonClass, boolean.class,
|
||||
boolean.class}, void.class);
|
||||
Method deleteAttachment = find(attClass, "L", new Class<?>[0], void.class);
|
||||
Method undoBegin = noArgVoid(undoClass, "D");
|
||||
Method undoCommit = noArgVoid(undoClass, "B");
|
||||
if (addBone == null || addSlot == null || addAttachment == null || nameGen == null
|
||||
|| undoBegin == null || undoCommit == null) {
|
||||
System.err.println("[new15] 编辑器 API 没找齐,停止");
|
||||
return;
|
||||
}
|
||||
Object blendNormal = blendClass.getField("D").get(null);
|
||||
Object blendAdditive = blendClass.getField("B").get(null);
|
||||
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||
Object empty = arrayClass.getConstructor().newInstance();
|
||||
Random random = new Random();
|
||||
int made = 0;
|
||||
|
||||
undoBegin.setAccessible(true);
|
||||
undoBegin.invoke(null);
|
||||
for (int i = 0; i < COUNT; i++) {
|
||||
// ---- 骨骼:x/y 0~500 随机,其余按 0/1 ----
|
||||
Object bone = boneCtor.newInstance(selected, skeleton, selected);
|
||||
set(bone, "D", String.class, nameGen.invoke(null, "bone", bones, null));
|
||||
setFloatField(loader, bone, "Z", random.nextFloat() * 500f); // x
|
||||
setFloatField(loader, bone, "V", random.nextFloat() * 500f); // y
|
||||
setFloatField(loader, bone, "F", 0f); // rotation
|
||||
setFloatField(loader, bone, "Code", 1f); // scaleX
|
||||
setFloatField(loader, bone, "SourceFile", 1f); // scaleY
|
||||
setFloatField(loader, bone, "StackMapTable", 0f); // shearX
|
||||
setFloatField(loader, bone, "LineNumberTable", 0f); // shearY
|
||||
addBone.setAccessible(true);
|
||||
addBone.invoke(skeleton, bone);
|
||||
|
||||
// ---- 插槽:alpha 0.5~1 随机;blend normal/additive 随机 ----
|
||||
Object out = arrayClass.getConstructor().newInstance();
|
||||
addSlot.setAccessible(true);
|
||||
Object slot = addSlot.invoke(uh, templateSlot, bone, Boolean.FALSE, out);
|
||||
if (slot == null) continue;
|
||||
// ADD_SLOT 会把模板插槽整个复制,连它挂着的附件一起复制 → 记下来待会删
|
||||
Object inherited = readSlotAttachment(slotClass, attClass, slot);
|
||||
float alpha = 0.5f + random.nextFloat() * 0.5f;
|
||||
Object color = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(1f, 1f, 1f, alpha);
|
||||
set(slot, "B", colorClass, color); // setup 颜色
|
||||
set(slot, "D", colorClass, color); // animate 颜色
|
||||
set(slot, "D", blendClass, random.nextBoolean() ? blendNormal : blendAdditive);
|
||||
|
||||
// ---- 附件:复制"输入名字"的附件,变换 0/0/0/1/1 ----
|
||||
addAttachment.setAccessible(true);
|
||||
Object attachment = addAttachment.invoke(uh, template, slot, Boolean.FALSE, Boolean.FALSE, empty);
|
||||
if (attachment == null) continue;
|
||||
setFloatField(loader, attachment, "D", 0f); // x
|
||||
setFloatField(loader, attachment, "B", 0f); // y
|
||||
setFloatField(loader, attachment, "S", 0f); // rotation
|
||||
setFloatField(loader, attachment, "L", 1f); // scaleX
|
||||
setFloatField(loader, attachment, "I", 1f); // scaleY
|
||||
if (!name.equals(SceneObjects.nameOf(attachment))) {
|
||||
set(attachment, "D", String.class, name); // 编辑器若自动改名,强制回输入的名字
|
||||
}
|
||||
// 插槽的当前附件 = 我们这份(两个字段都写,setup/animate 都覆盖)
|
||||
set(slot, "B", attClass, attachment);
|
||||
set(slot, "D", attClass, attachment);
|
||||
// 删掉模板插槽带来的那份(保证"一根骨骼 = 1 插槽 + 1 附件")
|
||||
if (inherited != null && inherited != attachment && deleteAttachment != null) {
|
||||
deleteAttachment.setAccessible(true);
|
||||
deleteAttachment.invoke(inherited);
|
||||
}
|
||||
made++;
|
||||
}
|
||||
undoCommit.setAccessible(true);
|
||||
undoCommit.invoke(null);
|
||||
if (refresh != null) {
|
||||
refresh.setAccessible(true);
|
||||
refresh.invoke(staticField(og, treeClass), skeleton, Boolean.TRUE, Boolean.TRUE);
|
||||
}
|
||||
System.err.println("[new15] 完成:在 " + SceneObjects.nameOf(selected) + " 下建了 " + made
|
||||
+ " 根骨骼(每根 1 插槽 1 附件 \"" + name + "\")");
|
||||
}
|
||||
|
||||
// ==================== 对话框子类(ASM) ====================
|
||||
|
||||
private static synchronized Class<?> dialogClass(final ClassLoader loader) throws Exception {
|
||||
if (dialogClass != null) return dialogClass;
|
||||
final String internal = "com/spineview/NewFifteenPrompt";
|
||||
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "xl", null);
|
||||
writer.visitField(Opcodes.ACC_PUBLIC, "NAME", "Ljava/lang/String;", null, null).visitEnd();
|
||||
writer.visitField(Opcodes.ACC_PUBLIC, "DONE", "Z", null, null).visitEnd();
|
||||
// 构造:super(title, body, (Ge) null)
|
||||
MethodVisitor ctor = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)V", null, null);
|
||||
ctor.visitCode();
|
||||
ctor.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
ctor.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
ctor.visitVarInsn(Opcodes.ALOAD, 2);
|
||||
ctor.visitInsn(Opcodes.ACONST_NULL);
|
||||
ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;LGe;)V", false);
|
||||
ctor.visitInsn(Opcodes.RETURN);
|
||||
ctor.visitMaxs(0, 0);
|
||||
ctor.visitEnd();
|
||||
// 抽象回调:OK 时拿到名字 → 记 NAME,并关窗
|
||||
MethodVisitor name = writer.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/String;)V",
|
||||
null, null);
|
||||
name.visitCode();
|
||||
name.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
name.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
name.visitFieldInsn(Opcodes.PUTFIELD, internal, "NAME", "Ljava/lang/String;");
|
||||
name.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
name.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "dl", "b_", "()Z", false); // remove()
|
||||
name.visitInsn(Opcodes.POP);
|
||||
name.visitInsn(Opcodes.RETURN);
|
||||
name.visitMaxs(0, 0);
|
||||
name.visitEnd();
|
||||
// OK/Cancel 总回调:不管哪个都置 DONE
|
||||
MethodVisitor done = writer.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/Object;)V",
|
||||
null, null);
|
||||
done.visitCode();
|
||||
done.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
done.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
done.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "L", "(Ljava/lang/Object;)V", false);
|
||||
done.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
done.visitInsn(Opcodes.ICONST_1);
|
||||
done.visitFieldInsn(Opcodes.PUTFIELD, internal, "DONE", "Z");
|
||||
done.visitInsn(Opcodes.RETURN);
|
||||
done.visitMaxs(0, 0);
|
||||
done.visitEnd();
|
||||
writer.visitEnd();
|
||||
final byte[] bytes = writer.toByteArray();
|
||||
dialogLoader = new ClassLoader(loader) {
|
||||
@Override protected Class<?> loadClass(String name, boolean resolve)
|
||||
throws ClassNotFoundException {
|
||||
if (name.equals("com.spineview.NewFifteenPrompt")) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
dialogClass = Class.forName("com.spineview.NewFifteenPrompt", true, dialogLoader);
|
||||
return dialogClass;
|
||||
}
|
||||
|
||||
// ==================== 小工具 ====================
|
||||
|
||||
private static Object global(ClassLoader loader, Class<?> type) {
|
||||
try {
|
||||
Class<?> og = Class.forName("OG", true, loader);
|
||||
for (Field field : og.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value != null) return value;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 读不到就返回 null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 挂进 Stage:用"扫描 getMethods 里 1 个 Actor 参数 + void"的那个(getMethod 那个实测不生效)。 */
|
||||
private static void addActor(Object stage, Object actor) throws Exception {
|
||||
ClassLoader loader = stage.getClass().getClassLoader();
|
||||
Class<?> actorClass = Class.forName("dl", true, loader);
|
||||
Method add = null;
|
||||
for (Method method : stage.getClass().getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 1) continue;
|
||||
if (!actorClass.isAssignableFrom(types[0])) continue;
|
||||
if (method.getReturnType() != void.class) continue;
|
||||
add = method;
|
||||
}
|
||||
if (add == null) throw new NoSuchMethodException("Stage.addActor");
|
||||
add.setAccessible(true);
|
||||
add.invoke(stage, actor);
|
||||
}
|
||||
|
||||
private static void center(Object stage, Object dialog) {
|
||||
try {
|
||||
float width = floatOf(dialog, "L"); // getWidth
|
||||
float height = floatOf(dialog, "I"); // getHeight
|
||||
float cx = floatOf(stage, "D") / 2f; // Stage.getWidth / 2
|
||||
float cy = floatOf(stage, "B") / 2f; // Stage.getHeight / 2
|
||||
Class<?> actorClass = Class.forName("dl", true, dialog.getClass().getClassLoader());
|
||||
Method setPosition = null;
|
||||
for (Method method : actorClass.getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length == 3 && types[0] == float.class && types[1] == float.class
|
||||
&& types[2] == int.class) setPosition = method;
|
||||
}
|
||||
if (setPosition != null) {
|
||||
setPosition.setAccessible(true);
|
||||
// align=0 → 以 (x,y) 为中心
|
||||
setPosition.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy), Integer.valueOf(0));
|
||||
}
|
||||
System.err.println("[new15] 弹窗 " + width + "x" + height + " 居中于 (" + cx + "," + cy + ")");
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[new15] 居中失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
private static void invalidate(ClassLoader loader, Object dialog) {
|
||||
try {
|
||||
Class<?> widgetGroup = Class.forName("eX", true, loader);
|
||||
for (Method method : widgetGroup.getMethods()) {
|
||||
if (method.getName().equals("b_") && method.getParameterTypes().length == 0
|
||||
&& method.getReturnType() == void.class) {
|
||||
method.setAccessible(true);
|
||||
method.invoke(dialog);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 没 invalidate 也不致命
|
||||
}
|
||||
}
|
||||
|
||||
private static void removeQuietly(Object dialog) {
|
||||
try {
|
||||
Class<?> actorClass = Class.forName("dl", true, dialog.getClass().getClassLoader());
|
||||
Method remove = null;
|
||||
for (Method method : actorClass.getMethods()) {
|
||||
if (method.getName().equals("b_") && method.getParameterTypes().length == 0
|
||||
&& method.getReturnType() == boolean.class) remove = method;
|
||||
}
|
||||
if (remove != null) {
|
||||
remove.setAccessible(true);
|
||||
remove.invoke(dialog);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 已经关掉了
|
||||
}
|
||||
}
|
||||
|
||||
private static Object readSlotAttachment(Class<?> slotClass, Class<?> attClass, Object slot)
|
||||
throws Exception {
|
||||
for (Class<?> type = slotClass; type != null && type != Object.class; type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) || field.getType() != attClass) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(slot);
|
||||
if (value != null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Field fieldOfType(Class<?> owner, Class<?> type) {
|
||||
for (Class<?> current = owner; current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object firstFieldOfType(Class<?> owner, Object target, Class<?> type) {
|
||||
for (Class<?> current = owner; current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object value = field.get(target);
|
||||
if (value != null) return value;
|
||||
} catch (Throwable ignored) {
|
||||
// 下一个
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object arrayOfType(Class<?> owner, Object target, Class<?> arrayClass,
|
||||
Class<?> elementType) {
|
||||
for (Class<?> current = owner; current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers()) || !arrayClass.isAssignableFrom(field.getType())) {
|
||||
continue;
|
||||
}
|
||||
field.setAccessible(true);
|
||||
Object array;
|
||||
try {
|
||||
array = field.get(target);
|
||||
} catch (Throwable ignored) {
|
||||
continue;
|
||||
}
|
||||
if (array == null) continue;
|
||||
for (int i = 0; i < size(arrayClass, array); i++) {
|
||||
Object element = get(arrayClass, array, i);
|
||||
if (element != null && elementType.isInstance(element)) return array;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setFloatField(ClassLoader loader, Object target, String name, float value)
|
||||
throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getName().equals(name) || field.getType() != float.class) continue;
|
||||
field.setAccessible(true);
|
||||
field.setFloat(target, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void set(Object target, String name, Class<?> type, Object value) throws Exception {
|
||||
for (Class<?> current = target.getClass(); current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (!field.getName().equals(name) || field.getType() != type) continue;
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method method : target.getClass().getMethods()) {
|
||||
if (!method.getName().equals(name)) continue;
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
if (method.getReturnType() != float.class) continue;
|
||||
method.setAccessible(true);
|
||||
return ((Float) method.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 读不到
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
private static Object call(Object target, String name, Class<?>[] parameterTypes,
|
||||
Class<?> returnType, Object... args) throws Exception {
|
||||
for (Class<?> current = target.getClass(); current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Method method : current.getDeclaredMethods()) {
|
||||
if (!method.getName().equals(name)) continue;
|
||||
if (!Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||||
if (returnType != null && method.getReturnType() != returnType) continue;
|
||||
method.setAccessible(true);
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
}
|
||||
throw new NoSuchMethodException(target.getClass().getName() + "." + name);
|
||||
}
|
||||
|
||||
private static Object staticField(Class<?> owner, Class<?> type) {
|
||||
for (Field field : owner.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
Object value = field.get(null);
|
||||
if (value != null) return value;
|
||||
} catch (Throwable ignored) {
|
||||
// 下一个
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Method find(Class<?> owner, String name, Class<?>[] parameterTypes,
|
||||
Class<?> returnType) {
|
||||
for (Class<?> current = owner; current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Method method : current.getDeclaredMethods()) {
|
||||
if (!method.getName().equals(name)) continue;
|
||||
if (!Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||||
if (returnType != null && method.getReturnType() != returnType) continue;
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Method noArgVoid(Class<?> owner, String name) {
|
||||
for (Method method : owner.getDeclaredMethods()) {
|
||||
if (!method.getName().equals(name)) continue;
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
if (method.getReturnType() != void.class) continue;
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int size(Class<?> arrayClass, Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> current = arrayClass; current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
for (Field field : current.getDeclaredFields()) {
|
||||
if (field.getType() != int.class) continue;
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
return field.getInt(array);
|
||||
} catch (Throwable ignored) {
|
||||
// 下一个
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Object get(Class<?> arrayClass, Object array, int index) {
|
||||
for (Method method : arrayClass.getMethods()) {
|
||||
if (!method.getName().equals("D")) continue;
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 1 || types[0] != int.class) continue;
|
||||
if (method.getReturnType() != Object.class) continue;
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
return method.invoke(array, Integer.valueOf(index));
|
||||
} catch (Throwable ignored) {
|
||||
// 下一个
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -136,6 +136,8 @@ public final class ObjectPanel {
|
||||
// 每个按钮一行:上段是**一条整带**(不要左右两列),整体读起来才是"上/中/下"
|
||||
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
}
|
||||
// 新按钮:点一下弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼(每根 1 插槽 1 附件)
|
||||
NewFifteen.install(loader, skin, buttons, tableClass, cellClass, actorClass);
|
||||
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
|
||||
Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
|
||||
layoutCell(nameCell, Obf.CELL_LEFT);
|
||||
|
||||
Reference in new issue
Block a user