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
12 KiB
12 KiB
00 · Oracle —— Windows 真值源工具
总纲:
../PLAN.md§07。本文件是 oracle 这条关键路径的详细施工文档。 oracle 不是一个里程碑,但它和 M0 并行起步,M0/M1/M2 的主验证全部依赖它。
目标
在 Windows 侧建一个精简 harness,链接锁定版本的 granny2.dll + 抽出来的 EterGrnLib,
对"给定 .gr2 + .msa + 时刻 t"能稳定导出:
- 两层骨骼世界矩阵(① 裸 Granny,② 过完 EterGrnLib)
- mesh 0 蒙皮后的顶点坐标
- 确定态截图(固定相机 / 光 / 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-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.mdT4 对齐(第一天用 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 期望值一致。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.dllC 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 / Blender 一致。
- 走路动画某帧的顶点与层① 世界矩阵手算 LBS 的结果一致(自洽)。
- bind pose(t=0 / identity)下,
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。
本步风险(从 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 状态记录:已做 / 延后 / 放弃