面板(viewpanel/src/com/spineview/) - ObjectPanel:Refresh / New Bone / New Slot / New BBox / New Path / Delete / Rename / Inspect + 名字输入框 + 对象列表(点行=选中)+ 状态行,全英文。 - SceneObjects:查(Tree 控件按层级/扁平列全部对象)、选中(VZ.D(Tree.Node,boolean) —— VZ.D(Object) 内部只是 Selection.L(),不真选中)、读当前选择(VZ.B())。 - SceneActions:增/删/改 + 撤销包装(un.D()/un.B())+ 树节点重建(VZ.D(GP,true,true)); 统一在编辑器渲染线程执行(OG.B(Runnable);OG.I() 有线程断言,非渲染线程会抛 RuntimeException); 输入名字对话框自动化:找到『有 TextField 字段 + L(Object)V 回调』的对话框对象 → 填名字 → 调 L(true)。 - 实测:骨骼新建自动编号(bone7/bone8);模型层允许重名、按名字查只返回第一个;删父骨骼连子节点一起删; 不能在渲染线程里 sleep(有些创建是下一帧做的)。 - 探针:-Dspine.probe=objects|panels|select|crud|matrix|panel。 可识别源码版(工具) - tools/deobf/Renamer.java:ASM Remapper 只改成员名不改类名(indy 运行时要用加密串里的类名反射查)。 - tools/deobf/anchors.tsv(手动 14 条有证据的锚点)+ anchors-libgdx.tsv(MapMethods 对 gdx-1.11.0 指纹自动生成 178 条)→ names.tsv 192 条。 - 产物 ~/Desktop/spine398/src-named(4176 类全量,原 src-readable 不动);已验证 Table.add/clearChildren、SelectBox.getSelected/setSelected/setSelectedIndex、Gp.name。 - 文档:混淆名对照表.md 新增第六节(工具链 + 已验证 + 已知限制)。
265 lines
11 KiB
Java
265 lines
11 KiB
Java
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 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 static final String SCROLL_PANE = "com.badlogic.gdx.scenes.scene2d.ui.ScrollPane";
|
||
|
||
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 上 `I` 既有无参返回 Cell 的 row(),也有无参返回 Table 的 top()),
|
||
* 少判一个条件就会调到别的方法上。名字出处见 {@link Obf}。
|
||
*/
|
||
private static Object buildPanel(ClassLoader loader, Object skin) throws Exception {
|
||
// 面板内容(增删改查)在 ObjectPanel 里,这里只负责把它建出来。
|
||
return ObjectPanel.build(loader, skin);
|
||
}
|
||
|
||
/**
|
||
* 在 Cell 上调一串"无参、返回 Cell"的布局方法(fill/expand/top…,名字见 {@link Obf})。
|
||
* 单个调用失败只记一行日志,不让整个面板装不上。
|
||
*/
|
||
private static void layoutCell(Object cell, String... names) {
|
||
for (String name : names) {
|
||
try {
|
||
Method method = cell.getClass().getMethod(name);
|
||
method.setAccessible(true);
|
||
method.invoke(cell);
|
||
} catch (Throwable error) {
|
||
System.err.println("[view] 单元格布局 " + name + " 调用失败: " + error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 调用 Table 上的实例方法:名字 + 参数类型 + 返回类型一起判(同名方法太多)。 */
|
||
private static Object call(Object target, String name, Class<?>[] parameterTypes,
|
||
Class<?> returnType, Object... arguments) throws Exception {
|
||
for (Method method : target.getClass().getMethods()) {
|
||
if (!method.getName().equals(name)) continue;
|
||
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
|
||
if (returnType != null && method.getReturnType() != returnType) continue;
|
||
method.setAccessible(true);
|
||
return method.invoke(target, arguments);
|
||
}
|
||
throw new NoSuchMethodException(target.getClass().getName() + "." + name
|
||
+ " 参数=" + java.util.Arrays.toString(parameterTypes));
|
||
}
|
||
|
||
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() + " 的方法");
|
||
}
|
||
}
|