按 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
31 KiB
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;warriordance_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 里引用(
A1B3…)。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 之后(正式移植,粗排,各阶段数周–数月)
- 材质保真
B1(扩 libgr2 API)→B2–B11·H6。 - 动画 + 组合
C2/C3/C4/C5/C6·D1–D4。 - 资产管线
G1–G3·G5(移动端纹理压缩)。 - 独立子项目(
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。 - 打磨
C8(IK)·C9(LOD)·E1/E2/E3/E5(阴影 / 环境 / 后处理 / 透明排序)·H3(CI)·I4(游戏相机)。