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

12 KiB
Raw Blame History

00 · Oracle —— Windows 真值源工具

总纲:../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 第一天并行。
  • 需要一个会 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 CLIoracle 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 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 行):GrannyReadEntireFileFromMemoryGrannyGetFileInfo → 打印 Skeletons/Meshes/Materials/Textures/Animations 的 count、FromFileName、每 skeleton 的骨骼数。
  • 收集候选 granny2.dll2.4.x 优先,再 2.6.x / 2.9.x / 2.11.x 兜底),各链一遍 granny_probewarrior_cheongrin.gr2 + 3 个 zone 静态件 + 1 个 action/*.gr2
  • 选返回全部合理的那个,git-track 进 oracle/vendor/granny2.dll,判定写 oracle/GRANNY-VERSION.md(列出每个候选的输出)。
  • AC(选定 DLL 下):
    • warrior_cheongrin.gr2Skeletons==1Meshes>=1Animations==0FromFileName 是可读路径串、骨骼数 ∈ [20, 120](人形合理范围)—— 与 M0 T4 期望值一致。
    • action/dance_1.gr2Animations==1Duration ∈ (0, 60] 秒。
    • zone 静态件:Skeletons==0 或 1rigid)、Meshes>=1
    • 3+ 文件全过,其余候选 DLL 的失败表现记进 GRANNY-VERSION.md

T3 · dump 层①(裸 Granny [M]

  • granny2.dll C API。输入是两个 gr2model.gr2(骨架 + ModelInstance+ anim.gr2Animation)。
  • 链路:GrannyReadEntireFileFromMemory(model)GrannyInstantiateModel → 读 anim.gr2Animations[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-3T8 后收紧到 noise_floor)。
    • 顶点数与 M0 T6 / Blender 一致。
    • 走路动画某帧的顶点与层① 世界矩阵手算 LBS 的结果一致(自洽)。

T6 · 确定态截图 harness [M] —— M1 视觉验证依赖

  • 独立最小 D3D9 渲染器~200300 行,不依赖 EterGrnLib):CreateDeviceCreateRenderTarget 离屏 → 用 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] ≈ IInverseWorld4x4 来自文件)。
  • AC
    • 首轮:所有骨 max|M I| < 1e-4sanity 天花板)。
    • 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 两个值。
    • sanitymat < 1e-3vtx < 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 全过 ACGRANNY-VERSION.md 留档。
  • T7 自检:全骨 max|M I| < 首轮 1e-4、T8 后 < noise_floor.mat这是硬门禁,不过则下游作废。
  • T3 / T5:对测试资产集(M0 T9c 产出)产出稳定可复现,含正确性锚(t=0 == bind pose / 原始顶点)。
  • T6:确定态截图两次一致 + 视觉 sanity(人形直立)+ 换 t 有姿势变化。
  • T8noise_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

本步风险(从 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-4T8 后收紧)
  • test/noise_floor.json 产出(mat/vtx < 1e-3),M2 引用它设 ε
  • 层① 与 Blender 三方对拍在测试资产集上一致
  • Oracle-Full,可空)T2/T4 状态记录:已做 / 延后 / 放弃