Compare commits

...
12 Commits
Author SHA1 Message Date
tianmo 6580611012 动画面板:新增两个动画测试按钮 + 把动画模型 API 全部落进对照表
## 功能(面板源码,重启后仍在)
- 新增 AnimationTools:
  - Animate Random Props:随机骨骼/插槽/附件 → ani_api_test
    旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)
    插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
  - Curve Test 6 Modes:curve_test,6 根骨骼各一条旋转轴(0°→45°,0f/30f),
    分别 linear / curve 平滑(0.25,0,0.75,1) / stepped / ease-in(0.42,0,1,1) /
    ease-out(0,0,0.58,1) / 对称自动(0.25,0.25,0.75,0.75)
- ObjectPanel.build 里挂载这两个按钮,复用面板自己的 wire() 点击轮询

## 对照表(tools/deobf/anchors.tsv + 混淆名对照表.md 第十一节)
- GP.I=animations、GP.D:LFT;.D:F=fps、Gl=动画(name/skeleton/timelineGroups/register)
- Gl.D/B/L/I(Gp)=旋转/位移/缩放/斜切;Gl.D/B(Gp,GU)=插槽颜色/附件;Gl.D()=绘制顺序
- FH=组;FI=时间轴(runtimeTimelineClass、keys、findKey、insertKey、invalidateRuntime)
- FN=关键帧(time、curve、c2x/c2y/c3x/c3y、setAngle/setX/setY、Color、Gn)
- FV=曲线枚举(linear/curve/stepped;'自动'=curve+控制值)
- 踩坑记录:
  * 关键帧数值写在 FN.D()/B()/L() 的 float 字段,**不是** FN.D():ga(ga 是 deform 的),
    否则会出现'有帧但运行时不变'
  * 编辑器时间轴刻度就是**帧**,关键帧时间写 0/30,不要按 1 秒=30 帧换算
  * FI.D(float,int) 建帧要求时间轴已有关键帧(先有鸡问题)
  * fI.removeRange 两个 (int,int) 重载都不缩容 → 清空动画直接换新 fI
names.tsv 合并成 380 条,payload-named.zip 重产(7271 处成员引用)。

验证:全部在运行中的编辑器里逐条读回(时间轴/关键帧/曲线枚举/控制值),
重启后 pid 94663 单实例,日志确认 '[animtool] 两个动画按钮已加入'。
2026-10-17 12:57:17 +08:00
tianmo 8df990a058 把 New 15 Bone+Slot+Att 按钮移植进面板源码(重启后仍在)
- 新类 com.spineview.NewFifteen:按钮 → 编辑器自带名称面板(xl + ASM 子类拿名字) → 按名字匹配附件
  → 当前选中骨骼下建 15 根骨骼,每根 1 插槽 1 附件(该名字的副本)
  骨骼 x/y 0~500 随机、rotation/scale/shear=0/1/0;插槽 alpha 0.5~1、blend normal/additive 随机;
  附件 x/y/rotation=0、scaleX/scaleY=1(并替换掉 ADD_SLOT 带过来的模板附件)
  弹窗按 pref 尺寸(>=360x170)撑开、同一次渲染回调里居中
- ObjectPanel.build 里挂一行按钮(复用 ObjectPanel.wire 的点击轮询)
- build.sh 的 javac 加 -cp ../tools/asm/asm.jar(NewFifteen 现场生成 xl 子类需要 ASM)
依据:tools/deobf/anchors.tsv + 混淆名对照表.md(全部字节码/实测确认)
2026-10-17 10:25:22 +08:00
tianmo 64361e389e 保留本次功能的最终实现源码(热更 agent 第 10 版)
内容:不重启、纯编辑器自身 API 加一个按钮 → 弹名称输入面板(编辑器自带 xl 对话框,ASM 现场生成子类拿名字)
→ 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼,每根 1 插槽 1 附件(输入的附件名):
- 骨骼 x/y 0~500 随机、rotation/scale/shear = 0/1/1/0/0
- 插槽 alpha 0.5~1 随机、blend normal/additive 随机
- 附件在插槽上 x/y/rotation=0、scaleX/scaleY=1(并换掉 ADD_SLOT 带来的模板附件)
- 弹窗按 pref 尺寸(399x170)撑开、同一次渲染回调里居中(不再先闪左下角)
同期其余临时件(build*/、*.jar、实验探针源码)已移出到 /tmp/spineview-tools-live-backup-1016-1933(可恢复)
2026-10-16 19:33:51 +08:00
tianmo 0a2da7f11b spine修改 2026-10-16 19:32:49 +08:00
tianmo f40979f965 对照表:补 WidgetGroup 尺寸读取(min/pref/maxW/H) + 对话框显示不全/闪左下角的两个修正
- 弹窗建出来默认 150x150,正文和按钮被裁 → 显式设 width/height=max(pref,min,下限) 再 invalidate
- '先左下角再跳中间' = addActor 后过一帧才居中;改成尺寸+居中在同一次渲染回调里做完
names.tsv 338 条,payload-named.zip 已重产(5617 处引用)
2026-10-16 19:31:04 +08:00
tianmo 42b40f6f65 对照表:补上'操作类'API(加插槽/加附件/加骨骼/不重名/撤销/名称输入面板/Actor 几何),全部来自热更实测
这次任务(不重启,纯编辑器自身 API:按钮→名称面板→按名字匹配附件→当前选中骨骼下建 15 根骨骼,每根 1 插槽 1 附件)
用到的 API 全部落表,含踩坑:
- ADD_SLOT 会把模板插槽连它自带的附件一起复制('插槽上的附件变换没改'就是这个)
- xl.D(dt) 不会把对话框挂进 Stage,必须再 dt.D(dl)V(Stage.addActor)
- dl.b_()Z=remove 与 eX.b_()V=invalidate 同名不同描述符,按名字取会删不掉
- et(Table) 也声明 int L()/I(),按名字取 Actor 几何必须按返回类型 float 过滤
- iz 枚举顺序 normal/additive/multiply/screen 取自 clinit
- un.Z()=撤销(实测栈 23->22、对象数回退)
names.tsv 合并成 332 条,payload-named.zip 已重新产出(5605 处引用)
2026-10-16 19:26:25 +08:00
tianmo b8e84c7aa5 停用 grabScrollFocus:它把列表的 ScrollPane 当 Stage.addActor 的参数传进去,导致列表被搬出面板画到窗口左下角
现象:点过面板按钮后,列表(Inspector 114 行)画在窗口左下角一小块里,面板中段只剩红/绿描边。
原因(探针实测 pid 59699):
  - 面板根表格只剩 2 个子控件(按钮块 + 日志 Label),ScrollPane 不见了;
  - ScrollPane 的父级变成 do[0,0,0x0](Stage 根),绝对坐标 (57,824);
  - grabScrollFocus 在 Stage 上找「1 个 Actor 参数 + void」当 setScrollFocus,
    只排除 dl/do/eX;但编辑器的 Stage 类 dt **自己声明** D(Ldl;)V(字节码 = root.addActor),
    于是唯一候选就是 addActor,被 invoke 后 Group.addActor 先把 ScrollPane 摘下来再挂到 Stage 根。
修复(用户选方案 B):不再调用 grabScrollFocus(滚动焦点本来就靠鼠标点列表拿到)。
复验(新进程 pid 60682,同一探针):root 子控件 = 3,ScrollPane 父级 = root,
绝对坐标 (2115,830) 回到停靠框内。
2026-10-16 18:47:34 +08:00
tianmo 7f06c6744a 新增阶段五文档:面板接真实数据(增删改查/属性读写/Inspector 真名翻译/滚动焦点/统计)+ 14 条踩坑点 + 未完成清单 2026-10-16 18:46:16 +08:00
tianmo bb817b85e5 映射表:用编辑器自带的 JSON 导出器(sB)反查字段含义(权威对照)
依据:导出 JSON 时每个 key 后面紧跟被读的字段,所以 key 名就是字段语义。
- 可读载荷 spine398.jar(DecAll 产出)javap -c sB 能直接看到 key 紧跟 getfield;
- 未解密载荷用 strings.tsv(class sB, method D) 的密文顺序核对,281 条顺序逐条一致(三段解密器各自成组)。
新增:Gp(骨骼) length/transform/skin;GU(插槽) color/dark/attachment/blend(注意是 B 不是 D);
各附件 GO/GE/Gx/Gr/Gs 与附件基类 Gn;约束 Gw/GX/GD。
修正:删掉被导出器证伪的 Gp.Class=length 与'静姿态 setup 组'推断(C:F 就是 length)。
同步:names.tsv 合并成 297 条,payload-named.zip 重新产出。
2026-10-16 18:30:03 +08:00
tianmo 7f519edc67 热更(不重启)整理新建附件:用编辑器自身 API 重置图片附件变换 + 删除占位(skin)附件
- 确认 GO 的字段/入口:x=D(float)、y=B(float)、rotation=S、scaleX=L、scaleY=I、width=C、height=Z;
  GO.D(FFFFF)V 就是编辑器自己的 setTransform(x,y,rotation,scaleX,scaleY)。
  依据:载荷 JSON 写入器 sb_0.java:669-675 + javap -c GO(两条独立证据),
  width/height 与 mega_dg4.png 真实像素 157x154 对上。
- 确认删除入口:Gn.L()V => skeleton.removeAttachment(this,true,true);GP.L=附件表、GP.Z=插槽表;
  占位附件是载荷类 Hi(hi_2.java 的 I() 返回 "placeholder")。
- 实机(JDK8 attach,进程 56177 全程没重启):20 个新建插槽的图片附件 x/y/rotation 归 0、scale 归 1;
  20 个 Hi 占位附件删除;骨架附件表 43 -> 23;逐个核对删除占位没连带删掉图片附件。
- 顺带把 tools/live(attach 热更工具源码)入库,jar/build 加进 .gitignore。
2026-10-16 18:26:40 +08:00
tianmo 0886ba647c 补回误删的 listQuietly(上一次改动把编译搞坏了,编辑器起不来);+10 Red Bones 暂用已验证可行的一根一次路径 2026-10-16 18:07:36 +08:00
tianmo 4000a06d61 +10 Red Bones 修两处:①每建一根等一帧(编辑器下一帧才真建,同帧连调只生效一次)②颜色按映射表写 Gp 的 color 字段(混淆名 B),不再取'第一个 Color 字段' 2026-10-16 17:57:06 +08:00
70 changed files with 9367 additions and 53 deletions

No files matched your search

+3
View File
@@ -25,6 +25,9 @@ rea/
viewpanel/build/ viewpanel/build/
viewpanel/compat-classes/ viewpanel/compat-classes/
*.class *.class
# 热更(attach)小工具:只留源码,jar 和编译输出是临时的
tools/live/build*/
tools/live/*.jar
# ---- 从载荷派生的批量产物(本机可重新生成)---- # ---- 从载荷派生的批量产物(本机可重新生成)----
viewpanel/analysis/spine-398/ viewpanel/analysis/spine-398/
Binary file not shown.
+338 -14
View File
@@ -38,27 +38,25 @@ Gp Code F scaleX
Gp SourceFile F scaleY Gp SourceFile F scaleY
Gp StackMapTable F shearX Gp StackMapTable F shearX
Gp LineNumberTable F shearY Gp LineNumberTable F shearY
# 静止姿态(rest pose)那一组:值与默认姿态吻合(0,0,0,1,1,0),且未改动时与上面那组相等 —— 推断,留注释待复核 # 【已作废·未定】静止姿态(rest pose)那一组(D/B/L/I/S/C 的 float)原本只是"值与默认姿态吻合"的推断,
Gp D F setupX # 2026-10-16 被 JSON 导出器证伪:导出器写 length 用的就是 Gp.C:F(见文件末尾"JSON 导出器"一节)。
Gp B F setupY # C 只能是 length,故整组推断不成立;这些字段的语义**目前未定**,不写进表里(宁可显示混淆名也不写错名)。
Gp L F setupRotation # Gp D F setupX
Gp I F setupScaleX # Gp B F setupY
Gp S F setupScaleY # Gp L F setupRotation
Gp C F setupShear # Gp I F setupScaleX
# Gp S F setupScaleY
# 引用类字段:skeleton 由 hd.D(set, bone.D)+GP.D(LGp;) 的用法确定;parent 由创建骨骼时 bone.D=parent 确定 # 引用类字段:skeleton 由 hd.D(set, bone.D)+GP.D(LGp;) 的用法确定;parent 由创建骨骼时 bone.D=parent 确定
Gp D LGP; skeleton Gp D LGP; skeleton
Gp D LGp; parent Gp D LGp; parent
Gp B Lcom/badlogic/gdx/graphics/Color; color Gp B Lcom/badlogic/gdx/graphics/Color; color
# ---- 2026-10-16 setup pose 实测补充(用户拍照:root/bone/slot/region 四种对象)---- # ---- 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 / Ljava,java=worldScale*:都是"数值相等"的弱证据,
Gp Class F length # 其中 length 已被 JSON 导出器改成 Gp.C:F(导出器写 length 时读的就是 C)。
Gp Ljava F worldScaleX # 剩下 Class/Ljava/java/init/String 这几个 float 导出器一个都没用,语义未定,不写进表。
Gp java F worldScaleY # GU(插槽):面板 Slot 属性只有 Name/Color;Inspector 里 String D=mega_dg4、Gp D=所属骨骼
# GU(插槽 mega_dg4):面板 Slot 属性只有 Name/Color;Inspector 里 String D=mega_dg4、Gp D=所属骨骼、Gn D=附件
GU D Ljava/lang/String; name GU D Ljava/lang/String; name
GU D LGp; bone 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 与 # GO(region 附件 mega_dg4):面板只有 Image path/Color;Inspector 六个 float 与
# Region 附件的 旋转49.00014 / 缩放(1.997,1.335) / 宽高(157,154) 一一对应 # Region 附件的 旋转49.00014 / 缩放(1.997,1.335) / 宽高(157,154) 一一对应
GO S F rotation GO S F rotation
@@ -103,3 +101,329 @@ dR I (F)LdR; height
# 故 B()F = getHeight、D()F = getWidth。 # 故 B()F = getHeight、D()F = getWidth。
dt B ()F getHeight dt B ()F getHeight
dt D ()F getWidth dt D ()F getWidth
# ---- 图片附件 GO 的 7 个 float 字段(2026-10-16 确认,两条独立证据)----
# 证据 1:编辑器自己的 JSON 写入器 sb_0.java:669-675(写 region 时依次读)
# x=GO.D y=GO.B scaleX=GO.L scaleY=GO.I rotation=GO.S width=GO.C height=GO.Z
# 旁证:width/height 与图片实际像素一致(mega_dg4.png = 157x154,运行时读到的 C=157.0 Z=154.0)
# 证据 2:GO 自己的 setTransform 字节码 javap -c GO 的 D(FFFFF)V:
# putfield D(第1个参数) -> putfield B(2) -> putfield S(3) -> putfield L(4) -> putfield I(5)
# => D(float) = x、B(float) = y(这两个字段名与别的字段重复,必须按类型取 float 那个)
GO D F x
GO B F y
# 图片附件的"设置变换"入口(编辑器自己用的):setTransform(x, y, rotation, scaleX, scaleY)
GO D (FFFFF)V setTransform
# 图片附件没有 shear 字段:region 只有 x/y/rotation/scaleX/scaleY(+ 只读的 width/height)
# ---- 附件删除(编辑器自己的删除流程)----
# 证据:javap -c Gn 的 L()V => this.GU.skeleton.removeAttachment(this, true, true)
# GP 的 D(Gn,ZZ)V 字节码里 getfield L:LfI; fI.B(Object,boolean) 把它从附件表移除,
# 并同步清掉 GU 上指向它的引用(同一方法里遍历 Z:LfI 即插槽表)。
Gn L ()V delete
GP L (LGn;ZZ)V removeAttachment
GP L LfI attachments
GP Z LfI slots
# ---- 附件种类 / 占位附件 ----
# 证据:可读源码 hi_2.java(= 载荷类 Hi)的 I() 直接 return "placeholder";
# 它出现在骨架附件表里但"既不是当前附件也不是静止附件",就是 skin/占位附件。
Gn I ()Ljava/lang/String; type
# 附件上指向自己所属占位附件的引用:GP.removeAttachment 的占位分支按它级联删除
Gn D LHi; placeholderRef
# ============================================================================
# 【权威来源】编辑器自带的 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
# ^ 面板自己的"罗列场景对象 / 删除对象"(走编辑器删除流程,自带撤销)——清理误建对象时用它
# ============================================================================
# 【动画模型】(2026-10-17 实测:新建 Gl / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线均跑通并读回验证)
# ============================================================================
# ---- 骨架上的动画表与 fps ----
GP I LfI animations
# ^ 骨架的动画表(fI,元素是 Gl)。新建动画 = new Gl() → 设 D:LGP、D:String → Gl.D() 注册 → animations.add(gl)
GP D LFT; projectSettings
FT D F fps
# ^ 本项目 fps=30(编辑器时间轴刻度直接用帧:关键帧时间写 0 和 30,不要换算成秒)
# ---- Gl = 动画 ----
Gl D LGP; skeleton
Gl D Ljava/lang/String; name
Gl D LfI timelineGroups
# ^ 时间轴组表(FH)。清空动画:直接换一个新的空 fI(removeRange 两个重载实测都不缩容)
Gl D ()V register
# ^ 把动画注册进骨架的动画列表模型(编辑器 New Animation 命令里就是这么做的)
Gl D (LGp;)LFI; rotateTimeline
Gl B (LGp;)LFI; translateTimeline
Gl L (LGp;)LFI; scaleTimeline
Gl I (LGp;)LFI; shearTimeline
# ^ 骨骼四条轴(取或建):运行时类分别是 ih / io / ii / ij
Gl D (LGp;LGU;)LFI; slotColorTimeline
Gl B (LGp;LGU;)LFI; slotAttachmentTimeline
# ^ 插槽两条轴:hU / ip
Gl D ()LFI; drawOrderTimeline
# ^ 骨架级绘制顺序轴(hZ)
# ---- FH = 时间轴组(按对象分组)----
FH D LGl; animation
FH D LGp; bone
FH D LfI timelines
# ---- FI = 时间轴 ----
FI D Ljava/lang/Class; runtimeTimelineClass
# ^ 运行时轴类:ih=Rotate(旋转) io=Translate(位移) ii=Scale(缩放) ij=Shear(斜切)
# hU=SlotColor(插槽颜色) ip=SlotAttachment(附件切换) hZ=DrawOrder(绘制顺序)
FI D LfI keys
FI D (F)LFN; findKey
# ^ 按时间精确找关键帧;找不到返回 null
FI D (LFN;Z)V insertKey
# ^ 插入关键帧(第二参=是否记撤销)
FI L ()V invalidateRuntime
# ^ 标脏运行时轴(改完关键帧调它,让编辑器重建 il)
FI D (FI;LFH;)V copyFrom
# ---- FN = 关键帧(数值字段以动画 JSON 导出器为准)----
FN D F time
FN D LFV; curve
# ^ 曲线类型;FV 枚举见下
FN B F curveC2x
FN L F curveC2y
FN I F curveC3x
FN S F curveC3y
# ^ 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1)
FN D (F)V setAngle
FN D ()F getAngle
# ^ 旋转轴的 angle(**不是** FN.D():ga!ga 是 deform 的数值数组)
FN B (F)V setX
FN B ()F getX
FN L (F)V setY
FN L ()F getY
# ^ 位移/缩放/斜切轴的 x / y
FN D Lcom/badlogic/gdx/graphics/Color; color
FN D LGn; attachment
# ^ 插槽颜色 / 挂哪个附件(null = 隐藏)
FN D LOr; valueSchema
FN D ()Lga; values
# ^ deform 用的数值数组(旋转/位移/缩放/斜切**不要**写这里)
# ---- FV = 曲线枚举(名字来自 FV.<clinit>)----
FV D LFV; linear
FV B LFV; curve
FV L LFV; stepped
# ^ 依次是 linear / curve(贝塞尔) / stepped;"自动"= curve + 一组控制值(如 0.25,0.25,0.75,0.75)
# ---- 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.
+159 -11
View File
@@ -1,39 +1,158 @@
# 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点 # 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
# owner name desc newName # owner name desc newName
FH D LGl; animation
FH D LGp; bone
FH D LfI timelines
FI D (F)LFN; findKey
FI D (FI;LFH;)V copyFrom
FI D (LFN;Z)V insertKey
FI D LfI keys
FI D Ljava/lang/Class; runtimeTimelineClass
FI L ()V invalidateRuntime
FN B ()F getX
FN B (F)V setX
FN B F curveC2x
FN D ()F getAngle
FN D ()Lga; values
FN D (F)V setAngle
FN D F time
FN D LFV; curve
FN D LGn; attachment
FN D LOr; valueSchema
FN D Lcom/badlogic/gdx/graphics/Color; color
FN I F curveC3x
FN L ()F getY
FN L (F)V setY
FN L F curveC2y
FN S F curveC3y
FT D F fps
FV B LFV; curve
FV D LFV; linear
FV L LFV; stepped
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 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 I F scaleY
GO L F scaleX GO L F scaleX
GO S F rotation GO S F rotation
GO Z F height GO Z F height
GU D LGn; attachment GP D (LGp;)V addBone
GP D LFT; projectSettings
GP I LfI animations
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 LGp; bone
GU D Lcom/badlogic/gdx/graphics/Color; color GU D Liz; blend
GU D Ljava/lang/String; name 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
Gl B (LGp;)LFI; translateTimeline
Gl B (LGp;LGU;)LFI; slotAttachmentTimeline
Gl D ()LFI; drawOrderTimeline
Gl D ()V register
Gl D (LGp;)LFI; rotateTimeline
Gl D (LGp;LGU;)LFI; slotColorTimeline
Gl D LGP; skeleton
Gl D LfI timelineGroups
Gl D Ljava/lang/String; name
Gl I (LGp;)LFI; shearTimeline
Gl L (LGp;)LFI; scaleTimeline
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 B Lcom/badlogic/gdx/graphics/Color; color
Gp C F setupShear Gp C F length
Gp Class F length
Gp Code F scaleX Gp Code F scaleX
Gp D (Ljava/lang/String;Z)V setName Gp D (Ljava/lang/String;Z)V setName
Gp D F setupX
Gp D LGP; skeleton Gp D LGP; skeleton
Gp D LGp; parent Gp D LGp; parent
Gp D LiC; transform
Gp D Ljava/lang/String; name Gp D Ljava/lang/String; name
Gp F F rotation Gp F F rotation
Gp I F setupScaleX
Gp L F setupRotation
Gp LineNumberTable F shearY Gp LineNumberTable F shearY
Gp Ljava F worldScaleX
Gp S F setupScaleY
Gp SourceFile F scaleY Gp SourceFile F scaleY
Gp StackMapTable F shearX Gp StackMapTable F shearX
Gp String Z skinOverride
Gp V F y Gp V F y
Gp Z F x 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 ()Ljava/lang/String; getSelected
KP B (I)V setSelectedIndex KP B (I)V setSelectedIndex
KP B (Ljava/lang/String;)V setSelected KP B (Ljava/lang/String;)V setSelected
KP D (Ldt;)V setStage 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 B (ZZ)V setCheckedAndFire
com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked com/badlogic/gdx/scenes/scene2d/ui/Button I (Z)V setChecked
com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked com/badlogic/gdx/scenes/scene2d/ui/Button String ()Z isChecked
@@ -121,8 +240,10 @@ dl B (I)F getY
dl B (Lde;)Lde; stageToLocalCoordinates dl B (Lde;)Lde; stageToLocalCoordinates
dl B (Ldl;Lde;)Lde; localToActorCoordinates dl B (Ldl;Lde;)Lde; localToActorCoordinates
dl B (Z)V setDebug dl B (Z)V setDebug
dl C ()F getRight
dl C (FF)V setOrigin dl C (FF)V setOrigin
dl D ()Ldl; debug dl D ()Ldl; debug
dl D ()Ldt; getStage
dl D ()Ljava/lang/Object; getUserObject dl D ()Ljava/lang/Object; getUserObject
dl D ()Ljava/lang/String; getName dl D ()Ljava/lang/String; getName
dl D ()V clearActions dl D ()V clearActions
@@ -138,17 +259,24 @@ dl D (Ldn;)Z addListener
dl D (Ljava/lang/Object;)V setUserObject dl D (Ljava/lang/Object;)V setUserObject
dl D (Ljava/lang/String;)V setName dl D (Ljava/lang/String;)V setName
dl D (Z)V setVisible dl D (Z)V setVisible
dl D Ldo; parent
dl D Ldt; stage
dl F ()V clipEnd dl F ()V clipEnd
dl I ()F getHeight
dl I ()Z isTouchable dl I ()Z isTouchable
dl L ()F getWidth
dl L ()V clear dl L ()V clear
dl L ()Z hasParent dl L ()Z hasParent
dl L (Ldn;)Z addCaptureListener dl L (Ldn;)Z addCaptureListener
dl L (Ldv;)V setTouchable dl L (Ldv;)V setTouchable
dl S ()F getTop
dl S (FF)V setOrigin dl S (FF)V setOrigin
dl S (Lde;)Lde; localToStageCoordinates dl S (Lde;)Lde; localToStageCoordinates
dl V ()V toBack dl V ()V toBack
dl Z ()V toFront dl Z ()V toFront
dl b_ ()Z remove dl b_ ()Z remove
dl c_ ()F getX
dl d_ ()F getY
do B ()Ldo; debugAll do B ()Ldo; debugAll
do Code ()V childrenChanged do Code ()V childrenChanged
do D (F)V act do D (F)V act
@@ -169,17 +297,24 @@ do LineNumberTable ()Z isTransform
do init ()Z hasChildren do init ()Z hasChildren
dt B ()F getHeight dt B ()F getHeight
dt D ()F getWidth dt D ()F getWidth
dt D (Ldl;)V addActor
eM C ()V expandTo eM C ()V expandTo
eM D ()F getHeight eM D ()F getHeight
eM D ()Z hasChildren eM D ()Z hasChildren
eM D (Ljava/lang/Object;)LeM; findNode eM D (Ljava/lang/Object;)LeM; findNode
eM I ()V collapseAll eM I ()V collapseAll
eM S ()V expandAll eM S ()V expandAll
eX Class ()F getMaxHeight
eX D (LJ;F)V draw eX D (LJ;F)V draw
eX LineNumberTable ()F getMinWidth
eX Ljava ()F getMaxWidth
eX Object ()Z needsLayout eX Object ()Z needsLayout
eX StackMapTable ()F getPrefWidth
eX StackMapTable ()V layout eX StackMapTable ()V layout
eX String ()F getMinHeight
eX V (Z)V setFillParent eX V (Z)V setFillParent
eX b_ ()V invalidate eX b_ ()V invalidate
eX init ()F getPrefHeight
et B (Lca;)V drawDebugBounds et B (Lca;)V drawDebugBounds
et B (LeN;)Let; padTop et B (LeN;)Let; padTop
et B (Z)V setDebug et B (Z)V setDebug
@@ -232,3 +367,16 @@ fI D ([Ljava/lang/Object;)LfI; with
fI D ([Ljava/lang/Object;)V addAll fI D ([Ljava/lang/Object;)V addAll
fI L ()Ljava/lang/Object; first fI L ()Ljava/lang/Object; first
fI L ()V reverse 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
1 # 合并后的名字映射表:确定(=)指纹 + 手动/实测锚点
2 # owner
3 FH
4 FH
5 FH
6 FI
7 FI
8 FI
9 FI
10 FI
11 FI
12 FN
13 FN
14 FN
15 FN
16 FN
17 FN
18 FN
19 FN
20 FN
21 FN
22 FN
23 FN
24 FN
25 FN
26 FN
27 FN
28 FT
29 FV
30 FV
31 FV
32 GD
33 GD
34 GD
35 GD
36 GD
37 GD
38 GD
39 GD
40 GD
41 GD
42 GE
43 GE
44 GE
45 GE
46 GO
47 GO
48 GO
49 GO
50 GO
51 GO
52 GO
53 GO
54 GO
55 GO
56 GU GP
57 GP
58 GP
59 GP
60 GP
61 GP
62 GU
63 GU
64 GU
65 GU
66 GU
67 GU
68 GU
69 Gp GU
70 GU
71 GX
72 GX
73 GX
74 GX
75 GX
76 GX
77 GX
78 GX
79 GX
80 GX
81 GX
82 GX
83 GX
84 GX
85 Gl
86 Gl
87 Gl
88 Gl
89 Gl
90 Gl
91 Gl
92 Gl
93 Gl
94 Gl
95 Gl
96 Gn
97 Gn
98 Gn
99 Gn
100 Gp
101 Gp
Gp
102 Gp
103 Gp
Gp
104 Gp
105 Gp
106 Gp
107 Gp
108 Gp
Gp
Gp
109 Gp
Gp
Gp
110 Gp
111 Gp
112 Gp
113 Gp
114 Gp
115 Gp Gr
116 Gr
117 Gr
118 Gs
119 Gs
120 Gs
121 Gs
122 Gw
123 Gw
124 Gw
125 Gw
126 Gw
127 Gw
128 Gw
129 Gx
130 Gx
131 Gx
132 Gx
133 Gx
134 Gx
135 Gx
136 Gx
137 Gx
138 Gx
139 Gx
140 Gx
141 Gx
142 Gx
143 KP
144 KP
145 KP
146 KP
147 Kp
148 Kp
149 OG
150 UH
151 UH
152 VZ
153 com.spineview.ObjectPanel
154 com.spineview.SceneActions
155 com.spineview.SceneObjects
156 com/badlogic/gdx/scenes/scene2d/ui/Button
157 com/badlogic/gdx/scenes/scene2d/ui/Button
158 com/badlogic/gdx/scenes/scene2d/ui/Button
240 dl
241 dl
242 dl
243 dl
244 dl
245 dl
246 dl
247 dl
248 dl
249 dl
259 dl
260 dl
261 dl
262 dl
263 dl
264 dl
265 dl
266 dl
267 dl
268 dl
269 dl
270 dl
271 dl
272 dl
273 dl
274 dl
275 dl
276 dl
277 dl
278 dl
279 dl
280 do
281 do
282 do
297 do
298 dt
299 dt
300 dt
301 eM
302 eM
303 eM
304 eM
305 eM
306 eM
307 eX
308 eX
309 eX
310 eX
311 eX
312 eX
313 eX
314 eX
315 eX
316 eX
317 eX
318 et
319 et
320 et
367 fI
368 fI
369 fI
370 iz
371 iz
372 iz
373 iz
374 un
375 un
376 un
377 vH
378 vH
379 xl
380 xl
381 xl
382 xl
+2
View File
@@ -0,0 +1,2 @@
Agent-Class: BoneMaker
Can-Retransform-Classes: false
+1
View File
@@ -0,0 +1 @@
Agent-Class: BoneMaker2
+1
View File
@@ -0,0 +1 @@
Agent-Class: BoneMaker3
+1
View File
@@ -0,0 +1 @@
Agent-Class: BoneMaker4
+1
View File
@@ -0,0 +1 @@
Agent-Class: BoneMaker5
+1
View File
@@ -0,0 +1 @@
Agent-Class: BoneMaker6
+1
View File
@@ -0,0 +1 @@
Agent-Class: BoneMaker7
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe1
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe2
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe3
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe4
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe5
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe6
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe7
+1
View File
@@ -0,0 +1 @@
Agent-Class: GeomProbe8
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: JsonDiff1
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: JsonDiff2
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: JsonDiff3
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: JsonDiff4
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: Probe3
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: Probe4
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: Probe5
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: Probe6
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: Probe7
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: Probe9
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: ResetMaker1
+2
View File
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Agent-Class: ResetMaker2
+1
View File
@@ -0,0 +1 @@
Agent-Class: SlotMaker1
+1
View File
@@ -0,0 +1 @@
Agent-Class: SlotMaker2
+803
View File
@@ -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") + "]";
}
}
+14
View File
@@ -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);
}
}
+195
View File
@@ -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);
}
}
+195
View File
@@ -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);
}
}
+196
View File
@@ -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);
}
}
+229
View File
@@ -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);
}
}
+229
View File
@@ -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);
}
}
+161
View File
@@ -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;
}
}
+161
View File
@@ -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;
}
}
+257
View File
@@ -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; }
}
+270
View File
@@ -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; }
}
+279
View File
@@ -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; }
}
+197
View File
@@ -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; }
}
+203
View File
@@ -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; }
}
+177
View File
@@ -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; }
}
+178
View File
@@ -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; }
}
+185
View File
@@ -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; }
}
+310
View File
@@ -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;
}
}
+311
View File
@@ -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;
}
}
+311
View File
@@ -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;
}
}
+159
View File
@@ -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;
}
}
+255
View File
@@ -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;
}
}
+232
View File
@@ -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;
}
}
+232
View File
@@ -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;
}
}
+215
View File
@@ -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;
}
}
+221
View File
@@ -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;
}
}
+330
View File
@@ -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;
}
}
+343
View File
@@ -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;
}
}
+166
View File
@@ -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;
}
}
+169
View File
@@ -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;
}
}
+3 -1
View File
@@ -20,7 +20,9 @@ mkdir -p build/classes
echo "== 编译 Java 侧(javac $("$JAVA_HOME/bin/javac" -version 2>&1))" echo "== 编译 Java 侧(javac $("$JAVA_HOME/bin/javac" -version 2>&1))"
find src -name '*.class' -delete 2>/dev/null || true find src -name '*.class' -delete 2>/dev/null || true
"$JAVA_HOME/bin/javac" --release 8 -encoding UTF-8 -d build/classes $(find src -name '*.java') # 面板里有一处用了 ASM(NewFifteen 现场生成编辑器对话框子类),编译期也要能看到 asm.jar
"$JAVA_HOME/bin/javac" --release 8 -encoding UTF-8 -cp "../tools/asm/asm.jar" \
-d build/classes $(find src -name '*.java')
# 保名类清单:这套运行时把 libGDX 基类混淆了,但 ui/* 和 editor/* 保名。 # 保名类清单:这套运行时把 libGDX 基类混淆了,但 ui/* 和 editor/* 保名。
# 旧版本的明文载荷里能直接列出这些类名,用它们做静态字段扫描的候选。 # 旧版本的明文载荷里能直接列出这些类名,用它们做静态字段扫描的候选。
@@ -0,0 +1,403 @@
package com.spineview;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* 动画面板工具(正式按钮,重启后仍在):
* - "Animate Random Props":随机骨骼/插槽/附件,把所有能 K 帧的属性做成 0f/30f 两个关键帧(值变化明显)
* - "Curve Test 6 Modes":6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别用 linear / curve 平滑 /
* stepped / ease-in / ease-out / 对称自动 六种曲线
*
* 动画模型(全部来自字节码,见 混淆名对照表.md 第十一节):
* GP.I:fI = 动画表;Gl = 动画(D:LGP 骨架、D:String 名字、D:fI 时间轴组)
* Gl.D(Gp)=旋转、Gl.B(Gp)=位移、Gl.L(Gp)=缩放、Gl.I(Gp)=斜切
* Gl.D(Gp,GU)=插槽颜色、Gl.B(Gp,GU)=附件切换
* FH = 组(D:Gl、D:Gp、D:fI 时间轴);FI = 时间轴(D:Class 运行时类、D:fI 关键帧、L() 重建)
* FN = 关键帧(D:F 时间、D:FV 曲线、B/L/I/S 贝塞尔控制值;
* D(float)=angle、B(float)=x、L(float)=y、D:Color 颜色、D:Gn 附件)
* FV = 曲线枚举(D=linear、B=curve、L=stepped)
*/
final class AnimationTools {
private static boolean installed;
private AnimationTools() {
}
/** 由 ObjectPanel.build 调用:加两个按钮并接上点击。 */
static void install(final ClassLoader loader, Object skin, Object buttonsTable,
Class<?> tableClass, final Class<?> cellClass, final Class<?> actorClass) {
if (installed) return;
try {
addButton(loader, skin, buttonsTable, cellClass, actorClass, "Animate Random Props",
new Runnable() {
@Override public void run() {
final ClassLoader classLoader = loader;
Thread thread = new Thread(new Runnable() {
@Override public void run() {
try { animateRandom(classLoader); } catch (Throwable error) {
System.err.println("[animtool] 随机动画失败: " + error);
error.printStackTrace(System.err);
}
}
}, "spineview-animate-random");
thread.setDaemon(true);
thread.start();
}
});
addButton(loader, skin, buttonsTable, cellClass, actorClass, "Curve Test 6 Modes",
new Runnable() {
@Override public void run() {
final ClassLoader classLoader = loader;
Thread thread = new Thread(new Runnable() {
@Override public void run() {
try { curveTest(classLoader); } catch (Throwable error) {
System.err.println("[animtool] 曲线测试失败: " + error);
error.printStackTrace(System.err);
}
}
}, "spineview-curve-test");
thread.setDaemon(true);
thread.start();
}
});
installed = true;
System.err.println("[animtool] 两个动画按钮已加入");
} catch (Throwable error) {
System.err.println("[animtool] 加按钮失败: " + error);
}
}
private static void addButton(ClassLoader loader, Object skin, Object buttonsTable,
Class<?> cellClass, Class<?> actorClass, String text, Runnable action) throws Exception {
Class<?> textButton = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
final Object button = textButton.getConstructor(String.class, skinClass).newInstance(text, skin);
Object cell = find(buttonsTable.getClass(), "D", new Class<?>[] {actorClass}, cellClass)
.invoke(buttonsTable, button);
find(cellClass, "C", new Class<?>[0], cellClass).invoke(cell); // Cell.left()
find(buttonsTable.getClass(), "I", new Class<?>[0], cellClass).invoke(buttonsTable);
ObjectPanel.wire(loader, button, action);
}
// ==================== 按钮 1:随机对象的所有可 K 属性 ====================
static void animateRandom(ClassLoader loader) throws Exception {
final ClassLoader L = loader;
final Class<?> gp = Class.forName("GP", true, L);
final Class<?> boneC = Class.forName("Gp", true, L);
final Class<?> slotC = Class.forName("GU", true, L);
final Class<?> attC = Class.forName("Gn", true, L);
final Class<?> glC = Class.forName("Gl", true, L);
final Class<?> fiC = Class.forName("FI", true, L);
final Class<?> fnC = Class.forName("FN", true, L);
final Class<?> colorC = Class.forName("com.badlogic.gdx.graphics.Color", true, L);
SceneActions.onEditorThread(L, new Runnable() {
@Override public void run() {
try {
Object skeleton = null;
List<Object> bones = new ArrayList<Object>(), slots = new ArrayList<Object>(),
atts = new ArrayList<Object>();
for (Object o : SceneObjects.listAllNodes(L)) {
if (o == null) continue;
if (skeleton == null && gp.isInstance(o)) skeleton = o;
if (boneC.isInstance(o)) bones.add(o);
if (slotC.isInstance(o)) slots.add(o);
if (attC.isInstance(o)) atts.add(o);
}
if (skeleton == null || bones.isEmpty() || slots.isEmpty() || atts.isEmpty()) {
System.err.println("[animtool] 场景对象不够");
return;
}
Random random = new Random();
Object bone = bones.get(random.nextInt(bones.size()));
Object slot = slots.get(random.nextInt(slots.size()));
Object slotBone = fieldOfType(slotC, slot, boneC);
List<Object> own = new ArrayList<Object>();
for (Object a : atts) if (fieldOfType(attC, a, slotC) == slot) own.add(a);
Object attachment = own.isEmpty() ? atts.get(random.nextInt(atts.size()))
: own.get(random.nextInt(own.size()));
System.err.println("[animtool] 随机选中:骨骼=" + SceneObjects.nameOf(bone)
+ " 插槽=" + SceneObjects.nameOf(slot) + " 附件=" + SceneObjects.nameOf(attachment));
Object anim = animation(L, skeleton, "ani_api_test");
set(anim, "D", Class.forName("fI", true, L),
Class.forName("fI", true, L).getConstructor().newInstance());
Object rotate = find(glC, "D", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
scalarPair(L, rotate, fiC, fnC, 0f, "D", 0f);
scalarPair(L, rotate, fiC, fnC, 30f, "D", 90f);
Object translate = find(glC, "B", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
xy(L, translate, fiC, fnC, 0f, 0f, 0f);
xy(L, translate, fiC, fnC, 30f, 120f, -60f);
Object scale = find(glC, "L", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
xy(L, scale, fiC, fnC, 0f, 1f, 1f);
xy(L, scale, fiC, fnC, 30f, 1.6f, 0.6f);
Object shear = find(glC, "I", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
xy(L, shear, fiC, fnC, 0f, 0f, 0f);
xy(L, shear, fiC, fnC, 30f, 25f, 15f);
Object color = find(glC, "D", new Class<?>[] {boneC, slotC}, fiC).invoke(anim, slotBone, slot);
color(L, color, fiC, fnC, colorC, 0f, 1f, 1f, 1f, 1f);
color(L, color, fiC, fnC, colorC, 30f, 1f, 0.2f, 0.2f, 0.25f);
Object att = find(glC, "B", new Class<?>[] {boneC, slotC}, fiC).invoke(anim, slotBone, slot);
attachment(L, att, fiC, fnC, attC, 0f, attachment);
attachment(L, att, fiC, fnC, attC, 30f, null);
refresh(L, skeleton);
System.err.println("[animtool] ani_api_test 已重建:6 条时间轴(0f/30f)");
} catch (Throwable error) {
System.err.println("[animtool] 随机动画失败: " + error);
error.printStackTrace(System.err);
}
}
});
}
// ==================== 按钮 2:六种曲线 ====================
static void curveTest(ClassLoader loader) throws Exception {
final ClassLoader L = loader;
final Class<?> gp = Class.forName("GP", true, L);
final Class<?> boneC = Class.forName("Gp", true, L);
final Class<?> glC = Class.forName("Gl", true, L);
final Class<?> fiC = Class.forName("FI", true, L);
final Class<?> fnC = Class.forName("FN", true, L);
final Class<?> fvC = Class.forName("FV", true, L);
SceneActions.onEditorThread(L, new Runnable() {
@Override public void run() {
try {
Object skeleton = null;
Map<String, Object> bones = new LinkedHashMap<String, Object>();
for (Object o : SceneObjects.listAllNodes(L)) {
if (o == null) continue;
if (skeleton == null && gp.isInstance(o)) skeleton = o;
if (boneC.isInstance(o)) bones.put(SceneObjects.nameOf(o), o);
}
if (skeleton == null) return;
String[] order = {"root", "bone", "bone2", "bone3", "bone5", "lizi"};
String[] labels = {"linear", "curve smooth", "stepped", "ease-in", "ease-out", "symmetric auto"};
Object linear = fvC.getField("D").get(null);
Object curve = fvC.getField("B").get(null);
Object stepped = fvC.getField("L").get(null);
Object[] modes = {linear, curve, stepped, curve, curve, curve};
float[][] controls = {{0, 0, 0, 0}, {0.25f, 0f, 0.75f, 1f}, {0, 0, 0, 0},
{0.42f, 0f, 1f, 1f}, {0f, 0f, 0.58f, 1f}, {0.25f, 0.25f, 0.75f, 0.75f}};
Object anim = animation(L, skeleton, "curve_test");
set(anim, "D", Class.forName("fI", true, L),
Class.forName("fI", true, L).getConstructor().newInstance());
int made = 0;
for (int i = 0; i < order.length; i++) {
Object bone = bones.get(order[i]);
if (bone == null) continue;
Object tl = find(glC, "D", new Class<?>[] {boneC}, fiC).invoke(anim, bone);
Object k0 = key(L, tl, fiC, fnC, 0f);
Object k1 = key(L, tl, fiC, fnC, 30f);
setFloatMethod(fnC, k0, "D", 0f); // angle 0
setFloatMethod(fnC, k1, "D", 45f); // angle 45
set(k0, "D", fvC, modes[i]);
set(k0, "B", float.class, Float.valueOf(controls[i][0]));
set(k0, "L", float.class, Float.valueOf(controls[i][1]));
set(k0, "I", float.class, Float.valueOf(controls[i][2]));
set(k0, "S", float.class, Float.valueOf(controls[i][3]));
rebuild(fiC, tl);
System.err.println("[animtool] curve_test " + order[i] + " = " + labels[i]);
made++;
}
refresh(L, skeleton);
System.err.println("[animtool] curve_test 已重建:共 " + made + " 条旋转轴(0f/30f,0°→45°)");
} catch (Throwable error) {
System.err.println("[animtool] 曲线测试失败: " + error);
error.printStackTrace(System.err);
}
}
});
}
// ==================== 时间轴/关键帧 ====================
/** 取或建动画(并按编辑器自己的 Gl.D() 注册)。 */
private static Object animation(ClassLoader L, Object skeleton, String name) throws Exception {
Class<?> gp = Class.forName("GP", true, L);
Class<?> glC = Class.forName("Gl", true, L);
Class<?> fI = Class.forName("fI", true, L);
Field animsF = gp.getDeclaredField("I");
animsF.setAccessible(true);
Object anims = animsF.get(skeleton);
for (int i = 0; i < size(fI, anims); i++) {
Object a = get(fI, anims, i);
if (a != null && name.equals(fieldOfType(glC, a, String.class))) return a;
}
Object anim = glC.getConstructor().newInstance();
set(anim, "D", gp, skeleton);
set(anim, "D", String.class, name);
for (Method m : glC.getDeclaredMethods()) {
if (m.getName().equals("D") && m.getParameterTypes().length == 0
&& m.getReturnType() == void.class) { m.setAccessible(true); m.invoke(anim); }
}
add(fI, anims, anim);
System.err.println("[animtool] 新建动画 " + name);
return anim;
}
private static Object key(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC, float time)
throws Exception {
Method get = find(fiC, "D", new Class<?>[] {float.class}, fnC);
Object k = get.invoke(tl, Float.valueOf(time));
if (k != null) return k;
k = fnC.getConstructor(fiC).newInstance(tl);
set(k, "D", float.class, Float.valueOf(time));
find(fiC, "D", new Class<?>[] {fnC, boolean.class}, void.class)
.invoke(tl, k, Boolean.FALSE);
return k;
}
private static void scalarPair(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
float time, String setter, float value) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
setFloatMethod(fnC, k, setter, value);
rebuild(fiC, tl);
}
private static void xy(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
float time, float x, float y) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
setFloatMethod(fnC, k, "B", x);
setFloatMethod(fnC, k, "L", y);
rebuild(fiC, tl);
}
private static void color(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC, Class<?> colorC,
float time, float r, float g, float b, float a) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
Object color = fieldOfType(fnC, k, colorC);
if (color == null) color = colorC.getConstructor().newInstance();
colorC.getField("r").setFloat(color, r);
colorC.getField("g").setFloat(color, g);
colorC.getField("b").setFloat(color, b);
colorC.getField("a").setFloat(color, a);
set(k, "D", colorC, color);
rebuild(fiC, tl);
}
private static void attachment(ClassLoader L, Object tl, Class<?> fiC, Class<?> fnC,
Class<?> attC, float time, Object attachment) throws Exception {
Object k = key(L, tl, fiC, fnC, time);
set(k, "D", attC, attachment);
rebuild(fiC, tl);
}
private static void rebuild(Class<?> fiC, Object tl) throws Exception {
find(fiC, "L", new Class<?>[0], void.class).invoke(tl);
}
private static void refresh(ClassLoader L, Object skeleton) throws Exception {
Class<?> gp = Class.forName("GP", true, L);
Class<?> vz = Class.forName("VZ", true, L);
Class<?> og = Class.forName("OG", true, L);
Object tree = staticField(og, vz);
Method refresh = find(vz, "D", new Class<?>[] {gp, boolean.class, boolean.class}, void.class);
if (refresh != null) refresh.invoke(tree, skeleton, Boolean.TRUE, Boolean.TRUE);
}
// ==================== 反射工具 ====================
private static void setFloatMethod(Class<?> owner, Object target, String name, float value)
throws Exception {
for (Class<?> c = owner; c != null && c != Object.class; c = c.getSuperclass()) {
for (Method m : c.getDeclaredMethods()) {
if (!m.getName().equals(name)) continue;
Class<?>[] types = m.getParameterTypes();
if (types.length != 1 || types[0] != float.class) continue;
m.setAccessible(true);
m.invoke(target, Float.valueOf(value));
return;
}
}
throw new NoSuchMethodException("float " + name);
}
private static Object fieldOfType(Class<?> owner, Object target, Class<?> type) {
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;
}
private static Object staticField(Class<?> owner, Class<?> type) {
for (Field f : owner.getDeclaredFields()) {
if (!Modifier.isStatic(f.getModifiers()) || f.getType() != type) continue;
f.setAccessible(true);
try {
Object v = f.get(null);
if (v != null) return v;
} catch (Throwable ignored) {
// 下一个
}
}
return null;
}
private 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;
}
}
throw new NoSuchFieldException(name);
}
private 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;
}
}
throw new IllegalStateException(owner.getName() + "." + name);
}
private static void add(Class<?> arrayClass, Object array, Object item) throws Exception {
find(arrayClass, "D", new Class<?>[] {Object.class}, void.class).invoke(array, item);
}
private static int size(Class<?> arrayClass, Object array) {
if (array == null) return 0;
for (Class<?> c = arrayClass; 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;
}
private static Object get(Class<?> arrayClass, Object array, int index) {
try {
return find(arrayClass, "D", new Class<?>[] {int.class}, Object.class)
.invoke(array, Integer.valueOf(index));
} catch (Throwable ignored) {
return null;
}
}
}
+682
View File
@@ -0,0 +1,682 @@
package com.spineview;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Random;
import java.util.Set;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* “New 15 Bone+Slot+Att” 按钮(正式版,重启后仍在)。
*
* 流程:点一下 → 弹编辑器自带的名称输入面板 → 用输入的名字匹配骨架里的附件
* → 名字不对/找不到就结束 → 否则在**当前选中骨骼**下建 15 根骨骼,
* 每根 1 个插槽 + 1 个附件(该名字的附件副本)。
*
* 参数要求(全部按已确认的字段语义写,见 混淆名对照表.md):
* 骨骼:x/y 各 0~500 随机;rotation=0;scaleX=scaleY=1;shearX=shearY=0
* 插槽:颜色 alpha 0.5~1 随机;blend = normal / additive 随机
* 附件(挂在插槽上):x=y=rotation=0;scaleX=scaleY=1
*
* 关键 API(都在 tools/deobf/anchors.tsv 里,附字节码依据):
* UH.D(GU,Gp,Z,fI)GU addSlot ⚠️ 会连模板插槽的附件一起复制
* UH.D(Gn,GU,Z,Z,fI)Gn addAttachmentCopy(内部 Gn.D(GP) 复制模板附件)
* GP.D(Gp)V addBone
* OG.D(String,fI,GU)String uniqueName
* VZ.D(GP,Z,Z)V refresh
* un.D()/un.B() undoBegin/undoCommit
* Gn.L()V delete
* dt.D(dl)V Stage.addActor
* xl.D(dt) / xl.L(Object) / xl.L(String) 编辑器名称面板:显示 / OK-Cancel 回调 / 拿到名字(抽象)
*/
final class NewFifteen {
private static final int COUNT = 15;
private static final String BUTTON_TEXT = "New 15 Bone+Slot+Att";
/** 生成的对话框子类(ASM 现生成:xl 是抽象类,且必须在载荷类加载器那边实现)。 */
private static Class<?> dialogClass;
private static ClassLoader dialogLoader;
private static boolean installed;
private NewFifteen() {
}
/** 由 ObjectPanel.build 调用:往按钮表格里加一行按钮并接上点击。 */
static void install(final ClassLoader loader, Object skin, Object buttonsTable,
Class<?> tableClass, Class<?> cellClass, Class<?> actorClass) {
if (installed) return;
try {
Class<?> textButton = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.TextButton", true, loader);
Class<?> skinClass = Class.forName("com.badlogic.gdx.scenes.scene2d.ui.Skin", true, loader);
final Object button = textButton.getConstructor(String.class, skinClass)
.newInstance(BUTTON_TEXT, skin);
Object cell = call(buttonsTable, "D", new Class<?>[] {actorClass}, cellClass, button);
call(cell, "C", new Class<?>[0], cellClass); // Cell.left()
call(buttonsTable, "I", new Class<?>[0], cellClass); // Table.row()
ObjectPanel.wire(loader, button, new Runnable() {
@Override public void run() {
// 面板路由线程里:只负责开线程走流程,别把它堵住(要等用户输入)
Thread thread = new Thread(new Runnable() {
@Override public void run() {
try {
promptThenCreate(loader);
} catch (Throwable error) {
System.err.println("[new15] 流程失败: " + error);
error.printStackTrace(System.err);
}
}
}, "spineview-new15");
thread.setDaemon(true);
thread.start();
}
});
installed = true;
System.err.println("[new15] 按钮已加入: " + BUTTON_TEXT);
} catch (Throwable error) {
System.err.println("[new15] 加按钮失败: " + error);
}
}
// ==================== 弹面板 → 等名字 → 建对象 ====================
private static void promptThenCreate(final ClassLoader loader) throws Exception {
final Object[] holder = new Object[1];
SceneActions.onEditorThread(loader, new Runnable() {
@Override public void run() {
try {
holder[0] = showPrompt(loader);
} catch (Throwable error) {
System.err.println("[new15] 弹面板失败: " + error);
error.printStackTrace(System.err);
}
}
});
final Object dialog = holder[0];
if (dialog == null) {
System.err.println("[new15] 面板没建出来");
return;
}
String name = waitForName(loader, dialog, 300000L);
if (name == null || name.trim().isEmpty()) {
System.err.println("[new15] 没拿到名字(取消/超时)→ 结束");
removeQuietly(dialog);
return;
}
final String finalName = name.trim();
System.err.println("[new15] 输入的名字 = " + finalName);
removeQuietly(dialog);
SceneActions.onEditorThread(loader, new Runnable() {
@Override public void run() {
try {
create(loader, finalName);
} catch (Throwable error) {
System.err.println("[new15] 建对象失败: " + error);
error.printStackTrace(System.err);
}
}
});
}
/** 在渲染线程里:建对话框 → 挂进 Stage → 按 pref 撑开 → 居中(都在同一帧里完成,不会先闪别处)。 */
private static Object showPrompt(ClassLoader loader) throws Exception {
Class<?> xl = Class.forName("xl", true, loader);
Class<?> stageClass = Class.forName("dt", true, loader);
Object stage = global(loader, stageClass);
if (stage == null) throw new IllegalStateException("拿不到 Stage");
Class<?> dialog = dialogClass(loader);
Object dialogObject = dialog.getConstructor(String.class, String.class)
.newInstance("New Bones + Slots + Attachment",
"Enter an existing attachment name in this skeleton:");
// ① 挂进 Stage(实测 xl.D(dt) 不会挂;Stage.addActor 用"扫描 getMethods"那个才生效)
addActor(stage, dialogObject);
// ② 走编辑器自己的显示流程(把键盘焦点给名字框)
Method show = null;
for (Method method : xl.getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 1 && types[0] == stageClass) show = method;
}
if (show != null) {
try {
show.setAccessible(true);
show.invoke(dialogObject, stage);
} catch (Throwable error) {
System.err.println("[new15] xl.D(dt) 失败: " + error);
}
}
// ③ 尺寸:默认 150x150 会把正文/按钮裁掉 → 用 pref(下限 360x170)
float prefW = floatOf(dialogObject, "StackMapTable"); // getPrefWidth
float prefH = floatOf(dialogObject, "init"); // getPrefHeight
float minW = floatOf(dialogObject, "LineNumberTable"); // getMinWidth
float minH = floatOf(dialogObject, "String"); // getMinHeight
float width = Math.max(Math.max(prefW, minW), 360f);
float height = Math.max(Math.max(prefH, minH), 170f);
setFloatField(loader, dialogObject, "L", width); // Actor.width
setFloatField(loader, dialogObject, "I", height); // Actor.height
invalidate(loader, dialogObject);
// ④ 居中:setPosition(x,y,align) 在 align=0 时是"以 (x,y) 为中心"
center(stage, dialogObject);
return dialogObject;
}
/** 轮询用户操作:OK 时 NAME 有值;Cancel 时 DONE=true 且 NAME 为空;窗口被移除时 stage 字段变 null。 */
private static String waitForName(ClassLoader loader, Object dialog, long timeoutMs)
throws Exception {
Field nameField = dialog.getClass().getField("NAME");
Field doneField = dialog.getClass().getField("DONE");
Field stageField = fieldOfType(Class.forName("dl", true, loader),
Class.forName("dt", true, loader));
long deadline = System.currentTimeMillis() + timeoutMs;
while (System.currentTimeMillis() < deadline) {
Object name = nameField.get(dialog);
if (name != null) return (String) name;
if (Boolean.TRUE.equals(doneField.get(dialog))) return null;
if (stageField != null && stageField.get(dialog) == null) return null;
Thread.sleep(120L);
}
return null;
}
// ==================== 建 15 组对象 ====================
private static void create(ClassLoader loader, String name) throws Exception {
Class<?> boneClass = Class.forName("Gp", true, loader);
Class<?> slotClass = Class.forName("GU", true, loader);
Class<?> attClass = Class.forName("Gn", true, loader);
Class<?> hiClass = Class.forName("Hi", true, loader);
Class<?> skeletonClass = Class.forName("GP", true, loader);
Class<?> arrayClass = Class.forName("fI", true, loader);
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, loader);
Class<?> blendClass = Class.forName("iz", true, loader);
Class<?> uhClass = Class.forName("UH", true, loader);
Class<?> og = Class.forName("OG", true, loader);
Class<?> undoClass = Class.forName("un", true, loader);
Class<?> treeClass = Class.forName("VZ", true, loader);
Object selected = SceneObjects.selected(loader);
if (selected == null || !boneClass.isInstance(selected)) {
System.err.println("[new15] 当前选中的不是骨骼 → 结束");
return;
}
Object skeleton = firstFieldOfType(boneClass, selected, skeletonClass);
if (skeleton == null) {
System.err.println("[new15] 拿不到骨架 → 结束");
return;
}
Object bones = arrayOfType(skeletonClass, skeleton, arrayClass, boneClass);
Object slots = arrayOfType(skeletonClass, skeleton, arrayClass, slotClass);
Object attachments = arrayOfType(skeletonClass, skeleton, arrayClass, attClass);
if (bones == null || slots == null || attachments == null) {
System.err.println("[new15] 骨架的骨骼/插槽/附件表没找齐 → 结束");
return;
}
// 按名字找附件(优先非 Hi 占位)
Object template = null;
for (int i = 0; i < size(arrayClass, attachments); i++) {
Object candidate = get(arrayClass, attachments, i);
if (candidate == null || !attClass.isInstance(candidate)) continue;
if (!name.equals(SceneObjects.nameOf(candidate))) continue;
if (!hiClass.isInstance(candidate)) {
template = candidate;
break;
}
if (template == null) template = candidate;
}
if (template == null) {
System.err.println("[new15] 骨架里没有名为 \"" + name + "\" 的附件 → 结束");
return;
}
Object templateSlot = size(arrayClass, slots) > 0 ? get(arrayClass, slots, 0) : null;
Object uh = staticField(og, uhClass);
if (uh == null) {
System.err.println("[new15] 拿不到编辑器模型助手 UH → 结束");
return;
}
Method addBone = find(skeletonClass, "D", new Class<?>[] {boneClass}, void.class);
Method addSlot = find(uhClass, "D", new Class<?>[] {slotClass, boneClass, boolean.class,
arrayClass}, slotClass);
Method addAttachment = find(uhClass, "D", new Class<?>[] {attClass, slotClass, boolean.class,
boolean.class, arrayClass}, attClass);
Method nameGen = find(og, "D", new Class<?>[] {String.class, arrayClass, slotClass}, String.class);
Method refresh = find(treeClass, "D", new Class<?>[] {skeletonClass, boolean.class,
boolean.class}, void.class);
Method deleteAttachment = find(attClass, "L", new Class<?>[0], void.class);
Method undoBegin = noArgVoid(undoClass, "D");
Method undoCommit = noArgVoid(undoClass, "B");
if (addBone == null || addSlot == null || addAttachment == null || nameGen == null
|| undoBegin == null || undoCommit == null) {
System.err.println("[new15] 编辑器 API 没找齐,停止");
return;
}
Object blendNormal = blendClass.getField("D").get(null);
Object blendAdditive = blendClass.getField("B").get(null);
Constructor<?> boneCtor = boneClass.getConstructor(boneClass, skeletonClass, boneClass);
Object empty = arrayClass.getConstructor().newInstance();
Random random = new Random();
int made = 0;
undoBegin.setAccessible(true);
undoBegin.invoke(null);
for (int i = 0; i < COUNT; i++) {
// ---- 骨骼:x/y 0~500 随机,其余按 0/1 ----
Object bone = boneCtor.newInstance(selected, skeleton, selected);
set(bone, "D", String.class, nameGen.invoke(null, "bone", bones, null));
setFloatField(loader, bone, "Z", random.nextFloat() * 500f); // x
setFloatField(loader, bone, "V", random.nextFloat() * 500f); // y
setFloatField(loader, bone, "F", 0f); // rotation
setFloatField(loader, bone, "Code", 1f); // scaleX
setFloatField(loader, bone, "SourceFile", 1f); // scaleY
setFloatField(loader, bone, "StackMapTable", 0f); // shearX
setFloatField(loader, bone, "LineNumberTable", 0f); // shearY
addBone.setAccessible(true);
addBone.invoke(skeleton, bone);
// ---- 插槽:alpha 0.5~1 随机;blend normal/additive 随机 ----
Object out = arrayClass.getConstructor().newInstance();
addSlot.setAccessible(true);
Object slot = addSlot.invoke(uh, templateSlot, bone, Boolean.FALSE, out);
if (slot == null) continue;
// ADD_SLOT 会把模板插槽整个复制,连它挂着的附件一起复制 → 记下来待会删
Object inherited = readSlotAttachment(slotClass, attClass, slot);
float alpha = 0.5f + random.nextFloat() * 0.5f;
Object color = colorClass.getConstructor(float.class, float.class, float.class, float.class)
.newInstance(1f, 1f, 1f, alpha);
set(slot, "B", colorClass, color); // setup 颜色
set(slot, "D", colorClass, color); // animate 颜色
set(slot, "D", blendClass, random.nextBoolean() ? blendNormal : blendAdditive);
// ---- 附件:复制"输入名字"的附件,变换 0/0/0/1/1 ----
addAttachment.setAccessible(true);
Object attachment = addAttachment.invoke(uh, template, slot, Boolean.FALSE, Boolean.FALSE, empty);
if (attachment == null) continue;
setFloatField(loader, attachment, "D", 0f); // x
setFloatField(loader, attachment, "B", 0f); // y
setFloatField(loader, attachment, "S", 0f); // rotation
setFloatField(loader, attachment, "L", 1f); // scaleX
setFloatField(loader, attachment, "I", 1f); // scaleY
if (!name.equals(SceneObjects.nameOf(attachment))) {
set(attachment, "D", String.class, name); // 编辑器若自动改名,强制回输入的名字
}
// 插槽的当前附件 = 我们这份(两个字段都写,setup/animate 都覆盖)
set(slot, "B", attClass, attachment);
set(slot, "D", attClass, attachment);
// 删掉模板插槽带来的那份(保证"一根骨骼 = 1 插槽 + 1 附件")
if (inherited != null && inherited != attachment && deleteAttachment != null) {
deleteAttachment.setAccessible(true);
deleteAttachment.invoke(inherited);
}
made++;
}
undoCommit.setAccessible(true);
undoCommit.invoke(null);
if (refresh != null) {
refresh.setAccessible(true);
refresh.invoke(staticField(og, treeClass), skeleton, Boolean.TRUE, Boolean.TRUE);
}
System.err.println("[new15] 完成:在 " + SceneObjects.nameOf(selected) + " 下建了 " + made
+ " 根骨骼(每根 1 插槽 1 附件 \"" + name + "\")");
}
// ==================== 对话框子类(ASM) ====================
private static synchronized Class<?> dialogClass(final ClassLoader loader) throws Exception {
if (dialogClass != null) return dialogClass;
final String internal = "com/spineview/NewFifteenPrompt";
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "xl", null);
writer.visitField(Opcodes.ACC_PUBLIC, "NAME", "Ljava/lang/String;", null, null).visitEnd();
writer.visitField(Opcodes.ACC_PUBLIC, "DONE", "Z", null, null).visitEnd();
// 构造:super(title, body, (Ge) null)
MethodVisitor ctor = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
"(Ljava/lang/String;Ljava/lang/String;)V", null, null);
ctor.visitCode();
ctor.visitVarInsn(Opcodes.ALOAD, 0);
ctor.visitVarInsn(Opcodes.ALOAD, 1);
ctor.visitVarInsn(Opcodes.ALOAD, 2);
ctor.visitInsn(Opcodes.ACONST_NULL);
ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "<init>",
"(Ljava/lang/String;Ljava/lang/String;LGe;)V", false);
ctor.visitInsn(Opcodes.RETURN);
ctor.visitMaxs(0, 0);
ctor.visitEnd();
// 抽象回调:OK 时拿到名字 → 记 NAME,并关窗
MethodVisitor name = writer.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/String;)V",
null, null);
name.visitCode();
name.visitVarInsn(Opcodes.ALOAD, 0);
name.visitVarInsn(Opcodes.ALOAD, 1);
name.visitFieldInsn(Opcodes.PUTFIELD, internal, "NAME", "Ljava/lang/String;");
name.visitVarInsn(Opcodes.ALOAD, 0);
name.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "dl", "b_", "()Z", false); // remove()
name.visitInsn(Opcodes.POP);
name.visitInsn(Opcodes.RETURN);
name.visitMaxs(0, 0);
name.visitEnd();
// OK/Cancel 总回调:不管哪个都置 DONE
MethodVisitor done = writer.visitMethod(Opcodes.ACC_PUBLIC, "L", "(Ljava/lang/Object;)V",
null, null);
done.visitCode();
done.visitVarInsn(Opcodes.ALOAD, 0);
done.visitVarInsn(Opcodes.ALOAD, 1);
done.visitMethodInsn(Opcodes.INVOKESPECIAL, "xl", "L", "(Ljava/lang/Object;)V", false);
done.visitVarInsn(Opcodes.ALOAD, 0);
done.visitInsn(Opcodes.ICONST_1);
done.visitFieldInsn(Opcodes.PUTFIELD, internal, "DONE", "Z");
done.visitInsn(Opcodes.RETURN);
done.visitMaxs(0, 0);
done.visitEnd();
writer.visitEnd();
final byte[] bytes = writer.toByteArray();
dialogLoader = new ClassLoader(loader) {
@Override protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (name.equals("com.spineview.NewFifteenPrompt")) {
Class<?> found = findLoadedClass(name);
if (found == null) found = defineClass(name, bytes, 0, bytes.length);
if (resolve) resolveClass(found);
return found;
}
return super.loadClass(name, resolve);
}
};
dialogClass = Class.forName("com.spineview.NewFifteenPrompt", true, dialogLoader);
return dialogClass;
}
// ==================== 小工具 ====================
private static Object global(ClassLoader loader, Class<?> type) {
try {
Class<?> og = Class.forName("OG", true, loader);
for (Field field : og.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
field.setAccessible(true);
Object value = field.get(null);
if (value != null) return value;
}
} catch (Throwable ignored) {
// 读不到就返回 null
}
return null;
}
/** 挂进 Stage:用"扫描 getMethods 里 1 个 Actor 参数 + void"的那个(getMethod 那个实测不生效)。 */
private static void addActor(Object stage, Object actor) throws Exception {
ClassLoader loader = stage.getClass().getClassLoader();
Class<?> actorClass = Class.forName("dl", true, loader);
Method add = null;
for (Method method : stage.getClass().getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) continue;
if (!actorClass.isAssignableFrom(types[0])) continue;
if (method.getReturnType() != void.class) continue;
add = method;
}
if (add == null) throw new NoSuchMethodException("Stage.addActor");
add.setAccessible(true);
add.invoke(stage, actor);
}
private static void center(Object stage, Object dialog) {
try {
float width = floatOf(dialog, "L"); // getWidth
float height = floatOf(dialog, "I"); // getHeight
float cx = floatOf(stage, "D") / 2f; // Stage.getWidth / 2
float cy = floatOf(stage, "B") / 2f; // Stage.getHeight / 2
Class<?> actorClass = Class.forName("dl", true, dialog.getClass().getClassLoader());
Method setPosition = null;
for (Method method : actorClass.getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 3 && types[0] == float.class && types[1] == float.class
&& types[2] == int.class) setPosition = method;
}
if (setPosition != null) {
setPosition.setAccessible(true);
// align=0 → 以 (x,y) 为中心
setPosition.invoke(dialog, Float.valueOf(cx), Float.valueOf(cy), Integer.valueOf(0));
}
System.err.println("[new15] 弹窗 " + width + "x" + height + " 居中于 (" + cx + "," + cy + ")");
} catch (Throwable error) {
System.err.println("[new15] 居中失败: " + error);
}
}
private static void invalidate(ClassLoader loader, Object dialog) {
try {
Class<?> widgetGroup = Class.forName("eX", true, loader);
for (Method method : widgetGroup.getMethods()) {
if (method.getName().equals("b_") && method.getParameterTypes().length == 0
&& method.getReturnType() == void.class) {
method.setAccessible(true);
method.invoke(dialog);
return;
}
}
} catch (Throwable ignored) {
// 没 invalidate 也不致命
}
}
private static void removeQuietly(Object dialog) {
try {
Class<?> actorClass = Class.forName("dl", true, dialog.getClass().getClassLoader());
Method remove = null;
for (Method method : actorClass.getMethods()) {
if (method.getName().equals("b_") && method.getParameterTypes().length == 0
&& method.getReturnType() == boolean.class) remove = method;
}
if (remove != null) {
remove.setAccessible(true);
remove.invoke(dialog);
}
} catch (Throwable ignored) {
// 已经关掉了
}
}
private static Object readSlotAttachment(Class<?> slotClass, Class<?> attClass, Object slot)
throws Exception {
for (Class<?> type = slotClass; type != null && type != Object.class; type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != attClass) continue;
field.setAccessible(true);
Object value = field.get(slot);
if (value != null) return value;
}
}
return null;
}
private static Field fieldOfType(Class<?> owner, Class<?> type) {
for (Class<?> current = owner; current != null && current != Object.class;
current = current.getSuperclass()) {
for (Field field : current.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
field.setAccessible(true);
return field;
}
}
return null;
}
private static Object firstFieldOfType(Class<?> owner, Object target, Class<?> type) {
for (Class<?> current = owner; current != null && current != Object.class;
current = current.getSuperclass()) {
for (Field field : current.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
field.setAccessible(true);
try {
Object value = field.get(target);
if (value != null) return value;
} catch (Throwable ignored) {
// 下一个
}
}
}
return null;
}
private static Object arrayOfType(Class<?> owner, Object target, Class<?> arrayClass,
Class<?> elementType) {
for (Class<?> current = owner; current != null && current != Object.class;
current = current.getSuperclass()) {
for (Field field : current.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || !arrayClass.isAssignableFrom(field.getType())) {
continue;
}
field.setAccessible(true);
Object array;
try {
array = field.get(target);
} catch (Throwable ignored) {
continue;
}
if (array == null) continue;
for (int i = 0; i < size(arrayClass, array); i++) {
Object element = get(arrayClass, array, i);
if (element != null && elementType.isInstance(element)) return array;
}
}
}
return null;
}
private static void setFloatField(ClassLoader loader, Object target, String name, float value)
throws Exception {
for (Class<?> type = target.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (Field field : type.getDeclaredFields()) {
if (!field.getName().equals(name) || field.getType() != float.class) continue;
field.setAccessible(true);
field.setFloat(target, value);
return;
}
}
}
private static void set(Object target, String name, Class<?> type, Object value) throws Exception {
for (Class<?> current = target.getClass(); current != null && current != Object.class;
current = current.getSuperclass()) {
for (Field field : current.getDeclaredFields()) {
if (!field.getName().equals(name) || field.getType() != type) continue;
field.setAccessible(true);
field.set(target, value);
return;
}
}
}
private static float floatOf(Object target, String name) {
try {
for (Method method : target.getClass().getMethods()) {
if (!method.getName().equals(name)) continue;
if (method.getParameterTypes().length != 0) continue;
if (method.getReturnType() != float.class) continue;
method.setAccessible(true);
return ((Float) method.invoke(target)).floatValue();
}
} catch (Throwable ignored) {
// 读不到
}
return -1f;
}
private static Object call(Object target, String name, Class<?>[] parameterTypes,
Class<?> returnType, Object... args) throws Exception {
for (Class<?> current = target.getClass(); current != null && current != Object.class;
current = current.getSuperclass()) {
for (Method method : current.getDeclaredMethods()) {
if (!method.getName().equals(name)) continue;
if (!Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
if (returnType != null && method.getReturnType() != returnType) continue;
method.setAccessible(true);
return method.invoke(target, args);
}
}
throw new NoSuchMethodException(target.getClass().getName() + "." + name);
}
private static Object staticField(Class<?> owner, Class<?> type) {
for (Field field : owner.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers()) || field.getType() != type) continue;
field.setAccessible(true);
try {
Object value = field.get(null);
if (value != null) return value;
} catch (Throwable ignored) {
// 下一个
}
}
return null;
}
private static Method find(Class<?> owner, String name, Class<?>[] parameterTypes,
Class<?> returnType) {
for (Class<?> current = owner; current != null && current != Object.class;
current = current.getSuperclass()) {
for (Method method : current.getDeclaredMethods()) {
if (!method.getName().equals(name)) continue;
if (!Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
if (returnType != null && method.getReturnType() != returnType) continue;
method.setAccessible(true);
return method;
}
}
return null;
}
private static Method noArgVoid(Class<?> owner, String name) {
for (Method method : owner.getDeclaredMethods()) {
if (!method.getName().equals(name)) continue;
if (method.getParameterTypes().length != 0) continue;
if (method.getReturnType() != void.class) continue;
method.setAccessible(true);
return method;
}
return null;
}
private static int size(Class<?> arrayClass, Object array) {
if (array == null) return 0;
for (Class<?> current = arrayClass; current != null && current != Object.class;
current = current.getSuperclass()) {
for (Field field : current.getDeclaredFields()) {
if (field.getType() != int.class) continue;
field.setAccessible(true);
try {
return field.getInt(array);
} catch (Throwable ignored) {
// 下一个
}
}
}
return 0;
}
private static Object get(Class<?> arrayClass, Object array, int index) {
for (Method method : arrayClass.getMethods()) {
if (!method.getName().equals("D")) continue;
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || types[0] != int.class) continue;
if (method.getReturnType() != Object.class) continue;
try {
method.setAccessible(true);
return method.invoke(array, Integer.valueOf(index));
} catch (Throwable ignored) {
// 下一个
}
}
return null;
}
}
+74 -27
View File
@@ -136,6 +136,10 @@ public final class ObjectPanel {
// 每个按钮一行:上段是**一条整带**(不要左右两列),整体读起来才是"上/中/下" // 每个按钮一行:上段是**一条整带**(不要左右两列),整体读起来才是"上/中/下"
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass); call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
} }
// 新按钮:点一下弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建 15 根骨骼(每根 1 插槽 1 附件)
NewFifteen.install(loader, skin, buttons, tableClass, cellClass, actorClass);
// 动画工具按钮:随机对象 6 属性动画 / 六种曲线测试
AnimationTools.install(loader, skin, buttons, tableClass, cellClass, actorClass);
call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass); call(buttons, Obf.TABLE_ROW, new Class<?>[0], cellClass);
Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField); Object nameCell = call(buttons, Obf.TABLE_ADD, new Class<?>[] {actorClass}, cellClass, state.nameField);
layoutCell(nameCell, Obf.CELL_LEFT); layoutCell(nameCell, Obf.CELL_LEFT);
@@ -636,41 +640,64 @@ public final class ObjectPanel {
* 建用编辑器自己的 New Bone(带撤销),建完立刻改该骨骼的 x/y 与 color 字段。 * 建用编辑器自己的 New Bone(带撤销),建完立刻改该骨骼的 x/y 与 color 字段。
*/ */
private static void createTenRedBones(final State state) { private static void createTenRedBones(final State state) {
SceneActions.onEditorThread(state.loader, new Runnable() { // 编辑器自己的 API 路径(不经 UI、不依赖下一帧):一次建 10 根 → 一次刷新 → 一次撤销
@Override final Object parent = currentTarget(state);
public void run() {
// 父级取"编辑器里的当前选择"(用户是在层级面板里选的,不是点我们列表行)
Object parent = currentTarget(state);
if (parent == null) { if (parent == null) {
setStatus(state, "+10 Red Bones: select a parent bone first."); setStatus(state, "+10 Red Bones: select a parent bone first.");
return; 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() {
try { try {
Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, state.loader); Class<?> colorClass = Class.forName("com.badlogic.gdx.graphics.Color", true, state.loader);
Object red = colorClass.getConstructor(float.class, float.class, float.class, Object red = colorClass.getConstructor(float.class, float.class, float.class, float.class)
float.class).newInstance(Float.valueOf(1f), Float.valueOf(0f), Float.valueOf(0f), .newInstance(Float.valueOf(1f), Float.valueOf(0f), Float.valueOf(0f), Float.valueOf(1f));
Float.valueOf(1f)); Class<?> undo = Class.forName("un", true, state.loader);
int made = 0; 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;
}
if (begin != null) {
begin.setAccessible(true);
begin.invoke(null);
}
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
java.util.List<Object> before = SceneObjects.listAllNodes(state.loader); Object bone = SceneActions.createBone(state.loader, skeleton, parent, "red_" + (i + 1),
SceneActions.create(state.loader, parent, "Bone"); (float) (Math.random() * 400.0 - 200.0), (float) (Math.random() * 400.0 - 200.0),
Object fresh = firstNew(before, SceneObjects.listAllNodes(state.loader)); red);
if (fresh == null) continue; if (bone != null) made[0]++;
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++;
} }
setStatus(state, "made " + made + " red bones under " + SceneObjects.nameOf(parent)); if (commit != null) {
commit.setAccessible(true);
commit.invoke(null);
}
SceneActions.refresh(state.loader, skeleton);
} catch (Throwable error) { } catch (Throwable error) {
setStatus(state, "+10 Red Bones failed: " + error); System.err.println("[panel] +10 Red Bones(API) 失败: " + error);
error.printStackTrace(System.err);
} }
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,18 +734,24 @@ public final class ObjectPanel {
} }
} }
/** 骨骼颜色:找类型是 libGDX Color 的字段写进去。 */ /**
* 骨骼颜色:按映射表写 `Gp` 的 color 字段(混淆名 B,描述符 Lcom/badlogic/gdx/graphics/Color;)。
* 表里查不到时才退回"第一个 Color 类型字段"。
*/
private static void setBoneColor(Object bone, Object color) { private static void setBoneColor(Object bone, Object color) {
try { try {
String obf = obfNameOf("Gp", "Lcom/badlogic/gdx/graphics/Color;", "color");
for (Class<?> type = bone.getClass(); type != null && type != Object.class; for (Class<?> type = bone.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) { type = type.getSuperclass()) {
for (java.lang.reflect.Field field : type.getDeclaredFields()) { for (java.lang.reflect.Field field : type.getDeclaredFields()) {
if (!field.getType().getName().equals("com.badlogic.gdx.graphics.Color")) continue; if (field.getType().getName().equals("com.badlogic.gdx.graphics.Color")) {
if (obf != null && !field.getName().equals(obf)) continue;
field.setAccessible(true); field.setAccessible(true);
field.set(bone, color); field.set(bone, color);
return; return;
} }
} }
}
} catch (Throwable error) { } catch (Throwable error) {
System.err.println("[panel] 设置颜色失败: " + error); System.err.println("[panel] 设置颜色失败: " + error);
} }
@@ -791,12 +824,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): * 识别方式(不猜名字、也不会误调 addActor):
* - 只在 **Stage 自己声明的**方法里找(`uk`/`dt`/`lpt6` 这些层), * - 只在 **Stage 自己声明的**方法里找(`uk`/`dt`/`lpt6` 这些层),
* `addActor` 声明在 Group 上、不在这一层,所以不会被误选; * `addActor` 声明在 Group 上、不在这一层,所以不会被误选;
* - 候选 setter = 「1 个参数是 Actor 类型、返回 void」;候选 getter = 「无参、返回 Actor 类型」; * - 候选 setter = 「1 个参数是 Actor 类型、返回 void」;候选 getter = 「无参、返回 Actor 类型」;
* - 调 setter 后看哪个 getter 变成了我们的面板 —— 那就是 setScrollFocus 那一对,日志会写出来。 * - 调 setter 后看哪个 getter 变成了我们的面板 —— 那就是 setScrollFocus 那一对,日志会写出来。
*/ */
@SuppressWarnings("unused")
static void grabScrollFocus(final State state) { static void grabScrollFocus(final State state) {
try { try {
Object stage = SceneObjects.global(state.loader, "uk"); Object stage = SceneObjects.global(state.loader, "uk");
@@ -1347,8 +1391,11 @@ public final class ObjectPanel {
} }
try { try {
((Runnable) route[1]).run(); ((Runnable) route[1]).run();
Object st = LAST_STATE; // 这里以前会调 grabScrollFocus(state) 去抢滚动焦点。
if (st instanceof State) grabScrollFocus((State) st); // 2026-10-16 停用:它会把列表的 ScrollPane 当成 Stage.addActor 的参数传进去,
// 结果 ScrollPane 被搬出面板、挂到 Stage 根,列表就画到窗口左下角去了
// (证据:dt.D(Ldl;)V 的字节码 = root.addActor;探针实测 root 子控件只剩 2 个)。
// 滚动焦点本来就靠鼠标点一下列表拿到,不需要这段。
} catch (Throwable error) { } catch (Throwable error) {
System.err.println("[panel] 动作失败: " + error); System.err.println("[panel] 动作失败: " + error);
error.printStackTrace(System.err); error.printStackTrace(System.err);
@@ -901,4 +901,110 @@ public final class SceneActions {
} }
return count; 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;
}
}
}
} }
+246
View File
@@ -344,3 +344,249 @@ macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所
> 教训也写进技能了:**新确认的映射必须当场写进 `tools/deobf/anchors.tsv` 并同步 `names.tsv` / `src-named`**, > 教训也写进技能了:**新确认的映射必须当场写进 `tools/deobf/anchors.tsv` 并同步 `names.tsv` / `src-named`**,
> 不许只留在对话里(见 `.codex/skills/scope-discipline/SKILL.md` 末节)。 > 不许只留在对话里(见 `.codex/skills/scope-discipline/SKILL.md` 末节)。
---
## 八、图片附件(`GO`)与占位附件(`Hi`)(2026-10-16 实机确认)
这一节是"给已经建好的附件收尾"时确认的:重置图片附件的变换、删掉多余的 skin/占位附件。
### 图片附件的 7 个 float 字段
| `GO` 字段 | 真名 | 依据 |
| --- | --- | --- |
| `D`(float 的那个) | **x** | ①编辑器自己的 JSON 写入器 `sb_0.java:669-675` 写 `"x"` 时读的就是它 ②`javap -c GO` 的 `D(FFFFF)V` 第一个 `putfield` 是它 |
| `B`(float 的那个) | **y** | 同上(JSON 写 `"y"`、`D(FFFFF)V` 第 2 个 `putfield`) |
| `S` | **rotation** | JSON 写 `"rotation"`;`D(FFFFF)V` 第 3 个参数 |
| `L` | **scaleX** | JSON 写 `"scaleX"`;`D(FFFFF)V` 第 4 个参数 |
| `I` | **scaleY** | JSON 写 `"scaleY"`;`D(FFFFF)V` 第 5 个参数 |
| `C` / `Z` | **width / height** | JSON 写 `"width"/"height"`;实测 `C=157.0 Z=154.0` 与 `mega_dg4.png` 真实像素 157x154 一致 |
> 注意:`GO` 上有多个同名字段 `D`(float / iU / pL / long / float[]),
> 按名字 `getDeclaredField("D")` 会取错;必须挑**类型是 float 的那个**。
`GO.D(FFFFF)V` = 编辑器自己的 **setTransform(x, y, rotation, scaleX, scaleY)**,重置一个图片附件的变换就用它。
图片附件**没有 shear(斜切)字段**:region 只有 x/y/rotation/scaleX/scaleY。
### 附件删除 / 占位附件
| 载荷签名 | 真名 | 依据 |
| --- | --- | --- |
| `Gn.L()V` | **delete(删掉这个附件)** | `javap -c Gn`:`this.GU.skeleton.removeAttachment(this, true, true)` |
| `GP.L(LGn;ZZ)V` | **removeAttachment** | 字节码里 `getfield L:LfI; fI.B(Object,true)` 从附件表移除,并清掉 `GU`(插槽表 `Z:LfI`)上指向它的引用 |
| `GP.L:LfI` / `GP.Z:LfI` | **attachments / slots** | 骨架的附件表(本次从 43 项减到 23 项)/ 插槽表 |
| `Gn.I()Ljava/lang/String;` | **type** | 占位附件 `Hi`(可读源码 `hi_2.java`)直接 `return "placeholder"` |
| `Hi`(载荷类) | **占位附件(skin placeholder)** | 它挂在骨架附件表里,但**既不是插槽的当前附件也不是静止附件**;`GP.removeAttachment` 会按 `Gn.D:LHi` 把引用它的附件级联删掉 |
**本次实机结果**(不重启,JDK8 attach 热更,进程仍是同一个):
- 20 个新建插槽的图片附件:`x/y/rotation` 从 `-99.49988 / -302.0 / 40.00014`、`scaleX/scaleY` 从 `1.997 / 1.532` → 全部重置为 `0/0/0` 与 `1/1`(宽度高度 157x154 保持不变)。
- 20 个 `Hi` 占位附件全部删除;骨架附件表 `43 → 23`(正好 23 个插槽各 1 个图片附件)。
- 删除占位附件**不会**连带删掉图片附件:日志里逐个核对了"删除后该插槽当前附件=GO"。
---
## 九、权威对照:编辑器自带的 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 的正中) |
---
## 十一、动画模型(2026-10-17 实测:新建动画 / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线全部跑通)
这一节是"在动画面板下用编辑器自身 API 建 30 帧动画"时确认的整套模型。每条都真跑过并读回验证。
### 骨架 / 动画
| 载荷签名 | 真名 | 依据 / 坑 |
| --- | --- | --- |
| `GP.I:LfI` | **animations(动画表)** | 元素是 `Gl`;新建动画后 `animations.add(gl)` |
| `GP.D:LFT;` → `FT.D:F` | **projectSettings.fps** | 本项目 fps=**30**。⚠️ 编辑器时间轴刻度直接用**帧**:关键帧时间写 0 和 30,不要换算成秒 |
| `Gl.D:LGP;` / `Gl.D:Ljava/lang/String;` | skeleton / name | |
| `Gl.D:LfI` | **timelineGroups(时间轴组表)** | 元素是 `FH`。清空动画最稳的做法:直接换一个**新的空 fI**(`removeRange` 的两个 `(int,int)` 重载实测都不缩容) |
| `Gl.D()V` | **register(注册到动画列表模型)** | 编辑器 New Animation 命令里就是这么做的 |
| `Gl.D(Gp)FI` / `Gl.B(Gp)FI` / `Gl.L(Gp)FI` / `Gl.I(Gp)FI` | **旋转 / 位移 / 缩放 / 斜切轴** | 运行时类依次 `ih` / `io` / `ii` / `ij` |
| `Gl.D(Gp,GU)FI` / `Gl.B(Gp,GU)FI` | **插槽颜色 / 附件切换轴** | 运行时类 `hU` / `ip` |
| `Gl.D()FI` | **绘制顺序轴** | 运行时类 `hZ` |
### 时间轴 / 关键帧
| 载荷签名 | 真名 | 依据 / 坑 |
| --- | --- | --- |
| `FH.D:Gl` / `FH.D:Gp` / `FH.D:LfI` | animation / bone / timelines | `FH` 是按"骨骼或插槽"分组的一层 |
| `FI.D:Ljava/lang/Class;` | **runtimeTimelineClass** | `ih`=Rotate、`io`=Translate、`ii`=Scale、`ij`=Shear、`hU`=SlotColor、`ip`=SlotAttachment、`hZ`=DrawOrder |
| `FI.D:LfI` | **keys(关键帧表)** | 元素是 `FN` |
| `FI.D(float)FN` | **findKey** | 按时间精确找关键帧(找不到返回 null) |
| `FI.D(FN,boolean)V` | **insertKey** | 插入关键帧(第二参=是否记撤销) |
| `FI.L()V` | **invalidateRuntime** | 标脏运行时轴(改完关键帧调它重建) |
| `FI.D(float,int)Z` | **addKeyFromPose** | 按当前姿态在指定时间建关键帧;⚠️ 时间轴上**必须已经有关键帧**,否则抛 `frameCount must be > 0` |
| `FN` 字段/方法 | 真名 | 依据 / 坑 |
| --- | --- | --- |
| `D:F` | **time** | 关键帧时间(帧) |
| `D:FV` | **curve** | 曲线类型,见下 |
| `B/L/I/S:F` | **curveC2x / c2y / c3x / c3y** | 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1) |
| `D(float)` / `D()F` | **setAngle / getAngle** | 旋转轴的角度。⚠️ **不是** `FN.D():ga`! |
| `B(float)` / `B()F` | **setX / getX** | 位移/缩放/斜切的 x |
| `L(float)` / `L()F` | **setY / getY** | 位移/缩放/斜切的 y |
| `D:Color` | **color** | 插槽颜色关键帧 |
| `D:Gn` | **attachment** | 附件切换关键帧(null = 隐藏) |
| `D:Or` / `D():ga` | **valueSchema / values** | **deform** 用的数值数组(旋转/位移/缩放/斜切不要写这里——曾因此"帧有值但不生效") |
### 曲线枚举 `FV`(名字取自 `<clinit>`)
| 字段 | 真名 |
| --- | --- |
| `FV.D` | **linear(线性)** |
| `FV.B` | **curve(贝塞尔)** |
| `FV.L` | **stepped(阶跃)** |
"自动"不是单独的枚举值,而是 **curve + 一组控制值**(本次实测过 0.25,0.25,0.75,0.75 的对称自动;ease-in 用 0.42,0,1,1;ease-out 用 0,0,0.58,1)。
### 面板按钮(正式源码)
- `viewpanel/src/com/spineview/AnimationTools.java`
- **Animate Random Props**:随机骨骼/插槽/附件 → `ani_api_test`:旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)、插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
- **Curve Test 6 Modes**:`curve_test`:6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别 linear / curve 平滑 / stepped / ease-in / ease-out / 对称自动
+128
View File
@@ -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)。