编辑器交互/数据 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

+241 -99
View File
@@ -1,123 +1,265 @@
# 编辑器交互逻辑 & 数据 API 调研
目标:在动手写"面板里对场景对象增删改查"之前,先把编辑器**用户交互链路**和**真正生成数据的代码**摸清楚。
本文只记录查到的事实 + 出处 + 还没弄清的地方。(混淆名 ⇒ 真实名见 [`混淆名对照表.md`](混淆名对照表.md)。)
本文只记录**已核实的事实 + 出处 + 还没弄清的地方**。混淆名 ⇒ 真实名见 [`混淆名对照表.md`](混淆名对照表.md)。
> ⚠️ 上一轮的两条结论是**错的**,本文已改正(错因见 §7):
> 1. "`KP.B(父级, 类型名)` 是建对象的 API" —— `KP` 是 libGDX `SelectBox`,`B(String)` 是 `setSelected`;
> 2. "`xw_2.D` / `acK.D` 是骨架/slot 数据" —— 它们是**选中对象的属性面板控件**,不是场景数据。
---
## 一、交互层:工具(Tool)
## 0. 读载荷的三个坑(踩过,写下来免得再踩)
所有工具都在菜单注册表 `ll` 的构造函数里注册(`~/Desktop/spine398/src-readable/ll.java`):
1. **类名大小写敏感,macOS 文件系统不敏感**。载荷里同时存在 `OG`/`og`、`XW`/`xW`、`Yr`/`yR`、`Gp`/`GP`、`VZ`/`vz` 这类**只差大小写**的类。
把 zip 解到 mac 的目录里会互相覆盖(`javap -cp <目录>` 会给出自相矛盾的答案)。
→ 一律**直接从 zip 读**(`javap -cp <载荷.zip> <类名>`,或 `python3 /tmp/clsdump.py <zip> <类名>`)。
2. **同名不同描述符**:`OG` 上有 **34 个**都叫 `D` 的静态字段(`D:LEditor;`、`D:LVZ;`、`D:LUq;`、`D:LSkin;`…)。
反射取字段必须**名字 + 类型**一起判。
3. **可读源码里的方法名不带描述符**:`rewrite_source.py` 只按"混淆名 → 类.方法"改名,
于是 `SelectBox.getSelected()` 与 `SelectBox.setSelected(String)` 都被写成 `KP.B`,读起来像同一个方法。
→ 需要精确名字时查 `~/Desktop/spine398/sites.tsv`(本轮新产出的**逐站点实测表**,见 §8)。
| 菜单名 | 动作类(CFR 名) | 备注 |
| --- | --- | --- |
| `Create Tool` | `oo` | 新建对象用(靠视口点拖) |
| `Scale/Rotate/Translate/Shear/Pose/Weights Tool` | `oi_1` / `oj_0` / `ok_1` / `om` / `on_0` / `op` | |
| `Last Tool` | `oq_1` | 切回上一个工具 |
---
`oo`(Create Tool)点下去做的事:
## 1. 全局入口:`OG`(编辑器静态持有者)
```java
og_1.D.D.D.I(true); // 打开"创建"交互(og_1.D = 编辑器上下文单例,后面是逐层嵌套的字段)
```
`OG` 是一个只有静态字段/方法的类,编辑器的一切都挂在它上面(`javap -p -cp <载荷> OG`):
→ 新建对象本身是**交互式**的:工具打开后由视口里的按下/拖动事件驱动;菜单里的 `New Bone`
只是"用当前选择当父级"的等价入口(见下)。
## 二、层级树(Tree 视图)与节点模型
- Tree 视图 Actor = `VZ`(2819 行,`us.Tree` 面板的内容类)。
- 树节点 = `uo_2`(Node),每个节点包一个**包装对象**:
| 包装类 | 代表的场景对象(按用法推断) |
| 字段(载荷名 + 描述符) | 真实含义 |
| --- | --- |
| `xw_2` | skeleton 节点(`xw_2.D` = 骨架数据) |
| `gp_2` | bone 节点(`xw_2.java:367` 有 `"Bone: " + ((gp_2)object).D`) |
| `acK` / `acW` | slot 节点(`acK.java` 里有 "Set Parent" / "Sets the slot's parent bone") |
| `GU` | Draw Order 里的对象 |
| `Yr` | `xw_2` 的父类(一批节点包装的基类) |
| `md_1` | 节点里保存的模型引用 |
| `D : Lcom/esotericsoftware/spine/editor/Editor;` | 编辑器单例(保名类 `Editor`) |
| `D : LVZ;` | **Tree 视图 Actor**(载荷名 VZ,`extends com.badlogic.gdx.scenes.scene2d.ui.Table`) |
| `D : LUq;` | **libGDX `Tree` 控件**(载荷名 Uq,`extends ...ui.Tree`;`Uq.<init>` 末尾 `putstatic OG.D:LUq;`) |
| `D : Lcom/badlogic/gdx/scenes/scene2d/ui/Skin;` | 皮肤 |
| `D : Ldt;` | Stage |
| `D : LtD;` | 视图装配类(我们挂面板用的那个) |
| `D : Lcom/esotericsoftware/spine/editor/Prefs;` / `Prefs$Advanced` | 偏好设置 |
| `D : LUH;` | 模型编辑助手(载荷名 `uh_0`,见 §5) |
| `Z(String)String` | 字符串解密(读源码时看到 `OG.Z("Bone")` 就是它,参数其实是密文) |
| `D(LUo;)LGP;` | **Tree.Node → 场景对象**(`Uo` = Tree.Node,`GP` = 骨架对象) |
树里的固定结构(见 `VZ.java` 建节点处):`skeleton → root → bone/slot…`,
另有 `Draw Order#startcaps`、`Skins`、`Events`、`Animations` 这些 `icon-*Root` 固定根。
> 出处:`javap -p -cp <载荷> OG`;`Uq.<init>` 结尾 `putstatic OG.D:LUq;`。
### 当前选择放在哪
---
反编译源码里到处是这种读取:
## 2. 交互层:工具(Tool)与菜单
```java
et_0 sel = og_1.D; // 编辑器上下文单例
sel = sel.D; // ← 当前选择对象(菜单动作都作用在它上面)
... og_1.D.D.D // 再深一层(视图/工具相关)
```
所有工具/菜单都在菜单注册表 `ll` 的构造函数里注册(`~/Desktop/spine398/src-readable/ll.java`):
`mn_1`(New Bone)的判断就依赖它:
```java
if (sel instanceof xw_2) { KP.B(((xw_2)sel).D, "Bone"); return true; } // 选中 skeleton
if (sel instanceof acK) { KP.B(((acK)sel).D, "Bone"); return true; } // 选中 slot
return false; // 没选中/不支持 → 什么都不做
```
## 三、数据层:真正"建出对象"的 API
所有 `New X` 最后都落到**同一个方法**:
```java
KP.B(target, "Bone"); // mn_1 New Bone
KP.B(target, "Slot"); // mo New Slot
KP.B(target, "Skin Placeholder"); // mp
KP.B(target, "Bounding Box"); // mq
KP.B(target, "Path"); // ms_1
KP.B(target, "Clipping"); // mt
KP.B(target, "IK Constraint"); // mu_1
KP.B(og_1.D.D.D.D, "Path Constraint"); // mv_0
KP.B(og_1.D.D.D.D, "Skin"); // mx_0
```
另有树视图自己的分支:`acM.java:98`、`yi_0.java:28`。
**这就是"增"的直接入口**:第一个参数是**父级对象**(选中项),第二个参数是类型名。
也就是说编辑器内部不是"凭空建一根骨头",模型本身就是"挂在父对象下面的树"。
### ⚠️ 还没定死:`KP` 到底是哪个类
- 载荷里确实有个 `KP.class`,但它是 `KP extends eW` 的 **SelectBox 控件**,
只有 `public static fI D(fI)` 一个静态方法,**没有 `B`**,不是我们要的那个。
- 可读源码树里的 `KP` 来自**解码 invokedynamic 调用点**(编辑器把几乎所有调用都编译成
invokedynamic,所以 `javap` 里看不到 `invokestatic KP.B`)。调用映射在
`~/Desktop/spine398/obmap.tsv`,能看到几十条 `KP.B` / `KP.D` / `KP.L`(如 `RE mq KP.B`、`Yr gy KP.D`)。
→ 下一步:用 `tools/decrypt/` 把某个调用点(例如 `mn` 里那次)的**真实 owner 类**解出来,
或在运行时按"方法名 + 描述符(对象 + String 两参)"在类表 `LPt4.mc` 里搜候选类逐个验证。
## 四、改 / 删 / 查
| 操作 | 入口 | 说明 |
| 菜单名 | 动作类(CFR 名) | 载荷类名 |
| --- | --- | --- |
| 删 | `Delete`(`pb_0`) | 作用在当前选择上 |
| 改名 | `Rename`(`lo`) | |
| 复制 | `Duplicate`(`lp_0`)、`Cut`/`Copy`/`Paste`(`pv`/`pw`/`px_2`) | |
| 层级 | `Set Parent`(`mm_1`)、`Parent`/`Child`/`Previous Sibling`/`Next Sibling` | |
| 顺序 | `Draw Order Up`/`Down`(`lu_1`/`ls_1` 等) | |
| 查 | 树节点本身就是查询结果;`Select - Bones`/`Select - Attachments` 负责筛选 | |
| `Create Tool` | `oo` | `oo` |
| `New Bone` / `New Slot` / `New Skin Placeholder` / `New Bounding Box` / `New Path` / `New Clipping` | `mn_1` / `mo` / `mp` / `mq` / `ms_1` / `mt` | `mn` / `mo` / `mp` / `mq` / `ms` / `mt` |
| `Delete` / `Rename` / `Duplicate` | `pb_0` / `lo` / `lp_0` | `pB` / `lo` / `lp` |
| `Select - Bones` / `Select - Attachments` | `pn_0` / `pt_1` | `pn` / `pt` |
| `Export` / `Save Project` | `md` / `ly_1` | `md` / `ly` |
| `Undo` / `Redo` | `lm_0` / `nw` | `lm` / `nw` |
**撤销**:`Undo`/`Redo` 也在这套命令系统里(`lm_0`/`nw`)。菜单路径的动作会进撤销栈;
直接反射调 `KP.B` 很可能**绕过撤销** —— 真要写入时优先走菜单命令(`Commands.run`),
或先看清动作类自己有没有压栈。
出处:`ll.java:77,136,141,144-148,166-200,201-206`。
## 五、删除边界(需求确认)
**关键点:这些动作几乎都只是"点某个界面按钮"或"改某个下拉框"**,真正的逻辑在那些控件的监听器里。
- `skeleton`:整个场景的根节点,**可以删**(删它等于清空所有场景对象);
- `root`:所有对象的根骨骼,只要 skeleton 在就必然存在,**不允许删除**。
---
面板做删除时要在选择集里拦住 `root`。
## 3. 层级树模型:节点 → 场景对象(✅ 已运行时验证)
## 六、下一步(按顺序)
运行时实测(探针见 `viewpanel/src/com/spineview/SceneObjects.java`,用 `-Dspine.probe=objects` 触发):
1. 解出 `KP` 的真实类名(indy 调用映射 / 运行时按签名搜 `LPt4.mc`),确认 `B(Object,String)` 的描述符。
2. 确认"父级对象"参数的确切类型:`xw_2.D`(skeleton 节点里的骨架数据)、`acK.D`(slot 节点里的数据)。
3. 搞清选择怎么设置(`og_1.D.D` 是可直接写的字段,还是只能由视图点击设置)——
否则"对着指定对象增删改"无从指定。
4. 确认新建/删除后**谁负责刷新界面**(`VZ` 重扫模型?动作里带 invalidate?)。
5. 最后才做面板 UI:对象树列表 + 新建/删除/改名按钮。
```
Uo -> GP skeleton name=skeleton ← 最上面的 skeleton 节点
Uo -> Gp bone name=root ← root 骨骼(它的删除按钮是 disabled 状态)
Uo -> Gp bone name=bone
Uo -> GU slot name=mega_dg4
Uo -> GO GO name=mega_dg4 ← 附件
...
Uo -> Gk Gk ... ← Skins / Draw Order / Events / Animations 这些固定根
```
| 类(载荷名) | 含义 | 出处 |
| --- | --- | --- |
| `Uq` | libGDX `Tree` 控件 | `Uq extends com.badlogic.gdx...ui.Tree` |
| `eM` / `Uo` | libGDX `Tree.Node`;`eM.D:Ljava/lang/Object;` = 节点上挂的对象,`eM.D:LfI;` = children | `javap -p -cp <载荷> eM` |
| `Gj` | 场景对象基类(`GP`/`Gp`/`GU`/`GT`/`GO`… 都 extends `Gj`) | `javap -p` |
| `GP` | **骨架对象**(一个 skeleton 一份):名字 + 骨骼/插槽/附件表 | `GP.toString()` 返回名字;`GP.D(Ljava/lang/String;)LGp;` = 按名字找骨骼 |
| `Gp` | **骨骼**(`D:LGP;` 指向所属骨架,`D:LGp;` 指向父骨骼,`D:Ljava/lang/String;` 名字) | `javap -p` |
| `GU` | **插槽**(`D:LGn;` = 当前附件) | `javap -p` |
| `GO` | **附件**(实测节点上挂的就是它) | 探针输出 |
| `Gk` / `Gl` / `pL` … | 树上的分组/文件夹节点(Skins、Events、Animations…) | 探针输出 |
### 查(列对象)
| 目的 | API | 说明 |
| --- | --- | --- |
| 列**全部**节点对象(带层级) | 从 `OG.D:LUq;`(Tree 控件)出发,读它的 root 节点数组 + 逐层 `Tree.Node.children` | 探针实测有效;面板就是这么做的 |
| 列**当前选中**的对象 | `VZ.D(fI)LfI;`:把选中对象的数组填进传入的 `Array` 返回(选择为空时返回 0 个) | `javap -c VZ`:遍历 Selection 后逐个 `fI.add(node.object)` |
| 取"最后一个选中对象" | `VZ.B()Ljava/lang/Object;` = `Tree.Selection.getLastSelected().object` | 逐站点表:`VZ.B() → virtual fC.D()Ljava/lang/Object;` |
---
## 4. 选择、增、删、改(全部落到载荷字节码,逐条可复现)
### 4.1 当前选择怎么设置(问题 3)
```java
Object treeView = OG.D; // 字段 D:LVZ —— Tree 视图
treeView.D(Object sceneObject); // VZ.D(Ljava/lang/Object;)LUo; = Tree.findNode + 选中,返回 Tree.Node
treeView.B(); // VZ.B()Object = 取当前选中对象
```
`VZ.D(Object)` 的字节码:`getfield D:LUq;`(Tree 控件)→ `Uq.D(Object)LUo;`(= `Tree.findNode`)
→ 再调 `VZ.D(Uo)` 选中。**这个方法是 public、可以直接反射调用**,面板就是用它"点谁选谁"。
### 4.2 "增":`New Bone` 其实是在改一个 SelectBox(问题 1、2)
`mn`(New Bone)的字节码(`javap -c -cp <载荷> mn`):
```
getstatic OG.D:LVZ // Tree 视图
getfield VZ.D:LVY // 当前选中对象的**属性面板控件**(VY 的子类)
instanceof XW // XW = 骨架对象的属性面板
getfield XW.D:LKP; // 解析到 Yr.D:SelectBox —— 属性面板上的"类型下拉框"
... OG.Z(cipher) → "Bone"
invokedynamic hEjBGGtT : (Object,Object)V → virtual KP.B(Ljava/lang/String;)V
```
`hEjBGGtT` 的密文解出来就是 `\u00b6 2 KP 1 B (Ljava/lang/String;)V`,即
**`KP.B(String)V` = libGDX `SelectBox.setSelected(String)`**;同理
`KP.B()Ljava/lang/String;` = `getSelected()`、`KP.B(I)V` = `setSelectedIndex(int)`。
也就是说 `New Bone` = "把选中对象的类型下拉框设成 Bone",**真正建对象的代码在下拉框的 ChangeListener 里**:
| 监听器 | 内容 |
| --- | --- |
| `Yi`(`yi_0.java:27-80`) | `if (getSelected().equals("Bone")) { gp_2 bone = new gp_2(); bone.D = <选中对象>; … GP.D(bone.D, bone); GP.StackMapTable(bone.D); Editor.D(editor, bone.D, true); VZ.D(editor, bone.D, true, true); VZ.L(editor, null); }`,另有 `Slot`/`Bounding Box`/`Clipping`/`Point`/`Path`/`IK Constraint`… 分支 |
| `acM.D(fh_2,dl)`(`acM.java:98-150`) | 树上"在当前节点下新建"的同款分支 |
**结论**:没有"`create(parent, type)`"这种单函数 API。要建对象有两条路:
1. **走编辑器自己的路**(推荐):先 `VZ.D(parentObject)` 选中父级,再 `Commands.run("New Bone")` ——
编辑器自己改下拉框 → 触发监听器 → 建对象、插树节点、进撤销栈。
2. 直接复刻监听器里的模型调用(`new Gp()` → 设字段 → `GP.D(bone)`/`GP.StackMapTable()` → `Editor.D(...)` → `VZ.D(...,true,true)`),
麻烦且容易漏步骤(面板不做这条路)。
### 4.3 父级参数到底是什么(问题 2)
`XW` / `acW` / `acK` / `Yr` 这些**不是场景数据**,而是**每种对象的"属性面板"控件**(`extends VY extends Table`),
在 `vn_0.java:77,85`(载荷类 `VN`)里被成批 `new` 出来注册。
`VZ.D:LVY` 就是"当前显示的那个属性面板"。所以:
- `((XW)sel).D` = 骨架属性面板上的**类型下拉框**(SelectBox),不是骨架数据;
- 场景数据在树节点上(`eM.D:Ljava/lang/Object;`),实测类型是 `GP`/`Gp`/`GU`/`GO`。
### 4.4 "删"与"改":都是点属性面板上的按钮
`pB`(Delete,`javap -c -cp <载荷> pB`):
```java
if (OG.D:LEditor.D instanceof In) { In.D 关闭; return true; } // 有弹窗先关弹窗
if (Editor.C) { OG.D:LtD → tD.D:LTD → TD.D:LSl → Sl.Z(ImageButton).a_(); return true; }
VZ treeView = OG.D; // Tree 视图
if (treeView.D instanceof VO) { // 选中对象的属性面板
ImageButton delete = ((VO) treeView.D).L; // ← 删除按钮
if (!delete.L /*isDisabled*/) delete.a_(); // 点它(disabled 就不点)
}
```
`lo`(Rename):`if (!((VO) treeView.D).B.L) ((VO)treeView.D).B.a_();` —— 点**改名按钮**。
**删除的边界正好是需求要的**:`Delete` 会检查按钮的 disabled 位;
`root` 骨骼的按钮是 disabled(编辑器自己拦住),而 skeleton 节点可以删。
真正的删除动作在确认框回调里(`vq_0.java:27-62`):
```java
if (!ok) return;
un.D(); // ① 撤销栈:记录一次编辑
for (Tree.Node node : selection.D()) { // 遍历选中节点
Gp bone = (Gp) node.object;
bone.L(); // ② 从模型里摘掉
hd.D(set, bone.D); // 记下受影响的骨架
}
un.B(); // ③ 撤销栈:提交
rW.B(editor, true); // ④ 刷新视口
for (GP skeleton : set) VZ.D(editor, skeleton, true, true); // ⑤ 重建树节点
fC.C(); // ⑥ 清空选择
```
### 4.5 撤销栈(问题 5)
载荷类 **`un`** 就是撤销/重做栈(`javap -p -c -cp <载荷> un`):
| 方法 | 含义 |
| --- | --- |
| `un.D()` / `un.B()` | **成对使用**:一次可撤销编辑的"开始 / 提交"(内部都落到 `private static D(String 标签)`,会顺手砍掉 redo 分支) |
| `un.B(Object)` | 把某个对象记进当前编辑(存一份拷贝,`Editor.D:LsF` 提供拷贝) |
| `un.L(Object)` | 从当前编辑里移除该对象 |
| `un.L()` | 清空历史 |
| `Undo`/`Redo` 菜单(`lm_0`/`nw`) | `mv_2.D = true; og_1.D.D.B.a_()` —— 点工具栏按钮,靠 `mv_2.D` 静态标志避免递归入栈 |
**结论**:直接反射改模型(`bone.L()` 之类)**不会**自动进撤销栈,必须自己 `un.D()` … `un.B()` 包一层;
走菜单/按钮的那条路天然带撤销。面板优先走菜单命令。
---
## 5. 模型编辑助手 `uh_0`(载荷类,`OG.D:LUH;`)
`uh_0`(`uh_0.java`,1700 行)是"直接改模型"的 API 集合(`javap -p -cp <载荷> uh`):
| 方法 | 作用(按调用点推断) |
| --- | --- |
| `D(LGp;LGp;Z…)LGp;` | 加骨骼 |
| `D(LGU;LGp;Z…)LGU;` | 加插槽 |
| `D(LGn;LGU;ZZ…)LGn;` | 加附件 |
| `D(LGT;ZZ)V` / `D(LGT;)V` | 附件相关 |
| `B(GP;LFL;)V` / `L(GP;LFL;)V` / `D(GP;LFL;)V` | 新建事件 / 新建动画(会弹"输入名字"对话框) |
它是 `New X` 里"建完对象之后往模型里塞"的那部分;除非要绕开 UI,否则不必自己调。
---
## 6. 查 / 选 / 导出(问题 6)
| 操作 | 真实实现 | 出处 |
| --- | --- | --- |
| `Select - Bones` | 先 `fC.C(tree)`(清空选择),再对每个骨骼 `VZ.D(editor, bone)` 逐个选中,最后 `VZ.ConstantCallSite/MethodHandle/F(...)` 刷新 + `SX.LineNumberTable(...)` 刷新属性面板 | `pn_0.java:22-36` |
| `Select - Attachments` | 同上,只是查的是附件 | `pt_1.java:22-36` |
| `Rename` | 点属性面板的改名按钮 → 就地编辑名字 TextField(改完由监听器写回模型) | `lo.java:15-26` |
| `Delete` | 见 §4.4(含确认框 `Confirm Delete` 与撤销栈) | `pb_0.java:17-42`、`Yr.java:353-380`、`vq_0.java:27-62` |
| `Export` | `md.D()` = `og_1.D.D.S.a_()`,点"导出"按钮弹出导出配置框 | `md.java:13-19` |
| 属性面板刷新 | `SX.LineNumberTable(og_1.D.D.D.D)` | `pn_0.java:35` |
> **导出链路(本轮确认)**:阶段二用的"面板产出 JSON → 命令行 `Spine --import`"是**进程外**方案;
> 现在面板跑在编辑器进程内,`Export` 就是编辑器自己的菜单动作,直接 `Commands.run("Export")` 即可,
> 不再需要外部 CLI(外部方案仍可作兜底,见 `阶段二-面板实现.md:55-70`)。
---
## 7. 上一轮两条错误结论的错因(复盘)
1. `KP.B(parent,"Bone")` 被当成"建对象 API":`rewrite_source.py` 按**混淆名**改名、丢了描述符,
`SelectBox.getSelected()`/`setSelected(String)`/`setSelectedIndex(int)` 全被写成 `KP.B`。
实测解码(`tools/decrypt/DumpSites.java`)才是真相:`virtual KP.B(Ljava/lang/String;)V`。
2. `xw_2.D` 被当成"骨架数据":CFR 把 `XW.D` 解析到了父类 `Yr.D:SelectBox`,而 `xw_2` 本身
**不是**树节点,是骨架的**属性面板控件**;场景数据是树节点上的 `GP`/`Gp`/`GU`。
---
## 8. 本轮新增的工具(都可复现)
| 工具 | 用途 | 用法 |
| --- | --- | --- |
| `tools/decrypt/DumpSites.java` | **逐站点**解出每个 indy 调用的"真实类.方法 + 描述符" | `java -cp "asm.jar:asm-tree.jar:tools/decrypt/build:载荷(全 public 化)" com.spineview.dec.DumpSites <载荷> <out.tsv> [类名…]` |
| `tools/decrypt/ResolveSites.java` | 同上,但走运行时 `CallSite` 解析(交叉验证用) | `java ... com.spineview.dec.ResolveSites <载荷> <类名…>` |
| `tools/decrypt/make_public.py <载荷> <输出zip>` | 把载荷方法全 public 化(indy 引导方法要能调) | 见 `混淆名对照表.md` §一 |
| `~/Desktop/spine398/sites.tsv` | 全载荷 55637 个站点的解码结果(`caller / indyName / siteDesc / decoded / nearbyLdc`) | 查"某方法里的调用到底是什么" |
| 探针 `-Dspine.probe=objects` | 运行时把树里的场景对象整份打到日志 | 见 `viewpanel/src/com/spineview/SceneObjects.java` |
---
## 9. 还没弄清 / 还没验证的点
1. `Commands.run("New Bone")` 在**已选中父级**时是否真能建出骨骼(本轮要实现并截图验证)。
2. `Delete` 的确认框是否对所有对象类型都弹(目前只知道骨骼会弹)。
3. 属性面板控件(`Yr`/`acW`)上各按钮的语义(`VO.D`/`VO.B`/`VO.L` 已确认是"某几个按钮",逐个对应关系未逐条验证)。
4. `un.D()/un.B()` 的精确语义("开始/提交"是从 `vq_0` 的用法推断的),以及能否由面板安全地手工包一层。