Moveable Entity Block Module
The Moveable Entity Block module allows blocks that have Block Entities to be pushed by pistons while preserving their internal data correctly. This solves the vanilla Minecraft limitation where blocks with block entities either cannot be pushed by pistons or lose their data when pushed.
I. IMoveableEntityBlock Interface
A block that implements this interface can be pushed by pistons and will have its block entity data migrated to the new position.
package dev.anvilcraft.lib.v2.piston;
public interface IMoveableEntityBlock extends EntityBlock {
/**
* Called when the block is pushed out of its original position by a piston.
* Returns the block entity data (as a CompoundTag) that should be migrated to the new position.
*
* @param level The current level
* @param pos The block's current position
* @return NBT data to be migrated
*/
default CompoundTag clearData(Level level, BlockPos pos) {
return new CompoundTag();
}
/**
* Called when the block arrives at its new position and stops moving.
* Restores the migrated NBT data into the block entity at the new position.
*
* @param level The current level
* @param pos The block's new position
* @param nbt NBT data returned from clearData
*/
default void setData(Level level, BlockPos pos, CompoundTag nbt) {
}
}II. How It Works
- When a piston attempts to push a block, the framework intercepts the push logic via Mixin (
PistonBaseBlockMixin); - For blocks that implement
IMoveableEntityBlock,clearData()is called to extract the block entity data, which travels with the piston movement; - After the block arrives at its new position,
PistonMovingBlockEntityMixincallssetData()to write the data back into the block entity at the new position.
III. Full Example
1. Define the Block
import dev.anvilcraft.lib.v2.piston.IMoveableEntityBlock;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
public class MyStorageBlock extends BaseEntityBlock implements IMoveableEntityBlock {
public MyStorageBlock(Properties properties) {
super(properties);
}
@Override
public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new MyStorageBlockEntity(pos, state);
}
/**
* Save block entity data before the block is pushed away
*/
@Override
public CompoundTag clearData(Level level, BlockPos pos) {
BlockEntity be = level.getBlockEntity(pos);
if (be == null) return new CompoundTag();
// Save full data (without position metadata)
return be.saveWithoutMetadata(level.registryAccess());
}
/**
* Restore block entity data after the block arrives at its new position
*/
@Override
public void setData(Level level, BlockPos pos, CompoundTag nbt) {
BlockEntity be = level.getBlockEntity(pos);
if (be == null) return;
be.loadAdditional(nbt, level.registryAccess());
// Notify clients of the update
be.setChanged();
level.sendBlockUpdated(pos, be.getBlockState(), be.getBlockState(), 3);
}
}2. Define the Block Entity
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.core.HolderLookup;
public class MyStorageBlockEntity extends BlockEntity {
private int storedValue = 0;
public MyStorageBlockEntity(BlockPos pos, BlockState state) {
super(MyBlockEntities.MY_STORAGE.get(), pos, state);
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
tag.putInt("stored_value", this.storedValue);
}
@Override
public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
this.storedValue = tag.getInt("stored_value");
}
}3. Register the Block
You can combine this with the Registrum module:
public static final BlockEntry<MyStorageBlock> MY_STORAGE_BLOCK = REGISTRUM
.block("my_storage_block", MyStorageBlock::new)
.properties(p -> p.strength(2.0f))
.blockEntity(MyBlockEntities::MY_STORAGE, MyStorageBlockEntity::new).build()
.register();IV. Notes
- The block must extend
EntityBlock(or one of its implementations such asBaseEntityBlock) and implementIMoveableEntityBlock; - The default implementation of
clearData()returns an emptyCompoundTag, meaning no data is preserved — you must override this method if you want to keep data; - The default implementation of
setData()is a no-op — you must override this method if you want to restore data; - This module works via Mixin; ensure the Mixin configuration is loaded correctly (when using the aggregate artifact, this is handled automatically);
- All piston push directions (horizontal, upward, downward) are supported — the framework has no directional restriction.
V. Sticky Pistons
When a sticky piston pulls the block back, the same flow is triggered: clearData() and setData() are called correctly. If a block should not be retractable by sticky pistons, return PushReaction.BLOCK from the block's getPistonPushReaction() method — but this is vanilla behavior and unrelated to this module.