SYS.READ_STREAM | UTF-8
PATH: BlockUpdate/03-特殊的更新行为.md
WORDS:3,748
EST_TIME:13 MIN

##03 特殊的更新行为

部分特殊的更新范围
本部分将介绍一些方块的特殊的更新行为

基础部分

  • 红石粉的更新简述
  • 斜侧铁轨的更新

进阶部分

  • 详谈红石粉的更新及其代码解析
  • 铁轨链的递归检查行为

#01中,我们引入了NC更新的“更新源”的概念,最普通常见的方块更新:例如放置、破坏方块产生的更新,都是以变化方块的位置为更新源,向六个方向发出NC更新的,这种就可以称作“一阶毗邻更新”。

而红石粉则不然。举例来说,在红石粉的能量等级发生变化时,会发出“二阶毗邻更新”,指的是:红石粉以自身位置和自身毗邻的六个方块分别为更新源,分别向六个方向发出NC更新。

“一阶”与“二阶”的命名,是由最远受到NC更新的方块距离发出更新方块的曼哈顿距离1得来的。一阶毗邻更新即最远受到NC更新的方块距离发出更新方块的曼哈顿距离为1,二阶毗邻更新即该距离为2。

红石粉在发出二阶毗邻更新时,7个更新源的先后顺序是基于红石粉坐标的哈希信息随机排列的,其顺序见下表。这也就是红石粉更新具有位置性的由来。这些更新源有97%的概率被分为三组(-Y, +Z, +XO+Y, -Z, -X),并以如下顺序发出更新:

注:表中O表示源红石粉,-X等表示更新源相对于源红石粉的方位

然后最后这种顺序的概率
-Y, +Z, +XO+Y, -Z, -X24.267%
+Y, -Z, -XO-Y, +Z, +X24.267%
O-Y, +Z, +X+Y, -Z, -X12.133%
O+Y, -Z, -X-Y, +Z, +X12.133%
-Y, +Z, +X+Y, -Z, -XO12.133%
+Y, -Z, -X-Y, +Z, +XO12.133%
其他各项<0.2%

各组别内的更新顺序是固定的,但组别排列顺序随机。除此之外,还有一些其他的概率极低的排列选项。

斜放的动力铁轨(PoweredRailBlock)的激活状态(powered=true|false)变化时,它会按如下顺序依次发出两组更新:

  1. 第一组:依次以上方方块、自身、下方方块为更新核发出NC更新。
  2. 第二组:依次以自身、以下方方块、以上方方块为更新核发出NC更新。

调用栈如下:

perl// EXECUTABLE_BLOCK
PoweredRailBlock.updateBlockState(...) ├── world.setBlockState(pos, ..., 3) // pos, flags=3 │ ├── worldChunk.setBlockState(...) │ │ └── PoweredRailBlock.onStateReplaced(...) │ │ ├── world.updateNeighborsAlways(pos.up(), this) // 1.1 上方 │ │ ├── world.updateNeighborsAlways(pos, this) // 1.2 自身 │ │ └── world.updateNeighborsAlways(pos.down(), this) // 1.3 下方 │ └── world.updateNeighbors() // 2.1 自身 ├── world.updateNeighborsAlways(pos.down(), this) // 2.2 下方 └── world.updateNeighborsAlways(pos.up(), this) // 2.3 上方

源码简析如下:

java// EXECUTABLE_BLOCK
protected void updateBlockState(BlockState state, World world, BlockPos pos, Block neighbor) { boolean bl = (Boolean)state.get(POWERED); boolean bl2 = world.isReceivingRedstonePower(pos) || this.isPoweredByOtherRails(world, pos, state, true, 0) || this.isPoweredByOtherRails(world, pos, state, false, 0); if (bl2 != bl) { world.setBlockState(pos, (BlockState)state.with(POWERED, bl2), 3); // 2.1 自身 world.updateNeighborsAlways(pos.down(), this); // 2.2 下方 if (((RailShape)state.get(SHAPE)).isAscending()) { world.updateNeighborsAlways(pos.up(), this); // 2.3 仅斜铁轨 上方 } } } // 见调用栈,由于setBlockState先调用到onStateReplaced然后再updateNeighbors // 所以先执行这一组NC更新,然后再以自身为更新源发出NC更新,即上文中标注的2.1 public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { if (!moved) { super.onStateReplaced(state, world, pos, newState, moved); if (((RailShape)state.get(this.getShapeProperty())).isAscending()) { world.updateNeighborsAlways(pos.up(), this); // 1.1 仅斜铁轨 上方 } if (this.forbidCurves) { // 所有不能弯曲的铁轨 也就是除了普通铁轨 world.updateNeighborsAlways(pos, this); // 1.2 自身 world.updateNeighborsAlways(pos.down(), this); // 1.3 下方 } } }

铁轨在接受到NC更新时会检测自身充能状态。当满足以下两个条件之一时,它将会判断自己被激活powered=true

  1. 直接接收到红石信号,即被直接充能
  2. 由相连的其他铁轨充能,即被间接充能

铁轨检查自身是否被间接充能并不是检测相连铁轨是否被充能,而是递归寻找整条铁轨链上距离自身8个铁轨及以内的 (若认为直接相连的两个铁轨的距离为1个铁轨)、被直接充能的铁轨,过程如下:

  1. 初始时距离变量为0。若距离=8,则终止检查并返回False,即不被间接充能。
  2. 铁轨具有两个方向,当开始检查时,铁轨先检查哪一个方向由一个布尔值控制。当布尔值为True时,且当铁轨形状为南北/东西朝向时,它检查南/西侧铁轨;当铁轨形状是上下斜向shape=ascending_*时,它检查南侧上方/西侧上方的铁轨。当布尔值为False时反之。此布尔值在开始检查时先为True,后为False。且由于逻辑运算短路2:若第一次布尔值为True时检查成功,则不会进行第二次布尔值为False的检查。在递归检查过程中 (见步骤6) 此布尔值不会改变。
  3. 移动坐标(i, j, k)到接下来要检查的铁轨位置。
  4. 判断当前铁轨能否可能在当前检查方向上与当前方向的下方的一个铁轨相连。若当前铁轨为向东/西/南/北侧向上,且检查方向为东/西/南/北,则认为不能。此步骤是为步骤8准备的。例如:当前铁轨为北侧向上,此时在向南侧检查,则此铁轨可能与南侧下方的铁轨相连;当前铁轨为北侧向上,此时在向北侧检查,则此铁轨不可能与南侧下方的铁轨相连。
  5. 若当前铁轨形状为南侧向上/北侧向上或东侧向上/西侧向上,则认为当前铁轨形状为南北朝向或东西朝向——这一形状更改是为下一步骤 (6.b.) 准备的。
  6. 检查坐标(i, j, k)
    1. 检查当前位置是否是一个铁轨,如果不是,则返回False,结束当前步骤检查。
    2. 检查当前位置是否与原铁轨相连。此步骤并不检查“相连”,而是检查“不相连”:若原铁轨形状为东西朝向,则若当前铁轨形状为南北/北侧向上/南侧向上,则不相连,返回False,结束步骤6检查。若原铁轨为南北朝向,则若当前铁轨形状为东西/东侧向上/西侧向上,则不相连,返回False,结束当前步骤检查。
    3. 步骤i.ii.实际都是iii.iv.的前置条件。此步骤检查当前位置的铁轨是否被直接充能,如果是,则返回True,结束当前步骤检查。
    4. 跳至步骤1,距离增加1,并按照开始检查的布尔值进行下一个铁轨的检查 (递归)
  7. 若步骤6返回True,则终止检查并返回True
  8. (见步骤4判断) 若当前铁轨可能在当前检查方向上与当前方向的下方的一个铁轨相连,那么检查坐标(i, j, k)下方的方块,即跳至步骤6,但检查的坐标为(i, j-1, k)。在步骤4的例子中,若步骤3返回False,那么此时将检查原铁轨南侧下方的铁轨。
java// EXECUTABLE_BLOCK
// 这是起始点 @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; }

图

一个平铁轨可能与看似不相连的斜铁轨判断为相连。同样的结构若红石块直接充能平铁轨则斜铁轨不会被充能

当活塞推动一个亮起的侦测器到位、且到位位置无侦测器计划刻时,它不会对毗邻方块(除输出端指向的方块)发出常规意义上由活塞推动触发的NC更新(指通常情况下活塞推动一般方块到位后,会在方块到位位置发出由flags=3setBlockState触发的NC更新),原理如下:

  1. 到位分两种情况:
    1. 一般到位:调用PistonBlockEntity.tick,其中调用setBlockStateflags=67
    2. 强制到位:调用PistonBlockEntity.finish,其中调用setBlockStateflags=3
  2. setBlockState中调用worldChunk.setBlockState,然后调用onBlockAdded
  3. ObserverBlock.onBlockAdded中:
    1. 判断当前位置的旧方块是否为侦测器,由于旧方块是b36,判断true,下一步
    2. 判断自身是否激活且当前位置无侦测器计划刻,由于自身亮起powered=true且当前位置无侦测器计划刻,判断true,下一步
    3. setBlockStateflags=18,将自身激活状态变为powered=true -> powered=false,且发出PP更新
    4. 发出常规的侦测器的NC更新,即对输出端指向的方块及指向方块的毗邻方块发出NC更新
  4. 由于在步骤3.iii.中,侦测器powered属性改变,此时继续步骤1的setBlockState,但由于blockState2 == state即前后状态是否一致为falsesetBlockState不再发出常规的NC更新,而是直接结束

调用栈如下:

perl// EXECUTABLE_BLOCK
PistonBlockEntity.tick(...) | PistonBlockEntity.finish(...) └── world.setBlockState(pos, state, flags=67) | world.setBlockState(pos, state, flags=3) ├── // state: Block{minecraft:observer}[facing=..., powered=true] ├── worldChunk.setBlockState(pos, state, (flags & 64) != 0) │ ├── BlockState blockState = chunkSection.setBlockState(j, k, l, state); // Block{minecraft:moving_piston}[..] │ └── state.onBlockAdded(this.world, pos, blockState, moved) │ └── ObserverBlock.onBlockAdded(state, this.world, pos, oldState, moved) │ ├── if (!state.isOf(oldState.getBlock())): // true │ │ // state: Block{minecraft:observer}[...], oldState: Block{minecraft:moving_piston}[..] │ │ └── if (!world.isClient() && (Boolean)state.get(POWERED) && !world.getBlockTickScheduler().isQueued(pos, this)): // true │ │ ├── world.setBlockState(pos, state.with(powered=false), flags=18) │ │ │ ├── 状态变为 powered=false │ │ │ └── 发出PP更新 │ │ └── this.updateNeighbors(world, pos, blockState) // 对输出端指向及指向的毗邻发出NC更新 │ └── return ├── BlockState blockState2 = getBlockState(pos) // Block{minecraft:observer}[facing=..., powered=false] └── if (blockState2 == state): // false,powered不同 └── 跳过NC更新和PP更新,结束
  1. 曼哈顿距离,即两坐标在各坐标轴上的距离的和,在Minecraft的三维空间中有d=|x|+|y|+|z|

  2. 逻辑运算短路,计算机进行逻辑判断语句时的一种“熔断”机制。例如A and B中,若A不成立,则逻辑判断熔断,返回False,不再判断B是否成立;A or B中,若A成立,则逻辑判断熔断,返回True,不再判断B是否成立。