Files
mtgodot-poc/docs/BACKLOG.md
T
shenleiandClaude Opus 5 fb4d2d222b docs: 文档审计——修正失效链接与过期结论
按 docs/ 目录逐篇核对,只改与事实/路径不符的部分,不动尚未验证的计划条目:

- `docs/steps/**` 在早前整理时已迁到 `docs/reference/steps/**`,全仓的旧路径
  引用(PLAN.md / README.md / M0-gr2-reader.md 及 libgr2 的头注释)一并改正。
- CLIENT-GAP.md / CLIENT-GAP-FIX.md / BACKLOG.md / MIDREVIEW.md:把已经落地的
  条目从「待办」改为已完成,删掉与代码现状矛盾的描述。
- ANDROID-TESTING.md / CLIENT-PORT.md / CLIENT-ROADMAP.md / GODOT-POC-PLAN.md /
  SHINSOO-WORLD-RENDERING.md / PARITY-GAP.md:同上,另补当前实际的构建/测试入口。
- THIRD-PARTY.md:补齐实际在用的第三方来源与许可说明。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJugvEJwz3FK4hw9ti3SRb
2026-09-08 17:15:17 +09:00

31 KiB
Raw Blame History

metin2-client — 未完成工作清单

更新:2026-08-29 · 配套 GODOT-POC-PLAN.md(计划)、MIDREVIEW.md(中期评审)、 SHINSOO-WORLD-RENDERING.md(世界渲染实施)、PARITY-GAP.md(距参考图的保真化差距)。

POC 的核心问题——Godot + 自研 loader 能否正确读 + 渲 + 播 Metin2 .gr2 骨骼模型—— 在 macOS 上已答「能」:数值对拍 Granny ≤6.5e-5;warrior dance_1 的 CPU 与 GPU(自写着色器) 蒙皮两条路径已抽帧核对(t=7 / t=19)一致;sura/assassin/shaman(含 v7)加载渲染正常(未逐帧对拍)。 本文件是「离一个可用客户端还差什么」的分层清单,供正式移植排期。

状态:✅ 已做 · 🟡 部分 · ⬜ 未做 · ⏭ 推迟到正式移植(不影响 Phase 1 中期评审 / Phase 2 bring-up) · ❌ 本 POC 明确不做

规模(粗估,单人 / 对本代码库熟悉后,置信度低 —— 排期时须自行细化): S ≈ 1–2 天 · M ≈ 1–2 周 · L ≈ 3 周+ · XL ≈ 独立子项目

ID 稳定,可在 commit / issue 里引用(A1 B3 …)。B 段取代 GODOT-POC-PLAN.md §M2.5 T2.5.6 的差距清单(那处不再单独维护)。


A. Phase 1 收尾(S–M)

ID 项 状态 规模 说明 / 依赖
A1 发型 mesh 实际挂接 🟡 M ✅ CPU 路径:Metin2Model::set_hair_gr2() + _load_hair() —— 加载发型 gr2,骨骼按名重映射到 base skeleton(warrior 74/74 匹配),hair mesh 并入 cpu_skin 每帧蒙皮,alpha_scissor 镂空材质。✅ GPU 蒙皮路径的 hair(_build_gpu_mesh 并入 + ARRAY_BONES remap,无需 shader 改)。✅ 武器挂点(C5);.msm target_skin 换色。
A2 anim player 与 model 共享逐帧矩阵 ✅ S Metin2Model::current_skin_matrices() 缓存完整 affine deformer 矩阵:reload 初始化为 bind pose,CPU/GPU 路径逐帧刷新;发型 / 武器 C++ 桥接可直接复用。
A3 _frame_model 相机框选 ✅ S 已改为按所有 surface 顶点的 1%–99% 分位盒取景并加 8% 边距;assassin headless 冒烟得到稳定 trimmed-mesh 框选,孤立几何和远端挂点不再拖偏相机。
A4 静态(无动画)展示路径统一 🟡 S 无 anim player 时走 Skeleton3D bind pose(与动画时的 cpu_skin/gpu_skin 不同代码路径)。实测 M1 bind pose 正确(skin_matrix ≈ I,见 test/shear-bones-survey.md),非 bug;为一致性 + 少一条代码路径,宜统一走蒙皮路径的 bind pose。
A5 CPU 路径每帧全量重建 mesh 🟡 S–M cpu_mesh->clear_surfaces()+add_surface_from_arrays 每帧全量(疑似 30 角色 24fps 的主因 —— 未 profile 确认,见 F4)。改成原地 Mesh::surface_update_vertex_region,或人群直接切 MTGODOT_GPUSKIN=1。
A6 ShaderMaterial 静态 Shader 退出泄漏 ✅ S Shader cache 由扩展在 scene-level terminator 显式 unref();CPU/GPU headless 退出不再报告该静态引用泄漏。
A7 GPU 蒙皮跑 stress harness 🟡 S 已确认 _run_stress 会让每个实例进入 bone-texture GPU LBS,且 headless 2 角色冒烟成功;报告文案已纠正。headless dummy renderer 的帧数无性能意义,仍缺前台 Metal 人群基线。
A8 loader 健壮性 🟡 S 解压失败、section 越界与 build_fileinfo 失败现会严格传播;新增 4 类损坏输入 CTest,全量真实语料仍为 9166/9166。仍缺 0 骨 / 索引越界 / NaN 的桥接层用例及 H1 draw 冒烟。
A9 reload() 遗留蒙皮状态 ✅ S 重载/切模型前现在清空 cpu_mesh、GPU 标志、bone texture、parts/material cache 与 .msm 派生状态,避免复用旧模型资源或 CPU/GPU 双重蒙皮。
A10 动画 mesh 裁剪盒 ✅ S CPU LBS 每帧按有限的变形后顶点刷新 custom_aabb;GPU 路径继续使用保守扩展盒。

B. 材质 / 观感保真(M2.5,M;取代 GODOT-POC-PLAN §M2.5 T2.5.6)

现状:贴图 = gr2 material 绑定(Mesh::material_textures[matidx],mesh-local,use_gr2_materials=true 默认) → 找不到时 _resolve_texture 文件名启发式(face/hair/stem 关键字 + 目录扫描)。blend / cull / alpha 全靠 guess_blend() 按表面名关键字猜。着色器 SRC_MIX / SRC_SKIN(skinned)/ SRC_ADD / SRC_ADD_SKIN(additive;静态与 GPU 蒙皮均覆盖)。

前置 B1:libgr2 已有 MaterialInfo{name, diffuse_texture, all_textures, map_count, alpha_blend, two_sided} + Mesh::material_textures,其中 blend/two-sided 仍是名字、贴图数与 ExtendedData 的不完整推断;任意 D3D stage 状态不在 GR2 中。

ID 项 状态 规模 说明
B1 libgr2 暴露 blend / cull / alpha 数据 🟡 M Metin2 gr2 不带 D3DRS —— 客户端靠 Name(Blend* + MapCount>1) + ExtendedData("Two-sided")。libgr2 MaterialInfo 已加 map_count / alpha_blend / two_sided(名字 + map 推断,static_object.cpp 已用来切 TRANSPARENCY_ALPHA / CULL_DISABLED)。gr2fuzz 9166/9166 不回归。⬜ 精确解析 ExtendedData 变体,并由 B2 端口原客户端实际使用的固定功能材质分支。
B2 原客户端固定功能材质状态兼容 ⬜ M 现在只近似单 stage MODULATE(TEXTURE, DIFFUSE)。先盘点 GameLib/ActorInstanceRender.cpp、EterGrnLib/ModelInstanceRender.cpp、EterGrnLib/Material.cpp 在目标资产上实际触发的 blend/alpha-test/sphere-map/多贴图分支,再扩 SRC_MIX;GR2 不承载任意 D3D stage 状态,不按未经证实的 op 清单猜实现。
B3 alpha-test(镂空) 🟡 S hair 表面名 → AlphaTest(B4 里 decide_blend 仍保留这条,GR2 材质 flag 不区分镂空 vs 不透明)。⬜ 改成贴图 alpha 分布判定;阈值硬编码 0.5,应取材质 D3DCMP_* ref。
B4 blend 模式取自材质 ✅ 首版 S metin2_model decide_blend():优先 gr2::MaterialInfo.alpha_blend(dump_materials 端口 EterGrnLib Material.cpp:233 = Name Blend* + MapCount>1),仅 hair 镂空 / effect 加法这两类 GR2 没编码的走表面名兜底。decide_two_sided():MaterialInfo.two_sided ‖ hair/cloak/cape/skirt/leaf 名。match_material() 按 diffuse_texture/name basename 回查。两处(body + weapon)都改。m2_material 加 cull_disabled shader 变体(shader_for(add,skin,two_sided))。⬜ 精确 ExtendedData 变体、alpha-test ref 取材质 D3DCMP_*。
B5 每材质多张贴图(spec / mask / …) ⬜ M 现在只解析 diffuse。Mesh::material_textures + libgr2 材质 API 扩成 map。
B6 贴图采样设置(filter / wrap / mip / 各向异性) ⬜ S 现在 shader 里写死 filter_linear_mipmap, repeat_enable。
B7 顶点色 ✅ 排查=不适用 — 全资产 .gr2(PC/monster2/effect)无 DiffuseColor/Color0 顶点成员(grep assets = 0)。Metin2 逐顶点色 = 地形雾(MapOutdoorRenderSTP 运行时动态 VB,非资产,Godot Environment 雾已覆盖)+ .mse colorfactor/粒子色(属 E7/§1.2)。libgr2 / build_mesh 无需动。详见 PARITY §2.6。
B8 切线帧(若确有法线贴图资产) ⬜ M PNT332 无切线。xrender 时期结论:Metin2 资产基本不用法线贴图 —— 先排查有没有,再决定是否做(SurfaceTool::generate_tangents() 或从 UV 算)。
B9 镜面 / 自发光 🟡 M Sphere-map 高光管线已端口(SRC_MIX/SRC_SKIN 加 spec_map/spec_power/spec_enable,复现 Material.cpp:305 __ApplySpecularRenderState:不透明面、相机空间 reflect().xy 查 spheremap.jpg、EMISSION += sphere*tex.a*power)。Metin2Model.specular_power 属性,默认 0 休眠。✅ 装备层已接:project/ui/equip_model.gd:283-288 _apply_body() 读 item_proto 的 specular 列并 set("specular_power", sp / 100.0)。⬜ 只驱动身体/护甲一个部位(武器 / 发型 / 时装未接),且 power 未按受控参考帧标定(PARITY §0 门禁)。SRC_ADD / SRC_ADD_SKIN 已覆盖 additive 发光的一种,且 additive 动画面不再漏蒙皮。
B10 背面剔除 / 双面 ✅ 首版 S 世界静态对象(W3/B1)已按 two_sided + 贴图名切 CULL_DISABLED。✅ 角色也已按材质切:m2_material.cpp:188-211 shader_for() 对 two_sided 把 cull_back replace 成 cull_disabled(opaque/blend × 普通/蒙皮 四个变体各自缓存),metin2_model.cpp:1182(身体)/ :769(武器)经 decide_two_sided() 驱动,_load_hair() 直接置 true。⬜ 判据仍是启发式 —— gr2::MaterialInfo.two_sided 按材质名(2side/twoside/double/leaf/tree/ivy/fence)+ 表面名兜底(hair/cloak/cape/skirt/ribbon/leaf),不是真 ExtendedData 的 "Two-sided"。
B11 winding 逐模型验 ⬜ S flip_winding 默认 false(4 个骨架验过,未全量)。

C. 动画运行时(S–L)

ID 项 状态 规模 说明
C1 root motion(Accumulation) 🟡 S .msa 已解析、get_accumulation() 暴露;未作用到节点 transform。
C2 动画混合 / crossfade 🟡 M ✅ Metin2AnimPlayer.blend_time(默认 0.15s):切 anim_path 时旧 clip 冻结,逐骨对旧/新 world_pose slerp+lerp,skin = invWorld · world_blended 重建;mul4x3 复用 libgr2 语义。权重曲线改为 2w²−w³(ease-in),对齐客户端 GrannySetControlEaseInCurve(t0,t1,0,0,1,1) Hermite(原用 smoothstep)。⬜ shear 骨过渡期精确保持、受控帧核对时长、非循环 clip 结束回退(现 playing=false 定格,apply_pose 已 clamp,够用)。
C3 additive 动画 / 部分身体 mask ⬜ M 上半身动作叠在移动上等。
C4 .msa loop / 一次性动作 🟡 S 已增加整段 loop 属性:false 时夹到末帧、停止并发 playback_finished;实际 .msa LoopData(count/cancel/start/end)已解析并由 get_loop_data() 暴露,真实资产冒烟通过。剩余:按原客户端规则执行片段循环次数/取消窗口;整段 loop/once 仍需由动作注册层指定,不能从 LoopData 猜。
C5 挂点系统 🟡 M ✅ 武器挂手骨:Metin2Model::set_weapon_gr2() / set_weapon_bone()(默认 equip_right_hand),子 MeshInstance3D 每帧取 gr2::sample_pose 的 world_pose[bone](Metin2AnimPlayer::apply_pose 驱动;对齐客户端 GetBoneMatrixPointer)。⬜ 特效挂骨、左手/双持/盾、.mse trail。
C6 motion event 类型语义 🟡 首版 M game_scene._on_local_motion_event 分派(对齐 RaceMotionData EMotionEventType):有 sound → _audio.play_at;有 effect → fx.spawn_at(basename) 挂 EffectPosition;type 2 SCREEN_WAVING → cam.shake;type 3 SCREEN_FLASHING → 屏闪 ColorRect;type 6/9 FLY/WARP → 钩子(skill_fx 已单独处理飞行物)。eterngrn_polish_test.gd 覆盖。⬜ .msa type 数字与运行时枚举的历史偏差核对、武器拖尾(C 未列,见 WeaponTrace)、投掷点精确。
C7 播放速率(数据驱动) ⬜ S time_scale 有,未按动作类型 / 装备驱动。
C8 IK / look-at / 踩地校正 ⬜ L 无。
C9 LOD 选择 🟡 M ✅ 距离切 mesh(lod_distances 默认 [18,42,90]m,重建 cpu_mesh+材质+蒙皮)。+ 淡入淡出:切档时把旧 mesh 快照进 LodGhost MeshInstance3D(冻结),shader lod_fade uniform(mix/skin)在 ~0.18s 内旧 1→0 / 新 0→1(LODController::BlendRenderWithOneTexture);距离阈值加 15% 迟滞防抖。⬜ GPU skin 路径的 LOD、受控帧核对。

D. 模型组合(S–L)

ID 项 状态 规模 说明
D1 多部件装配(身体 + 武器 + 发型 + 时装 + 翅膀,共享骨架) ⬜ L .msm + 物品系统。核心:多 MeshInstance3D 绑同一骨架 + 骨名重映射;部位遮挡 / 隐藏规则。
D2 武器模型(独立 .gr2 挂手骨) 🟡 M ✅ 右手单持跑通(见 C5;rigid mesh 分支 build_mesh 已验,warrior + 00040.gr2 idle/run 跟手,贴图按材质绑定名解析)。⬜ 左手/双持/盾、.msm/物品系统给路径、_lod 变体。
D3 贴图换色(SourceSkin→TargetSkin) 🟡 S ✅ 发型:Metin2Model.hair_skin(TargetSkin dds 覆盖发型 albedo,对齐 SetMaterialImagePointer);get_hair_options() 返回绝对 source_skin/target_skin;formats.msm_hair CTest。⬜ 护甲/时装部位的 skin 表。
D4 装备 / 时装换装系统 ⬜ L 材质槽、部位互斥、隐藏规则。D1 的上层。

E. 渲染功能(S–XL)

E7–E10(特效 / 地形 / 水 / 植被)+ 地图加载 / 环境 / 第三人称场景的完整实施规格见 SHINSOO-WORLD-RENDERING.md(W0–W8 / R1 / R2,≈3.5–5.5 月起, Phase 2 三设备 bring-up 之后启动;自用,无正式门禁,W0 可提前并行)。对应的地图 / 环境 / 资产解析 工作项见下方 E11–E13,以及 G7 / H7 / I5。

ID 项 状态 规模 说明
E1 阴影调优 🟡 S–M DirectionalLight3D shadow 已开,出图看着正常;range / cascade / bias 未按 unit_scale=0.01 系统性调,需验漏影 / 摩尔纹。✅ 分层(PARITY §3.3):树 + shadowflag≠1 静态物体不投实时阴影(靠烘焙 shadowmap.dds),shadowflag=1 + 角色投实时;tree_shadows/static_shadows 开关。
E2 环境 🟡 M 角色演示:procedural sky + sky ambient。世界(W5):extension/src/environment_builder.cpp 从 .msenv 建 Sun(方向光)+ WorldEnv(Gradient 三段天空 / Material.Ambient / 深度雾 foglevel / Filmic+adjustment)。⬜ Character 光、云层、lens flare、昼夜。SHINSOO §9-W5。
E3 后处理 🟡 M tonemap(Filmic)+ adjustment 调色 + glow(intensity 0.35)+ SSAO(radius 1.2)+ MSAA 4× 已开。⬜ 按受控参考帧标定曝光/对比/饱和/glow/SSAO;TAA/FXAA 按平台取舍。
E4 雾 🟡 S 角色演示 MTGODOT_FOG=1。世界(W5):environment_builder 按 .msenv foglevel 设深度雾 begin/end + 雾色。⬜ 与 .wtr 水雾 / 高度雾绑定。
E5 透明排序 🟡 M render_priority 启发式。自重叠 alpha(头发 / 飘带)可能排错;需按深度排序或 OIT。
E6 Decal / 投影器 ⬜ S 血迹 / 阴影贴花。
E7 特效 / 粒子系统(.mse) ⏭ L–XL 真实资产是 ASCII Group/List 文本,原客户端 EffectLib/EffectData.cpp、ParticleSystemData.cpp、EffectMesh.cpp、SimpleLightData.cpp 已有 parser/语义真值。W7 首版尚未端口。任务是把 Particle/Mesh/Light/时间事件和挂点映射到 Godot,不是逆向私有二进制;先做最终机位必要子集,不阻塞 Phase 2 bring-up。SHINSOO §9-W7。
E8 地形 🟡 XL ✅ 几何(W1):formats/terrain_mesh.cpp + Metin2World(A1 20/20 ArrayMesh + UV,sample_height 对拍 areadata z)。✅ splat 数据(W2):formats/splat.cpp 端口 RAW_GenerateSplat,terrain_splat.cpp 用 2 张 RGBA 权重图 + Texture2DArray 出图。⚠️ 当前 min(8, splat.layers.size()),而 A1 单区块最多 13 个非零层,会静默丢层;UV 也仍是错误的 128/scale,应改为区块归一化 U=8*UScale、V=-8*VScale。✅ 流式首版:逐区块节点 + 3×3 装卸 + get_perf()。⬜ 全活动图层、原始 UV/offset、源分辨率与 mip、动态角色阴影落地形、碰撞/LOD/worker/Mobile。SHINSOO §9-W1..W2/W8。
E9 水 🟡 L W7:water_builder.cpp 已按 water.wtr 逐层生成行 span 掩膜网格,SRC_WATER 仍是程序化波纹/菲涅耳。原客户端 parity 缺口是 special/water/01..30.dds 约 70ms 翻页、相机空间 UV、按水深逐顶点 alpha 和轻微高度动画。反射/折射/泡沫/normal map 属于参考帧确认后的增强;行列合并只减少几何量,不会减少现有每层一个 surface 的 draw call。SHINSOO §9-W7。
E10 SpeedTree 植被 🟡 L .spt 几何 loader 位于闭源 IDV .lib;实物为带符号的 COFF x86-64,不能链进 macOS/移动端且分发需确认授权。真实资产共 118 个。✅ R2 proxy:formats/spt.cpp 嗅探 bark/composite atlas;tree_placeholder.cpp 使用真实 DDS、确定性枝干、多组交叉叶簇和风摆;按 treefile 缓存共享 mesh + MultiMeshInstance3D(A1 368 棵 / 14 树种),并已移除非原版的逐实例随机 yaw/scale。⬜ 用 SpeedTreeRT.h + .lib 验证 Windows x64 离线 exporter,再补真实 branch/frond/leaf、LOD/impostor。SHINSOO §9-W4。
E11 地图文本格式解析(MapSetting / TextureSet / .msenv) ✅ M formats/map_setting.cpp + texture_set.cpp + environment.cpp(.msenv Group/List 文本树:方向光 / 材质 / foglevel / SkyBox+cloud / Gradient list / LensFlare)。m2_tokvec = LoadMultipleTextData 等价。CTest formats.map_formats + tools/map_probe 过真实 A1(4×5 / 17 层 / foglevel 6 / 10 行 Gradient)。
E12 区块二进制 + AreaData 解析(height.raw / tile.raw / attr.atr / water.wtr / areadata.txt / areaambiencedata.txt) ✅ M formats/terrain_files.cpp(magic 2634/5426 + 尺寸校验,真值取自 Terrain.cpp)+ area_data.cpp(含 areaproperty / areaambiencedata)。A1 全 20 区块 / 975 对象过 map_probe;areadata position = 地图全局 cm(W3 修正,SHINSOO §5.6)。
E13 Property CRC 注册表 + 五类对象实例化(Tree / Building / Effect / Ambience / DungeonBlock) 🟡 L ✅ 注册表(formats/property.cpp,A1 1330 CRC,136/136 命中)。✅ Building/DungeonBlock:static_object.cpp get_static_mesh()(复用 gr2_bridge,跨实例共享 mesh,CULL_BACK + leaf/fence 关键字 alpha-scissor)+ place_objects()(A1 601 实例 / 119 mesh / 0 缺;位置=地图全局 cm,ShadowFlag / portal_ids meta / .mdatr 计数)。✅ Tree(W4,见 E10)。✅ ypr 轴共轭 bug 已修(object_basis_godot(),roll=朝向不再翻滚,影响 66% A1 物体;CTest 钉约定)。⬜ Effect/Ambience、逐建筑朝向对原客户端复核、.mdatr 碰撞体、LOD。SHINSOO §9-W3。

F. 平台(Phase 2 —— 三设备 bring-up:Mac / 一加 13 / iPhone 16,自用,M–L)

ID 项 状态 规模 说明
F0 vendored 原生依赖 ✅ S libsodium / libzstd / miniLZO 从 Homebrew 改为源码内建(extension/third_party/,pinned submodule + vendored miniLZO)—— 同一棵树可交叉编 NDK / iOS SDK。2026-09-02 起第四个:Crypto++ 8.4.0(vendored 源码,MT_PROTOCOL=classic 的 DH2/CTR cipher 用,公有领域许可)。别名 mt3p::sodium / mt3p::zstd / mt3p::minilzo / mt3p::cryptopp,无源码改动,macOS 9/9 CTest + 真实 item/mob_proto 解码逐字一致。详见 docs/THIRD-PARTY.md。miniLZO 是 GPL —— 自用可接受(同 oodle1.c),发布前须换。
F1 iOS(iPhone 16 / Metal) 🟡 M ✅ 交叉编译打通:./build-ios.sh 出 libmtgodot.ios.template_{debug,release}.a(arm64 / minos 15.0),全依赖(libsodium/zstd/miniLZO/libgr2/formats/mtnet/godot-cpp)随之交叉编过;iOS 强制 extension 为 STATIC(不能 dlopen),.gdextension 加了 [dependencies] ios.* 归档清单。⬜ 剩 Godot 4.7 iOS 导出模板 + 导出 preset 链 project/bin/ios/*.a + 免费 provisioning 签名 + iPhone 16 真机跑 + Release/strip。预留 2–3 天。详见 docs/PLATFORMS.md。
F2 Android(一加 13 / Vulkan) 🟡 M ✅ CMake 已接 CMAKE_SYSTEM_NAME==Android(SHARED .so + arm64 名字后缀);./build-android.sh 走 NDK 自带 android.toolchain.cmake(arm64-v8a / android-24)。⬜ 本机无 NDK —— 装 NDK 后 ./build-android.sh 出 .so,再 Godot Android 导出 preset(Gradle)+ 一加 13 真机跑。只验 Vulkan,不做 Compatibility(GLES3) / 老机。详见 docs/PLATFORMS.md。
F3 三端一致性 ⬜ S 同工程 / 同 t / 同机位截图,iPhone 16 · 一加 13 · Mac 对齐。
F4 移动端性能 / 帧预算 🟡 M ✅ GPU 纹理压缩:extension/src/texture_util.{h,cpp} make_color_texture() —— 移动端(OS.has_feature("mobile"))或 MTGODOT_TEXCOMP=1 时对颜色贴图运行时 ASTC 8x8(~2bpp vs RGBA8 32bpp),编码失败回退 RGBA8;桌面默认关,逐调用点保留原 mipmap 标志 → OFF 时与旧路径逐字节一致。接入:角色皮肤(_load_dds)、建筑 albedo(static_object)、树皮/树冠(tree_placeholder);splat 控制图 / 阴影图 / 水 / HUD 保持 RGBA8。macOS+iOS 编过,ctest 9/9,桌面渲染无变化,MTGODOT_TEXCOMP=1 角色渲染正常。⬜ 剩:一加 13 / iPhone 16 单 + N 角色 bench(MTGODOT_GPUSKIN=1 人群)、构建成本(~170ms/角色,A5/G2)、移动端画质核对。
F5 App 生命周期 🟡 S–M ✅ project/app_lifecycle.gd(AppLifecycle 节点):把 NOTIFICATION_APPLICATION_PAUSED/RESUMED / FOCUS_IN/OUT / OS_MEMORY_WARNING / WM_GO_BACK_REQUEST 收成信号,默认动作 = 后台暂停树 + 停 BGM + 降 max_fps,前台还原;bind(m2client, audio) 一键接。M2Client(C++):_notification 收 PAUSED/RESUMED → suspend()/resume()(挂起时不 pump socket),reconnect() 用存好的凭据重登;新信号 suspended/resumed。已接 login.gd(回前台若在游戏中自动重连)+ world_demo.gd。headless smoke(suspend/resume/reconnect 绑定 + 幂等 + 信号)通过,ctest 9/9,iOS 编过。渲染上下文丢失由 Godot Vulkan 自恢复,GPU 资源全 RenderingServer 托管,无需处理。⬜ 剩真机验证后台 30s→掐连接→回前台重连、iOS 内存告警实际触发。
F6 打包 ⬜ S–M macOS / iOS / Android export preset、资产打包策略、iOS 签名。自用,无商店上架。
F9 实体点选精度 ✅ 首版 S player_controller._ray_pick_t():射线 vs 每个 pickable 下所有 MeshInstance3D 的世界 AABB 合并 + slab 相交(逐实体精确,大怪 / 小怪不再被固定 1.4 m 半径误判;grow(0.15) 宽容度)。无网格回退旧胶囊近似。对齐 EterGrnLib/ModelInstanceCollisionDetection::Intersect 的结果,不复刻逐三角。eterngrn_polish_test.gd 覆盖(命中 t / 未命中 -1 / 擦边 / 无网格回退)。⬜ 逐三角 / 骨骼包围体、GetHeight。
F7 触屏输入 / 手势相机 ✅ S game_camera.gd:单指拖拽环绕(8px deadzone,低于视作点击)/ 双指捏合缩放;player_controller.gd:单指轻点(≤12px 位移 & ≤0.35s)= 点地 / 点选,拖拽或双指手势时不触发。project.godot 关 emulate_mouse_from_touch(避免与轻点重复触发),桌面鼠标路径不变。两脚本 --check-only + 项目导入无错。
F8 godot-cpp 钉稳定版 ✅ S gitlink 精确锁定 101ae38034304346a46ea9ea84ae156d3e860496,.gitmodules 已移除移动的 master branch 配置;目标 API 固定为 Godot 4.7。

G. 资产管线(S–M)

ID 项 状态 规模 说明
G1 eterpack 读取器 🟡 M–L ✅ 本 fork 不是老 .eix/.epk —— 是 PackLib/Pack.cpp 的单文件包(header + XChaCha20 加密索引 + zstd 数据,可选逐文件加密,硬编码 PACK_KEY)。extension/src/pack/eterpack.{h,cpp}:EterPack(读,name-field 从 header 反推以兼容真实 PackMaker.exe 包)+ write_pack(写)+ packtool CLI(pack/unpack/list,跨平台 PackMaker 替代)。pack.roundtrip CTest + 真实 45 文件 textureset/ 目录明文/加密往返 diff 一致(含 CP949 中文名 + 嵌套目录)。⬜ 接进 AssetResolver(从包解析而非散文件)+ mmap 大包。
G2 跨实例资产缓存 / 去重 🟡 M 角色:每实例重建。世界(W8):Metin2World 的 PropertyRegistry / AssetResolver / StaticMeshCache(建筑 gr2→mesh,A1 119 份)/ DDS 解码缓存跨区块装卸复用。SHINSOO §9-W8。
G3 异步 / 多线程加载 🟡 M 角色 Metin2Model 全同步。世界(W8):Metin2World 流式装载分摊到 stream_budget 个区块/帧(主线程),未上 worker thread。SHINSOO §9-W8。
G4 .dds 原生 import 路径 🟡 S M1 T1.3 定为运行时 dxt.cpp(level 0 + generate_mipmaps())。Godot 原生 import 分支没接。
G5 GPU 压缩纹理 / mip 质量 / sRGB 通盘过 ⬜ S–M 现在软解 RGBA8 上传,GPU 端不压缩;移动端显存 / 带宽要压(BC7 / ASTC)。
G6 DXT 软解补 level-N mip 🟡 S mtgodot dxt.cpp 只解 level 0(xrender 版有完整 mip 链,可回移)。
G7 跨 pack 虚拟路径索引与覆盖优先级(AssetResolver) 🟡 M ✅ formats/asset_resolver.cpp:规范化(\→/ / 去盘符 / 小写 / .``..)+ ymir work/ 后缀索引 + by_rel + pack 优先级(基础包在前,metin2_patch_* 在后,stable_sort + 先注册者胜)+ 冲突计数。A1 索引 54212 文件、17/17 地表贴图可解析。⬜ 冲突明细导出 + 优先级快照进测试。是 G1(eterpack)的运行时前置。SHINSOO §4.2 / §9-W0。

H. 测试 / 工程(S–M)

ID 项 状态 规模 说明
H1 全语料过 Godot loader 的 fuzz ⬜ S–M gr2fuzz 只覆盖 libgr2 解析(9166/9166)。缺「把每个 .gr2 塞进 Metin2Model + 蒙皮 + draw」的冒烟(对标 xrender 的 render_fuzz)。
H2 全 race / 怪物 / NPC / 坐骑抽样 🟡 S 现在只抽查 ~6 个 PC 模型 + anim_probe 4 个 race。
H3 带容差的 golden 视觉回归 + CI 🟡 M compare.py 已对 Godot 非零退出、超时、旧/缺截图、0 surface、缺动画自检和 NaN 作硬失败;CPU/GPU 像素差仍是 advisory。缺校准后的结构化像素 / 直方图门禁 + 可截图的 CI renderer。
H4 扩展侧 dump 骨骼矩阵 / 顶点做对拍 ⬜ S 现在数值对拍靠 libgr2 层的 oracle_diff。加 MTGODOT_DUMP=<path> 从扩展导出,闭合「mtgodot == Granny」全链。
H5 Android NDK 上构建 tools ⬜ S gr2dump / gr2fuzz / oracle_diff 目前 macOS。
H6 逐材质 oracle 对拍(纸面确认) ⬜ S material_textures 已按渲染结果验(warrior/sura/assassin/shaman 全对),缺 run-diff-suite.sh 级别的逐材质数值确认。
H7 A1 地图格式 / 数值 / golden 回归 ⏭ M 地图数据无外部 oracle(不像 .gr2 有 Granny DLL):移植 Terrain.cpp/Area.cpp 公式做自洽 + minimap.dds 目视 + 受控机位 golden。formats/tests/map_formats_test.cpp 进 CTest;world-a1-r1/r2.png 进 compare.py。SHINSOO §11。

I. libgr2 覆盖面(S–M)

ID 项 状态 规模 说明
I1 >4 骨权重 / 8-bone-weight ✅ S libgr2::Mesh 已记录源 BoneWeights 槽数,gr2fuzz 纳入直方图。9166 文件中的 3118 个 skinned mesh 全部声明 4 槽,当前语料无需 ARRAY_FLAG_USE_8_BONE_WEIGHTS。新资产若出现其它槽数会在报告中显现。
I2 多骨架 .gr2 ⬜ S–M libgr2 的 extract_mesh 会按 BoneBindings 悬空最少给每个 mesh 选对骨架。缺口在桥接:Metin2Model 只把 fi.skeletons[0] 实例化成 Skeleton3D —— 绑到 skeletons[1](武器 / 挂点骨架)的 mesh 会错绑。需按 mesh 的实际骨架建多个 Skeleton3D 或合并。
I3 变形目标 / morph ⬜ S libgr2 不解析 granny_mesh.MorphTargets。Metin2 用骨骼驱动面部,大概率 N/A —— 确认后标 ❌ 或补。
I4 游戏相机 🟡 M 世界(W6):project/world_demo.gd 第三人称跟随(yaw/pitch/dist + 沿视线 sample_height 防穿)+ WASD + 贴地 + attr.atr 阻挡(逐轴)+ wait/walk/run 状态机 + NPC Label3D。⬜ 鼠标转视角交互壳、遮挡淡出、机位 preset;SpringArm3D/物理射线要完整覆盖建筑,先依赖 E13 的 .mdatr/静态对象碰撞体,当前地形碰撞只能解决地形防穿。SHINSOO §9-W6。
I5 地图 / 全局 / Godot 坐标统一转换模块(mtgodot::coord) 🟡 S ✅ formats/m2_coord.{h,cpp} 纯数学核心:position_to_godot / direction_to_godot / tile_id / tile_dir / chunk_origin_cm / height_raw_to_cm,与 make_conv(-90°X + 0.01)一致,CTest 覆盖。✅ ypr_basis(Metin2 空间,D3D 端口)+ object_basis_godot(共轭到 Godot Y-up,放置用;修了漏轴转换 bug),CTest 覆盖。⬜ extension 侧 godot 薄封装。SHINSOO §6。

已明确不做(本 POC,GODOT-POC-PLAN.md §01)

UI / Python 脚本层、多角色 AI、网络、玩法逻辑、eterpack(Phase 1 用散文件 —— 但 G1 是正式移植必做)。 .mse 特效 / 地形 / 水 / SpeedTree 在 E 段标 ⏭:正式移植必做、独立立项,但不影响 Phase 1 中期评审结论,也不阻塞 Phase 2 的三设备 bring-up。


排期

规模粗估、置信度低(见开头);下面的「周」是相对量级,不是承诺。

Sprint 1 —— 小项收尾(~1 周)

已完成:A3 · A6 · F8(godot-cpp 钉版)· I1,并补完 A9/A10 正确性问题。 剩余:A5(CPU 路径原地更新)· A7(前台 Metal stress 基线)· A8+H1(桥接层畸形用例与全语料 draw 冒烟)。

Sprint 2 —— 组合基础(~2 周)

A2 已完成。剩余:A1(发型挂接,M)· A4(静态路径统一)· I2(多骨架排查)。

Phase 2 —— 平台(三设备 bring-up,自用)

F1–F7:一加 13(Vulkan)+ iPhone 16(Metal,静态链接)跑起来 + 三端一致 + 两台真机性能留档(旗舰基线)+ 生命周期 + 触屏相机。无 Compatibility / 老机、无对外 go/no-go。

Phase 2 之后(正式移植,粗排,各阶段数周–数月)

  1. 材质保真 B1(扩 libgr2 API)→ B2–B11 · H6。
  2. 动画 + 组合 C2/C3/C4/C5/C6 · D1–D4。
  3. 资产管线 G1–G3 · G5(移动端纹理压缩)。
  4. 独立子项目(SHINSOO-WORLD-RENDERING.md,Phase 2 bring-up 之后) G7+E11+E12+I5(W0 路径 / 格式 / 坐标)→ E8(地形 geometry+splat)→ E13(Property 对象)→ E10(树)→ E2(环境)→ I4(相机)= R1;再 E9(水)+ E7(必要 .mse)+ UI + H7 = R2。
  5. 打磨 C8(IK)· C9(LOD)· E1/E2/E3/E5(阴影 / 环境 / 后处理 / 透明排序)· H3(CI)· I4(游戏相机)。