Files
mtgodot-poc/docs/reference/steps/00-oracle.md
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

185 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 00 · Oracle —— Windows 真值源工具
> 总纲:[`../PLAN.md`](../PLAN.md) §07。本文件是 oracle 这条**关键路径**的详细施工文档。
> oracle 不是一个里程碑,但它和 M0 并行起步,M0/M1/M2 的主验证全部依赖它。
---
## 目标
在 Windows 侧建一个精简 harness,链接**锁定版本**的 `granny2.dll` + 抽出来的 `EterGrnLib`
对"给定 `.gr2` + `.msa` + 时刻 `t`"能稳定导出:
1. 两层骨骼世界矩阵(① 裸 Granny,② 过完 EterGrnLib
2. mesh 0 蒙皮后的顶点坐标
3. 确定态截图(固定相机 / 光 / model clock / 单动作)
这套 dump 是 Windows-only,永远留在 Windows。
## 前置
- 无硬前置,可与 [M0](./M0-gr2-reader.md) 第一天并行。
- 需要一个会 Windows/MSVC + D3D9 的人(技能和写 libgr2 不同,见 PLAN §09 人力硬约束)。
## 两条轨(关键点)
oracle 分成两半,混在一起会误判工期:
| 轨 | 任务 | 依赖 | 谁需要 | 状态 |
|---|---|---|---|---|
| **Oracle-Lite** —— 只用 `granny2.dll` C API | T1 · T3 · T5 · T6 · T7 · T8 | 一个 `granny2.dll` + 几百行 D3D9 | M0 / M1 / M2 的验证全靠它 | **必做,关键路径** |
| **Oracle-Full** —— 加上抽出来的 `EterGrnLib` | T2 · T4 | 从完整 D3D9 客户端里拆 `EterGrnLib` + 依赖 | 只有正式移植的"层②"对拍 | **可延后**demo/PoC 不阻塞 |
> M2 的门禁只对**层①(裸 Granny)**。层② 是给正式移植准备的。所以 **Oracle-FullT2/T4)不在 demo 关键路径**`EterGrnLib` 拆不干净就整体延后。
## 交付物
| 产物 | 位置 | 轨 |
|---|---|---|
| `granny_probe.exe`(读 gr2 → 打 FileInfo | `oracle/` | Lite |
| `oracle` CLI`oracle dump <model.gr2> <anim.gr2> <t> -o out.bin` | `oracle/` | Lite |
| 最小 D3D9 渲染器(骨架+mesh→PNG,不依赖 EterGrnLib | `oracle/render/` | Lite |
| 锁定的 `granny2.dll` + 判定记录 | `oracle/vendor/` + `oracle/GRANNY-VERSION.md` | Lite |
| dump 二进制格式规范(含坐标约定) | `oracle/FORMAT.md` | Lite |
| 噪声地板测量 | `test/noise_floor.json` | Lite |
| 自检脚本(CI Windows runner | `oracle/selfcheck.*` | Lite |
| `oracle_etergrn` 静态库 + 层② dump | `oracle/full/` | Full(延后)|
---
## 构建顺序(依赖图)
```
T1(锁 DLL + granny_probe) ─┬─▶ T3(层① dump) ─┬─▶ T5(蒙皮顶点 dump) ─▶ T8(噪声地板)
│ │ │
│ └─▶ T7(自检, 先用 1e-4) ◀──┘ T8 后收紧
└─▶ T6(确定态截图 · 独立最小 D3D9) ← M1 视觉验证依赖
[延后 / Oracle-Full] T1 ─▶ T2(抽 EterGrnLib)[L] ─▶ T4(层② dump)
```
**推荐推进**:T1 → T3 → T7(早期最强信号)→ T5 → T6(M1 要)→ T8 → 收紧 T7。T2/T4 只在决定做正式移植时启动。
尺寸:**S** ≈ 0.51 天 · **M** ≈ 24 天 · **L** ≈ 1 周+
---
## 任务分解
> 每个 T 的 AC 是可勾选项。`warrior_cheongrin.gr2` 的期望 count 与 [`M0-gr2-reader.md`](./M0-gr2-reader.md) T4 对齐(第一天用 Blender 导入核对后两边一起填实)。
### T1 · 锁定 Granny 版本 + `granny_probe` **[S]**
> **已知**(M0 T2 从解压后的 gr2 里读到):资产由 **Granny Standard Exporter SDK 2.4.0.7** 导出。候选 `granny2.dll` **优先试 2.4.x 线**2.4.0.7 或最接近的 runtime)。9166 个里 11 个是文件格式 v7、其余 v6。
- 先写 `granny_probe.exe`~30 行):`GrannyReadEntireFileFromMemory``GrannyGetFileInfo` → 打印 `Skeletons/Meshes/Materials/Textures/Animations` 的 count、`FromFileName`、每 skeleton 的骨骼数。
- 收集候选 `granny2.dll`**2.4.x 优先**,再 2.6.x / 2.9.x / 2.11.x 兜底),各链一遍 `granny_probe``warrior_cheongrin.gr2` + 3 个 zone 静态件 + 1 个 `action/*.gr2`
- 选返回全部合理的那个,`git`-track 进 `oracle/vendor/granny2.dll`,判定写 `oracle/GRANNY-VERSION.md`(列出每个候选的输出)。
- **AC**(选定 DLL 下):
- [ ] `warrior_cheongrin.gr2``Skeletons==1``Meshes>=1``Animations==0``FromFileName` 是可读路径串、骨骼数 ∈ [20, 120](人形合理范围)—— 与 [M0 T4](./M0-gr2-reader.md) 期望值一致。
- [ ] `action/dance_1.gr2``Animations==1``Duration` ∈ (0, 60] 秒。
- [ ] zone 静态件:`Skeletons==0` 或 1rigid)、`Meshes>=1`
- [ ] 3+ 文件全过,其余候选 DLL 的失败表现记进 `GRANNY-VERSION.md`
### T3 · dump 层①(裸 Granny **[M]**
-`granny2.dll` C API。输入是**两个 gr2**`model.gr2`(骨架 + ModelInstance+ `anim.gr2``Animation`)。
- 链路:`GrannyReadEntireFileFromMemory(model)``GrannyInstantiateModel` → 读 `anim.gr2``Animations[0]``GrannyPlayControlledAnimation(startTime=0, anim, modelInstance)``GrannySetModelClock(modelInstance, t)``GrannySampleModelAnimations(...)``GrannyGetWorldPoseComposite4x4Array(worldPose, boneCount, 0, out4x4)`
- 输出:`boneCount` + 每骨 `float[16]`(行主序,Granny 原生)。**坐标约定写死**进 `FORMAT.md`:手系、单位、根变换是否已 apply;libgr2 侧声明同一约定。
- **AC**
- [ ] 固定 `(model, anim, t)` 连续两次 dump **byte-identical**(确定性)。
- [ ] **正确性锚**t=0 + identity 动画(或不 play 任何动画)时,层① 的每骨世界矩阵 == 该骨 `InverseWorld4x4` 求逆(`max|Δ| < 1e-4`,即 T7 折进来先跑一遍)。
- [ ] 骨骼数、骨骼名顺序与 `granny_probe` / Blender 一致。
### T5 · dump 蒙皮顶点 **[SM]**
- `GrannyNewMeshBinding(mesh, srcSkel, animSkel)` + `GrannyNewMeshDeformer(...)` + `GrannyDeformVertices(deformer, boneMatrixCount, worldPose4x4, vertexCount, srcVerts, dstVerts)`
- 另用 `GrannyCopyMeshVertices(mesh, PNT332Type, rawBuf)` 读原始顶点作对照。
- 输出 mesh 0 的 `vertexCount` + 每顶点 `float[3]`(要的话加法线)。
- **AC**
- [ ] bind poset=0 / identity)下,`GrannyDeformVertices` 的输出 == `GrannyCopyMeshVertices` 的原始顶点,`max‖Δ‖ < 1e-3`T8 后收紧到 noise_floor)。
- [ ] 顶点数与 [M0 T6](./M0-gr2-reader.md) / Blender 一致。
- [ ] 走路动画某帧的顶点与层① 世界矩阵手算 LBS 的结果一致(自洽)。
### T6 · 确定态截图 harness **[M]** —— M1 视觉验证依赖
- **独立最小 D3D9 渲染器**~200300 行,**不依赖 EterGrnLib**):`CreateDevice``CreateRenderTarget` 离屏 → 用 T3 的世界矩阵 + T5 的蒙皮顶点画三角 → `GetRenderTargetData` → 存 PNGPNG-0 / 无压缩)。
- 强制确定态:注入固定 view/proj、单方向光固定、`GrannySetModelClock(t)` 精确、单动作无混合、无 idle sway(本来就不走客户端 `ActorInstance`,所以天然没有)。
- **AC**
- [ ] 同参数两次截图 byte-identical(或 SSIM = 1.0)。
- [ ] **视觉 sanity**:warrior 是人形、直立、面朝已知方向(+Z)、不炸开 —— 和 Blender 同相机渲染目视一致。
- [ ]`t` 能看到姿势变化(动画链路真的接上了)。
### T7 · oracle 自检(信任前必须过) **[S]**
- 加载 gr2,dump t=0 层① 矩阵,对每骨验 `world_bind[i] · InverseWorld4x4[i] ≈ I``InverseWorld4x4` 来自文件)。
- **AC**
- [ ] 首轮:所有骨 `max|M I| < 1e-4`sanity 天花板)。
- [ ] T8 之后:收紧到 `max|M I| < noise_floor.mat`
- [ ] 过不了 = oracle 的坐标 / 读取有 bug**下游全部作废**,回 T3。
### T8 · 噪声地板 **[S]**
-`(model, anim, t)`:用选定 DLL 跑一遍 + 用一个相邻版本 `granny2.dll` 跑一遍(T1 收集的候选之一);再同版本重复 100 次。
-`mat`(矩阵元素 `max|Δ|`)和 `vtx`(顶点 `max‖Δ‖`)到 `test/noise_floor.json`
- **AC**
- [ ] `noise_floor.json` 产出,含 `mat` / `vtx` 两个值。
- [ ] **sanity**`mat < 1e-3``vtx < 1e-3`(模型单位)。若明显更大 → oracle 还有非确定源没关(回 T3/T6),不是"地板高"。
- [ ] M2 的 `ε_mat` / `ε_vtx` = 对应值 × 安全余量(×10)。
---
## 延后任务(Oracle-Full · 层②)
> 只在决定做正式移植时启动。demo / PoC 不需要。
### T2 · 抽 `EterGrnLib` 单独编 **[L]**
-`m2dev-client-src-main/src/``EterGrnLib` + 依赖最小集(`EterBase` 大部分、`EterLib` 数学 / `GrannyLib` 封装、`SphereLib` 视情况)。
- Windows/MSVC CMake 编成 `oracle_etergrn``#ifdef` 掉 D3D 渲染依赖(`ModelInstanceRender.cpp` 等),目标只是"加载 gr2、建 `CGrannyModelInstance`、跑动作混合、拿骨骼矩阵"。
- **AC**[ ] `oracle_etergrn` 链接通过;[ ] `new CGrannyModelInstance` + `SetModel(warrior_cheongrin.gr2)` 不崩。
- **风险**`EterGrnLib` 依赖 `EterLib` 一大坨(`GrpDevice` 等)。拆不干净 → **整体放弃层②**M0/M2 只对层① 本来就够。
### T4 · dump 层②(过 EterGrnLib **[M]** —— 依赖 T2
- `CGrannyModelInstance::Update(t)``GetBoneMatrixPointer()`,拿过完 LOD 骨骼裁剪 + `ActorInstanceBlend` 混合的最终矩阵。
- **AC**:[ ] 单动作无混合时,层② == 层①(noise_floor 内)——这是 sanity[ ] 混合场景(两动作 blend)与客户端一致(正式移植时再细化)。
---
## 门禁(Oracle-Lite = go / no-go
- **T1**:选定 DLL 下 3+ gr2 的 `GetFileInfo` 全过 AC`GRANNY-VERSION.md` 留档。
- **T7 自检**:全骨 `max|M I|` < 首轮 `1e-4`、T8 后 < `noise_floor.mat`。**这是硬门禁,不过则下游作废。**
- **T3 / T5**:对测试资产集(M0 T9c 产出)产出稳定可复现,含正确性锚(t=0 == bind pose / 原始顶点)。
- **T6**:确定态截图两次一致 + 视觉 sanity(人形直立)+ 换 t 有姿势变化。
- **T8**`noise_floor.json` 产出,`mat` / `vtx` 均 < `1e-3`(否则回 T3/T6 关非确定源)。
- Oracle-FullT2/T4**不是门禁**。
## 验证
- **三方 tie-break**:层① dump vs Blender `io_scene_gr2` 在同 `t` 计算的世界矩阵(Blender 侧写个小脚本)。三方不一致时,**先怀疑 oracle**DLL 版本 / 坐标约定 / T6 非确定源),再怀疑 libgr2。
- Blender 插件选型见 [M0 开工前 TODO](./M0-gr2-reader.md#开工前要定的-todo跑起来才能定非文档缺陷)。
## 本步风险(从 PLAN §08 筛)
| 风险 | 缓解 |
|---|---|
| granny2.dll 版本与资产不匹配 | T1 硬性锁定 + 每候选输出留档 |
| `EterGrnLib` 拆不干净(依赖 `EterLib` 渲染层) | T2/T4 是 **Oracle-Full**,本就延后。拆不动 → 整体放弃层②,M0/M2 只对层① 够用 |
| T6 需要一个 D3D9 渲染器 | 独立写 ~200300 行离屏 D3D9**不碰 EterGrnLib**(否则 T6 被 T2 阻塞,而 T6 是 M1 关键路径) |
| 确定态漏了程序化位移 | oracle 不走客户端 `ActorInstance`,天然无 idle sway;T6/T8 两次不一致就说明还有源,逐个查 |
| T7 阈值 / noise_floor 循环 | T7 首轮用 `1e-4` sanity 天花板;T8 测出真实地板后收紧 T7 |
## DoD 清单(Oracle-Lite
- [ ] `granny_probe.exe` + `oracle/vendor/granny2.dll` 锁定 + `GRANNY-VERSION.md`(含候选对比)
- [ ] `oracle dump <model.gr2> <anim.gr2> <t>` 可用;`oracle/FORMAT.md` 写清二进制布局 + 坐标约定
- [ ] T3 正确性锚:t=0 层① == bind pose`< 1e-4`
- [ ] T5 正确性锚:bind pose 蒙皮 == 原始顶点
- [ ] T6 确定态截图两次一致 + 视觉 sanity + 换 t 有变化
- [ ] T7 自检脚本在 Windows CI runner 绿(首轮 `1e-4`T8 后收紧)
- [ ] `test/noise_floor.json` 产出(`mat`/`vtx` < `1e-3`),M2 引用它设 ε
- [ ] 层① 与 Blender 三方对拍在测试资产集上一致
- [ ] Oracle-Full,可空)T2/T4 状态记录:已做 / 延后 / 放弃