# world.terrain-water-collision ## Scope 比较 40250 的户外地图高度、对象高度、属性阻挡、水面层、地形/对象拾取射线、桥梁和地面贴合调用链;同时核对加载失败、区块边界、地图切换、资源清理以及水面动画时序。 ## Reference call chain - `GameLib/MapOutdoor.cpp::GetTerrainHeight` / `GetHeight` - 以 200cm `CELLSCALE` 在 `height.raw` 的 131×131 样本上按 TL-BR 对角线做三角插值。 - `GetHeight` 在非 terrain-only 模式下通过 culling 的 `CAttributeInstance::GetHeight` 合并桥梁、楼梯和平台等 `.mdatr` 对象高度,取对象/地形最大值。 - `GameLib/AreaTerrain.cpp::CTerrain::GetHeight` - 使用 `HeightScale`、跨区块边界高度样本和固定的左/右三角分支;地图外返回 0。 - `GameLib/MapOutdoor.cpp::isAttrOn` / `GetAttr` / `CMapManager::isPhysicalCollision` - 世界厘米坐标转换为 100cm 属性格;bit flag 使用按位判断,16 以上的属性值使用高半字节相等规则。 - `isPhysicalCollision` 只判断 `ATTRIBUTE_BLOCK`,并把 Metin2 的 Y 取反后交给地图属性查询。 - `PRTerrainLib/Terrain.cpp::LoadWaterMapFile` / `GameLib/AreaTerrain.cpp::GetWaterHeight` - 读取 magic=5426、128×128 水区块、layer count 和 u16/u32 水高;水 id 映射到具体层高,错误加载会清空水图并返回失败。 - `MapOutdoorWater.cpp` 按水深计算顶点 alpha,并以 70ms/帧循环 30 张水纹理;水面共享 0 到 -15cm 的 1000~3000ms 高度扰动。 - `GameLib/MapOutdoor.cpp::GetPickingPointWithRay` - 先在 culling 对象上做 `CAttributeInstance::Picking`,再以 5/10/100cm 自适应步长和 5000/10000/100000cm 上限探测地形,返回离射线起点更近的对象或地形命中。 - `EterLib/AttributeInstance.cpp::GetHeight` / `Picking` - 对 `.mdatr` 高度三角形做 2D 包围盒、三角形和法向求交;对象命中包含可拾取的静态桥梁/平台,而不是只有网络角色。 ## Current call chain - `formats/terrain_files.cpp::load_height_map` / `load_attr_map` / `load_water_map` - 读取 `height.raw`、`attr.atr` 和 `water.wtr`,验证尺寸、magic、数据长度并支持 u16/u32 水高。 - `formats/terrain_mesh.cpp::terrain_height_at` / `build_terrain_mesh` - 已按 200cm 格、TL-BR 三角形和 `HeightScale` 生成 Godot 高度网格;`Metin2World::sample_height` 对已加载区块插值。 - `extension/src/metin2_world.cpp::build_chunk` - 为每区块建立地形 Mesh/HeightMapShape3D、属性对象 `.mdatr` 碰撞体、对象高度三角形和水面 Mesh;区块根节点统一卸载。 - `extension/src/metin2_world.cpp::sample_height` - 合并裸地形和已加载对象 `.mdatr` 高度,桥梁/平台命中时取更高表面;边界支持邻接区块的已加载对象查询。 - `extension/src/metin2_world.cpp::sample_attribute` / `is_blocked` - 读取 100cm 属性格;当前调用方主要使用 `ATTRIBUTE_BLOCK` 和 `ATTRIBUTE_WATER` 的 bit0/bit1。 - `extension/src/water_builder.cpp::build_chunk_water` / `water_depth.h` / `water_motion.h` - 每个水格保留四角深度 alpha,使用 400×HeightScale 的透明度公式、30 帧 shader 和共享高度扰动。 - `project/player_controller.gd::_ray_ground` - 通过每 1m 采样加二分寻找 `sample_height` 地表;实体点击单独使用网络实体 AABB/胶囊近似。 - `project/net_world.gd::_ground_world` / `_blocked` / `_pushed_position` - 网络实体和主角贴 `sample_height`;移动/击退通过 `is_blocked` 截断。 ## Audit findings (2026-09-20) ### 已对齐的部分 - `terrain_height_at` 的厘米单位、200cm 格、TL-BR 对角线、`xdist <= ydist` 三角分支和 `HeightScale` 已与 `CTerrain::GetHeight` 对齐;真实 A1/C1 桥面测试证明 `.mdatr` 高度会优先于河床。 - `attr.atr` 的 2634 magic、256×256 数据、100cm 属性格和 bit0 阻挡/bit1 水面查询已有实现;移动、击退和钓鱼入口都使用同一属性读取路径。 - `water.wtr` 的 5426 magic、128×128 id、layer count、u16/u32 高度兼容和失败尺寸检查已有实现;水面四角深度、浅滩透明度、动画纹理与共享高度扰动已有回归。 - 桥梁、平台等 `.mdatr` 高度三角形已进入 `sample_height`,区块卸载由根节点统一回收;A1/C1 全地图高度探针未发现桥面掉到河床的回归。 ### 材料差异 - 40250 的 `GetPickingPointWithRay` 同时比较静态对象 `CAttributeInstance::Picking` 和地形,并采用 5/10/100cm 自适应步长;当前 `PlayerController::_ray_ground` 只比较 `sample_height`,静态建筑/桥梁的射线命中没有进入点击结果。网络实体点击也只是节点 AABB/胶囊近似,尚未复用统一的世界拾取合同。 - 40250 暴露 `GetWaterHeight(int,int)`,钓鱼抛竿会把 600cm 目标点的 Z 设置为具体水层高;当前端 `get_fishing_rotation` 只验证 `ATTRIBUTE_WATER`,`_new_fishing` 没有读取水层高来生成等价的鱼钩目标位置。水面可见且钓鱼方向可判定,不代表抛竿位置已等价。 - `CTerrain::isAttrOn` 对 `byAttr >= 16` 使用高半字节相等规则;当前 `sample_attribute` 只返回原始字节、`is_blocked` 只支持 bit0,尚没有通用的 `is_attr_on` 兼容接口,因此 `BANPK` 以外的高半字节调用方缺少等价证据。 - 40250 的 `GetHeight` 会根据 `m_bEnableTerrainOnlyForHeight` 选择是否合并 culling 对象,并且对象高度查询、地形加载状态、地图坐标边界有明确返回分支;当前 `sample_height` 依赖区块已被实际加载以及 `objects_enabled/registry_ok`,资源未加载或 streaming 队列中时会暂时回退为裸地形,缺少“查询触发加载/延迟重试”的同等状态契约。 - Godot `HeightMapShape3D`/`.mdatr` 碰撞体和逻辑 `is_blocked` 是两套并行碰撞来源;当前角色移动主要走属性逻辑,静态对象物理体主要服务射线/弹道,尚没有证明两者在斜坡、对象边缘、跨区块和卸载帧的结果与 40250 `isPhysicalCollision`/`CAttributeInstance` 顺序一致。 - 当前水面视觉测试覆盖 geometry/material 和真实 A1 水深,但没有验证 `water id -> layer height -> fishing target Z` 的运行时链,也没有覆盖缺失水图后水状态清零、边界水格和地图切换期间 shader/motion 状态的完整清理矩阵。 - `TerrainDecal` 的高度贴花三角选择、`MapOutdoor` 的 normal/shadow/visibility 依赖和当前地形 patch 视锥剔除尚未形成可执行的功能合同;它们不阻塞当前移动主链,但不能因地形网格测试通过而宣称渲染/拾取完全一致。 ## Equivalence matrix | 项目 | 状态 | 备注 | |---|---|---| | Preconditions | PARTIAL | 资源尺寸/magic/区块加载已有检查;对象拾取、水层目标、高半字节属性和 streaming 未闭合 | | Branch structure | PARTIAL | 高度、属性、水面主分支已映射;对象/地形射线选择、地图外、延迟加载和水失败分支未全等价 | | Algorithms/formulas | PARTIAL | 高度插值、桥面取高、水深 alpha 已对齐;拾取步长、对象 picking 和水高接口不同 | | State transition order | PARTIAL | 区块构建→地形/碰撞/水面→对象高度基本一致;加载队列、点击命中优先级、钓鱼目标 Z 顺序未闭合 | | Constants/units | PARTIAL | 200cm terrain cell、100cm attr cell、600cm fishing probe、水深/动画常量已记录;射线 5/10/100cm 与当前 1m 采样不同 | | Timing/event sources | PARTIAL | 水纹 70ms 和扰动时序已有实现;streaming、地图卸载、拾取/钓鱼事件的端到端时序证据不足 | | Resource/data sources | PARTIAL | height/attr/water/mdatr 可读取;对象 culling、water layer 查询、terrain normal/decal/shadow 统一资源入口未完成 | | Protocol side effects | PARTIAL | 属性阻挡/钓鱼方向会影响移动和发包;水层高度未进入抛竿副作用,静态对象点击也未统一 | | Interruption/failure/cleanup | PARTIAL | 区块根节点卸载和基本加载失败已有路径;水状态、待处理拾取、streaming 中断和地图切换边界仍缺完整矩阵 | ## Regression evidence - `build/formats/formats_map_test`:高度插值、地形网格、地图格式和资源边界;当前无 `M2_ASSETS` 时跳过 live A1 断言。 - `project/test_bridge_height_parity.gd`:真实 A1 两座桥的 `.mdatr` 桥面高度、河床回退和跨度采样。 - `project/test_all_map_height_probe.gd`:A1/C1 真实地图桥面高度、地形回退、属性阻挡和跨跨度采样。 - `project/water_reference_test.gd`:真实 A1 水格、四角深度 alpha、浅滩和水材质/动画结构。 - `project/fishing_water_test.gd`:真实 A1 `ATTRIBUTE_WATER`、600cm 方向扫描和地图外拒绝。 - `project/test_click_target_effect_parity.gd`:地面命中信号、网络实体点选和静态弹道障碍;该测试的地面段使用 mock world,不能替代 40250 静态对象 ray picking。 - `project/player_move_test.gd`、`project/test_skill_ground_fix.gd`:移动阻挡和角色/技能贴地回归。 ## Deep audit round 2026-09-21 ### Fresh regression results - `test_bridge_height_parity.gd`、`test_all_map_height_probe.gd`、`water_reference_test.gd`、`fishing_water_test.gd`、`player_move_test.gd`、`test_skill_ground_fix.gd` 的断言均通过;A1 水面为 `failures=0`,15 pieces、212587 vertices。 - `test_click_target_effect_parity.gd` 断言通过,但进程报告 3 个 ObjectDB 和 1 个资源未释放;`fly_target_bounds_test.gd` 断言通过但 exit 139,并泄漏 48 个 ObjectDB。 - `formats_map_test`、`net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`net_classic_wire_test` 均退出码 0;formats live A1 断言因未设置 `M2_ASSETS` 跳过。 ### Static parity conclusion - 高度插值、桥面取高、属性 bit0/bit1、水图读取和局部水面渲染结构继续与参考接近;这部分测试不能证明完整拾取/碰撞等价。 - 当前 `_ray_ground` 主要走 1m 采样的 `sample_height`,没有实现 40250 对静态 `.mdatr` 对象先做 `Picking`、再以 5/10/100cm 自适应步长比较对象与地形命中的统一射线链。 - 水面方向检测尚未把 `water id -> layer height -> fishing target Z` 接入抛竿副作用;高半字节属性规则、streaming 中延迟重试和对象/地形双碰撞源也未闭合。 - 点击/飞行和真实地图测试的泄漏/exit 139 表明跨图、区块卸载和特效/目标清理仍不能视为稳定等价。 ### Active GameLib/PRTerrainLib terrain-core review - `GameLib/MapOutdoor.cpp` 除高度/水面外还负责 terrain/area 初始化、environment 绑定、视锥、对象和地形 ray picking;`MapOutdoorQuadtree.cpp` 建立/销毁四叉树,`MapOutdoorIndexBuffer.cpp` 为不同 patch/warp 方向生成索引,`TerrainPatch.cpp` 同时维护硬件/软件地形顶点、水顶点、光照和 patch proxy。 - `MapOutdoorCharacterShadow.cpp` 创建角色 shadow render target/depth surface、切换 viewport/render target 后恢复旧状态;当前 Godot terrain/splat/WorldEnvironment 有视觉替代,但没有同一设备资源、patch LOD、四叉树和 shadow pass 生命周期。 - `PRTerrainLib/Terrain.h`/`TerrainType.h`/`TextureSet.cpp`/`TextureSet.h` 补充 height/normal/water/texture-set 的底层数据和纹理层状态。当前格式测试与 A1 水面测试覆盖读取结果,但未证明 TextureSet、patch、quadtree、shadow 与 streaming 卸载保持同一资源边界。 - 本轮完成上述 GameLib/PRTerrainLib 活跃地图源码静态核对;静态网格与水面测试不能替代对象 picking、patch/culling、shadow 和资源清理等价性。 ## Status `PARTIAL`。地形插值、桥面高度、属性阻挡、水面渲染和钓鱼方向已经有较强实现与真实地图证据;静态对象射线优先级、具体水层高度、高半字节属性、streaming 边界和统一碰撞/清理时序仍未达到 40250 的一致实现。 ### Active EterLib collision-core review - `EterLib/AttributeData.cpp`/`AttributeInstance.h`/`CollisionData.cpp`/`CollisionData.h` 提供静态属性数据、碰撞形状、法线/命中信息和实例级查询;`CullingManager.cpp` 负责范围、射线、可见性注册和对象集合维护;`Decal.cpp` 还参与地表贴花的对象/高度依赖。 - 这些源码确认参考端的碰撞/拾取不是单纯高度采样:对象注册、射线、属性实例和清理顺序共同决定桥梁/平台/遮挡结果。当前端的 `sample_height`、Godot `HeightMapShape3D`、逻辑 `is_blocked` 和对象 AABB/胶囊是并行近似,未统一成同一 culling/attribute query 链。 - 因此本轮完成 `AttributeData`、`CollisionData`、`CullingManager`、`Decal` 的活跃源码静态核对,但没有把静态符号存在误记为 parity 通过;静态对象优先级、跨区块卸载和碰撞/贴花清理仍是 `PARTIAL`。