不知你是否见过下面这张截图:
MCPE-81981
乍一看没什么,仔细观察却发现满满的槽点:
MCPE-81981
这张图没有造假,它截自 Beta 1.16.0.66,并出现在 Mojang 漏洞追踪器上,编号 MCPE-81981。这个奇特的现象一直持续到 1.19.40.20 Preview 才被修复。
一. 分析
元件搜索过程
核心是广度优先搜索算法,搜索的起点为信号源(生产者或电容器)。最大搜索距离为 15,搜索距离即为阻尼(dampening)。
为实现该过程,mojang 使用一个叫 CircuitTrackingInfo 的结构来保存搜索到的元件信息,为方便下文称之为追踪表:
class CircuitTrackingInfo {
Entry mCurrent;
Entry mNearest;
Entry m2ndNearest;
Entry mPower;
int mDampening;
// Entry内包含元件的指针、坐标等信息
};
除此之外,还有 mDirectlyPowered、mData 这两个数据,前者用于描述激活方式,默认为 true,后者用于具体元件需要的信息,默认为 0,比如铁轨用于保存铁轨间信号传递距离。
上北下南
现在以拉杆搜索三个红石粉为例,实际感受一下搜索过程:
以上是简化的搜索流程,实际上远比这个复杂,比如特定情况下除了搜索 mNearest 的六个面,还会搜索 mNearest 顶部四周、底部四周的方块。
allowConnection、addSource
这两个函数除了字面功能外还要负责更新 mDirectlyPowered,addSource 还要更新 mDampening。
红石粉的 allowConnection 函数主要是各种判断引线压线的判断,这里省略。而红石粉的 addSource 则是本文重点,因为红石粉是唯一一个能使 mDampening 增加的元件。
在这个函数中,红石粉先检查 mNearest 是否为弱充能方块,如果是,则显然弱充能方块不能激活红石粉,直接返回 false。
然后,红石粉会更新 mDirectlyPowered,根据代码逻辑和个人经验 mDirectlyPowered 恒为真,所以暂时不细讲(实际上这里更新 mDirectlyPowered,并不会修改追踪表内的 mDirectlyPowered,除非 addSource 返回 true,也即元件成功添加信号源)。
最后,本文重点中的重点,红石粉调用 trackPowerSource 函数,这个函数是基类提供的,其给出了较为通用的向信号源表添加或更新信号源的方法,红石粉、中继器、比较器都借助了这个函数。添加成功返回 true。
bool BaseCircuitComponent::trackPowerSource(const CircuitTrackingInfo &info, int dampening, bool directlyPowered, int data);
该函数需要传入一个 dampening,红石粉也就是这个时候,对阻尼进行+1 操作,但是此时的+1 并没有修改追踪表内的阻尼,除非 addSource 返回 true。
这里微软的员工为自己挖了一个小坑,也即:trackPowerSource 函数得到的 dampening 值与追踪表(info 参数就是追踪表)内的阻尼不一致。
trackPowerSource
是不是很绕?没逝的,反正这段代码的作者自己也被绕了。所以也不用再看上面的话,试图理解什么要更新什么不要更新。
真假阻尼
现在我们有两点问题:
以开头的现象为例,来看看它们的阻尼:
if (iter.mDampening - 1 <= info.mDampening - 1) {
// 旧的小
} else {
// 换新的
}
其中 iter 就是表中的信号源。可见这个判断语句跟开头的图一样,槽点满满...
旧的信号源的阻尼为什么要减 1?存进信号源表内的已经是减过了的啊。
再看右边,这里又用的是 info.mDampening,也就是追踪表内的阻尼,前文提到过,追踪表内的阻尼与函数形参 dampening 的值不一样,前者小一个单位。
那好,跟着这个逻辑走会怎么样?
对于中间红石粉,前为 1 - 1 = 0,后为 0 - 1 = -1,要换新的;
对于右边红石粉,前为 2 - 1 = 1,后为 1 - 1 = 0, 要换新的。
好像也是要换新的,然后你就会发现,他把 info.mDampening - 1 的结果存入信号源表中的阻尼了(小于 0 的取 0)。
iter.mDampening = info.mDampening - 1 < 0 ? 0 : info.mDampening - 1;
于是你就会发现,中间红石粉阻尼为 0,结果是对的;右边红石粉阻尼也为 0,我不知道结果对不对,反正代码作者以为是对的。
因为
信号强度 = 信号源强度 - 阻尼
所以,右边的红石粉就是 15 - 0 = 15。
结束问题
再看看左边为什么从 2 直接变为 0,虽然这一排红石粉自己存的阻尼是错的,但是追踪表内的阻尼是没有问题的,而搜索过程中追踪表内的阻尼大于 15 就直接结束,拉杆和其附着的石头下面的红石粉对应追踪表阻尼为 1,数一数,最右边大小为 3 处红石粉的 A 路追踪表阻尼刚好为 15,会进行最后一次搜索,大小为 2 处红石粉虽然不会从 A 路被激活,但是仍会添加信号源;而再左边一格的红石粉(大小为 0),其无法被 A 路搜索到,即使被 B 路搜索到,也不会受前面乱七八糟的影响。
再来看看比较器,比较器也受这个影响,但是比较器不会增大阻尼,回到上面追踪表内的阻尼跟函数形参 dampening 本就是同一个值,然后在那个错误的 if 句的作用下,很巧地每个比较器都没有更新阻尼,都用的 A 路的阻尼,除了最左边的一个比较器,它已经不能被 A 路搜索到了,但是还能被 B 路搜索到,于是只有它一个阻尼是对的。
Mojang 修 Bug 之新增了 114514 个 Bug
这个修起来很简单,前面的 info.mDampening 全部换成函数形参 dampening,再把 if 句换成这样就行了:
if (iter.mDampening <= dampening- 1)
所以 mojang 在确认这个 bug 之后,迅速展开行动,仅仅花了一周零 817 天,便完成修复。同时,为了展现自己高超的技术并提高代码鲁棒性,mojang 预防性地修改了比较器的代码,最后为广大红石玩家带来了一个振奋人心的好东西:
好 东 西
这玩意我不讲,单纯乱修整出来的bug。
有关链接
E N D