diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head.json b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head.json
index f93b104ad..123f7756c 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head.json
@@ -4,7 +4,7 @@
 	"textures": {
 		"face": "block/piston_sticky/face",
 		"back": "block/piston/face",
-		"head": "block/piston/head",
+		"head": "block/piston_sticky/head",
 		"shaft": "block/piston/shaft",
 		"overlay": "#face",
 		"particle_up": "#head",
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_half.json b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_half.json
index c7a3c402c..d37ae4c46 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_half.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_half.json
@@ -4,7 +4,7 @@
 	"textures": {
 		"face": "block/piston_sticky/face",
 		"back": "block/piston/face",
-		"head": "block/piston/head",
+		"head": "block/piston_sticky/head",
 		"shaft": "block/piston/shaft",
 		"overlay": "#face",
 		"particle_up": "#head",
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_short.json b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_short.json
index 5c43f0a00..bcb6ee0a7 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_short.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_short.json
@@ -4,7 +4,7 @@
 	"textures": {
 		"face": "block/piston_sticky/face",
 		"back": "block/piston/face",
-		"head": "block/piston/head",
+		"head": "block/piston_sticky/head",
 		"shaft": "block/piston/shaft",
 		"shaft_end": "block/piston/shaft_end",
 		"overlay": "#face",
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/head.png b/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/head.png
new file mode 100644
index 000000000..503c84138
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/head.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/side.png b/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/side.png
index f0778e711..cdc7573a2 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/side.png and b/game/client/src/main/resources/assets/minecraft/textures/block/piston_sticky/side.png differ
diff --git a/game/core/src/main/java/net/minecraft/core/block/Block.java b/game/core/src/main/java/net/minecraft/core/block/Block.java
index d983689be..eacadee31 100644
--- a/game/core/src/main/java/net/minecraft/core/block/Block.java
+++ b/game/core/src/main/java/net/minecraft/core/block/Block.java
@@ -5,6 +5,9 @@ import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.MaterialColor;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.data.tag.ITaggable;
 import net.minecraft.core.data.tag.Tag;
@@ -563,6 +566,26 @@ public final class Block<T extends BlockLogic>
         return this.logic.getPlacedData(player, itemStack, world, tilePos, side, xHit, yHit);
     }
 
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        this.logic.onMoved(world, sourcePos, targetPos, block, indexInLine, line);
+    }
+
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+        this.logic.onMovingNeighborChanged(world, targetPos, block, indexInLine, line, source);
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        this.logic.onMoveComplete(world, tilePos);
+    }
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        this.logic.onFlung(world, sourcePos, data, block);
+    }
+
     @Override
     public boolean isClimbable(final @NotNull World world, final @NotNull TilePosc tilePos) {
         return this.logic.isClimbable(world, tilePos);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockInterface.java b/game/core/src/main/java/net/minecraft/core/block/BlockInterface.java
index 1025fba80..b7e321b4a 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockInterface.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockInterface.java
@@ -2,6 +2,9 @@ package net.minecraft.core.block;
 
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.entity.TileEntityActivator;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.EntityFallingBlock;
@@ -167,6 +170,12 @@ interface BlockInterface {
 
     void onRemoved(final @NotNull World world, final @NotNull TilePosc tilePos, final int data);
 
+    void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line);
+    void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source);
+    void onMoveComplete(final @NotNull World world, final @NotNull TilePosc tilePos);
+
+    void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block);
+
     /**
      * Always called when a block is removed by setBlock
      */
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogic.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogic.java
index 62b5b9bcf..b4b393752 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogic.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogic.java
@@ -5,12 +5,17 @@ import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.MaterialColor;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.CarriedPistonHead;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.block.tag.BlockTags;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.EntityItem;
 import net.minecraft.core.entity.Mob;
+import net.minecraft.core.entity.monster.MobHuman;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.IItemConvertible;
@@ -18,6 +23,7 @@ import net.minecraft.core.item.Item;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.item.tool.ItemToolShears;
 import net.minecraft.core.util.collection.NamespaceID;
+import net.minecraft.core.util.helper.DamageType;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.MathHelper;
 import net.minecraft.core.util.helper.Side;
@@ -211,6 +217,20 @@ public class BlockLogic implements BlockInterface, IItemConvertible {
         }
     }
 
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {}
+
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {}
+
+    // this is currently asymmetrical to onMoved, as part of the line might not have being converted to normal blocks
+    // TODO: revisit with better abstraction in the future 
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {}
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) { }
+
     @Override
     public void onPlacedByMob(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final @NotNull Mob mob, final double xHit, final double yHit) {
         this.onPlacedOnSide(world, tilePos, side, xHit, yHit);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicActivator.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicActivator.java
index 20dea1629..6999b675c 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicActivator.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicActivator.java
@@ -2,6 +2,9 @@ package net.minecraft.core.block;
 
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.item.Item;
 import net.minecraft.core.item.ItemStack;
@@ -74,17 +77,76 @@ public class BlockLogicActivator
         }
     }
 
+    @Override
+    public void onPlacedByWorld(@NotNull World world, @NotNull TilePosc tilePos) {
+        final int meta = world.getBlockData(tilePos);
+        final boolean hasSignal = this.getNeighborSignal(world, tilePos, BlockLogicRotatable.getDirectionFromMeta(meta));
+        final boolean isPowered = (meta & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+
+        world.setBlockData(tilePos, meta ^ MASK_POWERED);
+        if (hasSignal) {
+            world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay());
+        }
+    }
+
     @Override
     public void onNeighborChanged(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
         final int meta = world.getBlockData(tilePos);
         final boolean hasSignal = this.getNeighborSignal(world, tilePos, BlockLogicRotatable.getDirectionFromMeta(meta));
         final boolean isPowered = (meta & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+
+        world.setBlockDataNotify(tilePos, meta ^ MASK_POWERED);
+        if (hasSignal) {
+            world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay());
+        }
+    }
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        block.metadata &= ~MASK_POWERED;
+    }
+
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        final int meta = block.getBlockData();
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final boolean isPowered = (meta & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+        
+        block.setBlockData(meta ^ MASK_POWERED);
+        line.notifyBlocksOfNeighborChange(indexInLine, this.block);
+        if (hasSignal) {
+            world.scheduleBlockUpdate(targetPos, this.block, this.tickDelay());
+        }
+    }
+    
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+        final int meta = block.getBlockData();
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final boolean isPowered = (meta & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+    
+        block.setBlockData(meta ^ MASK_POWERED);
+        line.notifyBlocksOfNeighborChange(indexInLine, this.block);
+        if (hasSignal) {
+            world.scheduleBlockUpdate(targetPos, this.block, this.tickDelay());
+        }
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        final int meta = world.getBlockData(tilePos);
+        final boolean isPowered = (meta & MASK_POWERED) != 0;
+        final boolean hasSignal = this.getNeighborSignal(world, tilePos, BlockLogicRotatable.getDirectionFromMeta(meta));
+
+        if (hasSignal == isPowered) return;
 
-        if (hasSignal && !isPowered) {
+        world.setBlockDataNotify(tilePos, meta ^ MASK_POWERED);
+        if (hasSignal) {
             world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay());
-            world.setBlockDataNotify(tilePos, meta | MASK_POWERED);
-        } else if (!hasSignal && isPowered) {
-            world.setBlockDataNotify(tilePos, meta & ~MASK_POWERED);
         }
     }
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBasket.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBasket.java
index 0dfcb1aae..a911205ad 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBasket.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBasket.java
@@ -4,6 +4,9 @@ import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.entity.TileEntityBasket;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.block.support.PartialSupport;
@@ -101,10 +104,34 @@ public class BlockLogicBasket extends BlockLogic {
         // world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        if (!(block.getTileEntity() instanceof TileEntityBasket basket)) return;
+        if (basket.getNumUnitsInside() >= basket.getMaxUnits()) {
+            world.notifyShellBlocksInRadiusOfNeighborChange(2, sourcePos, Blocks.AIR);
+        }
+    }
+    
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        if (!(block.entity instanceof TileEntityBasket basket)) return;
+        if (basket.getNumUnitsInside() >= basket.getMaxUnits()) {
+            world.notifyShellBlocksInRadiusOfNeighborChange(2, sourcePos, Blocks.AIR);
+        }
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        if (!(world.getTileEntity(tilePos) instanceof TileEntityBasket basket)) return;
+        if (basket.getNumUnitsInside() >= basket.getMaxUnits()) {
+            world.notifyShellBlocksInRadiusOfNeighborChange(2, tilePos, Blocks.AIR);
+        }
+    }
+
     @Override
     public boolean isEmittingSignal(final @NotNull WorldSource source, final @NotNull TilePosc tilePos, final @NotNull Side side) {
-        final @NotNull TileEntityBasket tileEntity = (TileEntityBasket) Objects.requireNonNull(source.getTileEntity(tilePos));
-        return tileEntity.getNumUnitsInside() >= tileEntity.getMaxUnits();
+        if (!(source.getTileEntity(tilePos) instanceof TileEntityBasket basket)) return false;
+        return basket.getNumUnitsInside() >= basket.getMaxUnits();
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBedrock.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBedrock.java
index 166dc3ba5..f436e5c64 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBedrock.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBedrock.java
@@ -1,6 +1,10 @@
 package net.minecraft.core.block;
 
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
+import net.minecraft.core.entity.monster.MobHuman;
+import net.minecraft.core.util.helper.DamageType;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.DyeColor;
 import net.minecraft.core.world.World;
@@ -20,4 +24,14 @@ public class BlockLogicBedrock
             Blocks.PORTAL_DRIFT.getLogic().tryToCreatePortal(world, tilePos, DyeColor.SILVER);
         }
     }
+
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        final var thisguy = new MobHuman(world);
+        thisguy.setNickname("kirisoup");
+        thisguy.setPos(targetPos.x(), targetPos.y(), targetPos.z());
+        world.entityJoinedWorld(thisguy);
+        final var thatguy = world.getClosestPlayerToEntity(thisguy, 16D);
+        if (thatguy != null) thisguy.hurt(thatguy, 1, DamageType.GENERIC);
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java
index adad84a0d..ac1c40dad 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java
@@ -108,6 +108,7 @@ public class BlockLogicConduit extends BlockLogic {
         }
     }
 
+    // TODO: onMoved and onFlung
 
     /**
      * Defines the placement and routing behavior of the conduit when placed into the world by an entity.
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicDispenser.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicDispenser.java
index 47ef77a98..e94c41206 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicDispenser.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicDispenser.java
@@ -5,6 +5,9 @@ import java.util.Random;
 
 import net.minecraft.core.block.entity.TileEntityDispenser;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.entity.*;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.entity.projectile.*;
@@ -106,16 +109,45 @@ public class BlockLogicDispenser extends BlockLogicVeryRotatable {
 
     @Override
     public void onNeighborChanged(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        final boolean hasNeighborSignal = world.hasNeighborSignal(tilePos);
         final int data = world.getBlockData(tilePos);
-        if (hasNeighborSignal && (data & MASK_POWERED) == 0) {
+        final boolean hasSignal = world.hasNeighborSignal(tilePos);
+        final boolean isPowered = (data & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+        world.setBlockData(tilePos, data ^ MASK_POWERED);
+        if (hasSignal) {
             world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay());
-            world.setBlockData(tilePos, data | MASK_POWERED);
-        } else if (!hasNeighborSignal) {
-            world.setBlockData(tilePos, data & ~MASK_POWERED);
         }
     }
 
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        final int data = block.getBlockData();
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final boolean isPowered = (data & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+        block.setBlockData(data ^ MASK_POWERED);
+        if (hasSignal) {
+            world.scheduleBlockUpdate(targetPos, this.block, this.tickDelay());
+        }
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        final int data = world.getBlockData(tilePos);
+        final boolean hasSignal = world.hasNeighborSignal(tilePos);
+        final boolean isPowered = (data & MASK_POWERED) != 0;
+        if (hasSignal == isPowered) return;
+        world.setBlockData(tilePos, data ^ MASK_POWERED);
+        if (hasSignal) {
+            world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay());
+        }
+    }
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        block.metadata &= ~MASK_POWERED;
+    }
+
     @Override
     public void updateTick(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Random rand, final boolean isRandomTick) {
         this.dispenseItem(world, tilePos, rand);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java
index 01794530b..73b63fd20 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java
@@ -3,6 +3,9 @@ package net.minecraft.core.block;
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.ItemStack;
@@ -62,23 +65,58 @@ public class BlockLogicLamp extends BlockLogic implements IPainted {
 
     @Override
     public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
+        // what is the purpose of .hasDirectSignal?
         final boolean hasSignal = world.hasDirectSignal(tilePos) || world.hasNeighborSignal(tilePos);
+        final boolean isPowered = this.isInverted ^ this.isActive;
+        if (hasSignal == isPowered) return;
 
-        if (this.isInverted) {
-            if (hasSignal && this.isActive) {
-                world.setBlockTypeDataNotify(tilePos, Blocks.LAMP_INVERTED_IDLE, world.getBlockData(tilePos));
-            } else if (!hasSignal && !this.isActive) {
-                world.setBlockTypeDataNotify(tilePos, Blocks.LAMP_INVERTED_ACTIVE, world.getBlockData(tilePos));
-            }
-        } else {
-            if (hasSignal && !this.isActive) {
-                world.setBlockTypeDataNotify(tilePos, Blocks.LAMP_ACTIVE, world.getBlockData(tilePos));
-            } else if (!hasSignal && this.isActive) {
-                world.setBlockTypeDataNotify(tilePos, Blocks.LAMP_IDLE, world.getBlockData(tilePos));
-            }
+        world.setBlockTypeDataNotify(tilePos, this.getBlockForSignal(hasSignal), world.getBlockData(tilePos));
+    }
+
+    private @Nullable Block<BlockLogicLamp> getBlockForSignal(boolean hasSignal) {
+        return switch (((hasSignal) ? 0b01 : 0b00) | ((this.isInverted) ? 0b10 : 0b00)) {
+            default -> null;
+            case 0b00 -> Blocks.LAMP_IDLE;
+            case 0b01 -> Blocks.LAMP_ACTIVE;
+            case 0b10 -> Blocks.LAMP_INVERTED_ACTIVE;
+            case 0b11 -> Blocks.LAMP_INVERTED_IDLE;
+        };
+    }
+
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final var newBlock = this.getBlockForSignal(hasSignal);
+        block.setBlockType(newBlock);
+        if (hasSignal != (this.isInverted ^ this.isActive)) {
+            line.notifyBlocksOfNeighborChange(indexInLine, newBlock);
         }
     }
 
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final var newBlock = this.getBlockForSignal(hasSignal);
+        block.setBlockType(newBlock);
+        if (hasSignal != (this.isInverted ^ this.isActive)) {
+            line.notifyBlocksOfNeighborChange(indexInLine, newBlock);
+        }
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        final boolean hasSignal = world.hasDirectSignal(tilePos) || world.hasNeighborSignal(tilePos);
+        final boolean isPowered = this.isInverted ^ this.isActive;
+        if (hasSignal == isPowered) return;
+
+        world.setBlockTypeDataNotify(tilePos, this.getBlockForSignal(hasSignal), world.getBlockData(tilePos));
+    }
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        block.blockId = this.getBlockForSignal(false).id();
+    }
+
     @Override
     public boolean onInteracted(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Player player, @Nullable Side side, double xHit, double yHit) {
         if (player.getHeldItem() == null) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLog.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLog.java
index a0639d527..be61c32b0 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLog.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLog.java
@@ -19,8 +19,8 @@ public class BlockLogicLog extends BlockLogicAxisAligned {
         if (world.areBlocksLoaded(tilePos.add(-r2, -r2, -r2, new TilePos()), tilePos.add(r2, r2, r2, new TilePos()))) {
             TilePos queryPos = new TilePos();
             for (queryPos.x = tilePos.x() - r; queryPos.x <= tilePos.x() + r; queryPos.x++) {
-                for (queryPos.y = tilePos.x() - r; queryPos.y <= tilePos.y() + r; queryPos.y++) {
-                    for (queryPos.z = -tilePos.x() - r; queryPos.z <= tilePos.z() + r; queryPos.z++) {
+                for (queryPos.y = tilePos.y() - r; queryPos.y <= tilePos.y() + r; queryPos.y++) {
+                    for (queryPos.z = tilePos.z() - r; queryPos.z <= tilePos.z() + r; queryPos.z++) {
                         Block<?> block = world.getBlockType(queryPos);
                         if (!(block.getLogic() instanceof BlockLogicLeavesBase)) {
                             continue;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMatcher.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMatcher.java
index d247ec354..283f3fa11 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMatcher.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMatcher.java
@@ -2,6 +2,8 @@ package net.minecraft.core.block;
 
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.entity.Mob;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.enums.PlacementMode;
@@ -64,6 +66,11 @@ public class BlockLogicMatcher extends BlockLogicAxisAligned {
     public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
         world.scheduleBlockUpdate(tilePos, this.block, tickDelay());
     }
+    
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        world.scheduleBlockUpdate(targetPos, this.block, tickDelay());
+    }
 
     public void updateState(@NotNull World world, @NotNull TilePosc tilePos) {
         Axis a = BlockLogicAxisAligned.metaToAxis(world.getBlockData(tilePos) & BlockLogicAxisAligned.MASK_DIRECTION);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMotionSensor.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMotionSensor.java
index 794d81e3e..f23bab145 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMotionSensor.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMotionSensor.java
@@ -80,4 +80,5 @@ public class BlockLogicMotionSensor extends BlockLogicVeryRotatable {
         return new ItemStack[]{new ItemStack(Blocks.MOTION_SENSOR_IDLE)};
     }
 
+    // TODO: refactor tileEntity tick to work with moving block as well
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicNote.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicNote.java
index c45b67779..d889de532 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicNote.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicNote.java
@@ -5,6 +5,9 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.sound.SoundCategory;
 import net.minecraft.core.util.helper.Direction;
@@ -32,14 +35,47 @@ public class BlockLogicNote extends BlockLogic {
 
     @Override
     public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        boolean powered = world.hasNeighborSignal(tilePos);
-        int data = world.getBlockData(tilePos);
-        if (isPowered(data) && !powered) {
-            world.setBlockData(tilePos, setPowered(data, false));
-        } else if (!isPowered(data) && powered) {
-            triggerNote(world, tilePos);
-            world.setBlockData(tilePos, setPowered(data, true));
-        }
+        final int data = world.getBlockData(tilePos);
+        final boolean hasSignal = world.hasNeighborSignal(tilePos);
+        final boolean isPowered = isPowered(data);
+        if (isPowered == hasSignal) return;
+        world.setBlockData(tilePos, data ^ MASK_POWERED);
+        if (hasSignal) triggerNote(world, tilePos);
+    }
+
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        final int data = block.getBlockData();
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final boolean isPowered = isPowered(data);
+        if (isPowered == hasSignal) return;
+        block.setBlockData(data ^ MASK_POWERED);
+        if (hasSignal) triggerNote(world, targetPos);
+    }
+
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+        final int data = block.getBlockData();
+        final boolean hasSignal = line.hasNeighborSignal(indexInLine);
+        final boolean isPowered = isPowered(data);
+        if (isPowered == hasSignal) return;
+        block.setBlockData(data ^ MASK_POWERED);
+        if (hasSignal) triggerNote(world, targetPos);
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        final int data = world.getBlockData(tilePos);
+        final boolean hasSignal = world.hasNeighborSignal(tilePos);
+        final boolean isPowered = isPowered(data);
+        if (isPowered == hasSignal) return;
+        world.setBlockData(tilePos, data ^ MASK_POWERED);
+        if (hasSignal) triggerNote(world, tilePos);
+    }
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        block.metadata &= ~MASK_POWERED;
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicOreRedstone.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicOreRedstone.java
index 782117b85..1ba79015d 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicOreRedstone.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicOreRedstone.java
@@ -16,6 +16,9 @@ import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.LevelListener;
 import net.minecraft.core.world.World;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
+import net.minecraft.core.block.piston.MovingLine.LineDirection;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
@@ -80,6 +83,13 @@ public class BlockLogicOreRedstone extends BlockLogic {
         lightRedstone(world, tilePos);
     }
 
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        block.setBlockType(this.illuminatedBlock);
+        if (this.block != this.illuminatedBlock) line.updateSignal(indexInLine, LineDirection.BOTH, true);
+        world.scheduleBlockUpdate(targetPos, this.illuminatedBlock, 20 + world.rand.nextInt(9 * 20));
+    }
+
     private void lightRedstone(@NotNull World world, @NotNull TilePosc tilePos) {
         spawnParticles(world, tilePos);
         if (!this.illuminated) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkinRedstone.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkinRedstone.java
index 3fe46ef80..be3126176 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkinRedstone.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkinRedstone.java
@@ -2,11 +2,11 @@ package net.minecraft.core.block;
 
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.Materials;
-import net.minecraft.core.util.helper.Direction;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.WorldSource;
-import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
 
@@ -20,13 +20,20 @@ public class BlockLogicPumpkinRedstone extends BlockLogicVeryRotatable {
     public void onPlacedByWorld(@NotNull World world, @NotNull TilePosc tilePos) {
         world.notifyShellBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
-
+    
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
-        TilePos queryPos = new TilePos();
-        for (final var dir : Direction.all){
-            world.notifyBlocksOfNeighborChange(tilePos.add(dir, queryPos), this.block);
-        }
+        world.notifyShellBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
+    }
+    
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        world.notifyShellBlocksInRadiusOfNeighborChange(2, sourcePos, this.block);
+    }
+
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRail.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRail.java
index b55e3f37e..9784c72c7 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRail.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRail.java
@@ -160,6 +160,9 @@ public class BlockLogicRail extends BlockLogic implements ISupportable {
             performRailTurn(world, tilePos, false);
         }
     }
+
+    // TODO: onMoved
+
     // Checks that the rail is placed on top of a block and doesn't slope into the air
 
     private boolean isRailValid(@NotNull World world, @NotNull TilePosc tilePos) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRedstone.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRedstone.java
index b5c8862f7..9a8bb162f 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRedstone.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRedstone.java
@@ -1,6 +1,8 @@
 package net.minecraft.core.block;
 
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.WorldSource;
@@ -40,7 +42,16 @@ public class BlockLogicRedstone extends BlockLogic {
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
         world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
-
+    
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+        world.notifyBlocksInRadiusOfNeighborChange(2, sourcePos, this.block);
+    }
+    
+    @Override
+    public void onMoveComplete(@NotNull World world, @NotNull TilePosc tilePos) {
+        world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
+    }
 
     @Override
     public void animationTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSpikes.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSpikes.java
index 8236bc71d..10f062f76 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSpikes.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSpikes.java
@@ -15,6 +15,9 @@ import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.LevelListener;
 import net.minecraft.core.world.World;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.util.helper.DamageType;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.pos.TilePosc;
@@ -68,25 +71,47 @@ public class BlockLogicSpikes extends BlockLogic
 
 	@Override
 	public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-		if(world.isClientSide) {
-			return;
-		}
+		if(world.isClientSide) return;
 
-		int meta = world.getBlockData(tilePos);
-		boolean wasPowered = (meta & MASK_POWERED) != 0;
-		boolean isPowered = world.hasNeighborSignal(tilePos);
+		final int meta = world.getBlockData(tilePos);
+		final boolean wasPowered = (meta & MASK_POWERED) != 0;
+		final boolean isPowered = world.hasNeighborSignal(tilePos);
+		if (isPowered == wasPowered) return;
+        world.setBlockDataNotify(tilePos, meta ^ MASK_POWERED);
 		
-		if(isPowered != wasPowered) {
-			if(isPowered) {
-				world.playBlockEvent(null, tilePos, LevelListener.EVENT_PISTON_RETRACT, 0);
-				world.setBlockDataNotify(tilePos, meta | MASK_POWERED);
-			}else {
-				world.playBlockEvent(null, tilePos, LevelListener.EVENT_PISTON_EXTEND, 0);
-				world.setBlockDataNotify(tilePos, meta & ~MASK_POWERED);
-			}
-		}
+        final var event = (isPowered) ? LevelListener.EVENT_PISTON_RETRACT : LevelListener.EVENT_PISTON_EXTEND;
+        world.playBlockEvent(null, tilePos, event, 0);
 	}
 
+    @Override
+    public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+		final int meta = block.getBlockData();
+		final boolean wasPowered = (meta & MASK_POWERED) != 0;
+		final boolean isPowered = line.hasNeighborSignal(indexInLine);
+		if (isPowered == wasPowered) return;
+        block.setBlockData(meta ^ MASK_POWERED);
+
+        final var event = (isPowered) ? LevelListener.EVENT_PISTON_RETRACT : LevelListener.EVENT_PISTON_EXTEND;
+        world.playBlockEvent(null, targetPos, event, 0);
+    }
+
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+		final int meta = block.getBlockData();
+		final boolean wasPowered = (meta & MASK_POWERED) != 0;
+		final boolean isPowered = line.hasNeighborSignal(indexInLine);
+		if (isPowered == wasPowered) return;
+        block.setBlockData(meta ^ MASK_POWERED);
+
+        final var event = (isPowered) ? LevelListener.EVENT_PISTON_RETRACT : LevelListener.EVENT_PISTON_EXTEND;
+        world.playBlockEvent(null, targetPos, event, 0);
+    }
+
+    @Override
+    public void onFlung(@NotNull World world, @NotNull TilePosc sourcePos, int data, @NotNull CarriedBlock block) {
+        block.metadata &= ~MASK_POWERED;
+    }
+
     @Override
     public @NotNull AABBdc getBoundsFromState(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
         double thickness = 3/16d;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTNT.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTNT.java
index 0ecdc1f33..9127650cd 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTNT.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTNT.java
@@ -2,6 +2,8 @@ package net.minecraft.core.block;
 
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.entity.EntityPrimedTNT;
 import net.minecraft.core.enums.EnumDropCause;
@@ -34,6 +36,28 @@ public class BlockLogicTNT extends BlockLogic {
 		}
 	}
 
+	@Override
+	public void onMoved(@NotNull World world, @NotNull TilePosc sourcePos, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line) {
+		if (line.hasNeighborSignal(indexInLine)) {
+			block.setBlockType(Blocks.AIR);
+			line.notifyBlocksOfNeighborChange(indexInLine, Blocks.AIR);
+			EntityPrimedTNT tnt = new EntityPrimedTNT(world, sourcePos.x() + 0.5F, sourcePos.y() + 0.5F, sourcePos.z() + 0.5F);
+			world.entityJoinedWorld(tnt);
+			world.playSoundAtEntity(null, tnt, "tile.tnt.fuse", 1.0F, 1.0F);
+		}
+	}
+
+	@Override
+	public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+		if (line.hasNeighborSignal(indexInLine)) {
+			block.setBlockType(Blocks.AIR);
+			line.notifyBlocksOfNeighborChange(indexInLine, Blocks.AIR);
+			EntityPrimedTNT tnt = new EntityPrimedTNT(world, line.sourcePos(indexInLine).x() + 0.5F, line.sourcePos(indexInLine).y() + 0.5F, line.sourcePos(indexInLine).z() + 0.5F);
+			world.entityJoinedWorld(tnt);
+			world.playSoundAtEntity(null, tnt, "tile.tnt.fuse", 1.0F, 1.0F);
+		}
+	}
+
 	@Override
 	public ItemStack[] getBreakResult(@NotNull World world, @NotNull EnumDropCause dropCause, int data, @Nullable TileEntity tileEntity)
 	{
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTrapDoor.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTrapDoor.java
index c9d86983b..0855f0212 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTrapDoor.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTrapDoor.java
@@ -3,6 +3,8 @@ package net.minecraft.core.block;
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.piston.IMovingBlock;
+import net.minecraft.core.block.piston.MovingLine;
 import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.block.support.PartialSupport;
@@ -132,20 +134,33 @@ public class BlockLogicTrapDoor extends BlockLogic implements IPaintable {
 
     @Override
     public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        if (world.isClientSide) {
-            return;
-        }
+        if (world.isClientSide) return;
+        if (!block.isSignalSource()) return;
 
-        if (this.material != Materials.METAL && this.material != Materials.STEEL && block.isSignalSource() || (this.material == Materials.METAL || this.material == Materials.STEEL) && block.isSignalSource()) {
-            int meta = world.getBlockData(tilePos);
-            boolean isOpened = isTrapdoorOpen(meta);
-            boolean isPowered = world.hasNeighborSignal(tilePos);
+        final int meta = world.getBlockData(tilePos);
+        final boolean isOpened = isTrapdoorOpen(meta);
+        final boolean isPowered = world.hasNeighborSignal(tilePos);
 
-            if (isOpened != isPowered) {
-                world.setBlockDataNotify(tilePos, setTrapdoorOpen(meta, isPowered));
-                world.playBlockEvent(null, tilePos, LevelListener.EVENT_DOOR_SOUND, 0);
-            }
-        }
+        if (isOpened == isPowered) return;
+
+        world.setBlockDataNotify(tilePos, meta ^ MASK_OPEN);
+        world.playBlockEvent(null, tilePos, LevelListener.EVENT_DOOR_SOUND, 0);
+    }
+
+    @Override
+    public void onMovingNeighborChanged(@NotNull World world, @NotNull TilePosc targetPos, @NotNull IMovingBlock block, int indexInLine, @NotNull MovingLine line, @NotNull Block<?> source) {
+        if (world.isClientSide) return;
+        if (!source.isSignalSource()) return;
+
+        final int meta = block.getBlockData();
+        final boolean isOpened = isTrapdoorOpen(meta);
+        final boolean isPowered = line.hasNeighborSignal(indexInLine);
+
+        if (isOpened == isPowered) return;
+
+        block.setBlockData(meta ^ MASK_OPEN);
+        line.notifyBlocksOfNeighborChange(indexInLine, this.block);
+        world.playBlockEvent(null, targetPos, LevelListener.EVENT_DOOR_SOUND, 0);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/Blocks.java b/game/core/src/main/java/net/minecraft/core/block/Blocks.java
index f1c2b4de5..ba2da2dcf 100644
--- a/game/core/src/main/java/net/minecraft/core/block/Blocks.java
+++ b/game/core/src/main/java/net/minecraft/core/block/Blocks.java
@@ -813,7 +813,7 @@ public final class Blocks {
     public static final @NotNull Block<BlockLogicPistonBase> PISTON_BASE_STICKY = register("piston.base.sticky", "minecraft:block/piston_base_sticky", 521, (BlockLogicSupplier<BlockLogicPistonBase>)b -> 
         new BlockLogicPistonBase(
             b, PistonCommon.TYPE_STICKY,
-            (byte)12, (byte)1, null, true, Double.NaN
+            (byte)12, (byte)1, EnumDropCause.SILK_TOUCH, true, Double.NaN
         ))
         .withSound(BlockSounds.STONE).withDisabledNeighborNotifyOnMetadataChange()
         .withTags(BlockTags.MINEABLE_BY_PICKAXE);
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntitySensor.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntitySensor.java
index 81624ba86..e75903a14 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntitySensor.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntitySensor.java
@@ -18,6 +18,7 @@ import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
 
 public class TileEntitySensor extends TileEntity {
+    private final AABBd detectionBox = new AABBd();
 
     private int getSightRange(World world, double x, double y, double z, Direction facing) {
         if (facing == Direction.NONE) {
@@ -60,7 +61,7 @@ public class TileEntitySensor extends TileEntity {
         double maxY = Math.max(y1, y2) + 1;
         double maxZ = Math.max(z1, z2) + 1;
 
-        return new AABBd(minX, minY, minZ, maxX, maxY, maxZ);
+        return this.detectionBox.setMin(minX, minY, minZ).setMax(maxX, maxY, maxZ);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/motion/CarriedBlock.java b/game/core/src/main/java/net/minecraft/core/block/motion/CarriedBlock.java
index f7a2ded33..66612a564 100644
--- a/game/core/src/main/java/net/minecraft/core/block/motion/CarriedBlock.java
+++ b/game/core/src/main/java/net/minecraft/core/block/motion/CarriedBlock.java
@@ -13,7 +13,6 @@ import net.minecraft.core.util.helper.MathHelper;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.ICarriable;
 import net.minecraft.core.world.World;
-import net.minecraft.core.world.pos.TilePosc;
 
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java
index 762cb1c4d..5235ee1cb 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java
@@ -54,6 +54,7 @@ public class BlockLogicPistonBase extends BlockLogic {
 		this.typeId = typeId;
 
         assert typeId >= 0 && typeId < 3;
+        assert pushLimit <= 64;
 
         this.pushLimit = pushLimit;
         this.crushLimit = crushLimit;
@@ -310,39 +311,55 @@ public class BlockLogicPistonBase extends BlockLogic {
     }
 
     private static void push(@NotNull World world, @NotNull TilePosc headPos, @NotNull Direction dir, int len) {
-        final var src = new TilePos();
-        final var tar = new TilePos();
-
-        for (int i = len - 1; i >= 0; i--) {
-            src.set(headPos);
-            dir.offsetScaled(src, i);
-            tar.set(src);
-            dir.offset(tar);
-
-            final @NotNull Block<?> block;
-            final int data;
-            var entity = world.getTileEntity(src);
-            
-            if (entity instanceof TileEntityMovingPistonBlock moving && !moving.isSourcePiston) {
-                assert world.getBlockType(src) == Blocks.PISTON_MOVING;
-                block = moving.movingBlock;
-                data = moving.movingBlockData;
-                entity = moving.movingTileEntity;
-            } else {
-                block = world.getBlockType(src);
-                data = world.getBlockData(src);
+        if (len <= 0) return;
+        final TilePos pos1 = new TilePos(), pos2 = new TilePos();
+
+        final var line = new MovingLine(len, world, dir, headPos, true);
+
+        /* block type,data,entity lookup */ {
+            final var queryPos = pos1.set(headPos);
+            for (int i = 0;  i < len;  i++, queryPos.add(dir)) {
+                line.blocks[i] = world.getBlockType(queryPos);
+                line.data[i] = (byte)world.getBlockData(queryPos);
+                line.entities[i] = world.getTileEntity(queryPos);
+
+                // We DON'T handle moving piston, as it is ruled out in movability test
+                assert line.blocks[i] != Blocks.PISTON_MOVING;
+                assert !(line.entities[i] instanceof TileEntityMovingPistonBlock);
             }
+        }
+
+        line.computeConductMask();
+        line.computeSignalMask();
+        
+        /* move */ {
+            final var tar = dir.offsetScaled(pos1.set(headPos), len);
+            final var src = dir.offsetScaled(pos2.set(headPos), len - 1);
+
+            for (int i = len;  i > 0;  src.sub(dir), tar.sub(dir)) {
+                i--;
+                assert tar != headPos;
 
-            world.removeTileEntity(src);
-            final var moving = new TileEntityMovingPistonBlock(
-                MovingType.EXTENDING,
-                block, data, entity, dir);
-            world.setBlockTypeDataEntity(tar, Blocks.PISTON_MOVING, data, moving);
+                world.removeTileEntity(src);
+                world.setBlockTypeRaw(src, Blocks.AIR);
 
-            if (i >= 1) world.setBlockTypeRaw(src, Blocks.AIR);
+                world.setBlockTypeDataEntity(tar, Blocks.PISTON_MOVING, line.data[i], line.createMoving(i));
+            }
+        }
+
+        /* update data */ {
+            final var tar = dir.offsetScaled(pos1.set(headPos), len);
+            final var src = dir.offsetScaled(pos2.set(headPos), len - 1);
+
+            for (int i = len;  i > 0;  src.sub(dir), tar.sub(dir)) {
+                i--;
+                assert tar != headPos;
+                line.blocks[i].onMoved(world, src, tar, line.movingBlocks[i], i, line);
+            }
         }
     }
 
+
     private void breakTail(
         @NotNull EnumDropCause cause, @NotNull World world, @NotNull TilePosc headPos,
         @NotNull Direction dir, int off
@@ -383,8 +400,9 @@ public class BlockLogicPistonBase extends BlockLogic {
         final int blockMeta = world.getBlockData(headPos);
         final var tileEntity = world.getTileEntity(headPos);
         world.removeTileEntity(headPos);
-        world.setBlockTypeNotify(headPos, Blocks.AIR);
-        
+        world.setBlockTypeRaw(headPos, Blocks.AIR);
+        world.notifyBlocksOfNeighborChange(headPos, Blocks.AIR);
+
         if (world.isClientSide) return;
         
         var flungBlock = new EntityFallingBlock(world, 
@@ -399,6 +417,7 @@ public class BlockLogicPistonBase extends BlockLogic {
 
         world.entityJoinedWorld(flungBlock);
         flungBlock.fling(vx, vy, vz, 1f);
+        block.onFlung(world, headPos, blockMeta, flungBlock.carriedBlock);
     }
 
     private void retract(@NotNull World world, @NotNull TilePosc tilePos, int data) {
@@ -406,13 +425,11 @@ public class BlockLogicPistonBase extends BlockLogic {
         final var dir = Direction.fromId(DIRECTION.get(data));
         final var headPos = new TilePos(tilePos).add(dir);
 
-        if (world.getTileEntity(headPos) instanceof TileEntityMovingPistonBlock moving) {
-            if (!moving.isExtending) {
-                moving.finalTick();
-            } else {
-                world.setBlockType(headPos, Blocks.AIR);
-            }
+        if (world.getTileEntity(headPos) instanceof TileEntityMovingPistonBlock moving && !moving.isExtending) {
+            moving.finalTick();
         }
+        world.setBlockTypeRaw(headPos, Blocks.AIR);
+        world.removeTileEntity(headPos);
 
         final var moving = new TileEntityMovingPistonBlock(
             MovingType.RETRACTING_PISTON,
@@ -420,12 +437,7 @@ public class BlockLogicPistonBase extends BlockLogic {
         world.setBlockTypeRaw(tilePos, Blocks.AIR);
         world.setBlockTypeDataEntity(tilePos, Blocks.PISTON_MOVING, data, moving);
 
-        if (this.canPull) {
-            pull(world, headPos, dir);
-        } else {
-            world.setBlockType(headPos, Blocks.AIR);
-            world.removeTileEntity(headPos);
-        }
+        if (this.canPull) pull(world, headPos, dir);
 
         // logically, retraction should only update head immediately, and base 3 ticks after
         // but this emulates the old behavior
@@ -454,17 +466,21 @@ public class BlockLogicPistonBase extends BlockLogic {
             world.setBlockType(tar, Blocks.AIR);
             return;
         }
-        
+
         final var block = world.getBlockType(src);
         final var data = world.getBlockData(src);
-
         world.removeTileEntity(src);
-        final var moving = new TileEntityMovingPistonBlock(
-            MovingType.RETRACTING,
-            block, data, entity, dir);
-        world.setBlockTypeDataEntity(tar, Blocks.PISTON_MOVING, data, moving);
 
-        world.setBlockTypeNotify(src, Blocks.AIR);
+        final var line = new MovingLine(1, world, dir, headPos, false);
+        line.blocks[0] = block;
+        line.data[0] = (byte)data;
+        line.entities[0] = entity;
+
+        world.setBlockTypeRaw(src, Blocks.AIR);
+        world.setBlockTypeDataEntity(tar, Blocks.PISTON_MOVING, data, line.createMoving(0));
+
+        block.onMoved(world, src, tar, line.movingBlocks[0], 0, line);
+        world.notifyBlocksOfNeighborChange(src, Blocks.AIR);
     }
 
     // BlockLogic impl:
@@ -563,14 +579,14 @@ public class BlockLogicPistonBase extends BlockLogic {
     public int getPistonPushReaction(@NotNull World world, @NotNull TilePosc tilePos) {
         final var data = fixLegacyBaseData(world, tilePos);
 
-        return (IS_DETACHED_OR_EXTENDED.bool(data))
+        return (IS_EXTENDED.bool(data))
             ? Material.PISTON_CANT_PUSH
             : super.getPistonPushReaction(world, tilePos);
     }
 
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
-        data = validateBaseData(data, this);
+        data = fixLegacyBaseData(world, tilePos, this, data);
         if (IS_DETACHED.bool(data)) return;
 
         final var dir = Direction.fromId(DIRECTION.get(data));
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonMoving.java b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonMoving.java
index 67f43c127..d5d6fbc35 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonMoving.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonMoving.java
@@ -21,7 +21,6 @@ import java.util.List;
 
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
 
 public class BlockLogicPistonMoving extends BlockLogic {
@@ -60,6 +59,30 @@ public class BlockLogicPistonMoving extends BlockLogic {
         return false;
     }
 
+    @Override
+    public boolean renderAsNormalBlockOnCondition(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
+        if (!(source.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock moving)) return false;
+        if (!moving.isInFinalTick()) return false;
+        // TODO: purely data-based api
+        return moving.movingBlock.renderAsNormalBlockOnCondition(source, tilePos);
+    }
+
+    @Override
+    public boolean isEmittingSignal(@NotNull WorldSource source, @NotNull TilePosc tilePos, @NotNull Side side) {
+        if (!(source.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock moving)) return false;
+        if (!moving.isInFinalTick()) return false;
+        // TODO: purely data-based api
+        return moving.movingBlock.isEmittingSignal(source, tilePos, side);
+    }
+
+    @Override
+    public boolean isEmittingDirectSignal(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        if (!(world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock moving)) return false;
+        if (!moving.isInFinalTick()) return false;
+        // TODO: purely data-based api
+        return moving.movingBlock.isEmittingDirectSignal(world, tilePos, side);
+    }
+
     @Override
     public ItemStack[] getBreakResult(@NotNull World world, @NotNull EnumDropCause dropCause, int data, @Nullable TileEntity tileEntity) {
         return null;
@@ -68,8 +91,8 @@ public class BlockLogicPistonMoving extends BlockLogic {
     @Override
     public void dropWithCause(@NotNull World world, @NotNull EnumDropCause dropCause, @NotNull TilePosc tilePos, int data, @Nullable TileEntity tileEntity, @Nullable Player player) {
         if (world.isClientSide) return;
-        if (world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock piston) {
-            piston.movingBlock().dropWithCause(world, EnumDropCause.WORLD, tilePos, data, null, null);
+        if (world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock moving) {
+            moving.movingBlock().dropWithCause(world, EnumDropCause.WORLD, tilePos, data, null, null);
         }
     }
     
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/IMovingBlock.java b/game/core/src/main/java/net/minecraft/core/block/piston/IMovingBlock.java
new file mode 100644
index 000000000..d35dca5bd
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/IMovingBlock.java
@@ -0,0 +1,17 @@
+package net.minecraft.core.block.piston;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.entity.TileEntity;
+
+public interface IMovingBlock {
+    @NotNull Block<?> getBlockType();
+    int getBlockData();
+    @Nullable TileEntity getTileEntity();
+    
+    void setBlockType(@NotNull Block<?> block);
+    void setBlockData(int data);
+    void setTileEntity(@Nullable TileEntity tileEntity);
+}
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/MovingLine.java b/game/core/src/main/java/net/minecraft/core/block/piston/MovingLine.java
new file mode 100644
index 000000000..53312fb3c
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/MovingLine.java
@@ -0,0 +1,264 @@
+package net.minecraft.core.block.piston;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.entity.TileEntity;
+import net.minecraft.core.block.piston.PistonCommon.MovingType;
+import net.minecraft.core.util.helper.Direction;
+import net.minecraft.core.util.helper.Side;
+import net.minecraft.core.world.World;
+import net.minecraft.core.world.pos.TilePos;
+import net.minecraft.core.world.pos.TilePosc;
+
+public final class MovingLine {
+    public final int len;
+    public final boolean isExtending;
+
+    public final @NotNull Direction direction;
+    public final @NotNull TilePosc origin;
+
+    final @NotNull World world;
+    
+    final @NotNull Block<?>[] blocks;
+    final byte[] data;
+    final @Nullable TileEntity[] entities;
+    long conductMask = 0;
+    long signalMask = 0;
+
+    final @Nullable TileEntityMovingPistonBlock[] movingBlocks;
+
+    MovingLine(int len, @NotNull World world, @NotNull Direction dir, @NotNull TilePosc origin, boolean isExtending) {
+        assert len < 64;
+        this.len = len;
+        this.direction = dir;
+        this.origin = origin;
+        this.world = world;
+        this.isExtending = isExtending;
+        this.blocks = new Block<?>[len];
+        this.data = new byte[len];
+        this.entities = new TileEntity[len];
+        this.movingBlocks = new TileEntityMovingPistonBlock[len];
+    }
+
+    public @NotNull Direction movingDirection() {
+        return (this.isExtending) ? direction : direction.opposite();
+    }
+
+    public @NotNull Block<?> blockType(int index) {
+        assert index >= 0 && index < this.len;
+        assert this.blocks.length >= this.len;
+        return this.blocks[index];
+    }
+
+    public byte blockData(int index) {
+        assert index >= 0 && index < this.len;
+        assert this.data.length >= this.len;
+        return this.data[index];
+    }
+    
+    public boolean hasNeighborSignal(int index) {
+        assert index >= 0 && index < Math.min(this.len, 64);
+        
+        final long bitMask = 1L << index;
+        return (signalMask & bitMask) != 0;
+    }
+
+    public static enum LineDirection {
+        NONE(0b00),
+        FORWARD(0b01),
+        BACKWARD(0b10),
+        BOTH(0b11);
+
+        LineDirection(int mask) {
+            this.mask = (byte)mask;
+        }
+
+        public static @NotNull LineDirection from(@NotNull MovingLine line, @NotNull Direction dir) {
+            if (dir == Direction.NONE) return NONE;
+            return switch (line.direction.id ^ dir.id) {
+                default -> NONE;
+                case 0 -> FORWARD;
+                case 1 -> BACKWARD;
+            };
+        }
+
+        public final byte mask;
+        public boolean isForward() {
+            return (this.mask & FORWARD.mask) != 0;
+        }
+        public boolean isBackward() {
+            return (this.mask & BACKWARD.mask) != 0;
+        }
+    }
+
+    long computeConductMask() {
+        final var queryPos = new TilePos();
+        long set;
+
+        set = 0b1L;
+        queryPos.set(this.origin);
+        for (int i = 0;  i < this.len;  i++, set <<= 1, queryPos.add(this.direction)) {
+            if (!world.isBlockNormalCube(queryPos)) continue;
+            if (this.blocks[i] == Blocks.BLOCK_REDSTONE) continue;
+            if (this.blocks[i] == Blocks.PUMPKIN_REDSTONE) continue;
+            if (this.blocks[i] == Blocks.MATCHER_ACTIVE) continue;
+            this.conductMask |= set;
+        }
+
+        return this.conductMask;
+    }
+
+    // TODO: this can be far more efficient, but the pumpkin edgecase sucks
+    long computeSignalMask() {
+        final var p0 = new TilePos();
+        final var p1 = new TilePos();
+        long set;
+        
+        set = 0b10;
+        p0.set(this.origin);
+        for (int i = 0;  i < this.len - 1;  i++, set <<= 1, p0.add(this.direction)) {
+            var sig = this.hasSignal(this.world, p0, i, this.direction.opposite().side(), p1);
+            this.signalMask |= (sig) ? set : 0;
+        }
+        set = 0b01;
+        p0.set(this.origin).add(this.direction);
+        for (int i = 1;  i < this.len;  i++, set <<= 1, p0.add(this.direction)) {
+            var sig = this.hasSignal(this.world, p0, i, this.direction.side(), p1);
+            this.signalMask |= (sig) ? set : 0;
+        }
+        return this.signalMask;
+    }
+
+    private boolean hasSignal(
+        @NotNull World world, @NotNull TilePosc tilePos,
+        int index, @NotNull Side side, @NotNull TilePos queryPos
+    ) {
+        final Direction d1, d2;
+        d1 = this.direction;
+        d2 = this.direction.opposite();
+        final @NotNull Block<?> block = this.blocks[index];
+        if (world.isBlockNormalCube(tilePos)) direct_sig: {
+            if (block == Blocks.BLOCK_REDSTONE) break direct_sig;
+            if (block == Blocks.PUMPKIN_REDSTONE) break direct_sig;
+            if (block == Blocks.MATCHER_ACTIVE) break direct_sig;
+            if (world.hasDirectSignal(queryPos.set(tilePos).add(d1), d1.side())) return true;
+            if (world.hasDirectSignal(queryPos.set(tilePos).add(d2), d2.side())) return true;
+        }
+        if (block == Blocks.PUMPKIN_REDSTONE) {
+            final var pumpkinDir = Direction.fromId(this.data[index]);
+            if (pumpkinDir != d1 && world.hasDirectSignal(queryPos.set(tilePos).add(d1), d1.side())) return true;
+            if (pumpkinDir != d2 && world.hasDirectSignal(queryPos.set(tilePos).add(d2), d2.side())) return true;
+        }
+        return block.isEmittingSignal(world, tilePos, side);
+    }
+
+    public void updateSignal(int index, @NotNull LineDirection direction, boolean isPowered) {
+        final var qpos = new TilePos();
+        if (direction.isBackward()) {
+            this.setPowered(index - 1, isPowered);
+            this.notifyBlockOfNeighborChange(index - 1, qpos, this.blocks[index]);
+        }
+        if (direction.isForward()) {
+            this.setPowered(index + 1, isPowered);
+            this.notifyBlockOfNeighborChange(index + 1, qpos, this.blocks[index]);
+        }
+    }
+
+    public void updateDirectSignal(int index, @NotNull LineDirection direction, boolean isPowered) {
+        final var qpos = new TilePos();
+        for (int i = -1; i <= 1; i += 2) {
+            if (i < 0 && !direction.isBackward()) continue;
+            if (i > 0 && !direction.isForward()) continue;
+
+            int j = index;
+            
+            j += i;
+            this.setPowered(j, isPowered);
+            this.notifyBlockOfNeighborChange(j, qpos, this.blocks[index]);
+            
+            if (!this.canBlockBePowered(j, qpos)) continue;
+
+            j += i;
+            this.setPowered(j, isPowered);
+            this.notifyBlockOfNeighborChange(j, qpos, this.blocks[index]);
+        }
+    }
+
+    public @NotNull TilePosc sourcePos(int index) {
+        return this.getSourcePos(index, new TilePos());
+    }
+
+    public @NotNull TilePosc targetPos(int index) {
+        return this.getTargetPos(index, new TilePos());
+    }
+
+    public @NotNull TilePos getSourcePos(int index, @NotNull TilePos qpos) {
+        assert index >= 0 && index < this.len;
+        qpos.set(this.origin);
+        this.direction.offsetScaled(qpos, index);
+        return qpos;
+    }
+    
+    public @NotNull TilePos getTargetPos(int index, @NotNull TilePos qpos) {
+        assert index >= 0 && index < this.len;
+        qpos.set(this.origin).add(this.movingDirection());
+        this.direction.offsetScaled(qpos, index);
+        return qpos;
+    }
+
+    private boolean canBlockBePowered(int i, @NotNull TilePos qpos) {
+        return this.blocks[i].getMaterial().isSolidBlocking() && 
+            // the only implementers of this only reads block data
+            // TODO refactor this and world.isBlockNormalCube to use a dedicated method
+            this.blocks[i].renderAsNormalBlockOnCondition(this.world, this.getTargetPos(i, qpos)); 
+}
+
+    private void setPowered(int i, boolean f) {
+        if (f) {
+            this.signalMask |= 1L << i;
+            this.signalMask &= (1L << this.len) - 1;
+        } else {
+            this.signalMask &= ~(1L << i);
+        }
+    }
+
+    public void notifyBlocksOfNeighborChange(int index, @Nullable Block<?> source) {
+        assert index >= 0 && index < this.len;
+        if (source == null) source = this.blocks[index];
+
+        final var qpos = new TilePos();
+        this.notifyBlockOfNeighborChange(index - 1, qpos, this.blocks[index]);
+        this.notifyBlockOfNeighborChange(index + 1, qpos, this.blocks[index]);
+    }
+
+    public void notifyBlocksInRadiusOfNeighborChange(int radius, int index, @NotNull Block<?> source) {
+        assert index >= 0 && index < this.len;
+        if (radius <= 0) return;
+        if (source == null) source = this.blocks[index];
+
+        final var qpos = new TilePos();
+        for (int i = Math.max(0, index - radius); i < index; i++) {
+            this.notifyBlockOfNeighborChange(i, qpos, source);
+        }
+
+        for (int i = index + 1; i < Math.min(this.len, index + radius + 1); i++) {
+            this.notifyBlockOfNeighborChange(i, qpos, source);
+        }
+    }
+
+    private void notifyBlockOfNeighborChange(int i, @NotNull TilePos qpos, @NotNull Block<?> source) {
+        if (i < 0 || i >= this.len) return;
+        this.getTargetPos(i, qpos);
+        this.blocks[i].onMovingNeighborChanged(world, qpos, this.movingBlocks[i], i, this, source);
+    }
+
+    @NotNull TileEntityMovingPistonBlock createMoving(int index) {
+        assert index >= 0 && index < this.len;
+        return this.movingBlocks[index] = new TileEntityMovingPistonBlock(
+            (this.isExtending) ? MovingType.EXTENDING : MovingType.RETRACTING,
+            this.blocks[index], this.data[index], this.entities[index], this.direction);
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/PistonCommon.java b/game/core/src/main/java/net/minecraft/core/block/piston/PistonCommon.java
index 568e7f29c..7c628eb80 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/PistonCommon.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/PistonCommon.java
@@ -72,13 +72,7 @@ public abstract class PistonCommon {
 
     public static final int DATA_INVALID = ~0;
 
-    public static int fixLegacyBaseData(@NotNull World world, @NotNull TilePosc basePos) {
-        final var baseBlock = world.getBlockType(basePos);
-        final BlockLogicPistonBase base;
-        assert baseBlock.getLogic() instanceof BlockLogicPistonBase;
-        base = (BlockLogicPistonBase) baseBlock.getLogic();
-        
-        int baseData = world.getBlockData(basePos);
+    public static int fixLegacyBaseData(@NotNull World world, @NotNull TilePosc basePos, @NotNull BlockLogicPistonBase base, int baseData) {
         if (NEW_FORMAT_SIGNATURE.bool(baseData)) return baseData;
 
         baseData = TYPE.set(baseData, base.typeId);
@@ -117,12 +111,26 @@ public abstract class PistonCommon {
             baseData = IS_EXTENDED.set(baseData, 1);
         }
 
-        world.setBlockData(basePos, baseData);
+        return baseData;
+    }
+
+    public static int fixLegacyBaseData(@NotNull World world, @NotNull TilePosc basePos) {
+        final var baseBlock = world.getBlockType(basePos);
+        final BlockLogicPistonBase base;
+        assert baseBlock.getLogic() instanceof BlockLogicPistonBase;
+        base = (BlockLogicPistonBase) baseBlock.getLogic();
+        
+        int baseData = world.getBlockData(basePos);
+        int newBaseData = fixLegacyBaseData(world, basePos, base, baseData);
+        if (newBaseData == baseData) return newBaseData;
+
+        world.setBlockData(basePos, newBaseData);
         return baseData;
     }
 
     public static int validateBaseData(int data, @NotNull BlockLogicPistonBase base) {
         data = TYPE.set(data, base.typeId);
+        data = NEW_FORMAT_SIGNATURE.set(data, 1);
         if (DIRECTION.get(data) > 5) data = DIRECTION.set(data, 0);
         // if (IS_DETACHED.bool(data)) data = IS_EXTENDED.set(data, 1);
         return data;
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java b/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java
index cdde69de5..f45c1cad8 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java
@@ -2,6 +2,7 @@ package net.minecraft.core.block.piston;
 
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.entity.TileEntityDispatcher;
+import net.minecraft.core.block.piston.PistonCommon.MovingType;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.util.helper.Direction;
@@ -19,10 +20,13 @@ import org.joml.primitives.AABBdc;
 import java.util.*;
 import static net.minecraft.core.block.piston.PistonCommon.*;
 
-public class TileEntityMovingPistonBlock extends TileEntity
-{
+/**
+ * TODO: make movingTileEntity tick the moving block
+ */
+public class TileEntityMovingPistonBlock extends TileEntity implements IMovingBlock {
     public static final int TICKS_TO_EXTEND = 2;
     private static final float PROGRESS_DELTA = 1f / TICKS_TO_EXTEND;
+    private static final float VPROGRESS_DELTA = 1f / (TICKS_TO_EXTEND + 1);
 
     @NotNull Block<?> movingBlock;
     int movingBlockData;
@@ -31,6 +35,8 @@ public class TileEntityMovingPistonBlock extends TileEntity
     @NotNull Direction direction;
     boolean isExtending;
     boolean isSourcePiston;
+    int lifetime = 0;
+    long lastTick;
     float progressCur = 0F, progressPrev = 0F;
 
     // these would otherwise be reallocated each tick, might as well store them here
@@ -89,7 +95,7 @@ public class TileEntityMovingPistonBlock extends TileEntity
         this.initCollision();
     }
 
-    private final void initCollision() {
+    private void initCollision() {
         assert this.worldObj != null;
         assert this.worldObj.isChunkLoaded(new ChunkPos(this.tilePos));
 
@@ -113,12 +119,10 @@ public class TileEntityMovingPistonBlock extends TileEntity
     @Override
     public void tick() {
         if (!this.isCollisionInited) this.initCollision();
-        this.progressPrev = this.progressCur;
-        if (this.progressCur > (1.0F - 0x1p-12F)) {
+        this.lastTick = this.worldObj.getWorldTime();
+        if (this.lifetime ++ >= TICKS_TO_EXTEND) {
             this.finalTick();
         } else {
-            this.progressCur += PROGRESS_DELTA;
-
             this.sweptCollision.set(this.collision);
             this.sweptCollision.translate(this.directionScaled);
             this.sweptCollision.union(this.collision);
@@ -137,14 +141,18 @@ public class TileEntityMovingPistonBlock extends TileEntity
 
     public float progress(float partialTick) {
         partialTick = Math.min(partialTick, 1.0F);
-        return this.progressPrev + (this.progressCur - this.progressPrev) * partialTick;
+        final var progressPrev = this.lifetime * VPROGRESS_DELTA;
+        return progressPrev + VPROGRESS_DELTA * partialTick;
     }
 
     public float progressSigned(float partialTick) {
         final var p = this.progress(partialTick);
-        return (this.isExtending)
-        ? p - 1.0F
-        : 1.0F - p;
+        return (this.isExtending) ? p - 1.0F : 1.0F - p;
+    }
+
+    public boolean isInFinalTick() {
+        if (this.lifetime < TICKS_TO_EXTEND) return false;
+        return this.lastTick + 1 >= this.worldObj.getWorldTime();
     }
 
     public float xOffset(float partialTick) {
@@ -173,10 +181,12 @@ public class TileEntityMovingPistonBlock extends TileEntity
             this.movingTileEntity.validate();
             this.worldObj.replaceTileEntity(selfPos, this.movingTileEntity);
         }
-        
-        Block.disableNormalEntityLogic = true;
-        this.movingBlock.onPlacedByWorld(this.worldObj, selfPos);
-        Block.disableNormalEntityLogic = false;
+
+        this.movingBlock.onMoveComplete(worldObj, selfPos);
+
+        // Block.disableNormalEntityLogic = true;
+        // this.movingBlock.onPlacedByWorld(this.worldObj, selfPos);
+        // Block.disableNormalEntityLogic = false;
 
         assert this.movingTileEntity == null || this.worldObj.getTileEntity(selfPos) == this.movingTileEntity 
             : "Piston failed to actually move entity!";
@@ -193,7 +203,8 @@ public class TileEntityMovingPistonBlock extends TileEntity
             this.movingTileEntity = TileEntityDispatcher.createAndLoadEntity(entityTag);
         }
         this.direction = Direction.fromId(compoundTag.getInteger("facing"));
-        this.progressPrev = this.progressCur = compoundTag.getFloat("progress");
+        this.lifetime = compoundTag.getInteger("lifetime");
+        // this.progressPrev = this.progressCur = compoundTag.getFloat("progress");
         this.isExtending = compoundTag.getBoolean("extending");
     }
 
@@ -207,7 +218,37 @@ public class TileEntityMovingPistonBlock extends TileEntity
             compoundTag.put("tileEntity", entityTag);
         }
         compoundTag.putInt("facing", this.direction.id);
-        compoundTag.putFloat("progress", this.progressPrev);
+        compoundTag.putInt("lifetime", this.lifetime);
         compoundTag.putBoolean("extending", this.isExtending);
     }
+    @Override
+    public @NotNull Block<?> getBlockType() {
+        return this.movingBlock;
+    }
+
+    @Override
+    public int getBlockData() {
+        return this.movingBlockData;
+    }
+
+    @Override
+    public @Nullable TileEntity getTileEntity() {
+        return this.movingTileEntity;
+    }
+
+    @Override
+    public void setBlockType(@NotNull Block<?> block) {
+        this.movingBlock = block;
+    }
+
+    @Override
+    public void setBlockData(int data) {
+        this.movingBlockData = data;
+        this.worldObj.setBlockData(this.tilePos, data);
+    }
+
+    @Override
+    public void setTileEntity(@Nullable TileEntity tileEntity) {
+        this.movingTileEntity = tileEntity;
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/EntityFallingBlock.java b/game/core/src/main/java/net/minecraft/core/entity/EntityFallingBlock.java
index d6577933a..e4f2f14cd 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/EntityFallingBlock.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/EntityFallingBlock.java
@@ -181,15 +181,13 @@ public class EntityFallingBlock extends Entity
                     !this.fallChecker.canFallInto(this.world, this.carriedBlock, blockPos.down(new TilePos()));
 
                 final boolean isPlaced = shoudPlaceHere &&
-                    this.world.setBlockType(blockPos, Blocks.getBlock(this.carriedBlock.blockId));
+                    this.world.setBlockTypeData(blockPos, this.carriedBlock.block(), this.carriedBlock.metadata);
 
                 if (!isPlaced) {
                     this.drop();
                     break fall_or_drop;
                 }
 
-                this.world.setBlockData(blockPos, this.carriedBlock.metadata);
-
                 final var tileEntity = this.carriedBlock.entity;
                 if (tileEntity != null) {
                     tileEntity.validate();
@@ -203,6 +201,11 @@ public class EntityFallingBlock extends Entity
                     }
                 }
 
+                // TODO there needs to be a smarter way of doing this :|
+                Block.disableNormalEntityLogic = true;
+                this.carriedBlock.block().onPlacedByWorld(world, blockPos);
+                Block.disableNormalEntityLogic = false;
+
                 this.world.notifyBlockChange(blockPos, this.carriedBlock.block());
             }
 
diff --git a/game/core/src/main/java/net/minecraft/core/world/World.java b/game/core/src/main/java/net/minecraft/core/world/World.java
index ad34e9345..8677e4835 100644
--- a/game/core/src/main/java/net/minecraft/core/world/World.java
+++ b/game/core/src/main/java/net/minecraft/core/world/World.java
@@ -2537,15 +2537,16 @@ public abstract class World implements MutableWorldSource {
 
     public boolean hasSignal(final @NotNull TilePosc tilePos, final @NotNull Side side) {
         final @NotNull Block<?> block = this.getBlockType(tilePos);
-        if (this.isBlockNormalCube(tilePos) && block != Blocks.BLOCK_REDSTONE && block != Blocks.PUMPKIN_REDSTONE && block != Blocks.MATCHER_ACTIVE) {
-            return this.hasDirectSignal(tilePos);
+        if (this.isBlockNormalCube(tilePos)) direct_sig: {
+            if (block == Blocks.BLOCK_REDSTONE) break direct_sig;
+            if (block == Blocks.PUMPKIN_REDSTONE) break direct_sig;
+            if (block == Blocks.MATCHER_ACTIVE) break direct_sig;
+            if (this.hasDirectSignal(tilePos)) return true;
         }
-
         if (block == Blocks.PUMPKIN_REDSTONE) {
-            return this.pumpkinHasDirectSignal(tilePos) || block.isEmittingSignal(this, tilePos, side);
-        } else {
-            return block.isEmittingSignal(this, tilePos, side);
+            if (this.pumpkinHasDirectSignal(tilePos)) return true;
         }
+        return block.isEmittingSignal(this, tilePos, side);
     }
 
     @Deprecated
@@ -2553,8 +2554,9 @@ public abstract class World implements MutableWorldSource {
         return this.hasSignal(new TilePos(x, y, z), side);
     }
 
+    // TODO: generalize direct signal face exclusion
     // Special version of hasDirectSignal that excludes the front side of the redstone lantern.
-    private boolean pumpkinHasDirectSignal(final @NotNull TilePosc tilePos) {
+    public boolean pumpkinHasDirectSignal(final @NotNull TilePosc tilePos) {
         for (final @NotNull Side side : Side.sides) {
             if (Side.fromId(this.getBlockData(tilePos)) == side) {
                 continue;
