Files
mtgodot-poc/docs/CLIENT-GAP.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

496 lines
57 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# m2dev-client → mtgodot-poc 1:1 功能差距
> 对照范围:`../m2dev-client-src-main/`(客户端源码)、`../m2dev-client-main/`
>(可执行客户端与配置)以及本工程当前实现。
>
> 本文按“协议、数据、交互、表现均与原客户端等价”的严格 1:1 标准审计;
> `CLIENT-ROADMAP.md` 中的“P0–P10 首版完成”只表示主要纵向流程已有首版,不等于 1:1 完成。
> 渲染细节另见 `PARITY-GAP.md`,动画、材质和资产管线另见 `BACKLOG.md`,网络实现另见
> `CLIENT-PORT.md`。
>
> 更新日期:2026-08-31(增量 38
>
> **状态列图例**:✅ 达到本文末“1:1 完成判据” · 🟡 有首版 / 部分语义 · ⬜ 未实现 ·
> ❌ 明确不做(超出 m2dev-client 参考范围,除非另立需求)。
>
> ### 不做清单(❌)
>
> - **拍卖行 / auction house**m2dev-client 无此系统 —— `0x08xx` 段只有
> `SHOP`/`MYSHOP`/`SAFEBOX_*`/`MALL_*`,全源码 / 协议头 / 资产零 `auction` 命中。
> 要做即“新造协议 + 配套服务端”,不属 1:1 移植。**标注为不做**,除非后续明确提需求。
> - **反作弊(anti-cheat**`CG_HACK`(0x0B03) 客户端异常上报,以及桌面外壳的
> `ProcessCRC` / `ProcessScanner` / 内存扫描等。自用客户端不需要,**标注为不做**,
> 除非服务端强制校验才回头做最小上报。
>
> ### 变更记录
>
> - **2026-08-31 增量 30EterGrnLib 对齐复核)**:查代码确认以下已落地,表内相关行同步。
> ① **材质 blend / two-sided 改读 GR2 材质信息**`metin2_model::decide_blend/decide_two_sided`
> 优先 `gr2::MaterialInfo.alpha_blend/two_sided` = `dump_materials` 端口 `Material.cpp:233`
> body + weapon 两处;`m2_material` 加 `cull_disabled` shader 变体)。
> ② **LOD 淡入**`LodGhost` 冻结旧 mesh + `lod_fade` uniform0.18s,距离阈值 15% 迟滞)。
> ③ **crossfade 权重改 ease-in `2w²−w³`**,对齐 `GrannySetControlEaseInCurve(0,0,1,1)`。
> ④ **`.msa` motion event 类型分派**`game_scene._on_local_motion_event`sound / effect /
> SCREEN_WAVING 震屏 / SCREEN_FLASHING 屏闪 / FLY·WARP 钩子)。
> ⑤ **角色接触阴影**`project/ui/char_shadow.gd`:强制 `cast_shadow=ON` + 脚下渐变 Decal
> `player_view` / `mob_view` 调用)。
> ⑥ **点选精度**`player_controller._ray_pick_t`:射线 vs 子 mesh 合并世界 AABB slab 相交,
> 无 mesh 回退胶囊)。
> ⑦ **武器挂点复核**:隔离探针实测 weapon MI 原点 == `equip_right_hand` 骨骼原点
> Δ≤0.06 m(旧「偏 10 m」为 cpu_skin / MapCoord 修复前的失效结论);新增 grip
> pre-transform `weapon_pre = mul4x3(weaponInvWorld[0], weaponLocal[0])`(对齐
> `ModelInstanceUpdate.cpp:171` 的 `weaponComposite[0]`),30 cm 位移夹断退回原始手部位姿。
> `03150` 类网格在 .gr2 里就离骨骼原点 ~3 m(资产缺陷,非变换)。
> 新增测试 `eterngrn_polish_test.gd``ctest 10/10` + 29 套 GDScript + iOS 全绿。
> - **2026-08-31 增量 38ESC 系统菜单 + 游戏设置窗)**:
> ① **`system_menu_ui.gd`**`systemdialog.py` 真 uiscriptthinboard 8 键)= `uisystem.py`
> `SystemDialog.__LoadSystemMenu_Default` 的 1:1`system_option_button` → `system_option_ui.open()`
> `game_option_button` → `game_option_ui.open()``change_button` → `client.say(0,"/phase_select")`
> = `net.ExitGame()` 里 `SendChatPacket("/phase_select")`),`logout_button` → `"/logout"`
> = `net.LogOutGame()`),`mall_button` → `"/in_game_mall"``exit_button` → `get_tree().quit()`
> `help_button` → 占位提示,`cancel_button` / 标题栏 → 关。ESC 现在开这个菜单(`game_scene`
> 把 ESC 从直接开 system_option 改成开 system_menu)。
> ② **`game_option_ui.gd`**`gameoptiondialog.py` 真 uiscript= `uigameoption.py`
> `block_{exchange,party,guild,whisper,friend,party_request}_button`(toggle) →
> `client.say(0,"/setblockmode " + str(mask ^ bit))``EBlockAction` 位 1<<0..1<<5,本地
> `_block_mode` 跟踪 + 持久化,服务器回包同步待补);`pvp_{peace,revenge,guild,free}`(radio) →
> `"/pkmode {0,1,4,2}"``__OnClickPvPMode*`);`name_color`/`target_board`/`view_chat`/
> `always_show_name`/`show_damage`/`salestext` radio → 持久化 `user://system_option.cfg [gameopt]`
> `systemSetting.Set*Flag` 等价;渲染侧钩子待补)。
> 新增 `system_menu_ui_test.gd`(两窗装载 + `/setblockmode` 位运算 + `/pkmode` + `/phase_select`/
> `/logout`/`/in_game_mall` + 显示开关持久化 + 二次实例读 cfg)。C++ 无改动;`ctest 10/10` +
> 34 套 GDScript + 导入 + iOS 全绿。
> - **2026-08-31 增量 37(组队成员信息板 + `CG_PARTY_SET_STATE`**
> ① **C++**`wire.h` 加 `CGPartySetState{u16 hdr,u16 len,u32 pid,u8 role,u8 on}`(断言 10
> + `EPartyRole` 枚举(NORMAL 0/LEADER 1/ATTACKER 2/TANKER 3/BUFFER 4/SKILL_MASTER 5/
> BERSERKER 6/DEFENDER 7);`game_client.send_party_set_state(pid,role,on)`
> `M2Client.party_set_state(pid,role,on)` + bind`get_party()` dict 补 `state`(完整角色字节)
> 和 `affects[7]`。
> ② **`party_ui.gd`** 从「名字 + 一条 HP」重做成 1:1 迁移 `assets/root/uiparty.py`
> `PartyMemberInfoBoard` + `partymemberinfoboard.py` 布局:每员 strip = 角色状态按钮
> (队长可点 → 弹角色菜单 普通/攻击/坦克/狂战/辅助/宗师/防御 → `party_set_state`;普通 =
> 清当前角色 `on=false`+ 踢出 → `party_leave` = `SendPartyRemovePacket`+ 名字(+★队长)
> + HP gauge + `affects[7]` 非零 → 附加效果 chip(tooltip 带值,槽位名暂定)。点名字 →
> `set_target(vid)`。顶部:EXP 分配开关(`party_set_distribute` 0 不均分 / 1 均分)+
> 组队治疗(`party_use_skill(PARTY_SKILL_HEAL=1, 0)`)。队长判定 = `get_party()` 里本地
> `get_main_vid()` 那条的 `leader`。角色菜单按党技能等级门控(Tanker≥10…Defender≥40)暂略。
> ③ **修 `p8_test` 隐患**`_init` 原来 `_run()` 未 `await``_run` 第一个 `await process_frame`
> 之后的所有断言(含增量 34 的 `shop_ex` 多货架)从未真正生效 —— 改成 `await _run()` 后
> 暴露 `shop_ui.refresh()` 的 `queue_free` 延迟 bug(旧 row 当帧未消失 → `get_child_count`
> 多计),已改 `remove_child` + `queue_free`。
> `ctest 10/10` + 33 套 GDScript + 导入 + iOS 全绿。
> - **2026-08-31 增量 36(系统设置窗 + 私人商店/选魔石补真 uiscript**
> ① **系统设置窗** `project/ui/system_option_ui.gd` —— `UiScript`+`UiBuild` 装
> `assets/uiscript/uiscript/systemoptiondialog.py`;控件绑定逐字对照
> `assets/root/uisystemoption.py``music/sound_volume_controller`(sliderbar) →
> `snd.SetMusicVolume/SetSoundVolume`(接 `Audio.master_bgm/master_sfx` + 即时改在播 BGM 声道);
> `camera_short/long`(radio) → `game_camera.max_dist`(近 11 / 远 20,并夹当前 dist);
> `fog_level0/1/2`(浓/中/淡 radio) → `Environment.fog_enabled` + `fog_density`
> `[0.055/0.018/0.004]``tiling_cpu/gpu`+`apply`(原 CPU/GPU 分块渲染)Godot 渲染器无对应,
> 保留占位。设置持久化到 `user://system_option.cfg`= 原 `systemSetting` 配置文件),
> `setup()` 时读回即时生效,`.msenv` 换 `Environment` 后 `_on_main_set` 重新 `_apply_all`。
> `game_scene`ESC 呼出(`ui_manager` 先关最顶层窗并吃掉事件 → 到 `game_scene` 说明无窗打开)。
> 新增 `system_option_ui_test.gd`(装载 + 音量/镜头/雾绑定 + cfg 持久化 + 二次实例读 cfg)。
> ② **私人商店窗改走真 uiscript** `privateshopbuilder.py`board + TitleBar + NameLine +
> `ItemSlot` 5×8=40 grid + Ok/Close):40 格从 `_win.nodes` 索引,`NameLine`(text) 上盖
> `LineEdit`,背包候选面板挂窗右侧(原客户端靠主背包拖拽,此处点选)。`itemStock` 交互模型
> (拿起 / 落位 / 价格弹窗 / 排序 + display_pos / 撤下 / 개설 / 철수)不变。测试拆出独立
> `private_shop_ui_test.gd`uiscript,缺资产跳过),`shop_cube_mall_test` 私人商店段移除。
> ③ **选魔石窗** `select_item_ui.gd` 早已走真 uiscript `selectitemwindow.py`(增量 33),无需改。
> C++ 无改动;`ctest 10/10` + 33 套 GDScript + 导入 + iOS 全绿。
> - **2026-08-31 增量 35(私人商店开设窗 = `PrivateShopBuilder` 交互模型)**`private_shop_ui.gd`
> 从「前 5 行 checkbox」重做成 1:1 复刻 `assets/root/uiprivateshopbuilder.py` 的
> `itemStock` 模型:左侧背包候选(点一件“拿起”)+ 右侧 40 格(`shop.SHOP_SLOT_COUNT`5×8);
> 拿着件点空格 → 价格输入(`uiCommon.MoneyInputDialog`)→ 落位(`AddPrivateShopItemStock` +
> `itemStock[targetSlot]=(src)`);点已占用格 → 撤下(`OnSelectItemSlot` /
> `DelPrivateShopItemStock`)。개설(OkButton) 把 stock 按格号排序、`display_pos = 格号`、
> 上限 39`PRIVATE_SHOP_ITEM_MAX_NUM` = `TPacketCGMyShop::bCount` cap)打包成
> `M2Client.open_private_shop(sign, [{vnum,count,inv_cell,price,display_pos}])`
> = `TPacketCGMyShop` + `TShopItemTable`×N,字段 1:1)。`shop_cube_mall_test` 私人商店段
> 重写(拿起 / 落位 / 价格弹窗 / 排序 + display_pos / 撤下 / 철수)。C++ 无改动;
> `ctest 10/10` + 31 套 GDScript + 导入 + iOS 全绿。
> NOTE: 面板仍手绘(与 mall_ui / cube_ui 一致),真 `.sub` 版 `privateshopbuilder.py` uiscript 待后续。
> - **2026-08-31 增量 34`SHOP_GC_START_EX` 多货架商店)**`entity_store` 新增
> `SHOP_GC_START_EX`(10) 解析 —— `TPacketGCShopStartEx{u32 owner_vid, u8 tab_count}`
> + `tab_count` × `{ShopTabHead(char name[32]+u8 coin_type=33B), ShopItem[40]}``wire.h`
> 加 `ShopTabHead` + 断言 33`SHOP_TAB_NAME_MAX=32`/`SHOP_TAB_COUNT_MAX=3`)。
> `ShopEntry` 加 `pos`(货架内槽位,买位置用),`ShopTab{name,coin_type,items}` +
> `m_shop_tabs``SHOP_GC_START` 也改成按固定 40 槽数组填 `pos``SHOP_GC_END` 清货架。
> `shop_items()` 仍是 tab 0 的镜像(旧调用不动)。`M2Client.get_shop()` → `{vid, open,
> tabs:[{name, coin_type, items:[{pos,vnum,price,count}]}]}`。`shop_ui.gd``tabs>1` 显示
> 货架按钮,买位置 = `tabIdx*SHOP_SLOT_COUNT(40)+slotPos`1:1 对齐 `uishop.py`
> `GetIndexFromSlotPos`)。`net_entity_test` 加双货架合成包断言(名称 / coin_type / 槽位保留 /
> tab0 镜像 / END 清空),`p8_test` 加多货架 UI + `buy pos 5` / `buy pos 43` 断言。
> `ctest 10/10` + 31 套 GDScript + 导入 + iOS 全绿。
> - **2026-08-31 增量 33(选魔石窗,样板第 2 例)**:新增 `project/ui/select_item_ui.gd`
> ——`UiScript`+`UiBuild` 装载 `assets/uiscript/uiscript/selectitemwindow.py`5×8
> `grid_table`);填格逻辑逐字照 `assets/root/uiselectitem.py.RefreshSlot`:遍历背包前
> `INVENTORY_PAGE_SIZE*2` 格,只留 `item.IsMetin``proto.type == ITEM_TYPE_METIN` = 10
> 且 `GetItemGrade <= 2`= 物品**内部名**最后一位数字,非数字按 0)的物品,最多 54 个。
> 点格 → `client.script_select_item(inventoryCell)`= `net.SendSelectItemPacket`
> `CG_SCRIPT_SELECT_ITEM` 0x0903+ 关窗;ExitButton / 关闭 → `script_select_item(0)`
> `uiselectitem.Close`)。触发链 1:1`quest_dialog.parse_script` 识别 EventManager
> `[SELECT_ITEM]` token → 新增 `select_item_requested` 信号(对应 `PythonEventManager`
> `EVENT_TYPE_SELECT_ITEM` → `interfacemodule.BINARY_OpenSelectItemWindow`)→
> `game_scene` 接到 `select_item_ui.open()`。新增 `select_item_ui_test.gd`token 解析 +
> 过滤 + 选包 + 关窗发 0);`ctest 10/10` + 31 套 GDScript + 导入 + iOS 全绿。
> - **2026-08-31 增量 32(角色状态窗 = 1:1 窗口迁移样板)**:新增
> `project/ui/char_status_ui.gd`——直接用 `UiScript`+`UiBuild` 装载
> `assets/uiscript/uiscript/characterwindow.py` 真布局;数值绑定逐字对照
> `assets/root/uicharacter.py.RefreshStatus`Level/Exp/RestExp、HP/SP、
> STR/DEX/HTH/INT、ATT=`(min|max)+ATT_BONUS+ATTACKER_BONUS`、
> DEF=`DEF_GRADE(+DEF_BONUS)`、MATT=`MAG_ATT+(min|max)MAGIC_WEP`、MDEF、
> ASPD/MSPD/CSPD/EREPointTypes 索引取自 m2dev `Packet.h`)。加点 / 按钮发
> `client.say(0,"/stat ht"|"/stat- ht" …)`,与原 `statusPlusCommandDict` /
> `statusMinusCommandDict` 完全一致;`POINT_STAT>0` 才显示加点按钮。STATUS/SKILL/
> EMOTICON/QUEST 四页做 `SetState` 页签切换(技能 / 表情 / 任务的数据仍在各自专用窗)。
> 名称 / 帮会 / 职业头像取 `get_entity(get_main_vid())`。`game_scene` 挂 V / C 热键。
> 新增 `char_status_ui_test.gd``ctest 10/10` + 30 套 GDScript + 导入 + iOS 全绿。
> 这是「原 `interfacemodule.py` 逐窗口迁移」的样板,后续选道具网格 / 商店 START_EX /
> 私人商店 39 格照抄这套(uiscript 装载 + 逐字数值绑定 + 原聊天命令 / 封包)。
> - **2026-08-31 增量 31(真服 E2E 二轮扫,单会话)**:以
> `MT_NET_TRACE=1 MT_E2E_SWEEP=1 ./build/extension/net_e2e` 完成一次
> auth→选人→PHASE_GAME 会话;出站明文 / 加密字节均已落盘。10 个安全探针全部发送成功、
> 450ms 后仍在线、`unknown_header=0``CG_CHARACTER_POSITION`(5B)、
> `CG_SYNC_POSITION`(16B)、`CG_SCRIPT_SELECT_ITEM`(8B)、`CG_QUEST_CANCEL`(4B)、
> `CG_PARTY_USE_SKILL`(9B)、`CG_FLY_TARGETING`/`CG_ADD_FLY_TARGETING`(16B)、
> `CG_FISHING`(5B)、`CG_SHOOT`(5B)、`CG_USE_SKILL`(12B)。`CG_QUEST_CANCEL` 触发 1 条
> quest-change;钓鱼收到真服 `Please choose a Fishing Pole.`(无鱼竿的预期业务拒绝),其余
> 无业务事件但均无断连 / 无解析错误。被动 `GC_LAND_LIST` 436B 在登录洪流中真服收到并解析。
> 扫描暴露一个代码缺口:`GC_FLY_TARGETING`/`GC_ADD_FLY_TARGETING` 与
> `GC_CHANGE_SKILL_GROUP` 已有 `EntityStore` 实现却未列入 `GameClient` 世界分派,现已补路由并由
> `net_entity_test` 复核。`CG_DUNGEON`、`CG_WARP`、改名 / 符号 / 物品变更等破坏性或独立
> 连接协议本轮未发送,仍保持“待真服验”。
> - **2026-08-31 增量 5(实现复核)**:修正文档中已经落后的功能状态。
> **情侣状态**已不再是“整体未做”:`GC_LOVER_INFO`(70B) / `GC_LOVE_POINT_UPDATE`(5B)
> 已接入 `EntityStore`,并通过 `M2Client.get_lover()` / `lover_changed` 驱动常驻
> `love_ui`C++ 状态测试和 `love_ui_test.gd` 均有覆盖;仍缺完整情侣交互与真服验收。
> **角色 / 装备渲染**已补本地主角的身体盔甲 / 时装 parts、头盔 / 假发 / 发型、右手武器、
> 左手盾牌、`item_proto.specular/100` 球面高光、角色 LOD 迟滞 + 0.18s 淡入、动作切换
> ease-in、`.msa` 音效 / 特效 / 震屏 / 屏闪事件和角色接触阴影首版。**地图 / 材质**行同步
> 记录 16 层 splat、原 UV / 源分辨率、水纹序列 / 水深 alpha、terrain patch 剔除和物体
> Y-up 旋转修复。协议审计总缺口仍为 **24 个 header**,但 `GC_LOVER_*` 已从“声明未消费”
> 清单移除;输入行修正为已支持 F1–F4 与数字键 1–9。
> 本次同时补充“参考模块覆盖索引”,把协议表之外的聊天、TextTail、HUD、UI 运行时、输入 / 相机、
> 登录串场和桌面外壳差距单独列出。
> - **2026-08-31 增量 6(位置同步上行)**:补齐 `CG_CHARACTER_POSITION`(0x0A60) /
> `CG_SYNC_POSITION`(0x0303) 的 wire 结构、尺寸断言、`GameClient` 发送接口和
> `M2Client.character_position()` / `sync_positions()` API;回环测试覆盖姿态包、最多 16 个
> 坐标元素、字段值和空批次 / 超限拒绝。两项已从“缺失 header”移入“已声明但待核”——仍需
> 接入原版的自动碰撞纠偏时机、姿态动作和真服务器端到端验收。
> - **2026-08-31 增量 7(任务应答协议)**:补齐 `CG_SCRIPT_SELECT_ITEM`(0x0903) /
> `CG_QUEST_CANCEL`(0x0906) 的 wire 结构、尺寸断言、`GameClient` 发送接口和
> `M2Client.script_select_item()` / `quest_cancel()` API;回环测试验证选择值与取消包。
> 同时让 `QuestDialog` 支持基础 `[INPUT]` 文本框和 Escape 取消;缺失 header 清单由 22 项
> 降为 20 项。任务窗口仍需接入选道具网格、原版 IME / 输入校验和完整 `EventManager` 标签语义。
> - **2026-08-31 增量 8(组队技能上行)**:补齐 `CG_PARTY_USE_SKILL`(0x0705) 的 wire
> 结构、尺寸断言、`GameClient.send_party_use_skill()` 和 `M2Client.party_use_skill()`
> 回环测试验证技能索引 / 目标 VID。缺失 header 清单由 20 项降为 19 项;队伍技能的
> 冷却、权限和队员世界标记仍待补齐。
> - **2026-08-31 增量 9(飞行目标广播)**:补齐 `GC_FLY_TARGETING`(0x0411) /
> `GC_ADD_FLY_TARGETING`(0x0412) 的 20B wire 结构;`EntityStore` 持久化 shooter 的目标
> VID / 坐标并产生追加标记,`M2Client.fly_targeting` 信号向 Godot 暴露;C++ 实体测试覆盖
> 设置目标、追加目标和坐标回退。缺失 header 清单由 19 项降为 17 项。
> - **2026-08-31 增量 10(技能组切换)**:补齐 `GC_CHANGE_SKILL_GROUP`(0x021D) 的 5B
> wire 结构、`EntityStore` 技能组状态 / dirty 标记、`M2Client.get_skill_group()` 和
> `skill_group_changed` 信号;切换会同时使技能快照失效,C++ 实体测试覆盖状态更新。
> 缺失 header 清单由 17 项降为 16 项。
> - **2026-08-31 增量 11(角色改名)**:补齐 `CG_CHANGE_NAME`(0x010B) /
> `GC_CHANGE_NAME`(0x010C) 的固定长度 wire 结构、`GameClient` 改名发送与角色槽位更新、
> `M2Client.change_name()` 及 `char_name_changed` 信号;回环测试覆盖名称字节和 pid 广播。
> 缺失 header 清单由 16 项降为 14 项。
> - **2026-08-31 增量 12(公会创建请求)**:补齐 `GC_REQUEST_MAKE_GUILD`(0x0731) 的
> 无负载包解析,`GameClient` 产生请求事件,`M2Client.guild_make_requested` 向 Godot 暴露,
> 回环测试覆盖事件消费。缺失 header 清单由 14 项降为 13 项。
> - **2026-08-31 增量 13(主动传送请求)**:补齐 `CG_WARP`(0x0305) 的 4B wire 结构、
> `GameClient.send_warp()` 和 `M2Client.request_warp()`;回环测试验证上行请求及现有
> `GC_WARP` 目标回传。缺失 header 清单由 13 项降为 12 项。
> - **2026-08-31 增量 14(挖矿动作广播)**:补齐 `GC_DIG_MOTION`(0x0308) 的 13B wire
> 结构、`EntityStore` 挖矿动作事件、`M2Client.dig_motion` 信号和 `NetWorld` 朝向 / 动画
> 元数据处理;实体、回环和 Godot 桥接测试覆盖。缺失 header 清单由 12 项降为 11 项。
> - **2026-08-31 增量 15(公会符号下载)**:补齐 `CG_SYMBOL_CRC`(0x0723) / `GC_SYMBOL_DATA`(0x0732)
> 的 16B / 8B 结构和原始数据流;`MarkClient` 支持 CRC 请求与变长符号文件接收,
> `M2Client.download_guild_symbol()` / `get_guild_symbol()` / `guild_symbol_ready` 已接入,
> 回环测试覆盖 CRC 字段、guild id 和数据长度。缺失 header 清单由 11 项降为 9 项。
> - **2026-08-31 增量 16(钓鱼协议)**:补齐 `CG_FISHING`(0x0B01) / `GC_FISHING`(0x0B10)
> 的 5B / 10B wire 结构、旋转方向换算、`EntityStore` 事件和 `M2Client.fishing()` /
> `fishing_event`;回环与 Godot 桥接测试覆盖。缺失 header 清单由 9 项降为 7 项。
> - **2026-08-31 增量 17(副本协议)**:补齐 `CG_DUNGEON`(0x0B02) / `GC_DUNGEON`(0x0B11)
> 的 4B / 5B wire 结构,支持目的地坐标变长 body,接入 `M2Client.request_dungeon()` /
> `dungeon_event`;实体、回环和 Godot 桥接测试覆盖。缺失 header 清单由 7 项降为 5 项。
> - **2026-08-31 增量 18(领地与观战者)**:补齐 `GC_LAND_LIST`(0x0B12) 的 4B 头 + 24B
> 元素变长解析,以及 `GC_OBSERVER_ADD/REMOVE/MOVE`(0x0B200x0B22) 的 12B / 8B wire
> 结构;`EntityStore` 保存领地和观战者状态,`M2Client.get_land_areas()` /
> `get_observers()` 与 `land_areas_changed` / `observer_event` 已接入;小地图现在消费观战者
> 状态并绘制紫色菱形标记。实体、回环、Godot 桥接和 `p9_test.gd` 覆盖。缺失 header 清单由
> 5 项降为 **1 项**(仅剩 `CG_TEXT`,参考端未发现实际发送方)。
> - **2026-08-31 增量 19(钓鱼动作消费)**`NetWorld` 和 `NetPlay` 现在消费
> `fishing_event` 的 START / STOP / REACT / SUCCESS / FAIL 子头,记录方向并尝试驱动远端及
> 本地主角的钓鱼动作;FISH 子头按参考客户端语义保留为鱼获物品事件,不误当作角色 VID。
> `combat_fx_test.gd`、`netbridge_test.gd` 覆盖动作分发。
> - **2026-08-31 增量 20(钓鱼结果日志)**`ChatUI` 消费 `GC_FISHING` 的 SUCCESS / FAIL /
> FISH 事件,将命中、失败和鱼获物品写入系统 / 战斗日志;`chat_test.gd` 覆盖结果路由。
> - **2026-08-31 增量 21(小地图领地标记)**:小地图继续消费 `GC_LAND_LIST`,把全局厘米坐标
> 的领地矩形绘制为公会金色 / 中性青色边框;`p9_test.gd` 补充领地数据供 headless 重绘路径
> 自检。观战者紫色菱形与领地边框均已接入,仍缺原版 minimap 贴图和 Atlas 交互。
> - **2026-08-31 增量 22(小地图缩放)**`minimap.gd` 新增 `set_scale()` / `get_scale()`
> 对齐原 `CPythonMiniMap::SetScale` 的像素 / 米缩放语义;鼠标滚轮在小地图区域内调整缩放,
> `p9_test.gd` 验证缩放会改变世界坐标投影。仍缺原版贴图、Atlas 拖动和坐标查询窗口。
> - **2026-08-31 增量 23(飞行目标消费)**`NetWorld` 为每个 shooter 保存
> `FLY_TARGETING` / `ADD_FLY_TARGETING` 目标队列;`GC_CREATE_FLY` 在终点 VID 缺失时回退到
> 队列中的目标 VID 或全局厘米坐标,补上原版 `SetFlyTarget` / `AddFlyTarget` 的关键语义。
> `netbridge_test.gd` 覆盖无终点 VID 的投射物创建;真实弹道模型、命中时机和特效仍待补齐。
> - **2026-08-31 增量 24(弓技能发射时机)**`SkillTable.is_ranged()` 根据
> `weapon_limit=BOW` 识别远程技能;`GameScene` 为弓技能暂存 `CG_SHOOT`,在 `.msa` 的 FLY
> motion event 到达时发送,超时自动清理,避免近战技能误发。`skill_test.gd` 与
> `eterngrn_polish_test.gd` 覆盖远近程识别及一次性发射。
> - **2026-08-31 增量 25Atlas 大地图窗口)**:新增 `AtlasUI`,复用地图区块
> `minimap.dds` 构建 Atlas 底图;支持 M 键打开 / 关闭、地图名称、玩家位置(全局厘米)显示、
> 左键拖动和右键复位。无底图资源时仍保留可交互坐标查询;`atlas_test.gd` 与
> `gamescene_test.gd` 覆盖窗口装配和输入路径。
> - **2026-08-31 增量 26(队伍地图标记)**:圆形小地图和 Atlas 从 `get_party()` / 实体快照
> 区分队员,绘制绿色队员点、队长环和 Atlas 队员标记;`p9_test.gd` / `atlas_test.gd` 覆盖
> 队伍标记绘制路径。队伍世界标记已不再是纯 UI 列表,仍需按原版颜色和离线状态逐项校准。
> - **2026-08-31 增量 27(输入差距复核)**:根据 `game_camera.gd` 当前实现修正文档:桌面右键
> 拖拽环绕、滚轮缩放和触屏单指 / 双指手势已经存在,不再列为缺失功能;剩余差距改为相机
> 模式、灵敏度 / 键位持久化及窗口冲突规则。
> - **2026-08-31 增量 28(移动速度校准)**`GC_CHANGE_SPEED` 已暴露实体的
> `moving_speed``NetPlay` 在主角 `entity_info` 到达时调用 `PlayerController.set_server_speed()`
> 以原版 100 为基准缩放本地步行 / 奔跑预测(0.253.0 限幅),`netplay_test.gd` 覆盖 150 → 1.5×。
> - **2026-08-31 增量 29(攻击速度校准)**`GC_CHARACTER_ADD[2]` / `GC_CHARACTER_UPDATE`
> 的 `attack_speed` 已进入 `EntityStore` / `M2Client.entity_dict()`;本地主角的
> `POINT_ATT_SPEED` 也通过 `get_points()` 暴露。`NetPlay` 按 m2dev 的 `attack_speed / 100.0`
> 动作速率换算自动攻击间隔(0.25–3.0 倍限幅,100 → 0.6 秒),并由 `netplay_test.gd` /
> `net_entity_test` 覆盖实体字段和 150 → 0.4 秒。仍需用真实 `.msa` motion duration、武器 / 坐骑
> 规则和服务端攻击合法性逐帧校准。
> - **2026-08-31 增量 4(真机验证)**`net_e2e` 加 post-2026-08-30 协议探针(env 门禁
> `MT_E2E_CHARCREATE` / `MT_E2E_MYSHOP` / `MT_E2E_CUBE`)。真服 `192.168.21.203`
> 确认 1:1`CG_CHARACTER_CREATE`(77B/u16 job) + `GC_PLAYER_CREATE_FAILURE`(5B) +
> `GC_EMPIRE`(5B) + `GUILD_GC_SKILL_INFO`(17B body) + `GUILD_GC_WAR` + `CG_MYSHOP`
> (38B+13B×N) 全部被真服接受 / 解析正确,无 unknown header。Cube/MALL 送出路径通但
> 需 NPC 交互;`CG_CHARACTER_DELETE` / 龙魂精炼 / 会徽上传(破坏性)仍未真机验。
> - **2026-08-31 增量 3**:两个 ⬜ 项落地首版 —— **私人商店 / 道具商城**
> `CG_MYSHOP` / `GC_MALL_OPEN|SET|DEL` / `CG_MALL_CHECKOUT` + `private_shop_ui` /
> `mall_ui`)、**Cube 제작**`GC_CHAT/COMMAND` 的 `cube …` 总线 → `CubeState` +
> `cube_ui`)。仍缺能量条、原版拖放 UI 细节。
> **拍卖行**经核查 m2dev-client 无此系统 → 标注 ❌ 不做(见上「不做清单」)。
> **反作弊**`CG_HACK` + `ProcessCRC`/`ProcessScanner`)→ 标注 ❌ 不做。
> **「协议覆盖审计」段**改为逐条清单:脚本核对 `wire.h` vs `Packet.h`,主网络层
> 仍缺 **24 个 header**(位置同步上行 / 战斗广播 / 任务 / 社交 / 改名 / 世界玩法整组;
> `CG_HACK` 已移入「不做」),另有 3 个「已声明未消费 / 待核」。
> - **2026-08-31 增量 2**
> - 角色创建 / 删除流程完成首版并接近 1:1(原为“暂不纳入”)——见新表行
> 「选人 / 建号 / 删号」。
> - 怪物 / NPC 真模型渲染修复:`root/npclist.txt`vnum→代号→目录)解析 +
> `net_world.catch_up()` 补拉协程化 `setup()` 漏掉的初始 spawn 洪流。世界玩法行
> 的“NPC / 怪物首版”由“能出占位胶囊”提升为“能出真模型 + 贴图 + 动作 + 名字牌 /
> 血条”。
> - `GC_EMPIRE`(0x0109) 已暴露为 `M2Client.get_empire()`;选人页显示国家名 + 国旗。
> - 关键 1:1 差距表新增“状态”列。
## 结论
`mtgodot-poc` 已经不是早期“只能登录、移动和攻击”的骨架:登录到 PHASE_GAME、基础战斗、
背包 / 装备、技能 UI、任务、聊天、组队 / 好友、商店 / 交易 / 仓库、公会、精炼、龙魂、
地图和部分特效都已有可运行首版。
当前的主要差距已从“有没有功能”转为:
1. 部分关键协议只实现了表现包或收包,没有完成原客户端的完整上行语义;
2. 数据模型已扩到 24 个 wear 位置和独立 16 格腰带,并接入部分装备 / 时装换模,但原版
costume / belt 窗口、拖放、tooltip 和部位规则仍不完整;
3. 多数玩法窗口是简化实现,未复现原 Python UI 的完整行为;
4. EffectLib、SpeedTree、材质和世界表现仍有明确占位或近似;
5. 打包资产路径、桌面配置和输入体验尚未形成原客户端等价闭环。
因此当前应称为“核心功能纵切完成”,不能称为 `m2dev-client` 的 1:1 客户端。
## 已具备的首版能力
- 登录、服务器 / 频道选择、频道负载状态、断线重连、选人、**建号 / 删号**、进入游戏;
- Metin2 风选人页(背景 / 3D 角色 / 职业书法字 / 国家 + 国旗 / 四维条 / ◀▶ 槽位 / 建删弹窗);
- 基础移动、攻击、选目标、生命 / 法力 / 经验、伤害飘字、affect、死亡与复活窗口;
- 背包、基础装备换模、地面物品、技能窗口和快捷栏读取;
- 聊天、私聊、基础任务对话和任务日志;
- 组队、好友、情侣名 / 爱意值状态、NPC 商店、交易、仓库;
- 公会、公会技能、公会战、会徽上 / 下载、精炼、龙魂精炼;
- 地形、建筑、水体、**NPC / 怪物真模型(vnum→npclist.txt→目录)**、小地图、频道、昼夜和天气首版;
- GR2 模型 / 动画、换装首版、武器 / 盾挂点、距离 LOD / 淡入、角色接触阴影、
`item_proto` 驱动的球面高光和 `.mse` 粒子首版。
这些项目只说明已有入口和基础闭环;是否 1:1 仍以下表为准。
## 关键 1:1 差距
| 优先级 | 状态 | 模块 | 当前实现 | 需要迁移的原客户端语义 |
|---|---|---|---|---|
| P1 | 🟡 | 选人 / 建号 / 删号 / 改名 | `CG_CHARACTER_CREATE`(0x0201) / `CG_CHARACTER_DELETE`(0x0202) / `CG_CHANGE_NAME`(0x010B) / `GC_PLAYER_CREATE_SUCCESS`(0x020C) / `GC_PLAYER_CREATE_FAILURE`(0x020D) / `GC_PLAYER_DELETE_SUCCESS`(0x020E) / `GC_PLAYER_DELETE_WRONG_SOCIAL_ID`(0x020F) / `GC_CHANGE_NAME`(0x010C) 全部有尺寸断言;`GameClient` 发送接口、角色槽位补丁和 `drain_*_events()``M2Client` 同名建删改 API、4 个建删结果信号及 `char_name_changed``net.loopback_flow` 覆盖建删和改名名称字节 / pid 广播。**真机验证(2026-08-31`net_e2e MT_E2E_CHARCREATE=1`**:建号、`GC_PLAYER_CREATE_FAILURE``GC_EMPIRE` 已验。选人页有建号 / 删号弹窗与国家国旗 | `CG_CHARACTER_DELETE` 破坏性,真机未验;改名 UI / 改名卡消耗和重复名错误提示未接;建号缺外形 / 发型选择与属性再分配 UI(现发 `shape=0` + 职业基础四维);缺 `SELECT_EMPIRE` 选国界面;原版选人台座、镜头动画、`OnCreateFailure` 完整错误码文案 |
| P0 | 🟡 | 技能施放协议 | `CG_USE_SKILL``CG_FLY_TARGETING``CG_ADD_FLY_TARGETING``CG_SHOOT` 已有尺寸断言、原生发送接口和 loopback 字节验证;服务端 `GC_FLY_TARGETING` / `GC_ADD_FLY_TARGETING` 已进入 `EntityStore`,通过 `M2Client.fly_targeting` 暴露,且已补入 `GameClient` 世界分派;`GC_CHANGE_SKILL_GROUP` 会更新 `get_skill_group()` 并使技能快照失效;快捷栏先发 `use_skill(skill_id,target_vid)`,再同步 `CG_MOVE` 技能动作;弓技能会在 `.msa` FLY 事件发送 `CG_SHOOT`。真服单会话已发送 `CG_USE_SKILL` / `CG_FLY_TARGETING` / `CG_ADD_FLY_TARGETING` / `CG_SHOOT`,均无断连 / unknown | 仍需实现范围 / 多目标选择器来自动驱动 `ADD_FLY_TARGETING`;冷却、目标合法性、真实弹道和客户端表现也须按技能类型细分;尚未在真服观察到对应 GC 飞行目标推送 |
| P0 | 🟡 | 快捷栏持久化 | 已覆盖 36 槽(4 页 × 9 格)、F1–F4 切页和 1–9 激活;拖放 / 清除 / 交换会分别发送 `CG_QUICKSLOT_ADD/DEL/SWAP`,服务器 `GC_*` 可完整恢复;技能冷却读取对应 `.msk``CoolTimeFormula` | 仍需补命令 / 表情快捷槽、物品拖放来源、鼠标物品态及原 UI 的完整快捷栏交互;冷却还未叠加原客户端的施法速度与所有特殊技能规则 |
| P0 | 🟡 | 物品操作 | 已有移动、使用、丢弃、拾取;Shift 选“来源→目标”发 `ITEM_USE_TO_ITEM`Ctrl+右键向当前目标 `ITEM_GIVE`Alt+右键以数量框发 `ITEM_DROP2``ITEM_OWNERSHIP` 会实时更新地面名条;`GC_VIEW_EQUIP` 已保存 11 格检查快照并弹出装备查看窗 | 仍需复刻原鼠标物品态、操作确认 / 错误提示、宝石专用规则,以及装备查看窗的原版槽位图标、3D 纸娃娃与交互 |
| P0 | 🟡 | 装备 / 背包数据模型 | 常规背包维持 90 格;运行时模型已扩展到 24 wear 位置,并将腰带改为独立 16 格;背包右侧动态呈现能力 / 时装 / 戒指 / 腰带和 4×4 腰带格,发包使用原客户端 `INVENTORY` 全局 cell`90 + wear` / `152 + belt`),按 `values[0]` 的原腰带等级规则锁格。`EquipModel` 已把本地主角 `parts[]`(时装优先)和装备槽接到身体 shape / TargetSkin、头盔 / 假发 / 发型、右手武器、左手盾牌,并用 `item_proto.specular/100` 驱动护甲球面高光 | 仍需复刻原 costume / belt 窗口的槽位贴图、分页、tooltip 和鼠标物品态;补齐饰品 / 翅膀等多部件遮挡规则、远端角色同等换装以及逐部位高光 / 卸装复位,不以当前兼容性扩展面板代替原 UI |
| P0 | 🟡 | 移动与战斗同步 | 有 `GC_MOVE`、本地预测、攻击;`M2Client.character_position()` 可发送姿态回报,`M2Client.sync_positions()` 可按原包批量发送最多 16 个坐标,`M2Client.request_warp()` 可发送 `CG_WARP` 主动传送请求;`GC_SYNC_POSITION` 批量坐标纠偏、`GC_CHANGE_SPEED``GC_WALK_MODE``GC_CHARACTER_POSITION``GC_DIG_MOTION` 已恢复到实体状态,`NetPlay``moving_speed/100` 校准主角步行 / 奔跑预测,挖矿广播可驱动目标朝向 / dig 动画,`WALK_MODE` 会驱动远端 walk/run 动画选择;真服单会话已发送 `CG_CHARACTER_POSITION`(5B) 与 `CG_SYNC_POSITION`(16B),均无断连 / unknown | 仍需按原客户端同步窗口、碰撞纠偏触发时机与保留动作处理,而不是只靠距离阈值和 lerp;姿态动作还没有完整 UI / 动画语义 |
| P0 | 🟡 | PVP / 决斗 / 阵营 | `GC_PVP``DUEL_START` 已进入状态层,并以 `pvp_changed` / `duel_started` 信号提供给 Godot;挑战 / 战斗 / 复仇关系会在世界实体上显示标签;实体已有部分 empire / alignment / pk 字段 | 仍需 PK 模式上行、原版名字颜色、可攻击判定、阵营规则和原版目标板 / 决斗 UI |
| P1 | 🟡 | Actor 战斗运行时 | 有骨骼动画、基础 combo / 硬直 / 相机抖动;动作切换按 Granny `EaseInCurve(0,0,1,1)``2w²−w³` 逐骨 crossfade;角色 mesh 支持距离 LOD、15% 迟滞和约 0.18s 新旧 mesh 淡入(`LodGhost` + `lod_fade` uniform);右手武器 / 左手盾按 `weaponComposite[0] = weaponInvWorld[0]·weaponLocal[0]` grip 复合挂 `equip_right_hand`(探针实测 Δ≤0.06 m,非标准米级偏移文件夹断回退);`.msa` motion event 已分派声音、EffectPosition 特效、震屏和屏闪(FLY / WARP 留钩子);玩家 / 怪物强制实时投影并附脚底接触阴影 Decal;实体 `attack_speed` 与本地 `POINT_ATT_SPEED` 已按原版 100 基准影响攻击请求间隔 | 仍需用真实 `.msa` motion duration、武器 / 坐骑规则和服务端合法性校准攻击节奏;另补 combo 取消窗口、精确受击 / 击退、角色碰撞、同步碰撞、武器拖尾、剩余 motion event / 骨骼挂点语义、GPU skin LOD、双持 / 左手武器和目标锁定相机;`03150` 类偏移网格需 per-weapon 偏移数据或修 .gr2 |
| P1 | 🟡 | 飞行物与命中特效 | `GC_FLY_TARGETING` / `GC_ADD_FLY_TARGETING` 保存 shooter 的目标队列和 VID / 坐标;`GC_CREATE_FLY` 可消费队列并生成 tween 发光球 | 对齐 `FlyingObjectManager`:真实技能弹道模型、命中时机 / 命中特效、消失条件、声音与 EffectLib 特效 |
| P1 | 🟡 | 任务 / NPC | 支持 `[ENTER]``[CLEAR]``[NEXT]``[DONE]`、基础 `[QUESTION]``[INPUT]` 文本框和任务日志;`M2Client.script_select_item()` / `quest_cancel()` 已可发送原协议应答,Escape 会走取消包;真服单会话已发送 `CG_SCRIPT_SELECT_ITEM`(8B) 与 `CG_QUEST_CANCEL`(4B),取消触发 1 条 quest-change 且无断连 / unknown | 补选道具网格、原版 CJK IME / 输入校验、QuestButton、立绘、完整 EventManager 标签、世界箭头和屏幕边缘指示;选道具包本轮未出现脚本业务回包 |
| P1 | 🟡 | 商店 / 交易 / 仓库 | 普通 NPC 商店、交易状态和仓库列表已有首版;**`SHOP_GC_START_EX` 多货架已解析(`m_shop_tabs``get_shop()`),`shop_ui` 带货架按钮 + `tabIdx*40+slot` 买位置** | 补图标格网格、数量输入、交易回滚提示、仓库密码 / 改密码、完整拖放规则、`coin_type` 非金币货币显示 |
| P1 | 🟡 | 私人商店 / 道具商城 | **首版**`CG_MYSHOP`(0x0802) `CGMyShopHead`(38) + `MyShopItem`(13)×N 有尺寸断言 + `GameClient::send_open_private_shop` / `send_close_private_shop`(=`SHOP_CG_END`) + loopback 字节级;`GC_MALL_OPEN`(0x0841)/`GC_MALL_SET`(0x0842)/`GC_MALL_DEL`(0x0843) 路由进 `EntityStore``m_mall[135]` + `mall_open/size/slot/dirty`),`CG_MALL_CHECKOUT`(0x0840) `CGMallCheckout`(8) 发送;`M2Client` `open/close_private_shop``get_mall_items`/`mall_checkout` + `mall_opened`/`mall_changed` 信号;`ui/private_shop_ui.gd`**已重做成 `PrivateShopBuilder.itemStock` 模型:40 格 grid + 拿起 / 落位 / 逐件价格弹窗 / 点占用格撤下 / 개설按格号排序 + display_pos + 上限 39**)、`ui/mall_ui.gd`(列表 + 取出)。**真机验证(2026-08-31`net_e2e MT_E2E_MYSHOP=1`**`CG_MYSHOP` 38B head + 13B item×1 被真服接受,无断连 / 无 shop error`SHOP_CG_END` 关店正常。`CG_MALL_CHECKOUT` 送出不断连,但 mall 需点 NPC 才 open。他人开店 `SHOP_GC_START_EX` 多货架已解析(见增量 34)。⬜ `SYMBOL_DATA`/`GC_SHOP_SIGN` 头顶招牌 3D、真 `.sub``privateshopbuilder.py` uiscript、鼠标真拖放物品态 |
| — | ❌ | 拍卖行 / auction house | 不做 | m2dev-client 无此系统(`0x08xx` 段只有 shop/myshop/safebox/mall,全源码 / 协议 / 资产零 `auction` 命中)。需自定义协议 + 服务端支持才能实现,**标注为不做**,除非后续明确提需求 |
| P1 | 🟡 | Cube (제작) / 能量系统 | **首版**Cube 走 `GC_CHAT`/`CHAT_TYPE_COMMAND` 文本总线(对齐 `ServerCommand()`),`EntityStore::apply_server_command` 解析 `cube open/close/info/success/fail/r_list/m_info`(配方 `v,c` 列表 + `@`/`&`/`|`/`/` 分层材料 + 金币)→ `CubeState` + `drain_cube_events()``GameClient::send_cube_make/material_info/result_list``/cube make|mInfo|rList`);`M2Client` `get_cube`/`cube_make`/`cube_request_*` + `cube_opened`/`cube_closed`/`cube_changed`/`cube_result` 信号;`ui/cube_ui.gd`(配方列表 + 材料/金币 + 제작)。`net.entity_store` + `net.loopback_flow` 覆盖。真机(2026-08-31`/cube rList` 送出不断连,但服务器只在玩家站到 cube NPC 前才回应 → `cube open=0`,需 NPC 交互完整验。⬜ 能量条(`POINT_ENERGY` / affect)、原 `uiCube.py` 材料格 3D 图标 / 拖放 / 결과 애니、NPC 前完整验 send 命令字 |
| P1 | 🟡 | 组队 | 邀请、接受、离队;**成员信息板(`party_ui.gd`1:1 迁移 `uiparty.PartyMemberInfoBoard`):每员角色状态按钮(队长弹菜单 → `CG_PARTY_SET_STATE` 0x0704 分配 攻/坦/狂/辅/宗/防 或踢人)+ 名字(★队长) + HP gauge + `affects[7]` 附加效果 chip;顶部 EXP 分配开关(`party_set_distribute`+ 组队治疗(`party_use_skill(1,0)`**;真服单会话 `CG_PARTY_USE_SKILL`(9B) 后仍在线;小地图 / Atlas 绿色队员点 + 队长环 | 补角色菜单按党技能等级门控(Tanker≥10…Defender≥40)、`affects[7]` 精确槽位语义、真版 `.sub` 图标 / 颜色、队伍技能冷却 / 权限、离线颜色语义 |
| P1 | 🟡 | 好友 / 情侣 | 好友列表、上线状态和私聊入口已有;`GC_LOVER_INFO`(70B) / `GC_LOVE_POINT_UPDATE`(5B) 已有尺寸断言、`EntityStore::LoverInfo` 状态、`M2Client.get_lover()` / `lover_changed``love_ui` 常驻显示伴侣名和 0–100 爱意值;`net.entity_store``love_ui_test.gd` 覆盖 | 补好友分组和完整 messenger 行为;情侣仍需完整交互 / 状态图标与 affect 表现、断线 / 切图生命周期、原版文案和真服务器端到端验收 |
| P1 | 🟡 | 公会 | 信息、成员、技能、公会战、会徽上 / 下载和部分操作已有首版;`GC_REQUEST_MAKE_GUILD`(0x0731) 已解析并以 `M2Client.guild_make_requested` 通知;`CG_SYMBOL_CRC` / `GC_SYMBOL_DATA` 已支持原始符号文件下载 | 补完整权限 / 等级页、公告与日志、成员管理、创建公会 UI / 应答流程、公会战应答、积分板、领地和公会建筑 |
| P1 | 🟡 | 世界玩法 | 有 `CG_WARP` 主动请求、`GC_WARP` 同服 / 跨服处理、`CG_FISHING` / `GC_FISHING` 钓鱼方向与状态事件、`NetWorld` / `NetPlay` 钓鱼动作分发、`ChatUI` 鱼获 / 失败日志、`CG_DUNGEON` / `GC_DUNGEON` 副本请求与状态事件、`GC_DIG_MOTION` 挖矿动作广播、`GC_LAND_LIST` 领地状态、`GC_OBSERVER_ADD/REMOVE/MOVE` 观战者状态、频道、时间、NPC marker、天气粒子和 mount_vnum 字段;真服登录洪流已收到并解析 `GC_LAND_LIST`(436B),单会话发送 `CG_FISHING`(5B) 后收到“Please choose a Fishing Pole.”业务拒绝且连接保持;同服 `GC_WARP` 直接挪玩家,跨服 `addr≠0``M2Client` 复用 login key 直连目标 game server,断点重连也走该快路径 | 补完整钓鱼 UI / 鱼获背包落地、副本计时 / 入口 UI、坐骑真模型 / 骑乘动作 / 移速、地形贴花、DungeonBlock 和地图天气配置;`CG_DUNGEON` / `CG_WARP` 本轮为避免副作用未发送,跨服换图仍需真服端到端验收与场景生命周期清理 |
| P1 | 🟡 | 小地图 / Atlas | 可画玩家、实体、NPC、任务标记、观战者紫色菱形和 `GC_LAND_LIST` 领地边框;支持 `set_scale()` / 鼠标滚轮缩放;Atlas 支持底图、地图名称、玩家坐标、拖动和坐标查询 | 补原 minimap / Atlas 贴图细节、提示、队员 / NPC 分类、世界箭头、地图驱动天气配置和原版窗口视觉 |
| P1 | 🟡 | EffectLib | `.mse` Particle 已映射到 `GPUParticles3D` | 粒子 `.dds` 目前被程序化径向渐变替代,`.mde` 用 box 占位;需补 EffectMesh、SimpleLight、TexAni、MovingType、骨骼挂点和完整时间轴 |
| P1 | 🟡 | SpeedTree | `.spt` 只嗅探真实 bark / composite 贴图,几何为程序化 proxy | 迁移或离线转换真实树干、叶片、frond、LOD、billboard、风和阴影;当前不能作为 1:1 树木 |
| P1 | 🟡 | 地图 / 材质渲染 | A1 地形、静态物、水和 `.msenv` 环境已有首版;splat 已支持每区块最多 16 个活动层、原客户端 UV / offset 与最大源分辨率,地形拆 patch 做视锥 / 距离剔除;水使用原 30 帧 DDS 序列、水深 alpha 和轻微高度动画;静态物 Y-up 旋转共轭已修正。GR2 blend / two-sided 优先读材质信息,球面高光管线已接 `item_proto.specular`;角色有实时投影 + 接触阴影 | 补其余固定功能材质与多贴图分支、精确 alpha-test / blend / cull、透明排序、逐 patch 几何 LOD、baked / realtime 阴影 mask、血迹 / 施法阵等 Decal、云层和 lens flare;按受控参考帧校准水深、LOD、阴影、光照和材质 |
| P1 | 🟡 | UI 控件语义 | 80 个 `uiscript` 可解析和构建,覆盖约 20 类基础节点 | 解析成功不等于窗口完成;需补动画图片、slot 拖放、scrollbar/listbox 行为、tooltip、radio group、候选窗、原层级 / 锚点 / 焦点 / modal 规则 |
| P1 | 🟡 | UI 窗口行为 | 背包、技能、任务、聊天、社交和商店等有简化窗口;腰带背包、扩展装备和装备查看已有兼容窗口;**选人页已按 Metin2 风重做**;**角色状态窗(`char_status_ui.gd`)已按 `characterwindow.py` 真布局 + `uicharacter.py.RefreshStatus` 逐字绑定 + 原 `/stat` 加点命令做成,作为 1:1 窗口迁移样板** | **选魔石窗** `select_item_ui.gd`、**系统设置窗** `system_option_ui.gd`、**游戏设置窗** `game_option_ui.gd``/setblockmode` + `/pkmode` 真命令 + 显示开关持久化)、**ESC 系统菜单** `system_menu_ui.gd``systemdialog.py` + `/phase_select`/`/logout`/`/in_game_mall`)、**私人商店窗** `private_shop_ui.gd`、**组队成员板** `party_ui.gd` 均已按样板做成 | 按样板逐个复现原 `Interface.MakeInterface()` 装配与视觉 / 交互细节:帮助窗、Atlas、商城 / Cube 真 uiscript、Guild Building、game_option 的名字颜色 / 伤害数字等渲染侧钩子(拍卖行参考端无此窗口)|
| P2 | 🟡 | 输入 / 快捷键 | WASD、点地、触屏、右键拖拽环绕、滚轮缩放、双指捏合、I/K/J/O/G/L/Z、F1F4 快捷栏切页和数字键 1–9 激活已接;实体点选从固定半径提升为优先使用子 mesh 合并世界 AABB,无码型实体才回退胶囊近似 | 对齐空格连续攻击、显示名字、鼠标物品态、PK 模式、正式客户端截图、相机模式与灵敏度配置持久化、键位配置和窗口冲突规则 |
| P2 | 🟡 | CJK 输入与本地化 | 支持 KEY→VALUE 查表和基础字体注入 | 补 `PythonIME` 中 / 日 / 韩候选窗、复数 / 性别规则、字体自动切换、完整 item / mob 名称与 tooltip 本地化 |
| P2 | 🟡 | 音频 | 有 BGM crossfade、UI 音效池和 3D 音效 | 补地图 BGM 自动选择、`.msenv` 环境循环音、武器 / 技能 / 怪物 / NPC 声音事件和原距离衰减规则 |
| P2 | 🟡 | 客户端外壳 | Godot 工程已有 macOS / iOS / Android 构建脚本 | 补 `config.exe` 对应的分辨率、显示、音量、阴影、语言和鼠标配置;Movie / Web / Logo、补丁器约束、Discord RPC 按发布目标取舍 |
| P0 发布 | 🟡 | Pack 资产统一读取 | `PackMount` / `AssetSource` 已实现并有 roundtrip 测试 | `Metin2World` / `Metin2Model` 等仍有直接 `AssetResolver::resolve()` 的散文件路径;必须统一切到 `AssetSource::read/to_path()`,否则 `.epk/.eix` 打包客户端不等价 |
## 参考模块覆盖索引
下表以 `m2dev-client-src-main/src/UserInterface/` 的模块职责为索引,补充上表未展开的基础能力。
“已有”只表示当前有可运行首版;仍需满足本文末的 1:1 完成判据。
| 参考模块 / 能力域 | 当前实现 | 与 m2dev-client 的差距 | 优先级 |
|---|---|---|---|
| `AccountConnector` / `PythonNetworkStreamPhase*` 登录与相位 | auth → game → 选人 → LOADING → PHASE_GAME 已打通;`LoadingScreen``ReconnectUI`、跨服 `GC_WARP` 重连和频道状态探针均有首版 | 登录 / 选服 / 选频道的原版错误码、表单校验、超时与会话失效文案,服务器列表刷新和所有相位边界清理仍未逐项对齐 | P1 |
| `PythonChat` / `PythonTextTail` / `PythonPlayer` | 聊天四标签、私聊 / 组队 / 公会 / 喊话路由,实体气泡、名字、HP 条、伤害飘字和本地气泡已有 | 缺称号、帮会名、名字颜色和目标高亮框;`GC_DAMAGE_INFO`、经验 / 金钱变化的原版日志行、notice / big-notice 排版、历史滚动和 `InsultChecker` 过滤未完整迁移 | P1 |
| `PythonPlayer` HUD / affect / target | HP、SP、经验、等级、目标血条、死亡复活窗和 affect 色块条已绑定 | 原版状态栏布局、图标贴图、持续时间倒计时、目标板按钮 / 交互、所有 point 类型和战斗日志格式仍是简化版 | P1 |
| `PythonPlayerInput*` / `CameraProcedure` / `PythonApplicationCursor` | WASD、点地、触屏点选 / 单指相机拖动 / 双指捏合、桌面右键拖拽与滚轮缩放、网格 AABB 点选、相机遮挡淡出和碰撞已有;`GC_CHANGE_SPEED` 会校准本地移动速度 | 缺原版光标状态(talk / attack / buy / sell / pick 等)、相机模式与灵敏度配置持久化、键位配置、窗口冲突和空格连续攻击语义 | P1 |
| `EterPythonLib` / `PythonApplicationModule` UI 运行时 | 80 个真实 `uiscript` 可解析;约 20 类控件、窗口栈、modal、拖动标题栏和通用弹窗已有 | `ani_image``mark`、真实 `grid_table` / scrollbar / list 行为、`.dds` UI 贴图、鼠标物品态、焦点 / 锚点 / 层级和原版九宫格视觉仍未完全对齐 | P1 |
| `PythonEffect` / `PythonFly` / `EffectLib` | `.mse` Particle → `GPUParticles3D`、技能 / 服务器特效触发、基础飞行球和动作事件已有 | `.dds` 粒子纹理、`.mde` mesh、SimpleLight、MovingType / TexAni 时间轴、骨骼挂点、真实弹道 / 命中事件和声音仍缺 | P1 |
| `PythonMiniMap` / `PythonBackground` | 圆形小地图、实体 / NPC / 任务点、观战者、领地边框、队员 / 队长标记、鼠标滚轮缩放;Atlas 已有区块底图、拖动、坐标查询、地图名称和玩家 / 队员标记;地形 / 建筑 / 水体 / 天气和昼夜首版已有 | 缺原 minimap / Atlas 贴图细节、世界箭头、地图驱动天气配置和原版窗口视觉 | P1 |
| `PythonSystem` / `MovieMan` / `PythonApplicationWebPage` / `Discord` | Godot macOS / iOS / Android 构建脚本、基础窗口和音频设置入口已有 | 缺 `config.exe` 的分辨率 / 阴影 / 语言 / 鼠标配置持久化、Movie / Logo / Web 页面、补丁器约束及按发布目标取舍的 Discord RPC | P2 |
| `ProcessCRC` / `ProcessScanner` / `PythonProfiler` | 反作弊明确列为不做 | 若服务端未来强制校验,只补最小协议上报;性能采样、崩溃上报和诊断工具不属于当前 1:1 交付范围 | — |
## 协议覆盖审计
`m2dev-client-src-main/src/UserInterface/Packet.h` 为基准(`namespace CG` 68 个 +
`namespace GC` 109 个 = **177** 个协议键)。增量 2/3 已补 `CG_CHARACTER_CREATE` /
`CG_CHARACTER_DELETE` / `GC_PLAYER_CREATE_SUCCESS` / `GC_PLAYER_CREATE_FAILURE` /
`GC_PLAYER_DELETE_SUCCESS` / `GC_PLAYER_DELETE_WRONG_SOCIAL_ID` / `CG_MYSHOP` /
`CG_MALL_CHECKOUT` / `GC_MALL_OPEN` / `GC_MALL_SET` / `GC_MALL_DEL` 共 11 个,并把已声明的
`GC_EMPIRE` 接到 `M2Client.get_empire()`。增量 5 又把此前仅声明的 `GC_LOVER_INFO` /
`GC_LOVE_POINT_UPDATE` 接入状态、信号和 UI;增量 6 又补了
`CG_CHARACTER_POSITION` / `CG_SYNC_POSITION` 的上行构造和 API。这两项不再计入缺失清单,
故当前总数从 24 降为 22 个 header;增量 7 再补任务选择 / 取消上行,增量 8 补队伍技能上行,
增量 9 补飞行目标广播,增量 10 补技能组切换,增量 11 补角色改名,增量 12 补公会创建请求,
增量 13 补主动传送,增量 14 补挖矿动作,增量 15 补会徽符号下载,增量 16 补钓鱼协议,
增量 17 补副本协议,增量 18 补领地与观战者,当前为 **1 个 header**
排除仅命名不同、功能已覆盖的别名(`GC_CHAR_ADDITIONAL_INFO`=`GC_CHAR_ADD_INFO`
`GC_TARGET`=`GC_TARGET_INFO``GC_TARGET_CREATE_NEW`=`GC_TARGET_CREATE`
`GC_CHARACTER_UPDATE2`=`GC_CHARACTER_UPDATE``GC_OWNERSHIP`=`GC_ITEM_OWNERSHIP`
`GC_SEPCIAL_EFFECT`=`GC_SPECIAL_EFFECT``GC_REFINE_INFORMATION_NEW`=`GC_REFINE_INFO_NEW`),
以及 `CG_STATE_CHECKER` / `GC_RESPOND_CHANNELSTATUS``project/net/channel_status.gd` 单独实现)
`CG_HACK`(反作弊,见「不做清单」),
**当前主网络层仍缺 1 个 header**(脚本核对 `wire.h` vs `Packet.h`):
### 账号 / 命令通道
| header | 值 | 说明 |
|---|---|---|
| `CG_TEXT` | 0x0011 | 简易命令通道(Cube 现借 `CG_CHAT`/`CHAT_TYPE_COMMAND` 实现,非原路径) |
### 已声明但待核(不计入上面 1 个)
- `CG_CHARACTER_POSITION`(0x0A60) / `CG_SYNC_POSITION`(0x0303)wire、发送接口和
loopback 字节测试已覆盖;真服单会话已分别发送 5B / 16B,均保持在线且无 unknown;仍缺原版
自动触发语义、姿态动作链和服务端位置纠偏回包验收。
- `CG_SCRIPT_SELECT_ITEM`(0x0903) / `CG_QUEST_CANCEL`(0x0906)wire、发送接口和
loopback 字节测试已覆盖;真服单会话已分别发送 8B / 4B,取消触发 1 条 quest-change 且无
unknown;基础 `[INPUT]` / Escape 取消已接入,仍缺选道具网格、原版输入校验和有脚本时的任务
流程验收。
- `CG_PARTY_USE_SKILL`(0x0705)wire、发送接口和 loopback 字节测试已覆盖;真服单会话已发送
9B 且保持在线 / 无 unknown,但无队伍技能业务回包;仍缺冷却 / 权限、UI 入口和有效队伍真服
验收。
- `GC_FLY_TARGETING`(0x0411) / `GC_ADD_FLY_TARGETING`(0x0412)wire、EntityStore 状态、
`M2Client.fly_targeting` 信号和 `NetWorld` 目标队列消费已覆盖;本轮发现并修复
`GameClient` 未分派这两个 GC header 的 bug;真服本轮未主动推送,仍缺真实弹道模型、命中时机
和服务端广播验收。
- `GC_CHANGE_SKILL_GROUP`(0x021D)wire、EntityStore 状态、dirty 失效和
`M2Client.skill_group_changed` 已覆盖;本轮发现并修复 `GameClient` 未分派该 header 的 bug
仍缺技能组选择 UI、服务端权限和真服推送验收。
- `CG_CHANGE_NAME`(0x010B) / `GC_CHANGE_NAME`(0x010C)wire、角色槽位更新、
`M2Client.change_name()``char_name_changed` 已覆盖;仍缺改名 UI、改名卡消耗 / 重名错误
文案和真服验收。
- `GC_REQUEST_MAKE_GUILD`(0x0731):无负载 wire 解析和 `guild_make_requested` 信号已覆盖;仍缺
创建公会窗口、名称校验 / 应答和真服验收。
- `CG_SYMBOL_CRC`(0x0723) / `GC_SYMBOL_DATA`(0x0732)16B / 8B wire、`MarkClient` CRC 请求、
变长数据接收和 `M2Client.guild_symbol_ready` 已覆盖;仍缺本地符号缓存失效策略、UI 预览和
真服验收。
- `CG_FISHING`(0x0B01) / `GC_FISHING`(0x0B10)5B / 10B wire、旋转方向换算、
`EntityStore` 事件、`M2Client.fishing_event`、远端 / 本地主角动作分发和 ChatUI 结果日志已
覆盖;真服单会话已发送 5B,收到“Please choose a Fishing Pole.”业务拒绝且无断连 / unknown,
证明包布局和错误路径可达;仍缺持竿后的完整钓鱼 UI、鱼获背包落地和成功流程验收。
- `CG_DUNGEON`(0x0B02) / `GC_DUNGEON`(0x0B11)4B / 5B wire、目的地坐标变长 body、
`EntityStore` 事件和 `M2Client.dungeon_event` 已覆盖;仍缺副本计时 / 入口 UI、地图生命周期
清理和真服验收。
- `GC_LAND_LIST`(0x0B12)4B 固定头 + 24B `LandPacketElement` 变长解析、领地状态、
`M2Client.get_land_areas()` / `land_areas_changed` 和小地图领地矩形边框已覆盖;真服登录洪流
已收到并解析 436B;仍缺领地贴图 / 建筑和有实际领地变更时的推送验收。
- `GC_OBSERVER_ADD/REMOVE/MOVE`(0x0B200x0B22)12B / 8B wire、持久观战者状态、
`M2Client.get_observers()` / `observer_event` 和小地图紫色菱形标记已覆盖;仍缺真服验收。
- `CG_WARP`(0x0305)4B wire、`GameClient.send_warp()``M2Client.request_warp()` 和回环
请求 / `GC_WARP` 回传测试已覆盖;仍缺传送确认 UI、冷却 / 失败提示和真服验收。
- `GC_DIG_MOTION`(0x0308)13B wire、`EntityStore` 事件、`M2Client.dig_motion`
`NetWorld` 朝向 / 动画元数据处理已覆盖;仍缺真实挖矿动作资源、音效 / 特效和真服验收。
- `GC_ITEM_DROP`(0x0513):疑似被 `GC_ITEM_GROUND_ADD`(0x0515) 取代,暂按等价处理,未逐字节核。
- `GC_LOGIN_KEY`(0x0107)auth 握手用;`net_e2e` 真机 auth→PHASE_GAME 全程已打通,功能等价
(可能走 `GC_AUTH_SUCCESS` 0x0108),未单独立项。
“header 已声明”也不能直接视为完成:还需检查封包尺寸、变长 body、所有 sub-header、
`EntityStore` 状态、GDScript 信号、UI 消费和真服务器端到端行为。
## 建议迁移顺序
### M1:服务器语义正确
1. 把远程 `SHOOT` 时机、范围目标与技能类型冷却接入现有技能协议;
2. 快捷栏的命令 / 表情 / 物品拖放语义与特殊冷却规则;
3. 细化 24 wear / 腰带 / 装备查看的原版窗口表现,并完成完整物品操作;
4. 移动同步、PVP / 决斗和 Actor 战斗状态机;
5. 用 loopback + 真服抓包为每类协议建立双向验收。
### M2:主要玩法完整
1. 任务完整标签 / 输入 / 选道具 / 指引;
2. 商店 START_EX、仓库密码、商城、私人商店、Cube(拍卖行 m2dev-client 无,除非另起协议);
3. 组队角色、公会完整页面、情侣完整交互、领地与公会建筑;
4. 钓鱼、副本、坐骑、观战者和 Atlas。
### M3UI 与表现 1:1
1. 以原 `interfacemodule.py` 为窗口清单,逐窗口迁移行为;
2. 完成 tooltip、拖放、光标、IME、快捷键和系统设置;
3. 完成 EffectLib、SpeedTree、材质、地图表现和声音事件;
4. 固定原客户端参考机位、角色、地图、时间和 UI 状态,建立截图 / 录屏签核。
### M4:发布闭环
1. 所有资产调用方统一使用 `AssetSource`,验证纯 pack 运行;
2. 覆盖目标平台构建、前后台、输入和性能;
3. 按服务器要求决定 Discord / Web / Movie 等桌面能力是否迁移(**反作弊 `ProcessCRC` /
`ProcessScanner` 已标注 ❌ 不做**,除非服务端强制校验)。
## 1:1 完成判据
一个功能只有同时满足以下条件才标记为完成:
1. 原客户端涉及的 CG / GC 包和 sub-header 全部覆盖,并有尺寸断言;
2. 数据状态能正确恢复、重连、切图和清理;
3. UI 操作、错误提示、拖放、快捷键和窗口互斥与原客户端一致;
4. 模型、动画、特效、声音和名字 / tooltip 表现一致;
5. loopback、真服务器端到端、受控截图或交互录屏至少有一种自动门禁和一种人工签核。
在达到以上判据前,应继续使用“首版”“部分完成”描述,避免把“能解析布局”“有一个按钮”或
“能播放动作”写成 1:1 完成。