阶段四收尾:面板两行布局(row/expand/fill)+ 英文输出 + 混淆名对照表

- ViewInstaller:用 Table.row()/Cell.expand()+fill()/setFillParent 排出"
  按钮一行、可滚动文字区一行"的面板;修掉把 setDebug 当成 setFillParent 的误用
- Scene2dPanel:按钮 isChecked/setChecked 与 Label.setText 改用对照表定名,
  修掉"点一次反复触发"与"文字不刷新"两个 bug
- SceneReader:面板输出改英文(编辑器缺中文字形,中文会渲染成方块)
- 新增 tools/obfmap/MapMethods.java:载荷 vs 上游 libGDX 的字节码指纹比对工具
- 新增 混淆名对照表.md:类/方法/字段映射、推导办法、已踩的坑、验证状态
This commit is contained in:
tianmo committed 2026-10-16 14:06:42 +08:00
1 parent 2e7ab7c33c
commit 8f7b72271f
8 files changed
+677 -52

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
# 混淆名对照表(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` 组装之后、分组列表生成之前插 |
---
## 五、验证状态
| 结论 | 证据 |
| --- | --- |
| `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` 之后面板文字才真正刷新 |
未逐条验证的(结构成组、按声明顺序推定):`Cell` 的 max/pad 系列、`Button` 的 `isOver/isPressed`、
`Cell` 的 `growX/growY`。要用到时,先用 `MapMethods` 重跑一遍,再在编辑器里实测。