- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
90 lines
7.9 KiB
Markdown
90 lines
7.9 KiB
Markdown
# 40250 客户端对拍缺陷发现与修复工程指南 (CLIENT-PARITY-AUDIT-AND-FIX-GUIDE)
|
||
|
||
> **目标**:建立系统化的排查、探针探测与修复流程,解决 Godot 客户端从 40250 移植过程中由于“管线实现不完整”、“只实现裸数据漏掉复合层”导致的隐性细节缺陷(典型案例如:桥梁可行走但高度采样遗漏 `.mdatr` 导致掉落穿透)。
|
||
|
||
---
|
||
|
||
## 一、问题本质剖析:为何会出现此类缺陷?
|
||
|
||
在 Metin2 40250 原版中,几乎所有物理、移动、战斗与视觉表现都是**“多层复合叠加”**的。在向 Godot 移植的早期原型阶段,很多系统往往只实现了最底层的第 1 层,缺少了第 2 层和第 3 层的复合逻辑:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
subgraph 原版 40250 表现
|
||
L1["基础数据层 (如: PRTerrain height.raw 裸高度图)"]
|
||
L2["静态实体层 (如: CGraphicThingInstance / .mdatr 高度多边形)"]
|
||
L3["动态状态层 (如: 骨骼动作高度、骑乘偏置、水体/浮力判定)"]
|
||
L1 --> L2 --> L3 --> R1["最终真实表现 (如: 角色稳稳立于桥面)"]
|
||
end
|
||
|
||
subgraph 缺陷实现模式
|
||
D1["基础数据层 (fmt::terrain_height_at)"]
|
||
D1 --> R2["直接作为最终结果 (角色跌落到桥底裸地形)"]
|
||
end
|
||
```
|
||
|
||
如果仅依赖玩家跑图偶然触发,发现成本极高且容易重复反弹。必须转为**白盒走查、探针自动化与协议日志审计**。
|
||
|
||
---
|
||
|
||
## 二、高危缺陷领域清单与 40250 真实机制对照
|
||
|
||
| 领域 | 常见隐性缺陷表现 | 40250 原版真实机制(唯一真相源) | 关键代码位置 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| **1. 空间与地貌 (Terrain & Objects)** | • 桥梁、楼梯、门廊台阶悬空或掉落穿透<br>• 水陆交界处判断错误(干地游泳或踩深水不减速/不可钓鱼)<br>• 地下城/山洞进出门廊穿透掉落 | • `CMapOutdoor::GetHeight` 查询 `.mdatr` 中的 `heights` 网格多边形,与裸高度图取 `fMAX`<br>• `attr.atr` 中的 `ATTRIBUTE_WATER` 与水面多边形绝对高度差<br>• `CDungeonBlock::GetObjectHeight` | `GameLib/MapOutdoor.cpp`<br>`EterLib/AttributeInstance.cpp`<br>`GameLib/AreaTerrain.cpp` |
|
||
| **2. 物理与移动 (Physics & Locomotion)** | • 遇到陡坡时直接穿山或以相同速度瞬移上山<br>• 被怪物或技能击退(Knockback)时穿透墙体飞出地图<br>• 自动寻路走直线卡死在栅栏/岩石缝隙 | • `ActorInstanceSync.cpp` 坡度倾角与重力滑落向量计算<br>• `ActorInstanceCollisionDetection.cpp` 击退时沿障碍物法线切向滑动<br>• `CMapOutdoor::is_blocked` 的栅格阻挡光栅化与逐轴滑动降级 | `GameLib/ActorInstanceSync.cpp`<br>`GameLib/ActorInstanceCollisionDetection.cpp` |
|
||
| **3. 动作与挂点 (Animation & Attachment)** | • 武器/盾牌朝向偏转或拿反、脱离手部<br>• 骑马时双手未握缰绳或双腿穿透马鞍<br>• 更换头盔/发型时头部穿模或发色异常 | • `CItemData::GetItemScale` 与骨骼挂点矩阵 `m_matBone` 变换<br>• `InstanceBaseMotion.cpp` 的骑乘姿势 override (`COMBO_TYPE_HORSE`)<br>• `CRaceData::SetHair` 的 MSM 顶点隐藏机制与 Alpha Blending 管道 | `GameLib/ActorInstanceAttach.cpp`<br>`UserInterface/InstanceBaseMotion.cpp`<br>`GameLib/RaceData.cpp` |
|
||
| **4. 战斗与射弹 (Combat & Flying)** | • 弓箭/法球穿透墙体或直接打入地底<br>• 普攻挥刀动作还没劈下来伤害数值已提前跳出<br>• 怪物死后不立刻倒地或死亡姿势嵌入地面 | • `CFlyingInstance::Update` 中的 `BackgroundHit` 碰撞(与地面/建筑交点)<br>• `.msa` 中的 `MotionEvent`(命中帧 `HIT` 严格帧事件驱动)<br>• `ActorInstanceBlend.cpp` 死亡动作的地面投影与骨骼终帧冻结 | `GameLib/FlyingInstance.cpp`<br>`GameLib/ActorInstanceBattle.cpp`<br>`GameLib/ActorInstanceBlend.cpp` |
|
||
| **5. 交互与拾取 (Picking & Decal)** | • 鼠标点击桥面、屋顶或 NPC 拾取点偏移到桥下<br>• 地面范围技能光圈(Decal)漏到桥底或悬空 | • `CMapOutdoor::GetPickingPointWithRay` 射线先求交物体再求交地形<br>• `CTerrainDecal` 的多边形表面投影裁切 | `GameLib/MapOutdoor.cpp`<br>`GameLib/TerrainDecal.cpp` |
|
||
|
||
---
|
||
|
||
## 三、四大系统化排查手段(主动发现)
|
||
|
||
### 手段 1:全图几何与阻挡坐标探针(Headless Coordinate Probes)
|
||
由于地图静态放置数据由 `areadata.txt` 和 `attr.atr` 完全定义,无需人工跑图,可由自动化脚本扫盘检测:
|
||
1. **特殊静态模型巡检**:
|
||
- 提取地图上全部引用了含有 `.mdatr`(带有 `heights` 网格)的物体(桥梁、台阶、悬崖栈道等)。
|
||
- 在该物体几何中心上方向下打垂直射线或采样 `sample_height(x, z)`。
|
||
- **断言**:采样高度必须落在物体表面标高区间内,与裸地形高度应有明显的阶跃高差;若 `sample_height == terrain_height`,直接报警并定位对应模型路径与坐标。
|
||
2. **地貌高度一阶导数(梯度突变)扫描**:
|
||
- 按 0.5m 步长对可通行路网进行网格采样。
|
||
- 检测相邻点高度差:若连续两点高度差 $> 2.5\text{m}$ 且未标记 `ATTRIBUTE_BLOCK`,判定为地貌穿透或缝隙掉落点。
|
||
|
||
### 手段 2:40250 核心链路白盒对拍(Static Call-Graph Audit)
|
||
以 40250 源码的关键函数为索引,走查当前客户端对应的实现:
|
||
- **查阅 40250 源码**:`40250/Server Client TMP4/ClientVS22/source/`
|
||
- **重点审查模式**:
|
||
- 40250 中包含 `fMAX(...)`、`min(...)`、多层分支判断的函数,当前客户端是否只实现了其中单条简单路径?
|
||
- 40250 中从资源读取参数的逻辑(如 `.msa` 帧事件、`.msm` 挂点、`.mdatr` 碰撞体),当前客户端是否被硬编码(hardcoded)常量替代?
|
||
|
||
### 手段 3:未决协议与资源缺失日志审计(Log & Protocol Auditing)
|
||
在测试与实机运行期间,关注并收集控制台日志中的警告:
|
||
- 过滤关键 Warning:`unhandled packet`、`missing model`、`cannot find mdatr`、`mesh has no height`。
|
||
- 检查服务端发送的所有 GC 封包(如 `GC_MOVE`, `GC_SYNC_POSITION`, `GC_ATTACK`, `GC_FLY_TARGETING`),确认不仅有网络层的解析反序列化,更在表现层驱动了正确的物理与动画状态。
|
||
|
||
### 手段 4:自动化无头路网巡逻测试(Headless Patrol Suite)
|
||
利用 Godot `--headless` 启动无头客户端:
|
||
- 让角色沿关键地貌路网(广场 $\to$ 跨河桥梁 $\to$ 仓库台阶 $\to$ 城门通道 $\to$ 郊外野怪区)自动移动。
|
||
- 监控指标:
|
||
- 角色 Y 坐标是否发生非预期的下坠(掉水、穿地);
|
||
- `_blocked` 是否产生虚假阻挡导致角色提前停步卡死;
|
||
- 状态机是否在 `walk` / `run` / `wait` 之间异常震荡。
|
||
|
||
---
|
||
|
||
## 四、工程化修改与防回退治理原则
|
||
|
||
每次发现并修复类似细节问题时,必须严格执行以下三条工程原则:
|
||
|
||
### 原则 1:拒绝打补丁式硬编码(Treat the Root, Never Patch Coordinates)
|
||
- **禁止**针对个别坐标做特殊条件判断(如 `if x > 750 and x < 770: pos.y = 206.0`)。
|
||
- **必须**在底层管线实现通用机制(如在 C++ 引擎层解析 `.mdatr` 的 `heights` 并求交平面),确保全服数十张地图、数百座桥梁与所有建筑台阶自动获得修复。
|
||
|
||
### 原则 2:路线 A(严格 40250 原版实现)
|
||
- 不凭空发明“更好”的经验算法,遇到模糊细节直接翻阅 40250 对应 C++ 模块源码,将原版的数学公式、坐标系旋转(Z-up 转 Y-up、米制转换)与浮点容差 1:1 映射。
|
||
|
||
### 原则 3:修一处问题,必补一个 Parity 自动化回归测试
|
||
- 修复缺陷后,必须在 `project/` 下建立对应的 `test_*_parity.gd` 单元/集成测试(例如针对桥梁建立的 `test_bridge_height_parity.gd`)。
|
||
- 将测试纳入持续集成验证集(134+ Parity 测试套件),任何一次打包或构建前运行,确保旧缺陷绝不回退。
|