- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
19 KiB
客户端重构与 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交互后商店空白”、“任务无法打开”)的共同根源在于:
- 庞大的 GDScript 意大利面条代码:
net_play.gd(~2200 行)、game_scene.gd(~1800 行)、net_world.gd(~1760 行) 试图用 GDScript 凭空重写 40250 的几万行 C++ 客户端逻辑。
- 凭感觉实现的“伪逻辑”:
- 没有严格对照 40250 C++ 源码的状态机(如按键优先级、阻尼公式、连击输入窗、NPC点击距离分流、封包处理链),而是用自制计时器和启发式逻辑拼接,导致产生大量边界 Bug 和时序脱节。
- 数据没有单一真实来源(Single Source of Truth):
- 实体数据同时散落在 C++
EntityStore、net_play.gd、net_world.gd、client.ents和node.meta中,互相通过数十个信号来回同步,极易丢状态或时序错乱。
- 实体数据同时散落在 C++
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 严丝合缝):
- 对应源码:
Packet.h - 要求:每个
TPacketGC*(如TPacketGCShop、TPacketGCCharacterAdd、TPacketGCDamageInfo)的结构体字段、字节对齐、Subheader 常量 100% 照搬。
- 对应源码:
- 角色移动与物理状态机(1:1 算法复刻):
- 对应源码:
PythonPlayerInput.cpp、InstanceBaseMovement.cpp、ActorInstanceMovement.cpp - 要求:按键优先级判定(
NEW_GetMultiKeyDirRotation)、转向角 45° 阻尼与防过冲单向贴齐(GetRotatingDirection、SetAdvancingRotation)、碰撞切线滑移(AdjustDynamicCollisionMovement)、位移累加(__AccumulationMovement)。
- 对应源码:
- 战斗与打击判定状态机(1:1 状态机复刻):
- 对应源码:
ActorInstanceBattle.cpp、InstanceBaseBattle.cpp - 要求:连击段位状态转移(Combo 1→2→3→4)与按键输入时间窗(
ComboInputData)、.msa多命中时间窗(HitDataContainer)的毫秒级触发、普通受击方位判定与击倒击飞硬直。
- 对应源码:
- 业务数据模型与操作上下文(1:1 逻辑复刻):
- 对应源码:
PythonShop.cpp(商店货架数据)、PythonQuest.cpp(任务状态树)、PythonPlayerSkill.cpp(技能冷却与释放条件三门)。
- 对应源码:
3. 架构重构结论与策略
3.1 架构重构的必要性
现有项目积累了大量技术债务,表面上看是“连击不连贯”、“WASD翻跟头”、“NPC打不开商店”等散发 Bug,深层原因是架构失衡:
- “上帝文件(God Class)”过载:
net_play.gd(2200+行) 与game_scene.gd(1800+行) 把网络封包解码、角色状态、背包/商店业务、3D渲染节点管理、点击拾取、UI开关全部混在一起,牵一发而动全身。 - 缺乏单一真实来源(No Single Source of Truth):角色坐标、朝向、连击段位在
player_controller、net_play、net_world和 C++EntityStore中各自维护,靠数十个信号来回同步,极易时序混乱或丢状态。 - 启发式“伪逻辑”:没有遵循 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任务交互)
运行时数据与渲染流向:
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)**:
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
- 移动控制无缝适配:
- PC 端:WASD 键位经过 40250
NEW_GetMultiKeyDirRotation算成方向向量。 - 移动端:屏幕左侧的**虚拟摇杆(Virtual Joystick)**直接输出方向向量给
move_intent,底层 45° 转向阻尼与碰撞滑移算法无需修改一行代码。
- PC 端:WASD 键位经过 40250
- 战斗与技能轮盘:
- 移动端右侧可布局经典“大按键普攻 + 环形技能轮盘”,普攻按钮每次点击触发
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、全屏大卡片商店展示)。
- PC 端:加载
- 完全零冗余:两套 UI 共享 100% 的底层数据逻辑、网络协议与状态机。
6. 分阶段实施路线图
按照“先主要解决战斗系统不一致,再解决 NPC/商店/任务交互混乱”的优先级分步推进:
阶段一:战斗与连击系统精准对齐(优先级:最高)
对应 40250 源码:
ActorInstanceBattle.cpp、InstanceBaseBattle.cpp、formats/msa.h
- 普通攻击与连击链(Combo Chain)1:1 复刻:
- 移植 40250
ActorInstanceBattle.cpp:157-308状态机:m_dwcurComboIndex(连击段数 1→2→3→4)ComboInputData:严格按照.msa的InputStartTime、InputEndTime、NextComboTime判定是否允许按下一次普攻。
- 移植 40250
阶段一:移动与战斗核心状态机 40250 对齐(优先级:最高)✅ [COMPLETED]
对应 40250 源码:
PythonPlayerInput.cpp、ActorInstanceBattle.cpp、InstanceBaseBattle.cpp
- 废弃原有人为的外层普攻 CD 拦截:
- 彻底废除
_attack_cd吞键逻辑,完全对齐 40250 普攻/连击内禀门控机制。
- 彻底废除
- 严格 1:1 接入
.msa的连击输入数据窗:- 读取并严格应用
duration、InputStartTime、NextComboTime、InputEndTime。 - 完美支持长按空格连续 4 击(1→2→3→4 段无缝切换与回 Wait 重置)与快速连点。
- 读取并严格应用
- 受击与击倒对齐:
- 动作播放时长内动态释放受击锁,触发屏幕震颤与击退同步。
阶段二:NPC 交互与商店系统修复(优先级:高)✅ [COMPLETED]
对应 40250 源码:
PythonPlayerInputMouse.cpp、PythonNetworkStreamPhaseGame.cpp、PythonShop.cpp、uishop.py
- 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 狂点节流保护。
- 点击 NPC:距离
- 修复商店数据流与属性丢失(彻底解决白板商品 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 离开自动关闭窗口。
- C++
阶段三:任务系统与对话流对齐(优先级:中)✅ [COMPLETED]
对应 40250 源码:
PythonEventManager.h/cpp、PythonQuest.cpp、PythonNetworkStreamPhaseGame.cpp(RecvScriptPacket)、uiquest.py
- 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。
- 封包层精准对齐:
- 40250
CPythonEventManager脚本语法与表现层 1:1 解析:- 支持标准 40250 分号/管道符问题格式
[QUESTION 1;选项一|2;选项二|...],0-indexed 答案映射,保留向后兼容旧格式。 - 实现 40250
uiquest.py8 选项多页分页机制(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% 通过。
- 支持标准 40250 分号/管道符问题格式
阶段四:角色属性面板、背包与技能系统对齐(优先级:中)✅ [COMPLETED]
对应 40250 源码:
PythonPlayerSkill.cpp、PythonSkill.h、PythonItem.cpp、GameType.h、Packet.h、uicharacter.py
- 主动技能释放链与三层完整门控(40250 CheckSkillUsable 对齐):
- 严格 1:1 实现死亡状态(
CANNOT_ACT)、安全区攻击拦截(IN_SAFE,增益 Buff 技能放行)、武器类型匹配(NOT_MATCHABLE_WEAPON)、弓箭数量检验(EMPTY_ARROW)、SP/HP 消耗门检(NOT_ENOUGH_SP)、冷却时间阻断(WAIT_COOLTIME)以及施法距离/目标判定(超出距离进入RESERVED预约模式,进入范围后放行)。
- 严格 1:1 实现死亡状态(
- 装备与背包双端线协议对齐(40250 ItemPos 对齐):
_to_wire严格映射 40250TItemPos:背包槽位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)与性别门控。
- 角色属性面板实时换算与指令对齐(uicharacter.py:RefreshStatus 对齐):
- 物攻格式严格呈现
min-max(或相等时单值显示)、物防DEF_GRADE + DEF_BONUS、魔攻MAG_ATT + MAGIC_WEP纯正公式。 - 加点与洗点协议通过安全网络通道发送原生
/stat ht、/stat iq、/stat st、/stat dx与/stat-指令。
- 物攻格式严格呈现
- 快捷栏(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. 技术实施规范
- 严禁“自造算法”:
- 任何涉及移动、旋转、连击、受击、技能、网络包逻辑的代码,必须明确注明对应的 40250 源码文件和行号(例如
// 对齐 40250 ActorInstanceBattle.cpp:215),1:1 复刻其状态转移条件。
- 任何涉及移动、旋转、连击、受击、技能、网络包逻辑的代码,必须明确注明对应的 40250 源码文件和行号(例如
- 职责边界分明:
- C++ (GDExtension):拥有所有数据、状态机、网络封包、物理数学计算。
- GDScript / Godot Node:只做两件事:① 把 C++ 的位置和动作画在屏幕上;② 把屏幕上的按键和点击事件传给 C++。
- 严格单向数据流:
- 废除 GDScript 层各节点之间混乱的信号交叉监听,所有状态统一归拢到核心状态机。
8. 验证计划
- 单元测试与 Headless 自动化测试:
- 为连击状态机编写类似
test_wasd_steering_parity.gd的无头自动化回归测试。 - 模拟封包注入测试商店数据解析与 NPC 交互流程。
- 为连击状态机编写类似
- 实服联机验证:
- 配合真实 40250 服务端,联机点击杂货商/铁匠,验证商品列表 100% 正常展示并成功购买。
- 联机打怪,验证普通攻击连击节奏、打击僵直与伤害数字飘字时机与原版客户端完全吻合。