负坐标在两处破坏区块运算,而非一处
截断除法会把方块 −1 归入区块 0 而非 −1,而普通的 % 会给出 −1 的区块内偏移而非 15。两者都错了,而且在出生点以西之前都看起来没问题,区域坐标和区块坐标是同一套换算再往上一层,一个区域相当于 512×512 个方块。
每个区块计算在正象限里都微不足道,在另外三个象限里却都是错的。出错的地方恰好有两处,它们彼此独立,而且都会给出看似合理的答案。
两个运算
chunk = floor(block / 16) not trunc(block / 16)
offset = ((block % 16) + 16) % 16 not block % 16
区域坐标是同一对数值,只是高一级,用 32 代替 16:
region = floor(chunk / 32) -> r.<x>.<z>.mca
regionOff = ((chunk % 32) + 32) % 32
所以一个区域是 32 × 32 个区块,也就是 512 × 512 个方块。
各自在哪里出问题
除法。 大多数语言向零截断,而 floor 向负无穷取整。它们在正数上一致,在所有非整数倍的负数上都不一致:
- 方块
−1→floor(−1/16) = −1✓,但trunc(−1/16) = 0✗
方块 −1 位于原点以西一个方块,属于区块 −1。截断把它归入区块 0,和方块 +1 放在一起。每一个负的区块索引都会算出来高一位。
取余。 在 JavaScript、Java、C 以及大多数其他语言中,% 保留左操作数的符号:
- 方块
−1→ 正确偏移15,但−1 % 16 = −1✗
偏移 −1 根本不是一个 16 宽区块内的位置。用它去索引方块数组,要么抛异常,要么静默读到错误的格子。
一个实例演算
某一轴上的方块 −1290:
| 步骤 | 正确 | 朴素做法 |
|---|---|---|
| 区块 | floor(−1290/16) = −81 | trunc = −80 ✗ |
| 区块内偏移 | 6 | −1290 % 16 = −10 ✗ |
| 区域 | floor(−81/32) = −3 | trunc = −2 ✗ |
| 区域内偏移 | 15 | −81 % 32 = −17 ✗ |
四个值,四个错误答案,而且没有一个超出范围到看起来明显坏掉。区域 −3 覆盖区块 −96 到 −65,区块 −81 位于其起点之后 15 处——这正是修正后的取余所返回的结果。
下界转换在一个方向上是不可逆的
主世界到下界是除以 8,而且同样向下取整:
netherX = floor(overworldX / 8)
回来时会把你的精确位置乘以 8,所以整个下界方块 125 会映射回 1000–1007 中的某处,具体取决于你站在该方块内的哪个位置。对于方块坐标来说,这不是一次往返。floor 丢弃了余数:
- 主世界
1000→ 下界125→ 回到1000✓ - 主世界
1007→ 下界125→ 方块坐标显示1000✗ —— 少了七个方块
误差是每轴 0 到 7 个方块,所以 X 和 Z 上可以同时各差 7 个。这正是精确传送门链接需要把下界坐标写下来、而不是重新推导的全部原因:一旦除了,它来自的主世界位置就不再可恢复。
对于下界枢纽来说,这很少要紧——7 个方块还在传送门自身的捕获半径之内。对于链接两个不能互相抢流量的传送门来说,这就非常要紧了。
负的主世界坐标会以熟悉的方式让情况更糟:主世界 −1 得到下界 floor(−1/8) = −1,而截断得到 0,把你送到轴的另一侧。