## 功能(面板源码,重启后仍在)
- 新增 AnimationTools:
- Animate Random Props:随机骨骼/插槽/附件 → ani_api_test
旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)
插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
- Curve Test 6 Modes:curve_test,6 根骨骼各一条旋转轴(0°→45°,0f/30f),
分别 linear / curve 平滑(0.25,0,0.75,1) / stepped / ease-in(0.42,0,1,1) /
ease-out(0,0,0.58,1) / 对称自动(0.25,0.25,0.75,0.75)
- ObjectPanel.build 里挂载这两个按钮,复用面板自己的 wire() 点击轮询
## 对照表(tools/deobf/anchors.tsv + 混淆名对照表.md 第十一节)
- GP.I=animations、GP.D:LFT;.D:F=fps、Gl=动画(name/skeleton/timelineGroups/register)
- Gl.D/B/L/I(Gp)=旋转/位移/缩放/斜切;Gl.D/B(Gp,GU)=插槽颜色/附件;Gl.D()=绘制顺序
- FH=组;FI=时间轴(runtimeTimelineClass、keys、findKey、insertKey、invalidateRuntime)
- FN=关键帧(time、curve、c2x/c2y/c3x/c3y、setAngle/setX/setY、Color、Gn)
- FV=曲线枚举(linear/curve/stepped;'自动'=curve+控制值)
- 踩坑记录:
* 关键帧数值写在 FN.D()/B()/L() 的 float 字段,**不是** FN.D():ga(ga 是 deform 的),
否则会出现'有帧但运行时不变'
* 编辑器时间轴刻度就是**帧**,关键帧时间写 0/30,不要按 1 秒=30 帧换算
* FI.D(float,int) 建帧要求时间轴已有关键帧(先有鸡问题)
* fI.removeRange 两个 (int,int) 重载都不缩容 → 清空动画直接换新 fI
names.tsv 合并成 380 条,payload-named.zip 重产(7271 处成员引用)。
验证:全部在运行中的编辑器里逐条读回(时间轴/关键帧/曲线枚举/控制值),
重启后 pid 94663 单实例,日志确认 '[animtool] 两个动画按钮已加入'。
593 lines
39 KiB
Markdown
593 lines
39 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()`,以及**怎么设置当前选择**。
|
||
|
||
---
|
||
|
||
## 七、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 处成员引用)。
|
||
|
||
---
|
||
|
||
## 十、"操作类" API(2026-10-16 热更实测通过)
|
||
|
||
这一节是"不重启、纯用编辑器自身 API 加一个按钮:弹名称面板 → 按名字匹配附件 → 在当前选中骨骼下建
|
||
15 根骨骼(每根 1 插槽 1 附件)"那次任务里用到的 API。**每条都真跑过并核对过结果**,不是从字节码推的纸面结论;
|
||
字节码出处也一并写上。
|
||
|
||
### 建对象
|
||
|
||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `UH.D(LGU;LGp;Z;LfI;)LGU;` | **addSlot**(加插槽) | 实测建出插槽。⚠️ 它会把**模板插槽整个复制**,连模板插槽上挂的那份附件一起复制 —— 新插槽上于是挂着模板的附件(本次"插槽上的附件变换没改"就是这个原因) |
|
||
| `UH.D(LGn;LGU;ZZLfI;)LGn;` | **addAttachmentCopy**(按模板复制一个附件挂到插槽) | 内部 `Gn.D(LGP;)LGn;` 复制模板附件,再登记到骨架附件表 `GP.L` |
|
||
| `GP.D(LGp;)V` | **addBone** | 字节码 `bone.B = array.size; array.add(bone)` |
|
||
| `OG.D(String;LfI;LGU;)String` | **uniqueName**(编辑器自己的不重名生成) | 实测 `OG.D("bone",骨骼数组,null)` 返回 `bone7` |
|
||
| `GU.D(LGn;Z)V` | **setAttachment**(设插槽当前附件) | 字节码:第二参=是否记撤销;按 `Editor.V`(animate/setup)**只写 `D:LGn;` 或 `B:LGn;` 其中一个** → 想让两种模式都覆盖就两个字段都写 |
|
||
| `VZ.D(LGP;ZZ)V` | **refresh**(重建层级树节点) | 每批建完都调,树里立刻能看到 |
|
||
| `un.D()V` / `un.B()V` | **undoBegin / undoCommit** | 撤销帧的开始/提交 |
|
||
| `un.Z()V` | **undo(撤销一步)** | 实测:`up` 的撤销栈 23→22、redo 栈 0→1、场景对象数同步回退。误删骨骼就是靠它连点撤回来的。同族的 `un.S()/un.C()/un.V()/un.I()` **不是**撤销(分别=跳历史下标/置标志/按下标重放/无效果) |
|
||
|
||
### 名称输入面板(编辑器自带对话框)
|
||
|
||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `xl`(抽象类) | **名称输入对话框基类**(标题/正文/名字框/OK/Cancel) | 结构实测:标题 + 正文 Label + `Kp` 名字框 + OK/Cancel 两个 ImageTextButton |
|
||
| `xl.L(Ljava/lang/Object;)V` | **onOkCancel** | 收到 `Boolean`:TRUE=OK(读名字、查重、调下面的抽象方法);FALSE=Cancel 直接 return |
|
||
| `xl.L(Ljava/lang/String;)V` | **onNameEntered(抽象)** | OK 且名字合法时被调用,参数=输入的名字。想拿名字就子类化它(本次用 ASM 现场生成子类) |
|
||
| `xl.D(Ldt;)LvH;` | **show** | ⚠️ **光调它窗口不会挂进 Stage**(实测 parent 一直 null、屏幕上什么都不出现),必须再显式 `dt.D(Ldl;)V`(Stage.addActor) |
|
||
| `xl.D:LKp;` | **nameField**(名字框) | `Kp` 是 TextField 子类:`B()String`=getText、`B(String)V`=setText |
|
||
| `vH.B(Ljava/lang/Object;)V` | **okPressed** | `javap -c vH` 里唯一调 `L(Object)` 的地方=OK 按钮回调 |
|
||
| `dT.D(Ldt;Ldk;)LdT;` | show 内部实现 | `vH.D(dt)` 里 `invokespecial` 到它;里面 `stage.D(this)V` 就是 Stage.addActor |
|
||
|
||
### Actor / Stage / WidgetGroup(定位、挂载、删除)
|
||
|
||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `dt.D(Ldl;)V` | **Stage.addActor** | 字节码 `this.D:Ldo;.addActor(actor)`(root.addActor)。用它把对话框挂上屏幕。历史上"面板列表被搬出面板"也是它干的(把 Actor 从原父级摘走) |
|
||
| `dl.D:Ldo;` / `dl.D:Ldt;` | **parent / stage**(字段) | Actor 上两个同名字段,必须**按类型**取;`dl.D()Ldt;`=getStage |
|
||
| `dl.b_()Z` | **remove** | ⚠️ `eX.b_()V`(invalidate)同名不同描述符;`getMethod("b_")` 会取到 void 那个 → "调了删除却没删掉"(本次踩到,改成按返回类型 boolean 挑才对) |
|
||
| `eX.V(Z)V` | **setFillParent** | 与 `et.C(Z)V`=setClip、`et.B(Z)V`=setDebug 区分开 |
|
||
| `dl.c_()F` `dl.d_()F` `dl.L()F` `dl.I()F` `dl.S()F` `dl.C()F` | **getX / getY / getWidth / getHeight / getTop / getRight** | 全部按 `javap -p -c dl` 对出来(c_=x、d_=y、L=w、I=h、S=y+h、C=x+w)。⚠️ `et`(Table) 自己声明了 `int L()`/`int I()`(列数/行数)→ 按名字取要**同时按返回类型 float 过滤** |
|
||
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 时是"以 (x,y) 为中心"**(不按 left/right 就按 center 算)→ 摆屏幕正中直接传 Stage 中心即可 |
|
||
|
||
### 混合模式枚举
|
||
|
||
| 字段 | 真名 | 依据 |
|
||
| --- | --- | --- |
|
||
| `iz.D` / `iz.B` / `iz.L` / `iz.I` | **normal / additive / multiply / screen** | `iz.<clinit>` 里依次 `new iz("normal",0,770,1,771)`、`("additive",1,770,1,1)`、`("multiply",2,774,774,771)`、`("screen",3,1,1,769)`,常量名是常量池明文 |
|
||
|
||
### 面板自己的接口(复用,不重复造)
|
||
|
||
| 方法 | 用途 |
|
||
| --- | --- |
|
||
| `com.spineview.ObjectPanel.wire(ClassLoader, Object, Runnable)` | 把新按钮接上点击(轮询 `isChecked` + 路由线程)——新按钮直接复用 |
|
||
| `com.spineview.SceneObjects.listAllNodes(ClassLoader)` / `selected(ClassLoader)` / `nameOf(Object)` | 罗列场景对象 / 当前选中 / 取名字 |
|
||
| `com.spineview.SceneActions.delete(ClassLoader, Object)` | 走编辑器删除流程(自带撤销),清理误建对象时用 |
|
||
|
||
> 另外踩到两个**跟 API 无关但会浪费半天**的坑,记在这里:
|
||
> 1) **HotSpot 同名 agent 类只加载一次** —— 改了 agent 代码再 attach 同一个类名,跑的还是旧代码;每次改代码都要换类名。
|
||
> 2) 编辑器字体**没有中文字形**,对话框/按钮文字写中文会显示成乱码 —— 一律用英文。
|
||
|
||
### 对话框"显示不全 / 先闪左下角"的两个修正(2026-10-16 实测)
|
||
|
||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `eX.StackMapTable()F` / `eX.init()F` | **getPrefWidth / getPrefHeight** | `eX`(WidgetGroup) 六个 Layout 方法按声明顺序 = minW/minH/prefW/prefH/maxW/maxH。实测对话框 pref=**399x166**,但窗口刚建出来时是默认 **150x150** → 正文和按钮被裁。修法:`Actor.width/height = max(pref, min, 下限)` 再 `eX.b_()V`(invalidate) |
|
||
| `eX.LineNumberTable()F` / `eX.String()F` | **getMinWidth / getMinHeight** | 同上(对话框 min=302x166) |
|
||
| `eX.b_()V` | **invalidate** | 让内容按新尺寸重新布局(和 `dl.b_()Z`=remove 同名不同描述符) |
|
||
| `dl.D(FFI)V` | **setPosition(x,y,alignment)** | **align=0 → 以 (x,y) 为中心**。"先出现在左下角、再跳到中间"的原因就是:先 `addActor`(默认 (0,0))渲染了一帧,350ms 后才居中。修法:**尺寸 + 居中都在同一次渲染回调里做完**(本次实测 399x170 @ (1665.5,937) → 中心正好 (1865,1022) = 3730x2044 的正中) |
|
||
|
||
---
|
||
|
||
## 十一、动画模型(2026-10-17 实测:新建动画 / 4 条骨骼轴 / 2 条插槽轴 / 关键帧 / 6 种曲线全部跑通)
|
||
|
||
这一节是"在动画面板下用编辑器自身 API 建 30 帧动画"时确认的整套模型。每条都真跑过并读回验证。
|
||
|
||
### 骨架 / 动画
|
||
|
||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `GP.I:LfI` | **animations(动画表)** | 元素是 `Gl`;新建动画后 `animations.add(gl)` |
|
||
| `GP.D:LFT;` → `FT.D:F` | **projectSettings.fps** | 本项目 fps=**30**。⚠️ 编辑器时间轴刻度直接用**帧**:关键帧时间写 0 和 30,不要换算成秒 |
|
||
| `Gl.D:LGP;` / `Gl.D:Ljava/lang/String;` | skeleton / name | |
|
||
| `Gl.D:LfI` | **timelineGroups(时间轴组表)** | 元素是 `FH`。清空动画最稳的做法:直接换一个**新的空 fI**(`removeRange` 的两个 `(int,int)` 重载实测都不缩容) |
|
||
| `Gl.D()V` | **register(注册到动画列表模型)** | 编辑器 New Animation 命令里就是这么做的 |
|
||
| `Gl.D(Gp)FI` / `Gl.B(Gp)FI` / `Gl.L(Gp)FI` / `Gl.I(Gp)FI` | **旋转 / 位移 / 缩放 / 斜切轴** | 运行时类依次 `ih` / `io` / `ii` / `ij` |
|
||
| `Gl.D(Gp,GU)FI` / `Gl.B(Gp,GU)FI` | **插槽颜色 / 附件切换轴** | 运行时类 `hU` / `ip` |
|
||
| `Gl.D()FI` | **绘制顺序轴** | 运行时类 `hZ` |
|
||
|
||
### 时间轴 / 关键帧
|
||
|
||
| 载荷签名 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `FH.D:Gl` / `FH.D:Gp` / `FH.D:LfI` | animation / bone / timelines | `FH` 是按"骨骼或插槽"分组的一层 |
|
||
| `FI.D:Ljava/lang/Class;` | **runtimeTimelineClass** | `ih`=Rotate、`io`=Translate、`ii`=Scale、`ij`=Shear、`hU`=SlotColor、`ip`=SlotAttachment、`hZ`=DrawOrder |
|
||
| `FI.D:LfI` | **keys(关键帧表)** | 元素是 `FN` |
|
||
| `FI.D(float)FN` | **findKey** | 按时间精确找关键帧(找不到返回 null) |
|
||
| `FI.D(FN,boolean)V` | **insertKey** | 插入关键帧(第二参=是否记撤销) |
|
||
| `FI.L()V` | **invalidateRuntime** | 标脏运行时轴(改完关键帧调它重建) |
|
||
| `FI.D(float,int)Z` | **addKeyFromPose** | 按当前姿态在指定时间建关键帧;⚠️ 时间轴上**必须已经有关键帧**,否则抛 `frameCount must be > 0` |
|
||
|
||
| `FN` 字段/方法 | 真名 | 依据 / 坑 |
|
||
| --- | --- | --- |
|
||
| `D:F` | **time** | 关键帧时间(帧) |
|
||
| `D:FV` | **curve** | 曲线类型,见下 |
|
||
| `B/L/I/S:F` | **curveC2x / c2y / c3x / c3y** | 贝塞尔四个控制值(新建关键帧默认 0.25 / 0 / 0.75 / 1) |
|
||
| `D(float)` / `D()F` | **setAngle / getAngle** | 旋转轴的角度。⚠️ **不是** `FN.D():ga`! |
|
||
| `B(float)` / `B()F` | **setX / getX** | 位移/缩放/斜切的 x |
|
||
| `L(float)` / `L()F` | **setY / getY** | 位移/缩放/斜切的 y |
|
||
| `D:Color` | **color** | 插槽颜色关键帧 |
|
||
| `D:Gn` | **attachment** | 附件切换关键帧(null = 隐藏) |
|
||
| `D:Or` / `D():ga` | **valueSchema / values** | **deform** 用的数值数组(旋转/位移/缩放/斜切不要写这里——曾因此"帧有值但不生效") |
|
||
|
||
### 曲线枚举 `FV`(名字取自 `<clinit>`)
|
||
|
||
| 字段 | 真名 |
|
||
| --- | --- |
|
||
| `FV.D` | **linear(线性)** |
|
||
| `FV.B` | **curve(贝塞尔)** |
|
||
| `FV.L` | **stepped(阶跃)** |
|
||
|
||
"自动"不是单独的枚举值,而是 **curve + 一组控制值**(本次实测过 0.25,0.25,0.75,0.75 的对称自动;ease-in 用 0.42,0,1,1;ease-out 用 0,0,0.58,1)。
|
||
|
||
### 面板按钮(正式源码)
|
||
|
||
- `viewpanel/src/com/spineview/AnimationTools.java`
|
||
- **Animate Random Props**:随机骨骼/插槽/附件 → `ani_api_test`:旋转 0°→90°、位移 (0,0)→(120,-60)、缩放 (1,1)→(1.6,0.6)、斜切 (0,0)→(25,15)、插槽颜色 (1,1,1,1)→(1,0.2,0.2,0.25)、附件 显示→隐藏;全部只在 0f/30f
|
||
- **Curve Test 6 Modes**:`curve_test`:6 根骨骼各一条旋转轴(0°→45°,0f/30f),分别 linear / curve 平滑 / stepped / ease-in / ease-out / 对称自动
|