Files
mtgodot-poc/audit/contracts/world.terrain-water-collision.md
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

13 KiB
Raw Blame History

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。