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

19 KiB
Raw Blame History

客户端重构与 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++ EntityStorenet_play.gdnet_world.gdclient.entsnode.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 图形底层
(CGraphicDevice, CStateManager, CRenderTarget)
深度强绑定 DirectX 8/9,引入会导致无法在 macOS (Metal) 及移动端 (Vulkan) 编译。 Godot 渲染引擎(原生支持 Metal / Vulkan / WebGL
EterPythonLib 2D UI 系统
(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*(如 TPacketGCShopTPacketGCCharacterAddTPacketGCDamageInfo)的结构体字段、字节对齐、Subheader 常量 100% 照搬。
  2. 角色移动与物理状态机(1:1 算法复刻)
    • 对应源码:PythonPlayerInput.cppInstanceBaseMovement.cppActorInstanceMovement.cpp
    • 要求:按键优先级判定(NEW_GetMultiKeyDirRotation)、转向角 45° 阻尼与防过冲单向贴齐(GetRotatingDirectionSetAdvancingRotation)、碰撞切线滑移(AdjustDynamicCollisionMovement)、位移累加(__AccumulationMovement)。
  3. 战斗与打击判定状态机(1:1 状态机复刻)
    • 对应源码:ActorInstanceBattle.cppInstanceBaseBattle.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_controllernet_playnet_world 和 C++ EntityStore 中各自维护,靠数十个信号来回同步,极易时序混乱或丢状态。
  3. 启发式“伪逻辑”:没有遵循 40250 的严密数学公式与状态机,而是用散落的计时器和直觉代码修补。

3.2 重构策略:拒绝推倒重来,采用“绞杀者式渐进解耦(Strangler Fig Pattern)”

  • 保留成果(绝不推倒):已稳定的 C++ GR2 渲染层(metin2_modelmetin2_animmetin2_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任务交互)

运行时数据与渲染流向:

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_WKEY_SPACE 或鼠标点击,而是消费高阶输入意图(Input Intents**

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

由于业务数据(ShopManagerInventoryManagerQuestManager)已与界面彻底解耦:

  • 数据核心统一:所有数据模型只负责维护商品列表、道具属性、背包格子、对话选项,通过信号/事件对外广播。
  • UI 视图按端加载
    • PC 端:加载 DesktopHUD.tscn(支持鼠标 Hover 查看装备 Tooltip、右键穿戴、拖拽格子)。
    • 移动端:根据 OS.has_feature("mobile") 或屏幕分辨率动态加载 MobileHUD.tscn(更大的触控热区、点击弹出操作抽屉 Drawer、全屏大卡片商店展示)。
  • 完全零冗余:两套 UI 共享 100% 的底层数据逻辑、网络协议与状态机。

6. 分阶段实施路线图

按照“先主要解决战斗系统不一致,再解决 NPC/商店/任务交互混乱”的优先级分步推进:

阶段一:战斗与连击系统精准对齐(优先级:最高)

对应 40250 源码:ActorInstanceBattle.cppInstanceBaseBattle.cppformats/msa.h

  1. 普通攻击与连击链(Combo Chain1:1 复刻
    • 移植 40250 ActorInstanceBattle.cpp:157-308 状态机:
      • m_dwcurComboIndex(连击段数 1→2→3→4
      • ComboInputData:严格按照 .msaInputStartTimeInputEndTimeNextComboTime 判定是否允许按下一次普攻。

阶段一:移动与战斗核心状态机 40250 对齐(优先级:最高) [COMPLETED]

对应 40250 源码:PythonPlayerInput.cppActorInstanceBattle.cppInstanceBaseBattle.cpp

  1. 废弃原有人为的外层普攻 CD 拦截
    • 彻底废除 _attack_cd 吞键逻辑,完全对齐 40250 普攻/连击内禀门控机制。
  2. 严格 1:1 接入 .msa 的连击输入数据窗
    • 读取并严格应用 durationInputStartTimeNextComboTimeInputEndTime
    • 完美支持长按空格连续 4 击(1→2→3→4 段无缝切换与回 Wait 重置)与快速连点。
  3. 受击与击倒对齐
    • 动作播放时长内动态释放受击锁,触发屏幕震颤与击退同步。

阶段二:NPC 交互与商店系统修复(优先级:高) [COMPLETED]

对应 40250 源码:PythonPlayerInputMouse.cppPythonNetworkStreamPhaseGame.cppPythonShop.cppuishop.py

  1. NPC 点击与预约走向交互(__OnClickActor
    • 点击 NPC:距离 <= 500 cmCLICK_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/cppPythonQuest.cppPythonNetworkStreamPhaseGame.cppRecvScriptPacket)、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_answersend_script_buttonsend_quest_inputsend_quest_confirmsend_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.cppPythonSkill.hPythonItem.cppGameType.hPacket.huicharacter.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% 正常展示并成功购买。
    • 联机打怪,验证普通攻击连击节奏、打击僵直与伤害数字飘字时机与原版客户端完全吻合。