fix: 修复导航边界与路径平滑正确性 - #717
Conversation
Code Review — #717 fix: 修复导航边界与路径平滑正确性负高度世界边界、 但有一处需要说清楚:PR 描述说"修复节点坐标哈希,避免不同位置碰撞",实际上哈希函数本身没有改,加的是开放寻址探测。我实测发现探测序列的周期是 2^28 而非 2^32,并且构造出了第二类碰撞(x 的第 8 位与 z<0 标志位重叠)。这条需要作者确认是有意为之还是遗漏。 另外与 #712 有一处需要协调:#712 把 审阅方式:读 patch + 对照源码 + 实测(哈希碰撞枚举、探测序列周期、Spigot 1.20.4 的 Material 枚举)。未实跑 gradle 测试。 🟡 问题 1 — 哈希碰撞没有真修,只是加了开放寻址;探测序列周期为 2^28
fun createHash(x: Int, y: Int, z: Int): Int {
return y and 0xFF or (x and 0x7FFF shl 8) or (z and 0x7FFF shl 24) or (if (x < 0) -0x80000000 else 0) or (if (z < 0) 0x8000 else 0)
}
assertEquals(Node.createHash(4, -64, 8), Node.createHash(4, 192, 8))我实测了单柱碰撞规模: 第二类碰撞我构造出来了。 即 开放寻址本身是可行的补救, key = key * 31 + 1
check(key != initialKey) { "Unable to resolve navigation node hash collision" }我实测了这个序列的周期: 周期是 2^28 = 268435456,不是完整的 2^32。也就是说这个探测序列只能覆盖 int 空间的 1/16。实践中
建议:直接修 如果因为 🟡 问题 2 — 与 #712 的
|
| 项 | 结论 |
|---|---|
navigationMinHeight() 版本兼容 |
if (MinecraftVersion.isHigherOrEqual(V1_17)) minHeight else 0。World.getMinHeight() 是 1.17 加入的 API,1.16 及以下调用会 NoSuchMethodError,这个版本门槛设置正确 |
isWithinNavigationHeight 边界 |
y >= minHeight && y < maxHeight。Bukkit 的 maxHeight 是排他上界(1.20 为 320,最高可放方块的 y 是 319),用 < 正确 |
getBlockAtIfLoaded 前置越界检查 |
新增的高度检查在 callRegion 之前,避免了越界坐标进入区域调度。旧代码会把越界 y 传给 getBlockAt |
| 纵向扫描的边界安全 | getStartAtRegion 的三处 while 循环都加了 y < maxHeight - 1 上界与 > minHeight 下界。旧代码 while (true) + blockposition.y > 0 在负高度世界会漏掉 y<0 的可行节点,在接近上界时会越界读 |
| 循环结构从 while(true)+break 改为条件式 | 旧代码 while (true) { if (!cond) { --y; break }; ++y; ... } 与新代码 while (cond && y < max) { ++y; ... }; if (!cond) --y 在正常路径上等价,但新代码在撞到上界时不会多减一次 y。语义保持 |
PathType.OPEN 下坠检查的下界 |
if (air < world.navigationMinHeight()) 替代 if (air < 0)。负高度世界里旧代码会一直往下探到 y<minHeight,新代码正确截断 |
getWalkTargetValue 改用目标坐标取区域 |
pos.toLocation(world).callRegion { ... } 替代 location.callRegion。Folia 下评分的目标点可能不在实体所在区域,按目标坐标取区域是正确的 |
isInWater 区分水与岩浆 |
旧 location.block.isLiquid 对岩浆也返回 true,导致岩浆里的实体被当作"在水中"从而走 canFloat 分支。改用 getFluid().isWater() 正确 |
| waterlogged 识别 | 1.13+ 通过 (blockData as? Waterlogged)?.takeIf { it.isWaterlogged } 识别含水台阶/楼梯/栅栏等。旧代码完全不认这类方块。版本门槛 V1_13 正确(Waterlogged 接口是 1.13 引入) |
getOrCreateNavigationNode 的正确性 |
探测时比对 existing.x/y/z 三者全等才复用,否则继续探测;空槽则新建。逻辑上不会把不同坐标当成同一节点,达到了"避免碰撞后果"的目的 |
RandomPositionGenerator 的 moveUp 上界 |
moveUp(result, 0, world.maxHeight) 替代硬编码 256。1.18+ 世界高度 320,旧代码在 y>256 的区域完全无法向上探测 |
acceptsNavigationSurface 的逻辑 |
allowWater || !isWater。旧代码 onWater || blockType?.isWater() == true 是"允许水或当前是水"——即不允许水时反而接受水面候选,逻辑写反了。新版是"允许水,或当前不是水"。真 bug 修复 |
PathSmoothing.smooth 空路径保护 |
if (path.nodes.isEmpty()) return emptyList()。旧代码 smoothAtRegion 里 nodes.size <= 2 会走 nodes.map { nodeCenter(it) },空列表返回空列表,本身不崩;但新增的前置检查避免了进入 callRegion(在 #712 语义下会抛异常)。有实际价值 |
| 碰撞箱扫掠替代固定步长 | 旧代码 SAMPLE_STEP = 0.5 固定采样,可能跳过窄障碍;新代码按实体碰撞箱四边跨格的精确 t 值采样 + 相邻中点。对轴向移动是完备的 |
maxBx / maxBz 的计算 |
ceil(x + halfWidth).toInt() - 1 替代 floor(x + halfWidth).toInt()。当 x + halfWidth 恰好落在整数边界时,旧代码会多算一格(实体边缘贴合格线但未进入下一格)。新版正确 |
| 身体空间检查改用 PathType | 旧代码 if (block.type.isSolid) return false 只看实心;新代码走 PathTypeFactory + entity.getPathfindingMalus,能识别岩浆、仙人掌、火等危险方块(malus != 0)。这是 PR 描述"检查危险方块"的实现,方向正确 |
| 高度范围前置检查 | `isWithinNavigationHeight(by, ...) |
isWater() 精确匹配的版本安全性 |
实测确认(见 🔵 e):修掉了 WATER_BUCKET / WATER_CAULDRON / 睡莲的误判,且不影响 1.12 及更早版本 |
| 测试覆盖 | 新增 NavigationCorrectnessTest 覆盖高度边界(含 −65/−64/319/320 四个边界值)、哈希碰撞与开放寻址复用、acceptsNavigationSurface、平滑安全判定。都是纯逻辑测试,不依赖 Bukkit 运行时,能在 CI 跑 |
总结
负高度世界的边界处理(navigationMinHeight + 三处纵向扫描 + moveUp 上界)、acceptsNavigationSurface 的逻辑写反、isWater() 把水桶/炼药锅/睡莲误判为水、isInWater 把岩浆当水、碰撞箱扫掠替代固定步长采样,这些都是实打实的修复。
建议处理:
- 问题 1(哈希)——PR 描述说"修复节点坐标哈希"但哈希函数未改。我实测确认两类碰撞仍存在(y 只有 8 位;x 第 8 位与 z<0 标志位重叠,
x=128,z=1与x=0,z=-32767相同),且探测序列周期是 2^28 而非 2^32。建议直接改createHash(换 Long 键最干脆),或修正 PR 描述的表述。 - 问题 2(与 fix(bukkit): 收紧 Folia 调度与背包线程所有权 #712 的
callRegion冲突)——两个 PR 合并后 navigation 的 15 处callRegion在异步线程会抛异常,且影响普通 Paper。需要两边协调。 - 问题 3(支撑高度精确相等)——
abs(supportY - y) > 1e-3实际上要求脚下必须是满方块,半砖/农田/雪层全部判为不可站立,平滑可能退化为无操作。建议确认节点 y 与方块顶面的语义对应关系。 - 🔵 b(对角穿角采样)建议一并看一下。
说明:本次审阅未实跑 gradle 测试(含 PR 描述列出的两条命令)。以下为本机实测:
createHash的两类碰撞与单柱碰撞规模;key*31+1探测序列周期 2^28;Spigot 1.20.4 全部含 "WATER" 的 Material 枚举及isLegacy()行为。其余结论基于 patch 与仓库源码推导,已逐条注明依据位置。
原有问题
导航模块沿用了不完整的世界高度、水体和直线路径判定:
典型触发场景与后果
本 PR 修改
修改目的
确保寻路结果与真实世界边界和实体体积一致,平滑过程只能删除真正可安全直达的节点,避免路径穿墙、越界或进入危险地形。
兼容性与行为变化
验证
./gradlew :module:bukkit:bukkit-navigation:test --rerun-tasks --no-parallel./gradlew :module:bukkit:bukkit-navigation:build --rerun-tasks --no-parallelgit diff --check(仅 Windows LF→CRLF 提示)Refs #703