编辑器交互/数据 API 调研完成:解出 KP=SelectBox、选中链路、增删改真实落点与撤销栈

- 新工具 tools/decrypt/DumpSites.java:逐 indy 站点解出『类.方法 + 描述符』(带描述符,
  解决 rewrite_source.py 丢描述符导致的重载混淆);ResolveSites.java 走运行时 CallSite 交叉验证。
- 结论改正:KP = libGDX SelectBox,KP.B(String)V = setSelected;XW/acW/acK/Yr 是
  『属性面板控件』而不是场景数据;场景对象是树节点上的 GP/Gp/GU/GO。
- 补齐:当前选择怎么设(VZ.D(Object))、谁刷新界面(VZ.D(GP,ZZ) / rW.B / fC.C)、
  撤销栈(载荷类 un 的 D()/B() 成对包一层)、删除边界(root 的删除按钮本来就是 disabled)、
  查/选/改名/删除/导出的真实实现与出处。
- 新增运行时探针 -Dspine.probe=objects(SceneObjects.java),实测打印整棵场景对象树。
- 文档:编辑器交互与数据API.md 重写;混淆名对照表.md 增补类/方法对照与大小写同名类坑。
This commit is contained in:
tianmo committed 2026-10-16 14:38:25 +08:00
1 parent 2b23ce59db
commit 7fbb042aa6
6 files changed
+990 -126

No files matched your search

+86 -27
View File
@@ -197,14 +197,15 @@ OG.D → com.esotericsoftware.spine.editor.Editor
> 注意:`pE.D(String)` 只看"找得到找不到",**不看动作有没有真的生效**。
> 实测 `-Dspine.runCommand="New Bone"` 打印"执行完成"但骨骼树没变 —— 因为
> `mn_1.D()` 的源码是:
> `mn`(New Bone)的字节码是:
> ```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; // 没选中 → 什么都不做,也不报错
> 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`):
@@ -220,32 +221,25 @@ OG.D → com.esotericsoftware.spine.editor.Editor
**面板里的接口**:[`viewpanel/src/com/spineview/Commands.java`](viewpanel/src/com/spineview/Commands.java)
(`Commands.run(loader, "New Bone")`),验证入口是启动参数 `-Dspine.runCommand="<命令名>"`。
### 真正"建出一个能显示的骨骼"落在哪
### 真正"建出一个能显示的骨骼"落在哪(⚠️ 2026-10-16 更正)
用户在编辑器里新建骨骼是**交互式**的:先选父级骨骼,再用 Create Tool 在视口里点一下/拖一下。
菜单里的 `New Bone` 只是同一条路的"用当前选择当父级"版本 —— 所以没选中时它直接 `return false`。
把所有 `New X` 菜单动作摊开看,它们最后都落到**同一个静态方法**上(反编译源码里的名字):
**上一轮这里是错的**:`KP.B(target, "Bone")` 既不是"建对象 API",
`target` 也不是"父级场景数据"。真实情况(`tools/decrypt/DumpSites.java` 逐站点解密的实测结果):
```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(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 // ← 就是它
```
调用点(可读源码):`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`(载荷名)= `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`。
> ⚠️ 待办:`KP` 这个名字是**可读源码树里**的名字。载荷里另有一个也叫 `KP` 的类
> (`KP extends eW`,是个 SelectBox 控件),那是 CFR 大小写冲突造成的重名。
> 真正要调的那个 `KP` 得先用 `tools/obfmap/MapMethods` 之类的办法重新定位一次,
> 才能从面板里反射调用 —— 这是下一步的第一步。
调用点(可读源码):`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`。
### 场景对象的删除边界(需求确认)
@@ -256,7 +250,72 @@ KP.B(og_1.D.D.D.D, "Skin");
---
## 五、验证状态
## 四之三、编辑器结构 / 场景模型(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 → 骨架对象 | 查 |
| 结论 | 证据 |
| --- | --- |