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

275 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 混淆名对照表(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`/…) | `<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()` 的源码是:
> ```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()`,以及**怎么设置当前选择**。