-
-
Save jan-matula/08ee076f78c40e984419d97a397a84f1 to your computer and use it in GitHub Desktop.
Pistons add complex behaviour that requires more than a single block to function. There is the normal piston base (block 33), the sticky piston base (block 29), the piston head (block 34), and the moving piston block (block 36).
The piston base blocks metadata is bound to the following structure:
| retracted/extended | facing (3 bits) |
| | axis (2 bits) | plus/minus |
| normal/sticky | facing (3 bits) |
| | axis (2 bits) | plus/minus |
The facing directions are represented in 3 bits as follows:
- |
+ |
|
---|---|---|
Y |
0b00_0 or 0 |
0b00_1 or 1 |
Z |
0b01_0 or 2 |
0b01_1 or 3 |
X |
0b10_0 or 4 |
0b10_1 or 5 |
Orientation 0b111
or 7 is also used within the game as sort of a 'orientation not determined' state. A piston base will have this orientation until it determines the real orientation later. The last orientation, 0b110
or 6, is simply invalid. The behaviour of these orientations is explained in later sections; they use the same code anyways.
This format of orientations is used by the piston base and the piston head metadata as well as the piston block entity.
The piston head is relatively sparse on functionality. However, the specifics of how piston heads work are useful for some headless piston technology as well as pistons with multiple heads.
When a piston head block gets removed, the game will check if it has an extended piston base of any type (does not matter if the piston head is sticky or normal) behind it. If this is the case, it will drop it as an item and break it too.
What this means for headless pistons is that we can destroy the head while the piston is in a semi-retracted state and then cancel its retraction to make it headless.
When a piston head receives an update, game will check if the block behind it is a piston base, any regardless of metadata or kind. If so, propagate the update to the piston base. Otherwise, set the piston head to air.
This means you can update a piston by updating its piston head. This is something we all know quite intuitively though.
As you may know, when a block moves, it actually changes into a completely different block. This block is called 'the moving piston block' (this is actually only the case in later versions of the game, in 1.7 it is called piston_extension
) or 'block 36'.
The moving piston block is non-solid, invisible and immovable. This block always occupies the position where the block that is being moved should arrive. It is the representation of a block being moved.
Note that this document does not explain any interactions with entities (like collisions and moving entities).
Because a moving piston block needs to have associated data that does not fit in a nibble, it uses a so-called 'block entity' to store its state instead of using metadata. The block entity holds the following data:
- blockId: int
- blockData: int
- facing: int
- progress: float
- extending: boolean
The facing
field indicates the direction that the block is moving in. The progress
is initialized at 0.0
. It basically a value from 0 to 1 indicating how many blocks the moving block has moved so far.
Additionally, the block entity also stores a value called lastProgress
.
I recommend reading Space Walker's document if you want to know more about block entities generally.
During every game tick, the game will update all block entities. This is the so-called block entity phase. The piston block entity will get updated in the following way:
If the progress is greather or equal to 1.0
, the game will clear the block entity, turning it into a block. Otherwise, the game will just increase the progress by 0.5
. If the value reaches value over 1.0
after this, the game will just set it back to 1.0
.
A moving piston block without a piston block entity would be permanent.
There is also some interaction with entities involved in the piston block entity updates but they are not really relevant to piston mechanics.
Clearing a piston tile entity basically means turning the moving block into the actual block. The game will check if there is actually a moving piston block in the position and only then will it set the block accordinng to the block entity data. The block entity cannot just turn any block into any other block.
After setting the block, it will update it. This used to not be the case in versions before 1.5, which for example allowed for easy floating rails.
When a moving piston block gets broken it will clear the piston block entity (if there is one). Normally, this will not turn it into a block, because there is no longer a moving piston block in the position.
Theoretically, if you were to set a moving piston block in the place of a different moving piston block, it would immediately turn it into the block saved in the old moving block's piston block entity, since during the clearing of the block entity, there would be a moving piston block in that position. This can actually be done rather easily in b1.7.3.
For block events, there are actually already some really good resources already. They are from newer versions, but all the functionality has basically stayed the same since 1.3. I personally recommend Myren Eario's random video about block events, Selulance's video on microtick technology and Space Walker's document.
Pistons do not retract or extend immediately. Instead, they use something called 'block events'.
Block events are a type of scheduled action. There is no associated duration to a block event. Every tick, the game will try to process all the block events in the block event queue. This is done at a specific point during the tick, the block event phase.
Blocks can schedule block events thoughout the course of a game tick and the game will execute all of them in the order of creation.
A block event holds the following information:
-
The position of the block.
-
The block object (a block object is what handles the behaviour of a particular block ID).
-
The event ID. This is used to identify the action to be performed. The event ID for piston extension is
0
while1
is used for retraction. -
The event parameter. A block event can pass some additional information through this integer value. Pistons will use the block event parameter to pass their orientation.
The game will refuse to add a block event with the same data as a block event that is already in the queue. It is impossible to have duplicate block events. Right after a block event has been processed, it is possible to add a new identical one. This can still be in the same game tick.
During the block event phase, the game will be taking block events out of the queue to try processing them until the queue is empty. The block event is no longer in the queue when it is getting processed.
The game will refuse to process a block event if the block at its position no longer matches the block saved in its data.
Although piston behaviour revolves around more blocks than just the piston base block, the piston base is what provides most of piston mechanics and most piston bugs will revolve around piston base code. Piston base code is what glues piston behaviour together.
When a piston base block is updated, it will try to see if it should extend or retracted and react appropriately. The behaviour is summed up in the following pseudocode:
get metadata of piston
if metadata orientation is 7
exit
check if piston is powered
if piston metadata says retracted AND piston is powered
if piston can extend
add extension block event
else piston metadata says extended AND piston is not powered
set piston metadata to retracted (do not update neighbours)
add retraction block event
After the game obtians the piston's metadata, it checks for orientation 7, the 'orientation not determined' orientation. This is what ultimately determines the behaviour of pistons of this orientation, pistons with metadata values 7 and 15. These pistons will simply refuse to do anything.
Then pistons decide whether to extend, retract or neither. This is decided based on power and state.
In case a retraction is appropriate, pistons will set themselves to retracted instantly on update, that is, before actually performing the retration. We call this behaviour semi-retraction.
Pistons decide whether they are receiving power or not using their own dedicated procedure. They check for indirect power from all sides except the one they are facing. In addition to that, they will check for indirect power from all the sides of the block above. This property is called quasi-connectivity.
When a block event is received, the piston will first check if the action given by the event ID is still valid in the current state of things. In case the action is no longer appropriate the game will do what I call block event cancellation.
check if the piston is powered
if is retraction block event AND piston is powered
set piston metadata to extended (do not update neighbours)
exit unsuccessfully
else if is extension block event AND piston is not powered
exit unsuccessfully
...
Because the game always sets an extended piston base to a retracted one before scheduling the retraction block event the cancellation of the block event will have to set it back into the extended state. However, it will not execute any extension code, just change piston metadata.
Keep in mind that new metadata for the piston itself is always determined by taking the orientation saved in the block event and setting or clearing the 4th (the most significant) bit of the metadata nibble. If the orientation were to change between the scheduling of the block event and its processing, the first attempt at setting the pistons metadata would end up rotating the piston back into its original orientation.
Further action is simply decided by block event ID.
A piston extension could be seen as a 3 stage process:
-
Check for blocks to push (and potentially destroy a block).
-
Create moving blocks.
-
Update blocks adjacent to the extension.
This process of extension is all extracted into a separate procedure.
In 1.8+, where slime blocks exist, you would want to save the positions of all the blocks that need to be moved - that is the purpose of this stage after all. However, in 1.7 there are no slime blocks and all movable piston structures basically take the form of a block row going from the block in front of the piston to some second coordinate in the facing direction of the piston.
The game iterates through the blocks in front of the piston, keeping track of only the current block position, let's call this the cursor, and the block count (this is so it can enforce push limit more easily).
This mobility check can end the extension unsuccessfully if the game finds that the piston should not be able to extend.
If the check runs into a 'no push' mobility block (a block that should get destroyed by the piston), it will destroy it (updating adjacent blocks). Either this or the block being air will end the search for blocks. The game will continue onto the second stage remembering the position of the last checked block in the cursor.
Now the game will go backwards from the last checked position keeping track of the current block position and a block ID list.
The following pseudo-code outlines this process:
save current cursor pos for later
until cursor is at piston base pos
offset cursor in opposite of piston base facing direction
get block at cursor
get block metadata at cursor
if block id at the cursor is this block id AND cursor pos is piston base pos
set block at last cursor pos to moving piston (do not update neighbours)
set block entity at last cursor pos to new piston block entity of piston head
else
set block at last cursor pos to moving piston (do not update neighbours)
set block entity at last cursor pos to new piston block entity of block under cursor
add current block ID to the list
The game also saves the position of the last checked block from the previous stage for later.
It is maybe worth noting that the moving piston blocks are set with the metadata of the original block, the block that is being moved. Changing the metadata of the moving piston block will not, however, affect the metadata of the arriving block in any way. For the last moving piston block the game chooses a piston head.
The blocks will later arrive in the same order they were created in.
In the last stage of the game simply returns to the cursor to go through the moved block's positions updating blocks adjacent to them.
All block updates have a source of sorts. Some blocks can decide to only react to updates with sources meeting specific criteria like being able to emit power. An example of this would be TNT in 1.4 and below. This is the reason why the game creates the block ID list.
The block updates thus happen after all the blocks have already been turned into moving piston blocks from the farthest to the closest.
If the extension succeeds, i.e. the extension procedure exits succesfully, the game will set the pistons metadata to extended too.
While block updates are mostly suppressed for later in extensions, retractions basically update blocks every time they do anything. Take adjacent block updates as implicit in the following sections.
The first part of the retraction code is shared between the two piston types. The game will check for a block entity in front of the piston base. If the block entity is a piston block entity, it will 'clear' it, i.e. turn it into a block.
get block entity at pos 1 block in front of piston base
if block entity is piston block entity
clear piston block entity
set block at piston base pos to moving piston
set block entity at piston base pos to piston block entity
The intended scenario here is that there is a moving piston head in front of the piston base because it is still in the process of extending. Without headless pistons or some other exploit it is not possible to reach any other such scenario.
The game will set a moving piston block of the retracting piston in place of the actual piston. The piston block entity will have the orientation saved in the block event and will be retracting (or maybe more precisely, not extending).
This will emit block updates making the piston head realize it does not have a piston base behind it (but a moving piston block) and destroy itself. Interestingly, the piston head's destruction is explicitly attempted later, probably due to the code's author not realizing the redundancy.
Further action differs between sticky pistons and normal pistons.
Normal pistons will simply attempt to destroy the piston head again. Keep in mind that they will not succeed in the intended scenario (the piston head is no longer there anyway). The block may not be a piston head though. Using headless pistons you can trick the game into destroying any block in front of the piston base.
The behaviour of sticky pistons is a bit more complicated.
The game will get the block and metadata 2 blocks in front of the piston base. This is the position where the block that would get pulled is.
Based on the block there are 3 main cases that the game checks for (in the specified order):
-
The block is a moving piston block with a piston block entity (extending, matching the piston's orientation).
-
The block can be pulled.
-
The block cannot be pulled.
If the block is a moving piston block with an extending piston block entity that matches the piston's orientation, the game will attempt perform what is commonly called block dropping. It will clear the piston block entity turning the moving piston block into a block.
Note that this is the only case of the three where the game does not attempt to destroy the piston head. You can use this to clear piston block entities directly in front of headless pistons, regardless of the block entities orientation. (Actually, the piston block entity directly in front of the piston always gets clear. It is just that, in the other scenarios, the game will destroy them right after.)
If block dropping cannot be performed, the game will check whether the block is pullable. If this check succeeds, the game will create a retracting moving block using the orientation from the block event and destroy the original block.
set block at pos 1 block in front of piston base to moving piston
set block entity at pos 1 block in front of piston base pos to piston block entity
set block at pos 2 blocks in front of piston base to air
Here, the block directly in front of the piston gets destroyed by the placement of the moving piston block.
The case where the piston does not block drop and does not pull is identical to normal piston retraction, the game will simply try to destroy the piston head.