Files
mtgodot-poc/docs/CLIENT-REFACTOR-40250-PLAN.md
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

264 lines
19 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.
# 客户端重构与 40250 逻辑对齐实施方案
## 1. 现状诊断与核心痛点
### 1.1 现状与优势
- **渲染层(已基本稳定)**
- `extension/src/metin2_model.{h,cpp}`:基于开源 libgr2 的模型网格解析、材质贴图加载、GPU/CPU 骨骼蒙皮。
- `extension/src/metin2_anim.{h,cpp}`:骨骼动画采样、双动画平滑融合(Crossfade)、挂点武器同步。
- `extension/src/metin2_world.{h,cpp}`:地形高度场采样、贴图混合、水体、静态建筑碰撞。
- `project/game_camera.gd`:轨道摄像机、防穿模逻辑。
- **结论**:渲染层已具备优秀的跨平台能力(macOS/iOS/Android/Linux/Web),**无需推倒,纯粹作为“哑终端”画面渲染器(View)予以保留**。
### 1.2 逻辑层的混乱根源
当前各种 Bug(“WASD翻跟头”、“连击不流畅/手感怪异”、“NPC交互后商店空白”、“任务无法打开”)的共同根源在于:
1. **庞大的 GDScript 意大利面条代码**
- `net_play.gd` (~2200 行)、`game_scene.gd` (~1800 行)、`net_world.gd` (~1760 行) 试图用 GDScript 凭空重写 40250 的几万行 C++ 客户端逻辑。
2. **凭感觉实现的“伪逻辑”**
- 没有严格对照 40250 C++ 源码的状态机(如按键优先级、阻尼公式、连击输入窗、NPC点击距离分流、封包处理链),而是用自制计时器和启发式逻辑拼接,导致产生大量边界 Bug 和时序脱节。
3. **数据没有单一真实来源(Single Source of Truth**
- 实体数据同时散落在 C++ `EntityStore``net_play.gd``net_world.gd``client.ents``node.meta` 中,互相通过数十个信号来回同步,极易丢状态或时序错乱。
---
## 2. 1:1 复刻的边界与原则(复刻哪些,不复刻哪些)
> [!IMPORTANT]
> **核心原则**:**“形式上”不要 1:1 照搬类文件,但“内核上”必须 1:1 严格对齐封包、状态机与数学公式。**
### 2.1 坚决不要 1:1 复刻的部分(直接使用 Godot 原生)
40250 源码中超过 60% 的代码是在重复造 2004 年 DirectX 8 / Windows 的轮子,这些在现代跨平台环境中是沉重的技术包袱:
| 40250 原版模块 | 为什么不要 1:1 复刻 | Godot 现成替代方案 |
| :--- | :--- | :--- |
| **`EterLib` 图形底层**<br>(`CGraphicDevice`, `CStateManager`, `CRenderTarget`) | 深度强绑定 DirectX 8/9,引入会导致无法在 macOS (Metal) 及移动端 (Vulkan) 编译。 | **Godot 渲染引擎**(原生支持 Metal / Vulkan / WebGL |
| **`EterPythonLib` 2D UI 系统**<br>(`CWindowManager`, `CWindow`, `CButton`, `CScrollBar`) | 20 年前手工编写的固定坐标 2D 贴图拼接系统,不支持高分屏自适应、矢量字体、触屏手势。 | **Godot Control 体系**(完善的锚点自适应、触屏支持) |
| **`milesLib` 音频系统** | 2004 年的 Miles 闭源声卡驱动库。 | **Godot AudioServer / AudioStreamPlayer** |
| **`CCamera` 摄像机类** | 自行维护的 D3D 视锥矩阵,死板且不灵活。 | **Godot `Camera3D`**(现有的 `game_camera.gd` 表现良好) |
| **`EterGrnLib` 模型封装** | 依赖 Granny 2.8 闭源 Windows DLL。 | **现有 `metin2_model` + `metin2_anim`**(基于开源 libgr2,功能已完备) |
| **内嵌 Python 2.7 解释器** | Python 2.7 早已停止维护,在 iOS / Android 上打包极其臃肿且受 App Store 审核限制。 | **C++ 纯逻辑导出给 GDScript 调用** |
### 2.2 必须 1:1 严格复刻的部分(下沉至 C++ GDExtension 核心逻辑层)
下面这些是 Metin2 的“灵魂与运行规则”,必须 100% 对齐 40250 源码,漏掉一个判断分支就会导致手感完全走样:
1. **网络协议与封包结构(1:1 严丝合缝)**
- 对应源码:`Packet.h`
- 要求:每个 `TPacketGC*`(如 `TPacketGCShop``TPacketGCCharacterAdd``TPacketGCDamageInfo`)的结构体字段、字节对齐、Subheader 常量 100% 照搬。
2. **角色移动与物理状态机(1:1 算法复刻)**
- 对应源码:`PythonPlayerInput.cpp``InstanceBaseMovement.cpp``ActorInstanceMovement.cpp`
- 要求:按键优先级判定(`NEW_GetMultiKeyDirRotation`)、转向角 45° 阻尼与防过冲单向贴齐(`GetRotatingDirection``SetAdvancingRotation`)、碰撞切线滑移(`AdjustDynamicCollisionMovement`)、位移累加(`__AccumulationMovement`)。
3. **战斗与打击判定状态机(1:1 状态机复刻)**
- 对应源码:`ActorInstanceBattle.cpp``InstanceBaseBattle.cpp`
- 要求:连击段位状态转移(Combo 1→2→3→4)与按键输入时间窗(`ComboInputData`)、`.msa` 多命中时间窗(`HitDataContainer`)的毫秒级触发、普通受击方位判定与击倒击飞硬直。
4. **业务数据模型与操作上下文(1:1 逻辑复刻)**
- 对应源码:`PythonShop.cpp`(商店货架数据)、`PythonQuest.cpp`(任务状态树)、`PythonPlayerSkill.cpp`(技能冷却与释放条件三门)。
---
## 3. 架构重构结论与策略
### 3.1 架构重构的必要性
现有项目积累了大量技术债务,表面上看是“连击不连贯”、“WASD翻跟头”、“NPC打不开商店”等散发 Bug,深层原因是架构失衡:
1. **“上帝文件(God Class)”过载**:`net_play.gd` (2200+行) 与 `game_scene.gd` (1800+行) 把网络封包解码、角色状态、背包/商店业务、3D渲染节点管理、点击拾取、UI开关全部混在一起,牵一发而动全身。
2. **缺乏单一真实来源(No Single Source of Truth**:角色坐标、朝向、连击段位在 `player_controller``net_play``net_world` 和 C++ `EntityStore` 中各自维护,靠数十个信号来回同步,极易时序混乱或丢状态。
3. **启发式“伪逻辑”**:没有遵循 40250 的严密数学公式与状态机,而是用散落的计时器和直觉代码修补。
### 3.2 重构策略:拒绝推倒重来,采用“绞杀者式渐进解耦(Strangler Fig Pattern)”
- **保留成果(绝不推倒)**:已稳定的 C++ GR2 渲染层(`metin2_model``metin2_anim``metin2_world`)和摄像机防穿模逻辑完全保留。
- **业务逐块下沉**:按“战斗 -> 商店/NPC -> 任务 -> 背包”的顺序,把上帝文件中的业务逐个抽成独立的 Manager/状态机,先解耦跑通,再下沉对齐 40250。
---
## 4. 推荐的目标系统架构(`extension/src/m2_core/`
在 C++ GDExtension 中建立清晰、模块化、无 DirectX 依赖的逻辑层:
```
extension/src/m2_core/
├── protocol/ # 1:1 严格对齐 40250 的网络层
│ ├── packets.h # 1:1 复制 40250 Packet.h 的所有包结构体
│ └── packet_dispatcher.h # 1:1 复制 PythonNetworkStreamPhaseGame 的封包解析分发
├── player/ # 1:1 复制 CPythonPlayer 的核心逻辑
│ ├── player_input.cpp # 1:1 移植 PythonPlayerInput.cpp (按键优先级、朝向合成、预约动作)
│ ├── player_state.cpp # 1:1 移植 玩家属性、金币、背包、技能CD数据
│ └── player_combat.cpp # 1:1 移植 连击判定、攻击输入窗
├── world/ # 1:1 复制 CPythonCharacterManager & CInstanceBase
│ ├── instance_entity.cpp # 1:1 移植 实体状态机、45°阻尼移动、切线碰撞滑移
│ └── combat_resolver.cpp # 1:1 移植 命中范围、受击硬直计算
└── gameplay/ # 1:1 复制业务逻辑数据源
├── shop_system.cpp # 1:1 移植 PythonShop.cpp (解决商店商品空白)
└── quest_system.cpp # 1:1 移植 PythonQuest.cpp (解决NPC任务交互)
```
### 运行时数据与渲染流向:
```mermaid
flowchart TD
subgraph View ["Godot 4 表现与输入层 (View & Input)"]
InputHandler["Input / Touch / UI 事件捕获"]
GodotNodes["PlayerView / MobView (Metin2Model + Metin2AnimPlayer)"]
UI_Screens["ShopUI / QuestUI / InventoryUI / CharacterUI"]
end
subgraph LogicCore ["GDExtension 40250 纯逻辑核心 (Core Logic)"]
CPP_Input["PlayerInput (按键优先级/朝向合成/输入队列)"]
CPP_Move["InstanceEntity (45°阻尼/切线滑动/累积位移)"]
CPP_Combat["Combat & Combo (命中窗/连击状态机)"]
CPP_Net["NetworkStream & Dispatcher (收发严丝合缝对齐 40250)"]
CPP_Data["Shop & Quest Data (货架缓存/任务对话树)"]
end
subgraph Server ["40250 游戏服务端"]
GameServer["TCP Server"]
end
InputHandler -->|按键/点击事件| CPP_Input
CPP_Input --> CPP_Move
CPP_Move -->|状态包| CPP_Net
CPP_Combat <--> CPP_Net
CPP_Net <-->|TCP 封包| GameServer
CPP_Move -.->|输出权威坐标 (x,y,z) 与朝向 yaw| GodotNodes
CPP_Combat -.->|输出当前动作名、时间与打击帧| GodotNodes
CPP_Data -.->|响应数据变动通知| UI_Screens
```
---
## 5. 移动端(iOS / Android)操作界面与跨端支持设计
> [!NOTE]
> **结论**:**当前规划的解耦架构天然、完美支持后续无缝接入移动端专属操作界面与触控系统。**
### 5.1 核心解耦机制:输入意图抽象层(Input Intent Abstraction
重构后的架构将**“硬件输入采集”**与**“游戏角色行为”**彻底解耦。底层逻辑不再监听 `KEY_W``KEY_SPACE` 或鼠标点击,而是消费高阶**输入意图(Input Intents**
```mermaid
flowchart TD
subgraph InputSources ["输入源采集(多端隔离)"]
PC_Input["PC 桌面端输入<br>(WASD / 鼠标左右键 / 快捷键 1~4 / 空格)"]
Mobile_Input["移动端触控输入<br>(虚拟摇杆 / 技能轮盘 / 普攻大按钮 / 目标切换)"]
end
subgraph IntentLayer ["输入意图抽象接口 (PlayerInputIntent)"]
I_Move["move_intent(dir: Vector2, is_run: bool)"]
I_Attack["attack_intent()"]
I_Skill["skill_intent(slot_idx: int)"]
I_Target["target_intent(vid: int)"]
I_Interact["interact_intent(vid: int)"]
end
subgraph CoreLogic ["40250 核心状态机 (跨平台纯逻辑)"]
M2_Move["45° 转向阻尼 / 切线滑移碰撞"]
M2_Combat["连击输入时间窗 / 多段命中判定"]
M2_Target["500cm 距离门限 / 预约走向交互"]
end
PC_Input -->|转换映射| IntentLayer
Mobile_Input -->|转换映射| IntentLayer
IntentLayer --> CoreLogic
```
1. **移动控制无缝适配**
- PC 端:WASD 键位经过 40250 `NEW_GetMultiKeyDirRotation` 算成方向向量。
- 移动端:屏幕左侧的**虚拟摇杆(Virtual Joystick**直接输出方向向量给 `move_intent`,底层 45° 转向阻尼与碰撞滑移算法无需修改一行代码。
2. **战斗与技能轮盘**
- 移动端右侧可布局经典“大按键普攻 + 环形技能轮盘”,普攻按钮每次点击触发 `attack_intent()`,底层直接进入 40250 的 `ComboInputData` 输入缓冲队列。
### 5.2 双端自适应 UI 表现层(Dual-Mode Adaptive UI
由于业务数据(`ShopManager``InventoryManager``QuestManager`)已与界面彻底解耦:
- **数据核心统一**:所有数据模型只负责维护商品列表、道具属性、背包格子、对话选项,通过信号/事件对外广播。
- **UI 视图按端加载**
- PC 端:加载 `DesktopHUD.tscn`(支持鼠标 Hover 查看装备 Tooltip、右键穿戴、拖拽格子)。
- 移动端:根据 `OS.has_feature("mobile")` 或屏幕分辨率动态加载 `MobileHUD.tscn`(更大的触控热区、点击弹出操作抽屉 Drawer、全屏大卡片商店展示)。
- **完全零冗余**:两套 UI 共享 100% 的底层数据逻辑、网络协议与状态机。
---
## 6. 分阶段实施路线图
按照“**先主要解决战斗系统不一致,再解决 NPC/商店/任务交互混乱**”的优先级分步推进:
### 阶段一:战斗与连击系统精准对齐(优先级:最高)
> 对应 40250 源码:`ActorInstanceBattle.cpp`、`InstanceBaseBattle.cpp`、`formats/msa.h`
1. **普通攻击与连击链(Combo Chain1:1 复刻**
- 移植 40250 `ActorInstanceBattle.cpp:157-308` 状态机:
- `m_dwcurComboIndex`(连击段数 1→2→3→4
- `ComboInputData`:严格按照 `.msa``InputStartTime``InputEndTime``NextComboTime` 判定是否允许按下一次普攻。
### 阶段一:移动与战斗核心状态机 40250 对齐(优先级:最高)✅ [COMPLETED]
> 对应 40250 源码:`PythonPlayerInput.cpp`、`ActorInstanceBattle.cpp`、`InstanceBaseBattle.cpp`
1. **废弃原有人为的外层普攻 CD 拦截**
- 彻底废除 `_attack_cd` 吞键逻辑,完全对齐 40250 普攻/连击内禀门控机制。
2. **严格 1:1 接入 `.msa` 的连击输入数据窗**
- 读取并严格应用 `duration``InputStartTime``NextComboTime``InputEndTime`
- 完美支持长按空格连续 4 击(1→2→3→4 段无缝切换与回 Wait 重置)与快速连点。
3. **受击与击倒对齐**
- 动作播放时长内动态释放受击锁,触发屏幕震颤与击退同步。
### 阶段二:NPC 交互与商店系统修复(优先级:高)✅ [COMPLETED]
> 对应 40250 源码:`PythonPlayerInputMouse.cpp`、`PythonNetworkStreamPhaseGame.cpp`、`PythonShop.cpp`、`uishop.py`
1. **NPC 点击与预约走向交互(__OnClickActor**
- 点击 NPC:距离 `<= 500 cm``CLICK_DIST_NPC`)立即停步、面朝 NPC 发送 `HEADER_CG_ON_CLICK` (26)。
- 距离 `> 500 cm`:进入 `m_eReservedMode = CLICK_ACTOR` 预约模式,驱动角色寻路靠近,进入 500cm 瞬间停步交互。
- 加入 40250 `CPythonPlayer::__SendClickActorPacket` 的 1000ms 狂点节流保护。
2. **修复商店数据流与属性丢失(彻底解决白板商品 Bug)**
- C++ `mtnet::ShopEntry` 补齐 `sockets[3]``attrs[7]`,解析 `START` / `START_EX` / `UPDATE_ITEM` 全量属性。
- `m2_client` 修复切页被冲掉问题,仅在初次打开触发 `shop_opened`,格位更新触发 `shop_updated(pos)`
- UI 接入 40-slot 货架、多 Tab 切换(`tabIdx * 40 + slot`)、右键直接秒买(`UnselectItemSlot`)、超出 10m 离开自动关闭窗口。
### 阶段三:任务系统与对话流对齐(优先级:中)✅ [COMPLETED]
> 对应 40250 源码:`PythonEventManager.h/cpp`、`PythonQuest.cpp`、`PythonNetworkStreamPhaseGame.cpp``RecvScriptPacket`)、`uiquest.py`
1. **40250 任务与 NPC 交互协议全套对齐**
- 封包层精准对齐:`HEADER_GC_SCRIPT` (45)、`HEADER_CG_SCRIPT_ANSWER` (29)、`HEADER_CG_SCRIPT_BUTTON` (66)、`HEADER_CG_QUEST_INPUT_STRING` (30)、`HEADER_GC_QUEST_CONFIRM` (46)、`HEADER_CG_QUEST_CONFIRM` (31)。
- 在 C++ 网络会话层 (`ClassicSession`) 与 `M2Client` 实现全套安全发包接口:`send_script_answer``send_script_button``send_quest_input``send_quest_confirm``send_quest_cancel`
2. **40250 `CPythonEventManager` 脚本语法与表现层 1:1 解析**
- 支持标准 40250 分号/管道符问题格式 `[QUESTION 1;选项一|2;选项二|...]`,0-indexed 答案映射,保留向后兼容旧格式。
- 实现 40250 `uiquest.py` 8 选项多页分页机制(`MAX_CHOICES_PER_PAGE = 8`),自动挂载 `[上一页]` / `[下一页]` 导航行,精确计算全局索引 `page * 8 + slot`
- 皮肤与立绘对齐:`SKIN_NOWINDOW` (1) 与 `SKIN_CINEMA` (5) 隐藏背景板;`LEFTIMAGE` 自动偏移文本与按钮内容区宽度。
- 支持 `[INPUT]` 文本输入框即时提交、`[NEXT]` 剧情推进与 `[DONE]` 流程收尾(`script_answer(254)`),以及 ESC 键安全取消(`OnCancel`)。
- 编写 `test_quest_dialog_parity.gd` 覆盖 9 大测试场景 45 项断言全部 100% 通过。
### 阶段四:角色属性面板、背包与技能系统对齐(优先级:中)✅ [COMPLETED]
> 对应 40250 源码:`PythonPlayerSkill.cpp`、`PythonSkill.h`、`PythonItem.cpp`、`GameType.h`、`Packet.h`、`uicharacter.py`
1. **主动技能释放链与三层完整门控(40250 CheckSkillUsable 对齐)**
- 严格 1:1 实现死亡状态(`CANNOT_ACT`)、安全区攻击拦截(`IN_SAFE`,增益 Buff 技能放行)、武器类型匹配(`NOT_MATCHABLE_WEAPON`)、弓箭数量检验(`EMPTY_ARROW`)、SP/HP 消耗门检(`NOT_ENOUGH_SP`)、冷却时间阻断(`WAIT_COOLTIME`)以及施法距离/目标判定(超出距离进入 `RESERVED` 预约模式,进入范围后放行)。
2. **装备与背包双端线协议对齐(40250 ItemPos 对齐)**
- `_to_wire` 严格映射 40250 `TItemPos`:背包槽位 `0..89` 映射 `WINDOW_INVENTORY (1)`;装备槽位 `90..100` 映射 `WINDOW_EQUIPMENT (2)` 且内部 cell 严格从 `0` 开始偏移;时装槽位 `109..113` 映射 `WINDOW_INVENTORY (1)`
- 右键穿脱发送 `HEADER_CG_ITEM_USE` (11),拖拽移动发送 `HEADER_CG_ITEM_MOVE` (12),并严格走 `EquipRules` 职业、等级(`LIMIT_LEVEL`)与性别门控。
3. **角色属性面板实时换算与指令对齐(uicharacter.py:RefreshStatus 对齐)**
- 物攻格式严格呈现 `min-max`(或相等时单值显示)、物防 `DEF_GRADE + DEF_BONUS`、魔攻 `MAG_ATT + MAGIC_WEP` 纯正公式。
- 加点与洗点协议通过安全网络通道发送原生 `/stat ht``/stat iq``/stat st``/stat dx``/stat-` 指令。
4. **快捷栏(Quickbar)持久化与全量同步(40250 TQuickSlot 协议)**
- 快捷栏配置新增发送 `HEADER_CG_QUICKSLOT_ADD` (16)(支持技能、物品、动作表情)、交换发送 `HEADER_CG_QUICKSLOT_SWAP` (18)、清空发送 `HEADER_CG_QUICKSLOT_DEL` (17),并支持全量 36 格状态本地与服务端双向恢复。
- 编写 `test_skill_inventory_parity.gd` 覆盖 4 大领域 42 项测试断言全部 100% 通过。
---
## 7. 技术实施规范
1. **严禁“自造算法”**
- 任何涉及移动、旋转、连击、受击、技能、网络包逻辑的代码,必须明确注明对应的 40250 源码文件和行号(例如 `// 对齐 40250 ActorInstanceBattle.cpp:215`),1:1 复刻其状态转移条件。
2. **职责边界分明**
- **C++ (GDExtension)**:拥有所有数据、状态机、网络封包、物理数学计算。
- **GDScript / Godot Node**:只做两件事:① 把 C++ 的位置和动作画在屏幕上;② 把屏幕上的按键和点击事件传给 C++。
3. **严格单向数据流**
- 废除 GDScript 层各节点之间混乱的信号交叉监听,所有状态统一归拢到核心状态机。
---
## 8. 验证计划
1. **单元测试与 Headless 自动化测试**
- 为连击状态机编写类似 `test_wasd_steering_parity.gd` 的无头自动化回归测试。
- 模拟封包注入测试商店数据解析与 NPC 交互流程。
2. **实服联机验证**
- 配合真实 40250 服务端,联机点击杂货商/铁匠,验证商品列表 100% 正常展示并成功购买。
- 联机打怪,验证普通攻击连击节奏、打击僵直与伤害数字飘字时机与原版客户端完全吻合。