Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64361e389e | ||
|
|
0a2da7f11b | ||
|
|
f40979f965 | ||
|
|
42b40f6f65 | ||
|
|
b8e84c7aa5 | ||
|
|
7f06c6744a | ||
|
|
bb817b85e5 | ||
|
|
7f519edc67 | ||
|
|
0886ba647c | ||
|
|
4000a06d61 |
No files matched your search
@@ -25,6 +25,9 @@ rea/
|
||||
viewpanel/build/
|
||||
viewpanel/compat-classes/
|
||||
*.class
|
||||
# 热更(attach)小工具:只留源码,jar 和编译输出是临时的
|
||||
tools/live/build*/
|
||||
tools/live/*.jar
|
||||
|
||||
# ---- 从载荷派生的批量产物(本机可重新生成)----
|
||||
viewpanel/analysis/spine-398/
|
||||
|
||||
Binary file not shown.
+261
-14
@@ -38,27 +38,25 @@ Gp Code F scaleX
|
||||
Gp SourceFile F scaleY
|
||||
Gp StackMapTable F shearX
|
||||
Gp LineNumberTable F shearY
|
||||
# 静止姿态(rest pose)那一组:值与默认姿态吻合(0,0,0,1,1,0),且未改动时与上面那组相等 —— 推断,留注释待复核
|
||||
Gp D F setupX
|
||||
Gp B F setupY
|
||||
Gp L F setupRotation
|
||||
Gp I F setupScaleX
|
||||
Gp S F setupScaleY
|
||||
Gp C F setupShear
|
||||
# 【已作废·未定】静止姿态(rest pose)那一组(D/B/L/I/S/C 的 float)原本只是"值与默认姿态吻合"的推断,
|
||||
# 2026-10-16 被 JSON 导出器证伪:导出器写 length 用的就是 Gp.C:F(见文件末尾"JSON 导出器"一节)。
|
||||
# C 只能是 length,故整组推断不成立;这些字段的语义**目前未定**,不写进表里(宁可显示混淆名也不写错名)。
|
||||
# Gp D F setupX
|
||||
# Gp B F setupY
|
||||
# Gp L F setupRotation
|
||||
# Gp I F setupScaleX
|
||||
# Gp S F setupScaleY
|
||||
# 引用类字段:skeleton 由 hd.D(set, bone.D)+GP.D(LGp;) 的用法确定;parent 由创建骨骼时 bone.D=parent 确定
|
||||
Gp D LGP; skeleton
|
||||
Gp D LGp; parent
|
||||
Gp B Lcom/badlogic/gdx/graphics/Color; color
|
||||
# ---- 2026-10-16 setup pose 实测补充(用户拍照:root/bone/slot/region 四种对象)----
|
||||
# Gp(root/bone):面板 Length=0.0 与 float Class=0.0 同;Ljava/java 与 Code/SourceFile 相等 → 世界缩放
|
||||
Gp Class F length
|
||||
Gp Ljava F worldScaleX
|
||||
Gp java F worldScaleY
|
||||
# GU(插槽 mega_dg4):面板 Slot 属性只有 Name/Color;Inspector 里 String D=mega_dg4、Gp D=所属骨骼、Gn D=附件
|
||||
# 【已作废·未定】原写 Gp.Class:F=length / Ljava,java=worldScale*:都是"数值相等"的弱证据,
|
||||
# 其中 length 已被 JSON 导出器改成 Gp.C:F(导出器写 length 时读的就是 C)。
|
||||
# 剩下 Class/Ljava/java/init/String 这几个 float 导出器一个都没用,语义未定,不写进表。
|
||||
# GU(插槽):面板 Slot 属性只有 Name/Color;Inspector 里 String D=mega_dg4、Gp D=所属骨骼
|
||||
GU D Ljava/lang/String; name
|
||||
GU D LGp; bone
|
||||
GU D LGn; attachment
|
||||
GU D Lcom/badlogic/gdx/graphics/Color; color
|
||||
# GO(region 附件 mega_dg4):面板只有 Image path/Color;Inspector 六个 float 与
|
||||
# Region 附件的 旋转49.00014 / 缩放(1.997,1.335) / 宽高(157,154) 一一对应
|
||||
GO S F rotation
|
||||
@@ -103,3 +101,252 @@ dR I (F)LdR; height
|
||||
# 故 B()F = getHeight、D()F = getWidth。
|
||||
dt B ()F getHeight
|
||||
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
|
||||
|
||||
# ============================================================================
|
||||
# 【权威来源】编辑器自带的 JSON 导出器(载荷类 sB,2026-10-16)
|
||||
# ----------------------------------------------------------------------------
|
||||
# 为什么权威:导出 JSON 时**每个 key 后面紧跟被读的那个字段**,key 名就是字段语义。
|
||||
# 证据链(两处独立,顺序逐条一致):
|
||||
# 1) 可读载荷 ~/Desktop/spine398/spine398.jar(DecAll 产出:ldc 密文已换明文、解密调用已换 nop)
|
||||
# `javap -p -c -cp spine398.jar sB` → `ldc "rotation"` 紧跟 `getfield Gp.F:F`,全部可读;
|
||||
# 2) 未解密载荷 (payload-public.zip) 的 strings.tsv(class sB, method D):同一(类,方法)内
|
||||
# 密文顺序 = 字节码顺序,281 条与可读副本里的 key 顺序逐条对得上(含三组解密器)。
|
||||
# 导出器方法:sB.D(GP,boolean,boolean,gy,StringBuilder)=骨架;sB.D(gy,String,Gn,boolean)=附件。
|
||||
# 字段名重复(D/B/L/I/S…)时**必须按描述符取**,表里已经带描述符。
|
||||
# ============================================================================
|
||||
|
||||
# ---- Gp = 骨骼(导出器写 "bones" 段时读的就是这些)----
|
||||
Gp C F length
|
||||
# ^ length 是 C(之前写的 Class:F 是错的,已作废)
|
||||
Gp D LiC; transform
|
||||
# ^ iC = 继承模式枚举(normal/onlyTranslation/noScale…)
|
||||
Gp String Z skinOverride
|
||||
# ^ 导出器写 key="skin"(布尔:该骨骼是否覆盖当前 skin)
|
||||
|
||||
# ---- GU = 插槽(导出器写 "slots" 段)----
|
||||
GU B Lcom/badlogic/gdx/graphics/Color; color
|
||||
# ^ 写 key="color" 读的是 B(不是 D)
|
||||
GU I Lcom/badlogic/gdx/graphics/Color; darkColor
|
||||
# ^ 写 key="dark"
|
||||
GU I Z hasDarkColor
|
||||
# ^ 写 "dark" 之前先看这个布尔开关
|
||||
GU B LGn; attachment
|
||||
# ^ 写 key="attachment" 读的是 B(不是 D)
|
||||
GU D Liz; blend
|
||||
# ^ iz = 混合模式枚举
|
||||
|
||||
# ---- Gn = 附件基类 ----
|
||||
Gn D Ljava/lang/String; name
|
||||
|
||||
# ---- GO = 图片(region)附件 ----
|
||||
GO D Lcom/badlogic/gdx/graphics/Color; color
|
||||
GO B Ljava/lang/String; path
|
||||
# ^ 导出器写 key="path",值取自静态取值器 GO.B(GO)
|
||||
|
||||
# ---- GE = 点(point)附件 ----
|
||||
GE B Lcom/badlogic/gdx/graphics/Color; color
|
||||
GE D F x
|
||||
GE B F y
|
||||
GE L F rotation
|
||||
|
||||
# ---- Gx = 网格(mesh / linkedmesh)附件 ----
|
||||
Gx B Lcom/badlogic/gdx/graphics/Color; color
|
||||
Gx B Ljava/lang/String; path
|
||||
Gx D Lga; uvs
|
||||
Gx D Lhp; triangles
|
||||
Gx B LHl; vertices
|
||||
Gx L I hull
|
||||
# ^ 导出器写 hull = Gx.L / 2
|
||||
Gx D Lgf; edges
|
||||
Gx StackMapTable Z deform
|
||||
Gx D F width
|
||||
# ^ 只在导出器带 extra 标志时写
|
||||
Gx B F height
|
||||
Gx D LGx; parentMesh
|
||||
# ^ 写 "parent" 时先取它(父网格附件)
|
||||
Gx D LGT; skin
|
||||
# ^ 写 "skin" 时取父网格的 skin
|
||||
Gx D LHi; parentPlaceholder
|
||||
# ^ 父网格的占位附件(Hi)
|
||||
Gx D Ljava/lang/String; parentName
|
||||
# ^ 写 "parent"(非 linkedmesh 分支)
|
||||
|
||||
# ---- Gr = 包围盒(boundingbox)附件 ----
|
||||
Gr I Lga; vertexCount
|
||||
Gr B LHl; vertices
|
||||
Gr B Lcom/badlogic/gdx/graphics/Color; color
|
||||
|
||||
# ---- Gs = 裁剪(clipping)附件 ----
|
||||
Gs I Lga; vertexCount
|
||||
Gs B LHl; vertices
|
||||
Gs B Lcom/badlogic/gdx/graphics/Color; color
|
||||
Gs B LGU; end
|
||||
# ^ 裁剪结束的那个插槽
|
||||
|
||||
# ---- Gw = IK 约束 ----
|
||||
Gw D Ljava/lang/String; name
|
||||
Gw D LGp; target
|
||||
Gw D F mix
|
||||
Gw L F softness
|
||||
Gw F Z uniform
|
||||
Gw S Z stretch
|
||||
Gw Z Z compress
|
||||
|
||||
# ---- GX = 变换(transform)约束 ----
|
||||
GX D Ljava/lang/String; name
|
||||
GX B LGp; target
|
||||
GX SourceFile F rotation
|
||||
GX S Z local
|
||||
GX C Z relative
|
||||
GX L F x
|
||||
GX I F y
|
||||
GX StackMapTable F scaleX
|
||||
GX init F scaleY
|
||||
GX LineNumberTable F shearY
|
||||
GX S F rotateMix
|
||||
GX D F translateMix
|
||||
GX Z F scaleMix
|
||||
GX F F shearMix
|
||||
|
||||
# ---- GD = 路径(path)约束 ----
|
||||
GD D Ljava/lang/String; name
|
||||
GD D LGU; target
|
||||
GD D Ljm; positionMode
|
||||
GD D Ljo; spacingMode
|
||||
GD D Ljn; rotateMode
|
||||
GD Z F rotation
|
||||
GD D F position
|
||||
GD V F spacing
|
||||
GD S F rotateMix
|
||||
GD L F translateMix
|
||||
|
||||
# ============================================================================
|
||||
# 【面板/新按钮用到的"操作类" API】(2026-10-16 热更实测通过:不重启、纯编辑器自身 API)
|
||||
# 场景:点面板按钮 → 弹名称输入面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼,
|
||||
# 每根 1 插槽 1 附件(输入的附件名),位移 xy 0~500 随机、缩放 1、旋转/斜切 0,
|
||||
# 插槽 alpha 0.5~1 随机、blend normal/additive 随机、附件在插槽上变换 0/0/0/1/1。
|
||||
# 证据:`javap -p -c` 字节码 + attach 到运行进程逐条实测(每个 API 都真跑过并核对过结果)。
|
||||
# ============================================================================
|
||||
|
||||
# ---- 建对象(编辑器自己的"加"API)----
|
||||
# UH = OG 的模型编辑助手(OG.D:LUH;)。下面两个都实测建出来过:
|
||||
UH D (LGU;LGp;Z;LfI;)LGU; addSlot
|
||||
# ^ 加插槽:参数(插槽模板, 父骨骼, ?, 不重名用列表)。⚠️ 它会把模板插槽**整个复制**,连模板插槽上挂的附件一起复制(实测踩到过)
|
||||
UH D (LGn;LGU;ZZLfI;)LGn; addAttachmentCopy
|
||||
# ^ 加附件:参数(附件模板, 插槽, ?, ?, 列表);内部就是 `Gn.D(LGP;)LGn;` 复制一份模板附件,并挂到该插槽
|
||||
GP D (LGp;)V addBone
|
||||
# ^ 加骨骼:字节码 `bone.B = array.size; array.add(bone)`(骨架的骨骼表)
|
||||
OG D (Ljava/lang/String;LfI;LGU;)Ljava/lang/String; uniqueName
|
||||
# ^ 编辑器自己的不重名生成器:`OG.D("bone", 骨骼数组, null)` 会给出 bone7 这种名字
|
||||
GU D (LGn;Z)V setAttachment
|
||||
# ^ 设插槽的当前附件:第二参=是否记撤销;内部按 `Editor.V`(animate/setup)只写 `D:LGn;` 或 `B:LGn;` 其中一个 ——
|
||||
# 所以要"两种模式都覆盖"就直接写两个字段(本次就是这么做的)
|
||||
GU D LGn; attachmentAnimate
|
||||
# ^ 与 `GU.B:LGn;` 成对:`D` 是 animate 模式用的当前附件,`B` 是 setup/非 animate 用的(JSON 导出器读的是 B)
|
||||
VZ D (LGP;ZZ)V refresh
|
||||
# ^ 改完模型重建层级树节点(本次每批建完都调它)
|
||||
un D ()V undoBegin
|
||||
un B ()V undoCommit
|
||||
# ^ 撤销帧的 开始/提交(改模型前 D()、改完 B())
|
||||
un Z ()V undo
|
||||
# ^ ★ 撤销一步:实测有效(`up.D:` 栈 23→22、redo 栈 0→1,场景对象数同步回退)。
|
||||
# 本次误删骨骼后就是用连点 un.Z() 一键一键撤回来的。另外 un.S()/un.C()/un.V()/un.I() 不是撤销:
|
||||
# S=跳到历史某下标、C=置标志、V=按当前下标重放、I=up.D()(无效果)
|
||||
|
||||
# ---- 名称输入面板(编辑器自带的那套对话框)----
|
||||
xl L (Ljava/lang/Object;)V onOkCancel
|
||||
# ^ 对话框的 OK/Cancel 总回调:收到 Boolean(TRUE=OK、FALSE=Cancel)。
|
||||
# OK 时它会读名字框、去重校验,然后调下面的抽象方法;Cancel 直接 return
|
||||
xl L (Ljava/lang/String;)V onNameEntered
|
||||
# ^ **抽象方法**:OK 且名字合法时被调用,参数就是输入的名字。想拿名字就子类化它(本次是 ASM 现生成子类)
|
||||
xl D (Ldt;)LvH; show
|
||||
# ^ 显示对话框。⚠️ 实测:**光调它窗口不会挂进 Stage**(parent 一直 null、屏幕上什么都不出现),
|
||||
# 必须再显式 `dt.D(Ldl;)V`(Stage.addActor)挂上去,然后自己 setPosition
|
||||
xl D LKp; nameField
|
||||
# ^ 对话框里的名字输入框(Kp = TextField 子类):构造时会用它填入默认名,OK 时从它读名字
|
||||
vH B (Ljava/lang/Object;)V okPressed
|
||||
# ^ OK 按钮的回调:唯一会去调 `L(Object)` 的地方(javap -c vH 可见)
|
||||
vH L (Ljava/lang/Object;)V onOkCancelNoop
|
||||
# ^ 空实现(xl 覆盖了它)
|
||||
Kp B ()Ljava/lang/String; getText
|
||||
Kp B (Ljava/lang/String;)V setText
|
||||
# ^ 名字框取/填文字(xl/xp 的构造与 L(Object) 里就是这么用的)
|
||||
|
||||
# ---- Actor / Stage / WidgetGroup(定位、挂载、删除)----
|
||||
dt D (Ldl;)V addActor
|
||||
# ^ Stage.addActor:字节码就是 `this.D:Ldo;.addActor(actor)`(root.addActor)。
|
||||
# ⚠️ 用它把对话框挂上屏幕(配合 xl.D(dt));也踩过坑:它会把 Actor 从原父级摘走(当年把面板列表搬出面板就是它)
|
||||
dl D Ldo; parent
|
||||
dl D Ldt; stage
|
||||
# ^ Actor 上的两个同名字段:`do` 类型的是父控件,`dt` 类型的是所在 Stage。按类型取,别按名字取
|
||||
dl D ()Ldt; getStage
|
||||
dl b_ ()Z remove
|
||||
# ^ 把 Actor 从父级摘掉。⚠️ `eX.b_()V`(invalidate)与它同名不同描述符 —— `getMethod("b_")` 会取到 void 那个,
|
||||
# 结果就是"调了删除却没删掉"(本次踩到)
|
||||
eX b_ ()V invalidate
|
||||
eX V (Z)V setFillParent
|
||||
dl c_ ()F getX
|
||||
dl d_ ()F getY
|
||||
dl L ()F getWidth
|
||||
dl I ()F getHeight
|
||||
dl S ()F getTop
|
||||
dl C ()F getRight
|
||||
# ^ Actor 的几何读取(都在 dl 上用 `javap -p -c` 对出来的:c_=x、d_=y、L=width、I=height、S=y+h、C=x+w)。
|
||||
# ⚠️ `et`(Table) 自己还声明了 `int L()`/`int I()`(列数/行数),所以按名字取必须**同时按返回类型 float 过滤**
|
||||
dl D (FFI)V setPosition
|
||||
# ^ setPosition(x, y, alignment):**align=0 时是"以 (x,y) 为中心"**(b1 不是 left/right 就按 center 处理)。
|
||||
# 本次要把对话框摆屏幕正中就是靠这个:直接传 Stage 中心即可
|
||||
|
||||
# ---- 混合模式枚举(插槽 blend)----
|
||||
iz D Liz; normal
|
||||
iz B Liz; additive
|
||||
iz L Liz; multiply
|
||||
iz I Liz; screen
|
||||
# ^ `iz`(=BlendMode) 的常量顺序取自 `iz.<clinit>`:依次 new iz("normal",0,770,1,771)、("additive",1,770,1,1)、
|
||||
# ("multiply",2,774,774,771)、("screen",3,1,1,769) —— 名字是常量池里的明文
|
||||
|
||||
# ---- 面板自己那套(本次复用的)----
|
||||
com.spineview.ObjectPanel wire (Ljava/lang/ClassLoader;Ljava/lang/Object;Ljava/lang/Runnable;)V wireButton
|
||||
# ^ 把按钮接上点击(轮询 isChecked + 路由线程)——新按钮直接复用它,不用自己写监听
|
||||
com.spineview.SceneObjects listAllNodes (Ljava/lang/ClassLoader;)Ljava/util/List; listAllNodes
|
||||
com.spineview.SceneActions delete (Ljava/lang/ClassLoader;Ljava/lang/Object;)Z delete
|
||||
# ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它
|
||||
|
||||
# ---- WidgetGroup 的尺寸读取(本次用来把对话框撑到合适大小)----
|
||||
eX LineNumberTable ()F getMinWidth
|
||||
eX String ()F getMinHeight
|
||||
eX StackMapTable ()F getPrefWidth
|
||||
eX init ()F getPrefHeight
|
||||
eX Ljava ()F getMaxWidth
|
||||
eX Class ()F getMaxHeight
|
||||
# ^ 六个 Layout 方法按 eX 声明顺序对应 minWidth/minHeight/prefWidth/prefHeight/maxWidth/maxHeight
|
||||
# (面板日志里一直用的 `init` 读出来就是 prefHeight;本次实测对话框 pref=399x166)
|
||||
# ^ ⚠️ 窗口刚建出来时的尺寸是默认的 150x150(所以正文/按钮会被裁),
|
||||
# 要显式 `Actor.width/height = max(pref, min, 下限)` + `eX.b_()V`(invalidate) 才会按 pref 撑开
|
||||
|
Can't render this file because it contains an unexpected character in line 41 and column 97.
|
+117
-11
@@ -1,39 +1,116 @@
|
||||
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
|
||||
# owner name desc newName
|
||||
GD D F position
|
||||
GD D LGU; target
|
||||
GD D Ljava/lang/String; name
|
||||
GD D Ljm; positionMode
|
||||
GD D Ljn; rotateMode
|
||||
GD D Ljo; spacingMode
|
||||
GD L F translateMix
|
||||
GD S F rotateMix
|
||||
GD V F spacing
|
||||
GD Z F rotation
|
||||
GE B F y
|
||||
GE B Lcom/badlogic/gdx/graphics/Color; color
|
||||
GE D F x
|
||||
GE L F rotation
|
||||
GO B F y
|
||||
GO B Ljava/lang/String; path
|
||||
GO C F width
|
||||
GO D (FFFFF)V setTransform
|
||||
GO D F x
|
||||
GO D Lcom/badlogic/gdx/graphics/Color; color
|
||||
GO I F scaleY
|
||||
GO L F scaleX
|
||||
GO S F rotation
|
||||
GO Z F height
|
||||
GU D LGn; attachment
|
||||
GP D (LGp;)V addBone
|
||||
GP L (LGn;ZZ)V removeAttachment
|
||||
GP L LfI attachments
|
||||
GP Z LfI slots
|
||||
GU B LGn; attachment
|
||||
GU B Lcom/badlogic/gdx/graphics/Color; color
|
||||
GU D (LGn;Z)V setAttachment
|
||||
GU D LGn; attachmentAnimate
|
||||
GU D LGp; bone
|
||||
GU D Lcom/badlogic/gdx/graphics/Color; color
|
||||
GU D Liz; blend
|
||||
GU D Ljava/lang/String; name
|
||||
Gp B F setupY
|
||||
GU I Lcom/badlogic/gdx/graphics/Color; darkColor
|
||||
GU I Z hasDarkColor
|
||||
GX B LGp; target
|
||||
GX C Z relative
|
||||
GX D F translateMix
|
||||
GX D Ljava/lang/String; name
|
||||
GX F F shearMix
|
||||
GX I F y
|
||||
GX L F x
|
||||
GX LineNumberTable F shearY
|
||||
GX S F rotateMix
|
||||
GX S Z local
|
||||
GX SourceFile F rotation
|
||||
GX StackMapTable F scaleX
|
||||
GX Z F scaleMix
|
||||
GX init F scaleY
|
||||
Gn D LHi; placeholderRef
|
||||
Gn D Ljava/lang/String; name
|
||||
Gn I ()Ljava/lang/String; type
|
||||
Gn L ()V delete
|
||||
Gp B Lcom/badlogic/gdx/graphics/Color; color
|
||||
Gp C F setupShear
|
||||
Gp Class F length
|
||||
Gp C F length
|
||||
Gp Code F scaleX
|
||||
Gp D (Ljava/lang/String;Z)V setName
|
||||
Gp D F setupX
|
||||
Gp D LGP; skeleton
|
||||
Gp D LGp; parent
|
||||
Gp D LiC; transform
|
||||
Gp D Ljava/lang/String; name
|
||||
Gp F F rotation
|
||||
Gp I F setupScaleX
|
||||
Gp L F setupRotation
|
||||
Gp LineNumberTable F shearY
|
||||
Gp Ljava F worldScaleX
|
||||
Gp S F setupScaleY
|
||||
Gp SourceFile F scaleY
|
||||
Gp StackMapTable F shearX
|
||||
Gp String Z skinOverride
|
||||
Gp V F y
|
||||
Gp Z F x
|
||||
Gp java F worldScaleY
|
||||
Gr B LHl; vertices
|
||||
Gr B Lcom/badlogic/gdx/graphics/Color; color
|
||||
Gr I Lga; vertexCount
|
||||
Gs B LGU; end
|
||||
Gs B LHl; vertices
|
||||
Gs B Lcom/badlogic/gdx/graphics/Color; color
|
||||
Gs I Lga; vertexCount
|
||||
Gw D F mix
|
||||
Gw D LGp; target
|
||||
Gw D Ljava/lang/String; name
|
||||
Gw F Z uniform
|
||||
Gw L F softness
|
||||
Gw S Z stretch
|
||||
Gw Z Z compress
|
||||
Gx B F height
|
||||
Gx B LHl; vertices
|
||||
Gx B Lcom/badlogic/gdx/graphics/Color; color
|
||||
Gx B Ljava/lang/String; path
|
||||
Gx D F width
|
||||
Gx D LGT; skin
|
||||
Gx D LGx; parentMesh
|
||||
Gx D LHi; parentPlaceholder
|
||||
Gx D Lga; uvs
|
||||
Gx D Lgf; edges
|
||||
Gx D Lhp; triangles
|
||||
Gx D Ljava/lang/String; parentName
|
||||
Gx L I hull
|
||||
Gx StackMapTable Z deform
|
||||
KP B ()Ljava/lang/String; getSelected
|
||||
KP B (I)V setSelectedIndex
|
||||
KP B (Ljava/lang/String;)V setSelected
|
||||
KP D (Ldt;)V setStage
|
||||
Kp B ()Ljava/lang/String; getText
|
||||
Kp B (Ljava/lang/String;)V setText
|
||||
OG D (Ljava/lang/String;LfI;LGU;)Ljava/lang/String; uniqueName
|
||||
UH D (LGU;LGp;Z;LfI;)LGU; addSlot
|
||||
UH D (LGn;LGU;ZZLfI;)LGn; addAttachmentCopy
|
||||
VZ D (LGP;ZZ)V refresh
|
||||
com.spineview.ObjectPanel wire (Ljava/lang/ClassLoader;Ljava/lang/Object;Ljava/lang/Runnable;)V wireButton
|
||||
com.spineview.SceneActions delete (Ljava/lang/ClassLoader;Ljava/lang/Object;)Z delete
|
||||
com.spineview.SceneObjects listAllNodes (Ljava/lang/ClassLoader;)Ljava/util/List; listAllNodes
|
||||
com/badlogic/gdx/scenes/scene2d/ui/Button B (ZZ)V setCheckedAndFire
|
||||
com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
|
||||
com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked
|
||||
@@ -121,8 +198,10 @@ dl B (I)F getY
|
||||
dl B (Lde;)Lde; stageToLocalCoordinates
|
||||
dl B (Ldl;Lde;)Lde; localToActorCoordinates
|
||||
dl B (Z)V setDebug
|
||||
dl C ()F getRight
|
||||
dl C (FF)V setOrigin
|
||||
dl D ()Ldl; debug
|
||||
dl D ()Ldt; getStage
|
||||
dl D ()Ljava/lang/Object; getUserObject
|
||||
dl D ()Ljava/lang/String; getName
|
||||
dl D ()V clearActions
|
||||
@@ -138,17 +217,24 @@ dl D (Ldn;)Z addListener
|
||||
dl D (Ljava/lang/Object;)V setUserObject
|
||||
dl D (Ljava/lang/String;)V setName
|
||||
dl D (Z)V setVisible
|
||||
dl D Ldo; parent
|
||||
dl D Ldt; stage
|
||||
dl F ()V clipEnd
|
||||
dl I ()F getHeight
|
||||
dl I ()Z isTouchable
|
||||
dl L ()F getWidth
|
||||
dl L ()V clear
|
||||
dl L ()Z hasParent
|
||||
dl L (Ldn;)Z addCaptureListener
|
||||
dl L (Ldv;)V setTouchable
|
||||
dl S ()F getTop
|
||||
dl S (FF)V setOrigin
|
||||
dl S (Lde;)Lde; localToStageCoordinates
|
||||
dl V ()V toBack
|
||||
dl Z ()V toFront
|
||||
dl b_ ()Z remove
|
||||
dl c_ ()F getX
|
||||
dl d_ ()F getY
|
||||
do B ()Ldo; debugAll
|
||||
do Code ()V childrenChanged
|
||||
do D (F)V act
|
||||
@@ -169,17 +255,24 @@ do LineNumberTable ()Z isTransform
|
||||
do init ()Z hasChildren
|
||||
dt B ()F getHeight
|
||||
dt D ()F getWidth
|
||||
dt D (Ldl;)V addActor
|
||||
eM C ()V expandTo
|
||||
eM D ()F getHeight
|
||||
eM D ()Z hasChildren
|
||||
eM D (Ljava/lang/Object;)LeM; findNode
|
||||
eM I ()V collapseAll
|
||||
eM S ()V expandAll
|
||||
eX Class ()F getMaxHeight
|
||||
eX D (LJ;F)V draw
|
||||
eX LineNumberTable ()F getMinWidth
|
||||
eX Ljava ()F getMaxWidth
|
||||
eX Object ()Z needsLayout
|
||||
eX StackMapTable ()F getPrefWidth
|
||||
eX StackMapTable ()V layout
|
||||
eX String ()F getMinHeight
|
||||
eX V (Z)V setFillParent
|
||||
eX b_ ()V invalidate
|
||||
eX init ()F getPrefHeight
|
||||
et B (Lca;)V drawDebugBounds
|
||||
et B (LeN;)Let; padTop
|
||||
et B (Z)V setDebug
|
||||
@@ -232,3 +325,16 @@ fI D ([Ljava/lang/Object;)LfI; with
|
||||
fI D ([Ljava/lang/Object;)V addAll
|
||||
fI L ()Ljava/lang/Object; first
|
||||
fI L ()V reverse
|
||||
iz B Liz; additive
|
||||
iz D Liz; normal
|
||||
iz I Liz; screen
|
||||
iz L Liz; multiply
|
||||
un B ()V undoCommit
|
||||
un D ()V undoBegin
|
||||
un Z ()V undo
|
||||
vH B (Ljava/lang/Object;)V okPressed
|
||||
vH L (Ljava/lang/Object;)V onOkCancelNoop
|
||||
xl D (Ldt;)LvH; show
|
||||
xl D LKp; nameField
|
||||
xl L (Ljava/lang/Object;)V onOkCancel
|
||||
xl L (Ljava/lang/String;)V onNameEntered
|
||||
|
@@ -0,0 +1,2 @@
|
||||
Agent-Class: BoneMaker
|
||||
Can-Retransform-Classes: false
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker2
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker3
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker4
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker5
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker6
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: BoneMaker7
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe1
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe2
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe3
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe4
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe5
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe6
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe7
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: GeomProbe8
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff1
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff2
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff3
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: JsonDiff4
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe3
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe4
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe5
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe6
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe7
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: Probe9
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: ResetMaker1
|
||||
@@ -0,0 +1,2 @@
|
||||
Manifest-Version: 1.0
|
||||
Agent-Class: ResetMaker2
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: SlotMaker1
|
||||
@@ -0,0 +1 @@
|
||||
Agent-Class: SlotMaker2
|
||||
@@ -0,0 +1,803 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
/**
|
||||
* 第三版(点按钮没反应的原因已查明并修掉):
|
||||
* 1) 弹窗:编辑器自己的 `xl`(标题/正文/名字框/OK/Cancel)× ASM 生成的子类;
|
||||
* ⚠️ 实测:只调 `xl.D(dt)` 不会把窗口挂进 Stage(parent 一直 null,屏幕上什么都不出现),
|
||||
* 必须**显式 Stage.addActor(dialog)**(`dt.D(Ldl;)V`,字节码就是 root.addActor),
|
||||
* 再自己摆位置。中文在编辑器字体里是乱码,所以文字一律用英文。
|
||||
* 2) 清掉上一版/实验残留的对话框(类名 com.spineview.live.AttNamePrompt 的那些)。
|
||||
* 3) 名字捕获改成**每个对话框实例自己的字段**(不再用共享 static,避免多个等待线程互相串台)。
|
||||
* 4) 建对象部分与第二版相同(已干跑验证):当前选中骨骼下 15 根骨骼 + 1 插槽 + 1 附件。
|
||||
*/
|
||||
public final class AddButton10 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
|
||||
static Class<?> ARRAY, BONE, SLOT, ATT, GO, HI, SKELETON_C, UH_C, COLOR_C, IZ_C, XL, STAGE_C,
|
||||
ACTOR_C, GROUP_C, CELL_C, DIALOG_C, eX;
|
||||
static Object UH_INSTANCE, EDITOR, TREE_VIEW, STAGE, IZ_NORMAL, IZ_ADDITIVE;
|
||||
static Method ADD_SLOT, ADD_ATT, ADD_BONE, NAME_GEN, REFRESH, REFRESH2, UNDO_BEGIN, UNDO_COMMIT,
|
||||
ON_RENDER, ADD_ACTOR, SHOW;
|
||||
static Field DIALOG_PARENT_FIELD, DIALOG_NAME_FIELD, DIALOG_DONE_FIELD,
|
||||
DIALOG_STAGE_FIELD;
|
||||
static final int COUNT = 15;
|
||||
|
||||
public static void agentmain(String args, Instrumentation inst) {
|
||||
INST = inst;
|
||||
try {
|
||||
run();
|
||||
} catch (Throwable error) {
|
||||
log("失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
|
||||
static void log(String m) { System.err.println("[btn10] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { L = c.getClassLoader(); break; }
|
||||
}
|
||||
if (L == null) { log("找不到载荷类加载器"); return; }
|
||||
ARRAY = forName("fI");
|
||||
BONE = forName("Gp");
|
||||
SLOT = forName("GU");
|
||||
ATT = forName("Gn");
|
||||
GO = forName("GO");
|
||||
HI = forName("Hi");
|
||||
SKELETON_C = forName("GP");
|
||||
UH_C = forName("UH");
|
||||
COLOR_C = forName("com.badlogic.gdx.graphics.Color");
|
||||
IZ_C = forName("iz");
|
||||
XL = forName("xl");
|
||||
STAGE_C = forName("dt");
|
||||
ACTOR_C = forName("dl");
|
||||
GROUP_C = forName("do");
|
||||
CELL_C = forName("dR");
|
||||
eX = forName("eX");
|
||||
Class<?> og = forName("OG");
|
||||
Class<?> undo = forName("un");
|
||||
Class<?> vz = forName("VZ");
|
||||
UH_INSTANCE = staticFieldOfType(og, UH_C);
|
||||
EDITOR = staticFieldOfType(og, forName("com.esotericsoftware.spine.editor.Editor"));
|
||||
TREE_VIEW = staticFieldOfType(og, vz);
|
||||
STAGE = staticFieldOfType(og, STAGE_C);
|
||||
ADD_SLOT = find(UH_C, "D", new Class<?>[] {SLOT, BONE, boolean.class, ARRAY}, SLOT);
|
||||
ADD_ATT = find(UH_C, "D", new Class<?>[] {ATT, SLOT, boolean.class, boolean.class, ARRAY}, ATT);
|
||||
ADD_BONE = find(SKELETON_C, "D", new Class<?>[] {BONE}, void.class);
|
||||
NAME_GEN = find(og, "D", new Class<?>[] {String.class, ARRAY, SLOT}, String.class);
|
||||
REFRESH = find(vz, "D", new Class<?>[] {SKELETON_C, boolean.class, boolean.class}, void.class);
|
||||
if (REFRESH == null && EDITOR != null) {
|
||||
REFRESH2 = find(vz, "D", new Class<?>[] {EDITOR.getClass(), SKELETON_C, boolean.class,
|
||||
boolean.class}, void.class);
|
||||
}
|
||||
for (Method m : undo.getDeclaredMethods()) {
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != void.class) continue;
|
||||
if (m.getName().equals("D")) UNDO_BEGIN = m;
|
||||
if (m.getName().equals("B")) UNDO_COMMIT = m;
|
||||
}
|
||||
for (Method m : og.getDeclaredMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("B") && t.length == 1 && t[0] == Runnable.class) ON_RENDER = m;
|
||||
}
|
||||
ON_RENDER.setAccessible(true);
|
||||
// ⚠️ 实测:`STAGE_C.getMethod("D", ACTOR_C)` 拿到的那个调用后 parent 永远是 null,
|
||||
// 而"扫描 stage.getClass().getMethods() 里 1 个 Actor 参数 + void"的这个是真能挂上的。
|
||||
for (Method m : STAGE.getClass().getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1) continue;
|
||||
if (!ACTOR_C.isAssignableFrom(t[0])) continue;
|
||||
if (m.getReturnType() != void.class) continue;
|
||||
ADD_ACTOR = m;
|
||||
ADD_ACTOR.setAccessible(true);
|
||||
}
|
||||
log("addActor = " + ADD_ACTOR);
|
||||
for (Method m : XL.getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length == 1 && t[0] == STAGE_C) SHOW = m;
|
||||
}
|
||||
IZ_NORMAL = IZ_C.getField("D").get(null);
|
||||
IZ_ADDITIVE = IZ_C.getField("B").get(null);
|
||||
DIALOG_C = defineDialogClass();
|
||||
DIALOG_NAME_FIELD = DIALOG_C.getField("NAME");
|
||||
DIALOG_DONE_FIELD = DIALOG_C.getField("DONE");
|
||||
DIALOG_PARENT_FIELD = parentFieldOf(ACTOR_C);
|
||||
DIALOG_STAGE_FIELD = stageFieldOf(ACTOR_C);
|
||||
if (ADD_SLOT == null || ADD_ATT == null || ADD_BONE == null || NAME_GEN == null
|
||||
|| UNDO_BEGIN == null || UNDO_COMMIT == null) {
|
||||
log("API 没找齐,停");
|
||||
return;
|
||||
}
|
||||
onRender(new Runnable() {
|
||||
@Override public void run() {
|
||||
cleanupOrphans();
|
||||
}
|
||||
});
|
||||
installButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清掉前几版留下的对话框(含还留在屏幕上的实验弹窗)。
|
||||
* 遍历方式照抄面板自己的 fillDialog:从 Stage 出发按**字段**广搜(按 actor 树走会漏,实测过)。
|
||||
*/
|
||||
static void cleanupOrphans() {
|
||||
try {
|
||||
Class<?> actorC = ACTOR_C;
|
||||
Method remove = null;
|
||||
for (Method m : actorC.getMethods()) {
|
||||
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
|
||||
&& m.getReturnType() == boolean.class) remove = m;
|
||||
}
|
||||
remove.setAccessible(true);
|
||||
Deque<Object> queue = new ArrayDeque<Object>();
|
||||
Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||
queue.add(STAGE);
|
||||
int visited = 0, removed = 0;
|
||||
while (!queue.isEmpty() && visited < 200000) {
|
||||
Object a = queue.poll();
|
||||
if (a == null || !seen.add(a)) continue;
|
||||
visited++;
|
||||
String cn = a.getClass().getName();
|
||||
if (cn.startsWith("com.spineview.live.")) {
|
||||
try { remove.invoke(a); removed++; } catch (Throwable ignored) { }
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (cn.startsWith("java.") || cn.startsWith("javax.") || cn.startsWith("sun.")) continue;
|
||||
if (a instanceof Class || a instanceof String || a instanceof Number) continue;
|
||||
if (a.getClass().isArray()) {
|
||||
if (!a.getClass().getComponentType().isPrimitive()) {
|
||||
Object[] arr = (Object[]) a;
|
||||
for (Object item : arr) if (item != null) queue.add(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (Class<?> c = a.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType().isPrimitive()) continue;
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(a);
|
||||
if (v != null) queue.add(v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
log("遍历 " + cn + " 出错: " + t);
|
||||
}
|
||||
}
|
||||
log("清理旧对话框 " + removed + " 个(遍历 " + visited + " 个对象)");
|
||||
} catch (Throwable e) {
|
||||
log("清理旧对话框失败: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删掉上一版跑出来那批骨骼。
|
||||
* ⚠️ 判据必须精确,上一版我把"插槽有两个 Gn 字段"当判据,结果误伤了你原有的骨骼(已撤销恢复)。
|
||||
* 这次的判据:**骨架附件表 GP.L 里,同一个插槽被登记了 >=2 个附件** —— 只有我上一版那批是这样
|
||||
* (ADD_SLOT 带了模板插槽的附件 + 我又加了一个),原有插槽每个只有 1 个。
|
||||
*/
|
||||
static void cleanupLastBatch() {
|
||||
try {
|
||||
java.util.List<?> all = (java.util.List<?>) Class.forName("com.spineview.SceneObjects", true, L)
|
||||
.getMethod("listAllNodes", ClassLoader.class).invoke(null, L);
|
||||
java.util.List<Object> bones = new ArrayList<Object>();
|
||||
for (Object o : all) if (o != null && BONE.isInstance(o)) bones.add(o);
|
||||
if (bones.isEmpty()) return;
|
||||
Object skeleton = firstFieldOfType(BONE, bones.get(0), SKELETON_C);
|
||||
if (skeleton == null) return;
|
||||
Object slotsArr = arrayOfType(SKELETON_C, skeleton, SLOT);
|
||||
Object attsArr = arrayOfType(SKELETON_C, skeleton, ATT);
|
||||
// 统计每个插槽登记了几个附件
|
||||
Map<Object, Integer> perSlot = new IdentityHashMap<Object, Integer>();
|
||||
for (int i = 0; i < size(attsArr); i++) {
|
||||
Object a = get(attsArr, i);
|
||||
if (a == null) continue;
|
||||
Object slot = firstFieldOfType(ATT, a, SLOT);
|
||||
if (slot == null) continue;
|
||||
Integer c = perSlot.get(slot);
|
||||
perSlot.put(slot, Integer.valueOf(c == null ? 1 : c.intValue() + 1));
|
||||
}
|
||||
Method del = Class.forName("com.spineview.SceneActions", true, L)
|
||||
.getMethod("delete", ClassLoader.class, Object.class);
|
||||
del.setAccessible(true);
|
||||
int killed = 0;
|
||||
Set<Object> killedSet = Collections.newSetFromMap(new IdentityHashMap<Object, Boolean>());
|
||||
for (Map.Entry<Object, Integer> e : perSlot.entrySet()) {
|
||||
if (e.getValue().intValue() < 2) continue; // 正常插槽只有 1 个附件
|
||||
Object bone = firstFieldOfType(SLOT, e.getKey(), BONE);
|
||||
if (bone == null || !killedSet.add(bone)) continue;
|
||||
del.invoke(null, L, bone);
|
||||
killed++;
|
||||
}
|
||||
log("清理上一版残留骨骼 " + killed + " 根(判据:同插槽登记了 2 个以上附件)");
|
||||
} catch (Throwable e) {
|
||||
log("清理上一版失败: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
static void installButton() throws Exception {
|
||||
Class<?> panel = Class.forName("com.spineview.ObjectPanel");
|
||||
Field lastF = panel.getDeclaredField("LAST_STATE");
|
||||
lastF.setAccessible(true);
|
||||
Object state = lastF.get(null);
|
||||
if (state == null) { log("面板还没建"); return; }
|
||||
Object buttons = stateField(state, "buttons");
|
||||
Object skin = stateField(state, "skin");
|
||||
// 前几版一共往按钮区塞了 3 个按钮:这里把"第 11 个 TextButton 之后"的全摘掉,只留原来的 10 个
|
||||
// ⚠️ Actor.remove() 是 `b_()Z`;`eX.b_()V` 是 invalidate()。按名字随便取会拿到 void 那个 → 删不掉。
|
||||
Method remove = null;
|
||||
for (Method m : ACTOR_C.getMethods()) {
|
||||
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
|
||||
&& m.getReturnType() == boolean.class) remove = m;
|
||||
}
|
||||
remove.setAccessible(true);
|
||||
Object kids = childrenOf(buttons);
|
||||
int removedOld = 0;
|
||||
for (int i = size(kids) - 1; i >= 0; i--) {
|
||||
Object child = get(kids, i);
|
||||
if (child == null || !child.getClass().getName().contains("TextButton")) continue;
|
||||
if (seenButtonsFromEnd(kids, i) > 10) {
|
||||
remove.invoke(child);
|
||||
removedOld++;
|
||||
}
|
||||
}
|
||||
log("移除前几版按钮 " + removedOld + " 个");
|
||||
Class<?> textButton = forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton");
|
||||
Class<?> skinClass = forName("com.badlogic.gdx.scenes.scene2d.ui.Skin");
|
||||
Object button = textButton.getConstructor(String.class, skinClass)
|
||||
.newInstance("New 15 Bone+Slot+Att", skin);
|
||||
Method add = null, row = null, left = null;
|
||||
for (Method m : buttons.getClass().getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (m.getName().equals("D") && t.length == 1 && t[0] == ACTOR_C
|
||||
&& m.getReturnType() == CELL_C) add = m;
|
||||
if (m.getName().equals("I") && t.length == 0 && m.getReturnType() == CELL_C) row = m;
|
||||
}
|
||||
for (Method m : CELL_C.getMethods()) {
|
||||
if (m.getName().equals("C") && m.getParameterTypes().length == 0
|
||||
&& m.getReturnType() == CELL_C) left = m;
|
||||
}
|
||||
if (add == null) { log("找不到 Table.add(Actor)Cell"); return; }
|
||||
add.setAccessible(true);
|
||||
Object cell = add.invoke(buttons, button);
|
||||
if (left != null) { left.setAccessible(true); left.invoke(cell); }
|
||||
if (row != null) { row.setAccessible(true); row.invoke(buttons); }
|
||||
for (Method m : buttons.getClass().getMethods()) {
|
||||
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
|
||||
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(buttons); break; }
|
||||
}
|
||||
Method wire = panel.getDeclaredMethod("wire", ClassLoader.class, Object.class, Runnable.class);
|
||||
wire.setAccessible(true);
|
||||
wire.invoke(null, L, button, new Runnable() {
|
||||
@Override public void run() {
|
||||
Thread t = new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
try { promptAndCreate(); } catch (Throwable e) {
|
||||
log("流程失败: " + e);
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
}, "spineview-prompt15-v3");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
});
|
||||
log("按钮已装好(第 " + size(childrenOf(buttons)) + " 行)并接上点击");
|
||||
Thread verify = new Thread(new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
Thread.sleep(300L);
|
||||
Class.forName("AddButton10").getDeclaredMethod("promptAndCreate").invoke(null);
|
||||
} catch (Throwable e) { log("验证弹窗失败: " + e); }
|
||||
}
|
||||
}, "spineview-prompt15-verify");
|
||||
verify.setDaemon(true);
|
||||
verify.start();
|
||||
log("已触发一次弹窗验证(居中效果;Cancel 不会有任何改动)");
|
||||
}
|
||||
|
||||
/** 从前往后数,这个下标是第几个 TextButton(1 起) */
|
||||
static int seenButtonsFromEnd(Object kids, int index) {
|
||||
int n = 0;
|
||||
for (int i = 0; i <= index; i++) {
|
||||
Object c = get(kids, i);
|
||||
if (c != null && c.getClass().getName().contains("TextButton")) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static Object stateField(Object state, String name) throws Exception {
|
||||
Field f = state.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(state);
|
||||
}
|
||||
|
||||
/** 弹窗 → 等名字 → 建对象 */
|
||||
static void promptAndCreate() throws Exception {
|
||||
final Object[] holder = new Object[1];
|
||||
onRender(new Runnable() {
|
||||
@Override public void run() {
|
||||
try {
|
||||
holder[0] = showPrompt();
|
||||
} catch (Throwable e) {
|
||||
log("弹面板失败: " + e);
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
Object dialog = holder[0];
|
||||
if (dialog == null) { log("弹窗没建出来 → 结束"); return; }
|
||||
log("弹窗已显示:parent=" + (parentOf(dialog) != null) + " 位置=" + geom(dialog)
|
||||
+ " Stage=" + fl(STAGE, "D") + "x" + fl(STAGE, "B")
|
||||
+ " 在 Stage 孩子里=" + (indexOf(childrenOf(rootGroup()), dialog) >= 0));
|
||||
String name = waitForName(dialog, 300000L);
|
||||
if (name == null || name.trim().isEmpty()) {
|
||||
log("没拿到名字(取消/超时)→ 结束");
|
||||
removeQuietly(dialog);
|
||||
return;
|
||||
}
|
||||
final String finalName = name.trim();
|
||||
log("输入的名字 = " + finalName);
|
||||
removeQuietly(dialog);
|
||||
onRender(new Runnable() {
|
||||
@Override public void run() {
|
||||
try { create15(finalName); } catch (Throwable e) {
|
||||
log("建对象失败: " + e);
|
||||
e.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Object showPrompt() throws Exception {
|
||||
Object dialog = DIALOG_C.getConstructor(String.class, String.class)
|
||||
.newInstance("New Bones + Slots + Attachment",
|
||||
"Enter an existing attachment name in this skeleton:");
|
||||
// ① 先挂进 Stage(实测这一步必须用扫描出来的那个 addActor)
|
||||
ADD_ACTOR.invoke(STAGE, dialog);
|
||||
// ② 再走编辑器自己的显示流程(它会把键盘焦点给输入框)
|
||||
try { SHOW.invoke(dialog, STAGE); } catch (Throwable e) { log("xl.D(dt) 失败: " + e); }
|
||||
if (parentOf(dialog) == null) ADD_ACTOR.invoke(STAGE, dialog);
|
||||
// ③ 尺寸:默认窗口太小,正文/按钮会被裁。用编辑器自己的 pref 尺寸(拿不到就给下限)
|
||||
float prefW = fl(dialog, "StackMapTable"); // WidgetGroup.getPrefWidth
|
||||
float prefH = fl(dialog, "init"); // WidgetGroup.getPrefHeight
|
||||
float minW = fl(dialog, "LineNumberTable"); // getMinWidth
|
||||
float minH = fl(dialog, "String"); // getMinHeight
|
||||
float w = Math.max(Math.max(prefW, minW), 360f);
|
||||
float h = Math.max(Math.max(prefH, minH), 170f);
|
||||
setFloatField(dialog, "L", w); // Actor.width (dl.L:F)
|
||||
setFloatField(dialog, "I", h); // Actor.height (dl.I:F)
|
||||
for (Method m : eX.getMethods()) { // invalidate() 让内容按新尺寸重新布局
|
||||
if (m.getName().equals("b_") && m.getParameterTypes().length == 0
|
||||
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(dialog); break; }
|
||||
}
|
||||
log("窗口尺寸: pref=" + prefW + "x" + prefH + " min=" + minW + "x" + minH
|
||||
+ " -> 设为 " + w + "x" + h);
|
||||
// ④ 立刻摆到正中心(同一次回调里做完,不会再从左上角/左下角闪一下)
|
||||
center(dialog);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/** 等用户按 OK / Cancel:OK 时 L(String) 已把名字写进 NAME;Cancel 时 NAME 仍是 null */
|
||||
static String waitForName(Object dialog, long timeoutMs) throws Exception {
|
||||
long end = System.currentTimeMillis() + timeoutMs;
|
||||
while (System.currentTimeMillis() < end) {
|
||||
Object done = DIALOG_DONE_FIELD.get(dialog);
|
||||
Object name = DIALOG_NAME_FIELD.get(dialog);
|
||||
if (name != null) return (String) name;
|
||||
if (Boolean.TRUE.equals(done)) return null; // 点了 Cancel
|
||||
if (stageField(dialog) == null) return null; // 窗口被关掉/移除
|
||||
Thread.sleep(120L);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 读对话框自己的 stage 字段(dl.D:Ldt; = Actor.stage;remove() 之后会变 null) */
|
||||
static Object stageField(Object actor) {
|
||||
if (DIALOG_STAGE_FIELD == null) return STAGE;
|
||||
try { return DIALOG_STAGE_FIELD.get(actor); } catch (Throwable e) { return STAGE; }
|
||||
}
|
||||
|
||||
/** 按窗口真实尺寸把对话框摆到正中央(用 Actor.setPosition(float,float,int) 方法,不猜字段名) */
|
||||
static void center(Object dialog) {
|
||||
try {
|
||||
float w = fl(dialog, "L"), h = fl(dialog, "I");
|
||||
if (w <= 0f || w > 3000f) w = 300f;
|
||||
if (h <= 0f || h > 3000f) h = 180f;
|
||||
// setPosition(x,y,align) 在 align=0 时是"以 (x,y) 为中心",所以直接给 Stage 中心
|
||||
float cx = fl(STAGE, "D") / 2f;
|
||||
float cy = fl(STAGE, "B") / 2f;
|
||||
Method setPos = null;
|
||||
for (Method m : ACTOR_C.getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length == 3 && t[0] == float.class && t[1] == float.class && t[2] == int.class) setPos = m;
|
||||
}
|
||||
if (setPos == null) {
|
||||
for (Method m : ACTOR_C.getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length == 2 && t[0] == float.class && t[1] == float.class
|
||||
&& m.getReturnType() == void.class) setPos = m;
|
||||
}
|
||||
}
|
||||
if (setPos == null) { log("找不到 setPosition"); return; }
|
||||
setPos.setAccessible(true);
|
||||
if (setPos.getParameterTypes().length == 3) {
|
||||
setPos.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy), Integer.valueOf(0));
|
||||
} else {
|
||||
setPos.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy));
|
||||
}
|
||||
log("已居中(" + setPos.getName() + "):窗口 " + w + "x" + h + " -> (" + cx + "," + cy + ")"
|
||||
+ " 实际位置=" + geom(dialog));
|
||||
} catch (Throwable e) {
|
||||
log("居中失败: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
static Object rootGroup() { return firstFieldOfType(STAGE_C, STAGE, GROUP_C); }
|
||||
|
||||
static void removeQuietly(Object dialog) {
|
||||
try {
|
||||
Method rm = dialog.getClass().getMethod("b_");
|
||||
rm.setAccessible(true);
|
||||
rm.invoke(dialog);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
|
||||
/** 生成 xl 子类:OK 时把名字写进**自己的** NAME 字段并关窗 */
|
||||
static Class<?> defineDialogClass() throws Exception {
|
||||
final String internal = "com/spineview/live/AttPrompt8";
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "xl", null);
|
||||
cw.visitField(Opcodes.ACC_PUBLIC, "NAME", "Ljava/lang/String;", null, null).visitEnd();
|
||||
cw.visitField(Opcodes.ACC_PUBLIC, "DONE", "Z", null, null).visitEnd();
|
||||
MethodVisitor c = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)V", null, null);
|
||||
c.visitCode();
|
||||
c.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
c.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
c.visitVarInsn(Opcodes.ALOAD, 2);
|
||||
c.visitInsn(Opcodes.ACONST_NULL);
|
||||
c.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;LGe;)V", false);
|
||||
c.visitInsn(Opcodes.RETURN);
|
||||
c.visitMaxs(0, 0);
|
||||
c.visitEnd();
|
||||
MethodVisitor h = cw.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/String;)V", null, null);
|
||||
h.visitCode();
|
||||
h.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
h.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
h.visitFieldInsn(Opcodes.PUTFIELD, internal, "NAME", "Ljava/lang/String;");
|
||||
h.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
h.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "dl", "b_", "()Z", false);
|
||||
h.visitInsn(Opcodes.POP);
|
||||
h.visitInsn(Opcodes.RETURN);
|
||||
h.visitMaxs(0, 0);
|
||||
h.visitEnd();
|
||||
// 覆盖 xl.L(Object)(OK 就是 Boolean.TRUE、Cancel 就是 FALSE):不管哪种,跑完就置 DONE
|
||||
MethodVisitor d = cw.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/Object;)V", null, null);
|
||||
d.visitCode();
|
||||
d.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
d.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
d.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "L", "(Ljava/lang/Object;)V", false);
|
||||
d.visitVarInsn(Opcodes.ALOAD, 0);
|
||||
d.visitInsn(Opcodes.ICONST_1);
|
||||
d.visitFieldInsn(Opcodes.PUTFIELD, internal, "DONE", "Z");
|
||||
d.visitInsn(Opcodes.RETURN);
|
||||
d.visitMaxs(0, 0);
|
||||
d.visitEnd();
|
||||
cw.visitEnd();
|
||||
final byte[] bytes = cw.toByteArray();
|
||||
ClassLoader child = new ClassLoader(L) {
|
||||
@Override protected Class<?> loadClass(String name, boolean resolve)
|
||||
throws ClassNotFoundException {
|
||||
if (name.equals("com.spineview.live.AttPrompt8")) {
|
||||
Class<?> found = findLoadedClass(name);
|
||||
if (found == null) found = defineClass(name, bytes, 0, bytes.length);
|
||||
if (resolve) resolveClass(found);
|
||||
return found;
|
||||
}
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
};
|
||||
return Class.forName("com.spineview.live.AttPrompt8", true, child);
|
||||
}
|
||||
|
||||
// ==================== 建 15 根骨骼 + 插槽 + 附件 ====================
|
||||
|
||||
static void create15(String name) throws Exception {
|
||||
Object selected = Class.forName("com.spineview.SceneObjects", true, L)
|
||||
.getMethod("selected", ClassLoader.class).invoke(null, L);
|
||||
if (selected == null) { log("当前没有选中对象 → 结束"); return; }
|
||||
if (!BONE.isInstance(selected)) {
|
||||
log("当前选中的不是骨骼(" + selected.getClass().getName() + ")→ 结束");
|
||||
return;
|
||||
}
|
||||
Object parentBone = selected;
|
||||
Object skeleton = firstFieldOfType(BONE, parentBone, SKELETON_C);
|
||||
Object bones = arrayOfType(SKELETON_C, skeleton, BONE);
|
||||
Object slots = arrayOfType(SKELETON_C, skeleton, SLOT);
|
||||
Object attachments = arrayOfType(SKELETON_C, skeleton, ATT);
|
||||
if (bones == null || slots == null || attachments == null) {
|
||||
log("骨架数组没找齐 → 结束");
|
||||
return;
|
||||
}
|
||||
Object templateSlot = size(slots) > 0 ? get(slots, 0) : null;
|
||||
Object template = null;
|
||||
for (int i = 0; i < size(attachments); i++) {
|
||||
Object a = get(attachments, i);
|
||||
if (a == null || !ATT.isInstance(a)) continue;
|
||||
if (!name.equals(nameOf(a))) continue;
|
||||
if (!HI.isInstance(a)) { template = a; break; }
|
||||
if (template == null) template = a;
|
||||
}
|
||||
if (template == null) {
|
||||
log("骨架里没有名为 \"" + name + "\" 的附件 → 结束");
|
||||
return;
|
||||
}
|
||||
log("模板附件 = " + nameOf(template) + " (" + template.getClass().getName()
|
||||
+ ") 模板插槽 = " + nameOf(templateSlot));
|
||||
Random random = new Random();
|
||||
Object empty = ARRAY.getConstructor().newInstance();
|
||||
UNDO_BEGIN.setAccessible(true);
|
||||
ADD_BONE.setAccessible(true);
|
||||
UNDO_BEGIN.invoke(null);
|
||||
Constructor<?> boneCtor = BONE.getConstructor(BONE, SKELETON_C, BONE);
|
||||
int made = 0, replaced = 0;
|
||||
for (int i = 0; i < COUNT; i++) {
|
||||
Object bone = boneCtor.newInstance(parentBone, skeleton, parentBone);
|
||||
set(bone, "D", String.class, NAME_GEN.invoke(null, "bone", bones, null));
|
||||
setFloatField(bone, "Z", random.nextFloat() * 500f); // x
|
||||
setFloatField(bone, "V", random.nextFloat() * 500f); // y
|
||||
setFloatField(bone, "F", 0f); // rotation
|
||||
setFloatField(bone, "Code", 1f); // scaleX
|
||||
setFloatField(bone, "SourceFile", 1f); // scaleY
|
||||
setFloatField(bone, "StackMapTable", 0f); // shearX
|
||||
setFloatField(bone, "LineNumberTable", 0f); // shearY
|
||||
ADD_BONE.invoke(skeleton, bone);
|
||||
Object out = ARRAY.getConstructor().newInstance();
|
||||
Object slot = ADD_SLOT.invoke(UH_INSTANCE, templateSlot, bone, Boolean.FALSE, out);
|
||||
if (slot == null) { log("第 " + i + " 个插槽没建出来"); continue; }
|
||||
// ⚠️ ADD_SLOT 会把"模板插槽"整个复制一份 —— 包括模板插槽自己挂着的那个附件
|
||||
// (实测:新插槽上挂的是 mega_dg4 的副本,而不是我们要的"输入名字"的附件)
|
||||
Object inherited = readSlotAttachment(slot);
|
||||
float alpha = 0.5f + random.nextFloat() * 0.5f;
|
||||
Object color = COLOR_C.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(1f, 1f, 1f, alpha);
|
||||
set(slot, "B", COLOR_C, color);
|
||||
set(slot, "D", COLOR_C, color);
|
||||
set(slot, "D", IZ_C, random.nextBoolean() ? IZ_NORMAL : IZ_ADDITIVE);
|
||||
Object att = ADD_ATT.invoke(UH_INSTANCE, template, slot, Boolean.FALSE, Boolean.FALSE, empty);
|
||||
if (att == null) { log("第 " + i + " 个附件没建出来"); continue; }
|
||||
setFloatField(att, "D", 0f);
|
||||
setFloatField(att, "B", 0f);
|
||||
setFloatField(att, "S", 0f);
|
||||
setFloatField(att, "L", 1f);
|
||||
setFloatField(att, "I", 1f);
|
||||
String before = nameOf(att);
|
||||
if (!name.equals(before)) {
|
||||
set(att, "D", String.class, name);
|
||||
log(" 第 " + i + " 个附件名被编辑器改成过 \"" + before + "\",已改回");
|
||||
}
|
||||
// 把插槽的当前附件换成**我们这份**(两个字段都写:setup=B / animate=D)
|
||||
set(slot, "B", ATT, att);
|
||||
set(slot, "D", ATT, att);
|
||||
// 删掉模板插槽带来的那份(保证"一根骨骼 = 1 插槽 + 1 附件")
|
||||
if (inherited != null && inherited != att) {
|
||||
deleteAttachment(inherited);
|
||||
replaced++;
|
||||
}
|
||||
made++;
|
||||
}
|
||||
UNDO_COMMIT.setAccessible(true);
|
||||
UNDO_COMMIT.invoke(null);
|
||||
if (REFRESH != null) {
|
||||
REFRESH.setAccessible(true);
|
||||
REFRESH.invoke(TREE_VIEW, skeleton, Boolean.TRUE, Boolean.TRUE);
|
||||
} else if (REFRESH2 != null) {
|
||||
REFRESH2.setAccessible(true);
|
||||
REFRESH2.invoke(TREE_VIEW, EDITOR, skeleton, Boolean.TRUE, Boolean.TRUE);
|
||||
}
|
||||
log("完成:在 " + nameOf(parentBone) + " 下建了 " + made + " 根骨骼;每根 1 插槽 1 附件 \"" + name
|
||||
+ "\"(替换掉模板附件 " + replaced + " 个)");
|
||||
}
|
||||
|
||||
// ==================== 工具 ====================
|
||||
|
||||
/** 读插槽当前挂的附件(GU 上两个 Gn 字段随便哪个都指向同一份) */
|
||||
static Object readSlotAttachment(Object slot) {
|
||||
for (Class<?> c = slot.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != ATT) continue;
|
||||
f.setAccessible(true);
|
||||
try { Object v = f.get(slot); if (v != null) return v; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Gn.L()V = 编辑器自己的"删掉这个附件"(字节码:skeleton.removeAttachment(this,true,true)) */
|
||||
static void deleteAttachment(Object att) {
|
||||
try {
|
||||
Method del = find(ATT, "L", new Class<?>[0], void.class);
|
||||
del.setAccessible(true);
|
||||
del.invoke(att);
|
||||
} catch (Throwable e) {
|
||||
log("删除模板附件失败: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
static void onRender(Runnable body) throws Exception { ON_RENDER.invoke(null, body); }
|
||||
|
||||
static Class<?> forName(String n) throws ClassNotFoundException { return Class.forName(n, true, L); }
|
||||
|
||||
static String nameOf(Object o) {
|
||||
try {
|
||||
return (String) Class.forName("com.spineview.SceneObjects", true, L)
|
||||
.getMethod("nameOf", Object.class).invoke(null, o);
|
||||
} catch (Throwable t) { return "?"; }
|
||||
}
|
||||
|
||||
static Field parentFieldOf(Class<?> actorClass) {
|
||||
for (Field f : actorClass.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() == GROUP_C) { f.setAccessible(true); return f; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field stageFieldOf(Class<?> actorClass) {
|
||||
for (Field f : actorClass.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() == STAGE_C) { f.setAccessible(true); return f; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parentOf(Object actor) {
|
||||
try { return DIALOG_PARENT_FIELD == null ? null : DIALOG_PARENT_FIELD.get(actor); }
|
||||
catch (Throwable e) { return null; }
|
||||
}
|
||||
|
||||
static Object staticFieldOfType(Class<?> owner, Class<?> type) {
|
||||
for (Field f : owner.getDeclaredFields()) {
|
||||
if (!Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != type) continue;
|
||||
f.setAccessible(true);
|
||||
try { Object v = f.get(null); if (v != null) return v; } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object firstFieldOfType(Class<?> owner, Object target, Class<?> type) {
|
||||
if (target == null) return null;
|
||||
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
|
||||
f.setAccessible(true);
|
||||
try { Object v = f.get(target); if (v != null) return v; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrayOfType(Class<?> owner, Object target, Class<?> elem) {
|
||||
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (!ARRAY.isAssignableFrom(f.getType())) continue;
|
||||
f.setAccessible(true);
|
||||
Object v;
|
||||
try { v = f.get(target); } catch (Throwable ignored) { continue; }
|
||||
if (v == null) continue;
|
||||
Class<?> et = elemClass(v);
|
||||
if (et != null && elem.isAssignableFrom(et)) return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Class<?> elemClass(Object array) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null) return o.getClass();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object group) {
|
||||
if (group == null) return null;
|
||||
for (Field f : GROUP_C.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (!ARRAY.isAssignableFrom(f.getType())) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(group); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int indexOf(Object array, Object target) {
|
||||
for (int i = 0; i < size(array); i++) if (get(array, i) == target) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void set(Object target, String name, Class<?> type, Object value) throws Exception {
|
||||
for (Class<?> c = target.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (!f.getName().equals(name) || f.getType() != type) continue;
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void setFloatField(Object target, String name, float value) {
|
||||
try {
|
||||
set(target, name, float.class, Float.valueOf(value));
|
||||
} catch (Throwable ignored) {
|
||||
// 该类型没这个字段就跳过
|
||||
}
|
||||
}
|
||||
|
||||
static Method find(Class<?> owner, String name, Class<?>[] params, Class<?> ret) {
|
||||
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Method m : c.getDeclaredMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (!Arrays.equals(m.getParameterTypes(), params)) continue;
|
||||
if (ret != null && m.getReturnType() != ret) continue;
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); }
|
||||
catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> c = ARRAY; c != null && c != Object.class; c = c.getSuperclass()) {
|
||||
for (Field f : c.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); }
|
||||
catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
return "[x=" + fl(actor, "c_") + " y=" + fl(actor, "d_")
|
||||
+ " w=" + fl(actor, "L") + " h=" + fl(actor, "I") + "]";
|
||||
}
|
||||
}
|
||||
@@ -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,257 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来,
|
||||
* 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。
|
||||
*
|
||||
* 依据(都不是猜的):
|
||||
* - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY,
|
||||
* scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float);
|
||||
* - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height;
|
||||
* - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、
|
||||
* `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。
|
||||
*/
|
||||
public final class GeomProbe1 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Object STATE;
|
||||
static Class<?> OG;
|
||||
|
||||
static final String[] FLOAT_NAMES = {
|
||||
"x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation"
|
||||
};
|
||||
|
||||
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("[geom] " + 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<?> panel = Class.forName("com.spineview.ObjectPanel", true, L);
|
||||
Field last = panel.getDeclaredField("LAST_STATE");
|
||||
last.setAccessible(true);
|
||||
STATE = last.get(null);
|
||||
if (STATE == null) { log("LAST_STATE 还是 null(面板没建)"); return; }
|
||||
OG = Class.forName("OG", 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);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object buttons = state("buttons");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
Object status = state("status");
|
||||
log("==================== 面板控件几何 ====================");
|
||||
dump("root ", root);
|
||||
dump("buttons ", buttons);
|
||||
dump("listScroll ", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", status);
|
||||
log("---- root 的父链(往上看 4 层,看谁给了多少高度)----");
|
||||
Object p = parent(root);
|
||||
for (int i = 0; i < 4 && p != null; i++) {
|
||||
dump("parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- listScroll 的父链 ----");
|
||||
p = parent(scroll);
|
||||
for (int i = 0; i < 3 && p != null; i++) {
|
||||
dump("scroll.parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----");
|
||||
dumpScrollPaneFields(scroll);
|
||||
log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----");
|
||||
Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D");
|
||||
int n = size(children);
|
||||
log(" 子控件数 = " + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 2 && i < n - 2) continue;
|
||||
Object child = get(children, i);
|
||||
dump(" child[" + i + "] ", child);
|
||||
}
|
||||
log("---- 关键判定 ----");
|
||||
Object widget = findWidget(scroll);
|
||||
log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable)
|
||||
+ " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")");
|
||||
log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3)
|
||||
+ " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3));
|
||||
Object stage = callActor(root, "D"); // Actor.getStage()
|
||||
if (stage != null) {
|
||||
log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B"));
|
||||
}
|
||||
log("=====================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(label).append(' ').append(actor.getClass().getName());
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i;
|
||||
sb.append(' ').append(name).append('=').append(round(f.getFloat(actor)));
|
||||
i++;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (t.getSuperclass() == null) break;
|
||||
}
|
||||
sb.append(" |getHeight()=").append(round(floatOf(actor, "init")));
|
||||
Object parent = parent(actor);
|
||||
sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName());
|
||||
log(sb.toString());
|
||||
}
|
||||
|
||||
static void dumpScrollPaneFields(Object pane) {
|
||||
java.util.TreeSet<String> names = new java.util.TreeSet<String>();
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
Class<?> type = f.getType();
|
||||
if (type != float.class && type != int.class && type != boolean.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(pane);
|
||||
if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue()));
|
||||
names.add(t.getSimpleName() + "." + f.getName() + "=" + v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : names) sb.append(' ').append(s);
|
||||
log(" " + sb);
|
||||
}
|
||||
|
||||
static Object findWidget(Object pane) {
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType().getName().equals("dl")) {
|
||||
f.setAccessible(true);
|
||||
try { return f.get(pane); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object v = m.invoke(actor);
|
||||
if (v != null) return v;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object callActor(Object actor, String name) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(actor);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object call(Class<?> owner, Object target, String name) {
|
||||
try {
|
||||
for (Method m : owner.getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, int index) {
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
if (i == index) {
|
||||
f.setAccessible(true);
|
||||
try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method m : target.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
m.setAccessible(true);
|
||||
return ((Float) m.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != Object[].class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float round(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来,
|
||||
* 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。
|
||||
*
|
||||
* 依据(都不是猜的):
|
||||
* - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY,
|
||||
* scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float);
|
||||
* - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height;
|
||||
* - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、
|
||||
* `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。
|
||||
*/
|
||||
public final class GeomProbe2 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Object STATE;
|
||||
static Class<?> OG;
|
||||
|
||||
static final String[] FLOAT_NAMES = {
|
||||
"x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation"
|
||||
};
|
||||
|
||||
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("[geom2] " + 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<?> panel = null;
|
||||
int copies = 0;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
copies++;
|
||||
if (panel == null) panel = c;
|
||||
}
|
||||
log("已加载的 ObjectPanel 份数 = " + copies + " 用 " + panel
|
||||
+ " (loader=" + (panel == null ? "-" : panel.getClassLoader()) + ")");
|
||||
if (panel == null) { log("ObjectPanel 没加载"); return; }
|
||||
Field last = panel.getDeclaredField("LAST_STATE");
|
||||
last.setAccessible(true);
|
||||
STATE = last.get(null);
|
||||
log("LAST_STATE = " + STATE);
|
||||
if (STATE == null) { return; }
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getName().equals("OG")) { OG = c; break; }
|
||||
}
|
||||
if (OG == null) { log("OG 没加载"); 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); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object buttons = state("buttons");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
Object status = state("status");
|
||||
log("==================== 面板控件几何 ====================");
|
||||
dump("root ", root);
|
||||
dump("buttons ", buttons);
|
||||
dump("listScroll ", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", status);
|
||||
log("---- root 的父链(往上看 4 层,看谁给了多少高度)----");
|
||||
Object p = parent(root);
|
||||
for (int i = 0; i < 4 && p != null; i++) {
|
||||
dump("parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- listScroll 的父链 ----");
|
||||
p = parent(scroll);
|
||||
for (int i = 0; i < 3 && p != null; i++) {
|
||||
dump("scroll.parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----");
|
||||
dumpScrollPaneFields(scroll);
|
||||
log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----");
|
||||
Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D");
|
||||
int n = size(children);
|
||||
log(" 子控件数 = " + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 2 && i < n - 2) continue;
|
||||
Object child = get(children, i);
|
||||
dump(" child[" + i + "] ", child);
|
||||
}
|
||||
log("---- 关键判定 ----");
|
||||
Object widget = findWidget(scroll);
|
||||
log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable)
|
||||
+ " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")");
|
||||
log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3)
|
||||
+ " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3));
|
||||
Object stage = callActor(root, "D"); // Actor.getStage()
|
||||
if (stage != null) {
|
||||
log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B"));
|
||||
}
|
||||
log("=====================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(label).append(' ').append(actor.getClass().getName());
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i;
|
||||
sb.append(' ').append(name).append('=').append(round(f.getFloat(actor)));
|
||||
i++;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (t.getSuperclass() == null) break;
|
||||
}
|
||||
sb.append(" |getHeight()=").append(round(floatOf(actor, "init")));
|
||||
Object parent = parent(actor);
|
||||
sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName());
|
||||
log(sb.toString());
|
||||
}
|
||||
|
||||
static void dumpScrollPaneFields(Object pane) {
|
||||
java.util.TreeSet<String> names = new java.util.TreeSet<String>();
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
Class<?> type = f.getType();
|
||||
if (type != float.class && type != int.class && type != boolean.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(pane);
|
||||
if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue()));
|
||||
names.add(t.getSimpleName() + "." + f.getName() + "=" + v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : names) sb.append(' ').append(s);
|
||||
log(" " + sb);
|
||||
}
|
||||
|
||||
static Object findWidget(Object pane) {
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType().getName().equals("dl")) {
|
||||
f.setAccessible(true);
|
||||
try { return f.get(pane); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object v = m.invoke(actor);
|
||||
if (v != null) return v;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object callActor(Object actor, String name) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(actor);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object call(Class<?> owner, Object target, String name) {
|
||||
try {
|
||||
for (Method m : owner.getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, int index) {
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
if (i == index) {
|
||||
f.setAccessible(true);
|
||||
try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method m : target.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
m.setAccessible(true);
|
||||
return ((Float) m.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != Object[].class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float round(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针:把 Spine View 面板里每个控件的**真实坐标/尺寸**和父子关系打出来,
|
||||
* 用来查"列表里的内容跑到面板外面"到底是谁的坐标不对。不改任何状态。
|
||||
*
|
||||
* 依据(都不是猜的):
|
||||
* - Actor 的 9 个 float 字段按 libGDX 声明顺序 = x, y, width, height, originX, originY,
|
||||
* scaleX, scaleY, rotation(`javap -p dl` 里就是这一串 float);
|
||||
* - `dl.init()F` = getHeight(面板日志里一直在用),用来交叉验证哪个字段是 height;
|
||||
* - `eX.V(Z)V` = WidgetGroup.setFillParent、`eX.StackMapTable()V` = layout、
|
||||
* `et.C(Z)V` = Table.setClip、`do.D()Lhq;` = Group.getChildren(取自字节码反查)。
|
||||
*/
|
||||
public final class GeomProbe3 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Object STATE;
|
||||
static Class<?> OG;
|
||||
|
||||
static final String[] FLOAT_NAMES = {
|
||||
"x", "y", "width", "height", "originX", "originY", "scaleX", "scaleY", "rotation"
|
||||
};
|
||||
|
||||
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("[geom3] " + 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<?> panel = null;
|
||||
int copies = 0;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
copies++;
|
||||
if (panel == null) panel = c;
|
||||
}
|
||||
log("已加载的 ObjectPanel 份数 = " + copies + " 用 " + panel
|
||||
+ " (loader=" + (panel == null ? "-" : panel.getClassLoader()) + ")");
|
||||
if (panel == null) { log("ObjectPanel 没加载"); return; }
|
||||
// 可能有两份拷贝(各自的静态字段互不相干)——逐个看谁的 LAST_STATE 不是 null
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
log(" 拷贝 " + c + " loader=" + c.getClassLoader() + " LAST_STATE=" + v);
|
||||
if (v != null && STATE == null) { STATE = v; panel = c; }
|
||||
} catch (Throwable e) { log(" 读 " + c + " 的 LAST_STATE 失败: " + e); }
|
||||
}
|
||||
if (panel != null) L = panel.getClassLoader() != null ? panel.getClassLoader() : L;
|
||||
log("选中的 STATE = " + STATE);
|
||||
if (STATE == null) { return; }
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getName().equals("OG")) { OG = c; break; }
|
||||
}
|
||||
if (OG == null) { log("OG 没加载"); 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); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object buttons = state("buttons");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
Object status = state("status");
|
||||
log("==================== 面板控件几何 ====================");
|
||||
dump("root ", root);
|
||||
dump("buttons ", buttons);
|
||||
dump("listScroll ", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", status);
|
||||
log("---- root 的父链(往上看 4 层,看谁给了多少高度)----");
|
||||
Object p = parent(root);
|
||||
for (int i = 0; i < 4 && p != null; i++) {
|
||||
dump("parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- listScroll 的父链 ----");
|
||||
p = parent(scroll);
|
||||
for (int i = 0; i < 3 && p != null; i++) {
|
||||
dump("scroll.parent[" + i + "] ", p);
|
||||
p = parent(p);
|
||||
}
|
||||
log("---- ScrollPane 内部状态(widget 是谁、内容多高、滚到哪)----");
|
||||
dumpScrollPaneFields(scroll);
|
||||
log("---- listTable 里挂了多少行、第一行/最后一行在哪 ----");
|
||||
Object children = call(scroll.getClass().getClassLoader().loadClass("do"), listTable, "D");
|
||||
int n = size(children);
|
||||
log(" 子控件数 = " + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 2 && i < n - 2) continue;
|
||||
Object child = get(children, i);
|
||||
dump(" child[" + i + "] ", child);
|
||||
}
|
||||
log("---- 关键判定 ----");
|
||||
Object widget = findWidget(scroll);
|
||||
log(" ScrollPane 的 widget 是不是 listTable ? " + (widget == listTable)
|
||||
+ " (widget=" + (widget == null ? "null" : widget.getClass().getName()) + ")");
|
||||
log(" 两者尺寸: scroll=" + fl(scroll, 2) + "x" + fl(scroll, 3)
|
||||
+ " listTable=" + fl(listTable, 2) + "x" + fl(listTable, 3));
|
||||
Object stage = callActor(root, "D"); // Actor.getStage()
|
||||
if (stage != null) {
|
||||
log(" stage 尺寸: " + floatOf(stage, "D") + " x " + floatOf(stage, "B"));
|
||||
}
|
||||
log("=====================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(label).append(' ').append(actor.getClass().getName());
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
String name = i < FLOAT_NAMES.length ? FLOAT_NAMES[i] : "float" + i;
|
||||
sb.append(' ').append(name).append('=').append(round(f.getFloat(actor)));
|
||||
i++;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (t.getSuperclass() == null) break;
|
||||
}
|
||||
sb.append(" |getHeight()=").append(round(floatOf(actor, "init")));
|
||||
Object parent = parent(actor);
|
||||
sb.append(" |parent=").append(parent == null ? "null" : parent.getClass().getName());
|
||||
log(sb.toString());
|
||||
}
|
||||
|
||||
static void dumpScrollPaneFields(Object pane) {
|
||||
java.util.TreeSet<String> names = new java.util.TreeSet<String>();
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
Class<?> type = f.getType();
|
||||
if (type != float.class && type != int.class && type != boolean.class) continue;
|
||||
f.setAccessible(true);
|
||||
try {
|
||||
Object v = f.get(pane);
|
||||
if (v instanceof Float) v = Float.valueOf(round(((Float) v).floatValue()));
|
||||
names.add(t.getSimpleName() + "." + f.getName() + "=" + v);
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String s : names) sb.append(' ').append(s);
|
||||
log(" " + sb);
|
||||
}
|
||||
|
||||
static Object findWidget(Object pane) {
|
||||
for (Class<?> t = pane.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType().getName().equals("dl")) {
|
||||
f.setAccessible(true);
|
||||
try { return f.get(pane); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType().getName().equals("do") && m.getName().length() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object v = m.invoke(actor);
|
||||
if (v != null) return v;
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object callActor(Object actor, String name) {
|
||||
try {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(actor);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object call(Class<?> owner, Object target, String name) {
|
||||
try {
|
||||
for (Method m : owner.getMethods()) {
|
||||
if (!m.getName().equals(name) || m.getParameterTypes().length != 0) continue;
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, int index) {
|
||||
int i = 0;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != float.class) continue;
|
||||
if (i == index) {
|
||||
f.setAccessible(true);
|
||||
try { return f.getFloat(actor); } catch (Throwable ignored) { return -1f; }
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float floatOf(Object target, String name) {
|
||||
try {
|
||||
for (Method m : target.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
m.setAccessible(true);
|
||||
return ((Float) m.invoke(target)).floatValue();
|
||||
}
|
||||
} catch (Throwable ignored) { }
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != Object[].class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return ((Object[]) f.get(array))[index]; } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float round(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 只读探针 4:找"列表里的内容画到面板外面"是谁干的。
|
||||
*
|
||||
* 取值方法都来自 `javap -p -c dl`(Actor)的字节码,不是猜名字:
|
||||
* c_()F = getX (return 字段 D)
|
||||
* d_()F = getY (return 字段 B)
|
||||
* L()F = getWidth (return 字段 L)
|
||||
* I()F = getHeight (return 字段 I)
|
||||
* S()F = getTop (字段 B + 字段 I)
|
||||
* C()F = getRight (字段 D + 字段 L)
|
||||
* D()Ldo; = getParent;do.D()Lhq; = getChildren
|
||||
*/
|
||||
public final class GeomProbe4 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static Class<?> HQL;
|
||||
|
||||
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("[geom4] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
Class<?> panel = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) { STATE = v; panel = c; }
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("没找到活的 ObjectPanel.LAST_STATE"); return; }
|
||||
log("拿到面板 State: " + STATE + " (来自 " + panel + ")");
|
||||
HQL = Class.forName("hq", true, panel.getClassLoader());
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
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; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
log("========== 面板内三个关键控件(stage 坐标由父链累加得到)==========");
|
||||
dump("root ", root);
|
||||
dump("buttons ", state("buttons"));
|
||||
dump("listScroll", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", state("status"));
|
||||
log("========== listTable 有几行 / 每行落在哪 ==========");
|
||||
Object kids = childrenOf(listTable);
|
||||
int n = size(kids);
|
||||
log("行数 = " + n + " listTable 自身 " + geom(listTable));
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 3 && i < n - 3) continue;
|
||||
Object child = get(kids, i);
|
||||
log(" row[" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 可见=" + visible(child));
|
||||
}
|
||||
log("========== 从面板根往上爬,看每层给的高度 ==========");
|
||||
Object p = parent(root);
|
||||
int depth = 0;
|
||||
while (p != null && depth < 8) {
|
||||
log(" 上一级[" + depth + "] " + p.getClass().getName() + " " + geom(p));
|
||||
Object np = parent(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
depth++;
|
||||
}
|
||||
Object stageRoot = p;
|
||||
log("========== stage 根下挂了哪些顶格控件(这些都在窗口坐标里)==========");
|
||||
if (stageRoot != null) {
|
||||
log("stage 根 = " + stageRoot.getClass().getName() + " " + geom(stageRoot));
|
||||
Object top = childrenOf(stageRoot);
|
||||
int m = size(top);
|
||||
log("顶格控件数 = " + m);
|
||||
for (int i = 0; i < m; i++) {
|
||||
Object child = get(top, i);
|
||||
log(" [" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 子控件=" + size(childrenOf(child)) + " 可见=" + visible(child));
|
||||
// 名单里第 2 层也打一下,便于找到"画在左下角"的那一支
|
||||
Object grand = childrenOf(child);
|
||||
int gn = size(grand);
|
||||
for (int j = 0; j < gn && j < 12; j++) {
|
||||
Object g = get(grand, j);
|
||||
log(" └─ " + g.getClass().getName() + " " + geom(g)
|
||||
+ " 子控件=" + size(childrenOf(g)) + " 可见=" + visible(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I"))
|
||||
+ " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]";
|
||||
}
|
||||
|
||||
static String visible(Object actor) {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != boolean.class) continue;
|
||||
if (!m.getName().equals("V")) continue;
|
||||
try { m.setAccessible(true); return String.valueOf(m.invoke(actor)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
log(label + " " + actor.getClass().getName() + " " + geom(actor)
|
||||
+ " 子控件=" + size(childrenOf(actor)));
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (!m.getName().equals("D")) continue;
|
||||
if (!m.getReturnType().getName().equals("do")) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 只读探针 4:找"列表里的内容画到面板外面"是谁干的。
|
||||
*
|
||||
* 取值方法都来自 `javap -p -c dl`(Actor)的字节码,不是猜名字:
|
||||
* c_()F = getX (return 字段 D)
|
||||
* d_()F = getY (return 字段 B)
|
||||
* L()F = getWidth (return 字段 L)
|
||||
* I()F = getHeight (return 字段 I)
|
||||
* S()F = getTop (字段 B + 字段 I)
|
||||
* C()F = getRight (字段 D + 字段 L)
|
||||
* D()Ldo; = getParent;do.D()Lhq; = getChildren
|
||||
*/
|
||||
public final class GeomProbe5 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static Class<?> HQL;
|
||||
|
||||
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("[geom5] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
Class<?> panel = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) { STATE = v; panel = c; }
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("没找到活的 ObjectPanel.LAST_STATE"); return; }
|
||||
log("拿到面板 State: " + STATE + " (来自 " + panel + ")");
|
||||
// hq(快照数组)在载荷类加载器里,面板类本身在 AppClassLoader —— 不能混用
|
||||
ClassLoader payload = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { payload = c.getClassLoader(); break; }
|
||||
}
|
||||
log("载荷类加载器 = " + payload);
|
||||
HQL = Class.forName("hq", true, payload);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
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; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object scroll = state("listScroll");
|
||||
Object listTable = state("listTable");
|
||||
log("========== 面板内三个关键控件(stage 坐标由父链累加得到)==========");
|
||||
dump("root ", root);
|
||||
dump("buttons ", state("buttons"));
|
||||
dump("listScroll", scroll);
|
||||
dump("listTable ", listTable);
|
||||
dump("logLabel ", state("status"));
|
||||
log("========== listTable 有几行 / 每行落在哪 ==========");
|
||||
Object kids = childrenOf(listTable);
|
||||
int n = size(kids);
|
||||
log("行数 = " + n + " listTable 自身 " + geom(listTable));
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 3 && i < n - 3) continue;
|
||||
Object child = get(kids, i);
|
||||
log(" row[" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 可见=" + visible(child));
|
||||
}
|
||||
log("========== 从面板根往上爬,看每层给的高度 ==========");
|
||||
Object p = parent(root);
|
||||
int depth = 0;
|
||||
while (p != null && depth < 8) {
|
||||
log(" 上一级[" + depth + "] " + p.getClass().getName() + " " + geom(p));
|
||||
Object np = parent(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
depth++;
|
||||
}
|
||||
Object stageRoot = p;
|
||||
log("========== stage 根下挂了哪些顶格控件(这些都在窗口坐标里)==========");
|
||||
if (stageRoot != null) {
|
||||
log("stage 根 = " + stageRoot.getClass().getName() + " " + geom(stageRoot));
|
||||
Object top = childrenOf(stageRoot);
|
||||
int m = size(top);
|
||||
log("顶格控件数 = " + m);
|
||||
for (int i = 0; i < m; i++) {
|
||||
Object child = get(top, i);
|
||||
log(" [" + i + "] " + child.getClass().getName() + " " + geom(child)
|
||||
+ " 子控件=" + size(childrenOf(child)) + " 可见=" + visible(child));
|
||||
// 名单里第 2 层也打一下,便于找到"画在左下角"的那一支
|
||||
Object grand = childrenOf(child);
|
||||
int gn = size(grand);
|
||||
for (int j = 0; j < gn && j < 12; j++) {
|
||||
Object g = get(grand, j);
|
||||
log(" └─ " + g.getClass().getName() + " " + geom(g)
|
||||
+ " 子控件=" + size(childrenOf(g)) + " 可见=" + visible(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I"))
|
||||
+ " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]";
|
||||
}
|
||||
|
||||
static String visible(Object actor) {
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != boolean.class) continue;
|
||||
if (!m.getName().equals("V")) continue;
|
||||
try { m.setAccessible(true); return String.valueOf(m.invoke(actor)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static void dump(String label, Object actor) {
|
||||
if (actor == null) { log(label + " = null"); return; }
|
||||
log(label + " " + actor.getClass().getName() + " " + geom(actor)
|
||||
+ " 子控件=" + size(childrenOf(actor)));
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (!m.getName().equals("D")) continue;
|
||||
if (!m.getReturnType().getName().equals("do")) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = array.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
|
||||
/**
|
||||
* 只读探针 6:把面板的**整棵控件树**连坐标打出来(含 stage 坐标),
|
||||
* 用来定位"列表里的内容被画到面板外"。
|
||||
*
|
||||
* 取值方法来自字节码(javap -p -c):
|
||||
* dl.c_()F=getX dl.d_()F=getY dl.L()F=getWidth dl.I()F=getHeight
|
||||
* dl.D()Ldo; = getParent do.D()Lhq; = getChildren hq extends fI(Array),size 字段在 fI 里
|
||||
* ⚠️ et(Table) 自己也声明了 `int L()` / `int I()`(列数/行数),所以必须**按返回类型 float 过滤**。
|
||||
*/
|
||||
public final class GeomProbe6 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static Class<?> HQL, ARR, AO;
|
||||
|
||||
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("[geom6] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) STATE = v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("LAST_STATE 拿不到"); return; }
|
||||
ClassLoader payload = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) { payload = c.getClassLoader(); break; }
|
||||
}
|
||||
HQL = Class.forName("hq", true, payload);
|
||||
ARR = Class.forName("fI", true, payload);
|
||||
AO = Class.forName("dl", true, payload);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
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; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(STATE); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work(Object state) throws Exception {
|
||||
Object root = state("root");
|
||||
log("=========== 面板控件树(缩进=层级;坐标为 stage 绝对坐标)===========");
|
||||
walk(root, 0, 0f, 0f, 6);
|
||||
log("=========== 面板根再往上(谁给它多大地方)===========");
|
||||
Object p = parent(root);
|
||||
float ax = 0f, ay = 0f;
|
||||
for (int i = 0; i < 6 && p != null; i++) {
|
||||
log(" 上级[" + i + "] " + p.getClass().getName() + " " + geom(p));
|
||||
Object np = parent(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
}
|
||||
log("=========== 关键对象定位 ===========");
|
||||
Object scroll = state("listScroll"), table = state("listTable");
|
||||
log(" listScroll " + geom(scroll) + " 子控件=" + size(childrenOf(scroll)));
|
||||
log(" listTable " + geom(table) + " 行数=" + size(childrenOf(table)));
|
||||
log(" listTable 的 stage 坐标 = " + stageX(table) + "," + stageY(table));
|
||||
log("=======================================================");
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static void walk(Object actor, int depth, float px, float py, int maxDepth) {
|
||||
if (actor == null || depth > maxDepth) return;
|
||||
StringBuilder ind = new StringBuilder();
|
||||
for (int i = 0; i < depth; i++) ind.append(" ");
|
||||
float x = fl(actor, "c_"), y = fl(actor, "d_");
|
||||
Object kids = childrenOf(actor);
|
||||
int n = size(kids);
|
||||
log(ind + actor.getClass().getName() + " " + geom(actor)
|
||||
+ " stage=(" + r(px + x) + "," + r(py + y) + ") 子控件=" + n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object child = get(kids, i);
|
||||
walk(child, depth + 1, px + x, py + y, maxDepth);
|
||||
}
|
||||
}
|
||||
|
||||
static float stageX(Object actor) {
|
||||
float x = 0f;
|
||||
Object a = actor;
|
||||
for (int i = 0; i < 8 && a != null; i++) { x += fl(a, "c_"); a = parent(a); }
|
||||
return r(x);
|
||||
}
|
||||
|
||||
static float stageY(Object actor) {
|
||||
float y = 0f;
|
||||
Object a = actor;
|
||||
for (int i = 0; i < 8 && a != null; i++) { y += fl(a, "d_"); a = parent(a); }
|
||||
return r(y);
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[local x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I"))
|
||||
+ " top=" + r(fl(actor, "S")) + " right=" + r(fl(actor, "C")) + "]";
|
||||
}
|
||||
|
||||
static Object parent(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (!m.getName().equals("D")) continue;
|
||||
if (!m.getReturnType().getName().equals("do")) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object childrenOf(Object actor) {
|
||||
if (actor == null) return null;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (m.getParameterTypes().length != 0) continue;
|
||||
if (m.getReturnType() != HQL) continue;
|
||||
try { m.setAccessible(true); return m.invoke(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = ARR; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针 7:确认"列表的 ScrollPane 被搬出面板"这件事。
|
||||
* 1) 面板根表格现在挂了哪几个子控件(对比应该有的 buttons / ScrollPane / 日志 Label)
|
||||
* 2) ScrollPane 现在的父控件是谁、挂在哪一层、坐标多少
|
||||
* 3) Stage 上所有"1 个抢 Actor 参数 + 返回 void"的方法(grabScrollFocus 会逐个调用它们)
|
||||
* —— 特别标出**声明类不在白名单**的那些,它们就是可疑的"搬家"凶手
|
||||
*/
|
||||
public final class GeomProbe7 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static ClassLoader PAYLOAD;
|
||||
static Class<?> HQL, ARR, ACTOR_CLASS, GROUP_CLASS, WIDGETGROUP_CLASS;
|
||||
|
||||
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("[geom7] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) PAYLOAD = c.getClassLoader();
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) STATE = v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("LAST_STATE 拿不到"); return; }
|
||||
HQL = Class.forName("hq", true, PAYLOAD);
|
||||
ARR = Class.forName("fI", true, PAYLOAD);
|
||||
ACTOR_CLASS = Class.forName("dl", true, PAYLOAD);
|
||||
GROUP_CLASS = Class.forName("do", true, PAYLOAD);
|
||||
WIDGETGROUP_CLASS = Class.forName("eX", true, PAYLOAD);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
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; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object pane = state("listScroll");
|
||||
Object table = state("listTable");
|
||||
log("========== 1) 面板根表格现在挂了哪些子控件 ==========");
|
||||
Object kids = groupChildren(root);
|
||||
int n = size(kids);
|
||||
log("子控件数 = " + n + "(正常应该是 3:按钮块 / ScrollPane / 日志 Label)");
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object c = get(kids, i);
|
||||
log(" [" + i + "] " + c.getClass().getName() + " " + geom(c)
|
||||
+ " 是 listScroll 吗=" + (c == pane) + " 是 listTable 吗=" + (c == table));
|
||||
}
|
||||
log("========== 2) ScrollPane 现在挂在谁下面 ==========");
|
||||
Object p = pane;
|
||||
float ax = 0f, ay = 0f;
|
||||
for (int i = 0; i < 6 && p != null; i++) {
|
||||
log(" 层[" + i + "] " + p.getClass().getName() + " " + geom(p));
|
||||
ax += fl(p, "c_");
|
||||
ay += fl(p, "d_");
|
||||
Object np = parentOf(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
}
|
||||
log(" ScrollPane 的绝对坐标 ≈ (" + r(ax) + "," + r(ay) + ") ← 面板在 x≈2052 的位置");
|
||||
log("========== 3) Stage 上那些 1 个 Actor 参数 + void 的方法 ==========");
|
||||
Object stage = null;
|
||||
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, PAYLOAD);
|
||||
for (String name : new String[] {"uk", "dt"}) {
|
||||
try {
|
||||
stage = sceneObjects.getMethod("global", ClassLoader.class, String.class)
|
||||
.invoke(null, PAYLOAD, name);
|
||||
} catch (Throwable ignored) { }
|
||||
if (stage != null) { log(" (用 " + name + " 当 Stage)"); break; }
|
||||
}
|
||||
if (stage == null) { log(" 没拿到 Stage"); return; }
|
||||
String[] skip = {"dl", "do", "eX"};
|
||||
for (Method m : stage.getClass().getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1) continue;
|
||||
if (!t[0].isAssignableFrom(ACTOR_CLASS)) continue;
|
||||
if (m.getReturnType() != void.class) continue;
|
||||
String decl = m.getDeclaringClass().getName();
|
||||
boolean skipped = false;
|
||||
for (String s : skip) if (decl.equals(s)) skipped = true;
|
||||
log(" " + (skipped ? "(已排除)" : "★会被调用★") + " " + decl + "." + m.getName()
|
||||
+ "(" + t[0].getName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static Object groupChildren(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parentOf(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != GROUP_CLASS) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I")) + "]";
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = ARR; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 只读探针 7:确认"列表的 ScrollPane 被搬出面板"这件事。
|
||||
* 1) 面板根表格现在挂了哪几个子控件(对比应该有的 buttons / ScrollPane / 日志 Label)
|
||||
* 2) ScrollPane 现在的父控件是谁、挂在哪一层、坐标多少
|
||||
* 3) Stage 上所有"1 个抢 Actor 参数 + 返回 void"的方法(grabScrollFocus 会逐个调用它们)
|
||||
* —— 特别标出**声明类不在白名单**的那些,它们就是可疑的"搬家"凶手
|
||||
*/
|
||||
public final class GeomProbe8 {
|
||||
static Instrumentation INST;
|
||||
static Object STATE;
|
||||
static ClassLoader PAYLOAD;
|
||||
static Class<?> HQL, ARR, ACTOR_CLASS, GROUP_CLASS, WIDGETGROUP_CLASS;
|
||||
|
||||
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("[geom8] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
java.util.LinkedHashSet<ClassLoader> loaders = new java.util.LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
if (c.getClassLoader() instanceof URLClassLoader) loaders.add(c.getClassLoader());
|
||||
if (!c.getName().equals("com.spineview.ObjectPanel")) continue;
|
||||
try {
|
||||
Field f = c.getDeclaredField("LAST_STATE");
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(null);
|
||||
if (v != null) STATE = v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
if (STATE == null) { log("LAST_STATE 拿不到"); return; }
|
||||
// 可能有多个 URLClassLoader —— 逐个试,谁能加载 hq 谁就是载荷加载器
|
||||
for (ClassLoader cl : loaders) {
|
||||
try { HQL = Class.forName("hq", true, cl); PAYLOAD = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
log("载荷加载器 = " + PAYLOAD);
|
||||
if (HQL == null) { log("找不到能加载 hq 的加载器"); return; }
|
||||
HQL = Class.forName("hq", true, PAYLOAD);
|
||||
ARR = Class.forName("fI", true, PAYLOAD);
|
||||
ACTOR_CLASS = Class.forName("dl", true, PAYLOAD);
|
||||
GROUP_CLASS = Class.forName("do", true, PAYLOAD);
|
||||
WIDGETGROUP_CLASS = Class.forName("eX", true, PAYLOAD);
|
||||
Class<?> og = null;
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) if (c.getName().equals("OG")) og = c;
|
||||
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; }
|
||||
}
|
||||
onRender.setAccessible(true);
|
||||
onRender.invoke(null, new Runnable() {
|
||||
@Override public void run() {
|
||||
try { work(); } catch (Throwable e) { log("work 失败: " + e); e.printStackTrace(System.err); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
Object root = state("root");
|
||||
Object pane = state("listScroll");
|
||||
Object table = state("listTable");
|
||||
log("========== 1) 面板根表格现在挂了哪些子控件 ==========");
|
||||
Object kids = groupChildren(root);
|
||||
int n = size(kids);
|
||||
log("子控件数 = " + n + "(正常应该是 3:按钮块 / ScrollPane / 日志 Label)");
|
||||
for (int i = 0; i < n; i++) {
|
||||
Object c = get(kids, i);
|
||||
log(" [" + i + "] " + c.getClass().getName() + " " + geom(c)
|
||||
+ " 是 listScroll 吗=" + (c == pane) + " 是 listTable 吗=" + (c == table));
|
||||
}
|
||||
log("========== 2) ScrollPane 现在挂在谁下面 ==========");
|
||||
Object p = pane;
|
||||
float ax = 0f, ay = 0f;
|
||||
for (int i = 0; i < 6 && p != null; i++) {
|
||||
log(" 层[" + i + "] " + p.getClass().getName() + " " + geom(p));
|
||||
ax += fl(p, "c_");
|
||||
ay += fl(p, "d_");
|
||||
Object np = parentOf(p);
|
||||
if (np == p) break;
|
||||
p = np;
|
||||
}
|
||||
log(" ScrollPane 的绝对坐标 ≈ (" + r(ax) + "," + r(ay) + ") ← 面板在 x≈2052 的位置");
|
||||
log("========== 3) Stage 上那些 1 个 Actor 参数 + void 的方法 ==========");
|
||||
Object stage = null;
|
||||
Class<?> sceneObjects = Class.forName("com.spineview.SceneObjects", true, PAYLOAD);
|
||||
for (String name : new String[] {"uk", "dt"}) {
|
||||
try {
|
||||
stage = sceneObjects.getMethod("global", ClassLoader.class, String.class)
|
||||
.invoke(null, PAYLOAD, name);
|
||||
} catch (Throwable ignored) { }
|
||||
if (stage != null) { log(" (用 " + name + " 当 Stage)"); break; }
|
||||
}
|
||||
if (stage == null) { log(" 没拿到 Stage"); return; }
|
||||
String[] skip = {"dl", "do", "eX"};
|
||||
for (Method m : stage.getClass().getMethods()) {
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1) continue;
|
||||
if (!t[0].isAssignableFrom(ACTOR_CLASS)) continue;
|
||||
if (m.getReturnType() != void.class) continue;
|
||||
String decl = m.getDeclaringClass().getName();
|
||||
boolean skipped = false;
|
||||
for (String s : skip) if (decl.equals(s)) skipped = true;
|
||||
log(" " + (skipped ? "(已排除)" : "★会被调用★") + " " + decl + "." + m.getName()
|
||||
+ "(" + t[0].getName() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
static Object state(String name) throws Exception {
|
||||
Field f = STATE.getClass().getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
return f.get(STATE);
|
||||
}
|
||||
|
||||
static Object groupChildren(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != HQL) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object parentOf(Object actor) {
|
||||
for (Class<?> t = actor.getClass(); t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(f.getModifiers())) continue;
|
||||
if (f.getType() != GROUP_CLASS) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.get(actor); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String geom(Object actor) {
|
||||
if (actor == null) return "null";
|
||||
return "[x=" + r(fl(actor, "c_")) + " y=" + r(fl(actor, "d_"))
|
||||
+ " w=" + r(fl(actor, "L")) + " h=" + r(fl(actor, "I")) + "]";
|
||||
}
|
||||
|
||||
static int size(Object array) {
|
||||
if (array == null) return 0;
|
||||
for (Class<?> t = ARR; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getType() != int.class) continue;
|
||||
f.setAccessible(true);
|
||||
try { return f.getInt(array); } catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Object get(Object array, int index) {
|
||||
if (array == null) return null;
|
||||
for (Method m : array.getClass().getMethods()) {
|
||||
if (!m.getName().equals("D")) continue;
|
||||
Class<?>[] t = m.getParameterTypes();
|
||||
if (t.length != 1 || t[0] != int.class || m.getReturnType() != Object.class) continue;
|
||||
try { m.setAccessible(true); return m.invoke(array, Integer.valueOf(index)); } catch (Throwable ignored) { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static float fl(Object actor, String name) {
|
||||
if (actor == null) return -1f;
|
||||
for (Method m : actor.getClass().getMethods()) {
|
||||
if (!m.getName().equals(name)) continue;
|
||||
if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue;
|
||||
try { m.setAccessible(true); return ((Float) m.invoke(actor)).floatValue(); } catch (Throwable ignored) { }
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static float r(float v) { return Math.round(v * 100f) / 100f; }
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
import java.io.FileWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 抽查验证用:直接调用编辑器自己的 JSON 导出器(载荷类 sB),做"字段 -> key"的差分实验。
|
||||
*
|
||||
* 步骤(全程在编辑器渲染线程里跑):
|
||||
* 1. 导出一次 -> /tmp/spinecheck-before.json
|
||||
* 2. 改若干个字段(骨骼 length、插槽 color/dark、图片附件 color)
|
||||
* 3. 再导出 -> /tmp/spinecheck-after.json
|
||||
* 4. 把字段改回原值,再导出 -> /tmp/spinecheck-restored.json
|
||||
* 之后对比三份 JSON:对应 key 的值如果跟着字段变、又跟着改回来,就说明"字段 ↔ key"是对的。
|
||||
*
|
||||
* 不改任何长期状态(值都还原)。
|
||||
*/
|
||||
public final class JsonDiff2 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Class<?> ARRAY, NODE, GP, GPC, GU, GN, GO, GY, GB, COLOR;
|
||||
static Object SKELETON;
|
||||
static Object BONES, SLOTS;
|
||||
static Object BONE, SLOT, SLOT_ATTACH;
|
||||
static Method SLOT_CURRENT;
|
||||
static Field F_BONE_LENGTH, F_SLOT_COLOR, F_SLOT_DARK, F_SLOT_HASDARK, F_GO_COLOR;
|
||||
static Object OLD_LEN, OLD_SLOT_COLOR, OLD_GO_COLOR, OLD_DARK;
|
||||
static boolean OLD_HASDARK;
|
||||
static Field GY_SETTINGS;
|
||||
static Object WRITER;
|
||||
static StringWriter SW;
|
||||
static Object SB_INSTANCE;
|
||||
static Method EXPORT;
|
||||
|
||||
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("[jsondiff2] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
// 谁**真的能加载载荷类 GP**,谁才是编辑器类加载器(重启后第一个 URLClassLoader 未必是它)
|
||||
Set<ClassLoader> candidates = new LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
ClassLoader cl = c.getClassLoader();
|
||||
if (cl instanceof URLClassLoader) candidates.add(cl);
|
||||
}
|
||||
for (ClassLoader cl : candidates) {
|
||||
try { Class.forName("GP", false, cl); L = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
if (L == null) { log("找不到能加载 GP 的编辑器类加载器,候选 " + candidates.size() + " 个"); return; }
|
||||
log("编辑器类加载器 = " + L);
|
||||
ARRAY = Class.forName("fI", true, L);
|
||||
NODE = Class.forName("eM", true, L);
|
||||
GP = Class.forName("GP", true, L);
|
||||
GPC = Class.forName("Gp", true, L);
|
||||
GU = Class.forName("GU", true, L);
|
||||
GN = Class.forName("Gn", true, L);
|
||||
GO = Class.forName("GO", true, L);
|
||||
GY = Class.forName("gy", true, L);
|
||||
GB = Class.forName("gB", true, L);
|
||||
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||
Class<?> OG = Class.forName("OG", true, L);
|
||||
Class<?> SB = Class.forName("sB", true, L);
|
||||
|
||||
SKELETON = firstOfType(rootNodes(globalStatic(OG, "Uq")), GP);
|
||||
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||
BONES = arrFieldOfType(GP, GPC);
|
||||
SLOTS = arrFieldOfType(GP, GU);
|
||||
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||
log("骨架 = " + nameOf(SKELETON) + ",骨骼 " + size(BONES) + ",插槽 " + size(SLOTS));
|
||||
|
||||
BONE = byName(BONES, "bone");
|
||||
if (BONE == null && size(BONES) > 1) BONE = get(BONES, 1);
|
||||
SLOT = byName(SLOTS, "mega_dg4");
|
||||
if (SLOT == null && size(SLOTS) > 0) SLOT = get(SLOTS, 0);
|
||||
SLOT_ATTACH = SLOT_CURRENT.invoke(SLOT);
|
||||
log("样本骨骼 = " + nameOf(BONE) + ",样本插槽 = " + nameOf(SLOT)
|
||||
+ ",该插槽附件 = " + (SLOT_ATTACH == null ? "null" : SLOT_ATTACH.getClass().getName()));
|
||||
|
||||
F_BONE_LENGTH = floatField(GPC, "C");
|
||||
F_SLOT_COLOR = typedField(GU, "B", COLOR);
|
||||
F_SLOT_DARK = typedField(GU, "I", COLOR);
|
||||
F_SLOT_HASDARK = typedField(GU, "I", boolean.class);
|
||||
F_GO_COLOR = SLOT_ATTACH == null ? null : typedField(GO, "D", COLOR);
|
||||
|
||||
// 导出器:sB.D(GP, g, gB, Z, Z, Z, StringBuilder)
|
||||
SB_INSTANCE = SB.getConstructor().newInstance();
|
||||
EXPORT = SB.getMethod("D", GP, Class.forName("g", true, L), GB, boolean.class, boolean.class,
|
||||
boolean.class, StringBuilder.class);
|
||||
EXPORT.setAccessible(true);
|
||||
GY_SETTINGS = GY.getField("D");
|
||||
|
||||
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); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
String before = export("before");
|
||||
if (before == null || before.isEmpty()) { log("导出失败,实验终止"); return; }
|
||||
log("before.json 长度 = " + before.length());
|
||||
|
||||
// 1) 骨骼 length(Gp.C:F)
|
||||
OLD_LEN = F_BONE_LENGTH.get(BONE);
|
||||
F_BONE_LENGTH.set(BONE, 123.456f);
|
||||
// 2) 插槽 color(GU.B:Color)→ 用可辨识的紫
|
||||
OLD_SLOT_COLOR = F_SLOT_COLOR.get(SLOT);
|
||||
F_SLOT_COLOR.set(SLOT, newColor(0.25f, 0.5f, 0.75f, 1f));
|
||||
// 3) 插槽 dark(GU.I:Color + GU.I:boolean)
|
||||
if (F_SLOT_DARK != null) { OLD_DARK = F_SLOT_DARK.get(SLOT); F_SLOT_DARK.set(SLOT, newColor(1f, 0.2f, 0.1f, 1f)); }
|
||||
if (F_SLOT_HASDARK != null) { OLD_HASDARK = F_SLOT_HASDARK.getBoolean(SLOT); F_SLOT_HASDARK.setBoolean(SLOT, true); }
|
||||
// 4) 图片附件 color(GO.D:Color)
|
||||
if (F_GO_COLOR != null) { OLD_GO_COLOR = F_GO_COLOR.get(SLOT_ATTACH); F_GO_COLOR.set(SLOT_ATTACH, newColor(0.1f, 0.9f, 0.3f, 0.8f)); }
|
||||
log("已改:bone." + nameOf(BONE) + ".length=123.456,slot." + nameOf(SLOT) + ".color/dark,附件.color");
|
||||
export("after");
|
||||
|
||||
// 还原
|
||||
F_BONE_LENGTH.set(BONE, OLD_LEN);
|
||||
F_SLOT_COLOR.set(SLOT, OLD_SLOT_COLOR);
|
||||
if (F_SLOT_DARK != null && OLD_DARK != null) F_SLOT_DARK.set(SLOT, OLD_DARK);
|
||||
if (F_SLOT_HASDARK != null) F_SLOT_HASDARK.setBoolean(SLOT, OLD_HASDARK);
|
||||
if (F_GO_COLOR != null && OLD_GO_COLOR != null) F_GO_COLOR.set(SLOT_ATTACH, OLD_GO_COLOR);
|
||||
log("已还原原值");
|
||||
export("restored");
|
||||
}
|
||||
|
||||
static Object newColor(float r, float g, float b, float a) throws Exception {
|
||||
return COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(r, g, b, a);
|
||||
}
|
||||
|
||||
static String export(String tag) throws Exception {
|
||||
SW = new StringWriter();
|
||||
WRITER = GY.getConstructor(java.io.Writer.class).newInstance(SW);
|
||||
GY.getMethod("D").invoke(WRITER); // 初始化
|
||||
Object settings = null;
|
||||
Object[] values = (Object[]) GB.getMethod("values").invoke(null);
|
||||
for (int attempt = -1; attempt < values.length; attempt++) {
|
||||
settings = attempt < 0 ? null : values[attempt];
|
||||
GY_SETTINGS.set(WRITER, settings);
|
||||
try {
|
||||
EXPORT.invoke(SB_INSTANCE, SKELETON, null, settings, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE,
|
||||
new StringBuilder());
|
||||
if (SW.getBuffer().length() > 0) {
|
||||
log("导出(" + tag + ") 成功,settings=" + settings);
|
||||
write(tag, SW.toString());
|
||||
return SW.toString();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
if (attempt + 1 >= values.length) log("导出(" + tag + ") 各种 settings 都失败,最后一次: " + cause);
|
||||
}
|
||||
}
|
||||
return SW.getBuffer().toString();
|
||||
}
|
||||
|
||||
static void write(String tag, String text) throws Exception {
|
||||
FileWriter fw = new FileWriter("/tmp/spinecheck-" + tag + ".json");
|
||||
try { fw.write(text); } finally { fw.close(); }
|
||||
}
|
||||
|
||||
static Object byName(Object array, String name) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null && name.equals(nameOf(o))) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field floatField(Class<?> owner, String name) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == float.class) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field typedField(Class<?> owner, String name, Class<?> type) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == type) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrFieldOfType(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 v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
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,311 @@
|
||||
import java.io.FileWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 抽查验证用:直接调用编辑器自己的 JSON 导出器(载荷类 sB),做"字段 -> key"的差分实验。
|
||||
*
|
||||
* 步骤(全程在编辑器渲染线程里跑):
|
||||
* 1. 导出一次 -> /tmp/spinecheck-before.json
|
||||
* 2. 改若干个字段(骨骼 length、插槽 color/dark、图片附件 color)
|
||||
* 3. 再导出 -> /tmp/spinecheck-after.json
|
||||
* 4. 把字段改回原值,再导出 -> /tmp/spinecheck-restored.json
|
||||
* 之后对比三份 JSON:对应 key 的值如果跟着字段变、又跟着改回来,就说明"字段 ↔ key"是对的。
|
||||
*
|
||||
* 不改任何长期状态(值都还原)。
|
||||
*/
|
||||
public final class JsonDiff3 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Class<?> ARRAY, NODE, GP, GPC, GU, GN, GO, GY, GB, COLOR;
|
||||
static Object SKELETON;
|
||||
static Object BONES, SLOTS;
|
||||
static Object BONE, SLOT, SLOT_ATTACH;
|
||||
static Method SLOT_CURRENT;
|
||||
static Field F_BONE_LENGTH, F_SLOT_COLOR, F_SLOT_DARK, F_SLOT_HASDARK, F_GO_COLOR;
|
||||
static Object OLD_LEN, OLD_SLOT_COLOR, OLD_GO_COLOR, OLD_DARK;
|
||||
static boolean OLD_HASDARK;
|
||||
static Field GY_SETTINGS;
|
||||
static Object WRITER;
|
||||
static StringWriter SW;
|
||||
static Object SB_INSTANCE;
|
||||
static Method EXPORT;
|
||||
|
||||
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("[jsondiff3] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
// 谁**真的能加载载荷类 GP**,谁才是编辑器类加载器(重启后第一个 URLClassLoader 未必是它)
|
||||
Set<ClassLoader> candidates = new LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
ClassLoader cl = c.getClassLoader();
|
||||
if (cl instanceof URLClassLoader) candidates.add(cl);
|
||||
}
|
||||
for (ClassLoader cl : candidates) {
|
||||
try { Class.forName("GP", false, cl); L = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
if (L == null) { log("找不到能加载 GP 的编辑器类加载器,候选 " + candidates.size() + " 个"); return; }
|
||||
log("编辑器类加载器 = " + L);
|
||||
ARRAY = Class.forName("fI", true, L);
|
||||
NODE = Class.forName("eM", true, L);
|
||||
GP = Class.forName("GP", true, L);
|
||||
GPC = Class.forName("Gp", true, L);
|
||||
GU = Class.forName("GU", true, L);
|
||||
GN = Class.forName("Gn", true, L);
|
||||
GO = Class.forName("GO", true, L);
|
||||
GY = Class.forName("gy", true, L);
|
||||
GB = Class.forName("gB", true, L);
|
||||
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||
Class<?> OG = Class.forName("OG", true, L);
|
||||
Class<?> SB = Class.forName("sB", true, L);
|
||||
|
||||
SKELETON = firstOfType(rootNodes(globalStatic(OG, "Uq")), GP);
|
||||
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||
BONES = arrFieldOfType(GP, GPC);
|
||||
SLOTS = arrFieldOfType(GP, GU);
|
||||
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||
log("骨架 = " + nameOf(SKELETON) + ",骨骼 " + size(BONES) + ",插槽 " + size(SLOTS));
|
||||
|
||||
BONE = byName(BONES, "bone");
|
||||
if (BONE == null && size(BONES) > 1) BONE = get(BONES, 1);
|
||||
SLOT = byName(SLOTS, "mega_dg4");
|
||||
if (SLOT == null && size(SLOTS) > 0) SLOT = get(SLOTS, 0);
|
||||
SLOT_ATTACH = SLOT_CURRENT.invoke(SLOT);
|
||||
log("样本骨骼 = " + nameOf(BONE) + ",样本插槽 = " + nameOf(SLOT)
|
||||
+ ",该插槽附件 = " + (SLOT_ATTACH == null ? "null" : SLOT_ATTACH.getClass().getName()));
|
||||
|
||||
F_BONE_LENGTH = floatField(GPC, "C");
|
||||
F_SLOT_COLOR = typedField(GU, "B", COLOR);
|
||||
F_SLOT_DARK = typedField(GU, "I", COLOR);
|
||||
F_SLOT_HASDARK = typedField(GU, "I", boolean.class);
|
||||
F_GO_COLOR = SLOT_ATTACH == null ? null : typedField(GO, "D", COLOR);
|
||||
|
||||
// 导出器:sB.D(GP, g, gB, Z, Z, Z, StringBuilder)
|
||||
SB_INSTANCE = SB.getConstructor().newInstance();
|
||||
EXPORT = SB.getMethod("D", GP, Class.forName("g", true, L), GB, boolean.class, boolean.class,
|
||||
boolean.class, StringBuilder.class);
|
||||
EXPORT.setAccessible(true);
|
||||
GY_SETTINGS = GY.getField("D");
|
||||
|
||||
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); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
String before = export("before");
|
||||
if (before == null || before.isEmpty()) { log("导出失败,实验终止"); return; }
|
||||
log("before.json 长度 = " + before.length());
|
||||
|
||||
// 1) 骨骼 length(Gp.C:F)
|
||||
OLD_LEN = F_BONE_LENGTH.get(BONE);
|
||||
F_BONE_LENGTH.set(BONE, 123.456f);
|
||||
// 2) 插槽 color(GU.B:Color)→ 用可辨识的紫
|
||||
OLD_SLOT_COLOR = F_SLOT_COLOR.get(SLOT);
|
||||
F_SLOT_COLOR.set(SLOT, newColor(0.25f, 0.5f, 0.75f, 1f));
|
||||
// 3) 插槽 dark(GU.I:Color + GU.I:boolean)
|
||||
if (F_SLOT_DARK != null) { OLD_DARK = F_SLOT_DARK.get(SLOT); F_SLOT_DARK.set(SLOT, newColor(1f, 0.2f, 0.1f, 1f)); }
|
||||
if (F_SLOT_HASDARK != null) { OLD_HASDARK = F_SLOT_HASDARK.getBoolean(SLOT); F_SLOT_HASDARK.setBoolean(SLOT, true); }
|
||||
// 4) 图片附件 color(GO.D:Color)
|
||||
if (F_GO_COLOR != null) { OLD_GO_COLOR = F_GO_COLOR.get(SLOT_ATTACH); F_GO_COLOR.set(SLOT_ATTACH, newColor(0.1f, 0.9f, 0.3f, 0.8f)); }
|
||||
log("已改:bone." + nameOf(BONE) + ".length=123.456,slot." + nameOf(SLOT) + ".color/dark,附件.color");
|
||||
export("after");
|
||||
|
||||
// 还原
|
||||
F_BONE_LENGTH.set(BONE, OLD_LEN);
|
||||
F_SLOT_COLOR.set(SLOT, OLD_SLOT_COLOR);
|
||||
if (F_SLOT_DARK != null && OLD_DARK != null) F_SLOT_DARK.set(SLOT, OLD_DARK);
|
||||
if (F_SLOT_HASDARK != null) F_SLOT_HASDARK.setBoolean(SLOT, OLD_HASDARK);
|
||||
if (F_GO_COLOR != null && OLD_GO_COLOR != null) F_GO_COLOR.set(SLOT_ATTACH, OLD_GO_COLOR);
|
||||
log("已还原原值");
|
||||
export("restored");
|
||||
}
|
||||
|
||||
static Object newColor(float r, float g, float b, float a) throws Exception {
|
||||
return COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(r, g, b, a);
|
||||
}
|
||||
|
||||
static String export(String tag) throws Exception {
|
||||
SW = new StringWriter();
|
||||
WRITER = GY.getConstructor(java.io.Writer.class).newInstance(SW);
|
||||
GY.getMethod("D").invoke(WRITER); // 初始化
|
||||
Object settings = null;
|
||||
Object[] values = (Object[]) GB.getMethod("values").invoke(null);
|
||||
for (int attempt = -1; attempt < values.length; attempt++) {
|
||||
settings = attempt < 0 ? null : values[attempt];
|
||||
GY_SETTINGS.set(WRITER, settings);
|
||||
try {
|
||||
EXPORT.invoke(SB_INSTANCE, SKELETON, null, settings, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE,
|
||||
new StringBuilder());
|
||||
if (SW.getBuffer().length() > 0) {
|
||||
log("导出(" + tag + ") 成功,settings=" + settings);
|
||||
write(tag, SW.toString());
|
||||
return SW.toString();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
log("导出(" + tag + ") settings=" + settings + " 失败: " + cause);
|
||||
if (attempt < 0) cause.printStackTrace(System.err); // 第一发就打印完整栈,看清到底谁 null
|
||||
}
|
||||
}
|
||||
return SW.getBuffer().toString();
|
||||
}
|
||||
|
||||
static void write(String tag, String text) throws Exception {
|
||||
FileWriter fw = new FileWriter("/tmp/spinecheck-" + tag + ".json");
|
||||
try { fw.write(text); } finally { fw.close(); }
|
||||
}
|
||||
|
||||
static Object byName(Object array, String name) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null && name.equals(nameOf(o))) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field floatField(Class<?> owner, String name) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == float.class) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field typedField(Class<?> owner, String name, Class<?> type) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == type) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrFieldOfType(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 v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
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,311 @@
|
||||
import java.io.FileWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 抽查验证用:直接调用编辑器自己的 JSON 导出器(载荷类 sB),做"字段 -> key"的差分实验。
|
||||
*
|
||||
* 步骤(全程在编辑器渲染线程里跑):
|
||||
* 1. 导出一次 -> /tmp/spinecheck-before.json
|
||||
* 2. 改若干个字段(骨骼 length、插槽 color/dark、图片附件 color)
|
||||
* 3. 再导出 -> /tmp/spinecheck-after.json
|
||||
* 4. 把字段改回原值,再导出 -> /tmp/spinecheck-restored.json
|
||||
* 之后对比三份 JSON:对应 key 的值如果跟着字段变、又跟着改回来,就说明"字段 ↔ key"是对的。
|
||||
*
|
||||
* 不改任何长期状态(值都还原)。
|
||||
*/
|
||||
public final class JsonDiff4 {
|
||||
static Instrumentation INST;
|
||||
static ClassLoader L;
|
||||
static Class<?> ARRAY, NODE, GP, GPC, GU, GN, GO, GY, GB, COLOR;
|
||||
static Object SKELETON;
|
||||
static Object BONES, SLOTS;
|
||||
static Object BONE, SLOT, SLOT_ATTACH;
|
||||
static Method SLOT_CURRENT;
|
||||
static Field F_BONE_LENGTH, F_SLOT_COLOR, F_SLOT_DARK, F_SLOT_HASDARK, F_GO_COLOR;
|
||||
static Object OLD_LEN, OLD_SLOT_COLOR, OLD_GO_COLOR, OLD_DARK;
|
||||
static boolean OLD_HASDARK;
|
||||
static Field GY_SETTINGS;
|
||||
static Object WRITER;
|
||||
static StringWriter SW;
|
||||
static Object SB_INSTANCE;
|
||||
static Method EXPORT;
|
||||
|
||||
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("[jsondiff4] " + m); }
|
||||
|
||||
static void run() throws Exception {
|
||||
// 谁**真的能加载载荷类 GP**,谁才是编辑器类加载器(重启后第一个 URLClassLoader 未必是它)
|
||||
Set<ClassLoader> candidates = new LinkedHashSet<ClassLoader>();
|
||||
for (Class<?> c : INST.getAllLoadedClasses()) {
|
||||
ClassLoader cl = c.getClassLoader();
|
||||
if (cl instanceof URLClassLoader) candidates.add(cl);
|
||||
}
|
||||
for (ClassLoader cl : candidates) {
|
||||
try { Class.forName("GP", false, cl); L = cl; break; } catch (Throwable ignored) { }
|
||||
}
|
||||
if (L == null) { log("找不到能加载 GP 的编辑器类加载器,候选 " + candidates.size() + " 个"); return; }
|
||||
log("编辑器类加载器 = " + L);
|
||||
ARRAY = Class.forName("fI", true, L);
|
||||
NODE = Class.forName("eM", true, L);
|
||||
GP = Class.forName("GP", true, L);
|
||||
GPC = Class.forName("Gp", true, L);
|
||||
GU = Class.forName("GU", true, L);
|
||||
GN = Class.forName("Gn", true, L);
|
||||
GO = Class.forName("GO", true, L);
|
||||
GY = Class.forName("gy", true, L);
|
||||
GB = Class.forName("gB", true, L);
|
||||
COLOR = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
|
||||
Class<?> OG = Class.forName("OG", true, L);
|
||||
Class<?> SB = Class.forName("sB", true, L);
|
||||
|
||||
SKELETON = firstOfType(rootNodes(globalStatic(OG, "Uq")), GP);
|
||||
if (SKELETON == null) { log("拿不到骨架"); return; }
|
||||
BONES = arrFieldOfType(GP, GPC);
|
||||
SLOTS = arrFieldOfType(GP, GU);
|
||||
SLOT_CURRENT = method(GU, "D", new Class<?>[] {}, GN);
|
||||
log("骨架 = " + nameOf(SKELETON) + ",骨骼 " + size(BONES) + ",插槽 " + size(SLOTS));
|
||||
|
||||
BONE = byName(BONES, "bone");
|
||||
if (BONE == null && size(BONES) > 1) BONE = get(BONES, 1);
|
||||
SLOT = byName(SLOTS, "mega_dg4");
|
||||
if (SLOT == null && size(SLOTS) > 0) SLOT = get(SLOTS, 0);
|
||||
SLOT_ATTACH = SLOT_CURRENT.invoke(SLOT);
|
||||
log("样本骨骼 = " + nameOf(BONE) + ",样本插槽 = " + nameOf(SLOT)
|
||||
+ ",该插槽附件 = " + (SLOT_ATTACH == null ? "null" : SLOT_ATTACH.getClass().getName()));
|
||||
|
||||
F_BONE_LENGTH = floatField(GPC, "C");
|
||||
F_SLOT_COLOR = typedField(GU, "B", COLOR);
|
||||
F_SLOT_DARK = typedField(GU, "I", COLOR);
|
||||
F_SLOT_HASDARK = typedField(GU, "I", boolean.class);
|
||||
F_GO_COLOR = SLOT_ATTACH == null ? null : typedField(GO, "D", COLOR);
|
||||
|
||||
// 导出器:sB.D(GP, g, gB, Z, Z, Z, StringBuilder)
|
||||
SB_INSTANCE = SB.getConstructor().newInstance();
|
||||
EXPORT = SB.getMethod("D", GP, Class.forName("g", true, L), GB, boolean.class, boolean.class,
|
||||
boolean.class, StringBuilder.class);
|
||||
EXPORT.setAccessible(true);
|
||||
GY_SETTINGS = GY.getField("D");
|
||||
|
||||
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); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void work() throws Exception {
|
||||
String before = export("before");
|
||||
if (before == null || before.isEmpty()) { log("导出失败,实验终止"); return; }
|
||||
log("before.json 长度 = " + before.length());
|
||||
|
||||
// 1) 骨骼 length(Gp.C:F)
|
||||
OLD_LEN = F_BONE_LENGTH.get(BONE);
|
||||
F_BONE_LENGTH.set(BONE, 123.456f);
|
||||
// 2) 插槽 color(GU.B:Color)→ 用可辨识的紫
|
||||
OLD_SLOT_COLOR = F_SLOT_COLOR.get(SLOT);
|
||||
F_SLOT_COLOR.set(SLOT, newColor(0.25f, 0.5f, 0.75f, 1f));
|
||||
// 3) 插槽 dark(GU.I:Color + GU.I:boolean)
|
||||
if (F_SLOT_DARK != null) { OLD_DARK = F_SLOT_DARK.get(SLOT); F_SLOT_DARK.set(SLOT, newColor(1f, 0.2f, 0.1f, 1f)); }
|
||||
if (F_SLOT_HASDARK != null) { OLD_HASDARK = F_SLOT_HASDARK.getBoolean(SLOT); F_SLOT_HASDARK.setBoolean(SLOT, true); }
|
||||
// 4) 图片附件 color(GO.D:Color)
|
||||
if (F_GO_COLOR != null) { OLD_GO_COLOR = F_GO_COLOR.get(SLOT_ATTACH); F_GO_COLOR.set(SLOT_ATTACH, newColor(0.1f, 0.9f, 0.3f, 0.8f)); }
|
||||
log("已改:bone." + nameOf(BONE) + ".length=123.456,slot." + nameOf(SLOT) + ".color/dark,附件.color");
|
||||
export("after");
|
||||
|
||||
// 还原
|
||||
F_BONE_LENGTH.set(BONE, OLD_LEN);
|
||||
F_SLOT_COLOR.set(SLOT, OLD_SLOT_COLOR);
|
||||
if (F_SLOT_DARK != null && OLD_DARK != null) F_SLOT_DARK.set(SLOT, OLD_DARK);
|
||||
if (F_SLOT_HASDARK != null) F_SLOT_HASDARK.setBoolean(SLOT, OLD_HASDARK);
|
||||
if (F_GO_COLOR != null && OLD_GO_COLOR != null) F_GO_COLOR.set(SLOT_ATTACH, OLD_GO_COLOR);
|
||||
log("已还原原值");
|
||||
export("restored");
|
||||
}
|
||||
|
||||
static Object newColor(float r, float g, float b, float a) throws Exception {
|
||||
return COLOR.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(r, g, b, a);
|
||||
}
|
||||
|
||||
static String export(String tag) throws Exception {
|
||||
Object[] values = (Object[]) GB.getMethod("values").invoke(null);
|
||||
Object settings = null;
|
||||
for (int attempt = -1; attempt < values.length; attempt++) {
|
||||
settings = attempt < 0 ? null : values[attempt];
|
||||
SW = new StringWriter();
|
||||
WRITER = GY.getConstructor(java.io.Writer.class).newInstance(SW);
|
||||
GY_SETTINGS.set(WRITER, settings); // 先设 settings(照 sB 的字节码顺序)
|
||||
GY.getMethod("B").invoke(WRITER); // 再开根对象
|
||||
try {
|
||||
EXPORT.invoke(SB_INSTANCE, SKELETON, null, settings, Boolean.TRUE, Boolean.TRUE, Boolean.TRUE,
|
||||
new StringBuilder());
|
||||
if (SW.getBuffer().length() > 0) {
|
||||
log("导出(" + tag + ") 成功,settings=" + settings);
|
||||
write(tag, SW.toString());
|
||||
return SW.toString();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Throwable cause = e.getCause() == null ? e : e.getCause();
|
||||
log("导出(" + tag + ") settings=" + settings + " 失败: " + cause);
|
||||
if (attempt < 0) cause.printStackTrace(System.err); // 第一发就打印完整栈,看清到底谁 null
|
||||
}
|
||||
}
|
||||
return SW.getBuffer().toString();
|
||||
}
|
||||
|
||||
static void write(String tag, String text) throws Exception {
|
||||
FileWriter fw = new FileWriter("/tmp/spinecheck-" + tag + ".json");
|
||||
try { fw.write(text); } finally { fw.close(); }
|
||||
}
|
||||
|
||||
static Object byName(Object array, String name) {
|
||||
for (int i = 0; i < size(array); i++) {
|
||||
Object o = get(array, i);
|
||||
if (o != null && name.equals(nameOf(o))) return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field floatField(Class<?> owner, String name) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == float.class) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Field typedField(Class<?> owner, String name, Class<?> type) {
|
||||
for (Class<?> t = owner; t != null && t != Object.class; t = t.getSuperclass()) {
|
||||
for (Field f : t.getDeclaredFields()) {
|
||||
if (f.getName().equals(name) && f.getType() == type) { f.setAccessible(true); return f; }
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Object arrFieldOfType(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 v;
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
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,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;
|
||||
}
|
||||
}
|
||||
@@ -636,41 +636,64 @@ public final class ObjectPanel {
|
||||
* 建用编辑器自己的 New Bone(带撤销),建完立刻改该骨骼的 x/y 与 color 字段。
|
||||
*/
|
||||
private static void createTenRedBones(final State state) {
|
||||
// 编辑器自己的 API 路径(不经 UI、不依赖下一帧):一次建 10 根 → 一次刷新 → 一次撤销
|
||||
final Object parent = currentTarget(state);
|
||||
if (parent == null) {
|
||||
setStatus(state, "+10 Red Bones: select a parent bone first.");
|
||||
return;
|
||||
}
|
||||
final Object skeleton = SceneActions.skeletonOf(parent);
|
||||
if (skeleton == null) {
|
||||
setStatus(state, "+10 Red Bones: 找不到所属骨架");
|
||||
return;
|
||||
}
|
||||
final int[] made = new int[1];
|
||||
SceneActions.onEditorThread(state.loader, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 父级取"编辑器里的当前选择"(用户是在层级面板里选的,不是点我们列表行)
|
||||
Object parent = currentTarget(state);
|
||||
if (parent == null) {
|
||||
setStatus(state, "+10 Red Bones: select a parent bone first.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, state.loader);
|
||||
Object red = colorClass.getConstructor(float.class, float.class, float.class,
|
||||
float.class).newInstance(Float.valueOf(1f), Float.valueOf(0f), Float.valueOf(0f),
|
||||
Float.valueOf(1f));
|
||||
int made = 0;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
java.util.List<Object> before = SceneObjects.listAllNodes(state.loader);
|
||||
SceneActions.create(state.loader, parent, "Bone");
|
||||
Object fresh = firstNew(before, SceneObjects.listAllNodes(state.loader));
|
||||
if (fresh == null) continue;
|
||||
float x = (float) (Math.random() * 400.0 - 200.0);
|
||||
float y = (float) (Math.random() * 400.0 - 200.0);
|
||||
setBoneNumber(fresh, "x", x);
|
||||
setBoneNumber(fresh, "y", y);
|
||||
setBoneColor(fresh, red);
|
||||
made++;
|
||||
Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
|
||||
.newInstance(Float.valueOf(1f), Float.valueOf(0f), Float.valueOf(0f), Float.valueOf(1f));
|
||||
Class<?> undo = Class.forName("un", true, state.loader);
|
||||
java.lang.reflect.Method begin = null;
|
||||
java.lang.reflect.Method 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;
|
||||
}
|
||||
setStatus(state, "made " + made + " red bones under " + SceneObjects.nameOf(parent));
|
||||
if (begin != null) {
|
||||
begin.setAccessible(true);
|
||||
begin.invoke(null);
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Object bone = SceneActions.createBone(state.loader, skeleton, parent, "red_" + (i + 1),
|
||||
(float) (Math.random() * 400.0 - 200.0), (float) (Math.random() * 400.0 - 200.0),
|
||||
red);
|
||||
if (bone != null) made[0]++;
|
||||
}
|
||||
if (commit != null) {
|
||||
commit.setAccessible(true);
|
||||
commit.invoke(null);
|
||||
}
|
||||
SceneActions.refresh(state.loader, skeleton);
|
||||
} catch (Throwable error) {
|
||||
setStatus(state, "+10 Red Bones failed: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
System.err.println("[panel] +10 Red Bones(API) 失败: " + error);
|
||||
}
|
||||
refreshList(state);
|
||||
}
|
||||
});
|
||||
setStatus(state, "made " + made[0] + " red bones under " + SceneObjects.nameOf(parent));
|
||||
refreshList(state);
|
||||
}
|
||||
|
||||
/** 读一次树里的对象列表(读不到就给空表,不抛)。 */
|
||||
private static java.util.List<Object> listQuietly(State state) {
|
||||
try {
|
||||
return SceneObjects.listAllNodes(state.loader);
|
||||
} catch (Throwable error) {
|
||||
return new ArrayList<Object>();
|
||||
}
|
||||
}
|
||||
|
||||
/** 前后列表对比,取第一个新出现的对象。 */
|
||||
@@ -707,16 +730,22 @@ public final class ObjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
/** 骨骼颜色:找类型是 libGDX Color 的字段写进去。 */
|
||||
/**
|
||||
* 骨骼颜色:按映射表写 `Gp` 的 color 字段(混淆名 B,描述符 Lcom/badlogic/gdx/graphics/Color;)。
|
||||
* 表里查不到时才退回"第一个 Color 类型字段"。
|
||||
*/
|
||||
private static void setBoneColor(Object bone, Object color) {
|
||||
try {
|
||||
String obf = obfNameOf("Gp", "Lcom/badlogic/gdx/graphics/Color;", "color");
|
||||
for (Class<?> type = bone.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (java.lang.reflect.Field field : type.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals("com.badlogic.gdx.graphics.Color")) continue;
|
||||
field.setAccessible(true);
|
||||
field.set(bone, color);
|
||||
return;
|
||||
if (field.getType().getName().equals("com.badlogic.gdx.graphics.Color")) {
|
||||
if (obf != null && !field.getName().equals(obf)) continue;
|
||||
field.setAccessible(true);
|
||||
field.set(bone, color);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
@@ -791,12 +820,23 @@ public final class ObjectPanel {
|
||||
/**
|
||||
* 把编辑器的**滚动焦点**抢到面板的列表上(鼠标滚轮才会滚列表)。
|
||||
*
|
||||
* ⚠️ 2026-10-16 已停用(用户选方案 B):不再调用。原因见下面实测——
|
||||
* 它在 Stage 上找"1 个 Actor 参数 + void"的方法当 setScrollFocus,只排除了声明在
|
||||
* `dl`(Actor)/`do`(Group)/`eX`(WidgetGroup) 上的方法;可**编辑器的 Stage 类 `dt`
|
||||
* 自己声明了** `D(Ldl;)V`(字节码就是 `root.addActor(actor)`,即 Stage.addActor),
|
||||
* 于是它成了唯一候选被调用 → ScrollPane 被 Group.addActor 先摘下来再挂到 Stage 根,
|
||||
* 坐标保留局部值 (57,824) → 整块列表(114 行)被画到**窗口左下角**,
|
||||
* 被 ScrollPane 自己的裁剪框裁成 400x400 一小块(就是"列表里的 UI 跑到外面")。
|
||||
* 日志现场:`[panel] 没找到 setScrollFocus(候选 setter 1 个 / getter 0 个)`。
|
||||
* 实测焦点本来就是靠鼠标点一下列表拿到的,不需要这段。
|
||||
*
|
||||
* 识别方式(不猜名字、也不会误调 addActor):
|
||||
* - 只在 **Stage 自己声明的**方法里找(`uk`/`dt`/`lpt6` 这些层),
|
||||
* `addActor` 声明在 Group 上、不在这一层,所以不会被误选;
|
||||
* - 候选 setter = 「1 个参数是 Actor 类型、返回 void」;候选 getter = 「无参、返回 Actor 类型」;
|
||||
* - 调 setter 后看哪个 getter 变成了我们的面板 —— 那就是 setScrollFocus 那一对,日志会写出来。
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
static void grabScrollFocus(final State state) {
|
||||
try {
|
||||
Object stage = SceneObjects.global(state.loader, "uk");
|
||||
@@ -1347,8 +1387,11 @@ public final class ObjectPanel {
|
||||
}
|
||||
try {
|
||||
((Runnable) route[1]).run();
|
||||
Object st = LAST_STATE;
|
||||
if (st instanceof State) grabScrollFocus((State) st);
|
||||
// 这里以前会调 grabScrollFocus(state) 去抢滚动焦点。
|
||||
// 2026-10-16 停用:它会把列表的 ScrollPane 当成 Stage.addActor 的参数传进去,
|
||||
// 结果 ScrollPane 被搬出面板、挂到 Stage 根,列表就画到窗口左下角去了
|
||||
// (证据:dt.D(Ldl;)V 的字节码 = root.addActor;探针实测 root 子控件只剩 2 个)。
|
||||
// 滚动焦点本来就靠鼠标点一下列表拿到,不需要这段。
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[panel] 动作失败: " + error);
|
||||
error.printStackTrace(System.err);
|
||||
|
||||
@@ -901,4 +901,110 @@ public final class SceneActions {
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ==================== 直接模型 API:新建骨骼(不经 UI 菜单、不依赖下一帧) ====================
|
||||
//
|
||||
// 编辑器自己的"新建骨骼"代码就在 `Yi.run()` 里,去掉 UI 后本质是:
|
||||
// new Gp() → 设 name/parent/skeleton/x/y/color → 骨架.D(骨骼) ← GP 上的 (LGp;)V = addBone
|
||||
// → 骨架.StackMapTable() ← 重建索引 → Editor.D(骨骼, true) → VZ.D(骨架, true, true) 重建树
|
||||
// 一次调用可以连建 N 根,最后只刷新一次;外层包 un.D()/un.B() 就是"一次撤销"。
|
||||
|
||||
/**
|
||||
* 直接建一根骨骼(编辑器自己的序列,全部有字节码依据,见 `javap -c UH` 的 D(Gp,Gp,Z,gR,fI)):
|
||||
* new Gp(模板, 父.skeleton, 父) ← 构造器 (Gp, GP, Gp)
|
||||
* name = OG.D(父名字, skeleton.骨骼数组, null) ← 编辑器自己的不重名生成器
|
||||
* skeleton.addBone(bone) ← GP.D(Gp)(字节码:bone.B = size; array.add(bone))
|
||||
* 之后由调用方设 x/y/color 并统一刷新一次。
|
||||
*/
|
||||
public static Object createBone(ClassLoader loader, Object skeleton, Object parent, String name,
|
||||
float x, float y, Object color) {
|
||||
try {
|
||||
Class<?> boneClass = Class.forName("Gp", true, loader);
|
||||
Class<?> skeletonClass = Class.forName("GP", true, loader);
|
||||
Object bone = boneClass.getConstructor(boneClass, skeletonClass, boneClass)
|
||||
.newInstance(parent, skeleton, parent);
|
||||
|
||||
// 名字:优先用编辑器自己的生成器(找不到就用调用方给的名字)
|
||||
String finalName = name;
|
||||
try {
|
||||
Class<?> og = Class.forName("OG", true, loader);
|
||||
Class<?> arrayClass = Class.forName("fI", true, loader);
|
||||
Class<?> slotClass = Class.forName("GU", true, loader);
|
||||
Method gen = findMethod(og, "D",
|
||||
new Class<?>[] {String.class, arrayClass, slotClass}, String.class);
|
||||
Object bones = null;
|
||||
for (Field field : skeletonClass.getDeclaredFields()) {
|
||||
if (field.getType() == arrayClass) {
|
||||
field.setAccessible(true);
|
||||
bones = field.get(skeleton);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Object parentName = null;
|
||||
for (Field field : boneClass.getDeclaredFields()) {
|
||||
if (field.getType() == String.class) {
|
||||
field.setAccessible(true);
|
||||
parentName = field.get(parent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (gen != null && bones != null) {
|
||||
gen.setAccessible(true);
|
||||
Object generated = gen.invoke(null, "bone", bones, null);
|
||||
if (generated instanceof String && !((String) generated).isEmpty()) {
|
||||
finalName = (String) generated;
|
||||
}
|
||||
}
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[crud] 名字生成器跳过: " + error);
|
||||
}
|
||||
|
||||
Method addBone = findMethod(skeletonClass, "D", new Class<?>[] {boneClass}, void.class);
|
||||
if (addBone == null) throw new NoSuchMethodException("GP.D(Gp) addBone");
|
||||
addBone.setAccessible(true);
|
||||
addBone.invoke(skeleton, bone);
|
||||
|
||||
setByNameType(bone, "D", String.class, finalName);
|
||||
setByNameType(bone, "Z", float.class, Float.valueOf(x));
|
||||
setByNameType(bone, "V", float.class, Float.valueOf(y));
|
||||
if (color != null) {
|
||||
setByNameType(bone, "B", Class.forName("com.badlogic.gdx.graphics.Color", true, loader),
|
||||
color);
|
||||
}
|
||||
System.err.println("[crud] createBone ok name=" + finalName);
|
||||
return bone;
|
||||
} catch (Throwable error) {
|
||||
System.err.println("[crud] createBone 失败: " + error);
|
||||
Throwable cause = error.getCause();
|
||||
if (cause != null) System.err.println("[crud] cause: " + cause);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 按字段类型写(用于 D:LGP; / D:LGp; 这种同名不同描述符的坑)。 */
|
||||
static void setByType(Object target, String typeName, Object value) throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getType().getName().equals(typeName)) continue;
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 按"名字 + 类型"写字段。 */
|
||||
static void setByNameType(Object target, String name, Class<?> fieldType, Object value)
|
||||
throws Exception {
|
||||
for (Class<?> type = target.getClass(); type != null && type != Object.class;
|
||||
type = type.getSuperclass()) {
|
||||
for (Field field : type.getDeclaredFields()) {
|
||||
if (!field.getName().equals(name) || field.getType() != fieldType) continue;
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,3 +344,190 @@ macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所
|
||||
|
||||
> 教训也写进技能了:**新确认的映射必须当场写进 `tools/deobf/anchors.tsv` 并同步 `names.tsv` / `src-named`**,
|
||||
> 不许只留在对话里(见 `.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"。
|
||||
|
||||
---
|
||||
|
||||
## 九、权威对照:编辑器自带的 JSON 导出器(2026-10-16)
|
||||
|
||||
**思路**:编辑器导出 Spine JSON 时,写 key 的那段代码里,key 后面紧跟的就是它读的那个字段。
|
||||
所以"JSON key ↔ 混淆字段"的对照不用猜,直接读导出器即可。这一节就是那份对照,**已确认**。
|
||||
|
||||
**导出器是谁**:载荷类 `sB`(构造骨架 JSON 的执行者),两个关键方法
|
||||
|
||||
| 方法 | 干什么 |
|
||||
| --- | --- |
|
||||
| `sB.D(GP, boolean, boolean, gy, StringBuilder)` | 写骨架 JSON:`bones` / `slots` / `ik` / `transform` / `path` / `skins` / `events` / `animations` |
|
||||
| `sB.D(gy, String, Gn, boolean)` | 写一个附件(region / point / mesh / boundingbox / clipping) |
|
||||
|
||||
**怎么拿到明文**(两条独立证据,顺序逐条一致):
|
||||
|
||||
1. `~/Desktop/spine398/spine398.jar` 是可读载荷(`DecAll` 产出:`ldc 密文` 已换成明文、解密调用已换成 `nop`)。
|
||||
`javap -p -c -cp spine398.jar sB` 就能看到 `ldc "rotation"` 紧跟 `getfield Gp.F:F`。
|
||||
2. 未解密的载荷走 `strings.tsv`(`class sB, method D`,281 条):同一 (类, 方法) 内**密文顺序 = 字节码顺序**,
|
||||
与可读副本里的 key 顺序**逐条对得上**(该类的三段解密器 `za$Zr.J` / `aeB$Cp.I` / `aaQ$Wm.x` 各自成组、组内按方法顺序)。
|
||||
|
||||
### 骨骼 `Gp`
|
||||
|
||||
| 字段(带描述符) | JSON key | 备注 |
|
||||
| --- | --- | --- |
|
||||
| `D:Ljava/lang/String;` | name | |
|
||||
| `D:LGp;` | parent | 父骨骼 |
|
||||
| `C:F` | **length** | ⚠️ 之前写的 `Class:F=length` 作废 |
|
||||
| `F:F` | rotation | |
|
||||
| `Z:F` / `V:F` | x / y | |
|
||||
| `Code:F` / `SourceFile:F` | scaleX / scaleY | |
|
||||
| `StackMapTable:F` / `LineNumberTable:F` | shearX / shearY | |
|
||||
| `D:LiC;` | transform | `iC` = 继承模式枚举 |
|
||||
| `String:Z` | skin | 布尔:该骨骼是否覆盖当前 skin |
|
||||
| `B:Lcom/badlogic/gdx/graphics/Color;` | color | |
|
||||
|
||||
> 同时**作废**了原先"静姿态那一组 `D/B/L/I/S/C` = setupX…"的推断:`C:F` 被导出器证明是 `length`,
|
||||
> 那一组整体不成立,现在这些 float 的语义**未定**(宁可显示混淆名,也不写错名)。
|
||||
|
||||
### 插槽 `GU`
|
||||
|
||||
| 字段(带描述符) | JSON key | 备注 |
|
||||
| --- | --- | --- |
|
||||
| `D:Ljava/lang/String;` | name | |
|
||||
| `D:LGp;` | bone | 所属骨骼 |
|
||||
| `B:Lcom/badlogic/gdx/graphics/Color;` | **color** | ⚠️ 是 `B`,不是 `D`(之前写错了) |
|
||||
| `I:Lcom/badlogic/gdx/graphics/Color;` | dark | 二色染色的暗色 |
|
||||
| `I:Z` | —("dark" 的开关) | 写 `dark` 之前先看它 |
|
||||
| `B:LGn;` | **attachment** | ⚠️ 是 `B`,不是 `D`(之前写错了) |
|
||||
| `D:Liz;` | blend | `iz` = 混合模式枚举 |
|
||||
|
||||
### 附件(基类 `Gn` + 各子类)
|
||||
|
||||
| 类 | 字段(带描述符) | JSON key |
|
||||
| --- | --- | --- |
|
||||
| `Gn` | `D:Ljava/lang/String;` | name(附件名) |
|
||||
| `GO`(图片) | `D:Color` / `D:F` / `B:F` / `L:F` / `I:F` / `S:F` / `C:F` / `Z:F` / `B:Ljava/lang/String;` | color / x / y / scaleX / scaleY / rotation / width / height / path |
|
||||
| `GE`(点) | `B:Color` / `D:F` / `B:F` / `L:F` | color / x / y / rotation |
|
||||
| `Gx`(网格) | `B:Color` / `B:Ljava/lang/String;` / `D:Lga;` / `D:Lhp;` / `B:LHl;` / `L:I` / `D:Lgf;` / `StackMapTable:Z` / `D:F` / `B:F` | color / path / uvs / triangles / vertices / hull(=L/2) / edges / deform / width / height |
|
||||
| `Gx`(linkedmesh 分支) | `D:LGx;` / `D:LGT;` / `D:LHi;` / `D:Ljava/lang/String;` | parentMesh / skin / parentPlaceholder / parentName |
|
||||
| `Gr`(包围盒) | `I:Lga;` / `B:LHl;` / `B:Color` | vertexCount / vertices / color |
|
||||
| `Gs`(裁剪) | `I:Lga;` / `B:LHl;` / `B:Color` / `B:LGU;` | vertexCount / vertices / color / end(结束插槽) |
|
||||
|
||||
### 约束(顺带一起确认)
|
||||
|
||||
| 类 | 字段 → key |
|
||||
| --- | --- |
|
||||
| `Gw`(IK) | `D:String`=name、`D:LGp;`=target、`D:F`=mix、`L:F`=softness、`F:Z`=uniform、`S:Z`=stretch、`Z:Z`=compress |
|
||||
| `GX`(变换) | name、target(`B:LGp;`)、rotation(`SourceFile:F`)、local(`S:Z`)、relative(`C:Z`)、x(`L:F`)、y(`I:F`)、scaleX(`StackMapTable:F`)、scaleY(`init:F`)、shearY(`LineNumberTable:F`)、rotateMix(`S:F`)、translateMix(`D:F`)、scaleMix(`Z:F`)、shearMix(`F:F`) |
|
||||
| `GD`(路径) | name、target(`D:LGU;` 是**插槽**)、positionMode(`D:Ljm;`)、spacingMode(`D:Ljo;`)、rotateMode(`D:Ljn;`)、rotation(`Z:F`)、position(`D:F`)、spacing(`V:F`)、rotateMix(`S:F`)、translateMix(`L:F`) |
|
||||
|
||||
### 落表位置
|
||||
|
||||
以上全部写进了 `tools/deobf/anchors.tsv`(末尾"【权威来源】JSON 导出器"一节),再合并进
|
||||
`tools/deobf/names.tsv`(297 条),面板 Inspector 会按"真名(混淆名)"显示。
|
||||
重命名载荷也已同步产出:`/tmp/spinework/payload-named.zip`(4176 个类,5247 处成员引用)。
|
||||
|
||||
---
|
||||
|
||||
## 十、"操作类" API(2026-10-16 热更实测通过)
|
||||
|
||||
这一节是"不重启、纯用编辑器自身 API 加一个按钮:弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建
|
||||
15 根骨骼(每根 1 插槽 1 附件)"那次任务里用到的 API。**每条都真跑过并核对过结果**,不是从字节码推的纸面结论;
|
||||
字节码出处也一并写上。
|
||||
|
||||
### 建对象
|
||||
|
||||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||||
| --- | --- | --- |
|
||||
| `UH.D(LGU;LGp;Z;LfI;)LGU;` | **addSlot**(加插槽) | 实测建出插槽。⚠️ 它会把**模板插槽整个复制**,连模板插槽上挂的那份附件一起复制 —— 新插槽上于是挂着模板的附件(本次"插槽上的附件变换没改"就是这个原因) |
|
||||
| `UH.D(LGn;LGU;ZZLfI;)LGn;` | **addAttachmentCopy**(按模板复制一个附件挂到插槽) | 内部 `Gn.D(LGP;)LGn;` 复制模板附件,再登记到骨架附件表 `GP.L` |
|
||||
| `GP.D(LGp;)V` | **addBone** | 字节码 `bone.B = array.size; array.add(bone)` |
|
||||
| `OG.D(String;LfI;LGU;)String` | **uniqueName**(编辑器自己的不重名生成) | 实测 `OG.D("bone",骨骼数组,null)` 返回 `bone7` |
|
||||
| `GU.D(LGn;Z)V` | **setAttachment**(设插槽当前附件) | 字节码:第二参=是否记撤销;按 `Editor.V`(animate/setup)**只写 `D:LGn;` 或 `B:LGn;` 其中一个** → 想让两种模式都覆盖就两个字段都写 |
|
||||
| `VZ.D(LGP;ZZ)V` | **refresh**(重建层级树节点) | 每批建完都调,树里立刻能看到 |
|
||||
| `un.D()V` / `un.B()V` | **undoBegin / undoCommit** | 撤销帧的开始/提交 |
|
||||
| `un.Z()V` | **undo(撤销一步)** | 实测:`up` 的撤销栈 23→22、redo 栈 0→1、场景对象数同步回退。误删骨骼就是靠它连点撤回来的。同族的 `un.S()/un.C()/un.V()/un.I()` **不是**撤销(分别=跳历史下标/置标志/按下标重放/无效果) |
|
||||
|
||||
### 名称输入面板(编辑器自带对话框)
|
||||
|
||||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||||
| --- | --- | --- |
|
||||
| `xl`(抽象类) | **名称输入对话框基类**(标题/正文/名字框/OK/Cancel) | 结构实测:标题 + 正文 Label + `Kp` 名字框 + OK/Cancel 两个 ImageTextButton |
|
||||
| `xl.L(Ljava/lang/Object;)V` | **onOkCancel** | 收到 `Boolean`:TRUE=OK(读名字、查重、调下面的抽象方法);FALSE=Cancel 直接 return |
|
||||
| `xl.L(Ljava/lang/String;)V` | **onNameEntered(抽象)** | OK 且名字合法时被调用,参数=输入的名字。想拿名字就子类化它(本次用 ASM 现场生成子类) |
|
||||
| `xl.D(Ldt;)LvH;` | **show** | ⚠️ **光调它窗口不会挂进 Stage**(实测 parent 一直 null、屏幕上什么都不出现),必须再显式 `dt.D(Ldl;)V`(Stage.addActor) |
|
||||
| `xl.D:LKp;` | **nameField**(名字框) | `Kp` 是 TextField 子类:`B()String`=getText、`B(String)V`=setText |
|
||||
| `vH.B(Ljava/lang/Object;)V` | **okPressed** | `javap -c vH` 里唯一调 `L(Object)` 的地方=OK 按钮回调 |
|
||||
| `dT.D(Ldt;Ldk;)LdT;` | show 内部实现 | `vH.D(dt)` 里 `invokespecial` 到它;里面 `stage.D(this)V` 就是 Stage.addActor |
|
||||
|
||||
### Actor / Stage / WidgetGroup(定位、挂载、删除)
|
||||
|
||||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||||
| --- | --- | --- |
|
||||
| `dt.D(Ldl;)V` | **Stage.addActor** | 字节码 `this.D:Ldo;.addActor(actor)`(root.addActor)。用它把对话框挂上屏幕。历史上"面板列表被搬出面板"也是它干的(把 Actor 从原父级摘走) |
|
||||
| `dl.D:Ldo;` / `dl.D:Ldt;` | **parent / stage**(字段) | Actor 上两个同名字段,必须**按类型**取;`dl.D()Ldt;`=getStage |
|
||||
| `dl.b_()Z` | **remove** | ⚠️ `eX.b_()V`(invalidate)同名不同描述符;`getMethod("b_")` 会取到 void 那个 → "调了删除却没删掉"(本次踩到,改成按返回类型 boolean 挑才对) |
|
||||
| `eX.V(Z)V` | **setFillParent** | 与 `et.C(Z)V`=setClip、`et.B(Z)V`=setDebug 区分开 |
|
||||
| `dl.c_()F` `dl.d_()F` `dl.L()F` `dl.I()F` `dl.S()F` `dl.C()F` | **getX / getY / getWidth / getHeight / getTop / getRight** | 全部按 `javap -p -c dl` 对出来(c_=x、d_=y、L=w、I=h、S=y+h、C=x+w)。⚠️ `et`(Table) 自己声明了 `int L()`/`int I()`(列数/行数)→ 按名字取要**同时按返回类型 float 过滤** |
|
||||
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 时是"以 (x,y) 为中心"**(不按 left/right 就按 center 算)→ 摆屏幕正中直接传 Stage 中心即可 |
|
||||
|
||||
### 混合模式枚举
|
||||
|
||||
| 字段 | 真名 | 依据 |
|
||||
| --- | --- | --- |
|
||||
| `iz.D` / `iz.B` / `iz.L` / `iz.I` | **normal / additive / multiply / screen** | `iz.<clinit>` 里依次 `new iz("normal",0,770,1,771)`、`("additive",1,770,1,1)`、`("multiply",2,774,774,771)`、`("screen",3,1,1,769)`,常量名是常量池明文 |
|
||||
|
||||
### 面板自己的接口(复用,不重复造)
|
||||
|
||||
| 方法 | 用途 |
|
||||
| --- | --- |
|
||||
| `com.spineview.ObjectPanel.wire(ClassLoader, Object, Runnable)` | 把新按钮接上点击(轮询 `isChecked` + 路由线程)——新按钮直接复用 |
|
||||
| `com.spineview.SceneObjects.listAllNodes(ClassLoader)` / `selected(ClassLoader)` / `nameOf(Object)` | 罗列场景对象 / 当前选中 / 取名字 |
|
||||
| `com.spineview.SceneActions.delete(ClassLoader, Object)` | 走编辑器删除流程(自带撤销),清理误建对象时用 |
|
||||
|
||||
> 另外踩到两个**跟 API 无关但会浪费半天**的坑,记在这里:
|
||||
> 1) **HotSpot 同名 agent 类只加载一次** —— 改了 agent 代码再 attach 同一个类名,跑的还是旧代码;每次改代码都要换类名。
|
||||
> 2) 编辑器字体**没有中文字形**,对话框/按钮文字写中文会显示成乱码 —— 一律用英文。
|
||||
|
||||
### 对话框"显示不全 / 先闪左下角"的两个修正(2026-10-16 实测)
|
||||
|
||||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||||
| --- | --- | --- |
|
||||
| `eX.StackMapTable()F` / `eX.init()F` | **getPrefWidth / getPrefHeight** | `eX`(WidgetGroup) 六个 Layout 方法按声明顺序 = minW/minH/prefW/prefH/maxW/maxH。实测对话框 pref=**399x166**,但窗口刚建出来时是默认 **150x150** → 正文和按钮被裁。修法:`Actor.width/height = max(pref, min, 下限)` 再 `eX.b_()V`(invalidate) |
|
||||
| `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) |
|
||||
| `eX.b_()V` | **invalidate** | 让内容按新尺寸重新布局(和 `dl.b_()Z`=remove 同名不同描述符) |
|
||||
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 → 以 (x,y) 为中心**。"先出现在左下角、再跳到中间"的原因就是:先 `addActor`(默认 (0,0))渲染了一帧,350ms 后才居中。修法:**尺寸 + 居中都在同一次渲染回调里做完**(本次实测 399x170 @ (1665.5,937) → 中心正好 (1865,1022) = 3730x2044 的正中) |
|
||||
@@ -0,0 +1,128 @@
|
||||
# 阶段五:把面板接到"真实数据"上(对象增删改查 / 属性读取与改写 / 命名还原)
|
||||
|
||||
本文记录阶段五**已经做成的**、**实测到的数字**、**踩过的坑**,以及**还没做完的**。
|
||||
详细的"混淆名 ⇒ 真名"对照在 [`混淆名对照表.md`](混淆名对照表.md),交互链路在 [`编辑器交互与数据API.md`](编辑器交互与数据API.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一、阶段五做成了什么
|
||||
|
||||
### 1. 面板(Spine View)长这样(三段式)
|
||||
|
||||
| 段 | 内容 | 高度 |
|
||||
| --- | --- | --- |
|
||||
| 上 | `Refresh / New Bone / New Slot / New BBox / New Path / Delete / Rename / Inspect / Set Value` + 名字输入框 | 由 `spineview-fit` 线程设置(当前按 600 的比例) |
|
||||
| 中 | 列表 / Inspector(对象清单 或 选中对象的字段+方法) | 同上(1400 的比例) |
|
||||
| 下 | 日志(最近 6 条按钮动作提示) | 同上(400 的比例) |
|
||||
|
||||
面板文字**一律英文**(编辑器只装了拉丁字形,中文渲染成方块)。
|
||||
|
||||
### 2. 能做的事(都已实测跑通)
|
||||
|
||||
| 能力 | 实现 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| **列出**场景对象 | 走层级树的节点;`SceneObjects.listAllNodes()`(树里 28 个对象:skeleton/bone/slot/attachment + Constraints/Draw Order/… 固定根) | 面板列表与编辑器 Tree 一致 |
|
||||
| **选中**对象 | `VZ.D(Tree.Node, boolean)`(`SceneObjects.select`) | 编辑器 Tree 里对应行被高亮(截图已确认) |
|
||||
| **读**当前选择 | `VZ.B()` = `Selection.getLastSelected().object` | 面板 "selected: bone \"bone3\"" 与编辑器一致 |
|
||||
| **增**对象 | 先选中父级 → 跑编辑器命令 `New Bone` / `New Slot` / `New BBox` / `New Path` | 新建出 `bone7`、`bone8`(骨骼自动编号)等 |
|
||||
| **删**对象 | `un.D()` → `对象.L()` → `un.B()` → `VZ.D(骨架,true,true)` 重建树 | 删除后树与面板同步(父骨骼删除会连子骨骼一起删) |
|
||||
| **改**名字 | 同上,`对象.D(新名字,true)` + 重建树 | `bone3` → `panel_test` 生效 |
|
||||
| **改**属性(测试用) | Inspector 里点字段行 → 值读进输入框 → `Set Value` 直接写模型字段(带撤销) | 写回后值与编辑器属性面板同步 |
|
||||
| **输入框自动化** | 有些 `New X` 会弹"输入名字"对话框:找到"带 TextField 且有 `L(Object)V` 回调"的对话框对象 → 填名字 → 直接调 `L(true)`(等于点 OK),不弹给用户 | `[crud] 已自动填写对话框并点 OK` |
|
||||
| **跟随编辑器选择** | 后台线程 600ms 轮询 `VZ.B()`,变了就自动 Inspect | 日志 `[panel] Inspected slot "mega_dg6": 48 entries` |
|
||||
| **鼠标滚轮** | 点面板按钮后把 Stage 的 scroll focus 抢到列表(`grabScrollFocus`) | 用户实测:"拖动/点列表后滚动条出现、滚轮可用" |
|
||||
|
||||
### 3. 命名还原(把 `D/B/L` 变成有意义的名字)
|
||||
|
||||
| 产物 | 内容 |
|
||||
| --- | --- |
|
||||
| `tools/deobf/Renamer.java` | ASM `Remapper`:按映射表重命名**成员**(不改类名 —— 载荷的 indy 靠加密串里的原名反射查找) |
|
||||
| `tools/deobf/anchors.tsv` | 手动/实测锚点(含依据注释) |
|
||||
| `tools/deobf/anchors-libgdx.tsv` | `MapMethods` 对上游 **gdx-1.11.0** 的**唯一命中**(`= `)条目 |
|
||||
| `tools/deobf/anchors-libgdx-unconfirmed.tsv` | 只给了候选(`~n/m`)的条目 —— **不可信,别用** |
|
||||
| `tools/deobf/names.tsv` | 合并结果(当前 **232 条**):确定 + 手动/实测 |
|
||||
| `~/Desktop/spine398/src-named/` | 改名后的反编译源码(每个类单独反编译、`class_<载荷名>.java` 命名) |
|
||||
| `tools/decrypt/DumpSites.java` | 逐 invokedynamic 站点解出"真实类.方法 **+ 描述符**"(带描述符是关键) |
|
||||
|
||||
**面板 Inspector 现在会按映射表显示真名**,格式 `真名(混淆名)`,例如:
|
||||
|
||||
```
|
||||
float scaleX(L) = 1.0 float width(C) = 166.0
|
||||
float rotation(S) = 0.0 float height(Z) = 87.0
|
||||
```
|
||||
|
||||
### 4. 本轮实测出来的字段语义(都写进了 `anchors.tsv`)
|
||||
|
||||
| 对象 | 字段 | 含义 | 依据 |
|
||||
| --- | --- | --- | --- |
|
||||
| `Gp`(骨骼) | `Z/V` | 当前 x/y | 面板 Translate(-168,-116) ↔ Z=-168.00009/V=-116.0 |
|
||||
| | `F` / `Code` / `SourceFile` | 当前 rotation / scaleX / scaleY | 面板 Rotate 341.456 ↔ F=33.676704;Scale(0.8799,0.572) ↔ Code/SourceFile |
|
||||
| | `StackMapTable` / `LineNumberTable` | 当前 shearX / shearY | 面板 Shear(-52.221,-28.63) |
|
||||
| | `D/B/L/I/S/C` | 静止姿态(setup) 的 x/y/rotation/scaleX/scaleY/shear(推断) | 默认值 0,0,0,1,1,0;未改动时与上面那组相等 |
|
||||
| | `Class` / `Ljava` / `java` | length / 世界缩放 X / 世界缩放 Y | 面板 Length=0.0 ↔ Class=0.0;Ljava/java 与 Code/SourceFile 相等 |
|
||||
| `GO`(附件) | `S/L/I/C/Z` | rotation / scaleX / scaleY / width / height | 面板 Region 49°/1.997/1.335/157/154 一一对应 |
|
||||
| `GU`(插槽) | `String D` / `Gp D` / `Gn D` / `Color D` | 名字 / 所属骨骼 / 附件 / 颜色 | 面板 Slot 只有 Name+Color;树节点名一致 |
|
||||
| 通用 | `Table` 上 `D(Ldl;)LdR;`=`add`、`I()LdR;`=`row`、`SourceFile()`=`clearChildren` | | 指纹 + 目录铺满/换行实测 |
|
||||
|
||||
---
|
||||
|
||||
## 二、踩过的坑(按"代价从大到小")
|
||||
|
||||
1. **`VZ.D(Object)` 不是"选中"** —— 它内部是 `Selection.L()`(去掉选中项)。真正"设成选中"的是 `VZ.D(Tree.Node, boolean)`。这让我前面所有增删改都"执行了但没生效"。
|
||||
2. **动模型的代码必须在编辑器渲染线程** —— `OG.I()` 有线程断言,非渲染线程直接抛 `RuntimeException`;要用编辑器的 `OG.B(Runnable)` 切过去。
|
||||
3. **不能在渲染线程里 sleep** —— 编辑器有些创建是**下一帧**做的;占着渲染线程睡 300ms,那一帧永远不来,对象就建不出来。
|
||||
4. **`MapMethods` 的候选(`~n/m`)不是结论** —— 我把它们混进 `names.tsv` 当确定名字用,结果"拿的方法不对"。现在拆成 `anchors-libgdx.tsv`(只放 `=` 唯一命中)与 `anchors-libgdx-unconfirmed.tsv`。
|
||||
5. **同名不同描述符的坑就在我们自己代码里** —— `Cell.fill()` 的载荷名是 `D`,但 Cell 上有多个无参 `D()`,`getMethod("D")` 会取错;`fillY="L"` 是"按形状猜"的,没验证过(很可能取错)。
|
||||
6. **`ScrollPane.B(ZZ)V` 是 `setScrollingDisabled`,不是 `setForceScroll`** —— 我传了 `(false,true)`,等于**把纵向滚动关掉了**(现象:列表滚不动)。`setForceScroll` 是 `I(ZZ)V`。
|
||||
7. **滚轮不生效的根因是焦点** —— 给列表 ScrollPane 挂事件监听后,滚轮事件收到 **0 条**:焦点在画布上。解法:点面板后 `Stage.setScrollFocus(面板)`;找这个方法必须用 `getMethods()`(它声明在接口 `LPt9` 里,`getDeclaredMethods` 扫不到)。
|
||||
8. **`run.sh` 漏了 `import os`** —— python 那句报 NameError,**编辑器根本起不来**;而我用 `>/dev/null` 把错误吞了,白排查了好几轮。
|
||||
9. **`nohup … &` 启动会被会话回收** —— 必须 `subprocess.Popen(..., start_new_session=True)`,否则我这边一次工具调用结束,编辑器就被收掉(你几次看到"进程没了"就是这个)。
|
||||
10. **面板根表格的高度被父容器限制** —— `fillParent` 是"复制父容器尺寸",而父容器又按我们的内容算 → 互相依赖;结果 panel 永远等于自己算出来的高度(实测 root≈1360,停靠框 1944)→ 底部固定留一条空带(你说的"区域1")。**从面板内部无解**,只能用 `expand` 或把停靠栏拖大。
|
||||
11. **热更(换容器重建面板)把面板搞成空白** —— 那版已 `git revert`;要做得换一种容器(能让 dock 正确布局的 WidgetGroup)。
|
||||
12. **macOS 文件系统大小写不敏感** —— 一次反编译整包时 `GP.java`/`Gp.java`、`Yr.java`/`yR.java` 互相覆盖,导致"有些类像没改名"。现在改成每个类单独反编译 + `class_<载荷名>.java`。
|
||||
13. **`Table.setDebug(true)` 的红/绿线是调试描边**,不是布局错误;它给每层表格的每个 cell 各画一圈(红=根表格三段,绿=内部格)。
|
||||
14. **我自作主张加过"分页"**(你没要求)—— 已回滚,并按你说的把纪律写成项目级技能 `.codex/skills/scope-discipline`。
|
||||
|
||||
---
|
||||
|
||||
## 三、API 规模统计(载荷里的"面")
|
||||
|
||||
| 分组 | 类 | 方法 | public 方法 |
|
||||
| --- | --- | --- | --- |
|
||||
| LWJGL(保名) | 629 | 9564 | 5444 |
|
||||
| Spine 编辑器(保名,`editor/**`) | 57 | 312 | 184 |
|
||||
| libGDX(保名那部分) | 52 | 545 | 462 |
|
||||
| jglfw + libimagequant | 7 | 169 | 150 |
|
||||
| reflectasm + 其它第三方 | 13 | 98 | 55 |
|
||||
| **默认包(类名被混淆)** | **3418** | **15782** | **9918** |
|
||||
| 合计 | 4176 | 26470 | 16213 |
|
||||
|
||||
默认包那 3418 个是**混合体**:用"方法描述符多重集 + 上游 gdx-1.11.0 指纹"能较有把握认出 **~55 类 / ~907 方法**属于 libGDX(你举的"设置文字/颜色"在这层),**其余 ~3363 类 / ~14875 方法**是编辑器自有业务代码(Tree 视图、模型、命令系统、属性面板、导出…)。这只是"正例识别",真实环境自带会比 907 略多。
|
||||
|
||||
另外:invokedynamic 调用点 **55637** 个(`~/Desktop/spine398/sites.tsv`,已解出真实目标+描述符);编辑器注册的菜单命令 100 多条(`hotkeys.txt`)。
|
||||
|
||||
---
|
||||
|
||||
## 四、怎么跑(当前唯一的正确入口)
|
||||
|
||||
```bash
|
||||
cd "/Users/tianmokeji/Desktop/临时/spine加功能"
|
||||
JAVA_HOME=/tmp/jdk-17.0.20.1+1/Contents/Home ./viewpanel/build.sh # 编译
|
||||
./viewpanel/run.sh # 先杀旧实例再启动(约 20s)
|
||||
# 或双击 viewpanel/重启编辑器.command / spine-lab/启动实验室Spine.command(后者已修好,内部同样调 run.sh)
|
||||
tail -f /tmp/spineview.log # 日志([panel]/[view]/[crud] 前缀)
|
||||
```
|
||||
|
||||
调试开关:`-Dspine.probe=objects|panels|select|crud|matrix|panel`(探针)、`-Dspine.runCommand="New Bone"`(跑一条编辑器命令)、`-Dpanel.h=<像素>`(写死面板高度,默认 auto=读 Stage)。
|
||||
|
||||
---
|
||||
|
||||
## 五、还没做完 / 没验证的(别当成已完成)
|
||||
|
||||
1. **"列表控件铺满红框"**:要裁剪+滚动就不能铺满(ScrollPane 里 widget 按自己内容大小);要铺满就得去掉 ScrollPane(会溢出)。两条路都试过,**取舍未定**。
|
||||
2. **新确认的 `fillY="L"`、`CELL_FILL="D"`**:`D` 在映射表里是唯一命中的 `fill`,`L` 从没验证过 —— 列表没被纵向拉伸很可能就是这里取错了方法。
|
||||
3. **编辑器自有类(那 3363 个)的名字还原**:只做了 libGDX 层与少数手工锚点;业务层(`VZ`/`GP`/命令系统…)还没命名。
|
||||
4. **热更**:回滚了;要做就得换容器方案。
|
||||
5. **`GO/GU` 里还有一批未确认字段**(`boolean D/B/L/I/S/C/Z/V/F/Code`、`int D`、`Color B/L/I`…)。
|
||||
6. **调试描边常开**(`ObjectPanel` 里搜 `TABLE_SET_DEBUG` 可关),还没做开关。
|
||||
7. **导出链路**:现阶段未动(`Export` 菜单动作可用,见 `编辑器交互与数据API.md` §6)。
|
||||
Reference in new issue
Block a user