02 Pistons
1 Mechanics
Basic Section
Basic Section
Advanced Section
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.
There are essentially only two ways to create 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.
Pistons trigger self-checks if and only if the following two situations occur:
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.
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:
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.
When a piston self-checks, it always follows this behavior tree:
For a block to be pushable by a piston, it must simultaneously satisfy the following requirements:
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 to directly view the piston's attempted block placement order in-game.
The piston update order can be divided into five situations:
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:
In the block tick phase of each game tick, b36 adds 0.5 to its push progress
If we start observing from 0gt AT, the block placement is fully complete at 3gt AT, hence the term "3-gt piston delay"
When a piston triggers instant retraction, blocks skip the normal TE phase accumulation and are forced to place immediately. Trigger conditions are:
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:
movedBlocks and brokenBlocks reversed directly give the b36 arrival order and destruction order. We'll explain the reason in detail later.
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.
When a lit observer is removed, it emits NC update backward and extinguishes itself.
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
Attachment block dropping also produces updates, but since attachment blocks usually drop directly and are difficult to reuse. For example:
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
}
}
Since depths 1, 2, 3 are occupied, it will be scheduled to #4, which is "execute last" in the current situation.
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:
Principle analysis:
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.
Unfortunately, if you try to replicate the layout and activation sequence of 5.10.1 Breaking Bedrock
finishMost 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.
onSyncedBlockEventThis chapter actually isn't difficult, but since apart from the device demonstrated by in 's 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:
Therefore, we separately examine what happens when the piston head receives PP update and NC update
canSurviveWhat happens when you hit the note block?
The sticky piston receives NC update and self-checks. Calls trymove, adds retract block event, calls onSyncedBlockEvent. Performs the following judgment:
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;
}
}
}
public void finish() { // World exists and progress not full or client calculation if (this.world != null && (this.lastProgress < 1.0F || this.world.isClient)) { // Set progress to complete this.progress = 1.0F; this.lastProgress = this.progressThis chapter actually has no difficulty, but since apart from 's recent demonstration in 's , 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:
Therefore, we separately check what happens when the piston head receives PP update and NC update
canSurviveThe tryMove here needs disambiguation. In Java, methods in different classes can be given the same name. The tryMove here is in PistonHandler.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 is facing) Direction facing = (Direction)state.get(FACING); // Get the activated block state (indicating the block is extending) BlockState extendedState = (BlockState)shouldExtendThe 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.
tryMovetryMove 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:
movedBlocks and brokenBlocks lists.brokenBlocks list, push succeedsMojang implemented the analysis of linear structures and branch structures through two methods here, called tryMove and tryMoveAdjacentBlock.
tryMoveFirst, clarify when this method will be called:
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;
}
}
}
finishThe logic here is as follows:
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.
private boolean isFittingBase(BlockState baseState, BlockState extendedState) { Block block = baseState.getValue(TYPE) == PistonType.DEFAULT ? Blocks.PISTON : Blocks.STICKY_PISTON; return extendedState.is(block) && extendedState.getValue(PistonBaseBlock.EXTENDEDThe 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.
onRemoveOne sentence summary: When removed, if the state is fitting, the base is removed along with it and the piston drops.
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.
Therefore, for the case mentioned at the beginning of the chapter:
Therefore, the observer successfully replaced the original piston base.
// This method handles piston pushing of entities. 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.private static void pushEntities(World world, BlockPos pos, float f, PistonBlockEntity blockEntity) { // Get the direction b36 is moving Direction direction = blockEntity.getMovementDirection(); // Calculate the distance b36 needs to move in the current tickAfter 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.
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:
movedBlocks and brokenBlocks reversed directly give the b36 placement order and destruction order. The reason will be explained in detail later.
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;
}
/tick freezeIf 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:
ServerWorldClientWorldIn 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.
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.
What happens when you hit the note block?
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.
What happens when you hit the note block?
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.
What happens when you hit the note block?
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.
What happens when you pull down the lever?
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.
What happens when you pull down the lever?
The principle is similar to case 5, but the code called is here:
moveIf there's b36 in front, directly call the finish method to turn it to air.
What happens when you pull down the lever?
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.
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.
onRemoveOne sentence summary: When removed, if the state is fitting, the base is removed along with it and the piston drops.
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.
Therefore, for the case mentioned at the beginning of the chapter:
Therefore, the observer successfully replaced the original piston base.
// retract=0: retract// retract=1: extendprivate 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