02 Pistons
1 Mechanics
Basic Section
- Piston composition
- Creating headless pistons
- Piston self-check trigger methods and redstone signal detection (QC quasi-connectivity)
- Structural analysis and common logic for pistons attempting to move blocks
- Piston update order and block event response
Advanced Section
- Source code calls for piston self-check and signal detection
- B36 conversion order and push/pull structure analysis
- Low-level implementation of block movement and hash table usage
- B36 placement and entity calculations
1.1 Piston Composition
A complete piston consists of two parts: the piston base and the piston head.
The piston base fully controls the piston head, and pistons have two states: retracted and extended. Normally, when a piston is activated, the piston base enters the extended state and the piston head appears. When the piston retracts, the piston head disappears. It is also possible to create a headless piston with only the piston base and no piston head, or conversely a piston with only a piston head and no piston base.
1.2 Headless Pistons
1.2.1 Creating Headless Pistons
There are essentially only two ways to create headless pistons:
- When the piston is extended and the piston head is in the b36 state, use TNT or other methods to remove the b36
- Through specific means, trigger a retraction event on an unextended piston. This will be explained in more detail later
1.2.2 Basic Mechanics of Headless Pistons
From the piston base's perspective, the piston head still exists directly in front of it. This belief persists regardless of whether a piston head actually exists there. If an unpowered headless piston receives an update, it will self-check and retract.
1.3 Piston Self-Check Mechanism
1.3.1 Trigger Methods
Pistons trigger self-checks if and only if the following two situations occur:
- Being placed (including being placed by a player or being pushed/pulled into position by another piston)
- Receiving an NC update
It must be emphasized that piston self-checks can occur at any stage. In other words, the self-check is instantaneous. When the above situations occur, the piston will immediately self-check and attempt to extend or retract through certain logic. This "attempt" does not guarantee success.
1.3.2 Redstone Signal Check and QC Powering
The essence of self-checking is to determine whether the piston needs to act, so it first checks whether it is powered.
The piston's signal check range includes:
- Whether all first-order adjacent blocks (except in the push direction) are outputting redstone signals
- Whether the first-order adjacent blocks centered on the block directly above are outputting redstone signals
Because pistons check the adjacent powering status of the block above them, they can be powered "remotely" through the space above. This powering cannot directly send an NC update to the piston, so an additional NC update is needed. This is the origin of Quasi-Connectivity (QC), and also why BUDs can only detect NC updates.
1.3.3 Self-Check Logic Explanation
When a piston self-checks, it always follows this behavior tree:
- Should extend but not extended: Create an analysis instance to attempt to analyze the push structure in front. If analysis fails (exceeds push limit, blocked by immovable blocks, etc.), no action is taken; if analysis succeeds, add an extend block event to its current position
- Should not extend but already extended: Piston needs to retract
- If there is a "moving piston" (Moving_Piston, also called B36 or block 36, hereafter b36) in front moving in the same direction that is extending and won't finish this tick (progress <50%), trigger instant retraction and add the corresponding block event
- Otherwise, execute normal retraction and add the corresponding block event.
1.4 Piston Push/Pull and Movable Blocks
1.4.1 Movable Block Determination
For a block to be pushable by a piston, it must simultaneously satisfy the following requirements:
- Within the world's valid height range
- Cannot be obsidian, crying obsidian, respawn anchor, reinforced deepslate, or other hardcoded immovable blocks
- Block hardness cannot be -1
- If it's glazed terracotta, the push direction must match the piston's push direction
- The block cannot have a block entity (such as chests, furnaces, hoppers)
1.4.2 Attempting to Move Blocks and Structure Analysis
When a piston attempts to push, it dynamically maintains two lists: the moved blocks list and the broken blocks list.
It will determine which blocks to push and which to break in a certain order. The piston only considers breakable blocks along the push axis; blocks attached to the side of the push structure will be updated and drop later rather than entering the broken list.
Tip: Due to the extreme complexity of structure analysis (alternating linear and branch analysis on the stack), manual analysis is impractical. It is strongly recommended to use Fallen_Breath's PistOrder mod to directly view the piston's attempted block placement order in-game.
1.5 Piston Update Order
The piston update order can be divided into five situations:
- Piston/sticky piston only extends
- Same as piston/sticky piston pushing blocks, just with empty moving blocks
- Piston/sticky piston only retracts
- Create piston base b36, send PP update and NC update
- Remove piston head, send PP update and NC update
- Piston/sticky piston pushes blocks
- Remove blocks at broken block positions
- Create b36 at moving target positions, send PP update
- Create piston head b36, send redstone dust prepare update and PP update
- Send redstone dust prepare update and NC update at broken block positions
- Send NC update at moving original positions
- Send NC update at piston head position
- Send PP update and NC update at piston base.
- Sticky piston retracts blocks
- Create piston base b36, send PP update and NC update
- Remove blocks at broken block positions
- Create b36 at moving target positions, send PP update
- Remove uncovered blocks, send redstone dust prepare update, PP update
- Send NC update at moving original positions
- Sticky piston retraction fails
- Create piston base b36 block, send PP update and NC update
Note that this behavior does not include any updates at the piston head, which is commonly referred to as sticky piston retraction failure with no updates. Here are a few examples:
1.6 B36 and Block Placement
1.6.1 Normal Placement
In the block tick phase of each game tick, b36 adds 0.5 to its push progress
- 0gt BE: Start pushing, create b36, progress is 0 at this time
- 0gt TE: Progress +0.5, perform entity displacement calculation
- 1gt TE: Progress +0.5, perform entity displacement calculation, total progress reaches 1.0
- 2gt TE: Discovers progress is already greater than or equal to 1.0, b36 becomes the original block, sends final PP and NC updates.
If we start observing from 0gt AT, the block placement is fully complete at 3gt AT, hence the term "3-gt piston delay"
1.6.2 Instant Placement
When a piston triggers instant retraction, blocks skip the normal TE phase accumulation and are forced to place immediately. Trigger conditions are:
- Retraction event, and the piston head position's current block is b36.
- Retraction event, sticky piston has b36 one block outside the extension direction and that b36 is moving in the same direction as the piston's push axis.
Here are two examples for demonstration:
Piston head position is b36
Let's look at the following example:
In this example, the red glass is instantly placed from an observational standpoint. Theoretically, we provide the following explanation:
The repeater has higher priority than the redstone torch, so the normal piston adds a block event first, then the headless sticky piston adds a block event, attempting to pull back the block 1 block further forward (at this point actionType=1)
However, in the BE phase, due to the insertion of b36, the sticky piston's pull back is blocked and will not destroy this b36. At this point, the piston head position is b36, triggering instant retraction, and the red glass is instantly placed.
Sticky piston has b36 one block outside extension direction and b36 movement direction matches piston movement direction
In the above example, the red glass is instantly placed because:
The piston pushes the slime stick, moving the red glass and sending an NC update, which triggers the headless sticky piston's self-check. At this point, the sticky piston can successfully pull back, and there is b36 one block outside the extension direction. That b36's push direction matches the sticky piston's push direction, so it is instantly placed.
Of course, note that this instant placement only affects one block: the one directly in front of the sticky piston. Other blocks will still be converted to b36. We can use this to explain a device that seems like magic to many people:
In this device, when the lever is pulled, the sticky piston moves the block on its side 1gt after the block is placed. At this point, the block is carried forward by the slime block, but because the sticky piston is activated by the observer, the slime block is placed early and pushed back by the piston. Since the block is still in the b36 state, it gets left behind in the position it was carried to, thus extending the block flow.
Instant placement directly sets the progress to 1.0, removes the b36 block entity, replaces it with the original block, and sends updates. Note that instant push does not include displacement push/pull calculations for surrounding entities.
Note that since Mojang didn't include any entity calculations or special handling for waterlogged blocks in this part, instant push won't affect entities and won't remove waterlogged status. However, if TE-phase entity calculations occurred previously, those calculations are preserved; only the current tick's TE-phase entity calculations are ignored.
++frontBlockCount; // Increase offset
}
After completing the pull analysis, the game proceeds to handle the push part. Starting from the block we initially analyzed, it searches forward along the movement direction for blocks to be pushed. If a traversed block is already in the push list, the linear structure ends there. Further traversal would collide with previously analyzed structures, at which point the push block list is rearranged.
Traverse the blocks that have been processed (depending on the addition order). If a block is sticky, enter branch structure checking to find blocks stuck to the sides. If side sticking causes push failure, return the push failure result.
If the block directly ahead is air, the linear structure ends. If the block directly ahead is an immovable block or the piston itself, push fails. If the block directly ahead is a destroyable block, add that block to the destroyable block list and the linear structure ends.
Add all blocks to the moved block list. Check the moved block list; if its size exceeds 12, the push limit is reached and push fails.
#### Attempting to Add Branches `tryMoveAdjacentBlock`
```java
private boolean tryMoveAdjacentBlock(BlockPos pos) {
// Get the block state at the specified position
BlockState targetBlockState = this.world.getBlockState(pos);
// Iterate through all directions
for (Direction direction : Direction.values()) {
// Only process directions with different axes from the push direction
if (direction.getAxis() != this.motionDirection.getAxis()) {
// Get the adjacent block
BlockPos adjacentBlockPos = pos.offset(direction);
BlockState adjacentBlockState = this.world.getBlockState(adjacentBlockPos);
// If current block and adjacent block are sticky blocks, try to move the adjacent block
if (isAdjacentBlockStuck(adjacentBlockState, targetBlockState) && !this.tryMove(adjacentBlockPos, direction)) {
return false; // If adjacent block cannot be moved, return false
}
}
}
// If all adjacent blocks are successfully moved, return true
return true;
}
The input parameter block here is always a sticky block, so the following logic is executed:
- Iterate through all surrounding blocks, implemented by iterating through directions, in the same order as NC updates
- Since analyzing the two adjacent blocks in the same direction as the push axis is unnecessary, they are excluded here. If a side block is mutually stuck with this block, attempt to analyze its linear structure. If linear structure addition fails, return failure result level by level.
1.6.3 Moved Block List Processing
movedBlocks and brokenBlocks reversed directly give the b36 arrival order and destruction order. We'll explain the reason in detail later.
1.6.4 Analysis of b36 Arrival Order
Let's analyze the following example:
The push axis in this diagram is +z towards -z direction
First analyze the linear structure. The two slime blocks in front of the piston head are added to the moved block list first.
Since both blocks are sticky, the slime block closest to the piston attempts to analyze the regular block in the -x direction and adds it to the push block list, then analyzes the slime block in the +y direction and adds it to the push block list. It will have a next call, but this call task is pushed onto the system stack. The second block in the linear structure is pushed onto the stack to analyze the slime block in the -x direction. This slime block finds it conflicts with the regular block behind it, so it handles the collision. The merged new list has the regular block at index 3 and the slime block at index 4. The task of checking surrounding blocks for the slime block is pushed onto the system stack, waiting for the next round of calls. Finally, by analogy, the push order of the entire structure can be obtained.
Of course, if you're feeling lazy, just use pistorder directly.
1.7 Piston Event Response and Moving Block Source Code
1.7.1 Unified Entry Point for Events onSyncedBlockEvent
1.7.2 Signal Detection Source Code shouldExtend
The piston checks the first-order adjacency of two core positions. The traversal order strictly checks the piston itself first, then the first-order adjacency of the upper core. The order of checking adjacency strictly follows the NC update order.
1.7.3 Attempt to Move Source Code tryMove
tryMove determines the type of block event (actionType), 0 for extend, 1 for retract, 2 for instant retract
}
// If it's not a sticky piston, directly remove the target block
else {
world.removeBlock(pos.offset(facing), false);
}
// Play retraction sound
world.playSound(null, pos, SoundEvents.BLOCK_PISTON_CONTRACT, SoundCategory.BLOCKS, 0.5F, world.random.nextFloat() * 0.15F + 0.6F);
world.emitGameEvent(GameEvent.BLOCK_DEACTIVATE, pos, Emitter.of(pistonHeadState));
}
// Return true to indicate successful event handling
return true;
}
**Server Side**
If not on the client side, several server-specific events are executed:
- Check activation status. If activated and the block event is retract or instant retract, set the piston to extended state, perform pathfinding update, and the block event execution fails.
- If not activated and the block event is extend, the block event execution fails.
**Extension Event**
> Note: This part must be executed on both client and server sides
The piston will attempt to move the blocks in front, implemented through the `move` method. I'll supplement the details of this method later. If movement fails, the piston event execution fails.
Next, set the piston to extended state. Here the `bitflag` is `0b01000011` for placement removal update, pathfinding update, and NC update.
Finally, play the piston extension sound. Block event execution succeeds.
**Instant Retraction and Retraction Event**
First, get the piston head's block entity. If it's b36, make it arrive instantly.
Next, set the piston to b36. Here the `bitflag` is `0b00010100`, no PP update, pathfinding update.
Then set the piston as b36 block entity, first emit NC update, then emit PP update.
If this piston is a sticky piston, at the position 1 block beyond where the piston head extends, if there is a b36 1 block beyond where it tries to place the piston head and that b36's push direction matches the piston's, make the block arrive instantly.
If the piston behavior is not instant retract, the block is not air, the piston can move the block 1 block in front, and this block can be normally pulled back or is a piston or sticky piston, then attempt to pull back the front block. If the block 1 block in front cannot be pulled, delete the block at the piston head position.
If it's a regular piston, directly delete the block at the piston head position.
In any case, play the piston retraction sound. Block event execution succeeds.
#### Piston Moving Blocks `move`
```java
// retract=0: retract
// retract=1: extend
private boolean move(World world, BlockPos pos, Direction dir, boolean retract) {
// Piston head position
BlockPos headPos = pos.offset(dir);
// If it's a retraction action and the target position is a piston head
if (!retract && world.getBlockState(headPos).isOf(Blocks.PISTON_HEAD)) {
// Set piston head to air
// flag=0b00010100, no PP update, pathfinding update, client
world.setBlockState(headPos, Blocks.AIR.getDefaultState(), 20);
}
// Create PistonHandler instance to calculate legality of push or pull operation
PistonHandler pistonHandler = new PistonHandler(world, pos, dir, retract);
// If push/pull operation cannot be executed, cannot move
if (!pistonHandler.calculatePush()) {
return false;
} else {
// Use hash table to store blocks to be moved and their states for subsequent processing
Map<BlockPos, BlockState> map = Maps.newHashMap();
// Create moved block list and get moved block list
List<BlockPos> blocksToMove = pistonHandler.getMovedBlocks();
// Used to record original states of blocks to be moved
List<BlockState> blocksOriginal = Lists.newArrayList();
// Iterate through blocks to be moved, save their original states to list and store in map
for (int i = 0; i < blocksToMove.size(); ++i) {
BlockPos blockPos2 = (BlockPos) blocksToMove.get(i);
BlockState blockState = world.getBlockState(blockPos2);
blocksOriginal.add(blockState);
map.put(blockPos2, blockState);
}
// Get list of all block positions to be destroyed
List<BlockPos> list3 = pistonHandler.getBrokenBlocks();
// Used to record states of destroyed blocks
BlockState[] blockStates = new BlockState[blocksToMove.size() + list3.size()];
// Determine movement direction
Direction direction = retract ? dir : dir.getOpposite();
// Process destroyed blocks, from back to front
int j = 0;
for (int k = list3.size() - 1; k >= 0; --k) {
BlockPos blockPos3 = (BlockPos) list3.get(k);
BlockState blockState2 = world.getBlockState(blockPos3);
// If block has block entity, handle drop logic first
BlockEntity blockEntity = blockState2.hasBlockEntity() ? world.getBlockEntity(blockPos3) : null;
dropStacks(blockState2, world, blockPos3, blockEntity);
// Set block to air and trigger destroy event
world.setBlockState(blockPos3, Blocks.AIR.getDefaultState(), 18);
world.emitGameEvent(GameEvent.BLOCK_DESTROY, blockPos3, Emitter.of(blockState2));
// If block is not a fire-type block, add destruction particle effect
if (!blockState2.isIn(BlockTags.FIRE)) {
world.addBlockBreakParticles(blockPos3, blockState2);
}
// Save destroyed block state
blockStates[j++] = blockState2;
}
// Process blocks to be moved
for (int k = blocksToMove.size() - 1; k >= 0; --k) {
BlockPos targetPos = (BlockPos) blocksToMove.get(k);
BlockState targetState = world.getBlockState(targetPos);
// Move to target position
targetPos = targetPos.offset(direction);
map.remove(targetPos);
// Set target position to MOVING_PISTON state for animation handling
BlockState blockState3 = (BlockState) Blocks.MOVING_PISTON.getDefaultState().with(FACING, dir);
world.setBlockState(targetPos, blockState3, 68);
// Create b36 block entity to control movement animation
world.addBlockEntity(PistonExtensionBlock.createBlockEntityPiston(targetPos, blockState3, (BlockState) blocksOriginal.get(k), dir, retract, false));
blockStates[j++] = targetState;
}
// If it's an extension operation, add piston head
if (retract) {
PistonType pistonType = this.sticky ? PistonType.STICKY : PistonType.DEFAULT;
BlockState blockState4 = (BlockState) ((BlockState) Blocks.PISTON_HEAD.getDefaultState().with(PistonHeadBlock.FACING, dir)).with(PistonHeadBlock.TYPE, pistonType);
BlockState targetState = (BlockState) ((BlockState) Blocks.MOVING_PISTON.getDefaultState().with(PistonExtensionBlock.FACING, dir)).with(PistonExtensionBlock.TYPE, this.sticky ? PistonType.STICKY : PistonType.DEFAULT);
map.remove(headPos);
// Set current piston position to MOVING_PISTON state
world.setBlockState(headPos, targetState, 68);
// Add b36 block entity to control animation
### [!ADVANCED] B36 Conversion and Structure Analysis Source Code
> Friendly reminder, this is one of the most complex parts of this chapter. If you prefer, you can use the pistOrder mod directly without studying this section
```java
public boolean calculatePush() {
// Clear the moved blocks and broken blocks lists to recalculate
this.movedBlocks.clear();
this.brokenBlocks.clear();
// Get the starting point that the piston will push
BlockState pistonHeadFrontState = this.world.getBlockState(this.posTo);
// If the starting point cannot be pushed
if (!PistonBlock.isMovable(pistonHeadFrontState, this.world, this.posTo, this.motionDirection, false, this.pistonDirection)) {
// If the block is immovable and currently in retracted state, and the block behavior is DESTROY, mark it for destruction
if (this.retracted && pistonHeadFrontState.getPistonBehavior() == PistonBehavior.DESTROY) {
this.brokenBlocks.add(this.posTo); // Add to broken list
return true; // Piston can still operate (by destroying blocks)
} else {
return false; // Piston cannot push and cannot destroy target block
}
}
// If the target block is movable but cannot add linear structure, cannot push
else if (!this.tryMove(this.posTo, this.motionDirection)) {
return false;
}
else {
// Traverse all blocks marked as needing to move
for (int i = 0; i < this.movedBlocks.size(); ++i) {
BlockPos posToValidate = (BlockPos) this.movedBlocks.get(i);
// If the current block is a sticky block, try to move its surrounding sticky blocks
if (isBlockSticky(this.world.getBlockState(posToValidate)) && !this.tryMoveAdjacentBlock(posToValidate)) {
return false; // If moving surrounding sticky blocks fails, overall push fails
}
}
return true; // All blocks successfully moved or processed
}
}
When a piston needs to push, it will first execute the above code, which is used to analyze the movement structure. More detailed descriptions are needed here to help readers understand: movedBlocks and brokenBlocks are actually two arrayLists. Readers unfamiliar with Java can think of them as lists that support arbitrary insertion and retrieval. This code essentially executes operations in the following order:
- Clear the
movedBlocksandbrokenBlockslists. - Check if the starting position can be pushed
- If it cannot be pushed, check its response to piston behavior
- If it can be destroyed, add the block to the
brokenBlockslist, push succeeds - If it cannot be destroyed, push fails
- If it can be destroyed, add the block to the
- If it can be pushed, try to add its linear structure (we will analyze how pistons analyze linear structures later)
- If the linear structure cannot be added, push fails
- If the linear structure is successfully added, traverse all blocks that need to be pushed
- If there are sticky blocks (slime blocks and honey blocks), try to add branches
- If addition fails, push fails
- If there are sticky blocks (slime blocks and honey blocks), try to add branches
- If none of the above failed, push succeeds
- If it cannot be pushed, check its response to piston behavior
Mojang implemented the analysis of linear structures and branch structures through two methods here, called tryMove1 and tryMoveAdjacentBlock.
1.7.4 Linear Structure Analysis tryMove
First, clarify when this method will be called:
- This block is directly pushed by a block
- This block is directly pulled by a sticky block
It's worth noting that these two methods are strictly independent of each other, meaning there is no overlap between the two situations.
When called for the first reason, this block itself must not be an immovable block (see section 5.4.2 above for details). When called for the second reason, if this block itself is immovable, there is no need to analyze it since the block trying to pull it is sticky (this is the situation shown in the figure below).
With that analysis complete, let's examine the first part of this method's code.
world.addBlockEntity(PistonExtensionBlock.createBlockEntityPiston(headPos, targetState, blockState4, dir, true, true));
}
// Set remaining blocks in map to air
BlockState blockState5 = Blocks.AIR.getDefaultState();
for (BlockPos blockPos4 : map.keySet()) {
world.setBlockState(blockPos4, blockState5, 82);
}
// Update neighbor block states
for (Map.Entry<BlockPos, BlockState> entry : map.entrySet()) {
BlockPos blockPos5 = entry.getKey();
BlockState blockState6 = entry.getValue();
blockState2.prepare(world, blockPos5, 2);
blockState5.updateNeighbors(world, blockPos5, 2);
blockState5.prepare(world, blockPos5, 2);
}
// Update neighbor states of destroyed blocks
for (int l = list3.size() - 1; l >= 0; --l) {
BlockState targetState = blockStates[j++];
BlockPos blockPos5 = (BlockPos) list3.get(l);
targetState.prepare(world, blockPos5, 2);
world.updateNeighborsAlways(blockPos5, targetState.getBlock());
}
// Update neighbor states of moved blocks
for (int l = blocksToMove.size() - 1; l >= 0; --l) {
world.updateNeighborsAlways((BlockPos) blocksToMove.get(l), blockStates[j++].getBlock());
}
// If it's a retraction action, update piston position neighbors
if (retract) {
world.updateNeighborsAlways(headPos, Blocks.PISTON_HEAD);
}
return true; // If execution succeeds, return true
}
}
When the piston attempts to move blocks, it distinguishes between two cases: extension and retraction. The `retract` boolean represents the piston's action. When false, the piston retracts; when true, the piston extends. The piston then handles these two actions separately.
When the piston **attempts** to retract, the game first checks whether the expected piston head position actually contains a piston head. Retraction only proceeds if a piston head is present. Each time retraction executes, the game first sets the piston head position to air (deleting the piston head). The `setBlockState` bitflag at this point is `0b00010100`, meaning no PP update and client pathfinding update.
Before starting movement, whether retracting or extending, the game calls the `calculatePush` method analyzed in section [5.4.2](#进阶542-移动结构分析) to verify that movement is possible. If movement cannot succeed, the push/pull operation fails.
The game then begins moving blocks. It first processes the destroyed block list (`brokenBlocks`) in reverse order. If a destroyed block has a block entity that drops items, the items are dropped, then the block is set to air. At this point, no PP update is sent, but a pathfinding update is emitted.
After destroying the necessary blocks, the game begins moving blocks. Traverse the moved block list (`movedBlocks`) from back to front, set each block to b36 with `bitflag` `0b01101000` (placement removal update), then add a b36 block entity and store it in the `blockStates` list for later use.
If extending, create b36 at the piston head position (placement removal update), then add a b36 block entity to the piston head position.
After completing the above operations, set all uncovered blocks to air (placement removal update and pathfinding update). No PP update is sent at this point. Updates are then emitted uniformly: for all blocks in the hash table, first emit a redstone dust prepare update, then a block update (at the block's original position). If extending, the piston head completes its block update.
The above code well explains why b36 addition is in reverse order to the `movedBlocks` list. Additionally, since the hash table is unordered, this also explains why some machines that depend on b36 arrival order break after unloading (the original index is lost).
### [!ADVANCED] Arrival Source Code Analysis
#### Normal Arrival `tick`
> This is straightforward, so the detailed explanation was provided earlier. Therefore, only code is provided here for reference
```java
public static void tick(World world, BlockPos pos, BlockState state, PistonBlockEntity blockEntity) {
// Record current gt
blockEntity.savedWorldTime = world.getTime();
blockEntity.lastProgress = blockEntity.progress;
// If push is completed
if (blockEntity.lastProgress >= 1.0F) {
// If it's client side and current deathTick hasn't exceeded 5
if (world.isClient && deathTicks < 5) {
// Increase deathTicks count
++deathTicks;
} else {
// Remove block entity
world.removeBlockEntity(pos);
blockEntity.markRemoved();
// If this block is b36
if (world.getBlockState(pos).isOf(Blocks.MOVING_PISTON)) {
// Give PP update
BlockState blockState = Block.postProcessState(blockEntity.pushedBlock, world, pos);
// If it's air
if (blockState.isAir()) {
// Set to corresponding block, no update, bitflag=0b01010100;
world.setBlockState(pos, blockEntity.pushedBlock, 84);
// Update or destroy this block
Block.replace(blockEntity.pushedBlock, blockState, world, pos, 3);
} else {
// If it's a waterlogged block
if (blockState.contains(Properties.WATERLOGGED) && (Boolean)blockState.get(Properties.WATERLOGGED)) {
// Remove water from waterlogged block
blockState = (BlockState)blockState.with(Properties.WATERLOGGED, false);
}
// Set to corresponding block, give NC update, PP update, client update, bitflag=0b01000011
world.setBlockState(pos, blockState, 67);
// Receive NC update itself
world.updateNeighbor(pos, blockState.getBlock(), pos);
}
}
}
}
// If push is not completed
// Push progress +0.5
else {
float f = blockEntity.progress + 0.5F;
// Perform entity calculations
pushEntities(world, pos, f, blockEntity);
moveEntitiesInHoneyBlock(world, pos, f, blockEntity);
blockEntity.progress = f;
// If push progress >=1, set to 1
if (blockEntity.progress >= 1.0F) {
blockEntity.progress = 1.0F;
}
}
}
1.7.5 Instant Arrival finish
The logic here is as follows:
- If the piston is trying to push air, then it can push
- If this block is immovable, then it can push (see the justification above)
- If this block is the piston itself, then it can push
- If this block is already being pushed, then it can push
Next, the game defines a variable offset, which represents the block the piston is trying to push. If the movedBlocks list size plus 1 already exceeds the push limit, the push fails (adding this block would exceed the pushable size limit).
The game starts from this block and recursively pushes blocks in the opposite direction of the push direction, looking for where the linear pull is interrupted. When this position is air, a block that doesn't stick to this sticky block, an immovable block, or the piston itself, this row of blocks is the part pulled by the sticky block (the purpose of this part is to handle the following situation:)
If the newly found linear structure's pull part plus the original movedBlocks list size already exceeds 12, the push fails.
Next, add this pulled part to the moved blocks list in the reverse direction of the push direction: the frontmost block is added first, the rearmost block is added last.
Most of the content has been explained before, so we'll skip ahead here. We only look at the b36 special case in the latter half.
In 1.20, only the piston arm's source attribute is true; all others are false. Therefore, when this method is called, if the position is a b36 with the target block as a piston arm, it will directly become air. The rest won't be detailed, as it's all been covered before.
1.8 Piston Head
This chapter actually isn't difficult, but since apart from the device demonstrated by Menggui233 in PetrisAFE's video recently, no other uses have been seen, it's placed in the advanced section here.
We first observe the following device:
Now, pull down the lever
Observe that the piston base was replaced with an observer, while the piston head was retained. Why?
We first review what happens when b36 arrives:
- Give PP update at its own position
- Give NC and PP updates to first-order neighbors at its own position
- Give NC update at its own position
Therefore, we separately examine what happens when the piston head receives PP update and NC update
1.8.1 Piston Head Existence Condition Determination canSurvive
The isFittingBase method essentially states that a piston head is fitting if and only if the block behind it (more precisely, 1 block in the opposite direction of the push direction, hereafter referred to as behind) is an extending piston with the same extension direction. The canSurvive method requires that a piston head's existence is valid if and only if the piston head is fitting, or there is a b36 behind it with the same push direction.
1.8.2 Piston Head Removal onRemove
One sentence summary: When removed, if the state is fitting, the base is removed along with it and the piston drops.
1.8.3 Piston Head Receiving PP Update and NC Update
PP Update
The piston head actually only cares about PP updates from behind. When a PP update occurs behind, the piston head calls canSurvive to check if it's valid. If invalid, it disappears immediately.
NC Update
In any case, the piston head will not disappear due to receiving an NC update. When receiving an NC update, if it's valid, the update is treated as an NC update received by the block behind.
1.8.4 Case Analysis
Therefore, for the case mentioned at the beginning of the chapter:
- 0gt AT: Player pulls down lever, trapdoor triggers
- 2gt BE depth 0: Sticky piston is activated, updates headless piston
- 2gt BE depth 1: Headless piston eats sticky piston base, slime block & lit observer are pushed
- 4gt TE: Sticky piston head arrives first, checks that behind is b36 in the same direction as itself, self-check passes. Then lit observer b36 arrives, no update.
Therefore, the observer successfully replaced the original piston base.
1.9 b36-Related Entity Calculations
After completing the pull part analysis, the game will continue to process the push part. Starting from the block we initially analyzed, search forward along the movement direction for blocks that will be pushed. If the traversed block is already in the push list, it means this linear structure ends. Further traversal would collide with the previously analyzed structure, at which point the push block list is rearranged.
Traverse the blocks that have been completed (depending on the addition order). If it is a sticky block, enter branch structure checking to find blocks stuck to the side. If side sticking causes push failure, return push failure result.
If the block directly in front is air, the linear structure ends. If the block directly in front is an immovable block or the piston itself, push fails. If the block directly in front is a destroyable block, add that block to the destroyable blocks list, linear structure ends.
Add all blocks to the moved blocks list. Check the moved blocks list; if its size is greater than 12, the push limit is reached, push fails.
1.9.1 Attempt to Add Branch tryMoveAdjacentBlock
// If there are entities in the scan area, start processing
if (!list.isEmpty()) {
// Get the list of all sub-bounding boxes that make up the b36 voxel model
List<Box> list2 = voxelShape.getBoundingBoxes();
// Check if b36 is a slime block
boolean bl = blockEntity.pushedBlockState.isOf(Blocks.SLIME_BLOCK);
Iterator var12 = list.iterator();
// Iterate through all entities that may be affected
while (true) {
Entity entity;
while (true) {
if (!var12.hasNext()) {
return;
}
entity = (Entity)var12.next();
// Check if entity ignores b36 pushing
if (entity.getPistonBehavior() != PistonBehavior.IGNORE) {
if (!bl) {
break;
}
// If slime block is pushing, for entities other than server players, force their velocity component along the movement axis to 1.0
if (!(entity instanceof ServerPlayerEntity)) {
Vec3d vec3d = entity.getVelocity();
double e = vec3d.x;
double g = vec3d.y;
double h = vec3d.z;
switch (direction.getAxis()) {
case X:
e = direction.getOffsetX();
break;
case Y:
g = direction.getOffsetY();
break;
case Z:
h = direction.getOffsetZ();
}
entity.setVelocity(e, g, h);
break;
}
}
}
// Record the maximum overlap distance between entity and b36 in the movement direction
double i = 0.0;
// Iterate through each sub-bounding box of the b36 model
for (Box box2 : list2) {
// Check if the movement path of b36's sub-bounding box intersects with the entity's collision box
Box box3 = Boxes.stretch(offsetHeadBox(pos, box2, blockEntity), direction, d);
Box box4 = entity.getBoundingBox();
// If they intersect, calculate the overlap distance
if (box3.intersects(box4)) {
i = Math.max(i, getIntersectionSize(box3, direction, box4));
// Overlap distance is greater than actual push distance, this is the worst case, no need for extra checks
if (i >= d) {
break;
}
}
}
// If the final calculated maximum overlap distance is greater than 0, perform push
if (!(i <= 0.0)) {
// Determine the final push distance. This distance cannot exceed b36's own movement distance at this moment
// And add an extra 0.01 to prevent entity from getting stuck in b36 next tick
i = Math.min(i, d) + 0.01;
// Along the movement direction, push the entity the calculated distance
moveEntity(direction, entity, i, direction);
// If b36 is retracting, specially handle entities affected by it
if (!blockEntity.extending && blockEntity.source) {
push(pos, entity, direction, d);
}
}
}
}
}
}
// This method is used to solve the problem of entities getting stuck inside b36. It checks if the entity overlaps with the block at b36's position, and if so, calculates the minimum distance to push the entity out in the opposite direction. After a precise overlap depth comparison, it calibrates and executes this displacement, ensuring the entity is smoothly pushed away from b36.
private static void push(BlockPos pos, Entity entity, Direction direction, double amount) {
// Get the entity's bounding box
Box box = entity.getBoundingBox();
// Create a standard-sized (1x1x1) block bounding box at b36's position
Box box2 = VoxelShapes.fullCube().getBoundingBox().offset(pos);
// If the entity's bounding box doesn't overlap with the block's bounding box, the entity is not stuck, return directly
The input parameter block here is always sticky, so the following logic executes:
- Traverse all surrounding blocks by iterating through directions, in the same order as NC updates
- Since analyzing the two adjacent blocks in the same direction as the push axis is unnecessary, they are excluded here. If a side block is mutually stuck with this block, attempt to analyze its linear structure. If linear structure addition fails, return failure result recursively.
1.9.2 Moved Blocks List Processing
movedBlocks and brokenBlocks reversed directly give the b36 placement order and destruction order. The reason will be explained in detail later.
1.9.3 B36 Placement Order Analysis
Let's analyze the following example:
The push axis in this diagram is from +z to -z.
First, analyze the linear structure: the two slime blocks in front of the piston head are added to the moved blocks list first.
Since both blocks are sticky, the slime block closest to the piston attempts to analyze the regular block in the -x direction and adds it to the push block list, then analyzes the slime block in the +y direction and adds it to the push block list. It will have a next call, but this call task is pushed onto the system stack. The second block in the linear structure is pushed onto the stack to analyze the slime block in the -x direction. This slime block finds it conflicts with the regular block behind it, so it handles the collision. The merged list has the regular block at index 3 and the slime block at index 4. The task of checking surrounding blocks for the slime block is pushed onto the system stack, waiting for the next round of calls. Finally, by this process, the push order of the entire structure can be obtained.
Of course, if you're feeling lazy, just use pistorder directly.
if (box.intersects(box2)) {
// Get the opposite direction of the original push
Direction direction2 = direction.getOpposite();
// Calculate the minimum push-out distance needed to free the entity from the block
double d = getIntersectionSize(box2, direction2, box) + 0.01;
// Calculate the depth of the actual overlap area between entity and block
double e = getIntersectionSize(box2, direction2, box.intersection(box2)) + 0.01;
// If the two calculated push-out distances are almost equal (indicating the entity is only slightly touching), perform position calibration (float align)
if (Math.abs(d - e) < 0.01) {
d = Math.min(d, amount) + 0.01;
// Along the opposite direction, push the entity the calculated distance
moveEntity(direction, entity, d, direction2);
}
}
}
// This method implements the sticky effect of moving honey blocks on entities above, and only works when b36 pushes horizontally. Its core logic is: define a "sticky area" above the honey block surface, then synchronously move all entities that meet the conditions in that area with the honey block b36.
private static void moveEntitiesInHoneyBlock(World world, BlockPos pos, float f, PistonBlockEntity blockEntity) {
// The entire logic only triggers when b36 is a honey block
if (blockEntity.isPushingHoneyBlock()) {
// Get the direction b36 is moving
Direction direction = blockEntity.getMovementDirection();
// Honey block's sticky effect only works when pushing horizontally
if (direction.getAxis().isHorizontal()) {
// Get the actual top height of the honey block b36's collision box
double d = blockEntity.pushedBlockState.getCollisionShape(world, pos).getMax(Direction.Axis.Y);
// Define a sticky area above the honey block surface (from its top to Y=1.5), and make it move synchronously with b36
Box box = offsetHeadBox(pos, new Box(0.0, d, 0.0, 1.0, 1.5000010000000001, 1.0), blockEntity);
// Calculate the distance the honey block b36 needs to move in the current tick
double e = f - blockEntity.progress;
// Iterate through all entities in the sticky area that meet movement conditions
for (Entity entity : world.getOtherEntities((Entity)null, box, entityx -> canMoveEntity(box, entityx, pos))) {
// Along the movement direction, push all entities that meet conditions the calculated distance
moveEntity(direction, entity, e, direction);
}
}
}
}
**`pushEntities`: Handling Piston Pushing**
This method is the core of piston-entity interaction. It gets the movement direction and distance of b36 in the current game tick, and finds all entities that may contact along the path. Then, it precisely calculates the maximum overlap of each entity with b36 in the movement direction, and pushes the entity accordingly (while adding a tiny displacement of 0.01 to prevent getting stuck). This process also specially handles the bounce effect of slime blocks and the effect of b36 retraction on entities.
Its workflow is as follows:
1. Determine movement parameters: First, get the piston head's movement direction and precise distance in the current game tick.
2. Roughly filter entities: By calculating the space swept by the piston head in this frame, quickly find all entities that may be pushed.
3. Precisely calculate overlap: Iterate through each entity that may be affected, and compare its collision box with each sub-collision box of the piston head model. It precisely calculates the maximum overlap distance between the entity and piston head in the piston's movement direction.
4. Execute push: Based on the calculated maximum overlap distance, apply a displacement to the entity. To prevent the entity from getting stuck in the next frame, the push distance is increased by a tiny value (0.01).
5. Special case handling:
- Slime blocks: If the pushed block is a slime block, it produces a bounce effect on non-player entities, setting their velocity in the movement direction to a fixed value.
- Piston retraction: If the piston is retracting, it calls the push method to specially handle entities that may be pulled, ensuring they don't get stuck in the space after the piston arm disappears.
**`push`: Solving Entity Stuck Problem**
This method is a helper function specifically used to solve the problem of entities accidentally getting stuck in the block where the piston arm is located.
Its workflow is as follows:
1. Check overlap: First, check if the entity's collision box overlaps with the block collision box at the piston arm's target position. If there's no overlap, the entity is not stuck, and the method ends directly.
2. Calculate push-out distance: If overlap is detected, it calculates how far the entity needs to be pushed in the opposite direction to just separate. It performs two calculations and comparisons to ensure the push-out distance is precise and minimal, avoiding excessive displacement.
3. Execute displacement: Finally, it pushes the entity the calculated distance in the opposite direction of piston movement, smoothly solving the stuck problem.
**`moveEntitiesInHoneyBlock`: Implementing Honey Block Sticky Effect**
This method is specifically used to implement the "sticky" effect of moving honey blocks on entities above.
Its workflow is as follows:
1. Condition check: This function only triggers when the piston is pushing a honey block and moving horizontally.
2. Define sticky area: It defines a specific "sticky area" above the moving honey block surface (from the honey block top to Y=1.5 height).
3. Synchronous movement: The system finds all entities located in this sticky area.
4. Apply displacement: Finally, move these entities together with the honey block in the same direction for the same distance, achieving the effect of entities being "stuck" on the honey block moving together.
### [!ADVANCED] Headless Piston Creation Method Based on `Extended=False` Piston Retraction Event
Let's first recall the `onSyncedBlockEvent` mentioned earlier. Note that when the server executes this method and returns false, it doesn't send a packet to the client, so the client won't execute any animation or turn anything into b36 (that is, the client won't execute this method).
The `flag` input parameter of `setBlockState` mentioned earlier is only for pathfinding updates in the usual sense. A more detailed explanation is needed here.
We observe the following phenomenon:
1. Place a piston of any direction and any type. Place a redstone block at a first-order neighbor position (except the piston's facing direction). This step will activate the piston.
2. `/tick freeze`
3. Remove the redstone block
4. Replace the original piston with a piston of the same type in any direction
5. Place back the redstone block. The position requirement is the same as in step 1.
### [!ADVANCED] Piston Event Response and Moving Blocks Source Code
#### Unified Event Entry `onSyncedBlockEvent`
```java
public boolean onSyncedBlockEvent(BlockState state, World world, BlockPos pos, int actionType, int directionData) {
// Get the block's direction (e.g., the direction the piston faces)
Direction facing = (Direction)state.get(FACING);
// Get the activated block state (indicates the block is extending)
BlockState extendedState = (BlockState)state.with(EXTENDED, true);
// If not on the client side, perform some server-side operations
if (!world.isClient) {
// Check if the piston should extend (if there is a redstone signal)
boolean shouldExtend = this.shouldExtend(world, pos, facing);
// If should extend and event type is retract, update block state and return
if (shouldExtend && (actionType == 1 || actionType == 2)) {
world.setBlockState(pos, extendedState, 2);
return false;
}
// If should not extend and event type is 0, directly return false
if (!shouldExtend && actionType == 0) {
return false;
}
}
// Event type 0: piston extension event
if (actionType == 0) {
// If moving blocks fails, return false
if (!this.move(world, pos, facing, true)) {
return false;
}
// Set block to extended state and play extension sound
world.setBlockState(pos, extendedState, 67);
world.playSound((PlayerEntity)null, pos, SoundEvents.BLOCK_PISTON_EXTEND, SoundCategory.BLOCKS, 0.5F, world.random.nextFloat() * 0.25F + 0.6F);
world.emitGameEvent(GameEvent.BLOCK_ACTIVATE, pos, Emitter.of(extendedState));
}
// Event type 1 or 2: piston retraction event
else if (actionType == 1 || actionType == 2) {
// Get the block entity in front of the piston
BlockEntity blockEntityInFront = world.getBlockEntity(pos.offset(facing));
// If there is a piston block entity at that position, complete the retraction operation
if (blockEntityInFront instanceof PistonBlockEntity) {
((PistonBlockEntity)blockEntityInFront).finish();
}
// Create moving piston block state
BlockState pistonHeadState = Blocks.MOVING_PISTON.getDefaultState().with(PistonExtensionBlock.FACING, direction)
.with(PistonExtensionBlock.TYPE, this.sticky ? PistonType.STICKY : PistonType.DEFAULT);
// Set new piston block state and create new block entity
world.setBlockState(pos, pistonHeadState, 20);
world.addBlockEntity(PistonExtensionBlock.createBlockEntityPiston(pos, pistonHeadState, (BlockState)this.getDefaultState().with(FACING, Direction.byId(directionData & 7)), direction, false, true));
world.updateNeighbors(pos, pistonHeadState.getBlock());
pistonHeadState.updateNeighbors(world, pos, 2);
// If it's a sticky piston, execute special logic
if (this.sticky) {
// Calculate target position
BlockPos targetpos = pos.add(facing.getOffsetX() * 2, facing.getOffsetY() * 2, facing.getOffsetZ() * 2);
BlockState targetState = world.getBlockState(targetpos);
boolean hasMovingPistonInFront = false;
// If there is already a moving piston block at the target position, try to complete its extension
if (targetState.isOf(Blocks.MOVING_PISTON)) {
BlockEntity blockEntity2 = world.getBlockEntity(targetpos);
if (blockEntity2 instanceof PistonBlockEntity) {
PistonBlockEntity pistonBlockEntity = (PistonBlockEntity)blockEntity2;
if (pistonBlockEntity.getFacing() == facing && pistonBlockEntity.isExtending()) {
pistonBlockEntity.finish();
hasMovingPistonInFront = true;
}
}
}
// If no special case, remove target block or continue moving
if (!hasMovingPistonInFront) {
if (actionType != 1 || targetState.isAir() || !isMovable(targetState, world, targetpos, facing.getOpposite(), false, direction)
|| targetState.getPistonBehavior() != PistonBehavior.NORMAL && !targetState.isOf(Blocks.PISTON) && !targetState.isOf(Blocks.STICKY_PISTON)) {
world.removeBlock(pos.offset(facing), false);
} else {
this.move(world, pos, facing, false);
}
}
}
// If not a sticky piston, directly remove target block
else {
world.removeBlock(pos.offset(facing), false);
}
// Play retraction sound
world.playSound(null, pos, SoundEvents.BLOCK_PISTON_CONTRACT, SoundCategory.BLOCKS, 0.5F, world.random.nextFloat() * 0.15F + 0.6F);
world.emitGameEvent(GameEvent.BLOCK_DEACTIVATE, pos, Emitter.of(pistonHeadState));
}
// Return true indicating event was successfully processed
return true;
}
- Place an immovable block in the piston's facing direction
- Unfreeze with
/tick freeze
If the operation is completely error-free, this piston becomes a headless piston.
This is because in onSyncedBlockEvent:
Block events only verify block type and position when executing, not the block's facing direction. Therefore, at this piston's position, we get a piston with Extended=False state that can pass verification and execute, and this piston will execute a retraction event. On the server side, the piston is verified whether it should extend. At this point, the piston should indeed extend because it's activated by the redstone block, so it enters the first if branch condition. This branch condition first calls setBlockState, and let's see what setBlockState does when flag=2:
We find that in the branch call if ((flags & 2) != 0 && (!this.isClient || (flags & 4) == 0) && (this.isClient || worldChunk.getLevelType() != null && worldChunk.getLevelType().isAfter(ChunkLevelType.BLOCK_TICKING))), a method updateListeners is called. We'll find this updateListeners has two implementations:
ServerWorldClientWorld
In ServerWorld, this is just a normal pathfinding update. The pathfinding update here assumes the piston arm exists, but that's beside the point.
In ClientWorld, this updateListeners is responsible for rendering. Therefore, the client jumps ahead here, directly rendering the extended piston arm without rendering the piston head.
Next is the second extension event caused by piston self-check. This extension event will fail directly when executed. Whether client or server, no further explanation is needed here.
Therefore, a headless piston is created this way.
1.10 Case Analysis
1.10.1 Case 1
What happens when you hit the note block?
Piston drops
The sticky piston receives NC update and self-checks. Calls trymove, adds retract block event, calls onSyncedBlockEvent. Performs the following judgment:
After some judgment, enters the move method.
Since the target position is a piston head and it's a retraction action, set the piston head position to air, triggering the piston head's onRemove. The piston head drops along with the piston base.
1.10.2 Case 2
What happens when you hit the note block?
Sticky piston returns to complete state, nothing else happens
Steps are as described in the previous example, but here after entering move, it doesn't meet the condition for removing the piston head. So it proceeds to the next step
Since the structure in front cannot be pulled back, it directly return false, nothing happens.
1.10.3 Case 3
What happens when you hit the note block?
Regular block is removed
Back to onSyncedBlockEvent, still this judgment:
But here, the if branch is true (obsidian is immovable, !isMovable is true), so it directly removes the block in front of the piston, and the regular block is removed.
1.10.4 Case 4
What happens when you hit the note block?
Nothing happens
Same as case 2, enters the move method. When the piston tries to calculate if it can pull back, it checks itself, and the activated piston base itself is immovable, so return false, nothing happens.
1.10.5 Case 5
What happens when you pull down the lever?
Front piston becomes headless piston, back piston returns to normal piston
Still examining onSyncedBlockEvent, notice this code here:
When pulling down the lever, the front piston first creates a b36 at the position in front (note the piston base hasn't become b36 yet), then executes the above operation, calling the finish method of the previous piston's piston arm. As mentioned before, this method has special handling for piston arm b36, directly turning it to air. Therefore, the previous piston's piston arm is removed, becoming a headless piston. The sticky piston at the back position retracts, becoming a piston with Extended=False.
1.10.6 Case 6
What happens when you pull down the lever?
Two pistons swap heads
The principle is similar to case 5, but the code called is here:
1.10.7 Piston Moving Blocks move
If there's b36 in front, directly call the finish method to turn it to air.
1.10.8 Case 7
What happens when you pull down the lever?
Redstone block disappears, leftmost sticky piston drops block
Pull down the lever, the block in front of the headless piston is b36 and is updated. The redstone block arrives instantly, causing NC update, updating to the repeater, adding TT event
But note, onSyncedBlockEvent hasn't returned here yet, continues executing the following operation:
So the redstone block is removed. Emits NC update. Adds TT event.
The repeater lights up for 2gt, so the leftmost sticky piston drops the block.
2 Applications
2.1 Duplication
2.1.1 Lit Observer Removal Calculation
When a lit observer is removed, it emits NC update backward and extinguishes itself.
2.1.2 Observer-Based Duplication
The essence of duplication is actually that this block is added to the moved list, but at this point the block hasn't been converted to b36 yet. I insert an update in a lightning-fast manner, and this update causes something to be activated/dropped, but because this block itself has already rolled into the moved block list, when it arrives it's placed down again in the form of b36.
We divide duplication into two types: observer-based and attachment block-based. Observer-based duplication precisely utilizes the fact that as long as the observer is removed (replaced with b36), it will instantly produce an NC update.
Here's observer-based rail duplication:
In the moved block list, when the sticky piston extends, it first moves the left observer, creates b36 at the target position, then moves the slime block, covering the lit observer, update, the rail finds that below is actually b36, drops. Move the rail, create b36 at the target position.
This way we complete one duplication
2.1.3 Attachment Block Breaking-Based Duplication
2.1.3.1 BUD-Based TNT Duplication
Attachment block dropping also produces updates, but since attachment blocks usually drop directly and are difficult to reuse. For example:
2.1.3.2 Dust-Directed Minimalist TNT Duplication
2.1.3.3 Carpet Duplication
The figure below shows a very popular carpet duplication:
When the piston retracts, it completes one duplication. We'll analyze it briefly here:
// If it's an extend operation, add piston head
if (retract) {
PistonType pistonType = this.sticky ? PistonType.STICKY : PistonType.DEFAULT;
BlockState blockState4 = (BlockState) ((BlockState) Blocks.PISTON_HEAD.getDefaultState().with(PistonHeadBlock.FACING, dir)).with(PistonHeadBlock.TYPE, pistonType);
BlockState targetState = (BlockState) ((BlockState) Blocks.MOVING_PISTON.getDefaultState().with(PistonExtensionBlock.FACING, dir)).with(PistonExtensionBlock.TYPE, this.sticky ? PistonType.STICKY : PistonType.DEFAULT);
map.remove(headPos);
// Set current piston position to MOVING_PISTON state
world.setBlockState(headPos, targetState, 68);
// Add b36 block entity to control animation
world.addBlockEntity(PistonExtensionBlock.createBlockEntityPiston(headPos, targetState, blockState4, dir, true, true));
}
// Set remaining blocks in map to air
BlockState blockState5 = Blocks.AIR.getDefaultState();
for (BlockPos blockPos4 : map.keySet()) {
world.setBlockState(blockPos4, blockState5, 82);
}
// Update neighbor block states
for (Map.Entry<BlockPos, BlockState> entry : map.entrySet()) {
BlockPos blockPos5 = entry.getKey();
BlockState blockState6 = entry.getValue();
blockState2.prepare(world, blockPos5, 2);
blockState5.updateNeighbors(world, blockPos5, 2);
blockState5.prepare(world, blockPos5, 2);
}
// Update destroyed blocks' neighbor states
for (int l = list3.size() - 1; l >= 0; --l) {
BlockState targetState = blockStates[j++];
BlockPos blockPos5 = (BlockPos) list3.get(l);
targetState.prepare(world, blockPos5, 2);
world.updateNeighborsAlways(blockPos5, targetState.getBlock());
}
// Update moved blocks' neighbor states
for (int l = blocksToMove.size() - 1; l >= 0; --l) {
world.updateNeighborsAlways((BlockPos) blocksToMove.get(l), blockStates[j++].getBlock());
}
// If it's a retraction action, update piston position neighbors
if (retract) {
world.updateNeighborsAlways(headPos, Blocks.PISTON_HEAD);
}
return true; // If execution succeeds, return true
}
}
When a piston attempts to move blocks, it distinguishes between two situations: extend and retract. The `retract` boolean value here represents the piston's action. When its value is `false`, the piston retracts; when its value is `true`, the piston extends. Next, the piston will distinguish between the two piston actions and process them separately.
When the piston **attempts** to retract, the game will first check whether the position that **should be** the piston head is actually a piston head. Only if it is a piston head will retraction be executed. Each time retraction is executed, the game will first set the piston head position to air (delete the piston head). The `setBlockState` `bitflag` passed in at this time is `0b00010100`, i.e., no PP update and client pathfinding update.
Before formally starting to move, whether retracting or extending, the game will call the `calculatePush` method analyzed in section [5.4.2](#进阶542-移动结构分析) to analyze again whether it can move. If it cannot successfully move, the piston fails to execute push/pull.
Next, the game will formally start moving blocks. It will first process the broken blocks list (`brokenBlocks`) in reverse order. If the destroyed block has a block entity that drops items, drop the items, then set the anchored destroyed block (i.e., the block currently being processed) to air. At this time, no PP update, send pathfinding update.
After destroying the blocks that should be destroyed, the game will start moving blocks. Traverse the moved blocks list (`movedBlocks`) from back to front, set that block to b36, `bitflag` is `0b01101000`, i.e., placement removal update, then add b36 block entity to that block and add to the `blockStates` list for backup.
If extending, create b36 at the piston head position, placement removal update, then add b36 block entity to the piston head position.
After completing the above operations, set all uncovered blocks to air with placement removal update and pathfinding update. No PP update at this time. Then uniformly send updates. For all blocks in the hash table, first uniformly send redstone dust prepare update, then send block update (at the block's original position). If extending, the piston head completes block update.
The above code well explains why b36 addition is in reverse order with the `movedBlocks` list. Also, because the hash table is unordered, this also explains why some machines that depend on b36 placement order break after unloading. (After unloading, the original index is lost)
Moved block list:
1. Move the bottom slime block, create b36 at target position
2. Move the dead coral fan, create b36 at target position, emit PP update, self-check finds it should drop, emit PP update and NC update, guide the carpet above to drop in chain
3. Move remaining slime blocks and carpet
This completes the duplication.
### Block Events with Inconsistent Information
According to sections 5.1, 2.1.5, etc., we can find: when the piston executes a block event, it only verifies its `block position` and `block type`. Although `piston facing` is recorded, it only governs the direction of b36.
The `actual direction` when the piston executes the block event is sampled in real-time.
This means that before a piston's block event instance is executed in the block event phase, there exists a time window: at this point, we can freely change the properties of the piston at the corresponding position, or even destroy or replace this piston itself.

As shown in the figure, from the EU of the previous game tick to the BE of the current game tick, during this time we can modify the target object of a certain block event.
Similarly, from the EU of the current game tick to the BE of the next game tick, such a time window also exists.
But as long as when executing this piston's block event instance, if there's a piston at the execution position and the piston type hasn't changed, then the direction the piston attempts to push/pull will be determined by the facing at execution time, even though this facing may not be the facing when the block event was added.
> Interestingly, the animation when b36 retracts still follows the data parameter passed in the block event,
> so, even though the piston facing at block event execution differs from when it was added, the piston head animation will still face the piston facing when the block event was added (but the collision box is accurate).
> 
#### Breaking Bedrock
##### Cross-Game-Tick Bedrock Breaking
> The Piston's Revenge Journey
As mentioned before, we can sneakily modify the piston's properties, including facing, before the piston's block event phase.
This means we can change its facing to face bedrock when the piston extends, and then when the piston retracts, it will destroy the bedrock as if it were a piston head.
The demonstration animation below is a typical example of breaking bedrock by changing piston facing before the block event phase.

During the entire process, the following important events occurred
CECECE{gt0}-EU phase
- Left TNT explodes.
- Explosion destroys lever.
- Explosion destroys cobblestone.
- Right TNT explodes.
- Explosion destroys piston head, emits NC update.
- Piston base is updated. Since the lever has been blown up, the piston adds a **retract** block event to itself.
> Unfortunately, it has already missed the BE phase of this game tick, so it can only wait silently for the BE phase of gt1...
- Piston head destroys piston base.
CECECE{gt0}-AT phase
- Player places downward-facing new piston.
CECECE{gt0}-BE phase
- Piston retracts according to current facing, destroys bedrock.
The above is the entire process of breaking bedrock.
However, in structures like bedrock breakers, it's impossible for players to place pistons. We must achieve this effect through other means.
##### Bedrock Breaking Completed in Block Event Phase
The "before" in "before the piston's block event phase" doesn't only apply to other game phases. Even within the block event phase, you can still change the block corresponding to a certain block event before that event executes.

As you can see, the figure below is a simplified version of the bedrock breaking structure in some bedrock breakers...


As shown in the animation, during the entire process, the following important events occurred
> Premise--The numbers marked on the surface in the figure above are the depths of each piston, they attempt to extend or retract in this order in the BE phase.
CECECE{gt0}-BE phase
### [!ADVANCED] Placement Source Code Analysis
#### Normal Placement `tick`
> This is too simple to explain much, so the plain explanation was moved to the front (laugh). Therefore, only code is provided here for reference
```java
public static void tick(World world, BlockPos pos, BlockState state, PistonBlockEntity blockEntity) {
//Record current gt
blockEntity.savedWorldTime = world.getTime();
blockEntity.lastProgress = blockEntity.progress;
//If push is complete
if (blockEntity.lastProgress >= 1.0F) {
//If it's the client and current deathTick hasn't exceeded 5
if (world.isClient && deathTicks < 5) {
//Increase deathTicks count
++deathTicks;
} else {
//Remove block entity
world.removeBlockEntity(pos);
blockEntity.markRemoved();
//If this block is b36
if (world.getBlockState(pos).isOf(Blocks.MOVING_PISTON)) {
//Send PP update
BlockState blockState = Block.postProcessState(blockEntity.pushedBlock, world, pos);
//If it's air
if (blockState.isAir()) {
//Set to corresponding block, no update, bitflag=0b01010100;
world.setBlockState(pos, blockEntity.pushedBlock, 84);
//Update or destroy this block
Block.replace(blockEntity.pushedBlock, blockState, world, pos, 3);
} else {
//If it's a waterlogged block
if (blockState.contains(Properties.WATERLOGGED) && (Boolean)blockState.get(Properties.WATERLOGGED)) {
//Remove water from waterlogged block
blockState = (BlockState)blockState.with(Properties.WATERLOGGED, false);
}
//Set to corresponding block, send NC update, PP update, client update, bitflag=0b01000011
world.setBlockState(pos, blockState, 67);
//Receive NC update itself
world.updateNeighbor(pos, blockState.getBlock(), pos);
}
}
}
}
//If push is not complete
//Push progress +0.5
else {
float f = blockEntity.progress + 0.5F;
//Perform entity calculations
pushEntities(world, pos, f, blockEntity);
moveEntitiesInHoneyBlock(world, pos, f, blockEntity);
blockEntity.progress = f;
//If push progress >=1, set to 1
if (blockEntity.progress >= 1.0F) {
blockEntity.progress = 1.0F;
}
}
}
- Piston #1 retracts, destroys piston head, emits NC update
- The piston base in front of piston #1 is updated. Since it's in BUD state, the piston adds a retract block event to itself.
Since depths 1, 2, 3 are occupied, it will be scheduled to #4, which is "execute last" in the current situation.
- Piston head destroys piston base.
- The piston base in front of piston #1 is updated. Since it's in BUD state, the piston adds a retract block event to itself.
- Piston #2 extends, creates 3 b36s.
- Sticky piston #3 retracts, forcing the b36 of the downward-facing piston directly in front to arrive instantly.
- The downward-facing piston's current position has the block event caused by piston #1 earlier, retracts according to current facing, destroys bedrock.
2.2 Deceiving Piston to Add/Cancel Adding Block Events
2.2.1 Deceiving Piston to Cancel Adding Block Event: Push Limit Detection
Block events can also have situations where the piston thinks the event it wants to execute is inconsistent with the actual situation. The most typical is push limit detection.
The push limit detection mechanism was originally discovered by players _Kayleigh and Landmining. Its core lies in "deceiving" the piston. What's shown here is a simplified version made by Bright_Observer, convenient for analysis and understanding.
Although this device has directional design, this doesn't affect its detection function, so the construction direction can be freely chosen.
Operating phenomenon:
- After placing purple wool (the wool is connected to a very long pillar...) and hitting the note block, the regular piston will activate
- After breaking the purple wool and hitting, the regular piston remains unchanged
Principle analysis:
- The upward-pushing piston self-checks when updated
- The system checks push limit conditions
- If wool is placed, because the glass pillar above has exceeded the push limit, the piston won't add a block event
- At this point, activate two regular pistons
- The sticky piston is updated by the regular piston, self-checks and extends
- When wool doesn't exist:
Sticky piston operates normally
The extension state of the upper regular piston depends on facing
The lower regular piston remains in place due to depth relationship
Key finding:
The sticky piston's planned action may differ from the push structure at actual execution time. This characteristic is cleverly used for the detection mechanism.
2.2.2 Deceiving Piston to Add Block Event
Unfortunately, if you try to replicate the layout and activation sequence of 5.10.1 Breaking Bedrock
2.2.3 Instant Placement finish
Most of the content was explained before, so it's skipped here. We only look at the latter part of the b36 special case.
In 1.20, only the piston arm's source property is true, all others are false. Therefore, when this method is called, if the position is b36 with target block as piston arm, it will directly become air. The rest won't be detailed, as it's all been covered before.
2.3 Piston Head
This chapter actually has no difficulty, but since apart from Menggui233's recent demonstration in PetrisAFE's video, no other uses have been seen, so it's placed in the advanced section here.
We first observe the following device:
Now, pull down the lever
We observe that the piston base was replaced with an observer, while the piston head was preserved. Why?
We first review what happens when b36 arrives:
- Send PP update at its own position
- Send NC and PP updates to first-order adjacency at its own position
- Send NC update at its own position
Therefore, we separately check what happens when the piston head receives PP update and NC update
2.3.1 Piston Head Existence Condition Determination canSurvive
The isFittingBase method essentially states that a piston head is fitting if and only if the block behind the piston head (more precisely, 1 block in the opposite direction of the push direction, hereafter referred to as behind) is an extending piston with the same extension direction. The canSurvive method requires that a piston head's existence is valid if and only if the piston head is fitting, or there is a b36 behind it with the same push direction.
2.3.2 Piston Head Removal onRemove
One sentence summary: When removed, if the state is fitting, the base is removed along with it and the piston drops.
2.3.3 Piston Head Receives PP Update, NC Update
PP Update
The piston head actually only cares about PP updates from behind. When a PP update occurs behind, the piston head will call canSurvive to check if it is valid. If invalid, it immediately disappears.
NC Update
In any case, the piston head will not disappear due to receiving an NC update. When receiving an NC update, if it is valid, the update is treated as an NC update received by the block behind.
2.3.4 Case Analysis
Therefore, for the case mentioned at the beginning of the chapter:
- 0gt AT: Player pulls down lever, trapdoor triggers
- 2gt BE depth 0: Sticky piston is activated, updates headless piston
- 2gt BE depth 1: Headless piston eats sticky piston base, slime block & lit observer are pushed
- 4gt TE: Sticky piston head places first. At this point, it checks that behind is b36 in the same direction as itself, self-check passes. Then lit observer b36 places, no update.
Therefore, the observer successfully replaced the original piston base.
2.4 B36 Related Entity Calculations
Footnotes
-
The
tryMovehere needs disambiguation. InJava, methods in different classes can be given the same name. ThetryMovehere is inPistonHandler.java↩





