SYS.READ_STREAM | UTF-8
PATH: BlockUpdate/03-特殊的更新行为.md
WORDS:3,759
|EST_TIME:13 MIN
##03 特殊的更新行为
本部分将介绍一些方块的特殊的更新行为
- 详谈红石粉的更新及其代码解析
- 铁轨链的递归检查行为
在#01中,我们引入了 NC 更新的 “更新源” 的概念,最普通常见的方块更新:例如放置、破坏方块产生的更新,都是以变化方块的位置为更新源,向六个方向发出 NC 更新的,这种就可以称作 “一阶毗邻更新”。
而红石粉则不然。举例来说,在红石粉的能量等级发生变化时,会发出 “二阶毗邻更新”,指的是:红石粉以自身位置和自身毗邻的六个方块分别为更新源,分别向六个方向发出 NC 更新。
“一阶” 与 “二阶” 的命名,是由最远受到 NC 更新的方块距离发出更新方块的曼哈顿距离1得来的。一阶毗邻更新即最远受到 NC 更新的方块距离发出更新方块的曼哈顿距离为 1,二阶毗邻更新即该距离为 2。
#3.1.2 红石粉二阶毗邻更新的更新源顺序(红石粉更新的位置性)
红石粉在发出二阶毗邻更新时,7 个更新源的先后顺序是基于红石粉坐标的哈希信息随机排列的,其顺序见下表。这也就是红石粉更新具有位置性的由来。这些更新源有 97% 的概率被分为三组(-Y, +Z, +X、O、+Y, -Z, -X),并以如下顺序发出更新:
注:表中 O 表示源红石粉,-X 等表示更新源相对于源红石粉的方位
| 先 | 然后 | 最后 | 这种顺序的概率 |
|---|
| -Y, +Z, +X | O | +Y, -Z, -X | 24.267% |
| +Y, -Z, -X | O | -Y, +Z, +X | 24.267% |
| O | -Y, +Z, +X | +Y, -Z, -X | 12.133% |
| O | +Y, -Z, -X | -Y, +Z, +X | 12.133% |
| -Y, +Z, +X | +Y, -Z, -X | O | 12.133% |
| +Y, -Z, -X | -Y, +Z, +X | O | 12.133% |
各组别内的更新顺序是固定的,但组别排列顺序随机。除此之外,还有一些其他的概率极低的排列选项。
斜放的动力铁轨(PoweredRailBlock)的激活状态(powered=true|false)变化时,它会按如下顺序依次发出两组更新:
- 第一组:依次以上方方块、自身、下方方块为更新核发出 NC 更新。
- 第二组:依次以自身、以下方方块、以上方方块为更新核发出 NC 更新。
调用栈如下
源码简析如下
铁轨在接受到 NC 更新时会检测自身充能状态。当满足以下两个条件之一时,它将会判断自己被激活powered=true:
- 直接接收到红石信号,即被直接充能
- 由相连的其他铁轨充能,即被间接充能
铁轨检查自身是否被间接充能并不是检测相连铁轨是否被充能,而是递归寻找整条铁轨链上距离自身 8 个铁轨及以内的 (若认为直接相连的两个铁轨的距离为 1 个铁轨)、被直接充能的铁轨,过程如下:
- 初始时距离变量为
0。若距离=8,则终止检查并返回False,即不被间接充能。
- 铁轨具有两个方向,当开始检查时,铁轨先检查哪一个方向由一个布尔值控制。当布尔值为
True时,且当铁轨形状为南北/东西朝向时,它检查南/西侧铁轨;当铁轨形状是上下斜向shape=ascending_*时,它检查南侧上方 / 西侧上方的铁轨。当布尔值为False时反之。此布尔值在开始检查时先为True,后为False。且由于逻辑运算短路2:若第一次布尔值为True时检查成功,则不会进行第二次布尔值为False的检查。在递归检查过程中 (见步骤 6) 此布尔值不会改变。
- 移动坐标
(i, j, k)到接下来要检查的铁轨位置。
- 判断当前铁轨能否可能在当前检查方向上与当前方向的下方的一个铁轨相连。若当前铁轨为向东/西/南/北侧向上,且检查方向为东/西/南/北,则认为不能。此步骤是为步骤 8 准备的。例如:当前铁轨为北侧向上,此时在向南侧检查,则此铁轨可能与南侧下方的铁轨相连;当前铁轨为北侧向上,此时在向北侧检查,则此铁轨不可能与南侧下方的铁轨相连。
- 若当前铁轨形状为南侧向上/北侧向上或东侧向上/西侧向上,则认为当前铁轨形状为南北朝向或东西朝向 —— 这一形状更改是为下一步骤 (6.b.) 准备的。
- 检查坐标
(i, j, k):
- 检查当前位置是否是一个铁轨,如果不是,则返回
False,结束当前步骤检查。
- 检查当前位置是否与原铁轨相连。此步骤并不检查 “相连”,而是检查 “不相连”:若原铁轨形状为东西朝向,则若当前铁轨形状为南北/北侧向上/南侧向上,则不相连,返回
False,结束步骤 6 检查。若原铁轨为南北朝向,则若当前铁轨形状为东西/东侧向上/西侧向上,则不相连,返回False,结束当前步骤检查。
- 步骤 i.ii. 实际都是 iii.iv. 的前置条件。此步骤检查当前位置的铁轨是否被直接充能,如果是,则返回
True,结束当前步骤检查。
- 跳至步骤 1,距离增加
1,并按照开始检查的布尔值进行下一个铁轨的检查 (递归) 。
- 若步骤 6 返回
True,则终止检查并返回True。
- (见步骤 4 判断) 若当前铁轨可能在当前检查方向上与当前方向的下方的一个铁轨相连,那么检查坐标
(i, j, k)下方的方块,即跳至步骤 6,但检查的坐标为(i, j-1, k)。在步骤 4 的例子中,若步骤 3 返回False,那么此时将检查原铁轨南侧下方的铁轨。
一个平铁轨可能与看似不相连的斜铁轨判断为相连。同样的结构若红石块直接充能平铁轨则斜铁轨不会被充能
#3.5 【进阶】亮起侦测器到位无毗邻 NC 更新
当活塞推动一个亮起的侦测器到位、且到位位置无侦测器计划刻时,它不会对毗邻方块(除输出端指向的方块)发出常规意义上由活塞推动触发的 NC 更新(指通常情况下活塞推动一般方块到位后,会在方块到位位置发出由flags=3的setBlockState触发的 NC 更新),原理如下:
- 到位分两种情况:
- 一般到位:调用
PistonBlockEntity.tick,其中调用setBlockState,flags=67
- 强制到位:调用
PistonBlockEntity.finish,其中调用setBlockState,flags=3
- 在
setBlockState中调用worldChunk.setBlockState,然后调用onBlockAdded
- 在
ObserverBlock.onBlockAdded中:
- 判断当前位置的旧方块是否为侦测器,由于旧方块是 b36,判断
true,下一步
- 判断自身是否激活且当前位置无侦测器计划刻,由于自身亮起
powered=true且当前位置无侦测器计划刻,判断true,下一步
setBlockState,flags=18,将自身激活状态变为powered=true -> powered=false,且发出 PP 更新
- 发出常规的侦测器的 NC 更新,即对输出端指向的方块及指向方块的毗邻方块发出 NC 更新
- 由于在步骤 3.iii. 中,侦测器
powered属性改变,此时继续步骤 1 的setBlockState,但由于blockState2 == state即前后状态是否一致为false,setBlockState不再发出常规的 NC 更新,而是直接结束
// 这是起始点
@Override
protected void updateBlockState(BlockState state, World world, BlockPos pos, Block neighbor) {
boolean bl2;
boolean bl = state.get(POWERED);
boolean bl3 = bl2 = world.isReceivingRedstonePower(pos) || this.isPoweredByOtherRails(world, pos, state, true, 0) || this.isPoweredByOtherRails(world, pos, state, false, 0);
// 逻辑运算短路说的是这里 bl3。
// 如果被直接充能,那么直接结束。否则先 bl = true,后 false。见后
if (bl2 != bl) {
world.setBlockState(pos, (BlockState)state.with(POWERED, bl2), Block.NOTIFY_ALL);
world.updateNeighborsAlways(pos.down(), this);
if (state.get(SHAPE).isAscending()) {
world.updateNeighborsAlways(pos.up(), this);
}
}
}
// 便于区分,这块函数标记为 isPoweredByOtherRails(01)
protected boolean isPoweredByOtherRails(World world, BlockPos pos, BlockState state, boolean bl, int distance) {
// bl, 即上文中控制 “先检查哪一方向” 的布尔值
if (distance >= 8) {
return false;
}
int i = pos.getX();
int j = pos.getY();
int k = pos.getZ();
boolean bl2 = true;
RailShape railShape = state.get(SHAPE);
switch (railShape) { // 根据当前铁轨形状移位检查,由 bl 可知铁轨递归检查总是先检查 - x(西)或 + z(南)方向
case NORTH_SOUTH: {
if (bl) {
++k; // 铁轨南北向,则先 z 增(向南)
break;
}
--k; // 若南侧检查失败则 z 减(回过来向北),若南侧成功则熔断不再向北
break;
}
case EAST_WEST: {
if (bl) { // 同上,铁轨东西向,则先 x 减(向西)
--i;
break;
}
++i; // 同上,x 增(向东)
break;
}
case ASCENDING_EAST: { // 铁轨东侧上升,即东高西低的斜铁轨
if (bl) {
--i; // 因为西低,所以 - x 西侧可能存在同 y 的铁轨连接
} else {
++i; // 因为东高,所以 + x 东侧不可能存在同 y 的铁轨连接
++j; // 所以 y + 1,东侧若有连接那么铁轨必然高 1 格
bl2 = false; // 上文中步骤 4 的 “当前铁轨能否可能在当前检查方向上与当前方向的下方的一个铁轨相连” 的判断,这一判断默认是成立的。
// 举例来说,一个 y = 1 的平放铁轨,如果边上有一个下降的铁轨与它相连,那么这个铁轨一定比它低一格。
// 而此时检查东侧,因为东高,所以不可能东侧有低一格的铁轨相连。判断变为 false
}
railShape = RailShape.EAST_WEST; // 上文种步骤 5 的形状更改
break;
}
case ASCENDING_WEST: { // 同理
if (bl) {
--i;
++j;
bl2 = false;
} else {
++i;
}
railShape = RailShape.EAST_WEST;
break;
}
case ASCENDING_NORTH: { // 同理
if (bl) {
++k;
} else {
--k;
++j;
bl2 = false;
}
railShape = RailShape.NORTH_SOUTH;
break;
}
case ASCENDING_SOUTH: { // 还是同理
if (bl) {
++k;
++j;
bl2 = false;
} else {
--k;
}
railShape = RailShape.NORTH_SOUTH;
}
}
if (this.isPoweredByOtherRails(world, new BlockPos(i, j, k), bl, distance, railShape)) {
return true;
// 注意这里调用的不是当前代码块的这一函数。java 有通过入参类型匹配不同函数的性质。
// 这里调用的是下文中的 isPoweredByOtherRails(02)
}
return bl2 && this.isPoweredByOtherRails(world, new BlockPos(i, j - 1, k), bl, distance, railShape);
// 同上,调用的也是(02)
// 这里的 bl2 也就对应上文中步骤 8。j-1 也就是 y 坐标向下移了一格。
}
// 便于区分,这块函数标记为 isPoweredByOtherRails(02)
protected boolean isPoweredByOtherRails(World world, BlockPos pos, boolean bl, int distance, RailShape shape) {
// 这里 RailShape shape 是调用这一函数的铁轨的 shape,也就是上一个铁轨的 shape
BlockState blockState = world.getBlockState(pos);
if (!blockState.isOf(this)) { // 略
return false;
}
RailShape railShape = blockState.get(SHAPE); // 略
if (shape == RailShape.EAST_WEST && (railShape == RailShape.NORTH_SOUTH || railShape == RailShape.ASCENDING_NORTH || railShape == RailShape.ASCENDING_SOUTH)) {
return false; // 上一个铁轨与当前铁轨不相连,那么不可能被简介充能。对应步骤 6.ii.
// 在 isPoweredByOtherRails(01)中也就是上文步骤 5 有形状更改,形状更改的影响在这里就体现了
// 盲猜本意是为了便于检查,但是这导致斜铁轨由于被改变形状了,导致一个平铁轨可能与看似不相连的斜铁轨判断为相连
// 而且这种判断是单向的,因为铁轨递归判断不存在 + y 的行为。例子见后图
}
if (shape == RailShape.NORTH_SOUTH && (railShape == RailShape.EAST_WEST || railShape == RailShape.ASCENDING_EAST || railShape == RailShape.ASCENDING_WEST)) {
return false; // 同上
}
if (blockState.get(POWERED).booleanValue()) { // 如果铁轨是亮的(这里不是 “被直接/间接” 充能的判断,而是只要铁轨是亮的就行(nbt 中 powered = true)
if (world.isReceivingRedstonePower(pos)) {
return true; // 一直到找到被直接充能的铁轨才结束。这里就是整个递归判断为 true 的返回点
}
return this.isPoweredByOtherRails(world, pos, blockState, bl, distance + 1);
// 这里回到 isPoweredByOtherRails(01),distance + 1,递归。
}
return false;
}