阶段四:Spine 3.8.99 编辑器里挂上自建视图面板
目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示), 点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。 本轮打通: 1. 编辑器在我们自己的进程里完整启动 - 类加载器必须是 URLClassLoader 本身(自检要求) - Editor.main 必须在 AWT EDT 上调用 - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身 2. 3.8.99 全量解密 → 可读源码 - 字符串 17251/17461(伪造同名调用帧对齐 key) - 混淆动态派发还原 53871 处(调引导方法取 MemberName) 3. 自建视图 - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检) - 面板 Actor 由外部反射填进 us.spineView.D - 复用已有图标做别名(不新建图片资源) 最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开 另外: - ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩 - DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成 - 阶段四-视图面板挂载.md:完整记录做法与踩过的坑 不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、 Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
This commit is contained in:
commit
2e7ab7c33c
113 files changed
+25327
No files matched your search
@@ -0,0 +1,203 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 3.8.99 专用入口:先启动探针,再把控制权交给编辑器。
|
||||
*
|
||||
* 之所以能这么做,是因为 3.8.99 的载荷是明文的 —— 我们可以把它当作普通 classpath 用,
|
||||
* 自己当入口类,完全不需要改任何字节码、也不需要注入。
|
||||
*
|
||||
* 用法:
|
||||
* <x86_64 JDK>/bin/java -cp <本目录 class> \
|
||||
* -Dspine.payload=<载荷 zip> -Dspine.classes=<本目录 class> \
|
||||
* com.spineview.Launcher <project.spine>
|
||||
*
|
||||
* 两个关键点(都是从反编译源码里读出来的,不是猜的):
|
||||
* 1) Editor.main 开头有 `if (!EventQueue.isDispatchThread()) return;`,必须在 AWT 事件线程上调用。
|
||||
* 2) 编辑器自检里有 `if (getClassLoader(Editor.class).getClass() != URLClassLoader.class) 报错退出;`
|
||||
* —— 所以不能用 -cp(那是 AppClassLoader),必须自己 new 一个 URLClassLoader。
|
||||
*/
|
||||
public final class Launcher {
|
||||
public static void main(String[] args) throws Exception {
|
||||
// 1) 先把探针放起来(它自己会开后台线程,不阻塞)
|
||||
try {
|
||||
SceneScan.start();
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[spineview] 启动探针失败: " + error);
|
||||
}
|
||||
|
||||
// 2) 再把控制权交给真正的编辑器。
|
||||
// Editor.main 里有一句 `if (!EventQueue.isDispatchThread()) return;`,
|
||||
// 所以必须在 AWT 事件分发线程上调用它,否则它开门就返回、什么窗口都不建。
|
||||
String payloadPath = System.getProperty("spine.payload");
|
||||
if (payloadPath == null) throw new IllegalStateException("缺少 -Dspine.payload=<载荷 zip>");
|
||||
URL payload = new File(payloadPath).toURI().toURL();
|
||||
String classesPath = System.getProperty("spine.classes");
|
||||
// 编辑器会去 /tzax/*.png 找皮肤贴图,而它在启动器的 launcher.jar 里,载荷里没有。
|
||||
// 注意:整个 launcher.jar 不能上类路径 —— 它里面的类是 Java 10 编译的(class 54),
|
||||
// JDK8 加载会抛 UnsupportedClassVersionError,把 Prefs 初始化打断、编辑器根本建不出窗口。
|
||||
// 所以只用从它里面抽出来的**资源目录**。
|
||||
String resources = System.getProperty("spine.resources");
|
||||
String compat = System.getProperty("spine.compat");
|
||||
String launcherJar = System.getProperty("spine.launcher.jar");
|
||||
java.util.List<URL> urls = new java.util.ArrayList<URL>();
|
||||
urls.add(payload);
|
||||
if (classesPath != null) urls.add(new File(classesPath).toURI().toURL());
|
||||
if (compat != null) urls.add(new File(compat).toURI().toURL());
|
||||
if (resources != null) urls.add(new File(resources).toURI().toURL());
|
||||
if (launcherJar != null) urls.add(new File(launcherJar).toURI().toURL());
|
||||
URLClassLoader editorLoader = new URLClassLoader(urls.toArray(new URL[0]), null);
|
||||
System.err.println("[spineview] 编辑器类加载器 = " + editorLoader.getClass().getName());
|
||||
System.err.println("[spineview] 皮肤资源 /tzax/down-ur.png = "
|
||||
+ editorLoader.findResource("tzax/down-ur.png"));
|
||||
|
||||
final Class<?> editor = Class.forName("com.esotericsoftware.spine.editor.Editor", true, editorLoader);
|
||||
|
||||
// Editor.D:I 是"启动器版本号",反编译源码里 `Editor.D(kM)` 一开头就是:
|
||||
// if (D < 19 && !options.D) { 弹"安装已过时"对话框; return; }
|
||||
// 官方是原生库/启动器把这个字段填上的,我们自己跑就得补一下,否则永远进不到编辑器。
|
||||
java.util.List<java.lang.reflect.Field> versionFields =
|
||||
new java.util.ArrayList<java.lang.reflect.Field>();
|
||||
for (java.lang.reflect.Field field : editor.getDeclaredFields()) {
|
||||
java.lang.reflect.Modifier.toString(field.getModifiers());
|
||||
if (field.getType() != int.class) continue;
|
||||
if (!java.lang.reflect.Modifier.isStatic(field.getModifiers())) continue;
|
||||
field.setAccessible(true);
|
||||
System.err.println("[spineview] Editor 静态 int 字段: " + field.getName()
|
||||
+ " = " + field.getInt(null));
|
||||
if (field.getName().equals("D") || field.getName().startsWith("launcher")) {
|
||||
field.setInt(null, 100);
|
||||
versionFields.add(field);
|
||||
}
|
||||
}
|
||||
System.err.println("[spineview] 已把 " + versionFields.size() + " 个 Editor.D:I 设为 100");
|
||||
|
||||
final String[] arguments = args;
|
||||
javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
editor.getMethod("main", String[].class).invoke(null, (Object) arguments);
|
||||
} catch (Throwable error) {
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (java.lang.reflect.Field field : versionFields) {
|
||||
System.err.println("[spineview] main 返回后 Editor.D:I = " + field.getInt(null));
|
||||
}
|
||||
|
||||
// 视图已经由 view-agent 在内存里注册成"Spine View"(Views 菜单里能勾选/展开),
|
||||
// 这里不再额外往场景上贴悬浮控件,避免和正式视图重复。
|
||||
// 等编辑器界面搭好之后,把面板 Actor 填进 us.spineView.D(视图常量已经注册好了)。
|
||||
final ClassLoader viewLoader = editorLoader;
|
||||
Thread viewThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 皮肤一就绪就立刻补图标别名 + 把面板 Actor 填进 us.spineView.D。
|
||||
// 必须在编辑器"建面板"那一步之前完成,否则框架拿到的 Actor 是 null,
|
||||
// 点击 Views 菜单里的 Spine View 就不响应。
|
||||
if (!ViewInstaller.aliasIcon(viewLoader)) {
|
||||
System.err.println("[view] 没能抢到皮肤时间窗");
|
||||
return;
|
||||
}
|
||||
// 皮肤是一点点填的:图标有了不代表按钮样式也有了,所以失败就隔一会儿重试,
|
||||
// 但要尽快成功——框架"建面板"那一步之前必须把 Actor 填好。
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (ViewInstaller.installForLoader(viewLoader)) return;
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
System.err.println("[view] 重试 200 次仍未装上");
|
||||
}
|
||||
}, "spineview-install-view");
|
||||
viewThread.setDaemon(true);
|
||||
viewThread.start();
|
||||
|
||||
// Editor.main 可能建完窗口就返回;主线程得挂住,否则 JVM 会因为没有非守护线程直接退出。
|
||||
new java.util.concurrent.CountDownLatch(1).await();
|
||||
}
|
||||
|
||||
/** OG.D 就是编辑器单例(Editor.<clinit> 里 new Editor() 挂上去的)。 */
|
||||
private static Object findEditorInstance(ClassLoader loader) throws Exception {
|
||||
Class<?> holder = Class.forName("OG", true, loader);
|
||||
for (java.lang.reflect.Field field : holder.getDeclaredFields()) {
|
||||
if (!field.getName().equals("D")) continue;
|
||||
if (!java.lang.reflect.Modifier.isStatic(field.getModifiers())) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value != null && value.getClass().getName()
|
||||
.equals("com.esotericsoftware.spine.editor.Editor")) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void dump(String label, Object target, int depth) {
|
||||
if (target == null || depth > 1) return;
|
||||
System.err.println("[probe] " + label + " : " + target.getClass().getName());
|
||||
for (java.lang.reflect.Field field : target.getClass().getDeclaredFields()) {
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(target);
|
||||
String text = value == null ? "null" : value.getClass().getName();
|
||||
if (value instanceof Number || value instanceof Boolean || value instanceof String) {
|
||||
text = String.valueOf(value);
|
||||
}
|
||||
System.err.println("[probe] " + field.getType().getSimpleName() + " "
|
||||
+ field.getName() + " = " + text);
|
||||
} catch (Throwable error) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 有界广度遍历,把编辑器实例能摸到的"自家对象"列出来,用来看谁是场景数据。 */
|
||||
private static void walk(Object root) {
|
||||
java.util.Set<Object> seen = java.util.Collections.newSetFromMap(
|
||||
new java.util.IdentityHashMap<Object, Boolean>());
|
||||
java.util.ArrayDeque<Object> queue = new java.util.ArrayDeque<Object>();
|
||||
java.util.ArrayDeque<Integer> depths = new java.util.ArrayDeque<Integer>();
|
||||
queue.add(root);
|
||||
depths.add(Integer.valueOf(0));
|
||||
int printed = 0;
|
||||
while (!queue.isEmpty() && printed < 400) {
|
||||
Object current = queue.poll();
|
||||
int depth = depths.poll().intValue();
|
||||
if (current == null || !seen.add(current)) continue;
|
||||
if (current.getClass().isArray()) continue;
|
||||
String name = current.getClass().getName();
|
||||
if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.")
|
||||
|| name.startsWith("org.lwjgl") || name.startsWith("com.badlogic")) continue;
|
||||
java.lang.reflect.Field[] fields = current.getClass().getDeclaredFields();
|
||||
StringBuilder shape = new StringBuilder();
|
||||
for (java.lang.reflect.Field field : fields) {
|
||||
shape.append(field.getType().getSimpleName()).append(' ');
|
||||
}
|
||||
System.err.println("[walk] " + depth + " " + name + " 字段数=" + fields.length
|
||||
+ " 类型集合=" + shape);
|
||||
printed++;
|
||||
if (depth >= 2) continue;
|
||||
for (java.lang.reflect.Field field : fields) {
|
||||
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) continue;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(current);
|
||||
if (value == null) continue;
|
||||
queue.add(value);
|
||||
depths.add(Integer.valueOf(depth + 1));
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
/**
|
||||
* 把"按钮 + 文字显示"面板挂进 Spine 的 scene2d 场景树。
|
||||
*
|
||||
* 编辑器的窗口是 jglfw 建的原生 NSWindow,不是 AWT 窗口,没有 Swing 面板可挂;
|
||||
* 界面本身是 libGDX scene2d 画出来的,只能加进它的 Stage。
|
||||
*
|
||||
* 这套运行时把 libGDX 的方法名混淆了(类名保留):
|
||||
* Stage 的实现 = uk(继承 dt)、Group = do、Actor = dl、Table = et
|
||||
* 所以下面全部"按类型/签名找成员",不依赖混淆后的名字。
|
||||
*
|
||||
* Stage 实例在 tD 里只是局部变量,任何静态字段都没有;但 libGDX 里每个 Actor
|
||||
* 都记着自己所在的 Stage,而 Fx.D 是一个静态 Actor —— 从这里就能把 Stage 捞回来。
|
||||
*/
|
||||
public final class Scene2dPanel {
|
||||
private static final String STAGE = "dt";
|
||||
private static final String GROUP = "do";
|
||||
private static final String ACTOR = "dl";
|
||||
private static final String SKIN = "com.badlogic.gdx.scenes.scene2d.ui.Skin";
|
||||
private static final String LABEL = "com.badlogic.gdx.scenes.scene2d.ui.Label";
|
||||
private static final String TEXT_BUTTON = "com.badlogic.gdx.scenes.scene2d.ui.TextButton";
|
||||
private static final String WINDOW = "com.badlogic.gdx.scenes.scene2d.ui.Window";
|
||||
|
||||
private Scene2dPanel() {
|
||||
}
|
||||
|
||||
public static void installWhenReady(final ClassLoader loader) {
|
||||
System.err.println("[panel] 开始等 Stage/Skin…");
|
||||
if (System.getProperty("spine.dumpStatics") != null) {
|
||||
try {
|
||||
Thread.sleep(30000);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
}
|
||||
dumpStatics(loader, paths((java.net.URLClassLoader) loader));
|
||||
return;
|
||||
}
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (int i = 0; i < 240; i++) {
|
||||
try {
|
||||
Object stage = stageFromActorAnchor(loader);
|
||||
Object skin = null;
|
||||
if (stage != null) {
|
||||
Object root = firstFieldOfType(stage, GROUP);
|
||||
if (root != null) skin = skinFromRoot(root);
|
||||
}
|
||||
if (i % 10 == 0) {
|
||||
System.err.println("[panel] 第 " + i + " 轮: stage=" + nameOf(stage)
|
||||
+ " skin=" + nameOf(skin));
|
||||
}
|
||||
if (stage != null && skin != null) {
|
||||
install(loader, stage, skin, i);
|
||||
return;
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 查找失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
System.err.println("[panel] 等不到 Stage/Skin");
|
||||
}
|
||||
}, "spineview-panel");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static String nameOf(Object value) {
|
||||
return value == null ? "null" : value.getClass().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 有了 Stage 实例之后挂面板。Skin 从 Stage 根节点的子节点上取
|
||||
* (libGDX 的 Table 自己存着构造时用的 Skin)。
|
||||
*/
|
||||
public static void installForStage(final Object stage, final ClassLoader loader) {
|
||||
// Stage 是先建出来的,UI 是随后才一层层加进去的,所以要等面板挂上来之后才拿得到 Skin。
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
for (int i = 0; i < 240; i++) {
|
||||
Object skin = findSkinDeep(stage, 6, 20000);
|
||||
if (skin != null) {
|
||||
System.err.println("[panel] 第 " + i + " 轮拿到 Skin: "
|
||||
+ skin.getClass().getName());
|
||||
install(loader, stage, skin, i);
|
||||
return;
|
||||
}
|
||||
if (i % 20 == 0) {
|
||||
System.err.println("[panel] 第 " + i + " 轮还没拿到 Skin(Stage "
|
||||
+ nameOf(stage) + ")");
|
||||
}
|
||||
Thread.sleep(500);
|
||||
}
|
||||
System.err.println("[panel] 一直没拿到 Skin");
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] installForStage 失败");
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}, "spineview-skin");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static void install(final ClassLoader loader, Object stage, Object skin, int waited)
|
||||
throws Exception {
|
||||
Class<?> skinClass = Class.forName(SKIN, true, loader);
|
||||
Class<?> buttonClass = Class.forName(TEXT_BUTTON, true, loader);
|
||||
Class<?> labelClass = Class.forName(LABEL, true, loader);
|
||||
Class<?> windowClass = Class.forName(WINDOW, true, loader);
|
||||
|
||||
final Object button = buttonClass.getConstructor(String.class, skinClass)
|
||||
.newInstance("Read Scene Objects", skin);
|
||||
final Object label = labelClass.getConstructor(CharSequence.class, skinClass)
|
||||
.newInstance("Click the button to list bones / slots / attachments.", skin);
|
||||
|
||||
// setBounds 必须各自在自己的类层次上找:Button 和 Label 只有共同的 Actor 祖先,
|
||||
// 拿 Button 上找到的方法去 invoke Label 会报"object is not an instance of declaring class"。
|
||||
// 先挂在场景根节点上(视口左上角,避开编辑器自己的工具条)。
|
||||
findInstanceMethod(button.getClass(), 4, float.class).invoke(button, 60f, 560f, 200f, 32f);
|
||||
findInstanceMethod(label.getClass(), 4, float.class).invoke(label, 60f, 300f, 540f, 250f);
|
||||
Object root = firstFieldOfType(stage, GROUP);
|
||||
addChild(root, button);
|
||||
addChild(root, label);
|
||||
System.err.println("[panel] 已把按钮和文字面板挂到 Stage(等了 " + waited + " 轮)");
|
||||
|
||||
wireButton(button, label, loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把"按钮点击 → 读取场景对象 → 更新文字"这条链路接上。
|
||||
*
|
||||
* 按钮类的方法名被混淆了,没用名字去猜:Button 里有一对 setChecked(boolean)/isChecked(),
|
||||
* 我们把每个 setter 设成 true,再看哪个无参 boolean getter 跟着变 true,就标定了这一对。
|
||||
* libGDX 的 Button 自带一个 ClickListener,点一下就会翻转 isChecked,所以轮询它就等于监听点击。
|
||||
*/
|
||||
public static void wireButton(final Object button, final Object label, final ClassLoader loader)
|
||||
throws Exception {
|
||||
Class<?> buttonClass = button.getClass();
|
||||
Class<?> labelClass = label.getClass();
|
||||
Method setChecked = null;
|
||||
Method isChecked = null;
|
||||
for (Method setter : buttonClass.getMethods()) {
|
||||
if (setter.getParameterTypes().length != 1
|
||||
|| setter.getParameterTypes()[0] != boolean.class
|
||||
|| setter.getReturnType() != void.class) continue;
|
||||
setter.invoke(button, Boolean.TRUE);
|
||||
for (Method getter : buttonClass.getMethods()) {
|
||||
if (getter.getParameterTypes().length != 0
|
||||
|| getter.getReturnType() != boolean.class) continue;
|
||||
if (Boolean.TRUE.equals(getter.invoke(button))) {
|
||||
setChecked = setter;
|
||||
isChecked = getter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
setter.invoke(button, Boolean.FALSE);
|
||||
if (isChecked != null) break;
|
||||
}
|
||||
if (isChecked == null) {
|
||||
System.err.println("[panel] 没能标定 isChecked/setChecked");
|
||||
return;
|
||||
}
|
||||
System.err.println("[panel] isChecked=" + isChecked.getName()
|
||||
+ " setChecked=" + setChecked.getName());
|
||||
|
||||
final Method readChecked = isChecked;
|
||||
final Method resetChecked = setChecked;
|
||||
final Method setText = findMethod(labelClass, 1, CharSequence.class);
|
||||
final ClassLoader classLoader = loader;
|
||||
Thread poll = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
try {
|
||||
if (Boolean.TRUE.equals(readChecked.invoke(button))) {
|
||||
resetChecked.invoke(button, Boolean.FALSE);
|
||||
System.err.println("[panel] 按钮被触发,开始读取场景对象");
|
||||
// Actor 自己记着所在 Stage,直接问它就行
|
||||
Object stage = firstFieldOfType(button, STAGE);
|
||||
String text = SceneReader.read(classLoader, stage);
|
||||
System.err.println("[panel] 读取完成,共 " + text.length() + " 字符");
|
||||
setText.invoke(label, text);
|
||||
}
|
||||
Thread.sleep(80);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 轮询异常: " + error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "spineview-poll");
|
||||
poll.setDaemon(true);
|
||||
poll.start();
|
||||
|
||||
// 验证用:--Dspine.simulateClick=1 时自动把按钮"按"一次,方便无人值守地看结果。
|
||||
if (System.getProperty("spine.simulateClick") != null) {
|
||||
Thread auto = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(4000);
|
||||
resetChecked.invoke(button, Boolean.TRUE);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 自动触发失败: " + error);
|
||||
}
|
||||
}
|
||||
}, "spineview-autoclick");
|
||||
auto.setDaemon(true);
|
||||
auto.start();
|
||||
}
|
||||
}
|
||||
|
||||
/** 从 Fx.D(静态 Actor)出发拿 Stage:libGDX 里每个 Actor 都记着自己所在的 Stage。 */
|
||||
private static Object stageFromActorAnchor(ClassLoader loader) {
|
||||
try {
|
||||
Class<?> holder = Class.forName("Fx", true, loader);
|
||||
Object anchor = null;
|
||||
for (Field field : holder.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (!field.getType().getName().equals(ACTOR)) continue;
|
||||
field.setAccessible(true);
|
||||
anchor = field.get(null);
|
||||
if (anchor != null) break;
|
||||
}
|
||||
if (anchor == null) return null;
|
||||
return firstFieldOfType(anchor, STAGE);
|
||||
} catch (Throwable error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stage 根节点是 do(Group);拿它第一个子节点身上的 Skin(Table 都带 Skin 字段)。 */
|
||||
@SuppressWarnings("unused")
|
||||
private static Object skinFromRoot(Object root) throws Exception {
|
||||
Object children = firstArrayFieldOfType(root, "[L" + ACTOR + ";");
|
||||
if (!(children instanceof Object[])) return null;
|
||||
for (Object child : (Object[]) children) {
|
||||
if (child == null) continue;
|
||||
Object skin = firstFieldOfType(child, SKIN);
|
||||
if (skin != null) return skin;
|
||||
Object deep = skinFromActor(child, 3);
|
||||
if (deep != null) return deep;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 顺着 actor 树往下找 Skin,最多 dig 层。 */
|
||||
private static Object skinFromActor(Object actor, int dig) throws Exception {
|
||||
if (actor == null || dig < 0) return null;
|
||||
Object direct = firstFieldOfType(actor, SKIN);
|
||||
if (direct != null) return direct;
|
||||
if (dig == 0) return null;
|
||||
Object children = firstArrayFieldOfType(actor, "[L" + ACTOR + ";");
|
||||
if (!(children instanceof Object[])) return null;
|
||||
for (Object child : (Object[]) children) {
|
||||
Object found = skinFromActor(child, dig - 1);
|
||||
if (found != null) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object firstFieldOfType(Object target, String typeName) throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals(typeName)) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(target);
|
||||
if (value != null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 从任意对象出发做有界广度搜索,找第一个 Skin 实例。 */
|
||||
private static Object findSkinDeep(Object start, int maxDepth, int maxNodes) {
|
||||
java.util.Set<Object> seen = java.util.Collections.newSetFromMap(
|
||||
new java.util.IdentityHashMap<Object, Boolean>());
|
||||
java.util.ArrayDeque<Object> queue = new java.util.ArrayDeque<Object>();
|
||||
java.util.ArrayDeque<Integer> depths = new java.util.ArrayDeque<Integer>();
|
||||
queue.add(start);
|
||||
depths.add(Integer.valueOf(0));
|
||||
int nodes = 0;
|
||||
while (!queue.isEmpty() && nodes++ < maxNodes) {
|
||||
Object current = queue.poll();
|
||||
int depth = depths.poll().intValue();
|
||||
if (current == null || !seen.add(current)) continue;
|
||||
if (current.getClass().getName().equals(SKIN)) return current;
|
||||
if (depth >= maxDepth) continue;
|
||||
if (current.getClass().isArray()) {
|
||||
if (!current.getClass().getComponentType().isPrimitive()) {
|
||||
for (Object item : (Object[]) current) {
|
||||
if (item != null) {
|
||||
queue.add(item);
|
||||
depths.add(Integer.valueOf(depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String name = current.getClass().getName();
|
||||
if (depth > 0 && (name.startsWith("java.") || name.startsWith("javax.")
|
||||
|| name.startsWith("sun.") || name.startsWith("org.lwjgl"))) continue;
|
||||
for (Class<?> type = current.getClass(); type != null; type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (field.getType().isPrimitive()) continue;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(current);
|
||||
if (value != null) {
|
||||
queue.add(value);
|
||||
depths.add(Integer.valueOf(depth + 1));
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过读不到的
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Object firstFieldOfTypeOld(Object target, String typeName) throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals(typeName)) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(target);
|
||||
if (value != null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object firstArrayFieldOfType(Object target, String descriptor)
|
||||
throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null; type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals(descriptor)) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(target);
|
||||
if (value != null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按"参数个数 + 参数类型"找方法(名字被混淆了,只能这么找)。 */
|
||||
/** Table.add(Actor):一个参数、参数能容纳该 actor、返回值非 void。 */
|
||||
private static Method findAddMethod(Class<?> tableClass, Class<?> actorClass)
|
||||
throws NoSuchMethodException {
|
||||
for (Method method : tableClass.getMethods()) {
|
||||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 1) continue;
|
||||
if (!types[0].isAssignableFrom(actorClass)) continue;
|
||||
if (method.getReturnType() == void.class) continue;
|
||||
return method;
|
||||
}
|
||||
throw new NoSuchMethodException("找不到 add");
|
||||
}
|
||||
|
||||
/** Table.row():无参、返回值就是表格自己。 */
|
||||
private static Method findRowMethod(Class<?> tableClass) throws NoSuchMethodException {
|
||||
for (Method method : tableClass.getMethods()) {
|
||||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||||
if (method.getParameterTypes().length != 0) continue;
|
||||
// row() 声明在 Table 上、返回 Table;Window 继承来的,所以不能要求返回值正好是 Window
|
||||
Class<?> returns = method.getReturnType();
|
||||
if (returns != Object.class && returns.isAssignableFrom(tableClass)) return method;
|
||||
}
|
||||
throw new NoSuchMethodException("找不到 row");
|
||||
}
|
||||
|
||||
private static Method findInstanceMethod(Class<?> owner, int parameterCount,
|
||||
Class<?> parameterType) throws NoSuchMethodException {
|
||||
for (Method method : owner.getMethods()) {
|
||||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != parameterCount) continue;
|
||||
boolean match = true;
|
||||
for (Class<?> type : types) {
|
||||
if (parameterType != null && type != parameterType) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return method;
|
||||
}
|
||||
throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static Method findMethod(Class<?> owner, int parameterCount, Class<?> parameterType)
|
||||
throws NoSuchMethodException {
|
||||
for (Method method : owner.getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != parameterCount) continue;
|
||||
boolean match = true;
|
||||
for (Class<?> type : types) {
|
||||
if (parameterType != null && type != parameterType) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return method;
|
||||
}
|
||||
throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount);
|
||||
}
|
||||
|
||||
/** Group 的 addActor(Actor):找一个只有一个参数、且参数能容纳该 actor 的 void 方法。 */
|
||||
private static void addChild(Object group, Object actor) throws Exception {
|
||||
for (Method method : group.getClass().getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 1) continue;
|
||||
if (!types[0].isAssignableFrom(actor.getClass())) continue;
|
||||
if (method.getReturnType() != void.class) continue;
|
||||
method.invoke(group, actor);
|
||||
System.err.println("[panel] 用 " + method.getDeclaringClass().getSimpleName() + "."
|
||||
+ method.getName() + " 挂上 " + actor.getClass().getSimpleName());
|
||||
return;
|
||||
}
|
||||
throw new NoSuchMethodException("找不到接纳 " + actor.getClass().getName() + " 的单参方法");
|
||||
}
|
||||
|
||||
/** 全量扫描静态字段(用 Unsafe,不触发类初始化)。留作备用入口。 */
|
||||
static Object scanStaticNoInit(ClassLoader loader, List<String> paths, String typeName) {
|
||||
try {
|
||||
Object unsafe = unsafe();
|
||||
Class<?> unsafeClass = unsafe.getClass();
|
||||
Method base = unsafeClass.getMethod("staticFieldBase", Field.class);
|
||||
Method offset = unsafeClass.getMethod("staticFieldOffset", Field.class);
|
||||
Method getObject = unsafeClass.getMethod("getObject", Object.class, long.class);
|
||||
for (String path : paths) {
|
||||
try (ZipFile zip = new ZipFile(path)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
if (!name.endsWith(".class")) continue;
|
||||
Class<?> type;
|
||||
try {
|
||||
type = Class.forName(name.substring(0, name.length() - 6).replace('/', '.'),
|
||||
false, loader);
|
||||
} catch (Throwable ignored) {
|
||||
continue;
|
||||
}
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (!field.getType().getName().equals(typeName)) continue;
|
||||
try {
|
||||
Object fieldBase = base.invoke(unsafe, field);
|
||||
long fieldOffset = ((Long) offset.invoke(unsafe, field)).longValue();
|
||||
Object value = getObject.invoke(unsafe, fieldBase, Long.valueOf(fieldOffset));
|
||||
if (value != null) return value;
|
||||
} catch (Throwable ignored) {
|
||||
// 读不到就跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 不是 zip
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 忽略
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object unsafe() throws Exception {
|
||||
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
|
||||
Field field = unsafeClass.getDeclaredField("theUnsafe");
|
||||
field.setAccessible(true);
|
||||
return field.get(null);
|
||||
}
|
||||
|
||||
/** 把载荷里所有非空的静态字段倒出来(用 Unsafe,不触发类初始化)。 */
|
||||
static void dumpStatics(ClassLoader loader, List<String> paths) {
|
||||
try {
|
||||
Object unsafe = unsafe();
|
||||
Class<?> unsafeClass = unsafe.getClass();
|
||||
Method base = unsafeClass.getMethod("staticFieldBase", Field.class);
|
||||
Method offset = unsafeClass.getMethod("staticFieldOffset", Field.class);
|
||||
Method getObject = unsafeClass.getMethod("getObject", Object.class, long.class);
|
||||
for (String path : paths) {
|
||||
try (ZipFile zip = new ZipFile(path)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
if (!name.endsWith(".class")) continue;
|
||||
Class<?> type;
|
||||
try {
|
||||
type = Class.forName(name.substring(0, name.length() - 6).replace('/', '.'),
|
||||
false, loader);
|
||||
} catch (Throwable ignored) {
|
||||
continue;
|
||||
}
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (field.getType().isPrimitive()) continue;
|
||||
try {
|
||||
Object fieldBase = base.invoke(unsafe, field);
|
||||
long fieldOffset = ((Long) offset.invoke(unsafe, field)).longValue();
|
||||
Object value = getObject.invoke(unsafe, fieldBase, Long.valueOf(fieldOffset));
|
||||
if (value != null) {
|
||||
System.err.println("[statics] " + type.getName() + "." + field.getName()
|
||||
+ " : " + field.getType().getName() + " = " + value.getClass().getName());
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// 不是 zip
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[statics] 失败: " + error);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> paths(java.net.URLClassLoader loader) {
|
||||
List<String> paths = new ArrayList<String>();
|
||||
for (java.net.URL url : loader.getURLs()) {
|
||||
if (!"file".equals(url.getProtocol())) continue;
|
||||
try {
|
||||
paths.add(new java.io.File(url.toURI()).getAbsolutePath());
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.awt.Window;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
* 点按钮时执行的读取动作。
|
||||
*
|
||||
* 现在只做"能拿到什么就先列什么":先把编辑器窗口里的真实组件树、以及编辑器进程里
|
||||
* 自建的对象图列出来,确认通道打通;随后再把它换成真正的"骨骼 / 插槽 / 附件"场景对象。
|
||||
*/
|
||||
public final class SceneReader {
|
||||
private SceneReader() {
|
||||
}
|
||||
|
||||
public static String read(ClassLoader editorLoader, Object stage) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append("=== 场景树里的对象 ===\n");
|
||||
try {
|
||||
listActors(stage, out, 0, 0);
|
||||
} catch (Throwable error) {
|
||||
out.append("读取失败: ").append(error).append('\n');
|
||||
}
|
||||
out.append("=== 编辑器进程里的窗口 ===\n");
|
||||
for (Window window : Window.getWindows()) {
|
||||
if (window.isVisible()) {
|
||||
out.append(" ").append(window.getClass().getName()).append(" ")
|
||||
.append(window.getWidth()).append('x').append(window.getHeight()).append('\n');
|
||||
if (window instanceof java.awt.Container) {
|
||||
collectComponents((java.awt.Container) window, out, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
out.append("\n=== 编辑器实例可达的自建对象 ===\n");
|
||||
try {
|
||||
Object editor = findEditor(editorLoader);
|
||||
if (editor == null) {
|
||||
out.append("(还没等到编辑器实例)\n");
|
||||
} else {
|
||||
walk(editor, out);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
out.append("读取失败: ").append(error).append('\n');
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
/** 顺着 Stage 的 actor 树把对象列出来(scene2d 里的一切都是 Actor)。 */
|
||||
private static int listActors(Object actor, StringBuilder out, int depth, int count) {
|
||||
if (actor == null || depth > 3 || count > 80) return count;
|
||||
StringBuilder pad = new StringBuilder();
|
||||
for (int i = 0; i < depth; i++) pad.append(" ");
|
||||
out.append(pad).append(actor.getClass().getName()).append('\n');
|
||||
count++;
|
||||
Object children = children(actor);
|
||||
if (children instanceof Object[]) {
|
||||
for (Object child : (Object[]) children) {
|
||||
count = listActors(child, out, depth + 1, count);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static Object children(Object actor) {
|
||||
for (Class<?> type = actor.getClass(); type != null; type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
String name = field.getType().getName();
|
||||
if (!name.startsWith("[L") || !name.endsWith("dl;")) continue;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(actor);
|
||||
if (value instanceof Object[]) return value;
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void collectComponents(java.awt.Container container, StringBuilder out, int depth) {
|
||||
if (container == null || depth > 3) return;
|
||||
for (java.awt.Component child : container.getComponents()) {
|
||||
out.append(indent(depth)).append(child.getClass().getName());
|
||||
if (child instanceof java.awt.Container) {
|
||||
out.append(" 子组件=").append(((java.awt.Container) child).getComponentCount());
|
||||
}
|
||||
out.append('\n');
|
||||
if (child instanceof java.awt.Container) {
|
||||
collectComponents((java.awt.Container) child, out, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String indent(int depth) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < depth; i++) builder.append(" ");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/** OG.D 是编辑器单例(Editor.<clinit> 里 new Editor() 挂上去的)。 */
|
||||
static Object findEditor(ClassLoader loader) throws Exception {
|
||||
Class<?> holder = Class.forName("OG", true, loader);
|
||||
for (Field field : holder.getDeclaredFields()) {
|
||||
if (!field.getName().equals("D") || !Modifier.isStatic(field.getModifiers())) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value != null && value.getClass().getName()
|
||||
.equals("com.esotericsoftware.spine.editor.Editor")) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void walk(Object root, StringBuilder out) {
|
||||
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||
List<Object> queue = new ArrayList<Object>();
|
||||
List<Integer> depths = new ArrayList<Integer>();
|
||||
queue.add(root);
|
||||
depths.add(Integer.valueOf(0));
|
||||
int head = 0;
|
||||
int printed = 0;
|
||||
while (head < queue.size() && printed < 120) {
|
||||
Object current = queue.get(head);
|
||||
int depth = depths.get(head).intValue();
|
||||
head++;
|
||||
if (current == null || !seen.add(current) || current.getClass().isArray()) continue;
|
||||
String name = current.getClass().getName();
|
||||
if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun.")
|
||||
|| name.startsWith("org.lwjgl") || name.startsWith("com.badlogic")) continue;
|
||||
Field[] fields = current.getClass().getDeclaredFields();
|
||||
out.append(indent(depth)).append(name).append(" 字段数=").append(fields.length)
|
||||
.append('\n');
|
||||
printed++;
|
||||
if (depth >= 2) continue;
|
||||
for (Field field : fields) {
|
||||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||||
try {
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(current);
|
||||
if (value == null) continue;
|
||||
queue.add(value);
|
||||
depths.add(Integer.valueOf(depth + 1));
|
||||
} catch (Throwable ignored) {
|
||||
// 跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
package com.spineview;
|
||||
|
||||
/**
|
||||
* 这个类必须由**编辑器的类加载器**加载(所以命令里要把 viewpanel/build/classes
|
||||
* 一起放进 URLClassLoader 的 URLs),因为它是被插进 uk.<init> 里的调用目标。
|
||||
*
|
||||
* Stage 在编辑器里只是 tD 里的一个局部变量,任何静态字段都摸不到它,
|
||||
* 唯一稳的入口就是在它构造完成的那一刻把它接住。
|
||||
*/
|
||||
public final class StageHook {
|
||||
private StageHook() {
|
||||
}
|
||||
|
||||
public static void onStage(Object stage) {
|
||||
try {
|
||||
System.err.println("[panel] 接住 Stage: " + stage.getClass().getName());
|
||||
Scene2dPanel.installForStage(stage, StageHook.class.getClassLoader());
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] StageHook 失败");
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package com.spineview;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
/**
|
||||
* 被 ViewPatcher 插进 tD.<init> 的调用目标 —— 必须由**编辑器的类加载器**加载,
|
||||
* 所以运行时要把 viewpanel/build/classes 放进 URLClassLoader 的 URLs。
|
||||
*
|
||||
* 它的活:为新的 "Spine View" 视图建一个面板 Actor,塞进 us.SPINEVIEW.D。
|
||||
* 之后编辑器的停靠/折叠框架就会像对待其它视图一样对待它。
|
||||
*/
|
||||
public final class ViewInstaller {
|
||||
private static final String SKIN = "com.badlogic.gdx.scenes.scene2d.ui.Skin";
|
||||
private static final String TABLE = "et";
|
||||
private static final String ACTOR = "dl";
|
||||
private static final String LABEL = "com.badlogic.gdx.scenes.scene2d.ui.Label";
|
||||
private static final String TEXT_BUTTON = "com.badlogic.gdx.scenes.scene2d.ui.TextButton";
|
||||
|
||||
private ViewInstaller() {
|
||||
}
|
||||
|
||||
/** 判定实验用:只打印一行,不做任何别的事。 */
|
||||
public static void ping() {
|
||||
System.err.println("[view] ping 被调用");
|
||||
}
|
||||
|
||||
public static void install() {
|
||||
// 这是从 tD 的构造函数中途调进来的:这时候编辑器自己的资源(皮肤贴图走 obt: 协议)
|
||||
// 还在读,我们如果在同一个调用里 Class.forName 触发别的类的初始化,会把流搞坏。
|
||||
// 所以这里只起个后台线程,等构造函数跑完、界面搭好之后再真正安装。
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(3000);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
}
|
||||
doInstall();
|
||||
}
|
||||
}, "spineview-view-installer");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static void doInstall() {
|
||||
try {
|
||||
ClassLoader loader = ViewInstaller.class.getClassLoader();
|
||||
installForLoader(loader);
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[view] 安装失败");
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从编辑器外面调用也可以:只要把编辑器那个 URLClassLoader 传进来。
|
||||
*
|
||||
* 视图常量已经由 view-agent 注册进 us 枚举,这里只负责把面板 Actor 填进
|
||||
* us.spineView.D —— 之后编辑器的停靠框架就会像对待其它视图一样对待它。
|
||||
*/
|
||||
public static boolean installForLoader(ClassLoader loader) {
|
||||
try {
|
||||
Class<?> viewEnum = Class.forName("us", true, loader);
|
||||
Object view = viewEnum.getMethod("valueOf", String.class).invoke(null, "spineView");
|
||||
Object skin = findSkin(loader);
|
||||
if (skin == null) {
|
||||
System.err.println("[view] 找不到 Skin");
|
||||
return false;
|
||||
}
|
||||
Object panel = buildPanel(loader, skin);
|
||||
for (Field field : viewEnum.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals(ACTOR)) continue;
|
||||
field.setAccessible(true);
|
||||
field.set(view, panel);
|
||||
System.err.println("[view] 已把 Spine View 面板挂到视图 " + view);
|
||||
return true;
|
||||
}
|
||||
System.err.println("[view] 视图枚举里找不到 Actor 字段");
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[view] 安装失败(稍后重试): " + error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Skin 挂在 OG 的静态字段上(字节码里 OG 有 LSkin;D)。 */
|
||||
private static Object findSkin(ClassLoader loader) throws Exception {
|
||||
Class<?> holder = Class.forName("OG", true, loader);
|
||||
for (Field field : holder.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (!field.getType().getName().equals(SKIN)) continue;
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(null);
|
||||
if (value != null) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑器的停靠框架会按惯例去找 "icon-tab" + 视图名 的图标,找不到就直接抛异常把界面带崩。
|
||||
* 所以要在它建面板之前,把我们的图标别名到已有视图(Tree)的图标上。
|
||||
* 皮肤是在建面板之前填好的,这里用极短间隔轮询去抢这个时间窗。
|
||||
*/
|
||||
public static boolean aliasIcon(ClassLoader loader) {
|
||||
try {
|
||||
Class<?> skinClass = Class.forName(SKIN, true, loader);
|
||||
// libGDX 的 Skin 是"按类型分桶"存的:add(name, obj) 用 obj.getClass() 当桶,
|
||||
// 而 getDrawable(name) 是去 Drawable.class 这个桶里查 —— 两者对不上就会"找不到"。
|
||||
// 所以要用三参数的 add(name, obj, Class),并且把该 drawable 的所有接口/父类都注册一遍。
|
||||
Method addTyped = null;
|
||||
for (Method method : skinClass.getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length == 3 && types[0] == String.class && types[1] == Object.class
|
||||
&& types[2] == Class.class && method.getReturnType() == void.class) {
|
||||
addTyped = method;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (addTyped == null) return false;
|
||||
|
||||
// 停靠框架会按状态取不同的图标名;后缀没对应的就退回基础图标,
|
||||
// 反正图标是复用的,不新建任何图片资源。
|
||||
String[] suffixes = { "", "-over", "-down", "-up", "-checked", "-unchecked",
|
||||
"-disabled", "-inactive", "-active", "-on", "-off", "-selected", "-open",
|
||||
"-closed", "-hover", "-pressed" };
|
||||
for (int attempt = 0; attempt < 30000; attempt++) {
|
||||
Object skin = findSkin(loader);
|
||||
if (skin != null) {
|
||||
boolean any = false;
|
||||
Object base = getDrawable(skinClass, skin, "icon-tabTree");
|
||||
if (base == null) continue;
|
||||
for (String suffix : suffixes) {
|
||||
Object icon = getDrawable(skinClass, skin, "icon-tabTree" + suffix);
|
||||
if (icon == null) icon = base;
|
||||
for (Class<?> type : bucketTypes(icon.getClass())) {
|
||||
try {
|
||||
addTyped.invoke(skin, "icon-tabSpineView" + suffix, icon, type);
|
||||
} catch (Throwable ignored) {
|
||||
// 某些类型不接受就跳过
|
||||
}
|
||||
}
|
||||
any = true;
|
||||
}
|
||||
if (any) {
|
||||
System.err.println("[view] 已给 SpineView 注册图标别名");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Thread.sleep(2);
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[view] 图标别名失败: " + error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** drawable 的所有接口 + 父类 —— 把可能当桶用的类型都列出来。 */
|
||||
private static java.util.List<Class<?>> bucketTypes(Class<?> type) {
|
||||
java.util.List<Class<?>> list = new java.util.ArrayList<Class<?>>();
|
||||
for (Class<?> current = type; current != null && current != Object.class;
|
||||
current = current.getSuperclass()) {
|
||||
list.add(current);
|
||||
for (Class<?> iface : current.getInterfaces()) {
|
||||
if (!list.contains(iface)) list.add(iface);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Skin 里有一堆只有一个 String 参数的方法(取字体、取颜色、取 drawable…),名字都被混淆了。
|
||||
* 这里挨个试,哪个能用 "icon-tabTree" 取到非空对象、又不会抛异常,就是 getDrawable。
|
||||
*/
|
||||
private static Object getDrawable(Class<?> skinClass, Object skin, String name) {
|
||||
for (Method method : skinClass.getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 1 || types[0] != String.class) continue;
|
||||
if (method.getReturnType() == void.class) continue;
|
||||
if (method.getReturnType() == String.class) continue;
|
||||
try {
|
||||
Object value = method.invoke(skin, name);
|
||||
if (value != null) return value;
|
||||
} catch (Throwable ignored) {
|
||||
// 取错类型会抛,继续试下一个
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 面板内容:一个 Table,里面一行按钮、一行文字显示。 */
|
||||
private static Object buildPanel(ClassLoader loader, Object skin) throws Exception {
|
||||
Class<?> skinClass = Class.forName(SKIN, true, loader);
|
||||
Class<?> tableClass = Class.forName(TABLE, true, loader);
|
||||
Class<?> buttonClass = Class.forName(TEXT_BUTTON, true, loader);
|
||||
Class<?> labelClass = Class.forName(LABEL, true, loader);
|
||||
|
||||
Object table = tableClass.getConstructor().newInstance();
|
||||
Object button = buttonClass.getConstructor(String.class, skinClass)
|
||||
.newInstance("Read Scene Objects", skin);
|
||||
Object label = labelClass.getConstructor(CharSequence.class, skinClass)
|
||||
.newInstance("Click the button to list scene objects.", skin);
|
||||
|
||||
// 注意:这里必须用 Table 的单元格布局(add/row),不要换成 addActor + 绝对定位 ——
|
||||
// 那个版本在停靠框架里会抛 "Tab not found: Spine View",把安装打断。
|
||||
Method add = null;
|
||||
Method row = null;
|
||||
for (Method method : tableClass.getMethods()) {
|
||||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length == 1 && types[0].isAssignableFrom(buttonClass)
|
||||
&& method.getReturnType() != void.class) {
|
||||
if (add == null) add = method;
|
||||
}
|
||||
if (types.length == 0 && method.getReturnType() != Object.class
|
||||
&& method.getReturnType().isAssignableFrom(tableClass)) {
|
||||
if (row == null) row = method;
|
||||
}
|
||||
}
|
||||
if (add != null) {
|
||||
add.invoke(table, button);
|
||||
if (row != null) row.invoke(table);
|
||||
add.invoke(table, label);
|
||||
} else {
|
||||
System.err.println("[view] 找不到 Table.add");
|
||||
}
|
||||
Scene2dPanel.wireButton(button, label, loader);
|
||||
return table;
|
||||
}
|
||||
|
||||
private static Method findInstanceMethod(Class<?> owner, int parameterCount,
|
||||
Class<?> parameterType) throws NoSuchMethodException {
|
||||
for (Method method : owner.getMethods()) {
|
||||
if (Modifier.isStatic(method.getModifiers())) continue;
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != parameterCount) continue;
|
||||
boolean match = true;
|
||||
for (Class<?> type : types) {
|
||||
if (parameterType != null && type != parameterType) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return method;
|
||||
}
|
||||
throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount);
|
||||
}
|
||||
|
||||
/** Group.addActor(Actor):一个参数、能容纳该 actor、返回 void。 */
|
||||
private static void addChild(Object group, Object actor) throws Exception {
|
||||
for (Method method : group.getClass().getMethods()) {
|
||||
Class<?>[] types = method.getParameterTypes();
|
||||
if (types.length != 1) continue;
|
||||
if (!types[0].isAssignableFrom(actor.getClass())) continue;
|
||||
if (method.getReturnType() != void.class) continue;
|
||||
method.invoke(group, actor);
|
||||
return;
|
||||
}
|
||||
throw new NoSuchMethodException("找不到接纳 " + actor.getClass().getName() + " 的方法");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user