# 混淆名对照表(Spine 3.8.99 载荷) 这份文档记录**载荷里被混淆的类名/方法名/字段名分别对应什么**,以及**每一条是怎么推导出来的**。 以后要动编辑器内部的 scene2d 控件(加面板、读数据、导出),都先查这里,不要重新猜。 代码里对应的是 [`viewpanel/src/com/spineview/Obf.java`](viewpanel/src/com/spineview/Obf.java), 里面有同样的常量表,改代码时直接引用。 --- ## 一、推导办法(可复现) 1. **字节码指纹比对**:`tools/obfmap/MapMethods.java` - 把载荷类与**上游 libGDX 同签名方法**逐个比对:忽略名字,只比较 "操作码序列 + 常量 + 成员的描述符"(类名按"方法内首次出现的顺序"编号,绕开两边类名不同的问题)。 - 用法: ```bash CP=tools/asm/asm.jar:tools/asm/asm-tree.jar:tools/obfmap/build "$JAVA17" -cp "$CP" MapMethods <上游.class> <载荷.class> # 输出:载荷方法签名 -> 上游方法名(= 唯一匹配,~n/m = 按声明顺序消歧) ``` - 编译: ```bash "$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`/…) | `` 里建常量、按 `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.` | Stage 实现类的构造器 | Stage 在编辑器里只是局部变量,只能钩构造器拿实例 | | `Fx` 的静态 `dl` 字段 | 一个静态 Actor | 由它 `Actor.stage` 反查 Stage(比钩构造器更省事) | | `OG.D` | 编辑器单例 | `com.esotericsoftware.spine.editor.Editor` | | `us.` | 视图枚举初始化 | 加新视图必须在 `$VALUES` 组装之后、分组列表生成之前插 | --- ## 四之二、命令系统(增删改查的入口) 编辑器的所有菜单动作都走同一套命令系统。链路(反编译源码里读出来的): ``` OG.D → com.esotericsoftware.spine.editor.Editor 单例 └─ public pE D; → 命令 / 热键系统(Editor. 里 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()` 的源码是: > ```java > 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`](viewpanel/src/com/spineview/Commands.java) (`Commands.run(loader, "New Bone")`),验证入口是启动参数 `-Dspine.runCommand="<命令名>"`。 ### 真正"建出一个能显示的骨骼"落在哪 用户在编辑器里新建骨骼是**交互式**的:先选父级骨骼,再用 Create Tool 在视口里点一下/拖一下。 菜单里的 `New Bone` 只是同一条路的"用当前选择当父级"版本 —— 所以没选中时它直接 `return false`。 把所有 `New X` 菜单动作摊开看,它们最后都落到**同一个静态方法**上(反编译源码里的名字): ```java 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()`,以及**怎么设置当前选择**。