- 新工具 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 增补类/方法对照与大小写同名类坑。
334 lines
21 KiB
Markdown
334 lines
21 KiB
Markdown
# 混淆名对照表(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()`,以及**怎么设置当前选择**。
|