目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示), 点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。 本轮打通: 1. 编辑器在我们自己的进程里完整启动 - 类加载器必须是 URLClassLoader 本身(自检要求) - Editor.main 必须在 AWT EDT 上调用 - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身 2. 3.8.99 全量解密 → 可读源码 - 字符串 17251/17461(伪造同名调用帧对齐 key) - 混淆动态派发还原 53871 处(调引导方法取 MemberName) 3. 自建视图 - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检) - 面板 Actor 由外部反射填进 us.spineView.D - 复用已有图标做别名(不新建图片资源) 最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开 另外: - ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩 - DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成 - 阶段四-视图面板挂载.md:完整记录做法与踩过的坑 不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、 Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
165 lines
9.3 KiB
Markdown
165 lines
9.3 KiB
Markdown
# 阶段二 · 粒子面板实现(把力量放在已经跑通的那条线上)
|
||
|
||
阶段一把「往编辑器内部塞控件 / 掏对象」的路走完了:`addActor` 不渲染、改内部控件会 fatal error、
|
||
面板没有运行期注册 API。阶段二换了思路:**不再动编辑器内部结构**,只做四件事,
|
||
全部落在已经验证成功的两条链路上(窗口内 AppKit 面板 + `Spine --import` 导出链路)。
|
||
|
||
---
|
||
|
||
## 0. 本轮结论速览
|
||
|
||
| 项 | 状态 | 证据 |
|
||
| --- | --- | --- |
|
||
| 面板可拖动 / 可折叠,不遮右侧面板 | ✅ 已实现并在编辑器窗口内挂载成功 | `/tmp/spine-panel.log`:`面板已挂进编辑器窗口(340x474)`;拖动/折叠由自测台断言 |
|
||
| 面板内粒子参数 + 自有数据模型 | ✅ 已实现(26 项参数、多发射器、JSON 落盘) | 自测台 15 项断言全过,面板截图见 `/tmp/spine-panel-dragged.png` |
|
||
| 定向少量读取拿选中对象 | ⚠️ 通道已通、边界已收紧;编辑器类加载器还没抓到,读不到时面板会明说 | 探针返回 `count=-1`(`/tmp/spine-panel.log` 的 `auto probe`) |
|
||
| 面板产出 Spine JSON → `Spine --import` | ✅ 端到端跑通 | 自测台生成 `SpineParticle.json` → CLI 生成 `SpineParticle.spine` |
|
||
|
||
## 1. 面板:可拖动 / 可折叠 / 不遮挡
|
||
|
||
`native-hook/ui.m` 重写:
|
||
|
||
- 面板是**叠加**在编辑器 `contentView` 上的原生 `NSView`(不缩小、不重排编辑器自己的视图,
|
||
因此不会把右侧面板挤到别处);
|
||
- 整块空白处按住即可拖动,拖出界会被夹回窗口内(至少留 120px 抓得回来);
|
||
- 标题栏右侧「折叠 ▾ / 展开 ▸」把面板收成只剩 30px 标题栏;
|
||
- 位置、高度、折叠状态随数据模型一起存盘,下次启动原样恢复;
|
||
- 编辑器重建 `contentView` 时(每 5 秒检查一次子视图)自动重新挂载。
|
||
|
||
默认落在窗口左下角,不盖右侧面板;觉得碍事就拖走或折起来。
|
||
|
||
## 2. 粒子参数与数据模型(我们自己的结构)
|
||
|
||
`native-hook/panel_model.{h,m}`:
|
||
|
||
- `SPEmitter`:目标类型(骨骼/插槽/附件)、目标名字、发射模式(持续/爆发)、速率、爆发数量、
|
||
延迟、发射时长、发射形状(点/圆/矩形/锥形)、半径/矩形尺寸、生命周期 min-max、
|
||
初速度 min-max、发射角 min-max、重力与方向、缩放 min-max、透明度起止、旋转速度 min-max、图片名;
|
||
- `SPDocument`:发射器列表 + Spine 版本 + fps + 工程名 + 面板状态,落盘到
|
||
`$SPINE_USER_HOME/Library/Application Support/Spine/spine-particles/particles.json`
|
||
(实验室运行写 `spine-lab/home`,真实安装不被碰);
|
||
- 面板里改任何一项只动这个结构,**完全不接触编辑器的对象**,所以怎么改都不会崩。
|
||
|
||
## 3. 选中对象:定向少量读取
|
||
|
||
`native-hook/selection.c` 用 JNI 从 `Gdx.app` 出发做**有硬上限**的浅层读取:
|
||
深度 ≤ 3、对象 ≤ 220、每类字段 ≤ 64、不展开数组与 Java 容器、全程清异常;
|
||
结果按「字段名/类名像不像选择」打分,列成候选交给人在面板里点选,
|
||
避免去猜混淆过的字段名。探针诊断写在 `/tmp/spine-selection-probe.txt`。
|
||
|
||
当前限制:需要一个「编辑器类加载器」引用才能按名字找 `com.badlogic.gdx.Gdx`。
|
||
抓取通道(`dlsym("JNI_OnLoad")` / `dlsym("Java_...")`)已经保留,但本机这次运行里
|
||
编辑器还没触发这些查询,探针只能返回「读不到」。读不到时面板显示
|
||
「没读到选中对象(编辑器里先选中骨骼/插槽再试)」,目标名也可以直接手填。
|
||
|
||
## 4. 导出:JSON → `Spine --import`
|
||
|
||
`native-hook/panel_import.m` 调用的命令行与已验证的连接器完全一致:
|
||
|
||
```
|
||
Spine --update 4.3.xx --input SpineParticle.json --output SpineParticle.spine --import SpineParticle
|
||
```
|
||
|
||
要点(都是实测踩出来的):
|
||
|
||
- 子进程必须**剔除 `DYLD_INSERT_LIBRARIES` / `SPINE_PLUGIN_*`**,否则 CLI 会跟着注入器一起崩;
|
||
- 可执行文件默认取「当前正在跑的这个 Spine」,可用 `SPINE_PANEL_EXECUTABLE` 覆盖;
|
||
- 输出目录 `$SPINE_USER_HOME/Desktop/SpineParticle-Export/`(实验室运行写 `spine-lab/home/Desktop/...`)。
|
||
|
||
面板导出的 JSON 结构对齐已验证的导出器:`skeleton/bones/slots/skins/animations`,
|
||
把粒子按帧烘焙成骨骼(translate/rotate/scale)+ 每个粒子的插槽(区域附件 + rgba 淡出)。
|
||
|
||
## 5. 实测到的崩溃与定位(重要)
|
||
|
||
**现象**:只要 `hook.dylib` 挂上,Spine 启动阶段就会 SIGSEGV(`libjvm+0x2977c8`,
|
||
arm64 符号被裁;x86_64 同偏移落在 `Dependencies::assert_common_3` 一带),
|
||
连阶段一留下的旧 dylib 也一样崩——**这不是本轮改动引入的**。
|
||
|
||
**二分结果**(每次启动观察 50–70 秒,进程活下来才算过):
|
||
|
||
| 变体 | 结果 |
|
||
| --- | --- |
|
||
| 空 dylib(只有一个 constructor) | 存活 |
|
||
| 只重绑定 `_dlsym`,什么都不拦 | 存活 |
|
||
| 拦 `JNI_CreateJavaVM` 但直接转交 | 存活 |
|
||
| 拦 `JNI_CreateJavaVM` + 注入 JVM 选项 | 存活 |
|
||
| + 拦 `JNI_OnLoad` | 存活 |
|
||
| + `dlsym("Java_...")` 里做类加载器探针 | 存活 |
|
||
| + 拦 `ClassLoader.defineClass1` / 给 JNIEnv 表打补丁 | **崩溃** |
|
||
|
||
**结论**:经典的「dump 类字节码」那套钩子(`defineClass1` 拦截、`DefineClass` 打补丁)
|
||
是崩溃源。它只是排查用的诊断功能,面板不需要,已改为**默认关闭**,
|
||
要排查时用 `SPINE_HOOK_DUMP_CLASSES=1` 显式打开。关掉后编辑器启动正常,
|
||
面板也如期挂进窗口。
|
||
|
||
## 6. 文件清单
|
||
|
||
```
|
||
native-hook/
|
||
hook.c 注入器:dlsym 重绑定、JVM 选项注入、面板线程;类 dump 钩子默认关闭
|
||
hook_internal.h hook.c 暴露给探针的最小接口
|
||
selection.c 选中对象的定向少量读取(带硬上限)
|
||
ui.m 粒子面板(可拖动 / 可折叠 / 参数控件 / 导出按钮)
|
||
panel_model.{h,m} 粒子数据模型 + 存盘 + Spine JSON 烘焙
|
||
panel_import.{h,m} Spine CLI 导入(剔除注入环境变量)
|
||
build.sh 编译 hook.dylib
|
||
test/panel_harness.m 面板自测台(不启动 Spine:拖动/折叠/导出/导入 + 截图)
|
||
```
|
||
|
||
## 7. 跑法
|
||
|
||
```zsh
|
||
# 编译注入器
|
||
./native-hook/build.sh
|
||
|
||
# 面板自测(不碰 Spine,15 项断言 + 三张截图)
|
||
clang -fobjc-arc -I native-hook -I native-hook/agent/include -framework Cocoa \
|
||
-o /tmp/panel_harness native-hook/test/panel_harness.m native-hook/ui.m \
|
||
native-hook/panel_model.m native-hook/panel_import.m
|
||
SPINE_USER_HOME=/tmp/spine-panel-test \
|
||
SPINE_PANEL_EXECUTABLE="$PWD/spine-lab/Spine.app/Contents/MacOS/Spine" /tmp/panel_harness
|
||
|
||
# 编辑器实例(隔离 home,面板应出现在窗口左下角)
|
||
spine-lab/启动实验室Spine.command # 或 ./spine-lab/run-lab.sh
|
||
```
|
||
|
||
环境变量:`SPINE_PANEL_AUTO_PROBE=1` 启动后自动跑一次选中探针并写日志;
|
||
`SPINE_PANEL_EXECUTABLE` 指定 CLI 用的 Spine;`SPINE_HOOK_DUMP_CLASSES=1` 打开类 dump(会崩,仅排查用)。
|
||
|
||
## 8. 下一步(按收益排序)
|
||
|
||
1. 抓住「编辑器类加载器」:把探针挂到编辑器真正加载 JNI 库的时刻(或改成从
|
||
`JNI_OnLoad` 拿 VM 后再延迟重试),拿到它以后选中读取就能真正生效;
|
||
2. 面板里加图片资源选择(用现有 `SpineParticle` 项目的图片库),让导出带图;
|
||
3. 导入成功后自动在编辑器里打开生成的 `.spine`(`open -a Spine <file>`)。
|
||
|
||
---
|
||
|
||
## 9. 读取选中对象:实测出来的安全边界(重要)
|
||
|
||
面板已经改成「一个按钮 + 一块文字区」,按钮就是读当前选中对象。为了这条链路做了一轮
|
||
严格实测,结论如下(每次都是真启动编辑器、观察进程是否存活 + 有没有 Fatal error 弹窗):
|
||
|
||
| 做法 | 结果 |
|
||
| --- | --- |
|
||
| 注入 dylib、挂面板、拖动/折叠 | ✅ 稳定 |
|
||
| 在 `dlsym("Java_...")` / `JNI_OnLoad` 回调里 `FindClass` 抓类加载器 | ✅ 稳定(已成功抓到编辑器类加载器,认得 `com.badlogic.gdx.scenes.scene2d.ui.TextButton`) |
|
||
| 在编辑器上下文里 `FindClass` 做类体检(9 个类名) | ✅ 完成(日志 `report: 类体检完成`) |
|
||
| 调编辑器类加载器的 `loadClass(class_name)` 找类 | ❌ **必崩**(Fatal error 弹窗) |
|
||
| 反射读该加载器的字段(类名表 Map) | ❌ **必崩** |
|
||
| 在 JNI 回调里做反射 / 读对象字段 | ❌ 崩(回调时 JVM 持内部锁) |
|
||
| 外部线程 `Thread.getAllStackTraces()` | ❌ 崩 |
|
||
|
||
**能拿到的**:知道编辑器自己的类加载器是谁、能在它的上下文里按名字找到类。
|
||
**拿不到的**:从我们自己的线程或 JNI 回调里去读对象内容——这套加固运行时会直接把进程打死。
|
||
|
||
因此 `hook.c` 里加了 `SPINE_PANEL_ENABLE_QUERY` 开关,**默认关闭**,
|
||
面板点下去只会显示「深度读取已停用」,不会再把 Spine 打崩。
|
||
|
||
### 结论与下一步
|
||
|
||
唯一有成功先例的路线是阶段一验证过的那条:**把我们的类 `DefineClass` 进编辑器自己的类加载器,
|
||
让代码跑在 Java 侧线程上,用普通 Java 反射去读**(阶段一就是这样读到私有字段、甚至改掉了
|
||
工具栏按钮文字)。这条路需要能编译 Java 类(本机目前没有 JDK,需要装一个;阶段一当时用的是
|
||
`/tmp` 下的 JDK 17)。
|