# 混淆名对照表(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`(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.` 末尾 `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 处成员引用)。