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
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# 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-Full(T2/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.5–1 天 · **M** ≈ 2–4 天 · **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` 或 1(rigid)、`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 蒙皮顶点 **[S–M]**
|
||||
|
||||
- `GrannyNewMeshBinding(mesh, srcSkel, animSkel)` + `GrannyNewMeshDeformer(...)` + `GrannyDeformVertices(deformer, boneMatrixCount, worldPose4x4, vertexCount, srcVerts, dstVerts)`。
|
||||
- 另用 `GrannyCopyMeshVertices(mesh, PNT332Type, rawBuf)` 读原始顶点作对照。
|
||||
- 输出 mesh 0 的 `vertexCount` + 每顶点 `float[3]`(要的话加法线)。
|
||||
- **AC**:
|
||||
- [ ] bind pose(t=0 / identity)下,`GrannyDeformVertices` 的输出 == `GrannyCopyMeshVertices` 的原始顶点,`max‖Δ‖ < 1e-3`(T8 后收紧到 noise_floor)。
|
||||
- [ ] 顶点数与 [M0 T6](./M0-gr2-reader.md) / Blender 一致。
|
||||
- [ ] 走路动画某帧的顶点与层① 世界矩阵手算 LBS 的结果一致(自洽)。
|
||||
|
||||
### T6 · 确定态截图 harness **[M]** —— M1 视觉验证依赖
|
||||
|
||||
- **独立最小 D3D9 渲染器**(~200–300 行,**不依赖 EterGrnLib**):`CreateDevice` → `CreateRenderTarget` 离屏 → 用 T3 的世界矩阵 + T5 的蒙皮顶点画三角 → `GetRenderTargetData` → 存 PNG(PNG-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-Full(T2/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 渲染器 | 独立写 ~200–300 行离屏 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 状态记录:已做 / 延后 / 放弃
|
||||
Reference in New Issue
Block a user