Files
Spine_lab/混淆名对照表.md
T
tianmo a0080428f9 场景对象增删改查调研:命令系统入口 + 新建对象的真实落点
- Commands.java:Editor -> pE(命令系统) -> void D(String) 按名字执行命令
- Launcher:新增 -Dspine.runCommand="<命令名>" 便于验证
- SceneReader:Tree/选中项探针(actor 树按 SnapshotArray 结构遍历)
- 混淆名对照表.md:补命令系统一节、可执行命令名单、skeleton/root 删除边界、
  以及 New X 最终落到静态 KP.B(target, 类型名) 的结论与待办
2026-10-16 14:20:45 +08:00

16 KiB
Raw Blame History

混淆名对照表(Spine 3.8.99 载荷)

这份文档记录载荷里被混淆的类名/方法名/字段名分别对应什么,以及每一条是怎么推导出来的。 以后要动编辑器内部的 scene2d 控件(加面板、读数据、导出),都先查这里,不要重新猜。

代码里对应的是 viewpanel/src/com/spineview/Obf.java, 里面有同样的常量表,改代码时直接引用。


一、推导办法(可复现)

  1. 字节码指纹比对:tools/obfmap/MapMethods.java
    • 把载荷类与上游 libGDX 同签名方法逐个比对:忽略名字,只比较 "操作码序列 + 常量 + 成员的描述符"(类名按"方法内首次出现的顺序"编号,绕开两边类名不同的问题)。
    • 用法:
      CP=tools/asm/asm.jar:tools/asm/asm-tree.jar:tools/obfmap/build
      "$JAVA17" -cp "$CP" MapMethods <上游.class> <载荷.class>
      # 输出:载荷方法签名 -> 上游方法名(= 唯一匹配,~n/m = 按声明顺序消歧)
      
    • 编译:
      "$JAVA17" -cp tools/asm/asm.jar:tools/asm/asm-tree.jar -d tools/obfmap/build tools/obfmap/MapMethods.java
      
  2. 上游源码核对:拿 libGDX 1.11.0 的源码逐条看方法体语义 (gdx/src/com/badlogic/gdx/scenes/...),确认指纹给出的结论。
  3. 运行期验证:面板布局/按钮/文字这些已经在编辑器里真跑过,见文末"验证状态"。

上游版本怎么挑的:Cell/Table/Actor 的字段结构在 1.9.13 / 1.9.14 / 1.11.0 上一致, 指纹命中率也相同,所以用 1.11.0 作为参照(字段类型 Value、Cell.align 是 Integer)。


二️、最大的坑:同名不同描述符

JVM 允许同一个类里存在"名字相同、描述符不同"的字段和方法,这个载荷把这一招用满了:

  • dR(Cell)里有 30 多个字段都叫 D/B/L/I,靠类型区分 (eN D(Value)、dl D(Actor)、float D、et D(Table)…);
  • et(Table)上 D 这个名字同时是 add(Actor)、align(int)、pad(float)、pad(float×4)…

所以:定位任何成员都必须"名字 + 描述符"一起判,只看名字一定踩雷。已经踩过的两个:

踩坑 表现 原因
Label 的 D(CharSequence) 日志打印"读取完成 N 字符",面板文字却一直不刷新 有 D(CharSequence)V(= setText)和 D(CharSequence)Z(= textEquals)两个,按"1 个 CharSequence 参数"找会挑到 textEquals
Table 的 I() 调到 top() 而不是 row() I()LdR; = row()(返回 Cell)、I()Let; = top()(返回 Table),必须判返回类型

三、类对照

载荷名 真实类 依据
dl com.badlogic.gdx.scenes.scene2d.Actor 无父类;do extends dl;有 stage/parent 语义
do ...scene2d.Group extends dl;children 是 [Ldl;
eX ...ui.WidgetGroup extends do;有 setFillParent 语义(见 et.Z(boolean))
et ...ui.Table extends eX;有 cells(fI) / rowDefaults(Cell)
dR ...ui.Cell et.add(Actor) 的返回类型;implements hg(=Poolable)
dt ...scene2d.Stage uk extends dt
uk Stage 的实现(编辑器实际用的那个) StageHook 挂在它的构造器上
eN ...ui.Value 抽象类;Cell 的尺寸/间距字段都是它
fI com.badlogic.gdx.utils.Array Table.cells 的类型
fu ...scene2d.utils.Drawable Table.background(Drawable)
hg com.badlogic.gdx.utils.Pool.Poolable Cell implements hg
OG 编辑器单例持有者 静态字段 D = com.esotericsoftware.spine.editor.Editor
Fx 静态 Actor 锚点 静态字段里有 dl 实例,用来倒推 Stage
us 视图枚举(Editor/Preview/Tree/…) <clinit> 里建常量、按 MV/ut 分组
tD 编辑器 UI 装配类 给每个视图 new 出面板 Actor 的地方

四、方法对照

4.1 et = Table

载荷签名 真实方法 依据 / 备注
D(Ldl;)LdR; add(Actor) 返回 Cell;运行期已用于把按钮/文字区放进面板
D([Ldl;)Let; add(Actor...) 返回 Table(varargs 版,别和上面混)
L()LdR; add() 体 = return add((Actor)null)
I()LdR; row() 体与上游 Table.row() 逐句一致:判 cells.size、endRow()、invalidate()、换 rowDefaults;不调它所有控件都会挤在第一行
D(I)LdR; getCell(int) 不够就补 cell
B(Ldl;)LdR; getCell(Actor) 遍历 cells 找 actor
S()LdR; getRowDefaults()(行默认值 Cell) 体 = return rowDefaults
Z(Z)V setFillParent(boolean) 体 = this.C = bl(纯赋值)
C(Z)V setClip(boolean) 体 = clip = bl; setTransform(bl); invalidate()
B(Z)V setDebug(boolean) 体 = debug(bl ? Debug.all : Debug.none);调错会让单元格描上彩色边框(踩过)
I()Let; top() 返回 Table
S()Let; left()
L()Let; center()
C()Let; bottom()
Z()Let; right()
D(I)Let; align(int)
D(F)Let; pad(float)
D(FFFF)Let; pad(top,left,bottom,right)
B(F)Let; padTop(float) 与 L/I/S(F)Let; = padLeft/padBottom/padRight 一组(同形状,按声明顺序对)
D(Ljava/lang/String;)Let; background(String)
D(Lfu;)Let; background(Drawable)

4.2 dR = Cell

尺寸/间距(参数类型不同,靠描述符区分;都返回 Cell):

载荷签名 真实方法
D(dl)LdR; setActor(Actor)
D(eN,eN)LdR; size(Value width, Value height)
D(F)LdR; size(float)
D(FF)LdR; size(float,float)
D(eN)LdR; / B(eN)LdR; width(Value) / height(Value)
B(F)LdR; / L(F)LdR; width(float) / height(float)
L(eN)LdR; / I(eN)LdR; minWidth(Value) / minHeight(Value)
B(eN,eN)LdR; / S(eN)LdR; minSize(Value,Value) / minSize(Value)
C(eN)LdR; / Z(eN)LdR; prefWidth(Value) / prefHeight(Value)
S(F) / C(F) / Z(F) / V(F) minWidth(float) / minHeight(float) / prefWidth(float) / prefHeight(float)
F(F)LdR; 起连续 5 个 (F)LdR; maxWidth / maxHeight / padTop / padLeft / padBottom / padRight(同形状成组,按声明顺序对,未逐条验证)
V(eN)LdR; / F(eN)LdR; / Code(eN)LdR; space(Value) / spaceBottom(Value) / spaceRight(Value)
Code(F) / SourceFile(F) / StackMapTable(F) / init(F) / LineNumberTable(F) space(float) / spaceTop(float) / spaceLeft(float) / spaceBottom(float) / spaceRight(float)
String(F)LdR; pad(float)
D(FFFF)LdR; pad(top,left,bottom,right)

布局开关(这几条是面板能排好版的关键):

载荷签名 真实方法 依据
D()LdR; fill() 体 = 两个 float 字段 = 1f(上游 fillX = fillY = onef)
B()LdR; / L()LdR; fillX() / fillY() 各设一个 float 字段
StackMapTable()LdR; expand() 体 = 两个 Integer 字段 = onei(上游 expandX = expandY = onei)
init()LdR; / LineNumberTable()LdR; expandX() / expandY()
F()LdR; grow() 指纹唯一命中
Code()LdR; / SourceFile()LdR; growX() / growY() 同形状成组
C()LdR; left() 体 = align|8 & ~16
S()LdR; top() 体 = align|2 & ~4
Z()LdR; bottom() 体 = align|4 & ~2
V()LdR; right() 体 = align|16 & ~8
I()LdR; center() 体 = align = 1i
D(I)LdR; align(int)
D(Z)LdR; / B(Z)LdR; / D(ZZ)LdR; fill/uniform 系(未逐条验证)
D()F,B()F,L()F,I()F getMinWidth/getMinHeight/getPrefWidth/getPrefHeight
B()V / L()V / D()V row() / clear() / reset()

位掩码常量(都是 Integer 静态字段,值来自 libGDX Align): C=1(onei)、Z=1(center)、V=2(top)、F=4(bottom)、Code=8(left)、SourceFile=16(right)。

4.3 com.badlogic.gdx.scenes.scene2d.ui.Button(类名保留,方法名混淆)

载荷签名 真实方法
String()Z isChecked()(体 = return isChecked,读第 1 个 boolean 字段)
I(Z)V setChecked(boolean)(指纹唯一命中)
B(ZZ)V setChecked(boolean, boolean) —— 第二参是"是否派发事件",复位时传 false,否则点一次会读两次
a_(Z)V setDisabled(boolean)
java()Z isDisabled()
Class()Z / Ljava()Z isOver() / isPressed()(体分别是 clickListener.D() / clickListener.B(),两者顺序未逐条验证)

4.4 com.badlogic.gdx.scenes.scene2d.ui.Label(类名保留,方法名混淆)

载荷签名 真实方法 备注
D(Ljava/lang/CharSequence;)V setText(CharSequence)
D(Ljava/lang/CharSequence;)Z textEquals(CharSequence) 陷阱:长得像 setText,调它文字永远不刷新
D()LN; getText()(返回 StringBuilder)
D(LabelStyle)V / D()LabelStyle setStyle / getStyle

4.5 uk/dt = Stage、Fx/OG = 入口

载荷名 真实含义 备注
uk.<init> Stage 实现类的构造器 Stage 在编辑器里只是局部变量,只能钩构造器拿实例
Fx 的静态 dl 字段 一个静态 Actor 由它 Actor.stage 反查 Stage(比钩构造器更省事)
OG.D 编辑器单例 com.esotericsoftware.spine.editor.Editor
us.<clinit> 视图枚举初始化 加新视图必须在 $VALUES 组装之后、分组列表生成之前插

四之二、命令系统(增删改查的入口)

编辑器的所有菜单动作都走同一套命令系统。链路(反编译源码里读出来的):

OG.D                                → com.esotericsoftware.spine.editor.Editor 单例
  └─ public pE D;                   → 命令 / 热键系统(Editor.<init> 里 new pe_0())
       ├─ void D(String)            → 按名字执行(名字 = hotkeys.txt / 菜单项名)
       ├─ pC   D(String)            → 按名字取菜单项(不执行)
       └─ private final ll D;       → 菜单注册表:D(String,pC,String...) 注册、D(String) 查表

菜单项基类 pC(所有动作类都继承它,例如 mn_1=New Bone、pb_0=Delete):

成员 含义
boolean D() 执行并返回"是否处理了"(pC 基类实现 = og_1.D.S.a_(); return true;)
void D() 执行(无返回值);基类里是空实现,动作类各自覆盖
String D 名字(toString() 返回它)

注意:pE.D(String) 只看"找得到找不到",不看动作有没有真的生效。 实测 -Dspine.runCommand="New Bone" 打印"执行完成"但骨骼树没变 —— 因为 mn_1.D() 的源码是:

et_0 sel = og_1.D.D;                       // 当前选择
if (sel instanceof xw_2) { KP.B(...D, "Bone"); return true; }
if (sel instanceof acK)  { KP.B(...D, "Bone"); return true; }
return false;                              // 没选中 → 什么都不做,也不报错

也就是说 这些动作全都作用在"当前选择"上,要先能设置选择,才谈得上增删改。

可用的命令名(= hotkeys.txt 里的名字,~/Library/Application Support/Spine/hotkeys.txt):

类别 名字
删除/改名 Delete、Rename、Duplicate、Cut、Copy、Paste
新建 New Bone、New Slot、New Skin Placeholder、New Bounding Box、New Path、New Clipping、New IK Constraint、New Path Constraint、New Transform Constraint、New Skin、New Animation、New Event、New Folder、New Skeleton
层级/顺序 Set Parent、Parent、Child、Previous Sibling、Next Sibling、Previous Sibling Bone、Next Sibling Bone、Draw Order Up、Draw Order Down、Tree Up、Tree Down
显示 Visibility、Show All Bones/Slots、Expand、Collapse、Expand/Collapse、Tree Filter、Search
选择类 Select - Bones、Select - Child Bones、Select - Descendant Bones、Select - Attachments、Select - Visible Attachments、Select - Draw Order
其它 Undo、Redo、Rulers、Zoom To Fit、Setup/Animate Mode、Color、Replace

面板里的接口:viewpanel/src/com/spineview/Commands.java (Commands.run(loader, "New Bone")),验证入口是启动参数 -Dspine.runCommand="<命令名>"。

真正"建出一个能显示的骨骼"落在哪

用户在编辑器里新建骨骼是交互式的:先选父级骨骼,再用 Create Tool 在视口里点一下/拖一下。 菜单里的 New Bone 只是同一条路的"用当前选择当父级"版本 —— 所以没选中时它直接 return false。

把所有 New X 菜单动作摊开看,它们最后都落到同一个静态方法上(反编译源码里的名字):

KP.B(target, "Bone");              // 新建骨骼(target = 当前选择对象)
KP.B(target, "Slot");
KP.B(target, "Skin Placeholder");
KP.B(target, "Bounding Box");
KP.B(target, "Path");
KP.B(target, "Clipping");
KP.B(target, "IK Constraint");
KP.B(og_1.D.D.D.D, "Path Constraint");
KP.B(og_1.D.D.D.D, "Skin");

调用点(可读源码):mn_1(New Bone)、mo(New Slot)、mp、mq、ms_1、mt、mu_1、 mv_0、mx_0,以及树上的两个"选中类型分支" acM.java:98 / yi_0.java:28。

⚠️ 待办:KP 这个名字是可读源码树里的名字。载荷里另有一个也叫 KP 的类 (KP extends eW,是个 SelectBox 控件),那是 CFR 大小写冲突造成的重名。 真正要调的那个 KP 得先用 tools/obfmap/MapMethods 之类的办法重新定位一次, 才能从面板里反射调用 —— 这是下一步的第一步。

场景对象的删除边界(需求确认)

  • skeleton 是整个场景的根节点:删掉它等于清空全部场景对象;
  • root 是所有对象的根骨骼:只要 skeleton 在,root 就必然在,不允许删除。

所以面板做删除时,选择集里要拦住 root(skeleton 可以删)。


五、验证状态

结论 证据
et.D(Ldl;)LdR; = add、et.I()LdR; = row 面板从"按钮和文字挤在同一行"变成"按钮一行、文字区一行并铺满"(截图已确认)
dR.C()LdR; = left、dR.D()LdR; = fill、dR.StackMapTable()LdR; = expand 按钮靠左上、文字区撑满并出现滚动条
et.Z(Z)V = setFillParent 换成它之后面板铺满停靠框(先前误调 setDebug 时只会描出彩色单元格边框)
Button.String()Z = isChecked、I(Z)V = setChecked 点击能触发一次读取;用前缀自校准曾配错组合,导致反复触发
Label.D(CharSequence)V = setText 修掉 textEquals 之后面板文字才真正刷新
命令系统入口 Editor.pE.D(String) 可达 面板成功拿到命令系统并执行了 New Bone / Rulers(日志有 [cmd] 执行命令)

未逐条验证的(结构成组、按声明顺序推定):Cell 的 max/pad 系列、Button 的 isOver/isPressed、 Cell 的 growX/growY。要用到时,先用 MapMethods 重跑一遍,再在编辑器里实测。

已知未打通:命令"执行了但没生效"(New Bone / Rulers 都没看到界面变化)。 下一步要确认执行路径究竟是 pC.boolean D() 还是 pC.void D(),以及怎么设置当前选择。