热更(不重启)整理新建附件:用编辑器自身 API 重置图片附件变换 + 删除占位(skin)附件
- 确认 GO 的字段/入口:x=D(float)、y=B(float)、rotation=S、scaleX=L、scaleY=I、width=C、height=Z; GO.D(FFFFF)V 就是编辑器自己的 setTransform(x,y,rotation,scaleX,scaleY)。 依据:载荷 JSON 写入器 sb_0.java:669-675 + javap -c GO(两条独立证据), width/height 与 mega_dg4.png 真实像素 157x154 对上。 - 确认删除入口:Gn.L()V => skeleton.removeAttachment(this,true,true);GP.L=附件表、GP.Z=插槽表; 占位附件是载荷类 Hi(hi_2.java 的 I() 返回 "placeholder")。 - 实机(JDK8 attach,进程 56177 全程没重启):20 个新建插槽的图片附件 x/y/rotation 归 0、scale 归 1; 20 个 Hi 占位附件删除;骨架附件表 43 -> 23;逐个核对删除占位没连带删掉图片附件。 - 顺带把 tools/live(attach 热更工具源码)入库,jar/build 加进 .gitignore。
This commit is contained in:
1 parent
0886ba647c
commit
7f519edc67
22 files changed
+3780
No files matched your search
@@ -25,6 +25,9 @@ rea/
|
|||||||
viewpanel/build/
|
viewpanel/build/
|
||||||
viewpanel/compat-classes/
|
viewpanel/compat-classes/
|
||||||
*.class
|
*.class
|
||||||
|
# 热更(attach)小工具:只留源码,jar 和编译输出是临时的
|
||||||
|
tools/live/build*/
|
||||||
|
tools/live/*.jar
|
||||||
|
|
||||||
# ---- 从载荷派生的批量产物(本机可重新生成)----
|
# ---- 从载荷派生的批量产物(本机可重新生成)----
|
||||||
viewpanel/analysis/spine-398/
|
viewpanel/analysis/spine-398/
|
||||||
|
|||||||
@@ -103,3 +103,30 @@ dR I (F)LdR; height
|
|||||||
# 故 B()F = getHeight、D()F = getWidth。
|
# 故 B()F = getHeight、D()F = getWidth。
|
||||||
dt B ()F getHeight
|
dt B ()F getHeight
|
||||||
dt D ()F getWidth
|
dt D ()F getWidth
|
||||||
|
|
||||||
|
# ---- 图片附件 GO 的 7 个 float 字段(2026-10-16 确认,两条独立证据)----
|
||||||
|
# 证据 1:编辑器自己的 JSON 写入器 sb_0.java:669-675(写 region 时依次读)
|
||||||
|
# x=GO.D y=GO.B scaleX=GO.L scaleY=GO.I rotation=GO.S width=GO.C height=GO.Z
|
||||||
|
# 旁证:width/height 与图片实际像素一致(mega_dg4.png = 157x154,运行时读到的 C=157.0 Z=154.0)
|
||||||
|
# 证据 2:GO 自己的 setTransform 字节码 javap -c GO 的 D(FFFFF)V:
|
||||||
|
# putfield D(第1个参数) -> putfield B(2) -> putfield S(3) -> putfield L(4) -> putfield I(5)
|
||||||
|
# => D(float) = x、B(float) = y(这两个字段名与别的字段重复,必须按类型取 float 那个)
|
||||||
|
GO D F x
|
||||||
|
GO B F y
|
||||||
|
# 图片附件的"设置变换"入口(编辑器自己用的):setTransform(x, y, rotation, scaleX, scaleY)
|
||||||
|
GO D (FFFFF)V setTransform
|
||||||
|
# 图片附件没有 shear 字段:region 只有 x/y/rotation/scaleX/scaleY(+ 只读的 width/height)
|
||||||
|
# ---- 附件删除(编辑器自己的删除流程)----
|
||||||
|
# 证据:javap -c Gn 的 L()V => this.GU.skeleton.removeAttachment(this, true, true)
|
||||||
|
# GP 的 D(Gn,ZZ)V 字节码里 getfield L:LfI; fI.B(Object,boolean) 把它从附件表移除,
|
||||||
|
# 并同步清掉 GU 上指向它的引用(同一方法里遍历 Z:LfI 即插槽表)。
|
||||||
|
Gn L ()V delete
|
||||||
|
GP L (LGn;ZZ)V removeAttachment
|
||||||
|
GP L LfI attachments
|
||||||
|
GP Z LfI slots
|
||||||
|
# ---- 附件种类 / 占位附件 ----
|
||||||
|
# 证据:可读源码 hi_2.java(= 载荷类 Hi)的 I() 直接 return "placeholder";
|
||||||
|
# 它出现在骨架附件表里但"既不是当前附件也不是静止附件",就是 skin/占位附件。
|
||||||
|
Gn I ()Ljava/lang/String; type
|
||||||
|
# 附件上指向自己所属占位附件的引用:GP.removeAttachment 的占位分支按它级联删除
|
||||||
|
Gn D LHi; placeholderRef
|
||||||
|
Can't render this file because it contains an unexpected character in line 72 and column 99.
|
@@ -1,14 +1,23 @@
|
|||||||
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
|
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
|
||||||
# owner name desc newName
|
# owner name desc newName
|
||||||
|
GO B F y
|
||||||
GO C F width
|
GO C F width
|
||||||
|
GO D (FFFFF)V setTransform
|
||||||
|
GO D F x
|
||||||
GO I F scaleY
|
GO I F scaleY
|
||||||
GO L F scaleX
|
GO L F scaleX
|
||||||
GO S F rotation
|
GO S F rotation
|
||||||
GO Z F height
|
GO Z F height
|
||||||
|
GP L (LGn;ZZ)V removeAttachment
|
||||||
|
GP L LfI attachments
|
||||||
|
GP Z LfI slots
|
||||||
GU D LGn; attachment
|
GU D LGn; attachment
|
||||||
GU D LGp; bone
|
GU D LGp; bone
|
||||||
GU D Lcom/badlogic/gdx/graphics/Color; color
|
GU D Lcom/badlogic/gdx/graphics/Color; color
|
||||||
GU D Ljava/lang/String; name
|
GU D Ljava/lang/String; name
|
||||||
|
Gn D LHi; placeholderRef
|
||||||
|
Gn I ()Ljava/lang/String; type
|
||||||
|
Gn L ()V delete
|
||||||
Gp B F setupY
|
Gp B F setupY
|
||||||
Gp B Lcom/badlogic/gdx/graphics/Color; color
|
Gp B Lcom/badlogic/gdx/graphics/Color; color
|
||||||
Gp C F setupShear
|
Gp C F setupShear
|
||||||
|
|||||||
|
@@ -0,0 +1,14 @@
|
|||||||
|
import com.sun.tools.attach.VirtualMachine;
|
||||||
|
|
||||||
|
public final class Attacher {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
String pid = args[0], jar = args[1], arg = args.length > 2 ? args[2] : "10";
|
||||||
|
VirtualMachine vm = VirtualMachine.attach(pid);
|
||||||
|
try {
|
||||||
|
vm.loadAgent(jar, arg);
|
||||||
|
} finally {
|
||||||
|
vm.detach();
|
||||||
|
}
|
||||||
|
System.out.println("loaded agent into pid " + pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。
|
||||||
|
* 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。
|
||||||
|
*
|
||||||
|
* 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP):
|
||||||
|
* new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null)
|
||||||
|
* → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true)
|
||||||
|
*/
|
||||||
|
public final class BoneMaker {
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
int count = 10;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run(inst, count);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[live] BoneMaker 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(Instrumentation inst, int count) throws Exception {
|
||||||
|
// 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的)
|
||||||
|
// 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)),
|
||||||
|
// 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。
|
||||||
|
ClassLoader loader = null;
|
||||||
|
for (Class<?> c : inst.getAllLoadedClasses()) {
|
||||||
|
ClassLoader cl = c.getClassLoader();
|
||||||
|
if (cl instanceof java.net.URLClassLoader) {
|
||||||
|
loader = cl;
|
||||||
|
System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
System.err.println("[live] 找不到编辑器类加载器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName());
|
||||||
|
|
||||||
|
// 2) 当前选中对象(复用面板里已有的读取逻辑)
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader);
|
||||||
|
Object parent = sceneObjects.getMethod("selected", ClassLoader.class)
|
||||||
|
.invoke(null, loader);
|
||||||
|
if (parent == null) {
|
||||||
|
System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object skeleton = sceneObjects.getMethod("skeletonOf", Object.class).invoke(null, parent);
|
||||||
|
System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class)
|
||||||
|
.invoke(null, parent) + ",骨架 = " + skeleton);
|
||||||
|
|
||||||
|
// 3) 用编辑器自身 API 建 N 根
|
||||||
|
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||||
|
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||||
|
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||||
|
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||||
|
Class<?> treeViewClass = Class.forName("VZ", true, loader);
|
||||||
|
|
||||||
|
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||||
|
Method addBone = null;
|
||||||
|
for (Method m : skeletonClass.getMethods()) {
|
||||||
|
if (m.getName().equals("D") && m.getParameterTypes().length == 1
|
||||||
|
&& m.getParameterTypes()[0] == boneClass) {
|
||||||
|
addBone = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method nameGen = null;
|
||||||
|
for (Method m : og.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass
|
||||||
|
&& m.getReturnType() == String.class) {
|
||||||
|
nameGen = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method refresh = null;
|
||||||
|
for (Method m : treeViewClass.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class
|
||||||
|
&& t[2] == boolean.class) {
|
||||||
|
refresh = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object editor = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
editor = f.get(null);
|
||||||
|
if (editor != null) break;
|
||||||
|
}
|
||||||
|
Object treeView = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != treeViewClass) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
treeView = f.get(null);
|
||||||
|
if (treeView != null) break;
|
||||||
|
}
|
||||||
|
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
if (addBone == null || nameGen == null || treeView == null) {
|
||||||
|
System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen
|
||||||
|
+ " treeView=" + treeView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBone.setAccessible(true);
|
||||||
|
nameGen.setAccessible(true);
|
||||||
|
|
||||||
|
// skeleton 的骨骼数组字段(fI)
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : skeletonClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == arrayClass) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
bones = f.get(skeleton);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 父骨骼的名字(String 字段)
|
||||||
|
Object parentName = null;
|
||||||
|
for (Field f : boneClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
parentName = f.get(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 撤销包裹(un.D()/un.B())—— 整批算一次
|
||||||
|
Class<?> undo = Class.forName("un", true, loader);
|
||||||
|
Method begin = null, commit = null;
|
||||||
|
for (Method m : undo.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) begin = m;
|
||||||
|
if (m.getName().equals("B")) commit = m;
|
||||||
|
}
|
||||||
|
if (begin != null) {
|
||||||
|
begin.setAccessible(true);
|
||||||
|
begin.invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父)
|
||||||
|
Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成
|
||||||
|
setString(bone, boneClass, generated); // name
|
||||||
|
addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone
|
||||||
|
setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x
|
||||||
|
setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y
|
||||||
|
setField(bone, "B", colorClass, red); // color
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
commit.setAccessible(true);
|
||||||
|
commit.invoke(null);
|
||||||
|
}
|
||||||
|
if (refresh != null && editor != null) {
|
||||||
|
refresh.setAccessible(true);
|
||||||
|
refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次
|
||||||
|
System.err.println("[live] 已刷新树节点");
|
||||||
|
}
|
||||||
|
System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setFloat(Object target, String name, float value) throws Exception {
|
||||||
|
setField(target, name, float.class, Float.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setString(Object target, Class<?> boneClass, Object value) throws Exception {
|
||||||
|
setField(target, "D", String.class, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。
|
||||||
|
* 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。
|
||||||
|
*
|
||||||
|
* 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP):
|
||||||
|
* new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null)
|
||||||
|
* → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true)
|
||||||
|
*/
|
||||||
|
public final class BoneMaker2 {
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
int count = 10;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run(inst, count);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[live] BoneMaker 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(Instrumentation inst, int count) throws Exception {
|
||||||
|
// 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的)
|
||||||
|
// 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)),
|
||||||
|
// 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。
|
||||||
|
ClassLoader loader = null;
|
||||||
|
for (Class<?> c : inst.getAllLoadedClasses()) {
|
||||||
|
ClassLoader cl = c.getClassLoader();
|
||||||
|
if (cl instanceof java.net.URLClassLoader) {
|
||||||
|
loader = cl;
|
||||||
|
System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
System.err.println("[live] 找不到编辑器类加载器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName());
|
||||||
|
|
||||||
|
// 2) 当前选中对象(复用面板里已有的读取逻辑)
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader);
|
||||||
|
Object parent = sceneObjects.getMethod("selected", ClassLoader.class)
|
||||||
|
.invoke(null, loader);
|
||||||
|
if (parent == null) {
|
||||||
|
System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object skeleton = sceneObjects.getMethod("skeletonOf", Object.class).invoke(null, parent);
|
||||||
|
System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class)
|
||||||
|
.invoke(null, parent) + ",骨架 = " + skeleton);
|
||||||
|
|
||||||
|
// 3) 用编辑器自身 API 建 N 根
|
||||||
|
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||||
|
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||||
|
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||||
|
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||||
|
Class<?> treeViewClass = Class.forName("VZ", true, loader);
|
||||||
|
|
||||||
|
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||||
|
Method addBone = null;
|
||||||
|
for (Method m : skeletonClass.getMethods()) {
|
||||||
|
if (m.getName().equals("D") && m.getParameterTypes().length == 1
|
||||||
|
&& m.getParameterTypes()[0] == boneClass) {
|
||||||
|
addBone = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method nameGen = null;
|
||||||
|
for (Method m : og.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass
|
||||||
|
&& m.getReturnType() == String.class) {
|
||||||
|
nameGen = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method refresh = null;
|
||||||
|
for (Method m : treeViewClass.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class
|
||||||
|
&& t[2] == boolean.class) {
|
||||||
|
refresh = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object editor = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
editor = f.get(null);
|
||||||
|
if (editor != null) break;
|
||||||
|
}
|
||||||
|
Object treeView = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != treeViewClass) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
treeView = f.get(null);
|
||||||
|
if (treeView != null) break;
|
||||||
|
}
|
||||||
|
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
if (addBone == null || nameGen == null || treeView == null) {
|
||||||
|
System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen
|
||||||
|
+ " treeView=" + treeView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBone.setAccessible(true);
|
||||||
|
nameGen.setAccessible(true);
|
||||||
|
|
||||||
|
// skeleton 的骨骼数组字段(fI)
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : skeletonClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == arrayClass) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
bones = f.get(skeleton);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 父骨骼的名字(String 字段)
|
||||||
|
Object parentName = null;
|
||||||
|
for (Field f : boneClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
parentName = f.get(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 撤销包裹(un.D()/un.B())—— 整批算一次
|
||||||
|
Class<?> undo = Class.forName("un", true, loader);
|
||||||
|
Method begin = null, commit = null;
|
||||||
|
for (Method m : undo.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) begin = m;
|
||||||
|
if (m.getName().equals("B")) commit = m;
|
||||||
|
}
|
||||||
|
if (begin != null) {
|
||||||
|
begin.setAccessible(true);
|
||||||
|
begin.invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父)
|
||||||
|
Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成
|
||||||
|
setString(bone, boneClass, generated); // name
|
||||||
|
addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone
|
||||||
|
setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x
|
||||||
|
setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y
|
||||||
|
setField(bone, "B", colorClass, red); // color
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
commit.setAccessible(true);
|
||||||
|
commit.invoke(null);
|
||||||
|
}
|
||||||
|
if (refresh != null && editor != null) {
|
||||||
|
refresh.setAccessible(true);
|
||||||
|
refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次
|
||||||
|
System.err.println("[live] 已刷新树节点");
|
||||||
|
}
|
||||||
|
System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setFloat(Object target, String name, float value) throws Exception {
|
||||||
|
setField(target, name, float.class, Float.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setString(Object target, Class<?> boneClass, Object value) throws Exception {
|
||||||
|
setField(target, "D", String.class, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。
|
||||||
|
* 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。
|
||||||
|
*
|
||||||
|
* 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP):
|
||||||
|
* new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null)
|
||||||
|
* → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true)
|
||||||
|
*/
|
||||||
|
public final class BoneMaker3 {
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
int count = 10;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run(inst, count);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[live] BoneMaker 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(Instrumentation inst, int count) throws Exception {
|
||||||
|
// 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的)
|
||||||
|
// 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)),
|
||||||
|
// 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。
|
||||||
|
ClassLoader loader = null;
|
||||||
|
for (Class<?> c : inst.getAllLoadedClasses()) {
|
||||||
|
ClassLoader cl = c.getClassLoader();
|
||||||
|
if (cl instanceof java.net.URLClassLoader) {
|
||||||
|
loader = cl;
|
||||||
|
System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
System.err.println("[live] 找不到编辑器类加载器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName());
|
||||||
|
|
||||||
|
// 2) 当前选中对象(复用面板里已有的读取逻辑)
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader);
|
||||||
|
Object parent = sceneObjects.getMethod("selected", ClassLoader.class)
|
||||||
|
.invoke(null, loader);
|
||||||
|
if (parent == null) {
|
||||||
|
System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Class<?> sceneActions = Class.forName("com.spineview.SceneActions", true, loader);
|
||||||
|
Object skeleton = sceneActions.getMethod("skeletonOf", Object.class).invoke(null, parent);
|
||||||
|
System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class)
|
||||||
|
.invoke(null, parent) + ",骨架 = " + skeleton);
|
||||||
|
|
||||||
|
// 3) 用编辑器自身 API 建 N 根
|
||||||
|
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||||
|
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||||
|
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||||
|
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||||
|
Class<?> treeViewClass = Class.forName("VZ", true, loader);
|
||||||
|
|
||||||
|
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||||
|
Method addBone = null;
|
||||||
|
for (Method m : skeletonClass.getMethods()) {
|
||||||
|
if (m.getName().equals("D") && m.getParameterTypes().length == 1
|
||||||
|
&& m.getParameterTypes()[0] == boneClass) {
|
||||||
|
addBone = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method nameGen = null;
|
||||||
|
for (Method m : og.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass
|
||||||
|
&& m.getReturnType() == String.class) {
|
||||||
|
nameGen = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method refresh = null;
|
||||||
|
for (Method m : treeViewClass.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class
|
||||||
|
&& t[2] == boolean.class) {
|
||||||
|
refresh = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object editor = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
editor = f.get(null);
|
||||||
|
if (editor != null) break;
|
||||||
|
}
|
||||||
|
Object treeView = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != treeViewClass) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
treeView = f.get(null);
|
||||||
|
if (treeView != null) break;
|
||||||
|
}
|
||||||
|
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
if (addBone == null || nameGen == null || treeView == null) {
|
||||||
|
System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen
|
||||||
|
+ " treeView=" + treeView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBone.setAccessible(true);
|
||||||
|
nameGen.setAccessible(true);
|
||||||
|
|
||||||
|
// skeleton 的骨骼数组字段(fI)
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : skeletonClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == arrayClass) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
bones = f.get(skeleton);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 父骨骼的名字(String 字段)
|
||||||
|
Object parentName = null;
|
||||||
|
for (Field f : boneClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
parentName = f.get(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 撤销包裹(un.D()/un.B())—— 整批算一次
|
||||||
|
Class<?> undo = Class.forName("un", true, loader);
|
||||||
|
Method begin = null, commit = null;
|
||||||
|
for (Method m : undo.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) begin = m;
|
||||||
|
if (m.getName().equals("B")) commit = m;
|
||||||
|
}
|
||||||
|
if (begin != null) {
|
||||||
|
begin.setAccessible(true);
|
||||||
|
begin.invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父)
|
||||||
|
Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成
|
||||||
|
setString(bone, boneClass, generated); // name
|
||||||
|
addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone
|
||||||
|
setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x
|
||||||
|
setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y
|
||||||
|
setField(bone, "B", colorClass, red); // color
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
commit.setAccessible(true);
|
||||||
|
commit.invoke(null);
|
||||||
|
}
|
||||||
|
if (refresh != null && editor != null) {
|
||||||
|
refresh.setAccessible(true);
|
||||||
|
refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次
|
||||||
|
System.err.println("[live] 已刷新树节点");
|
||||||
|
}
|
||||||
|
System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setFloat(Object target, String name, float value) throws Exception {
|
||||||
|
setField(target, name, float.class, Float.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setString(Object target, Class<?> boneClass, Object value) throws Exception {
|
||||||
|
setField(target, "D", String.class, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。
|
||||||
|
* 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。
|
||||||
|
*
|
||||||
|
* 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP):
|
||||||
|
* new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null)
|
||||||
|
* → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true)
|
||||||
|
*/
|
||||||
|
public final class BoneMaker43 {
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
int count = 10;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run(inst, count);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[live] BoneMaker 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(Instrumentation inst, int count) throws Exception {
|
||||||
|
// 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的)
|
||||||
|
// 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)),
|
||||||
|
// 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。
|
||||||
|
ClassLoader loader = null;
|
||||||
|
for (Class<?> c : inst.getAllLoadedClasses()) {
|
||||||
|
ClassLoader cl = c.getClassLoader();
|
||||||
|
if (cl instanceof java.net.URLClassLoader) {
|
||||||
|
loader = cl;
|
||||||
|
System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
System.err.println("[live] 找不到编辑器类加载器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName());
|
||||||
|
|
||||||
|
// 2) 当前选中对象(复用面板里已有的读取逻辑)
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader);
|
||||||
|
Object parent = sceneObjects.getMethod("selected", ClassLoader.class)
|
||||||
|
.invoke(null, loader);
|
||||||
|
if (parent == null) {
|
||||||
|
System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Class<?> sceneActions = Class.forName("com.spineview.SceneActions", true, loader);
|
||||||
|
Object skeleton = sceneActions.getMethod("skeletonOf", Object.class).invoke(null, parent);
|
||||||
|
System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class)
|
||||||
|
.invoke(null, parent) + ",骨架 = " + skeleton);
|
||||||
|
|
||||||
|
// 3) 用编辑器自身 API 建 N 根
|
||||||
|
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||||
|
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||||
|
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||||
|
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||||
|
Class<?> treeViewClass = Class.forName("VZ", true, loader);
|
||||||
|
|
||||||
|
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||||
|
Method addBone = null;
|
||||||
|
for (Method m : skeletonClass.getMethods()) {
|
||||||
|
if (m.getName().equals("D") && m.getParameterTypes().length == 1
|
||||||
|
&& m.getParameterTypes()[0] == boneClass) {
|
||||||
|
addBone = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method nameGen = null;
|
||||||
|
for (Method m : og.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass
|
||||||
|
&& m.getReturnType() == String.class) {
|
||||||
|
nameGen = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method refresh = null;
|
||||||
|
for (Method m : treeViewClass.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class
|
||||||
|
&& t[2] == boolean.class) {
|
||||||
|
refresh = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object editor = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
editor = f.get(null);
|
||||||
|
if (editor != null) break;
|
||||||
|
}
|
||||||
|
Object treeView = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != treeViewClass) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
treeView = f.get(null);
|
||||||
|
if (treeView != null) break;
|
||||||
|
}
|
||||||
|
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
if (addBone == null || nameGen == null || treeView == null) {
|
||||||
|
System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen
|
||||||
|
+ " treeView=" + treeView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBone.setAccessible(true);
|
||||||
|
nameGen.setAccessible(true);
|
||||||
|
|
||||||
|
// skeleton 的骨骼数组字段(fI)
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : skeletonClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == arrayClass) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
bones = f.get(skeleton);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 父骨骼的名字(String 字段)
|
||||||
|
Object parentName = null;
|
||||||
|
for (Field f : boneClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
parentName = f.get(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 整段挪到**编辑器的渲染线程**上跑:un.D()/VZ.D() 内部有线程断言(OG.I()),
|
||||||
|
// 非渲染线程会抛 RuntimeException(这就是上一次失败的原因)。
|
||||||
|
final java.lang.Object[] shared = new java.lang.Object[] {parent, skeleton, boneClass,
|
||||||
|
skeletonClass, arrayClass, slotClass, colorClass, addBone, nameGen, refresh, editor,
|
||||||
|
treeView, bones, parentName};
|
||||||
|
final int[] madeOut = new int[1];
|
||||||
|
final java.lang.Throwable[] failure = new java.lang.Throwable[1];
|
||||||
|
java.lang.reflect.Method runOnRender = null;
|
||||||
|
for (java.lang.reflect.Method m : og.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals("B")) continue;
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 1 && t[0] == Runnable.class) {
|
||||||
|
runOnRender = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Runnable job = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// 4) 撤销包裹(un.D()/un.B())—— 整批算一次
|
||||||
|
Class<?> undo = Class.forName("un", true, loader);
|
||||||
|
Method begin = null, commit = null;
|
||||||
|
for (Method m : undo.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) begin = m;
|
||||||
|
if (m.getName().equals("B")) commit = m;
|
||||||
|
}
|
||||||
|
if (begin != null) {
|
||||||
|
begin.setAccessible(true);
|
||||||
|
begin.invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父)
|
||||||
|
Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成
|
||||||
|
setString(bone, boneClass, generated); // name
|
||||||
|
addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone
|
||||||
|
setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x
|
||||||
|
setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y
|
||||||
|
setField(bone, "B", colorClass, red); // color
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
commit.setAccessible(true);
|
||||||
|
commit.invoke(null);
|
||||||
|
}
|
||||||
|
if (refresh != null && editor != null) {
|
||||||
|
refresh.setAccessible(true);
|
||||||
|
refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次
|
||||||
|
System.err.println("[live] 已刷新树节点");
|
||||||
|
}
|
||||||
|
} catch (Throwable error) {
|
||||||
|
failure[0] = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (runOnRender == null) {
|
||||||
|
System.err.println("[live] 找不到 OG.B(Runnable)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runOnRender.setAccessible(true);
|
||||||
|
runOnRender.invoke(null, job);
|
||||||
|
if (failure[0] != null) throw new RuntimeException(failure[0]);
|
||||||
|
int made = madeOut[0];
|
||||||
|
System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setFloat(Object target, String name, float value) throws Exception {
|
||||||
|
setField(target, name, float.class, Float.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setString(Object target, Class<?> boneClass, Object value) throws Exception {
|
||||||
|
setField(target, "D", String.class, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到**正在运行的**编辑器进程里,用编辑器自身 API 给"当前选中的骨骼"加 10 根子骨骼。
|
||||||
|
* 用法:jattach(本目录的 Attach.java)loadAgent 这个 jar,参数 = 要建的根数。
|
||||||
|
*
|
||||||
|
* 全部调用都有字节码依据(见 混淆名对照表.md / javap -c UH、GP):
|
||||||
|
* new Gp(模板, 父.skeleton, 父) → name = OG.D(父名, skeleton.bones, null)
|
||||||
|
* → skeleton.addBone(bone) ← GP.D(Gp) → 设 x/y/color → VZ.D(editor, skeleton, true, true)
|
||||||
|
*/
|
||||||
|
public final class BoneMaker5 {
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
int count = 10;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) count = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run(inst, count);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
System.err.println("[live] BoneMaker 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void run(Instrumentation inst, int count) throws Exception {
|
||||||
|
// 1) 找到编辑器自己的类加载器(我们的面板类就是它加载的)
|
||||||
|
// 编辑器是用自己的 URLClassLoader 加载载荷+面板的(Launcher 里 new URLClassLoader(urls, null)),
|
||||||
|
// 所以直接找"哪个类的加载器是 URLClassLoader",最稳(系统加载器看不到载荷)。
|
||||||
|
ClassLoader loader = null;
|
||||||
|
for (Class<?> c : inst.getAllLoadedClasses()) {
|
||||||
|
ClassLoader cl = c.getClassLoader();
|
||||||
|
if (cl instanceof java.net.URLClassLoader) {
|
||||||
|
loader = cl;
|
||||||
|
System.err.println("[live] 命中 URLClassLoader 于类 " + c.getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loader == null) {
|
||||||
|
System.err.println("[live] 找不到编辑器类加载器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
System.err.println("[live] 编辑器类加载器 = " + loader.getClass().getName());
|
||||||
|
|
||||||
|
// 2) 当前选中对象(复用面板里已有的读取逻辑)
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, loader);
|
||||||
|
Object parent = sceneObjects.getMethod("selected", ClassLoader.class)
|
||||||
|
.invoke(null, loader);
|
||||||
|
if (parent == null) {
|
||||||
|
System.err.println("[live] 当前没有选中对象(请在层级面板里选一根骨骼)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Class<?> sceneActions = Class.forName("com.spineview.SceneActions", true, loader);
|
||||||
|
Object skeleton = sceneActions.getMethod("skeletonOf", Object.class).invoke(null, parent);
|
||||||
|
System.err.println("[live] 选中 = " + sceneObjects.getMethod("nameOf", Object.class)
|
||||||
|
.invoke(null, parent) + ",骨架 = " + skeleton);
|
||||||
|
|
||||||
|
// 3) 用编辑器自身 API 建 N 根
|
||||||
|
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||||
|
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||||
|
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||||
|
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||||
|
Class<?> og = Class.forName("OG", true, loader);
|
||||||
|
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
|
||||||
|
Class<?> treeViewClass = Class.forName("VZ", true, loader);
|
||||||
|
|
||||||
|
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
|
||||||
|
Method addBone = null;
|
||||||
|
for (Method m : skeletonClass.getMethods()) {
|
||||||
|
if (m.getName().equals("D") && m.getParameterTypes().length == 1
|
||||||
|
&& m.getParameterTypes()[0] == boneClass) {
|
||||||
|
addBone = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method nameGen = null;
|
||||||
|
for (Method m : og.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == String.class && t[1] == arrayClass && t[2] == slotClass
|
||||||
|
&& m.getReturnType() == String.class) {
|
||||||
|
nameGen = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Method refresh = null;
|
||||||
|
for (Method m : treeViewClass.getMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 3 && t[0] == skeletonClass && t[1] == boolean.class
|
||||||
|
&& t[2] == boolean.class) {
|
||||||
|
refresh = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object editor = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals("com.esotericsoftware.spine.editor.Editor")) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
editor = f.get(null);
|
||||||
|
if (editor != null) break;
|
||||||
|
}
|
||||||
|
Object treeView = null;
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != treeViewClass) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
treeView = f.get(null);
|
||||||
|
if (treeView != null) break;
|
||||||
|
}
|
||||||
|
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
if (addBone == null || nameGen == null || treeView == null) {
|
||||||
|
System.err.println("[live] API 没找齐: addBone=" + addBone + " nameGen=" + nameGen
|
||||||
|
+ " treeView=" + treeView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addBone.setAccessible(true);
|
||||||
|
nameGen.setAccessible(true);
|
||||||
|
|
||||||
|
// skeleton 的骨骼数组字段(fI)
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : skeletonClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == arrayClass) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
bones = f.get(skeleton);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 父骨骼的名字(String 字段)
|
||||||
|
Object parentName = null;
|
||||||
|
for (Field f : boneClass.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) {
|
||||||
|
f.setAccessible(true);
|
||||||
|
parentName = f.get(parent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 整段挪到**编辑器的渲染线程**上跑:un.D()/VZ.D() 内部有线程断言(OG.I()),
|
||||||
|
// 非渲染线程会抛 RuntimeException(这就是上一次失败的原因)。
|
||||||
|
final java.lang.Object[] shared = new java.lang.Object[] {parent, skeleton, boneClass,
|
||||||
|
skeletonClass, arrayClass, slotClass, colorClass, addBone, nameGen, refresh, editor,
|
||||||
|
treeView, bones, parentName};
|
||||||
|
final int[] madeOut = new int[1];
|
||||||
|
final java.lang.Throwable[] failure = new java.lang.Throwable[1];
|
||||||
|
java.lang.reflect.Method runOnRender = null;
|
||||||
|
for (java.lang.reflect.Method m : og.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals("B")) continue;
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (t.length == 1 && t[0] == Runnable.class) {
|
||||||
|
runOnRender = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Runnable job = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// 4) 撤销包裹(un.D()/un.B())—— 整批算一次
|
||||||
|
Class<?> undo = Class.forName("un", true, loader);
|
||||||
|
Method begin = null, commit = null;
|
||||||
|
for (Method m : undo.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) begin = m;
|
||||||
|
if (m.getName().equals("B")) commit = m;
|
||||||
|
}
|
||||||
|
if (begin != null) {
|
||||||
|
begin.setAccessible(true);
|
||||||
|
begin.invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
int made = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Object bone = boneCtor.newInstance(parent, skeleton, parent); // new Gp(模板, skeleton, 父)
|
||||||
|
Object generated = nameGen.invoke(null, "bone", bones, null); // 编辑器自己的不重名生成
|
||||||
|
setString(bone, boneClass, generated); // name
|
||||||
|
addBone.invoke(skeleton, bone); // GP.D(Gp) = addBone
|
||||||
|
setFloat(bone, "Z", (float) (Math.random() * 400 - 200)); // x
|
||||||
|
setFloat(bone, "V", (float) (Math.random() * 400 - 200)); // y
|
||||||
|
setField(bone, "B", colorClass, red); // color
|
||||||
|
made++;
|
||||||
|
}
|
||||||
|
if (commit != null) {
|
||||||
|
commit.setAccessible(true);
|
||||||
|
commit.invoke(null);
|
||||||
|
}
|
||||||
|
if (refresh != null && editor != null) {
|
||||||
|
refresh.setAccessible(true);
|
||||||
|
refresh.invoke(treeView, editor, skeleton, Boolean.TRUE, Boolean.TRUE); // 只刷新一次
|
||||||
|
System.err.println("[live] 已刷新树节点");
|
||||||
|
}
|
||||||
|
} catch (Throwable error) {
|
||||||
|
failure[0] = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (runOnRender == null) {
|
||||||
|
System.err.println("[live] 找不到 OG.B(Runnable)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runOnRender.setAccessible(true);
|
||||||
|
runOnRender.invoke(null, job);
|
||||||
|
if (failure[0] != null) throw new RuntimeException(failure[0]);
|
||||||
|
int made = madeOut[0];
|
||||||
|
System.err.println("[live] 完成:在 " + parentName + " 下建了 " + made + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setFloat(Object target, String name, float value) throws Exception {
|
||||||
|
setField(target, name, float.class, Float.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setString(Object target, Class<?> boneClass, Object value) throws Exception {
|
||||||
|
setField(target, "D", String.class, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
|
||||||
|
/** 挂到正在运行的编辑器里,用编辑器自身 API 给"当前选中骨骼"加 N 根子骨骼。中间对象全用静态字段。 */
|
||||||
|
public final class BoneMaker6 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static int COUNT = 10;
|
||||||
|
static Object PARENT, SKELETON, BONES, PARENT_NAME, EDITOR, TREE_VIEW, RED;
|
||||||
|
static Class<?> BONE, SKELETON_CLASS, ARRAY, SLOT, COLOR, UNDO, OG;
|
||||||
|
static Method ADD_BONE, NAME_GEN, REFRESH, BEGIN, COMMIT;
|
||||||
|
static int MADE;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) COUNT = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run();
|
||||||
|
} catch (Throwable error) {
|
||||||
|
log("失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String message) {
|
||||||
|
System.err.println("[live6] " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run() throws Exception {
|
||||||
|
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||||
|
if (c.getClassLoader() instanceof URLClassLoader) {
|
||||||
|
L = c.getClassLoader();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, L);
|
||||||
|
PARENT = sceneObjects.getMethod("selected", ClassLoader.class).invoke(null, L);
|
||||||
|
if (PARENT == null) { log("当前没有选中对象"); return; }
|
||||||
|
SKELETON = Class.forName("com.spineview.SceneActions", true, L)
|
||||||
|
.getMethod("skeletonOf", Object.class).invoke(null, PARENT);
|
||||||
|
log("选中 = " + sceneObjects.getMethod("nameOf", Object.class).invoke(null, PARENT)
|
||||||
|
+ ",骨架 = " + SKELETON);
|
||||||
|
|
||||||
|
BONE = Class.forName("Gp", true, L);
|
||||||
|
SKELETON_CLASS = Class.forName("GP", true, L);
|
||||||
|
ARRAY = Class.forName("fI", true, L);
|
||||||
|
SLOT = Class.forName("GU", true, L);
|
||||||
|
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||||
|
OG = Class.forName("OG", true, L);
|
||||||
|
UNDO = Class.forName("un", true, L);
|
||||||
|
ADD_BONE = find(SKELETON_CLASS, "D", new Class<?>[] {BONE}, void.class);
|
||||||
|
NAME_GEN = find(OG, "D", new Class<?>[] {String.class, ARRAY, SLOT}, String.class);
|
||||||
|
REFRESH = find(Class.forName("VZ", true, L), "D",
|
||||||
|
new Class<?>[] {SKELETON_CLASS, boolean.class, boolean.class}, void.class);
|
||||||
|
EDITOR = staticField(OG, "com.esotericsoftware.spine.editor.Editor");
|
||||||
|
TREE_VIEW = staticField(OG, "VZ");
|
||||||
|
for (Method m : UNDO.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) BEGIN = m;
|
||||||
|
if (m.getName().equals("B")) COMMIT = m;
|
||||||
|
}
|
||||||
|
RED = COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
for (Field f : SKELETON_CLASS.getDeclaredFields()) {
|
||||||
|
if (f.getType() == ARRAY) { f.setAccessible(true); BONES = f.get(SKELETON); break; }
|
||||||
|
}
|
||||||
|
for (Field f : BONE.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) { f.setAccessible(true); PARENT_NAME = f.get(PARENT); break; }
|
||||||
|
}
|
||||||
|
if (ADD_BONE == null || NAME_GEN == null || REFRESH == null || EDITOR == null || TREE_VIEW == null) {
|
||||||
|
log("API 没找齐: addBone=" + ADD_BONE + " nameGen=" + NAME_GEN + " refresh=" + REFRESH
|
||||||
|
+ " editor=" + EDITOR + " tree=" + TREE_VIEW);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 整段放到编辑器渲染线程(un.D()/VZ.D() 里有线程断言 OG.I())
|
||||||
|
Method onRender = null;
|
||||||
|
for (Method m : OG.getDeclaredMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||||
|
}
|
||||||
|
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||||
|
onRender.setAccessible(true);
|
||||||
|
onRender.invoke(null, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
work();
|
||||||
|
} catch (Throwable error) {
|
||||||
|
log("work 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log("完成:在 " + PARENT_NAME + " 下建了 " + MADE + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void work() throws Exception {
|
||||||
|
Constructor<?> ctor = BONE.getConstructor(BONE, SKELETON_CLASS, BONE);
|
||||||
|
ADD_BONE.setAccessible(true);
|
||||||
|
NAME_GEN.setAccessible(true);
|
||||||
|
if (BEGIN != null) { BEGIN.setAccessible(true); BEGIN.invoke(null); }
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
Object bone = ctor.newInstance(PARENT, SKELETON, PARENT); // new Gp(模板, 骨架, 父)
|
||||||
|
Object name = NAME_GEN.invoke(null, "bone", BONES, null); // 编辑器自己的不重名生成
|
||||||
|
set(bone, "D", String.class, name);
|
||||||
|
ADD_BONE.invoke(SKELETON, bone); // GP.D(Gp) = addBone
|
||||||
|
set(bone, "Z", float.class, Float.valueOf((float) (Math.random() * 400 - 200)));
|
||||||
|
set(bone, "V", float.class, Float.valueOf((float) (Math.random() * 400 - 200)));
|
||||||
|
set(bone, "B", COLOR, RED); // color
|
||||||
|
MADE++;
|
||||||
|
}
|
||||||
|
if (COMMIT != null) { COMMIT.setAccessible(true); COMMIT.invoke(null); }
|
||||||
|
if (REFRESH != null) {
|
||||||
|
REFRESH.setAccessible(true);
|
||||||
|
REFRESH.invoke(TREE_VIEW, EDITOR, SKELETON, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
log("已刷新树节点");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Method m : owner.getMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
if (!java.util.Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object staticField(Class<?> owner, String typeName) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
try {
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
|
||||||
|
/** 挂到正在运行的编辑器里,用编辑器自身 API 给"当前选中骨骼"加 N 根子骨骼。中间对象全用静态字段。 */
|
||||||
|
public final class BoneMaker7 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static int COUNT = 10;
|
||||||
|
static Object PARENT, SKELETON, BONES, PARENT_NAME, EDITOR, TREE_VIEW, RED;
|
||||||
|
static Class<?> BONE, SKELETON_CLASS, ARRAY, SLOT, COLOR, UNDO, OG;
|
||||||
|
static Method ADD_BONE, NAME_GEN, REFRESH, BEGIN, COMMIT;
|
||||||
|
static int MADE;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try {
|
||||||
|
if (args != null && args.trim().length() > 0) COUNT = Integer.parseInt(args.trim());
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 默认 10
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
run();
|
||||||
|
} catch (Throwable error) {
|
||||||
|
log("失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String message) {
|
||||||
|
System.err.println("[live6] " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void run() throws Exception {
|
||||||
|
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||||
|
if (c.getClassLoader() instanceof URLClassLoader) {
|
||||||
|
L = c.getClassLoader();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, L);
|
||||||
|
PARENT = sceneObjects.getMethod("selected", ClassLoader.class).invoke(null, L);
|
||||||
|
if (PARENT == null) { log("当前没有选中对象"); return; }
|
||||||
|
SKELETON = Class.forName("com.spineview.SceneActions", true, L)
|
||||||
|
.getMethod("skeletonOf", Object.class).invoke(null, PARENT);
|
||||||
|
log("选中 = " + sceneObjects.getMethod("nameOf", Object.class).invoke(null, PARENT)
|
||||||
|
+ ",骨架 = " + SKELETON);
|
||||||
|
|
||||||
|
BONE = Class.forName("Gp", true, L);
|
||||||
|
SKELETON_CLASS = Class.forName("GP", true, L);
|
||||||
|
ARRAY = Class.forName("fI", true, L);
|
||||||
|
SLOT = Class.forName("GU", true, L);
|
||||||
|
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||||
|
OG = Class.forName("OG", true, L);
|
||||||
|
UNDO = Class.forName("un", true, L);
|
||||||
|
ADD_BONE = find(SKELETON_CLASS, "D", new Class<?>[] {BONE}, void.class);
|
||||||
|
NAME_GEN = find(OG, "D", new Class<?>[] {String.class, ARRAY, SLOT}, String.class);
|
||||||
|
REFRESH = find(Class.forName("VZ", true, L), "D",
|
||||||
|
new Class<?>[] {SKELETON_CLASS, boolean.class, boolean.class}, void.class);
|
||||||
|
EDITOR = staticField(OG, "com.esotericsoftware.spine.editor.Editor");
|
||||||
|
TREE_VIEW = staticField(OG, "VZ");
|
||||||
|
for (Method m : UNDO.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||||
|
if (m.getName().equals("D")) BEGIN = m;
|
||||||
|
if (m.getName().equals("B")) COMMIT = m;
|
||||||
|
}
|
||||||
|
RED = COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||||
|
.newInstance(1f, 0f, 0f, 1f);
|
||||||
|
for (Field f : SKELETON_CLASS.getDeclaredFields()) {
|
||||||
|
if (f.getType() == ARRAY) { f.setAccessible(true); BONES = f.get(SKELETON); break; }
|
||||||
|
}
|
||||||
|
for (Field f : BONE.getDeclaredFields()) {
|
||||||
|
if (f.getType() == String.class) { f.setAccessible(true); PARENT_NAME = f.get(PARENT); break; }
|
||||||
|
}
|
||||||
|
if (ADD_BONE == null || NAME_GEN == null || REFRESH == null || EDITOR == null || TREE_VIEW == null) {
|
||||||
|
log("API 没找齐: addBone=" + ADD_BONE + " nameGen=" + NAME_GEN + " refresh=" + REFRESH
|
||||||
|
+ " editor=" + EDITOR + " tree=" + TREE_VIEW);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 整段放到编辑器渲染线程(un.D()/VZ.D() 里有线程断言 OG.I())
|
||||||
|
Method onRender = null;
|
||||||
|
for (Method m : OG.getDeclaredMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||||
|
}
|
||||||
|
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||||
|
onRender.setAccessible(true);
|
||||||
|
onRender.invoke(null, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
work();
|
||||||
|
} catch (Throwable error) {
|
||||||
|
log("work 失败: " + error);
|
||||||
|
error.printStackTrace(System.err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log("完成:在 " + PARENT_NAME + " 下建了 " + MADE + " 根骨骼");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void work() throws Exception {
|
||||||
|
Constructor<?> ctor = BONE.getConstructor(BONE, SKELETON_CLASS, BONE);
|
||||||
|
ADD_BONE.setAccessible(true);
|
||||||
|
NAME_GEN.setAccessible(true);
|
||||||
|
if (BEGIN != null) { BEGIN.setAccessible(true); BEGIN.invoke(null); }
|
||||||
|
for (int i = 0; i < COUNT; i++) {
|
||||||
|
Object bone = ctor.newInstance(PARENT, SKELETON, PARENT); // new Gp(模板, 骨架, 父)
|
||||||
|
Object name = NAME_GEN.invoke(null, "bone", BONES, null); // 编辑器自己的不重名生成
|
||||||
|
set(bone, "D", String.class, name);
|
||||||
|
ADD_BONE.invoke(SKELETON, bone); // GP.D(Gp) = addBone
|
||||||
|
set(bone, "Z", float.class, Float.valueOf((float) (Math.random() * 400 - 200)));
|
||||||
|
set(bone, "V", float.class, Float.valueOf((float) (Math.random() * 400 - 200)));
|
||||||
|
set(bone, "B", COLOR, RED); // color
|
||||||
|
MADE++;
|
||||||
|
}
|
||||||
|
if (COMMIT != null) { COMMIT.setAccessible(true); COMMIT.invoke(null); }
|
||||||
|
if (REFRESH != null) {
|
||||||
|
REFRESH.setAccessible(true);
|
||||||
|
REFRESH.invoke(TREE_VIEW, SKELETON, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
log("已刷新树节点");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Method m : owner.getMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
if (!java.util.Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object staticField(Class<?> owner, String typeName) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
try {
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// 下一个
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读探针:把树里所有场景对象、每个插槽的当前/静止附件、以及附件的字段全打出来。
|
||||||
|
* 不改任何数据。
|
||||||
|
*/
|
||||||
|
public final class Probe3 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
|
||||||
|
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("[probe3] " + m); }
|
||||||
|
|
||||||
|
static void run() throws Exception {
|
||||||
|
ClassLoader loader = null;
|
||||||
|
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||||
|
if (c.getClassLoader() instanceof URLClassLoader) { loader = c.getClassLoader(); break; }
|
||||||
|
}
|
||||||
|
if (loader == null) { log("找不到编辑器类加载器"); return; }
|
||||||
|
final ClassLoader L = loader;
|
||||||
|
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, L);
|
||||||
|
Method listAll = sceneObjects.getMethod("listAll", ClassLoader.class);
|
||||||
|
Method nameOf = sceneObjects.getMethod("nameOf", Object.class);
|
||||||
|
Method kind = sceneObjects.getMethod("kind", Object.class);
|
||||||
|
Method skeletonOf = Class.forName("com.spineview.SceneActions", true, L)
|
||||||
|
.getMethod("skeletonOf", Object.class);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Object> all = (List<Object>) listAll.invoke(null, L);
|
||||||
|
log("树里对象数 = " + all.size());
|
||||||
|
|
||||||
|
Class<?> GU = Class.forName("GU", true, L);
|
||||||
|
Class<?> Gn = Class.forName("Gn", true, L);
|
||||||
|
Method slotGetCurrent = method(GU, "D", new Class<?>[] {}, Gn);
|
||||||
|
Method slotGetSetup = method(GU, "B", new Class<?>[] {}, Gn);
|
||||||
|
log("GU.D() 当前附件 getter = " + slotGetCurrent);
|
||||||
|
log("GU.B() 静止附件 getter = " + slotGetSetup);
|
||||||
|
|
||||||
|
int listed = 0;
|
||||||
|
for (Object o : all) {
|
||||||
|
if (listaCap(listed++)) { log("…(列表截断)"); break; }
|
||||||
|
String extra = "";
|
||||||
|
if (GU.isInstance(o)) {
|
||||||
|
Object cur = slotGetCurrent == null ? null : slotGetCurrent.invoke(o);
|
||||||
|
Object setup = slotGetSetup == null ? null : slotGetSetup.invoke(o);
|
||||||
|
extra = " cur=" + describe(cur) + " setup=" + describe(setup);
|
||||||
|
}
|
||||||
|
log(" #" + (listed - 1) + " " + kind.invoke(null, o) + " \"" + nameOf.invoke(null, o)
|
||||||
|
+ "\" class=" + o.getClass().getName() + extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逐个插槽:附件字段 + 附件自己的字段
|
||||||
|
for (Object o : all) {
|
||||||
|
if (!GU.isInstance(o)) continue;
|
||||||
|
log("--- 插槽 \"" + nameOf.invoke(null, o) + "\" ---");
|
||||||
|
dumpFields(" 插槽字段", o, 0);
|
||||||
|
Object cur = slotGetCurrent == null ? null : slotGetCurrent.invoke(o);
|
||||||
|
Object setup = slotGetSetup == null ? null : slotGetSetup.invoke(o);
|
||||||
|
log(" 当前附件 = " + describe(cur));
|
||||||
|
dumpFields(" 当前附件字段", cur, 0);
|
||||||
|
log(" 静止附件 = " + describe(setup));
|
||||||
|
dumpFields(" 静止附件字段", setup, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 骨架身上的集合类字段:看看 skin 表长什么样
|
||||||
|
Object anySlot = null;
|
||||||
|
for (Object o : all) { if (GU.isInstance(o)) { anySlot = o; break; } }
|
||||||
|
Object skeleton = anySlot == null ? null : skeletonOf.invoke(null, anySlot);
|
||||||
|
log("骨架 = " + skeleton);
|
||||||
|
if (skeleton != null) {
|
||||||
|
Class<?> GP = Class.forName("GP", true, L);
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
if (v == null) continue;
|
||||||
|
String t = f.getType().getName();
|
||||||
|
if (t.startsWith("java.") || t.equals("fI")) {
|
||||||
|
log(" GP." + f.getName() + " : " + t + " size=" + sizeOf(v) + " -> " + brief(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean listaCap(int i) { return i >= 400; }
|
||||||
|
|
||||||
|
static int sizeOf(Object v) {
|
||||||
|
if (v instanceof java.util.Collection) return ((java.util.Collection<?>) v).size();
|
||||||
|
if (v instanceof java.util.Map) return ((java.util.Map<?, ?>) v).size();
|
||||||
|
if (v.getClass().isArray()) return java.lang.reflect.Array.getLength(v);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String brief(Object v) {
|
||||||
|
String s = String.valueOf(v);
|
||||||
|
return s.length() > 300 ? s.substring(0, 300) + "…" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describe(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
String name = null;
|
||||||
|
try {
|
||||||
|
Field f = null;
|
||||||
|
for (Class<?> t = o.getClass(); t != null; t = t.getSuperclass()) {
|
||||||
|
for (Field x : t.getDeclaredFields()) {
|
||||||
|
if (x.getType() == String.class && Modifier.isPublic(x.getModifiers())) { f = x; break; }
|
||||||
|
}
|
||||||
|
if (f != null) break;
|
||||||
|
}
|
||||||
|
if (f != null) { f.setAccessible(true); name = (String) f.get(o); }
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return o.getClass().getName() + "(name=" + name + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dumpFields(String title, Object o, int depth) {
|
||||||
|
if (o == null) { log(title + ": null"); return; }
|
||||||
|
StringBuilder sb = new StringBuilder(title + ":");
|
||||||
|
for (Class<?> t = o.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v;
|
||||||
|
try { v = f.get(o); } catch (Throwable e) { continue; }
|
||||||
|
Class<?> ft = f.getType();
|
||||||
|
if (ft.isPrimitive() || ft == String.class || ft.isEnum()
|
||||||
|
|| ft.getName().startsWith("com.badlogic.gdx.graphics.Color")) {
|
||||||
|
sb.append("\n ").append(t.getSimpleName()).append('.').append(f.getName())
|
||||||
|
.append(':').append(ft.getSimpleName()).append('=').append(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
if (!java.util.Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读探针 2:走一遍层级树(看用户在编辑器里到底看得见什么)、
|
||||||
|
* 打印骨架各集合字段的元素真实类型、以及每个插槽在皮肤里的附件。
|
||||||
|
*/
|
||||||
|
public final class Probe4 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN;
|
||||||
|
|
||||||
|
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("[probe4] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
Class<?> UQ = Class.forName("Uq", true, L);
|
||||||
|
Class<?> GP = Class.forName("GP", true, L);
|
||||||
|
Class<?> GU = Class.forName("GU", true, L);
|
||||||
|
Class<?> GJ = Class.forName("Gj", true, L);
|
||||||
|
Class<?> GL = Class.forName("Gl", true, L);
|
||||||
|
Class<?> GPC = Class.forName("Gp", true, L);
|
||||||
|
|
||||||
|
// Gn 的已加载子类(附件类型全家桶)
|
||||||
|
List<String> subs = new ArrayList<String>();
|
||||||
|
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||||
|
if (GN.isAssignableFrom(c) && c != GN) subs.add(c.getName());
|
||||||
|
}
|
||||||
|
log("Gn 已加载子类 = " + subs);
|
||||||
|
|
||||||
|
Object tree = global("Uq");
|
||||||
|
Object skeleton = null;
|
||||||
|
Object roots = null;
|
||||||
|
if (tree != null) {
|
||||||
|
roots = rootNodes(tree);
|
||||||
|
log("树 rootNodes 数 = " + size(roots));
|
||||||
|
walk(roots, 0);
|
||||||
|
} else {
|
||||||
|
log("拿不到 Uq");
|
||||||
|
}
|
||||||
|
|
||||||
|
skeleton = firstOfType(roots, GP);
|
||||||
|
if (skeleton == null) {
|
||||||
|
Object sel = selected();
|
||||||
|
if (sel != null) skeleton = skeletonOf(sel);
|
||||||
|
}
|
||||||
|
log("骨架 = " + skeleton);
|
||||||
|
if (skeleton == null) { log("拿不到骨架,结束"); return; }
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
int n = size(v);
|
||||||
|
if (n == 0) continue;
|
||||||
|
log(" GP." + f.getName() + " size=" + n + " 元素类型=" + elemType(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 皮肤
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
int n = size(v);
|
||||||
|
if (n == 0) continue;
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et == null || !GL.isAssignableFrom(et)) continue;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
Object skin = get(v, i);
|
||||||
|
log(" 皮肤[" + i + "] " + describe(skin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每个插槽的当前/静止附件 + 该插槽在皮肤里的条目
|
||||||
|
Object slots = null;
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
if (size(v) > 0 && GU.isAssignableFrom(elemClass(v))) { slots = v; break; }
|
||||||
|
}
|
||||||
|
Method cur = method(GU, "D", new Class<?>[] {}, GN);
|
||||||
|
Method setup = method(GU, "B", new Class<?>[] {}, GN);
|
||||||
|
for (int i = 0; slots != null && i < size(slots); i++) {
|
||||||
|
Object slot = get(slots, i);
|
||||||
|
log(" 插槽[" + i + "] \"" + nameOf(slot) + "\" bone=" + describe(field(slot, GU, GPC))
|
||||||
|
+ " 当前=" + describe(cur.invoke(slot)) + " 静止=" + describe(setup.invoke(slot)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void walk(Object nodes, int depth) {
|
||||||
|
if (nodes == null || depth > 8) return;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
Object kids = field(node, NODE, ARRAY);
|
||||||
|
StringBuilder pad = new StringBuilder();
|
||||||
|
for (int k = 0; k < depth; k++) pad.append("│ ");
|
||||||
|
log(" " + pad + "├ " + describe(obj) + (size(kids) > 0 ? " (" + size(kids) + " 子节点)" : ""));
|
||||||
|
walk(kids, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object anyOf(Object array, Class<?> type) {
|
||||||
|
for (int i = 0; i < size(array); i++) { Object o = get(array, i); if (o != null && type.isInstance(o)) return o; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在树里找第一个指定类型的对象(骨架就是树根节点上的对象)。 */
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object selected() {
|
||||||
|
try {
|
||||||
|
return Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("selected", ClassLoader.class).invoke(null, L);
|
||||||
|
} catch (Throwable t) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object skeletonOf(Object sceneObject) {
|
||||||
|
try {
|
||||||
|
return Class.forName("com.spineview.SceneActions", true, L)
|
||||||
|
.getMethod("skeletonOf", Object.class).invoke(null, sceneObject);
|
||||||
|
} catch (Throwable t) { 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 String elemType(Object array) {
|
||||||
|
Class<?> c = elemClass(array);
|
||||||
|
return c == null ? "?" : c.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object global(String typeName) {
|
||||||
|
try {
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 String describe(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
return o.getClass().getName() + "(" + nameOf(o) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object[] arrayItems(Object a) {
|
||||||
|
Object[] out = new Object[size(a)];
|
||||||
|
for (int i = 0; i < out.length; i++) out[i] = get(a, i);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读探针 3:把骨架里所有 GO(图片附件)连同它的插槽、变换值列出;
|
||||||
|
* 再把皮肤 "animation" 的每个条目列出。目的是看清"多出来的那批附件"是什么。
|
||||||
|
*/
|
||||||
|
public final class Probe5 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String m) { System.err.println("[probe5] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
Class<?> GP = Class.forName("GP", true, L);
|
||||||
|
Class<?> GU = Class.forName("GU", true, L);
|
||||||
|
Class<?> GO = Class.forName("GO", true, L);
|
||||||
|
Class<?> GPC = Class.forName("Gp", true, L);
|
||||||
|
Class<?> GL = Class.forName("Gl", true, L);
|
||||||
|
|
||||||
|
Object roots = rootNodes(global("Uq"));
|
||||||
|
Object skeleton = firstOfType(roots, GP);
|
||||||
|
if (skeleton == null) { log("拿不到骨架"); return; }
|
||||||
|
log("骨架 = " + nameOf(skeleton));
|
||||||
|
|
||||||
|
// 插槽
|
||||||
|
Object slots = arrFieldOfType(GP, skeleton, GU);
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object s = get(slots, i);
|
||||||
|
log(" slot[" + i + "] name=" + nameOf(s) + " bone=" + nameOf(field(s, GU, GPC)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有 GO 附件
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
if (size(v) == 0) continue;
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et == null || !GO.isAssignableFrom(et)) continue;
|
||||||
|
log("GP." + f.getName() + ":" + size(v) + " 个 GO");
|
||||||
|
for (int i = 0; i < size(v); i++) {
|
||||||
|
Object go = get(v, i);
|
||||||
|
log(" GO[" + i + "] name=" + nameOf(go)
|
||||||
|
+ " slot=" + nameOf(field(go, GN, GU))
|
||||||
|
+ " x=" + fval(go, "x") + " y=" + fval(go, "y") + " rot=" + fval(go, "rot")
|
||||||
|
+ " sx=" + fval(go, "sx") + " sy=" + fval(go, "sy")
|
||||||
|
+ " w=" + fval(go, "w") + " h=" + fval(go, "h"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 皮肤条目
|
||||||
|
Object skins = arrFieldOfType(GP, skeleton, GL);
|
||||||
|
for (int k = 0; k < size(skins); k++) {
|
||||||
|
Object skin = get(skins, k);
|
||||||
|
log("皮肤[" + k + "] " + nameOf(skin));
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object s = get(slots, i);
|
||||||
|
Object entry = null;
|
||||||
|
for (Method m : GL.getDeclaredMethods()) {
|
||||||
|
Class<?>[] p = m.getParameterTypes();
|
||||||
|
if (p.length != 2 || p[0] != GPC || p[1] != GU) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
try { entry = m.invoke(skin, field(s, GU, GPC), s); } catch (Throwable ignored) { }
|
||||||
|
if (entry != null) break;
|
||||||
|
}
|
||||||
|
if (entry != null) {
|
||||||
|
log(" 条目 slot=" + nameOf(s) + " -> " + describe(entry) + fields(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GO 的字段语义来自编辑器自己的 JSON 写入器 sb_0.java:669-675。 */
|
||||||
|
static String fval(Object go, String which) {
|
||||||
|
String[] names = { "D", "B", "S", "L", "I", "C", "Z" };
|
||||||
|
String[] keys = { "x", "y", "rot", "sx", "sy", "w", "h" };
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
if (!keys[i].equals(which)) continue;
|
||||||
|
try {
|
||||||
|
Field f = go.getClass().getField(names[i]);
|
||||||
|
return String.valueOf(f.getFloat(go));
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fields(Object o) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Class<?> t = o.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v;
|
||||||
|
try { v = f.get(o); } catch (Throwable e) { continue; }
|
||||||
|
if (v == null) continue;
|
||||||
|
String s = String.valueOf(v);
|
||||||
|
if (s.length() > 60) s = s.substring(0, 60) + "…";
|
||||||
|
sb.append(" ").append(f.getName()).append('=').append(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object arrFieldOfType(Class<?> owner, Object target, Class<?> elem) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(target);
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et != null && elem.isAssignableFrom(et)) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object global(String typeName) {
|
||||||
|
try {
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nameOf(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
try {
|
||||||
|
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describe(Object o) { return o == null ? "null" : o.getClass().getName(); }
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
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 Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读探针 3:把骨架里所有 GO(图片附件)连同它的插槽、变换值列出;
|
||||||
|
* 再把皮肤 "animation" 的每个条目列出。目的是看清"多出来的那批附件"是什么。
|
||||||
|
*/
|
||||||
|
public final class Probe6 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String m) { System.err.println("[probe6] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
Class<?> GP = Class.forName("GP", true, L);
|
||||||
|
Class<?> GU = Class.forName("GU", true, L);
|
||||||
|
Class<?> GO = Class.forName("GO", true, L);
|
||||||
|
Class<?> GPC = Class.forName("Gp", true, L);
|
||||||
|
Class<?> GL = Class.forName("Gl", true, L);
|
||||||
|
|
||||||
|
Object roots = rootNodes(global("Uq"));
|
||||||
|
Object skeleton = firstOfType(roots, GP);
|
||||||
|
if (skeleton == null) { log("拿不到骨架"); return; }
|
||||||
|
log("骨架 = " + nameOf(skeleton));
|
||||||
|
|
||||||
|
// 插槽
|
||||||
|
Object slots = arrFieldOfType(GP, skeleton, GU);
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object s = get(slots, i);
|
||||||
|
log(" slot[" + i + "] name=" + nameOf(s) + " bone=" + nameOf(field(s, GU, GPC)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有 GO 附件
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
if (size(v) == 0) continue;
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et == null || !GO.isAssignableFrom(et)) continue;
|
||||||
|
log("GP." + f.getName() + ":" + size(v) + " 个 GO");
|
||||||
|
for (int i = 0; i < size(v); i++) {
|
||||||
|
Object go = get(v, i);
|
||||||
|
log(" GO[" + i + "] name=" + nameOf(go)
|
||||||
|
+ " slot=" + nameOf(field(go, GN, GU))
|
||||||
|
+ " x=" + fval(go, "x") + " y=" + fval(go, "y") + " rot=" + fval(go, "rot")
|
||||||
|
+ " sx=" + fval(go, "sx") + " sy=" + fval(go, "sy")
|
||||||
|
+ " w=" + fval(go, "w") + " h=" + fval(go, "h"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 皮肤条目
|
||||||
|
Object skins = arrFieldOfType(GP, skeleton, GL);
|
||||||
|
for (int k = 0; k < size(skins); k++) {
|
||||||
|
Object skin = get(skins, k);
|
||||||
|
log("皮肤[" + k + "] " + nameOf(skin));
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object s = get(slots, i);
|
||||||
|
Object entry = null;
|
||||||
|
for (Method m : GL.getDeclaredMethods()) {
|
||||||
|
Class<?>[] p = m.getParameterTypes();
|
||||||
|
if (p.length != 2 || p[0] != GPC || p[1] != GU) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
try { entry = m.invoke(skin, field(s, GU, GPC), s); } catch (Throwable ignored) { }
|
||||||
|
if (entry != null) break;
|
||||||
|
}
|
||||||
|
if (entry != null) {
|
||||||
|
log(" 条目 slot=" + nameOf(s) + " -> " + describe(entry) + fields(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GO 的字段语义来自编辑器自己的 JSON 写入器 sb_0.java:669-675。 */
|
||||||
|
static String fval(Object go, String which) {
|
||||||
|
String[] names = { "D", "B", "S", "L", "I", "C", "Z" };
|
||||||
|
String[] keys = { "x", "y", "rot", "sx", "sy", "w", "h" };
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
if (!keys[i].equals(which)) continue;
|
||||||
|
try {
|
||||||
|
Field f = go.getClass().getField(names[i]);
|
||||||
|
return String.valueOf(f.getFloat(go));
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fields(Object o) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Class<?> t = o.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v;
|
||||||
|
try { v = f.get(o); } catch (Throwable e) { continue; }
|
||||||
|
if (v == null) continue;
|
||||||
|
String s = String.valueOf(v);
|
||||||
|
if (s.length() > 60) s = s.substring(0, 60) + "…";
|
||||||
|
sb.append(" ").append(f.getName()).append('=').append(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object arrFieldOfType(Class<?> owner, Object target, Class<?> elem) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(target);
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et != null && elem.isAssignableFrom(et)) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object global(String typeName) {
|
||||||
|
try {
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nameOf(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
try {
|
||||||
|
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describe(Object o) { return o == null ? "null" : o.getClass().getName(); }
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
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 Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读探针 4:一个插槽到底挂了几份附件、各是什么类;以及骨架附件表里每一项的类名和变换。
|
||||||
|
*/
|
||||||
|
public final class Probe7 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN, GO;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String m) { System.err.println("[probe7] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
GO = Class.forName("GO", true, L);
|
||||||
|
Class<?> GP = Class.forName("GP", true, L);
|
||||||
|
Class<?> GU = Class.forName("GU", true, L);
|
||||||
|
Class<?> GPC = Class.forName("Gp", true, L);
|
||||||
|
Method cur = method(GU, "D", new Class<?>[] {}, GN);
|
||||||
|
Method setup = method(GU, "B", new Class<?>[] {}, GN);
|
||||||
|
|
||||||
|
Object skeleton = firstOfType(rootNodes(global("Uq")), GP);
|
||||||
|
Object slots = arrFieldOfType(GP, skeleton, GU);
|
||||||
|
Object probe = null;
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
if ("mega_dg7".equals(nameOf(get(slots, i)))) { probe = get(slots, i); break; }
|
||||||
|
}
|
||||||
|
log("样本插槽 = " + nameOf(probe));
|
||||||
|
Object a = cur.invoke(probe), b = setup.invoke(probe);
|
||||||
|
log(" 当前附件 = " + describe(a));
|
||||||
|
log(" 静止附件 = " + describe(b));
|
||||||
|
log(" 是同一个对象吗 = " + (a == b));
|
||||||
|
log(" 当前附件字段:" + fields(a));
|
||||||
|
log(" 静止附件字段:" + fields(b));
|
||||||
|
Object bone = field(probe, GU, GPC);
|
||||||
|
log(" 所在骨骼 = " + nameOf(bone));
|
||||||
|
|
||||||
|
// 骨架里所有 Gn 附件
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et == null || !GN.isAssignableFrom(et)) continue;
|
||||||
|
log("GP." + f.getName() + ":" + size(v) + " 个附件");
|
||||||
|
for (int i = 0; i < size(v); i++) {
|
||||||
|
Object o = get(v, i);
|
||||||
|
log(" [" + i + "] " + o.getClass().getName() + " name=" + nameOf(o)
|
||||||
|
+ " slot=" + nameOf(field(o, GN, GU)) + " 当前?" + (o == a) + " 静止?" + (o == b)
|
||||||
|
+ (GO.isInstance(o) ? " " + goValues(o) : " " + fields(o)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字段语义来自编辑器自己的 JSON 写入器 sb_0.java:669-675。 */
|
||||||
|
static String goValues(Object go) {
|
||||||
|
String[] names = { "D", "B", "S", "L", "I", "C", "Z", "B" };
|
||||||
|
String[] keys = { "x", "y", "rotation", "scaleX", "scaleY", "width", "height", "path" };
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
try {
|
||||||
|
Field f = GO.getDeclaredField(names[i]);
|
||||||
|
f.setAccessible(true);
|
||||||
|
sb.append(keys[i]).append('=').append(f.get(go)).append(' ');
|
||||||
|
} catch (Throwable t) { sb.append(keys[i]).append("=? "); }
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fields(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Class<?> t = o.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v;
|
||||||
|
try { v = f.get(o); } catch (Throwable e) { continue; }
|
||||||
|
if (v == null) continue;
|
||||||
|
String s = String.valueOf(v);
|
||||||
|
if (s.length() > 50) s = s.substring(0, 50) + "…";
|
||||||
|
sb.append(" ").append(t.getSimpleName()).append('.').append(f.getName())
|
||||||
|
.append('=').append(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describe(Object o) { return o == null ? "null" : o.getClass().getName() + "@" + System.identityHashCode(o); }
|
||||||
|
|
||||||
|
static Object arrFieldOfType(Class<?> owner, Object target, Class<?> elem) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(target);
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et != null && elem.isAssignableFrom(et)) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object global(String typeName) {
|
||||||
|
try {
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nameOf(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
try {
|
||||||
|
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
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 Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读探针 4:一个插槽到底挂了几份附件、各是什么类;以及骨架附件表里每一项的类名和变换。
|
||||||
|
*/
|
||||||
|
public final class Probe9 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN, GO;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String m) { System.err.println("[probe9] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
GO = Class.forName("GO", true, L);
|
||||||
|
Class<?> GP = Class.forName("GP", true, L);
|
||||||
|
Class<?> GU = Class.forName("GU", true, L);
|
||||||
|
Class<?> GPC = Class.forName("Gp", true, L);
|
||||||
|
Method cur = method(GU, "D", new Class<?>[] {}, GN);
|
||||||
|
Method setup = method(GU, "B", new Class<?>[] {}, GN);
|
||||||
|
|
||||||
|
Object skeleton = firstOfType(rootNodes(global("Uq")), GP);
|
||||||
|
Object slots = arrFieldOfType(GP, skeleton, GU);
|
||||||
|
Object probe = null;
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
if ("mega_dg7".equals(nameOf(get(slots, i)))) { probe = get(slots, i); break; }
|
||||||
|
}
|
||||||
|
log("样本插槽 = " + nameOf(probe));
|
||||||
|
Object a = cur.invoke(probe), b = setup.invoke(probe);
|
||||||
|
log(" 当前附件 = " + describe(a));
|
||||||
|
log(" 静止附件 = " + describe(b));
|
||||||
|
log(" 是同一个对象吗 = " + (a == b));
|
||||||
|
log(" 当前附件字段:" + fields(a));
|
||||||
|
log(" 静止附件字段:" + fields(b));
|
||||||
|
Object bone = field(probe, GU, GPC);
|
||||||
|
log(" 所在骨骼 = " + nameOf(bone));
|
||||||
|
|
||||||
|
// 骨架里所有 Gn 附件
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(skeleton);
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et == null || !GN.isAssignableFrom(et)) continue;
|
||||||
|
log("GP." + f.getName() + ":" + size(v) + " 个附件");
|
||||||
|
for (int i = 0; i < size(v); i++) {
|
||||||
|
Object o = get(v, i);
|
||||||
|
log(" [" + i + "] " + o.getClass().getName() + " name=" + nameOf(o)
|
||||||
|
+ " slot=" + nameOf(field(o, GN, GU)) + " 当前?" + (o == a) + " 静止?" + (o == b)
|
||||||
|
+ (GO.isInstance(o) ? " " + goValues(o) : " " + fields(o)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段语义来自编辑器自己的 JSON 写入器 sb_0.java:669-675。
|
||||||
|
* x 特意按"声明里第一个 float 类型的 D 字段"取:GO 上有多个同名字段 D,按名字 getDeclaredField 会取错。
|
||||||
|
*/
|
||||||
|
static String goValues(Object go) {
|
||||||
|
String[] names = { "D", "B", "S", "L", "I", "C", "Z" };
|
||||||
|
String[] keys = { "x", "y", "rotation", "scaleX", "scaleY", "width", "height" };
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
try {
|
||||||
|
Field f = null;
|
||||||
|
for (Field cand : GO.getDeclaredFields()) {
|
||||||
|
if (cand.getName().equals(names[i]) && cand.getType() == float.class) { f = cand; break; }
|
||||||
|
}
|
||||||
|
f.setAccessible(true);
|
||||||
|
sb.append(keys[i]).append('=').append(f.getFloat(go)).append(' ');
|
||||||
|
} catch (Throwable t) { sb.append(keys[i]).append("=? "); }
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String fields(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Class<?> t = o.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v;
|
||||||
|
try { v = f.get(o); } catch (Throwable e) { continue; }
|
||||||
|
if (v == null) continue;
|
||||||
|
String s = String.valueOf(v);
|
||||||
|
if (s.length() > 50) s = s.substring(0, 50) + "…";
|
||||||
|
sb.append(" ").append(t.getSimpleName()).append('.').append(f.getName())
|
||||||
|
.append('=').append(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static String describe(Object o) { return o == null ? "null" : o.getClass().getName() + "@" + System.identityHashCode(o); }
|
||||||
|
|
||||||
|
static Object arrFieldOfType(Class<?> owner, Object target, Class<?> elem) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(target);
|
||||||
|
Class<?> et = elemClass(v);
|
||||||
|
if (et != null && elem.isAssignableFrom(et)) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object global(String typeName) {
|
||||||
|
try {
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
for (Field f : og.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(null);
|
||||||
|
if (v != null) return v;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nameOf(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
try {
|
||||||
|
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
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 Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热更:把"刚刚创建的附件"整理干净 —— 全部走编辑器自身 API,不重启。
|
||||||
|
*
|
||||||
|
* 1) 重置图片附件(GO)的变换:GO.D(FFFFF)V 就是编辑器自己的 setTransform,
|
||||||
|
* 字节码里依次写 D=x、B=y、S=rotation、L=scaleX、I=scaleY(见 javap -c GO)。
|
||||||
|
* 图片附件在 3.8 没有 shear 字段(JSON 写入器 sb_0.java:669-675 也只写 x/y/scaleX/scaleY/rotation/width/height)。
|
||||||
|
* 2) 删除 skin/占位附件:Hi.I() 返回 "placeholder",实例方法 Gn.L() 内部是
|
||||||
|
* `slot.skeleton.removeAttachment(this,true,true)`(javap -c Gn 已确认)。
|
||||||
|
*
|
||||||
|
* 目标对象 = 上一次热更里创建的 20 个插槽(骨骼是 bone6 的子骨骼)。
|
||||||
|
*/
|
||||||
|
public final class ResetMaker1 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN, GO, HI, GP, GU, GPC;
|
||||||
|
|
||||||
|
static Method SLOT_CURRENT, SLOT_SETUP, GO_SET, ATTACH_DELETE, REFRESH, UNDO_BEGIN, UNDO_END, UNDO_RECORD;
|
||||||
|
static Field GP_ATTACHMENTS, GP_SLOTS, BONE_PARENT;
|
||||||
|
static Object SKELETON, TREE_VIEW;
|
||||||
|
static Set<Object> OUR_SLOTS = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||||
|
static int RESET, DELETED, FAILED;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String m) { System.err.println("[reset1] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
GO = Class.forName("GO", true, L);
|
||||||
|
HI = Class.forName("Hi", true, L);
|
||||||
|
GP = Class.forName("GP", true, L);
|
||||||
|
GU = Class.forName("GU", true, L);
|
||||||
|
GPC = Class.forName("Gp", true, L);
|
||||||
|
Class<?> VZ = Class.forName("VZ", true, L);
|
||||||
|
Class<?> OG = Class.forName("OG", true, L);
|
||||||
|
Class<?> UN = Class.forName("un", true, L);
|
||||||
|
|
||||||
|
// 编辑器自己的"切渲染线程并等待"
|
||||||
|
Method onRender = null;
|
||||||
|
for (Method m : OG.getDeclaredMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||||
|
}
|
||||||
|
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||||
|
onRender.setAccessible(true);
|
||||||
|
|
||||||
|
// 骨架:树根节点上的对象
|
||||||
|
TREE_VIEW = globalStatic(OG, VZ.getName());
|
||||||
|
Object roots = rootNodes(globalStatic(Class.forName("OG", true, L), "Uq"));
|
||||||
|
SKELETON = firstOfType(roots, GP);
|
||||||
|
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||||
|
|
||||||
|
GP_ATTACHMENTS = firstArrayField(GP, GN);
|
||||||
|
GP_SLOTS = firstArrayField(GP, GU);
|
||||||
|
BONE_PARENT = firstTypeField(GPC, GPC);
|
||||||
|
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||||
|
SLOT_SETUP = method(GU, "B", new Class<?>[] {}, GN);
|
||||||
|
GO_SET = method(GO, "D", new Class<?>[] { float.class, float.class, float.class, float.class, float.class }, void.class);
|
||||||
|
ATTACH_DELETE = method(GN, "L", new Class<?>[] {}, void.class);
|
||||||
|
REFRESH = method(VZ, "D", new Class<?>[] { GP, boolean.class, boolean.class }, void.class);
|
||||||
|
for (Method m : UN.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length == 0 && m.getReturnType() == void.class) {
|
||||||
|
if (m.getName().equals("D")) UNDO_BEGIN = m;
|
||||||
|
if (m.getName().equals("B")) UNDO_END = m;
|
||||||
|
}
|
||||||
|
if (m.getParameterTypes().length == 1 && m.getParameterTypes()[0] == Object.class) UNDO_RECORD = m;
|
||||||
|
}
|
||||||
|
if (GO_SET == null || ATTACH_DELETE == null || SLOT_CURRENT == null) { log("API 没找齐"); return; }
|
||||||
|
if (UNDO_BEGIN != null) UNDO_BEGIN.setAccessible(true);
|
||||||
|
if (UNDO_END != null) UNDO_END.setAccessible(true);
|
||||||
|
if (UNDO_RECORD != null) UNDO_RECORD.setAccessible(true);
|
||||||
|
GO_SET.setAccessible(true);
|
||||||
|
ATTACH_DELETE.setAccessible(true);
|
||||||
|
|
||||||
|
collectOurSlots();
|
||||||
|
log("目标插槽 = " + OUR_SLOTS.size() + " 个");
|
||||||
|
|
||||||
|
onRender.invoke(null, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log("完成:重置 " + RESET + " 个附件的变换,删除 " + DELETED + " 个占位(skin)附件,失败 " + FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上一次热更建的 20 个插槽:骨骼是 bone6 的子骨骼。 */
|
||||||
|
static void collectOurSlots() throws Exception {
|
||||||
|
Object bone6 = null;
|
||||||
|
Object bones = firstArrayField(GP, GPC);
|
||||||
|
for (int i = 0; i < size(bones); i++) {
|
||||||
|
Object b = get(bones, i);
|
||||||
|
if ("bone6".equals(nameOf(b))) { bone6 = b; break; }
|
||||||
|
}
|
||||||
|
if (bone6 == null) { log("找不到 bone6"); return; }
|
||||||
|
for (int i = 0; i < size(slots()); i++) {
|
||||||
|
Object slot = get(slots(), i);
|
||||||
|
Object bone = field(slot, GU, GPC);
|
||||||
|
if (bone != null && field(bone, GPC, GPC) == bone6) OUR_SLOTS.add(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object slots() throws Exception { return GP_SLOTS.get(SKELETON); }
|
||||||
|
|
||||||
|
static void work() throws Exception {
|
||||||
|
if (UNDO_BEGIN != null) UNDO_BEGIN.invoke(null);
|
||||||
|
Object attachments = GP_ATTACHMENTS.get(SKELETON);
|
||||||
|
Object slots = slots();
|
||||||
|
log("处理前:附件表 " + size(attachments) + " 项,插槽 " + size(slots) + " 个");
|
||||||
|
|
||||||
|
// 1) 重置图片附件变换
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object slot = get(slots, i);
|
||||||
|
if (!OUR_SLOTS.contains(slot)) continue;
|
||||||
|
Object cur = SLOT_CURRENT.invoke(slot);
|
||||||
|
Object setup = SLOT_SETUP.invoke(slot);
|
||||||
|
if (cur != null && GO.isInstance(cur)) {
|
||||||
|
log(" 重置前 " + nameOf(slot) + ":" + goValues(cur));
|
||||||
|
if (UNDO_RECORD != null) UNDO_RECORD.invoke(null, cur);
|
||||||
|
GO_SET.invoke(cur, 0f, 0f, 0f, 1f, 1f);
|
||||||
|
log(" 重置后 " + nameOf(slot) + ":" + goValues(cur));
|
||||||
|
RESET++;
|
||||||
|
} else if (cur != null) {
|
||||||
|
log(" " + nameOf(slot) + " 当前附件不是图片:" + cur.getClass().getName());
|
||||||
|
}
|
||||||
|
if (setup != null && setup != cur && GO.isInstance(setup)) {
|
||||||
|
if (UNDO_RECORD != null) UNDO_RECORD.invoke(null, setup);
|
||||||
|
GO_SET.invoke(setup, 0f, 0f, 0f, 1f, 1f);
|
||||||
|
RESET++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 删除占位(skin/placeholder)附件:Hi 实例,且它的 slot 属于我们建的那批
|
||||||
|
for (int i = size(attachments) - 1; i >= 0; i--) {
|
||||||
|
Object a = get(attachments, i);
|
||||||
|
if (a == null || !HI.isInstance(a)) continue;
|
||||||
|
Object slot = field(a, GN, GU);
|
||||||
|
if (slot == null || !OUR_SLOTS.contains(slot)) continue;
|
||||||
|
String slotName = nameOf(slot);
|
||||||
|
Object cur = SLOT_CURRENT.invoke(slot);
|
||||||
|
String curDesc = cur == null ? "null" : cur.getClass().getName();
|
||||||
|
log(" 删除占位 " + a.getClass().getName() + " (slot=" + slotName + "),该插槽当前附件=" + curDesc);
|
||||||
|
try {
|
||||||
|
ATTACH_DELETE.invoke(a);
|
||||||
|
DELETED++;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
FAILED++;
|
||||||
|
log(" 删除失败:" + e);
|
||||||
|
}
|
||||||
|
Object after = SLOT_CURRENT.invoke(slot);
|
||||||
|
log(" 删除后该插槽当前附件=" + (after == null ? "null" : after.getClass().getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UNDO_END != null) UNDO_END.invoke(null);
|
||||||
|
if (REFRESH != null) {
|
||||||
|
REFRESH.setAccessible(true);
|
||||||
|
REFRESH.invoke(TREE_VIEW, SKELETON, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
log("已刷新树节点");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 结果核对
|
||||||
|
Object after = GP_ATTACHMENTS.get(SKELETON);
|
||||||
|
log("处理后:附件表 " + size(after) + " 项");
|
||||||
|
int withImage = 0, withPlaceholder = 0;
|
||||||
|
for (int i = 0; i < size(after); i++) {
|
||||||
|
Object a = get(after, i);
|
||||||
|
if (a == null) continue;
|
||||||
|
Object slot = field(a, GN, GU);
|
||||||
|
if (slot == null || !OUR_SLOTS.contains(slot)) continue;
|
||||||
|
if (GO.isInstance(a)) withImage++;
|
||||||
|
if (HI.isInstance(a)) withPlaceholder++;
|
||||||
|
}
|
||||||
|
log("核对:属于目标插槽的图片附件 " + withImage + " 个,占位附件 " + withPlaceholder + " 个");
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object slot = get(slots, i);
|
||||||
|
if (!OUR_SLOTS.contains(slot)) continue;
|
||||||
|
Object cur = SLOT_CURRENT.invoke(slot);
|
||||||
|
log(" " + nameOf(slot) + " 当前附件=" + (cur == null ? "null" : nameOf(cur) + " " + goValues(cur)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String goValues(Object go) {
|
||||||
|
if (go == null) return "null";
|
||||||
|
String[] names = { "D", "B", "S", "L", "I", "C", "Z" };
|
||||||
|
String[] keys = { "x", "y", "rotation", "scaleX", "scaleY", "width", "height" };
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
try {
|
||||||
|
Field f = GO.getDeclaredField(names[i]);
|
||||||
|
f.setAccessible(true);
|
||||||
|
sb.append(keys[i]).append('=').append(f.getFloat(go)).append(' ');
|
||||||
|
} catch (Throwable t) { sb.append(keys[i]).append("=? "); }
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field firstArrayField(Class<?> owner, Class<?> elem) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(SKELETON);
|
||||||
|
if (v != null && size(v) > 0 && elem.isAssignableFrom(elemClass(v))) return f;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field firstTypeField(Class<?> owner, Class<?> type) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
if (tree == null) return null;
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object globalStatic(Class<?> holder, String typeName) {
|
||||||
|
for (Class<?> t = holder; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nameOf(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
try {
|
||||||
|
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
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 Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 热更:把"刚刚创建的附件"整理干净 —— 全部走编辑器自身 API,不重启。
|
||||||
|
*
|
||||||
|
* 1) 重置图片附件(GO)的变换:GO.D(FFFFF)V 就是编辑器自己的 setTransform,
|
||||||
|
* 字节码里依次写 D=x、B=y、S=rotation、L=scaleX、I=scaleY(见 javap -c GO)。
|
||||||
|
* 图片附件在 3.8 没有 shear 字段(JSON 写入器 sb_0.java:669-675 也只写 x/y/scaleX/scaleY/rotation/width/height)。
|
||||||
|
* 2) 删除 skin/占位附件:Hi.I() 返回 "placeholder",实例方法 Gn.L() 内部是
|
||||||
|
* `slot.skeleton.removeAttachment(this,true,true)`(javap -c Gn 已确认)。
|
||||||
|
*
|
||||||
|
* 目标对象 = 上一次热更里创建的 20 个插槽(骨骼是 bone6 的子骨骼)。
|
||||||
|
*/
|
||||||
|
public final class ResetMaker2 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Class<?> ARRAY, NODE, GN, GO, HI, GP, GU, GPC;
|
||||||
|
|
||||||
|
static Method SLOT_CURRENT, SLOT_SETUP, GO_SET, ATTACH_DELETE, REFRESH, UNDO_BEGIN, UNDO_END, UNDO_RECORD;
|
||||||
|
static Field GP_ATTACHMENTS, GP_SLOTS, BONE_PARENT;
|
||||||
|
static Object SKELETON, TREE_VIEW;
|
||||||
|
static Set<Object> OUR_SLOTS = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||||
|
static int RESET, DELETED, FAILED;
|
||||||
|
|
||||||
|
public static void agentmain(String args, Instrumentation inst) {
|
||||||
|
INST = inst;
|
||||||
|
try { run(); } catch (Throwable e) { log("失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String m) { System.err.println("[reset2] " + 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 = Class.forName("fI", true, L);
|
||||||
|
NODE = Class.forName("eM", true, L);
|
||||||
|
GN = Class.forName("Gn", true, L);
|
||||||
|
GO = Class.forName("GO", true, L);
|
||||||
|
HI = Class.forName("Hi", true, L);
|
||||||
|
GP = Class.forName("GP", true, L);
|
||||||
|
GU = Class.forName("GU", true, L);
|
||||||
|
GPC = Class.forName("Gp", true, L);
|
||||||
|
Class<?> VZ = Class.forName("VZ", true, L);
|
||||||
|
Class<?> OG = Class.forName("OG", true, L);
|
||||||
|
Class<?> UN = Class.forName("un", true, L);
|
||||||
|
|
||||||
|
// 编辑器自己的"切渲染线程并等待"
|
||||||
|
Method onRender = null;
|
||||||
|
for (Method m : OG.getDeclaredMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||||
|
}
|
||||||
|
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||||
|
onRender.setAccessible(true);
|
||||||
|
|
||||||
|
// 骨架:树根节点上的对象
|
||||||
|
TREE_VIEW = globalStatic(OG, VZ.getName());
|
||||||
|
Object roots = rootNodes(globalStatic(Class.forName("OG", true, L), "Uq"));
|
||||||
|
SKELETON = firstOfType(roots, GP);
|
||||||
|
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||||
|
|
||||||
|
GP_ATTACHMENTS = firstArrayField(GP, GN);
|
||||||
|
GP_SLOTS = firstArrayField(GP, GU);
|
||||||
|
BONE_PARENT = firstTypeField(GPC, GPC);
|
||||||
|
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||||
|
SLOT_SETUP = method(GU, "B", new Class<?>[] {}, GN);
|
||||||
|
GO_SET = method(GO, "D", new Class<?>[] { float.class, float.class, float.class, float.class, float.class }, void.class);
|
||||||
|
ATTACH_DELETE = method(GN, "L", new Class<?>[] {}, void.class);
|
||||||
|
REFRESH = method(VZ, "D", new Class<?>[] { GP, boolean.class, boolean.class }, void.class);
|
||||||
|
for (Method m : UN.getDeclaredMethods()) {
|
||||||
|
if (m.getParameterTypes().length == 0 && m.getReturnType() == void.class) {
|
||||||
|
if (m.getName().equals("D")) UNDO_BEGIN = m;
|
||||||
|
if (m.getName().equals("B")) UNDO_END = m;
|
||||||
|
}
|
||||||
|
if (m.getParameterTypes().length == 1 && m.getParameterTypes()[0] == Object.class) UNDO_RECORD = m;
|
||||||
|
}
|
||||||
|
if (GO_SET == null || ATTACH_DELETE == null || SLOT_CURRENT == null) { log("API 没找齐"); return; }
|
||||||
|
if (UNDO_BEGIN != null) UNDO_BEGIN.setAccessible(true);
|
||||||
|
if (UNDO_END != null) UNDO_END.setAccessible(true);
|
||||||
|
if (UNDO_RECORD != null) UNDO_RECORD.setAccessible(true);
|
||||||
|
GO_SET.setAccessible(true);
|
||||||
|
ATTACH_DELETE.setAccessible(true);
|
||||||
|
|
||||||
|
collectOurSlots();
|
||||||
|
log("目标插槽 = " + OUR_SLOTS.size() + " 个");
|
||||||
|
|
||||||
|
onRender.invoke(null, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log("完成:重置 " + RESET + " 个附件的变换,删除 " + DELETED + " 个占位(skin)附件,失败 " + FAILED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 上一次热更建的 20 个插槽:骨骼是 bone6 的子骨骼。 */
|
||||||
|
static void collectOurSlots() throws Exception {
|
||||||
|
// 扫描骨架上的所有 fI 字段,找出装着名字叫 bone6 的那个列表(不靠字段顺序猜)
|
||||||
|
Object bone6 = null;
|
||||||
|
for (Class<?> t = GP; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(SKELETON);
|
||||||
|
if (v == null) continue;
|
||||||
|
StringBuilder names = new StringBuilder();
|
||||||
|
for (int i = 0; i < size(v); i++) {
|
||||||
|
Object o = get(v, i);
|
||||||
|
if (o == null) continue;
|
||||||
|
String n = nameOf(o);
|
||||||
|
if (i < 4) names.append(n).append(',');
|
||||||
|
if (GPC.isInstance(o) && "bone6".equals(n)) bone6 = o;
|
||||||
|
}
|
||||||
|
if (size(v) > 0) log(" 字段 GP." + f.getName() + " size=" + size(v) + " 元素=" + elemClass(v).getSimpleName() + " 前几个名字=" + names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bone6 == null) { log("找不到 bone6"); return; }
|
||||||
|
for (int i = 0; i < size(slots()); i++) {
|
||||||
|
Object slot = get(slots(), i);
|
||||||
|
Object bone = field(slot, GU, GPC);
|
||||||
|
if (bone != null && field(bone, GPC, GPC) == bone6) OUR_SLOTS.add(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object slots() throws Exception { return GP_SLOTS.get(SKELETON); }
|
||||||
|
|
||||||
|
static void work() throws Exception {
|
||||||
|
if (UNDO_BEGIN != null) UNDO_BEGIN.invoke(null);
|
||||||
|
Object attachments = GP_ATTACHMENTS.get(SKELETON);
|
||||||
|
Object slots = slots();
|
||||||
|
log("处理前:附件表 " + size(attachments) + " 项,插槽 " + size(slots) + " 个");
|
||||||
|
|
||||||
|
// 1) 重置图片附件变换
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object slot = get(slots, i);
|
||||||
|
if (!OUR_SLOTS.contains(slot)) continue;
|
||||||
|
Object cur = SLOT_CURRENT.invoke(slot);
|
||||||
|
Object setup = SLOT_SETUP.invoke(slot);
|
||||||
|
if (cur != null && GO.isInstance(cur)) {
|
||||||
|
log(" 重置前 " + nameOf(slot) + ":" + goValues(cur));
|
||||||
|
if (UNDO_RECORD != null) UNDO_RECORD.invoke(null, cur);
|
||||||
|
GO_SET.invoke(cur, 0f, 0f, 0f, 1f, 1f);
|
||||||
|
log(" 重置后 " + nameOf(slot) + ":" + goValues(cur));
|
||||||
|
RESET++;
|
||||||
|
} else if (cur != null) {
|
||||||
|
log(" " + nameOf(slot) + " 当前附件不是图片:" + cur.getClass().getName());
|
||||||
|
}
|
||||||
|
if (setup != null && setup != cur && GO.isInstance(setup)) {
|
||||||
|
if (UNDO_RECORD != null) UNDO_RECORD.invoke(null, setup);
|
||||||
|
GO_SET.invoke(setup, 0f, 0f, 0f, 1f, 1f);
|
||||||
|
RESET++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 删除占位(skin/placeholder)附件:Hi 实例,且它的 slot 属于我们建的那批
|
||||||
|
for (int i = size(attachments) - 1; i >= 0; i--) {
|
||||||
|
Object a = get(attachments, i);
|
||||||
|
if (a == null || !HI.isInstance(a)) continue;
|
||||||
|
Object slot = field(a, GN, GU);
|
||||||
|
if (slot == null || !OUR_SLOTS.contains(slot)) continue;
|
||||||
|
String slotName = nameOf(slot);
|
||||||
|
Object cur = SLOT_CURRENT.invoke(slot);
|
||||||
|
String curDesc = cur == null ? "null" : cur.getClass().getName();
|
||||||
|
log(" 删除占位 " + a.getClass().getName() + " (slot=" + slotName + "),该插槽当前附件=" + curDesc);
|
||||||
|
try {
|
||||||
|
ATTACH_DELETE.invoke(a);
|
||||||
|
DELETED++;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
FAILED++;
|
||||||
|
log(" 删除失败:" + e);
|
||||||
|
}
|
||||||
|
Object after = SLOT_CURRENT.invoke(slot);
|
||||||
|
log(" 删除后该插槽当前附件=" + (after == null ? "null" : after.getClass().getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UNDO_END != null) UNDO_END.invoke(null);
|
||||||
|
if (REFRESH != null) {
|
||||||
|
REFRESH.setAccessible(true);
|
||||||
|
REFRESH.invoke(TREE_VIEW, SKELETON, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
log("已刷新树节点");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 结果核对
|
||||||
|
Object after = GP_ATTACHMENTS.get(SKELETON);
|
||||||
|
log("处理后:附件表 " + size(after) + " 项");
|
||||||
|
int withImage = 0, withPlaceholder = 0;
|
||||||
|
for (int i = 0; i < size(after); i++) {
|
||||||
|
Object a = get(after, i);
|
||||||
|
if (a == null) continue;
|
||||||
|
Object slot = field(a, GN, GU);
|
||||||
|
if (slot == null || !OUR_SLOTS.contains(slot)) continue;
|
||||||
|
if (GO.isInstance(a)) withImage++;
|
||||||
|
if (HI.isInstance(a)) withPlaceholder++;
|
||||||
|
}
|
||||||
|
log("核对:属于目标插槽的图片附件 " + withImage + " 个,占位附件 " + withPlaceholder + " 个");
|
||||||
|
for (int i = 0; i < size(slots); i++) {
|
||||||
|
Object slot = get(slots, i);
|
||||||
|
if (!OUR_SLOTS.contains(slot)) continue;
|
||||||
|
Object cur = SLOT_CURRENT.invoke(slot);
|
||||||
|
log(" " + nameOf(slot) + " 当前附件=" + (cur == null ? "null" : nameOf(cur) + " " + goValues(cur)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String goValues(Object go) {
|
||||||
|
if (go == null) return "null";
|
||||||
|
String[] names = { "D", "B", "S", "L", "I", "C", "Z" };
|
||||||
|
String[] keys = { "x", "y", "rotation", "scaleX", "scaleY", "width", "height" };
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < keys.length; i++) {
|
||||||
|
try {
|
||||||
|
Field f = GO.getDeclaredField(names[i]);
|
||||||
|
f.setAccessible(true);
|
||||||
|
sb.append(keys[i]).append('=').append(f.getFloat(go)).append(' ');
|
||||||
|
} catch (Throwable t) { sb.append(keys[i]).append("=? "); }
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field firstArrayField(Class<?> owner, Class<?> elem) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(SKELETON);
|
||||||
|
if (v != null && size(v) > 0 && elem.isAssignableFrom(elemClass(v))) return f;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Field firstTypeField(Class<?> owner, Class<?> type) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object rootNodes(Object tree) {
|
||||||
|
if (tree == null) return null;
|
||||||
|
for (Class<?> t = tree.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try {
|
||||||
|
Object v = f.get(tree);
|
||||||
|
if (size(v) > 0 && NODE.isAssignableFrom(elemClass(v))) return v;
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object firstOfType(Object nodes, Class<?> type) {
|
||||||
|
if (nodes == null) return null;
|
||||||
|
for (int i = 0; i < size(nodes); i++) {
|
||||||
|
Object node = get(nodes, i);
|
||||||
|
Object obj = field(node, NODE, Object.class);
|
||||||
|
if (obj != null && type.isInstance(obj)) return obj;
|
||||||
|
Object deep = firstOfType(field(node, NODE, ARRAY), type);
|
||||||
|
if (deep != null) return deep;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object field(Object target, Class<?> from, Class<?> type) {
|
||||||
|
if (target == null) return null;
|
||||||
|
for (Class<?> t = from; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { return f.get(target); } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object globalStatic(Class<?> holder, String typeName) {
|
||||||
|
for (Class<?> t = holder; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Field f : t.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String nameOf(Object o) {
|
||||||
|
if (o == null) return "null";
|
||||||
|
try {
|
||||||
|
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||||
|
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||||
|
} catch (Throwable t) { return "?"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static int size(Object a) {
|
||||||
|
if (a == null) return 0;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(a); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object get(Object a, int i) {
|
||||||
|
if (a == null) return null;
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); return ((Object[]) f.get(a))[i]; }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
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 Method method(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||||
|
for (Method m : t.getDeclaredMethods()) {
|
||||||
|
if (!m.getName().equals(name) || !Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
m.setAccessible(true);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到运行中的编辑器:给"当前选中骨骼的每个子骨骼"加一个插槽,并挂一张图片(图片名取自树的 Images 节点)。
|
||||||
|
* 全部走编辑器自身 API(javap 读出来的):
|
||||||
|
* UH.D(GU 模板插槽, Gp 父骨骼, boolean, fI) → 新插槽(名字用编辑器自己的不重名生成)
|
||||||
|
* UH.D(String 图片名, GU 插槽) → new Hi():把图片挂到该插槽
|
||||||
|
* VZ.D(editor, skeleton, true, true) → 一次刷新
|
||||||
|
*/
|
||||||
|
public final class SlotMaker1 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Object PARENT, SKELETON, EDITOR, TREE_VIEW, TEMPLATE_SLOT, OUT;
|
||||||
|
static Class<?> BONE, SLOT, ARRAY, UH, GP, VZ;
|
||||||
|
static Method ADD_SLOT, ADD_IMAGE, REFRESH, BEGIN, COMMIT, GET_SLOT_BONE, LIST_NODES;
|
||||||
|
static String[] IMAGES = {"mega_dg4", "mega_dg5", "mega_dg6"};
|
||||||
|
static int MADE;
|
||||||
|
|
||||||
|
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("[live-slot] " + m); }
|
||||||
|
|
||||||
|
static void run() throws Exception {
|
||||||
|
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||||
|
if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; }
|
||||||
|
}
|
||||||
|
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, L);
|
||||||
|
PARENT = sceneObjects.getMethod("selected", ClassLoader.class).invoke(null, L);
|
||||||
|
if (PARENT == null) { log("没有选中对象"); return; }
|
||||||
|
SKELETON = Class.forName("com.spineview.SceneActions", true, L)
|
||||||
|
.getMethod("skeletonOf", Object.class).invoke(null, PARENT);
|
||||||
|
log("选中 = " + sceneObjects.getMethod("nameOf", Object.class).invoke(null, PARENT));
|
||||||
|
|
||||||
|
BONE = Class.forName("Gp", true, L);
|
||||||
|
SLOT = Class.forName("GU", true, L);
|
||||||
|
ARRAY = Class.forName("fI", true, L);
|
||||||
|
GP = Class.forName("GP", true, L);
|
||||||
|
UH = Class.forName("UH", true, L);
|
||||||
|
VZ = Class.forName("VZ", true, L);
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
Class<?> undo = Class.forName("un", true, L);
|
||||||
|
Object uh = staticField(og, "UH");
|
||||||
|
UH_INSTANCE = uh;
|
||||||
|
EDITOR = staticField(og, "com.esotericsoftware.spine.editor.Editor");
|
||||||
|
TREE_VIEW = staticField(og, "VZ");
|
||||||
|
if (uh == null) { log("拿不到 UH 实例(OG.D:LUH;)"); return; }
|
||||||
|
ADD_SLOT = find(UH, "D", new Class<?>[] {SLOT, BONE, boolean.class, ARRAY}, SLOT);
|
||||||
|
ADD_IMAGE = find(UH, "D", new Class<?>[] {String.class, SLOT}, Class.forName("Hi", true, L));
|
||||||
|
REFRESH = find(VZ, "D", new Class<?>[] {GP, 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")) BEGIN = m;
|
||||||
|
if (m.getName().equals("B")) COMMIT = m;
|
||||||
|
}
|
||||||
|
// 模板插槽:用骨架里的第一个现有插槽
|
||||||
|
Object slots = null;
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (f.getType() == ARRAY) { f.setAccessible(true); Object v = f.get(SKELETON);
|
||||||
|
if (v != null && arraySize(v) > 0) { slots = v; break; } }
|
||||||
|
}
|
||||||
|
if (slots != null) TEMPLATE_SLOT = arrayGet(slots, 0);
|
||||||
|
log("模板插槽 = " + TEMPLATE_SLOT + " addSlot=" + ADD_SLOT + " addImage=" + ADD_IMAGE);
|
||||||
|
if (ADD_SLOT == null || ADD_IMAGE == null || TEMPLATE_SLOT == null) { log("API 没找齐"); return; }
|
||||||
|
|
||||||
|
Method onRender = null;
|
||||||
|
for (Method m : og.getDeclaredMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||||
|
}
|
||||||
|
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||||
|
onRender.setAccessible(true);
|
||||||
|
onRender.invoke(null, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log("完成:给 " + MADE + " 根子骨骼各加了插槽 + 图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void work() throws Exception {
|
||||||
|
ADD_SLOT.setAccessible(true);
|
||||||
|
ADD_IMAGE.setAccessible(true);
|
||||||
|
if (BEGIN != null) { BEGIN.setAccessible(true); BEGIN.invoke(null); }
|
||||||
|
// 选中骨骼的子骨骼(Gp 上指向父骨骼的字段是 LGp;)
|
||||||
|
Field parentField = null;
|
||||||
|
for (Field f : BONE.getDeclaredFields()) {
|
||||||
|
if (f.getType() == BONE) { f.setAccessible(true); parentField = f; break; }
|
||||||
|
}
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (f.getType() == ARRAY) { f.setAccessible(true); Object v = f.get(SKELETON);
|
||||||
|
if (v != null && arraySize(v) >= 10) { bones = v; break; } }
|
||||||
|
}
|
||||||
|
int index = 0;
|
||||||
|
for (int i = 0; i < arraySize(bones); i++) {
|
||||||
|
Object bone = arrayGet(bones, i);
|
||||||
|
if (bone == null) continue;
|
||||||
|
Object parentOfBone = parentField == null ? null : parentField.get(bone);
|
||||||
|
if (parentOfBone != PARENT) continue; // 只处理"选中骨骼的子骨骼"
|
||||||
|
Object out = ARRAY.getConstructor().newInstance();
|
||||||
|
Object slot = ADD_SLOT.invoke(uh(), TEMPLATE_SLOT, bone, Boolean.FALSE, out);
|
||||||
|
if (slot == null) continue;
|
||||||
|
ADD_IMAGE.invoke(uh(), IMAGES[index % IMAGES.length], slot); // 挂图(轮流 3 张)
|
||||||
|
index++;
|
||||||
|
MADE++;
|
||||||
|
}
|
||||||
|
if (COMMIT != null) { COMMIT.setAccessible(true); COMMIT.invoke(null); }
|
||||||
|
if (REFRESH != null) {
|
||||||
|
REFRESH.setAccessible(true);
|
||||||
|
REFRESH.invoke(TREE_VIEW, SKELETON, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
log("已刷新树节点");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object UH_INSTANCE;
|
||||||
|
static Object uh() { return UH_INSTANCE; }
|
||||||
|
|
||||||
|
static int arraySize(Object array) {
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(array); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object arrayGet(Object array, int i) {
|
||||||
|
try {
|
||||||
|
Object[] items = null;
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); items = (Object[]) f.get(array); break; }
|
||||||
|
}
|
||||||
|
return items == null ? null : items[i];
|
||||||
|
} catch (Throwable ignored) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Method m : owner.getMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
if (!java.util.Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object staticField(Class<?> owner, String typeName) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
try { f.setAccessible(true); Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.lang.reflect.*;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 挂到运行中的编辑器:给"当前选中骨骼的每个子骨骼"加一个插槽,并挂一张图片(图片名取自树的 Images 节点)。
|
||||||
|
* 全部走编辑器自身 API(javap 读出来的):
|
||||||
|
* UH.D(GU 模板插槽, Gp 父骨骼, boolean, fI) → 新插槽(名字用编辑器自己的不重名生成)
|
||||||
|
* UH.D(String 图片名, GU 插槽) → new Hi():把图片挂到该插槽
|
||||||
|
* VZ.D(editor, skeleton, true, true) → 一次刷新
|
||||||
|
*/
|
||||||
|
public final class SlotMaker2 {
|
||||||
|
static Instrumentation INST;
|
||||||
|
static ClassLoader L;
|
||||||
|
static Object PARENT, SKELETON, EDITOR, TREE_VIEW, TEMPLATE_SLOT, OUT;
|
||||||
|
static Class<?> BONE, SLOT, ARRAY, UH, GP, VZ;
|
||||||
|
static Method ADD_SLOT, ADD_IMAGE, REFRESH, BEGIN, COMMIT, GET_SLOT_BONE, LIST_NODES;
|
||||||
|
static String[] IMAGES = {"mega_dg4", "mega_dg5", "mega_dg6"};
|
||||||
|
static int MADE;
|
||||||
|
|
||||||
|
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("[live-slot2] " + m); }
|
||||||
|
|
||||||
|
static void run() throws Exception {
|
||||||
|
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||||
|
if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; }
|
||||||
|
}
|
||||||
|
if (L == null) { log("找不到 URLClassLoader"); return; }
|
||||||
|
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, L);
|
||||||
|
PARENT = sceneObjects.getMethod("selected", ClassLoader.class).invoke(null, L);
|
||||||
|
if (PARENT == null) { log("没有选中对象"); return; }
|
||||||
|
SKELETON = Class.forName("com.spineview.SceneActions", true, L)
|
||||||
|
.getMethod("skeletonOf", Object.class).invoke(null, PARENT);
|
||||||
|
log("选中 = " + sceneObjects.getMethod("nameOf", Object.class).invoke(null, PARENT));
|
||||||
|
|
||||||
|
BONE = Class.forName("Gp", true, L);
|
||||||
|
SLOT = Class.forName("GU", true, L);
|
||||||
|
ARRAY = Class.forName("fI", true, L);
|
||||||
|
GP = Class.forName("GP", true, L);
|
||||||
|
UH = Class.forName("UH", true, L);
|
||||||
|
VZ = Class.forName("VZ", true, L);
|
||||||
|
Class<?> og = Class.forName("OG", true, L);
|
||||||
|
Class<?> undo = Class.forName("un", true, L);
|
||||||
|
Object uh = staticField(og, "UH");
|
||||||
|
UH_INSTANCE = uh;
|
||||||
|
EDITOR = staticField(og, "com.esotericsoftware.spine.editor.Editor");
|
||||||
|
TREE_VIEW = staticField(og, "VZ");
|
||||||
|
if (uh == null) { log("拿不到 UH 实例(OG.D:LUH;)"); return; }
|
||||||
|
ADD_SLOT = find(UH, "D", new Class<?>[] {SLOT, BONE, boolean.class, ARRAY}, SLOT);
|
||||||
|
ADD_IMAGE = find(UH, "D", new Class<?>[] {String.class, SLOT}, Class.forName("Hi", true, L));
|
||||||
|
REFRESH = find(VZ, "D", new Class<?>[] {GP, 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")) BEGIN = m;
|
||||||
|
if (m.getName().equals("B")) COMMIT = m;
|
||||||
|
}
|
||||||
|
// 模板插槽:用骨架里的第一个现有插槽
|
||||||
|
Object slots = null;
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (f.getType() != ARRAY) continue;
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object v = f.get(SKELETON);
|
||||||
|
if (v == null || arraySize(v) == 0) continue;
|
||||||
|
if (SLOT.isInstance(arrayGet(v, 0))) { slots = v; break; } // 只认"元素是插槽"的数组
|
||||||
|
}
|
||||||
|
if (slots != null) TEMPLATE_SLOT = arrayGet(slots, 0);
|
||||||
|
log("模板插槽 = " + TEMPLATE_SLOT + " addSlot=" + ADD_SLOT + " addImage=" + ADD_IMAGE);
|
||||||
|
if (ADD_SLOT == null || ADD_IMAGE == null || TEMPLATE_SLOT == null) { log("API 没找齐"); return; }
|
||||||
|
|
||||||
|
Method onRender = null;
|
||||||
|
for (Method m : og.getDeclaredMethods()) {
|
||||||
|
Class<?>[] t = m.getParameterTypes();
|
||||||
|
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) { onRender = m; break; }
|
||||||
|
}
|
||||||
|
if (onRender == null) { log("找不到 OG.B(Runnable)"); return; }
|
||||||
|
onRender.setAccessible(true);
|
||||||
|
onRender.invoke(null, new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
log("完成:给 " + MADE + " 根子骨骼各加了插槽 + 图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void work() throws Exception {
|
||||||
|
ADD_SLOT.setAccessible(true);
|
||||||
|
ADD_IMAGE.setAccessible(true);
|
||||||
|
if (BEGIN != null) { BEGIN.setAccessible(true); BEGIN.invoke(null); }
|
||||||
|
// 选中骨骼的子骨骼(Gp 上指向父骨骼的字段是 LGp;)
|
||||||
|
Field parentField = null;
|
||||||
|
for (Field f : BONE.getDeclaredFields()) {
|
||||||
|
if (f.getType() == BONE) { f.setAccessible(true); parentField = f; break; }
|
||||||
|
}
|
||||||
|
Object bones = null;
|
||||||
|
for (Field f : GP.getDeclaredFields()) {
|
||||||
|
if (f.getType() == ARRAY) { f.setAccessible(true); Object v = f.get(SKELETON);
|
||||||
|
if (v != null && arraySize(v) >= 10) { bones = v; break; } }
|
||||||
|
}
|
||||||
|
int index = 0;
|
||||||
|
for (int i = 0; i < arraySize(bones); i++) {
|
||||||
|
Object bone = arrayGet(bones, i);
|
||||||
|
if (bone == null) continue;
|
||||||
|
Object parentOfBone = parentField == null ? null : parentField.get(bone);
|
||||||
|
if (parentOfBone != PARENT) continue; // 只处理"选中骨骼的子骨骼"
|
||||||
|
Object out = ARRAY.getConstructor().newInstance();
|
||||||
|
Object slot = ADD_SLOT.invoke(uh(), TEMPLATE_SLOT, bone, Boolean.FALSE, out);
|
||||||
|
if (slot == null) continue;
|
||||||
|
ADD_IMAGE.invoke(uh(), IMAGES[index % IMAGES.length], slot); // 挂图(轮流 3 张)
|
||||||
|
index++;
|
||||||
|
MADE++;
|
||||||
|
}
|
||||||
|
if (COMMIT != null) { COMMIT.setAccessible(true); COMMIT.invoke(null); }
|
||||||
|
if (REFRESH != null) {
|
||||||
|
REFRESH.setAccessible(true);
|
||||||
|
REFRESH.invoke(TREE_VIEW, SKELETON, Boolean.TRUE, Boolean.TRUE);
|
||||||
|
log("已刷新树节点");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object UH_INSTANCE;
|
||||||
|
static Object uh() { return UH_INSTANCE; }
|
||||||
|
|
||||||
|
static int arraySize(Object array) {
|
||||||
|
try {
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == int.class) { f.setAccessible(true); return f.getInt(array); }
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) { }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object arrayGet(Object array, int i) {
|
||||||
|
try {
|
||||||
|
Object[] items = null;
|
||||||
|
for (Field f : ARRAY.getDeclaredFields()) {
|
||||||
|
if (f.getType() == Object[].class) { f.setAccessible(true); items = (Object[]) f.get(array); break; }
|
||||||
|
}
|
||||||
|
return items == null ? null : items[i];
|
||||||
|
} catch (Throwable ignored) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||||
|
for (Method m : owner.getMethods()) {
|
||||||
|
if (!m.getName().equals(name)) continue;
|
||||||
|
if (!java.util.Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||||
|
if (ret != null && m.getReturnType() != ret) continue;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Object staticField(Class<?> owner, String typeName) {
|
||||||
|
for (Field f : owner.getDeclaredFields()) {
|
||||||
|
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||||
|
if (!f.getType().getName().equals(typeName)) continue;
|
||||||
|
try { f.setAccessible(true); Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -344,3 +344,42 @@ macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所
|
|||||||
|
|
||||||
> 教训也写进技能了:**新确认的映射必须当场写进 `tools/deobf/anchors.tsv` 并同步 `names.tsv` / `src-named`**,
|
> 教训也写进技能了:**新确认的映射必须当场写进 `tools/deobf/anchors.tsv` 并同步 `names.tsv` / `src-named`**,
|
||||||
> 不许只留在对话里(见 `.codex/skills/scope-discipline/SKILL.md` 末节)。
|
> 不许只留在对话里(见 `.codex/skills/scope-discipline/SKILL.md` 末节)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、图片附件(`GO`)与占位附件(`Hi`)(2026-10-16 实机确认)
|
||||||
|
|
||||||
|
这一节是"给已经建好的附件收尾"时确认的:重置图片附件的变换、删掉多余的 skin/占位附件。
|
||||||
|
|
||||||
|
### 图片附件的 7 个 float 字段
|
||||||
|
|
||||||
|
| `GO` 字段 | 真名 | 依据 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `D`(float 的那个) | **x** | ①编辑器自己的 JSON 写入器 `sb_0.java:669-675` 写 `"x"` 时读的就是它 ②`javap -c GO` 的 `D(FFFFF)V` 第一个 `putfield` 是它 |
|
||||||
|
| `B`(float 的那个) | **y** | 同上(JSON 写 `"y"`、`D(FFFFF)V` 第 2 个 `putfield`) |
|
||||||
|
| `S` | **rotation** | JSON 写 `"rotation"`;`D(FFFFF)V` 第 3 个参数 |
|
||||||
|
| `L` | **scaleX** | JSON 写 `"scaleX"`;`D(FFFFF)V` 第 4 个参数 |
|
||||||
|
| `I` | **scaleY** | JSON 写 `"scaleY"`;`D(FFFFF)V` 第 5 个参数 |
|
||||||
|
| `C` / `Z` | **width / height** | JSON 写 `"width"/"height"`;实测 `C=157.0 Z=154.0` 与 `mega_dg4.png` 真实像素 157x154 一致 |
|
||||||
|
|
||||||
|
> 注意:`GO` 上有多个同名字段 `D`(float / iU / pL / long / float[]),
|
||||||
|
> 按名字 `getDeclaredField("D")` 会取错;必须挑**类型是 float 的那个**。
|
||||||
|
|
||||||
|
`GO.D(FFFFF)V` = 编辑器自己的 **setTransform(x, y, rotation, scaleX, scaleY)**,重置一个图片附件的变换就用它。
|
||||||
|
图片附件**没有 shear(斜切)字段**:region 只有 x/y/rotation/scaleX/scaleY。
|
||||||
|
|
||||||
|
### 附件删除 / 占位附件
|
||||||
|
|
||||||
|
| 载荷签名 | 真名 | 依据 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Gn.L()V` | **delete(删掉这个附件)** | `javap -c Gn`:`this.GU.skeleton.removeAttachment(this, true, true)` |
|
||||||
|
| `GP.L(LGn;ZZ)V` | **removeAttachment** | 字节码里 `getfield L:LfI; fI.B(Object,true)` 从附件表移除,并清掉 `GU`(插槽表 `Z:LfI`)上指向它的引用 |
|
||||||
|
| `GP.L:LfI` / `GP.Z:LfI` | **attachments / slots** | 骨架的附件表(本次从 43 项减到 23 项)/ 插槽表 |
|
||||||
|
| `Gn.I()Ljava/lang/String;` | **type** | 占位附件 `Hi`(可读源码 `hi_2.java`)直接 `return "placeholder"` |
|
||||||
|
| `Hi`(载荷类) | **占位附件(skin placeholder)** | 它挂在骨架附件表里,但**既不是插槽的当前附件也不是静止附件**;`GP.removeAttachment` 会按 `Gn.D:LHi` 把引用它的附件级联删掉 |
|
||||||
|
|
||||||
|
**本次实机结果**(不重启,JDK8 attach 热更,进程仍是同一个):
|
||||||
|
|
||||||
|
- 20 个新建插槽的图片附件:`x/y/rotation` 从 `-99.49988 / -302.0 / 40.00014`、`scaleX/scaleY` 从 `1.997 / 1.532` → 全部重置为 `0/0/0` 与 `1/1`(宽度高度 157x154 保持不变)。
|
||||||
|
- 20 个 `Hi` 占位附件全部删除;骨架附件表 `43 → 23`(正好 23 个插槽各 1 个图片附件)。
|
||||||
|
- 删除占位附件**不会**连带删掉图片附件:日志里逐个核对了"删除后该插槽当前附件=GO"。
|
||||||
Reference in new issue
Block a user