Files
Spine_lab/混淆名对照表.md
T
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

463 lines
29 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`(New Bone)的字节码是:
> ```java
> VY sel = OG.D:LVZ.D; // Tree 视图 → 当前选中对象的属性面板
> if (sel instanceof XW) { ((XW)sel).D.setSelected("Bone"); return true; } // 没选中 → 什么都不做
> if (sel instanceof acK) { ((acK)sel).D.setSelected("Bone"); return true; } // 也不报错
> return false;
> ```
> 也就是说 **这些动作全都作用在"当前选择"上**(改下拉框 / 点属性面板按钮),
> 要先能设置选择,才谈得上增删改。详见 [`编辑器交互与数据API.md`](编辑器交互与数据API.md) §4。
可用的命令名(= `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="<命令名>"`。
### 真正"建出一个能显示的骨骼"落在哪(⚠️ 2026-10-16 更正)
**上一轮这里是错的**:`KP.B(target, "Bone")` 既不是"建对象 API",
`target` 也不是"父级场景数据"。真实情况(`tools/decrypt/DumpSites.java` 逐站点解密的实测结果):
```java
// 载荷 mn(New Bone)里的两个 indy 站点:
IjFxVlIo : (Object)String -> static OG.Z(Ljava/lang/String;)Ljava/lang/String; // 字符串解密
hEjBGGtT : (Object,Object)V -> virtual KP.B(Ljava/lang/String;)V // ← 就是它
```
- `KP`(载荷名)= `com.badlogic.gdx.scenes.scene2d.ui.SelectBox`(`extends eW`,字段里有 `SelectBox$SelectBoxStyle`);
`KP.B(String)V` = `setSelected(T)`、`KP.B()Ljava/lang/String;` = `getSelected()`、`KP.B(I)V` = `setSelectedIndex(int)`。
- `((XW)sel).D` 解析到父类 `Yr.D:LKP;`,也就是**属性面板上的类型下拉框**;
设值会触发它的 ChangeListener(载荷类 `Yi` / `acM`),那里才真正 `new Gp()` / `GP.D(...)` / `VZ.D(...)`。
- 错因:`rewrite_source.py` 按混淆名改名、**丢掉描述符**,三个重载全被写成 `KP.B`。
调用点(可读源码):`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`。
### 场景对象的删除边界(需求确认)
- `skeleton` 是**整个场景的根节点**:删掉它等于清空全部场景对象;
- `root` 是**所有对象的根骨骼**:只要 skeleton 在,root 就必然在,**不允许删除**。
所以面板做删除时,选择集里要拦住 `root`(skeleton 可以删)。
---
## 四之三、编辑器结构 / 场景模型(2026-10-16 新增,逐条有字节码出处)
### 推导办法(本轮用的就是这一条,比按名字改名可靠)
```bash
# 1) 载荷方法全 public 化(indy 引导方法是 private 的,反射调不了)
python3 tools/decrypt/make_public.py <载荷> /tmp/payload-public.zip
# 2) 编译逐站点解码器
J8="$PWD/tools/jdk8u504-b01/Contents/Home/bin"
ASM="$PWD/tools/asm/asm.jar:$PWD/tools/asm/asm-tree.jar"
"$J8/javac" -cp "$ASM" -d tools/decrypt/build tools/decrypt/DumpSites.java
# 3) 把每个 invokedynamic 站点的**密文**按 BSM 自己的算法解出来
"$J8/java" -cp "tools/decrypt/build:$ASM:/tmp/payload-public.zip" \
com.spineview.dec.DumpSites /tmp/payload-public.zip /tmp/sites.tsv [类名...]
```
密文里存的就是 `[种类][类名长度][类名][方法名长度][方法名][描述符]`(`0xB8`=static、`0xB6`=virtual),
所以解码结果**带描述符**,重载不会混淆。全量结果在 `~/Desktop/spine398/sites.tsv`(55637 行)。
### ⚠️ 坑:载荷里有大量"只差大小写"的类名
`OG`/`og`、`XW`/`xW`、`Yr`/`yR`、`Gp`/`GP`、`VZ`/`vz`、`VO`/`vo` … 是**不同的类**。
macOS 文件系统大小写不敏感,解到目录里会互相覆盖 —— 所以核对时必须
`javap -cp <载荷.zip> <类名>`(直接读 zip)或用 python 解析,别解到磁盘。
### 类
| 载荷名 | 真实含义 | 依据 |
| --- | --- | --- |
| `OG` | **编辑器全局静态持有者**(34 个字面都叫 `D` 的静态字段:Editor / Tree 视图 VZ / Tree 控件 Uq / Skin / Stage / Prefs / 模型助手 UH…) | `javap -p OG`;`Uq.<init>` 末尾 `putstatic OG.D:LUq;` |
| `og` | "Tree View" 菜单动作(`ll.java:60` 注册) | `javap -p og`、`ll.java` |
| `VZ` | **Tree 视图 Actor**(`extends et`(Table),`implements Runnable`) | `javap -p VZ` |
| `Uq` | libGDX `Tree` 控件(`public final fC B` = Selection) | `javap -p Uq` |
| `eM` / `Uo` | libGDX `Tree.Node` / 编辑器子类;`eM.D:Ljava/lang/Object;` = 节点对象 | `javap -p eM` |
| `fC` | libGDX `Tree.Selection`(`D()` = getLastSelected,`D()LfI;` = items,`C()` = clear,`B()` = isEmpty) | 逐站点表 |
| `fI` | libGDX `Array`(`Object[] D` + `int D`) | `javap -p fI` |
| `Gj` | 场景对象基类 | `javap -p Gj` |
| `GP` | **骨架对象**(`toString()` = 名字;`D(String)LGp;` 按名找骨骼) | `javap -p GP`、`GP.java:1502` |
| `Gp` | **骨骼**(`D:LGp;` 父骨骼、`D:LGP;` 所属骨架、`L()V` 从模型移除) | `javap -p Gp`、`vq_0.java:45` |
| `GU` | **插槽** | `javap -p GU` |
| `GT` / `GO` | **附件**(GO 是实测挂在插槽节点上的那个) | `javap -p`、探针输出 |
| `VY` → `VO` → `Yr` → `XW`/`acK`… | **属性面板控件**链(`extends Table`);`VZ.D:LVY` 就是"当前显示的那个" | `javap -p`、`vn_0.java:77,85` |
| `VN`(可读名 `vn_0`) | 属性面板工厂:把每种对象的属性面板 `new` 出来注册 | `vn_0.java:77,85` |
| `uh`(可读名 `uh_0`) | **模型编辑助手**(加骨骼/插槽/附件、新建事件/动画…) | `uh_0.java`、`javap -p uh` |
| `un` | **撤销/重做栈**:`D()`/`B()` 成对包一次编辑,`B(Object)` 记录对象 | `javap -c un`、`vq_0.java:44-49` |
| `pB` / `lo` / `pn` / `pt` / `md` / `mn` / `mo` | `Delete` / `Rename` / `Select - Bones` / `Select - Attachments` / `Export` / `New Bone` / `New Slot` 菜单动作 | `ll.java:136-206` |
| `KP` | **`com.badlogic.gdx.scenes.scene2d.ui.SelectBox`**(属性面板上的类型下拉框) | `javap -p KP`、`DumpSites` 解码 |
| `Yi`(可读名 `yi_0`) | 类型下拉框的 ChangeListener:**真正建对象的地方** | `yi_0.java:27-80` |
| `Vq`(可读名 `vq_0`) | 删除确认框的"确定"回调:**真正的删除 + 撤销** | `vq_0.java:27-62` |
### 方法(都是"名字 + 描述符")
| 载荷签名 | 真实方法 | 用途 |
| --- | --- | --- |
| `VZ.D(Ljava/lang/Object;)LUo;` | Tree 视图:按场景对象选中节点 | 设置"当前选择" |
| `VZ.B()Ljava/lang/Object;` | Tree 视图:取当前选中对象 | 读"当前选择" |
| `VZ.D(LfI;)LfI;` | 收集**已选中**对象的数组 | 多选查询 |
| `VZ.D(LGP;ZZ)V` | 为某个模型对象插入/重建树节点 | 刷新(新建/删除后) |
| `VZ.D(LVY;)V` | 切换当前显示的属性面板 | 内部 |
| `VZ.invoke()V` | `L(null)` = 清搜索框 | 刷新 |
| `Uq.D(Ljava/lang/Object;)LUo;` | libGDX `Tree.findNode` | 内部 |
| `KP.B(Ljava/lang/String;)V` / `B()Ljava/lang/String;` / `B(I)V` | `SelectBox.setSelected` / `getSelected` / `setSelectedIndex` | 新建对象用 |
| `un.D()V` / `un.B()V` / `B(Ljava/lang/Object;)V` | 撤销栈:开始 / 提交 / 记录对象 | 撤销 |
| `GP.D(Ljava/lang/String;)LGp;` | 骨架:按名字找骨骼 | 查 |
| `Gp.L()V` | 骨骼:从骨架里移除自己 | 删 |
| `OG.D(LUo;)LGP;` | Tree.Node → 骨架对象 | 查 |
| 结论 | 证据 |
| --- | --- |
| `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()`,以及**怎么设置当前选择**。
---
## 七、ScrollPane 的方法映射(2026-10-16 踩坑确认)
| ScrollPane 载荷签名 | 真名 | 依据 |
| --- | --- | --- |
| `B(ZZ)V` | **`setScrollingDisabled(x, y)`** | MapMethods 对 gdx-1.11.0 指纹比对;**踩坑确认**:一直当它是 `setForceScroll`,传了 `(false, true)`,结果把纵向滚动关掉了 —— 现象就是"面板列表滚不动"。正解是 `(false, false)` 恢复双向滚动 |
| `L(FFFF)V` | `scrollTo(x, y, width, height)` | 同上 |
| `D(Ldl;)V` / `D(Ldl;Z)Z` | `addActor` / `removeActor` | 同上 |
> 教训也写进技能了:**新确认的映射必须当场写进 `tools/deobf/anchors.tsv` 并同步 `names.tsv` / `src-named`**,
> 不许只留在对话里(见 `.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 处成员引用)。