diff --git a/game/client/src/main/java/net/minecraft/client/Minecraft.java b/game/client/src/main/java/net/minecraft/client/Minecraft.java
index f4c5a0f09..84665e3d8 100644
--- a/game/client/src/main/java/net/minecraft/client/Minecraft.java
+++ b/game/client/src/main/java/net/minecraft/client/Minecraft.java
@@ -1184,9 +1184,9 @@ public class Minecraft
 
         if (leftClickDown && this.objectMouseOver instanceof final @NotNull HitResult.Tile hitTile) {
             final double xPlaced;
-            if (hitTile.side.getAxis() == Axis.X) {
+            if (hitTile.side.axis() == Axis.X) {
                 xPlaced = hitTile.location.x() - hitTile.tilePos.x();
-            } else if (hitTile.side.getAxis() == Axis.Z) {
+            } else if (hitTile.side.axis() == Axis.Z) {
                 xPlaced = hitTile.location.z() - hitTile.tilePos.z();
             } else {
                 xPlaced = hitTile.location.x() - hitTile.tilePos.x();
@@ -1225,9 +1225,9 @@ public class Minecraft
                         MathHelper.floor(this.thePlayer.z)
                     );
 
-                    Direction playerDirection = Direction.getHorizontalDirection(this.thePlayer);
+                    Direction playerDirection = Direction.getHorizontalLockable(this.thePlayer);
                     if (playerDirection.isVertical()) break bridge;
-                    Side side = playerDirection.getSide();
+                    Side side = playerDirection.side();
 
                     double yPlaced = 0.5d;
                     double xPlaced = 0.5d;
@@ -1262,9 +1262,9 @@ public class Minecraft
         } else if (this.objectMouseOver instanceof HitResult.Tile hitTile) {
             double xPlaced;
             double yPlaced = hitTile.location.y() - hitTile.tilePos.y();
-            if (hitTile.side.getAxis() == Axis.X) {
+            if (hitTile.side.axis() == Axis.X) {
                 xPlaced = hitTile.location.x() - hitTile.tilePos.x();
-            } else if (hitTile.side.getAxis() == Axis.Z) {
+            } else if (hitTile.side.axis() == Axis.Z) {
                 xPlaced = hitTile.location.z() - hitTile.tilePos.z();
             } else {
                 xPlaced = hitTile.location.x() - hitTile.tilePos.x();
@@ -1777,10 +1777,10 @@ public class Minecraft
                 GameSettings.ROTATION_OVERLAY_MODE.value++;
                 GameSettings.ROTATION_OVERLAY_MODE.value %= 4;
             } else {
-                if (this.thePlayer.rotationLock == null || this.thePlayer.rotationLock == Direction.NONE) {
-                    this.thePlayer.rotationLock = Direction.getDirection(this.thePlayer);
-                    this.thePlayer.rotationLockHorizontal = Direction.getHorizontalDirection(this.thePlayer);
-                    this.thePlayer.rotationLockVertical = Direction.getVerticalDirection(this.thePlayer);
+                if (this.thePlayer.rotationLock == null) {
+                    this.thePlayer.rotationLock = Direction.getLockable(this.thePlayer);
+                    this.thePlayer.rotationLockHorizontal = Direction.getHorizontalLockable(this.thePlayer);
+                    this.thePlayer.rotationLockVertical = Direction.getVerticalLockable(this.thePlayer);
                 } else {
                     this.thePlayer.rotationLock = null;
                     this.thePlayer.rotationLockHorizontal = null;
diff --git a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java
index 75279c62e..52068315a 100644
--- a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java
+++ b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java
@@ -441,8 +441,8 @@ public class PlayerLocal extends Player {
                 pushDirection = Direction.SOUTH;
             }
             final float pushStrength = 0.1F;
-            this.xd = pushDirection.getOffsetX() * pushStrength;
-            this.zd = pushDirection.getOffsetZ() * pushStrength;
+            this.xd = pushDirection.offsetX() * pushStrength;
+            this.zd = pushDirection.offsetZ() * pushStrength;
             return true;
         }
         return false;
diff --git a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java
index c1e672c65..96eda5d1a 100644
--- a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java
+++ b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java
@@ -237,9 +237,9 @@ public class PlayerLocalMultiplayer extends PlayerLocal implements Player.Player
     	super.syncPlacementMode();
     	
     	byte[] data = new byte[4];
-    	data[0] = (byte) (rotationLock != null ? rotationLock.getId() : -1);
-    	data[1] = (byte) (rotationLockHorizontal != null ? rotationLockHorizontal.getId() : -1);
-    	data[2] = (byte) (rotationLockVertical != null ? rotationLockVertical.getId() : -1);
+    	data[0] = (byte) (rotationLock != null ? rotationLock.id : -1);
+    	data[1] = (byte) (rotationLockHorizontal != null ? rotationLockHorizontal.id : -1);
+    	data[2] = (byte) (rotationLockVertical != null ? rotationLockVertical.id : -1);
     	data[3] = (byte) (placementModeOverride != null ? placementModeOverride.index() : -1);
     	
     	sendQueue.addToSendQueue(new PacketCustomPayload(PacketCustomPayload.CHANNEL_ROTATION_LOCK, data));
diff --git a/game/client/src/main/java/net/minecraft/client/gui/hud/HudIngame.java b/game/client/src/main/java/net/minecraft/client/gui/hud/HudIngame.java
index e315fd2c7..221b99f5e 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/hud/HudIngame.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/hud/HudIngame.java
@@ -265,7 +265,7 @@ public class HudIngame extends Gui {
 
                     int yaw = DynamicTexture.pmod(java.lang.Math.round(this.mc.thePlayer.yRot), 360);
                     int pitch = java.lang.Math.round(this.mc.thePlayer.xRot);
-                    drawDebugScreenLine("Facing: " + yaw + "°, " + pitch + "° (" + I18n.getInstance().translateKey(getFacingDirection(this.mc.thePlayer).getTranslationKey()) + ")");
+                    drawDebugScreenLine("Facing: " + yaw + "°, " + pitch + "° (" + I18n.getInstance().translateKey(getFacingDirection(this.mc.thePlayer).translationKey) + ")");
                 } else {
                     drawDebugScreenLine("Hold a compass for more information");
                 }
@@ -366,9 +366,9 @@ public class HudIngame extends Gui {
                         int lightZ = hitTile.tilePos.z();
 
                         Side side = hitTile.side;
-                        lightX += side.getOffsetX();
-                        lightY += side.getOffsetY();
-                        lightZ += side.getOffsetZ();
+                        lightX += side.offsetX();
+                        lightY += side.offsetY();
+                        lightZ += side.offsetZ();
 
                         drawDebugScreenLineRight("Light: Sky: " + this.mc.currentWorld.getSavedLightValue(LightLayer.Sky, lightX, lightY, lightZ) + " Block: " + this.mc.currentWorld.getSavedLightValue(LightLayer.Block, lightX, lightY, lightZ));
                     }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentInfoOverlay.java b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentInfoOverlay.java
index 61f3dbabf..a9b59b7a7 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentInfoOverlay.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentInfoOverlay.java
@@ -103,7 +103,7 @@ public class HudComponentInfoOverlay extends HudComponentMovable {
                 }
             }
             if (GameSettings.DIRECTION_IN_OVERLAY.value) {
-                lines.add(new RenderLine(I18n.getInstance().translateKey(HudIngame.getFacingDirection(mc.thePlayer).getTranslationKey()), 0xFFFFFFFF));
+                lines.add(new RenderLine(I18n.getInstance().translateKey(HudIngame.getFacingDirection(mc.thePlayer).translationKey), 0xFFFFFFFF));
             }
         }
 
@@ -176,7 +176,7 @@ public class HudComponentInfoOverlay extends HudComponentMovable {
         }
 
         boolean dirEnabled = GameSettings.DIRECTION_IN_OVERLAY.value;
-        lines.add(new RenderLine(I18n.getInstance().translateKey(Direction.NORTH.getTranslationKey()), getPreviewColor(0xFFFFFFFF, dirEnabled)));
+        lines.add(new RenderLine(I18n.getInstance().translateKey(Direction.NORTH.translationKey), getPreviewColor(0xFFFFFFFF, dirEnabled)));
 
         int clockColor = (GameSettings.OVERLAY_MODE.value == 2) ? 0xFFFFFF80 : 0xFFFFFFFF;
         boolean timeEnabled = GameSettings.TIME_IN_OVERLAY.value;
diff --git a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentRotationLock.java b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentRotationLock.java
index 8e8e5f8b9..51c83c970 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentRotationLock.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentRotationLock.java
@@ -21,7 +21,7 @@ public class HudComponentRotationLock
         if (mc.thePlayer == null) {
             return GameSettings.IMMERSIVE_MODE.drawOverlays();
         }
-        return mc.thePlayer.rotationLock != null && mc.thePlayer.rotationLock != Direction.NONE && GameSettings.IMMERSIVE_MODE.drawOverlays();
+        return mc.thePlayer.rotationLock != null && GameSettings.IMMERSIVE_MODE.drawOverlays();
     }
 
     @Override
@@ -40,10 +40,10 @@ public class HudComponentRotationLock
         boolean facingUp = player.xRot < -45;
         boolean facingDown = player.xRot > 45;
 
-        Direction dir = Direction.getDirection(player);
+        Direction dir = Direction.getLockable(player);
 
         if (dir.isHorizontal()) {
-            dir = dir.rotate(-Direction.getHorizontalDirection(player.yRot).rotate(2).index);
+            dir = dir.rotateY(Direction.fromYaw(player.yRot).rotateY(2).legacyIndex());
 
             if (facingUp) {
                 if (dir == Direction.SOUTH) dir = Direction.UP;
@@ -70,10 +70,10 @@ public class HudComponentRotationLock
         }
 
         String[] dirTextures = new String[]{"down", "up", "back", "front", "left", "right"};
-        hud.drawGuiIcon(x, y, 16, 16, TextureRegistry.getTexture("minecraft:gui/hud/rotation/" + prefix + "_" + dirTextures[dir.getId()]));
-        dir = Direction.getVerticalDirection(player);
+        hud.drawGuiIcon(x, y, 16, 16, TextureRegistry.getTexture("minecraft:gui/hud/rotation/" + prefix + "_" + dirTextures[dir.id]));
+        dir = Direction.getVerticalLockable(player);
         dirTextures = new String[]{"downer", "upper", "back", "front", "left", "right"};
-        hud.drawGuiIcon(x + 16, y + 4, 8, 8, TextureRegistry.getTexture("minecraft:gui/hud/rotation/" + prefix + "_" + dirTextures[dir.getId()]));
+        hud.drawGuiIcon(x + 16, y + 4, 8, 8, TextureRegistry.getTexture("minecraft:gui/hud/rotation/" + prefix + "_" + dirTextures[dir.id]));
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java
index f2dabc63c..b6f7835b4 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java
@@ -27,6 +27,7 @@ import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.IPainted;
 import net.minecraft.core.block.piston.BlockLogicPistonBase;
 import net.minecraft.core.block.piston.BlockLogicPistonHead;
+import net.minecraft.core.block.piston.PistonCommon;
 import net.minecraft.core.entity.EntityItem;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.ItemStack;
@@ -217,13 +218,12 @@ public class ViewerCategoryBlock extends ModelViewerCategory<ViewerCategoryBlock
                     meta |= BlockLogicFullyRotatable.directionToMeta(Direction.SOUTH);
                 }
                 if (Block.hasLogicClass(block, BlockLogicPistonBase.class)) {
-                    meta |= Direction.SOUTH.getId();
+                    meta |= Direction.SOUTH.id;
                 }
                 if (Block.hasLogicClass(block, BlockLogicPistonHead.class)) {
-                    meta |= Direction.SOUTH.getId();
-                    if (block == Blocks.PISTON_HEAD_STEEL) {
-                        meta = BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_STEEL, meta);
-                    }
+                    meta |= Direction.SOUTH.id;
+                    if (block == Blocks.PISTON_HEAD_STEEL)
+                        meta = PistonCommon.TYPE.set(meta, PistonCommon.TYPE_STEEL);
                 }
                 if (Block.hasLogicClass(block, BlockLogicLadder.class)) {
                     meta |= ((BlockLogicLadder)block.getLogic()).getMetaForSide(Side.SOUTH);
diff --git a/game/client/src/main/java/net/minecraft/client/player/controller/PlayerControllerMP.java b/game/client/src/main/java/net/minecraft/client/player/controller/PlayerControllerMP.java
index 7d210aca9..356fd58d1 100644
--- a/game/client/src/main/java/net/minecraft/client/player/controller/PlayerControllerMP.java
+++ b/game/client/src/main/java/net/minecraft/client/player/controller/PlayerControllerMP.java
@@ -77,7 +77,7 @@ public class PlayerControllerMP
     @Override
     public boolean useOrPlaceItemStackOnTile(final @NotNull Player player, final @NotNull World world, final @Nullable ItemStack itemStack, final @NotNull TilePosc tilePos, final @NotNull Side side, final double xPlaced, final double yPlaced) {
         this.syncCurrentPlayItem();
-        this.packetHandler.addToSendQueue(new PacketUseOrPlaceItemStack(tilePos.x(), tilePos.y(), tilePos.z(), side.getDirection(), player.inventory.getCurrentItem(), xPlaced, yPlaced, PacketUseOrPlaceItemStack.TYPE_USE_OR_PLACE_ON_TILE));
+        this.packetHandler.addToSendQueue(new PacketUseOrPlaceItemStack(tilePos.x(), tilePos.y(), tilePos.z(), side.direction(), player.inventory.getCurrentItem(), xPlaced, yPlaced, PacketUseOrPlaceItemStack.TYPE_USE_OR_PLACE_ON_TILE));
         return super.useOrPlaceItemStackOnTile(player, world, itemStack, tilePos, side, xPlaced, yPlaced);
     }
 
@@ -91,7 +91,7 @@ public class PlayerControllerMP
     @Override
     public boolean placeItemStackOnTile(final @NotNull Player player, final @NotNull World world, final @Nullable ItemStack itemStack, final @NotNull TilePosc tilePos, final @NotNull Side side, final double xPlaced, final double yPlaced) {
         this.syncCurrentPlayItem();
-        this.packetHandler.addToSendQueue(new PacketUseOrPlaceItemStack(tilePos.x(), tilePos.y(), tilePos.z(), side.getDirection(), player.inventory.getCurrentItem(), xPlaced, yPlaced, PacketUseOrPlaceItemStack.TYPE_PLACE_ONLY));
+        this.packetHandler.addToSendQueue(new PacketUseOrPlaceItemStack(tilePos.x(), tilePos.y(), tilePos.z(), side.direction(), player.inventory.getCurrentItem(), xPlaced, yPlaced, PacketUseOrPlaceItemStack.TYPE_PLACE_ONLY));
         return super.placeItemStackOnTile(player, world, itemStack, tilePos, side, xPlaced, yPlaced);
     }
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/ParticleEngine.java b/game/client/src/main/java/net/minecraft/client/render/ParticleEngine.java
index 28305d4eb..3158261e4 100644
--- a/game/client/src/main/java/net/minecraft/client/render/ParticleEngine.java
+++ b/game/client/src/main/java/net/minecraft/client/render/ParticleEngine.java
@@ -180,7 +180,7 @@ public final class ParticleEngine {
                     double xp = blockPos.x() + (xx + 0.5D) / amount;
                     double yp = blockPos.y() + (yy + 0.5D) / amount;
                     double zp = blockPos.z() + (zz + 0.5D) / amount;
-                    Side side = Side.getSideById(this.rand.nextInt(6));
+                    Side side = Side.fromId(this.rand.nextInt(6));
                     add((new ParticleBlockBreaking(this.world, xp, yp, zp, xp - blockPos.x() - 0.5D, yp - blockPos.y() - 0.5D, zp - blockPos.z() - 0.5D, block, side, data)).init(blockPos));
                 }
             }
diff --git a/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java b/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java
index e140ae371..42dd596ca 100644
--- a/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java
+++ b/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java
@@ -1890,10 +1890,10 @@ public class RenderGlobal
                 break;
 
             case EVENT_DISPENSER_PARTICLES: // Dispenser eject particles
-                Direction direction = Direction.getDirectionById(data);
-                int xOff = direction.getOffsetX();
-                int yOff = direction.getOffsetY();
-                int zOff = direction.getOffsetZ();
+                Direction direction = Direction.fromId(data);
+                int xOff = direction.offsetX();
+                int yOff = direction.offsetY();
+                int zOff = direction.offsetZ();
                 double px = (double) x + (double) xOff * 0.6D + 0.5D;
                 double py = (double) y + (double) yOff * 0.6D + 0.5D;
                 double pz = (double) z + (double) zOff * 0.6D + 0.5D;
@@ -1946,9 +1946,9 @@ public class RenderGlobal
                 break;
             case EVENT_CONDUIT_PORT_PARTICLES: // Conduit port open particles
                 Side s1 = unpackEventConduitSide(data);
-                int xO = s1.getOffsetX();
-                int yO = s1.getOffsetY();
-                int zO = s1.getOffsetZ();
+                int xO = s1.offsetX();
+                int yO = s1.offsetY();
+                int zO = s1.offsetZ();
                 double xP = (double) x + (double) xO * 0.6D + 0.5D;
                 double yP = (double) y + (double) yO * 0.6D + 0.5D;
                 double zP = (double) z + (double) zO * 0.6D + 0.5D;
@@ -1973,13 +1973,13 @@ public class RenderGlobal
 
                 double d = 0.0625;
 
-                Direction dir = Direction.directions[random.nextInt(Direction.directions.length)];
+                Direction dir = Direction.ID_MAP[random.nextInt(Direction.count)];
 
                 double lx = (double) x + random.nextFloat();
                 double ly = (double) y + random.nextFloat();
                 double lz = (double) z + random.nextFloat();
 
-                if (!this.world.isBlockOpaqueCube(x + dir.getOffsetX(), y + dir.getOffsetY(), z + dir.getOffsetZ())) {
+                if (!this.world.isBlockOpaqueCube(x + dir.offsetX(), y + dir.offsetY(), z + dir.offsetZ())) {
                     switch (dir) {
                         case UP -> ly = (y + 1) + d;
                         case DOWN -> ly = (y) - d;
@@ -2010,7 +2010,7 @@ public class RenderGlobal
 
     public static Side unpackEventConduitSide(int packed) {
         int sideId = (packed >> 4) & 0x07;
-        return Side.getSideById(sideId);
+        return Side.fromId(sideId);
     }
 
     public static int unpackEventConduitStrength(int packed) {
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelAxisAligned.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelAxisAligned.java
index e7e375603..663aaece5 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelAxisAligned.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelAxisAligned.java
@@ -57,8 +57,8 @@ public class BlockModelAxisAligned<T extends BlockLogic> extends BlockModelStand
     }
 
     public Side getRotatedSide(Side side, int data) {
-        int d = (6 * (data & 3) + side.getId());
-        return d < Sides.orientationLookUpXYZAligned.length ? Side.getSideById(Sides.orientationLookUpXYZAligned[d]) : Side.NONE;
+        int d = (6 * (data & 3) + side.id);
+        return d < Sides.orientationLookUpXYZAligned.length ? Side.fromId(Sides.orientationLookUpXYZAligned[d]) : Side.NONE;
     }
 
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java
index 326f64167..92e9a6ad9 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java
@@ -386,6 +386,7 @@ public final class BlockModelDispatcher
 
         addDispatch(new BlockModelGenericRepeater<>(Blocks.REPEATER_IDLE));
         addDispatch(new BlockModelGenericRepeater<>(Blocks.REPEATER_ACTIVE));
+        addDispatch(new BlockModelGenericTimer<>(Blocks.TIMER));
 
         addDispatch(new BlockModelGenericPiston<>(Blocks.PISTON_BASE, loadDataModel("minecraft:block/piston/base"), loadDataModel("minecraft:block/piston/extended")));
         addDispatch(new BlockModelGenericPiston<>(Blocks.PISTON_BASE_STICKY, loadDataModel("minecraft:block/piston_sticky/base"), loadDataModel("minecraft:block/piston_sticky/extended")));
@@ -393,11 +394,27 @@ public final class BlockModelDispatcher
 
         addDispatch(new BlockModelEmpty<>(Blocks.PISTON_MOVING).setAllTextures((IconCoordinate) null));
         addDispatch(new BlockModelGenericPistonHead<>(Blocks.PISTON_HEAD,
-            loadDataModel("minecraft:block/piston/head"), loadDataModel("minecraft:block/piston_sticky/head"), loadDataModel("minecraft:block/piston_steel/head"),
-            loadDataModel("minecraft:block/piston/head_half"), loadDataModel("minecraft:block/piston_sticky/head_half"), loadDataModel("minecraft:block/piston_steel/head_half")));
+            loadDataModel("minecraft:block/piston/head"),
+            loadDataModel("minecraft:block/piston_sticky/head"),
+            loadDataModel("minecraft:block/piston_steel/head"),
+            // loadDataModel("minecraft:block/piston/head_half"),
+            // loadDataModel("minecraft:block/piston_sticky/head_half"),
+            // loadDataModel("minecraft:block/piston_steel/head_half"),
+            loadDataModel("minecraft:block/piston/head_short"),
+            loadDataModel("minecraft:block/piston_sticky/head_short"),
+            loadDataModel("minecraft:block/piston_steel/head_short")
+        ));
         addDispatch(new BlockModelGenericPistonHead<>(Blocks.PISTON_HEAD_STEEL,
-            loadDataModel("minecraft:block/piston/head"), loadDataModel("minecraft:block/piston_sticky/head"), loadDataModel("minecraft:block/piston_steel/head"),
-            loadDataModel("minecraft:block/piston/head_half"), loadDataModel("minecraft:block/piston_sticky/head_half"), loadDataModel("minecraft:block/piston_steel/head_half")));
+            loadDataModel("minecraft:block/piston/head"),
+            loadDataModel("minecraft:block/piston_sticky/head"),
+            loadDataModel("minecraft:block/piston_steel/head"),
+            // loadDataModel("minecraft:block/piston/head_half"),
+            // loadDataModel("minecraft:block/piston_sticky/head_half"),
+            // loadDataModel("minecraft:block/piston_steel/head_half"),
+            loadDataModel("minecraft:block/piston/head_short"),
+            loadDataModel("minecraft:block/piston_sticky/head_short"),
+            loadDataModel("minecraft:block/piston_steel/head_short")
+        ));
 
         addDispatch(new BlockModelGeneric<>(Blocks.NOTEBLOCK, loadDataModel("minecraft:block/noteblock")));
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDoor.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDoor.java
index 2f7d22214..a45b80f7d 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDoor.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDoor.java
@@ -73,8 +73,8 @@ public class BlockModelDoor<T extends BlockLogicDoor> extends BlockModelStandard
     protected boolean shouldFlipTexture(@NotNull Side side, int meta) {
         if (side == Side.TOP || side == Side.BOTTOM) return false;
         int state = this.block.getLogic().getRotation(meta);
-        int l = state / 2 + (side.getId() & 1 ^ state);
-        if ((state == 0 || state == 2) ^ (side.getId() <= 3)) {
+        int l = state / 2 + (side.id & 1 ^ state);
+        if ((state == 0 || state == 2) ^ (side.id <= 3)) {
             return false;
         }
         l += (meta & 4) / 4;
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFluid.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFluid.java
index f9645e68b..5e9a2d954 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFluid.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFluid.java
@@ -202,7 +202,7 @@ public class BlockModelFluid<T extends BlockLogicFluid> extends BlockModelStanda
                 case 2 -> posB.set(x - 1, y, z); // West
                 case 3 -> posB.set(x + 1, y, z); // East
             }
-            IconCoordinate texture = getBlockTextureFromSideAndMetadata(Side.getSideById(side + 2), meta);
+            IconCoordinate texture = getBlockTextureFromSideAndMetadata(Side.fromId(side + 2), meta);
             if (!renderBlocks.renderAllFaces && !sideRenderFlags[side]) {
                 continue;
             }
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFullyRotatable.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFullyRotatable.java
index d8b60d364..7c24811eb 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFullyRotatable.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelFullyRotatable.java
@@ -71,9 +71,9 @@ public class BlockModelFullyRotatable<T extends BlockLogic> extends BlockModelSt
     @Override
     public @Nullable IconCoordinate getBlockTextureFromSideAndMetadata(@NotNull Side side, int data) {
         final Direction dir = BlockLogicFullyRotatable.metaToDirection(data);
-        if (side == dir.getSide()) {
+        if (side == dir.side()) {
             return this.blockTextures.get(Side.TOP);
         }
-        return side.getDirection() != dir.getOpposite() ? this.blockTextures.get(Side.NORTH) : this.blockTextures.get(Side.BOTTOM);
+        return side.direction() != dir.opposite() ? this.blockTextures.get(Side.NORTH) : this.blockTextures.get(Side.BOTTOM);
     }
 }
\ No newline at end of file
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelHorizontalRotation.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelHorizontalRotation.java
index abdc96b9b..b11b7f690 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelHorizontalRotation.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelHorizontalRotation.java
@@ -26,8 +26,8 @@ public class BlockModelHorizontalRotation<T extends BlockLogic> extends BlockMod
     @Override
     public @Nullable IconCoordinate getBlockTextureFromSideAndMetadata(@NotNull Side side, int data)
     {
-        int index = Sides.orientationLookUpHorizontal[6 * Math.min(data & BlockLogicRotatable.MASK_DIRECTION, 5) + side.getId()];
+        int index = Sides.orientationLookUpHorizontal[6 * Math.min(data & BlockLogicRotatable.MASK_DIRECTION, 5) + side.id];
         if (index >= Sides.orientationLookUpHorizontal.length) return this.blockTextures.get(Side.BOTTOM);
-        return super.getBlockTextureFromSideAndMetadata(Side.getSideById(index), data);
+        return super.getBlockTextureFromSideAndMetadata(Side.fromId(index), data);
     }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelRotatable.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelRotatable.java
index d73c9d3f5..cc944aa94 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelRotatable.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelRotatable.java
@@ -15,7 +15,7 @@ public class BlockModelRotatable<T extends BlockLogic> extends BlockModelStandar
 
     @Override
     public @Nullable IconCoordinate getBlockTextureFromSideAndMetadata(@NotNull Side side, int data) {
-        int index = Sides.orientationLookUp[6 * data + side.getId()];
-        return this.blockTextures.get(Side.getSideById(index));
+        int index = Sides.orientationLookUp[6 * data + side.id];
+        return this.blockTextures.get(Side.fromId(index));
     }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelStandard.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelStandard.java
index 666176f6e..0bd99cd7c 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelStandard.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelStandard.java
@@ -276,7 +276,7 @@ public class BlockModelStandard<T extends BlockLogic> extends BlockModel<T> {
         public TextureLayer set(@Nullable IconCoordinate coordinate, Side... sides) {
             for (Side s : sides) {
                 assert s != Side.NONE : "Cannot set texture on a 'NONE' side!";
-                this.coordinates[s.getId()] = coordinate;
+                this.coordinates[s.id] = coordinate;
             }
 
             this.hasTexture = false;
@@ -309,7 +309,7 @@ public class BlockModelStandard<T extends BlockLogic> extends BlockModel<T> {
 
         public @Nullable IconCoordinate get(@NotNull Side side) {
             assert side != Side.NONE : "Cannot set texture on a 'NONE' side!";
-            return this.coordinates[side.getId()];
+            return this.coordinates[side.id];
         }
 
         public @Nullable IconCoordinate get(int index) {
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelTrapDoor.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelTrapDoor.java
index 14cf408fd..5ed5ccf1f 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelTrapDoor.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelTrapDoor.java
@@ -79,10 +79,10 @@ public class BlockModelTrapDoor<T extends BlockLogic> extends BlockModelStandard
     public @Nullable IconCoordinate getBlockTextureFromSideAndMetadata(@NotNull Side side, int data) {
         int orientation = data & 0b11;
         if (BlockLogicTrapDoor.isTrapdoorOpen(data)) {
-            int index = Sides.orientationLookUpTrapdoorOpen[6 * orientation + side.getId()];
+            int index = Sides.orientationLookUpTrapdoorOpen[6 * orientation + side.id];
             return this.blockTextures.get(index);
         } else {
-            if (side.getAxis() == Axis.Y) {
+            if (side.axis() == Axis.Y) {
                 return this.blockTextures.get(Side.BOTTOM);
             } else {
                 return this.blockTextures.get(Side.SOUTH);
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelVeryRotatable.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelVeryRotatable.java
index 470553f46..c4b82c4ff 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelVeryRotatable.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelVeryRotatable.java
@@ -64,9 +64,9 @@ public class BlockModelVeryRotatable<T extends BlockLogic> extends BlockModelSta
         final Direction dir = BlockLogicVeryRotatable.metaToDirection(data);
         return switch (dir) { // I love brute forcing fixes so much :3
             case SOUTH -> side;
-            case EAST -> side.getDirection().rotate(1).getSide();
-            case NORTH -> side.getDirection().rotate(2).getSide();
-            case WEST -> side.getDirection().rotate(3).getSide();
+            case EAST -> side.direction().rotateY(3).side();
+            case NORTH -> side.direction().rotateY(2).side();
+            case WEST -> side.direction().rotateY(1).side();
             case UP -> switch (side) {
                 case NORTH -> Side.TOP;
                 case SOUTH -> Side.BOTTOM;
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/RenderBlocks.java b/game/client/src/main/java/net/minecraft/client/render/block/model/RenderBlocks.java
index 81a98d504..01ec287f8 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/RenderBlocks.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/RenderBlocks.java
@@ -91,9 +91,9 @@ public final class RenderBlocks {
 
     public void setRenderSide(@NotNull Side side, boolean shouldRender) {
         if (!shouldRender) {
-            this.renderBitMask |= (byte) (1 << side.getId());
+            this.renderBitMask |= (byte) (1 << side.id);
         } else {
-            this.renderBitMask &= (byte) ~(1 << side.getId());
+            this.renderBitMask &= (byte) ~(1 << side.id);
         }
     }
 
@@ -212,7 +212,7 @@ public final class RenderBlocks {
                               int lefX, int lefY, int lefZ, float lefP, float rigP
     ) {
         IconCoordinate tex = blockModel.getBlockTexture(worldSource, tilePos, side);
-        if (tex == null || ((this.renderBitMask >> side.getId()) & 1) != 0) return false;
+        if (tex == null || ((this.renderBitMask >> side.id) & 1) != 0) return false;
 
         boolean rendered = false;
         if (this.renderAllFaces || blockModel.shouldSideBeRendered(worldSource, bounds, tilePos.add(dirX, dirY, dirZ, new TilePos()), side, meta)) {
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPiston.java b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPiston.java
index df0372f54..cd622adc5 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPiston.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPiston.java
@@ -4,7 +4,7 @@ import net.minecraft.client.render.tessellator.TessellatorGeneral;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.BlockLogic;
 import net.minecraft.core.block.BlockLogicVeryRotatable;
-import net.minecraft.core.block.piston.BlockLogicPistonBase;
+import net.minecraft.core.block.piston.PistonCommon;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.pos.TilePos;
@@ -46,8 +46,8 @@ public class BlockModelGenericPiston<T extends BlockLogic> extends BlockModelGen
 
     @Override
     public @NotNull StaticBlockModel getModelFromData(int data) {
-        boolean extended = /*noCulling || */(data & BlockLogicPistonBase.MASK_POWERED) != 0;
-//        Direction direction = BlockLogicPistonBase.getDirection(meta);
-        return extended ? this.extended : super.getModelFromData(data);
+        return (/*noCulling || */PistonCommon.IS_DETACHED_OR_EXTENDED.bool(data)) 
+            ? this.extended 
+            : super.getModelFromData(data);
     }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPistonHead.java b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPistonHead.java
index 21092070a..4373c7aa8 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPistonHead.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericPistonHead.java
@@ -3,9 +3,7 @@ package net.minecraft.client.render.block.model.generic;
 import net.minecraft.client.render.tessellator.TessellatorGeneral;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.BlockLogic;
-import net.minecraft.core.block.BlockLogicVeryRotatable;
-import net.minecraft.core.block.piston.BlockLogicPistonBase;
-import net.minecraft.core.block.piston.BlockLogicPistonHead;
+import net.minecraft.core.block.piston.PistonCommon;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.pos.TilePos;
@@ -14,55 +12,50 @@ import org.useless.dragonfly.data.block.BlockModelData;
 import org.useless.dragonfly.models.block.StaticBlockModel;
 
 public class BlockModelGenericPistonHead<T extends BlockLogic> extends BlockModelGenericVeryRotatable<T> {
-    public final @NotNull StaticBlockModel sticky;
-    public final @NotNull StaticBlockModel steel;
-    public final @NotNull StaticBlockModel baseHalf;
-    public final @NotNull StaticBlockModel stickyHalf;
-    public final @NotNull StaticBlockModel steelHalf;
-    public BlockModelGenericPistonHead(@NotNull Block<T> block, @NotNull BlockModelData piston, @NotNull BlockModelData sticky, @NotNull BlockModelData steel, @NotNull BlockModelData pistonHalf, @NotNull BlockModelData stickyHalf, @NotNull BlockModelData steelHalf) {
+
+    public final @NotNull StaticBlockModel[] normal;
+    public final @NotNull StaticBlockModel[] schort;
+    // // it seems like the short versions are good enough for hiding the shaft while extending
+    // // should we delete the block model assets as well?
+    // public final @NotNull StaticBlockModel[] half;
+
+    public BlockModelGenericPistonHead(
+        @NotNull Block<T> block, 
+        @NotNull BlockModelData piston, @NotNull BlockModelData sticky, @NotNull BlockModelData steel, 
+        // @NotNull BlockModelData pistonHalf, @NotNull BlockModelData stickyHalf, @NotNull BlockModelData steelHalf,
+        @NotNull BlockModelData pistonShort, @NotNull BlockModelData stickyShort, @NotNull BlockModelData steelShort
+    ) {
         super(block, piston);
-        this.sticky = sticky.asModel();
-        this.steel = steel.asModel();
-        this.baseHalf = pistonHalf.asModel();
-        this.stickyHalf = stickyHalf.asModel();
-        this.steelHalf = steelHalf.asModel();
+        this.normal = new StaticBlockModel[] { piston.asModel(), sticky.asModel(), steel.asModel() };
+        // this.half = new StaticBlockModel[] { pistonHalf.asModel(), stickyHalf.asModel(), steelHalf.asModel() };
+        this.schort = new StaticBlockModel[] { pistonShort.asModel(), stickyShort.asModel(), steelShort.asModel() };
     }
+
     public void renderPistonHeadNoCulling(@NotNull TessellatorGeneral tessellator, @NotNull WorldSource worldSource, int meta, @NotNull TilePos tilePos, boolean flag) {
-        var model = switch (BlockLogicPistonHead.getPistonType(meta)) {
-            case BlockLogicPistonHead.TYPE_STICKY -> flag ? this.sticky : this.stickyHalf;
-            case BlockLogicPistonHead.TYPE_STEEL -> flag ? this.steel : this.steelHalf;
-            default -> flag ? this.staticModel : this.baseHalf;
-        };
-        final Direction dir = BlockLogicVeryRotatable.metaToDirection(worldSource.getBlockData(tilePos));
+        final var type = PistonCommon.TYPE.get(meta);
+        final var model = (flag) ? normal[type] : schort[type];
+
+        final var dir = Direction.fromId(PistonCommon.DIRECTION.get(meta));
+        final int rx, ry, rz;
         switch (dir) {
-            case UP -> {
-                model.renderAttached(this, tessellator, worldSource, tilePos, 1, 0, 0, 0, 0, 0, false, false, null);
-            }
-            case DOWN -> {
-                model.renderAttached(this, tessellator, worldSource, tilePos, -1, 0, 0, 0, 0, 0, false, false, null);
-            }
-            case NORTH -> {
-                model.renderAttached(this, tessellator, worldSource, tilePos, 0, 0, 0, 0, 0, 0, false, false, null);
-            }
-            case SOUTH -> {
-                model.renderAttached(this, tessellator, worldSource, tilePos, 0, 2, 0, 0, 0, 0, false, false, null);
-            }
-            case WEST -> {
-                model.renderAttached(this, tessellator, worldSource, tilePos, 0, 1, 0, 0, 0, 0, false, false, null);
-            }
-            case EAST -> {
-                model.renderAttached(this, tessellator, worldSource, tilePos, 0, 3, 0, 0, 0, 0, false, false, null);
-            }
+            case UP    -> { rx =  1; ry = 0; rz = 0; }
+            case DOWN  -> { rx = -1; ry = 0; rz = 0; }
+            case NORTH -> { rx =  0; ry = 0; rz = 0; }
+            case SOUTH -> { rx =  0; ry = 2; rz = 0; }
+            case WEST  -> { rx =  0; ry = 1; rz = 0; }
+            case EAST  -> { rx =  0; ry = 3; rz = 0; }
+            default -> { return; }
         }
-    }
 
+        model.renderAttached(this, tessellator, worldSource, tilePos, rx, ry, rz, 0, 0, 0, false, false, null);
+    }
 
     @Override
     public @NotNull StaticBlockModel getModelFromData(int data) {
-        return switch (BlockLogicPistonHead.getPistonType(data)) {
-            case BlockLogicPistonHead.TYPE_STICKY -> this.sticky;
-            case BlockLogicPistonHead.TYPE_STEEL -> this.steel;
-            default -> this.staticModel;
-        };
+        int type = PistonCommon.TYPE.get(data);
+        if (type >= 3) type = 0;
+        return PistonCommon.IS_DETACHED.bool(data)
+            ? schort[type]
+            : normal[type];
     }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericRope.java b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericRope.java
index 86d6f3407..44b83b468 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericRope.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericRope.java
@@ -27,8 +27,7 @@ public class BlockModelGenericRope<T extends BlockLogic> extends BlockModelGener
 
     @Override
     public boolean renderAttached(@NotNull TessellatorGeneral tessellator, @NotNull WorldSource worldSource, @NotNull TilePosc tilePos, boolean cullFaces, @Nullable IconCoordinate overrideTexture) {
-        Block<?> below = worldSource.getBlockType(tilePos.down(new TilePos()));
-        boolean hasFloor = below != null && below.canPlaceOnSurface();
+        boolean hasFloor = worldSource.isBlockOpaqueCube(tilePos.down(new TilePos()));
         boolean hasBlockNegX = canConnect(worldSource, tilePos, Direction.WEST);
         boolean hasBlockPosX = canConnect(worldSource, tilePos, Direction.EAST);
         boolean hasBlockNegY = canConnect(worldSource, tilePos, Direction.DOWN) || hasFloor;
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericTimer.java b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericTimer.java
new file mode 100644
index 000000000..c063da90f
--- /dev/null
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/generic/BlockModelGenericTimer.java
@@ -0,0 +1,38 @@
+package net.minecraft.client.render.block.model.generic;
+
+import net.minecraft.client.render.block.model.BlockModelDispatcher;
+import net.minecraft.client.render.tessellator.TessellatorGeneral;
+import net.minecraft.client.render.texture.stitcher.IconCoordinate;
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.BlockLogicRepeater;
+import net.minecraft.core.block.BlockLogicTimer;
+import net.minecraft.core.world.WorldSource;
+import net.minecraft.core.world.pos.TilePosc;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.useless.dragonfly.models.block.StaticBlockModel;
+
+public class BlockModelGenericTimer<T extends BlockLogicTimer> extends BlockModelGeneric<T> {
+    public final @NotNull StaticBlockModel[] modelsA = new StaticBlockModel[4];
+    public final @NotNull StaticBlockModel[] modelsB = new StaticBlockModel[4];
+    public BlockModelGenericTimer(@NotNull Block<T> block) {
+        super(block, BlockModelDispatcher.loadDataModel("minecraft:block/timer/0_a"));
+        for (int i = 0; i < 4; i++) {
+            this.modelsA[i] = BlockModelDispatcher.loadDataModel("minecraft:block/timer/" + i + "_a").asModel();
+            this.modelsB[i] = BlockModelDispatcher.loadDataModel("minecraft:block/timer/" + i + "_b").asModel();
+        }
+    }
+
+    @Override
+    public boolean renderAttached(@NotNull TessellatorGeneral tessellator, @NotNull WorldSource worldSource, @NotNull TilePosc tilePos, boolean cullFaces, @Nullable IconCoordinate overrideTexture) {
+        final int meta = worldSource.getBlockData(tilePos);
+        final int axis = meta & BlockLogicTimer.MASK_AXIS;
+        final int output = meta & BlockLogicTimer.MASK_OUTPUT;
+        final int tickDelay = meta & BlockLogicTimer.MASK_TICK_DELAY;
+
+        final @NotNull StaticBlockModel[] modelSet = (output == BlockLogicTimer.OUTPUT_A) ? this.modelsB : this.modelsA;
+        final int rotY = axis == BlockLogicTimer.AXIS_WEST_EAST ? 0 : 1;
+
+        return (modelSet[tickDelay >> 2]).renderAttached(this, tessellator, worldSource, tilePos, 0, rotY, 0, 0, 0, 0, false, cullFaces, overrideTexture);
+    }
+}
diff --git a/game/client/src/main/java/net/minecraft/client/render/item/model/ItemModelDispatcher.java b/game/client/src/main/java/net/minecraft/client/render/item/model/ItemModelDispatcher.java
index 0621b6019..a7c79b435 100644
--- a/game/client/src/main/java/net/minecraft/client/render/item/model/ItemModelDispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/render/item/model/ItemModelDispatcher.java
@@ -287,6 +287,8 @@ public class ItemModelDispatcher extends Dispatcher<Item, ItemModel> {
         addDispatch(new ItemModelMap(Items.MAP, "minecraft"));
         addDispatch(new ItemModelStandard(Items.TOOL_SHEARS, "minecraft"));
 
+        addDispatch(new ItemModelStandard(Items.TIMER, "minecraft"));
+
         addDispatch(new ItemModelStandard(Items.NETHERCOAL, "minecraft").setFullBright());
 
         addDispatch(new ItemModelStandard(Items.ARMOR_HELMET_STEEL, "minecraft"));
diff --git a/game/client/src/main/java/net/minecraft/client/render/particle/Particle.java b/game/client/src/main/java/net/minecraft/client/render/particle/Particle.java
index cf6ec065b..66f7294e6 100644
--- a/game/client/src/main/java/net/minecraft/client/render/particle/Particle.java
+++ b/game/client/src/main/java/net/minecraft/client/render/particle/Particle.java
@@ -150,7 +150,7 @@ public class Particle {
             switch (hitTile.side) {
                 case EAST:
                 case WEST: {
-                    this.x = hitTile.location.x() + hitTile.side.getOffsetX() * 0.1d * this.size;
+                    this.x = hitTile.location.x() + hitTile.side.offsetX() * 0.1d * this.size;
                     this.y += yd;
                     this.z += zd;
                     this.xd = 0;
@@ -160,7 +160,7 @@ public class Particle {
                     this.onGround = true;
                 case BOTTOM: {
                     this.x += xd;
-                    this.y = hitTile.location.y() + hitTile.side.getOffsetY() * 0.1d * this.size;
+                    this.y = hitTile.location.y() + hitTile.side.offsetY() * 0.1d * this.size;
                     this.z += zd;
                     this.yd = 0;
                     break;
@@ -169,7 +169,7 @@ public class Particle {
                 case NORTH: {
                     this.x += xd;
                     this.y += yd;
-                    this.z = hitTile.location.z() + hitTile.side.getOffsetZ() * 0.1d * this.size;
+                    this.z = hitTile.location.z() + hitTile.side.offsetZ() * 0.1d * this.size;
                     this.zd = 0;
                     break;
                 }
@@ -240,7 +240,7 @@ public class Particle {
                 if (tMin < maxT) {
                     return new HitResult.Tile(
                         tilePos,
-                        direction.getSide(),
+                        direction.side(),
                         new Vector3d(ray.oX + tMin * ray.dX, ray.oY + tMin * ray.dY, ray.oZ + tMin * ray.dZ));
                 }
             }
diff --git a/game/client/src/main/java/net/minecraft/client/render/particle/ParticleDispatcher.java b/game/client/src/main/java/net/minecraft/client/render/particle/ParticleDispatcher.java
index d67c0d5ca..d1a1dbea5 100644
--- a/game/client/src/main/java/net/minecraft/client/render/particle/ParticleDispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/render/particle/ParticleDispatcher.java
@@ -253,7 +253,7 @@ public final class ParticleDispatcher extends Dispatcher<String, ParticleEntry>
             public Particle newParticle(@NotNull World world, double x, double y, double z, double motionX, double motionY, double motionZ, int data) {
                 float pitch = 0;
                 float yaw = 0;
-                switch (Direction.getDirectionById(data)) {
+                switch (Direction.fromId(data)) {
                     case DOWN -> {
                         pitch = 0.0f;
                         yaw = 0.0f;
diff --git a/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererMovingPiston.java b/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererMovingPiston.java
index 496689bb9..c5b41a1ba 100644
--- a/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererMovingPiston.java
+++ b/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererMovingPiston.java
@@ -15,7 +15,6 @@ import net.minecraft.client.render.tessellator.TessellatorGeneral;
 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.BlockLogicPistonBaseSteel;
 import net.minecraft.core.block.piston.BlockLogicPistonHead;
 import net.minecraft.core.block.piston.TileEntityMovingPistonBlock;
 import net.minecraft.core.world.BlocksContainer;
@@ -33,8 +32,8 @@ public class TileEntityRendererMovingPiston extends TileEntityRenderer<TileEntit
     @Override
     public void doRender(TessellatorGeneral tessellator, TileEntityMovingPistonBlock tileEntity, double x, double y, double z,
                          float partialTick) {
-        Block<?> block = tileEntity.getMovedBlock();
-        if (tileEntity.getProgress(partialTick) < 1.0F) {
+        Block<?> block = tileEntity.movingBlock();
+        if (tileEntity.progress(partialTick) < 1.0F) {
             if (this.container == null || this.container.world != tileEntity.worldObj) {
                 this.container = new BlocksContainer(tileEntity.worldObj);
             }
@@ -45,20 +44,18 @@ public class TileEntityRendererMovingPiston extends TileEntityRenderer<TileEntit
             GLRenderer.enableState(State.BLEND);
             GLRenderer.disableState(State.CULL_FACE);
             tessellator.startDrawingQuads();
-            tessellator.setTranslation(((float) x - (float) tileEntity.tilePos.x) + tileEntity.getXOff(partialTick), ((float) y - (float) tileEntity.tilePos.y) + tileEntity.getYOff(partialTick), ((float) z - (float) tileEntity.tilePos.z) + tileEntity.getZOff(partialTick));
-            if (Block.hasLogicClass(block, BlockLogicPistonHead.class) && tileEntity.getProgress(partialTick) < 0.5F) {
+            tessellator.setTranslation(((float) x - (float) tileEntity.tilePos.x) + tileEntity.xOffset(partialTick), ((float) y - (float) tileEntity.tilePos.y) + tileEntity.yOffset(partialTick), ((float) z - (float) tileEntity.tilePos.z) + tileEntity.zOffset(partialTick));
+            if (Block.hasLogicClass(block, BlockLogicPistonHead.class) && tileEntity.progress(partialTick) < 0.5F) {
                 ((BlockModelGenericPistonHead<?>) BlockModelDispatcher.getInstance().getDispatch(block)).renderPistonHeadNoCulling(tessellator, tileEntity.worldObj, tileEntity.getBlockMeta(), tileEntPos, false);
             } else if (tileEntity.isSourcePiston() && !tileEntity.isExtending()) {
-                BlockModelGenericPiston<?> pistonBase = (BlockModelGenericPiston<?>) BlockModelDispatcher.getInstance().getDispatch(block);
-                if (Block.hasLogicClass(block, BlockLogicPistonBaseSteel.class)) { // Todo remove this terrible hardcoded render code
-                    ((BlockModelGenericPistonHead<?>) BlockModelDispatcher.getInstance().getDispatch(Blocks.PISTON_HEAD_STEEL)).renderPistonHeadNoCulling(tessellator, tileEntity.worldObj, BlockLogicPistonHead.TYPE_STEEL << 3, tileEntPos, tileEntity.getProgress(partialTick) < 0.5F);
-                } else {
-                    ((BlockModelGenericPistonHead<?>) BlockModelDispatcher.getInstance().getDispatch(Blocks.PISTON_HEAD)).renderPistonHeadNoCulling(tessellator, tileEntity.worldObj, tileEntity.getBlockMeta(), tileEntPos, tileEntity.getProgress(partialTick) < 0.5F);
-                }
+                ((BlockModelGenericPistonHead<?>) BlockModelDispatcher.getInstance().getDispatch(Blocks.PISTON_HEAD)).renderPistonHeadNoCulling(tessellator, tileEntity.worldObj, tileEntity.getBlockMeta(), tileEntPos, tileEntity.progress(partialTick) < 0.5F);
                 tessellator.setTranslation((float) x - (float) tileEntity.tilePos.x, (float) y - (float) tileEntity.tilePos.y, (float) z - (float) tileEntity.tilePos.z);
-                pistonBase.renderHeadless(tessellator, tileEntity.worldObj, tileEntPos);
+                ((BlockModelGenericPiston<?>) BlockModelDispatcher.getInstance().getDispatch(block)).renderHeadless(tessellator, tileEntity.worldObj, tileEntPos);
             } else {
-                this.container.setBlock(tileEntity.tilePos.x, tileEntity.tilePos.y, tileEntity.tilePos.z, block.id(), tileEntity.getMovedData(), tileEntity.getMovedEntity());
+                this.container.setBlockTypeData(tileEntity.tilePos, block, tileEntity.movingBlockData());
+                if (tileEntity.movingTileEntity() != null) {
+                    this.container.setTileEntity(tileEntity.tilePos, tileEntity.movingTileEntity());
+                }
                 BlockModelDispatcher.getInstance().getDispatch(block).renderNoCulling(tessellator, this.container, tileEntPos);
                 this.container.setLightReferenceEntity(null);
                 this.container.clear();
@@ -66,11 +63,11 @@ public class TileEntityRendererMovingPiston extends TileEntityRenderer<TileEntit
             tessellator.setTranslation(0.0D, 0.0D, 0.0D);
             tessellator.draw();
 
-            TileEntity movedEntity = tileEntity.getMovedEntity();
+            TileEntity movedEntity = tileEntity.movingTileEntity();
             TileEntityRenderer<TileEntity> renderer = TileEntityRenderDispatcher.instance.getRenderer(movedEntity);
             if (renderer != null) {
                 GLRenderer.pushFrame();
-                GLRenderer.modelM4f().translate(tileEntity.getXOff(partialTick), tileEntity.getYOff(partialTick), tileEntity.getZOff(partialTick));
+                GLRenderer.modelM4f().translate(tileEntity.xOffset(partialTick), tileEntity.yOffset(partialTick), tileEntity.zOffset(partialTick));
                 renderer.doRender(tessellator, movedEntity, x, y, z, partialTick);
                 GLRenderer.popFrame();
             }
diff --git a/game/client/src/main/java/org/useless/dragonfly/data/block/mojang/CompiledBlockModelMojangData.java b/game/client/src/main/java/org/useless/dragonfly/data/block/mojang/CompiledBlockModelMojangData.java
index 2120bd1c3..e48dcc3c2 100644
--- a/game/client/src/main/java/org/useless/dragonfly/data/block/mojang/CompiledBlockModelMojangData.java
+++ b/game/client/src/main/java/org/useless/dragonfly/data/block/mojang/CompiledBlockModelMojangData.java
@@ -80,12 +80,12 @@ public class CompiledBlockModelMojangData {
 
         this.particleIndices = new int[6];
         if (data.particleIndices != null) {
-            this.particleIndices[Direction.UP.getId()] = data.particleIndices.getOrDefault(Direction.UP, 0);
-            this.particleIndices[Direction.DOWN.getId()] = data.particleIndices.getOrDefault(Direction.DOWN, 0);
-            this.particleIndices[Direction.NORTH.getId()] = data.particleIndices.getOrDefault(Direction.NORTH, 0);
-            this.particleIndices[Direction.SOUTH.getId()] = data.particleIndices.getOrDefault(Direction.SOUTH, 0);
-            this.particleIndices[Direction.WEST.getId()] = data.particleIndices.getOrDefault(Direction.WEST, 0);
-            this.particleIndices[Direction.EAST.getId()] = data.particleIndices.getOrDefault(Direction.EAST, 0);
+            this.particleIndices[Direction.UP.id] = data.particleIndices.getOrDefault(Direction.UP, 0);
+            this.particleIndices[Direction.DOWN.id] = data.particleIndices.getOrDefault(Direction.DOWN, 0);
+            this.particleIndices[Direction.NORTH.id] = data.particleIndices.getOrDefault(Direction.NORTH, 0);
+            this.particleIndices[Direction.SOUTH.id] = data.particleIndices.getOrDefault(Direction.SOUTH, 0);
+            this.particleIndices[Direction.WEST.id] = data.particleIndices.getOrDefault(Direction.WEST, 0);
+            this.particleIndices[Direction.EAST.id] = data.particleIndices.getOrDefault(Direction.EAST, 0);
         }
 
         this.renderLayer = data.renderLayer == null ? 0 : data.renderLayer;
diff --git a/game/client/src/main/java/org/useless/dragonfly/data/entity/mojang/Cube.java b/game/client/src/main/java/org/useless/dragonfly/data/entity/mojang/Cube.java
index 813b609a0..2463611a9 100644
--- a/game/client/src/main/java/org/useless/dragonfly/data/entity/mojang/Cube.java
+++ b/game/client/src/main/java/org/useless/dragonfly/data/entity/mojang/Cube.java
@@ -75,17 +75,17 @@ public class Cube {
             final double sizeX = this.size[0];
             final double sizeY = this.size[1];
             final double sizeZ = this.size[2];
-            this.faces[Direction.UP.getId()] = new Face.Builder(u + sizeZ, v, sizeX, sizeZ);
-            this.faces[Direction.DOWN.getId()] = new Face.Builder(u + sizeX + sizeZ, v + sizeZ, sizeX, -sizeZ);
-            this.faces[Direction.NORTH.getId()] = new Face.Builder(u + sizeZ, v + sizeZ, sizeX, sizeY);
-            this.faces[Direction.SOUTH.getId()] = new Face.Builder(u + (sizeZ * 2) + sizeX, v + sizeZ, sizeX, sizeY);
-            this.faces[Direction.EAST.getId()] = new Face.Builder(u, v + sizeZ, sizeZ, sizeY);
-            this.faces[Direction.WEST.getId()] = new Face.Builder(u + sizeZ + sizeX, v + sizeZ, sizeZ, sizeY);
+            this.faces[Direction.UP.id] = new Face.Builder(u + sizeZ, v, sizeX, sizeZ);
+            this.faces[Direction.DOWN.id] = new Face.Builder(u + sizeX + sizeZ, v + sizeZ, sizeX, -sizeZ);
+            this.faces[Direction.NORTH.id] = new Face.Builder(u + sizeZ, v + sizeZ, sizeX, sizeY);
+            this.faces[Direction.SOUTH.id] = new Face.Builder(u + (sizeZ * 2) + sizeX, v + sizeZ, sizeX, sizeY);
+            this.faces[Direction.EAST.id] = new Face.Builder(u, v + sizeZ, sizeZ, sizeY);
+            this.faces[Direction.WEST.id] = new Face.Builder(u + sizeZ + sizeX, v + sizeZ, sizeZ, sizeY);
             return this;
         }
 
         public @NotNull Builder setFace(final @NotNull Direction direction, final @Nullable Face.Builder builder) {
-            this.faces[direction.getId()] = builder;
+            this.faces[direction.id] = builder;
             return this;
         }
 
@@ -111,9 +111,9 @@ public class Cube {
         }
 
         protected void mirror() {
-            final Face.Builder west = this.faces[Direction.WEST.getId()];
-            this.faces[Direction.WEST.getId()] = this.faces[Direction.EAST.getId()];
-            this.faces[Direction.EAST.getId()] = west;
+            final Face.Builder west = this.faces[Direction.WEST.id];
+            this.faces[Direction.WEST.id] = this.faces[Direction.EAST.id];
+            this.faces[Direction.EAST.id] = west;
             for (final Face.Builder builder : this.faces) {
                 if (builder == null) continue;
                 builder.setMirror();
diff --git a/game/client/src/main/java/org/useless/dragonfly/models/block/mojang/StaticBlockModelMojang.java b/game/client/src/main/java/org/useless/dragonfly/models/block/mojang/StaticBlockModelMojang.java
index 0b7b5cb8e..78971b1c4 100644
--- a/game/client/src/main/java/org/useless/dragonfly/models/block/mojang/StaticBlockModelMojang.java
+++ b/game/client/src/main/java/org/useless/dragonfly/models/block/mojang/StaticBlockModelMojang.java
@@ -104,17 +104,17 @@ public class StaticBlockModelMojang implements StaticBlockModel {
             for (int face = 0; face < element.faces; face++) {
                 Direction cullDir = element.cullfaces[face];
                 if (cullFaces && cullDir != null) {
-                    cullDir = Direction.rotateZ(Direction.rotateY(Direction.rotateX(cullDir, rotX), rotY), rotZ);
-                    if ((cullCache & (1 << cullDir.getId())) != 0) {
+                    cullDir = cullDir.rotateX(rotX).rotateY(rotY).rotateZ(rotZ);
+                    if ((cullCache & (1 << cullDir.id)) != 0) {
                         continue;
                     }
                     if (sourceModel.cullSide(worldSource, blockPos, cullDir)) {
-                        cullCache |= (byte) (1 << cullDir.getId());
+                        cullCache |= (byte) (1 << cullDir.id);
                         continue;
                     }
                 }
                 final IconCoordinate coordinate = overrideTexture == null ? element.textures[face] : overrideTexture;
-                Direction direction = Direction.rotateZ(Direction.rotateY(Direction.rotateX(element.directions[face], rotX), rotY), rotZ);
+                Direction direction = element.directions[face].rotateX(rotX).rotateY(rotY).rotateZ(rotZ);
 
                 final float minBrightness = element.lightEmission / 15f;
 
@@ -252,9 +252,9 @@ public class StaticBlockModelMojang implements StaticBlockModel {
                     b = 1;
                 }
 
-                final int dirX = element.flip ? -direction.getOffsetX() : direction.getOffsetX();
-                final int dirY = element.flip ? -direction.getOffsetY() : direction.getOffsetY();
-                final int dirZ = element.flip ? -direction.getOffsetZ() : direction.getOffsetZ();
+                final int dirX = element.flip ? -direction.offsetX() : direction.offsetX();
+                final int dirY = element.flip ? -direction.offsetY() : direction.offsetY();
+                final int dirZ = element.flip ? -direction.offsetZ() : direction.offsetZ();
 
                 final byte lightIndexTopLeft;
                 final byte lightIndexBottomLeft;
@@ -489,7 +489,7 @@ public class StaticBlockModelMojang implements StaticBlockModel {
         float v = uvs.y() - 0.5f;
         rotX = (rotX & 0b11);
         while (rotX > 0) {
-            if (dir.getAxis() == Axis.X) {
+            if (dir.axis() == Axis.X) {
                 if (dir == Direction.WEST) {
                     float _u = u;
                     u = -v;
@@ -505,13 +505,13 @@ public class StaticBlockModelMojang implements StaticBlockModel {
                     u = -u;
                 }
             }
-            dir = Direction.rotateX(dir, 1);
+            dir = dir.rotateX(1);
             rotX--;
         }
 
         rotY = (rotY & 0b11);
         while (rotY > 0) {
-            if (dir.getAxis() == Axis.Y) {
+            if (dir.axis() == Axis.Y) {
                 if (dir == Direction.DOWN) {
                     float _v = v;
                     v = u;
@@ -522,13 +522,13 @@ public class StaticBlockModelMojang implements StaticBlockModel {
                     v = -_u;
                 }
             }
-            dir = Direction.rotateY(dir, 1);
+            dir = dir.rotateY(1);
             rotY--;
         }
 
         rotZ = (rotZ & 0b11);
         while (rotZ > 0) {
-            if (dir.getAxis() == Axis.Z) {
+            if (dir.axis() == Axis.Z) {
                 if (dir == Direction.NORTH) {
                     float _u = u;
                     u = -v;
@@ -545,7 +545,7 @@ public class StaticBlockModelMojang implements StaticBlockModel {
                     u = _v;
                 }
             }
-            dir = Direction.rotateZ(dir, 1);
+            dir = dir.rotateZ(1);
             rotZ--;
         }
 
@@ -560,12 +560,12 @@ public class StaticBlockModelMojang implements StaticBlockModel {
 
     @Override
     public @Nullable IconCoordinate getParticle(@NotNull Side side) {
-        return this.compiled.textures.get("#particle_" + side.getDirection().name().toLowerCase(Locale.ROOT));
+        return this.compiled.textures.get("#particle_" + side.direction().name().toLowerCase(Locale.ROOT));
     }
 
     @Override
     public int particleColorIndex(@NotNull Side side) {
-        return this.compiled.particleIndices[side.getId()];
+        return this.compiled.particleIndices[side.id];
     }
 
     @Override
diff --git a/game/client/src/main/java/org/useless/dragonfly/models/entity/mojang/StaticEntityModelMojang.java b/game/client/src/main/java/org/useless/dragonfly/models/entity/mojang/StaticEntityModelMojang.java
index 70c8bc788..59187d286 100644
--- a/game/client/src/main/java/org/useless/dragonfly/models/entity/mojang/StaticEntityModelMojang.java
+++ b/game/client/src/main/java/org/useless/dragonfly/models/entity/mojang/StaticEntityModelMojang.java
@@ -174,7 +174,7 @@ public class StaticEntityModelMojang implements StaticEntityModel {
             final double maxZ = minZ + cube.size[2] + (cube.inflate + bone.inflate + this.inflation) * 2;
 
             // Top
-            Face face = cube.faces[Direction.UP.getId()];
+            Face face = cube.faces[Direction.UP.id];
             if (face != null) {
                 final double uMin = (face.uv[0]) / this.data.textureWidth;
                 final double uMax = (face.uv[0] + face.uv_size[0]) / this.data.textureWidth;
@@ -200,7 +200,7 @@ public class StaticEntityModelMojang implements StaticEntityModel {
             }
 
             // Bottom
-            face = cube.faces[Direction.DOWN.getId()];
+            face = cube.faces[Direction.DOWN.id];
             if (face != null) {
                 final double uMin = (face.uv[0]) / this.data.textureWidth;
                 final double uMax = (face.uv[0] + face.uv_size[0]) / this.data.textureWidth;
@@ -225,7 +225,7 @@ public class StaticEntityModelMojang implements StaticEntityModel {
             }
 
             // Front
-            face = cube.faces[Direction.NORTH.getId()];
+            face = cube.faces[Direction.NORTH.id];
             if (face != null) {
                 final double uMin = (face.uv[0]) / this.data.textureWidth;
                 final double uMax = (face.uv[0] + face.uv_size[0]) / this.data.textureWidth;
@@ -250,7 +250,7 @@ public class StaticEntityModelMojang implements StaticEntityModel {
             }
 
             // Back
-            face = cube.faces[Direction.SOUTH.getId()];
+            face = cube.faces[Direction.SOUTH.id];
             if (face != null) {
                 final double uMin = (face.uv[0]) / this.data.textureWidth;
                 final double uMax = (face.uv[0] + face.uv_size[0]) / this.data.textureWidth;
@@ -275,7 +275,7 @@ public class StaticEntityModelMojang implements StaticEntityModel {
             }
 
             // Right
-            face = cube.faces[Direction.WEST.getId()];
+            face = cube.faces[Direction.WEST.id];
             if (face != null) {
                 final double uMin = (face.uv[0]) / this.data.textureWidth;
                 final double uMax = (face.uv[0] + face.uv_size[0]) / this.data.textureWidth;
@@ -300,7 +300,7 @@ public class StaticEntityModelMojang implements StaticEntityModel {
             }
 
             // Left
-            face = cube.faces[Direction.EAST.getId()];
+            face = cube.faces[Direction.EAST.id];
             if (face != null) {
                 final double uMin = (face.uv[0]) / this.data.textureWidth;
                 final double uMax = (face.uv[0] + face.uv_size[0]) / this.data.textureWidth;
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston/head.json b/game/client/src/main/resources/assets/minecraft/models/block/piston/head.json
index b1107d71b..f866f84d0 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston/head.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston/head.json
@@ -20,7 +20,7 @@
 			"faces": {
 				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
 				"east": {"uv": [0, 0, 16, 4], "rotation": 90, "texture": "#head"},
-				"south": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "south"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#face"},
 				"west": {"uv": [0, 0, 16, 4], "rotation": 270, "texture": "#head", "cullface": "west"},
 				"up": {"uv": [0, 0, 16, 4], "texture": "#head", "cullface": "up"},
 				"down": {"uv": [0, 0, 16, 4], "rotation": 180, "texture": "#head", "cullface": "down"}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston/head_half.json b/game/client/src/main/resources/assets/minecraft/models/block/piston/head_half.json
index e638ca51f..e41f71cc2 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston/head_half.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston/head_half.json
@@ -20,7 +20,7 @@
 			"faces": {
 				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
 				"east": {"uv": [0, 0, 16, 4], "rotation": 90, "texture": "#head"},
-				"south": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "south"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#face"},
 				"west": {"uv": [0, 0, 16, 4], "rotation": 270, "texture": "#head", "cullface": "west"},
 				"up": {"uv": [0, 0, 16, 4], "texture": "#head", "cullface": "up"},
 				"down": {"uv": [0, 0, 16, 4], "rotation": 180, "texture": "#head", "cullface": "down"}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston/head_short.json b/game/client/src/main/resources/assets/minecraft/models/block/piston/head_short.json
new file mode 100644
index 000000000..0681ef1dc
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston/head_short.json
@@ -0,0 +1,43 @@
+{
+	"format_version": "1.21.6",
+	"credit": "Made with Blockbench",
+	"textures": {
+		"face": "block/piston/face",
+		"head": "block/piston/head",
+		"shaft": "block/piston/shaft",
+		"shaft_end": "block/piston/shaft_end",
+		"overlay": "#face",
+		"particle_up": "#head",
+		"particle_down": "#head",
+		"particle_north": "#face",
+		"particle_south": "#face",
+		"particle_west": "#head",
+		"particle_east": "#head"
+	},
+	"elements": [
+		{
+			"from": [0, 0, 0],
+			"to": [16, 16, 4],
+			"faces": {
+				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
+				"east": {"uv": [0, 0, 16, 4], "rotation": 90, "texture": "#head"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#face"},
+				"west": {"uv": [0, 0, 16, 4], "rotation": 270, "texture": "#head", "cullface": "west"},
+				"up": {"uv": [0, 0, 16, 4], "texture": "#head", "cullface": "up"},
+				"down": {"uv": [0, 0, 16, 4], "rotation": 180, "texture": "#head", "cullface": "down"}
+			}
+		},
+		{
+			"from": [6, 6, 4],
+			"to": [10, 10, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [6, 6, 6]},
+			"faces": {
+				"east": {"uv": [6, 0, 10, 12], "rotation": 90, "texture": "#shaft"},
+				"west": {"uv": [10, 4, 6, 16], "rotation": 90, "texture": "#shaft"},
+				"up": {"uv": [6, 0, 10, 12], "texture": "#shaft"},
+				"down": {"uv": [10, 4, 6, 16], "texture": "#shaft"},
+				"south": {"uv": [6, 6, 10, 10], "texture": "#shaft_end"}
+			}
+		}
+	]
+}
\ No newline at end of file
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head.json b/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head.json
index 1678b1f46..3e703ebfa 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head.json
@@ -20,7 +20,7 @@
 			"faces": {
 				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
 				"east": {"uv": [0, 0, 16, 6], "rotation": 90, "texture": "#head"},
-				"south": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "south"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#face"},
 				"west": {"uv": [0, 0, 16, 6], "rotation": 270, "texture": "#head", "cullface": "west"},
 				"up": {"uv": [0, 0, 16, 6], "texture": "#head", "cullface": "up"},
 				"down": {"uv": [0, 0, 16, 6], "rotation": 180, "texture": "#head", "cullface": "down"}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_half.json b/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_half.json
index bf91915d6..702cd8bbc 100644
--- a/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_half.json
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_half.json
@@ -20,7 +20,7 @@
 			"faces": {
 				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
 				"east": {"uv": [0, 0, 16, 6], "rotation": 90, "texture": "#head"},
-				"south": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "south"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#face"},
 				"west": {"uv": [0, 0, 16, 6], "rotation": 270, "texture": "#head", "cullface": "west"},
 				"up": {"uv": [0, 0, 16, 6], "texture": "#head", "cullface": "up"},
 				"down": {"uv": [0, 0, 16, 6], "rotation": 180, "texture": "#head", "cullface": "down"}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_short.json b/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_short.json
new file mode 100644
index 000000000..4da8264d4
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_steel/head_short.json
@@ -0,0 +1,42 @@
+{
+	"format_version": "1.21.6",
+	"credit": "Made with Blockbench",
+	"textures": {
+		"face": "block/piston_steel/face",
+		"head": "block/piston_steel/head",
+		"shaft": "block/piston_steel/shaft",
+		"overlay": "#face",
+		"particle_up": "#head",
+		"particle_down": "#head",
+		"particle_north": "#face",
+		"particle_south": "#face",
+		"particle_west": "#head",
+		"particle_east": "#head"
+	},
+	"elements": [
+		{
+			"from": [0, 0, 0],
+			"to": [16, 16, 6],
+			"faces": {
+				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
+				"east": {"uv": [0, 0, 16, 6], "rotation": 90, "texture": "#head"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#face"},
+				"west": {"uv": [0, 0, 16, 6], "rotation": 270, "texture": "#head", "cullface": "west"},
+				"up": {"uv": [0, 0, 16, 6], "texture": "#head", "cullface": "up"},
+				"down": {"uv": [0, 0, 16, 6], "rotation": 180, "texture": "#head", "cullface": "down"}
+			}
+		},
+		{
+			"from": [4, 4, 6],
+			"to": [12, 12, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [6, 6, 6]},
+			"faces": {
+				"east": {"uv": [4, 0, 12, 12], "rotation": 90, "texture": "#shaft"},
+				"west": {"uv": [12, 4, 4, 16], "rotation": 90, "texture": "#shaft"},
+				"up": {"uv": [4, 0, 12, 12], "texture": "#shaft"},
+				"down": {"uv": [12, 4, 4, 16], "texture": "#shaft"},
+				"south": {"uv": [4, 4, 12, 12], "texture": "#shaft"}
+			}
+		}
+	]
+}
\ No newline at end of file
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 425d0cabe..f93b104ad 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
@@ -21,7 +21,7 @@
 			"faces": {
 				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
 				"east": {"uv": [0, 0, 16, 4], "rotation": 90, "texture": "#head"},
-				"south": {"uv": [0, 0, 16, 16], "texture": "#back", "cullface": "south"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#back"},
 				"west": {"uv": [0, 0, 16, 4], "rotation": 270, "texture": "#head", "cullface": "west"},
 				"up": {"uv": [0, 0, 16, 4], "texture": "#head", "cullface": "up"},
 				"down": {"uv": [0, 0, 16, 4], "rotation": 180, "texture": "#head", "cullface": "down"}
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 fe345b6c1..c7a3c402c 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
@@ -21,7 +21,7 @@
 			"faces": {
 				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
 				"east": {"uv": [0, 0, 16, 4], "rotation": 90, "texture": "#head"},
-				"south": {"uv": [0, 0, 16, 16], "texture": "#back", "cullface": "south"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#back"},
 				"west": {"uv": [0, 0, 16, 4], "rotation": 270, "texture": "#head", "cullface": "west"},
 				"up": {"uv": [0, 0, 16, 4], "texture": "#head", "cullface": "up"},
 				"down": {"uv": [0, 0, 16, 4], "rotation": 180, "texture": "#head", "cullface": "down"}
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
new file mode 100644
index 000000000..5c43f0a00
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/piston_sticky/head_short.json
@@ -0,0 +1,44 @@
+{
+	"format_version": "1.21.6",
+	"credit": "Made with Blockbench",
+	"textures": {
+		"face": "block/piston_sticky/face",
+		"back": "block/piston/face",
+		"head": "block/piston/head",
+		"shaft": "block/piston/shaft",
+		"shaft_end": "block/piston/shaft_end",
+		"overlay": "#face",
+		"particle_up": "#head",
+		"particle_down": "#head",
+		"particle_north": "#face",
+		"particle_south": "#back",
+		"particle_west": "#head",
+		"particle_east": "#head"
+	},
+	"elements": [
+		{
+			"from": [0, 0, 0],
+			"to": [16, 16, 4],
+			"faces": {
+				"north": {"uv": [0, 0, 16, 16], "texture": "#face", "cullface": "north"},
+				"east": {"uv": [0, 0, 16, 4], "rotation": 90, "texture": "#head"},
+				"south": {"uv": [0, 0, 16, 16], "texture": "#back"},
+				"west": {"uv": [0, 0, 16, 4], "rotation": 270, "texture": "#head", "cullface": "west"},
+				"up": {"uv": [0, 0, 16, 4], "texture": "#head", "cullface": "up"},
+				"down": {"uv": [0, 0, 16, 4], "rotation": 180, "texture": "#head", "cullface": "down"}
+			}
+		},
+		{
+			"from": [6, 6, 4],
+			"to": [10, 10, 16],
+			"rotation": {"angle": 0, "axis": "y", "origin": [6, 6, 6]},
+			"faces": {
+				"east": {"uv": [6, 0, 10, 12], "rotation": 90, "texture": "#shaft"},
+				"west": {"uv": [10, 4, 6, 16], "rotation": 90, "texture": "#shaft"},
+				"up": {"uv": [6, 0, 10, 12], "texture": "#shaft"},
+				"down": {"uv": [10, 4, 6, 16], "texture": "#shaft"},
+				"south": {"uv": [6, 6, 10, 10], "texture": "#shaft_end"}
+			}
+		}
+	]
+}
\ No newline at end of file
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/0_a.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/0_a.json
new file mode 100644
index 000000000..8f50b81d0
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/0_a.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_a",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 10, 2, 7 ],
+            "to": [ 12, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 4, 7, 7 ],
+            "to": [ 6, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 4, 2, 6 ],
+            "to": [ 6, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 3, 2, 7 ],
+            "to": [ 7, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/0_b.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/0_b.json
new file mode 100644
index 000000000..024ee53a5
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/0_b.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_b",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 4, 2, 7 ],
+            "to": [ 6, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 10, 7, 7 ],
+            "to": [ 12, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 10, 2, 6 ],
+            "to": [ 12, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 9, 2, 7 ],
+            "to": [ 13, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/1_a.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/1_a.json
new file mode 100644
index 000000000..86efe5839
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/1_a.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_a",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 11, 2, 7 ],
+            "to": [ 13, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 3, 7, 7 ],
+            "to": [ 5, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 3, 2, 6 ],
+            "to": [ 5, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 2, 2, 7 ],
+            "to": [ 6, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/1_b.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/1_b.json
new file mode 100644
index 000000000..501ad687c
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/1_b.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_b",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 3, 2, 7 ],
+            "to": [ 5, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 11, 7, 7 ],
+            "to": [ 13, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 11, 2, 6 ],
+            "to": [ 13, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 10, 2, 7 ],
+            "to": [ 14, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/2_a.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/2_a.json
new file mode 100644
index 000000000..49d9544c8
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/2_a.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_a",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 12, 2, 7 ],
+            "to": [ 14, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 2, 7, 7 ],
+            "to": [ 4, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 2, 2, 6 ],
+            "to": [ 4, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 1, 2, 7 ],
+            "to": [ 5, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/2_b.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/2_b.json
new file mode 100644
index 000000000..7954566bc
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/2_b.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_b",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 2, 2, 7 ],
+            "to": [ 4, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 12, 7, 7 ],
+            "to": [ 14, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 12, 2, 6 ],
+            "to": [ 14, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 11, 2, 7 ],
+            "to": [ 15, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/3_a.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/3_a.json
new file mode 100644
index 000000000..b0d493e20
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/3_a.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_a",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 13, 2, 7 ],
+            "to": [ 15, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 1, 7, 7 ],
+            "to": [ 3, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 1, 2, 6 ],
+            "to": [ 3, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 0, 2, 7 ],
+            "to": [ 4, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/models/block/timer/3_b.json b/game/client/src/main/resources/assets/minecraft/models/block/timer/3_b.json
new file mode 100644
index 000000000..74468e0f7
--- /dev/null
+++ b/game/client/src/main/resources/assets/minecraft/models/block/timer/3_b.json
@@ -0,0 +1,60 @@
+{
+    "ambientocclusion": false,
+    "textures": {
+        "slab": "minecraft:block/polished_stone_top",
+        "top": "minecraft:block/timer/top_b",
+        "unlit": "minecraft:block/torch_redstone_idle",
+        "lit": "minecraft:block/torch_redstone_active",
+        "overlay": "#top",
+        "particle_up": "#top",
+        "particle_down": "#slab",
+        "particle_north": "#slab",
+        "particle_south": "#slab",
+        "particle_west": "#slab",
+        "particle_east": "#slab"
+    },
+    "elements": [
+        {   "from": [ 0, 0, 0 ],
+            "to": [ 16, 2, 16 ],
+            "faces": {
+                "down":  { "uv": [ 0,  0, 16, 16 ], "texture": "#slab", "cullface": "down" },
+                "up":    { "uv": [ 0,  0, 16, 16 ], "texture": "#top" },
+                "north": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "north" },
+                "south": { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "south" },
+                "west":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "west" },
+                "east":  { "uv": [ 0, 14, 16, 16 ], "texture": "#slab", "cullface": "east" }
+            }
+        },
+        {   "from": [ 1, 2, 7 ],
+            "to": [ 3, 7, 9 ],
+            "faces": {
+                "down":  { "uv": [ 7, 13, 9, 15 ], "texture": "#unlit" },
+                "up":    { "uv": [ 7,  6, 9,  8 ], "texture": "#unlit" },
+                "north": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "south": { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "west":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" },
+                "east":  { "uv": [ 7,  6, 9, 11 ], "texture": "#unlit" }
+            }
+        },
+        {   "from": [ 13, 7, 7 ],
+            "to": [ 15, 7, 9 ],
+            "faces": {
+                "up":   { "uv": [ 7, 6, 9, 8 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 13, 2, 6 ],
+            "to": [ 15, 8, 10 ],
+            "faces": {
+                "west": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "east": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        },
+        {   "from": [ 12, 2, 7 ],
+            "to": [ 16, 8, 9 ],
+            "faces": {
+                "north": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" },
+                "south": { "uv": [ 6, 5, 10, 11 ], "texture": "#lit" }
+            }
+        }
+    ]
+}
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft.png b/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft.png
index 35b8c6db8..343d3fa77 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft.png and b/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft_end.png b/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft_end.png
new file mode 100644
index 000000000..5b09dd000
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/block/piston/shaft_end.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_a.emiss.png b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_a.emiss.png
new file mode 100644
index 000000000..f5b203585
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_a.emiss.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_a.png b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_a.png
new file mode 100644
index 000000000..8379ac8e1
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_a.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_b.emiss.png b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_b.emiss.png
new file mode 100644
index 000000000..0079962d7
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_b.emiss.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_b.png b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_b.png
new file mode 100644
index 000000000..14965ba62
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/block/timer/top_b.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/item/timer.png b/game/client/src/main/resources/assets/minecraft/textures/item/timer.png
new file mode 100644
index 000000000..f55158e44
Binary files /dev/null and b/game/client/src/main/resources/assets/minecraft/textures/item/timer.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 645222364..d983689be 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
@@ -296,26 +296,11 @@ public final class Block<T extends BlockLogic>
         return this.logic.isCubeShaped();
     }
 
-    @Override
-    public boolean canPlaceOnSurface() {
-        return this.logic.canPlaceOnSurface();
-    }
-
-    @Override
-    public boolean canPlaceOnSurfaceOnCondition(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        return this.logic.canPlaceOnSurfaceOnCondition(world, tilePos);
-    }
-
     @Override
     public boolean renderAsNormalBlockOnCondition(final @NotNull WorldSource source, final @NotNull TilePosc tilePos) {
         return this.logic.renderAsNormalBlockOnCondition(source, tilePos);
     }
-
-    @Override
-    public boolean canPlaceOnSurface(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        return this.logic.canPlaceOnSurface(world, tilePos);
-    }
-
+    
     @Override
     public @NotNull ItemStack @Nullable [] getBreakResult(final @NotNull World world, final @NotNull EnumDropCause dropCause, final @NotNull TilePosc tilePos, final int data, final @Nullable TileEntity tileEntity) {
         return this.logic.getBreakResult(world, dropCause, tilePos, data, tileEntity);
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 d4d212a36..1025fba80 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
@@ -39,17 +39,6 @@ interface BlockInterface {
 
     boolean isCubeShaped();
 
-    @Deprecated
-    boolean canPlaceOnSurface();
-
-    @Deprecated
-    boolean canPlaceOnSurfaceOnCondition(final @NotNull World world, final @NotNull TilePosc tilePos);
-
-    @Deprecated
-    default boolean canPlaceOnSurfaceOnCondition(final @NotNull World world, final int x, final int y, final int z) {
-        return this.canPlaceOnSurfaceOnCondition(world, new TilePos(x, y, z));
-    }
-
     boolean renderAsNormalBlockOnCondition(final @NotNull WorldSource source, final @NotNull TilePosc tilePos);
 
     @Deprecated
@@ -57,14 +46,6 @@ interface BlockInterface {
         return this.renderAsNormalBlockOnCondition(world, new TilePos(x, y, z));
     }
 
-    @Deprecated
-    boolean canPlaceOnSurface(final @NotNull World world, final @NotNull TilePosc tilePos);
-
-    @Deprecated
-    default boolean canPlaceOnSurfaceOfBlock(final @NotNull World world, final int x, final int y, final int z) {
-        return this.canPlaceOnSurface(world, new TilePos(x, y, z));
-    }
-
     /**
      * Make sure if this gets overridden in a block that {@link net.minecraft.core.block.Block#getBreakResult(World, EnumDropCause, int, TileEntity)} also has a valid method since {@link EntityFallingBlock EntityFallingBlock} uses it!
      */
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 3d75f6085..bde66452d 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
@@ -78,26 +78,11 @@ public class BlockLogic implements BlockInterface, IItemConvertible {
         return true;
     }
 
-    @Override
-    public boolean canPlaceOnSurface() {
-        return this.isCubeShaped();
-    }
-
-    @Override
-    public boolean canPlaceOnSurfaceOnCondition(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        return this.canPlaceOnSurface();
-    }
-
     @Override
     public boolean renderAsNormalBlockOnCondition(final @NotNull WorldSource source, final @NotNull TilePosc tilePos) {
         return this.isCubeShaped();
     }
 
-    @Override
-    public boolean canPlaceOnSurface(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        return this.canPlaceOnSurface();
-    }
-
     @Override
     public @NotNull ItemStack @Nullable [] getBreakResult(final @NotNull World world, final @NotNull EnumDropCause dropCause, final @NotNull TilePosc tilePos, final int data, final @Nullable TileEntity tileEntity) {
         return this.getBreakResult(world, dropCause, data, tileEntity);
@@ -410,6 +395,10 @@ public class BlockLogic implements BlockInterface, IItemConvertible {
         }
     }
 
+
+    /**
+     * Note: tilePos is not necessarily a real position where the block exist, keep that in mind when overriding!
+     */
     @Override
     public void dropWithCause(final @NotNull World world, final @NotNull EnumDropCause dropCause, final @NotNull TilePosc tilePos, final int data, final @Nullable TileEntity tileEntity, final @Nullable Player player) {
         if (world.isClientSide)
@@ -443,7 +432,7 @@ public class BlockLogic implements BlockInterface, IItemConvertible {
     public @NotNull ISupport getSupport(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         return FullSupport.INSTANCE;
     }
-
+    
     @Override
     public boolean canStay(final @NotNull World world, final @NotNull TilePosc tilePos) {
         return true;
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 4aa798ee7..20dea1629 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
@@ -50,9 +50,9 @@ public class BlockLogicActivator
         if (!activator.locked(activator.stackSelector)) {
             final @Nullable ItemStack itemStack = activator.getNextStack();
 
-            final double px = direction.getOffsetX() * 0.6D + 0.5D;
-            final double py = direction.getOffsetY() * 0.6D + 0.5D;
-            final double pz = direction.getOffsetZ() * 0.6D + 0.5D;
+            final double px = direction.offsetX() * 0.6D + 0.5D;
+            final double py = direction.offsetY() * 0.6D + 0.5D;
+            final double pz = direction.offsetZ() * 0.6D + 0.5D;
 
             if (itemStack == null) {
                 final @Nullable Block<?> block = world.getBlockType(tilePos.add(direction, new TilePos()));
@@ -67,7 +67,7 @@ public class BlockLogicActivator
                 item.onUseByActivator(itemStack, world, activator, random, tilePos, direction, px, py, pz);
                 activator.nullDeadItems();
                 world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, tilePos.x(), tilePos.y(), tilePos.z(), "tile.activator.use", 1, 1);
-                world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_PARTICLES, direction.getId());
+                world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_PARTICLES, direction.id);
             }
         } else {
             activator.shiftSelector();
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicAir.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicAir.java
index bc9aa1af8..93f4ffb1d 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicAir.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicAir.java
@@ -32,26 +32,11 @@ public class BlockLogicAir
         return false;
     }
 
-    @Override
-    public boolean canPlaceOnSurface() {
-        return false;
-    }
-
-    @Override
-    public boolean canPlaceOnSurfaceOnCondition(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        return false;
-    }
-
     @Override
     public boolean renderAsNormalBlockOnCondition(final @NotNull WorldSource source, final @NotNull TilePosc tilePos) {
         return false;
     }
 
-    @Override
-    public boolean canPlaceOnSurface(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        return false;
-    }
-
     @Override
     public @NotNull ItemStack @Nullable [] getBreakResult(final @NotNull World world, final @NotNull EnumDropCause dropCause, final int data, final @Nullable TileEntity tileEntity) {
         return null;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicAxisAligned.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicAxisAligned.java
index 5305375e2..1f78a5629 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicAxisAligned.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicAxisAligned.java
@@ -19,14 +19,14 @@ public class BlockLogicAxisAligned
 
     @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) {
-        final @NotNull Axis axis = mob.getPlacementDirection(side, PlacementMode.SIDE).getAxis();
+        final @NotNull Axis axis = mob.getPlacementDirection(side, PlacementMode.SIDE).axis();
 
         world.setBlockDataNotify(tilePos, axisToMeta(axis));
     }
 
     @Override
     public void onPlacedOnSide(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final double xHit, final double yHit) {
-        final @NotNull Axis axis = side.getAxis();
+        final @NotNull Axis axis = side.axis();
 
         world.setBlockDataNotify(tilePos, axisToMeta(axis));
     }
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 2c3d16b1c..0dfcb1aae 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
@@ -92,18 +92,13 @@ public class BlockLogicBasket extends BlockLogic {
 
     @Override
     public void onPlacedByWorld(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        TilePos queryPos = new TilePos();
-        for (final @NotNull Direction d : Direction.directions) {
-            world.notifyBlocksOfNeighborChange(tilePos.add(d, queryPos), this.block);
-        }
+        // world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
     public void onRemoved(final @NotNull World world, final @NotNull TilePosc tilePos, final int data) {
-        TilePos queryPos = new TilePos();
-        for (final @NotNull Direction d : Direction.directions) {
-            world.notifyBlocksOfNeighborChange(tilePos.add(d, queryPos), this.block);
-        }
+        // // let tile entity handles ranged update
+        // world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBed.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBed.java
index e8f021467..c2f57246a 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBed.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBed.java
@@ -4,7 +4,9 @@ import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
+import net.minecraft.core.block.support.ISupportable;
 import net.minecraft.core.block.support.PartialSupport;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
@@ -12,7 +14,6 @@ import net.minecraft.core.enums.EnumSleepStatus;
 import net.minecraft.core.item.Items;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.util.PackedField;
-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;
@@ -23,7 +24,8 @@ import org.jetbrains.annotations.Nullable;
 import java.util.Objects;
 
 public class BlockLogicBed
-    extends BlockLogic {
+    extends BlockLogic
+    implements ISupportable {
 
     public BlockLogicBed(final @NotNull Block<?> block) {
         super(block, Materials.WOOD);
@@ -39,27 +41,27 @@ public class BlockLogicBed
         int data = world.getBlockData(tilePos);
         final @NotNull TilePosc headPos;
         final @NotNull TilePosc footPos;
-        if (!IS_HEAD.bool(data)) {
-            final int dir = DIRECTION.get(data);
+        final var f2hDir = footToHeadMap[DIRECTION.get(data)].direction();
+        if (IS_HEAD.bool(data)) {
             headPos = tilePos;
-            footPos = tilePos.add(headBlockToFootBlockMap[dir].getDirection(), new TilePos());
+            footPos = tilePos.sub(f2hDir, new TilePos());
             if (world.getBlockType(footPos) != this.block) return true;
+            data = world.getBlockData(footPos);
         } else {
-            final int dir = DIRECTION.get(data);
             footPos = tilePos;
-            headPos = tilePos.sub(headBlockToFootBlockMap[dir].getDirection(), new TilePos());
+            headPos = tilePos.add(f2hDir, new TilePos());
             if (world.getBlockType(headPos) != this.block) return true;
         }
 
         // Explode in invalid dimensions
         if (!world.getWorldType().mayRespawn()) {
-            world.setBlockType(headPos, Blocks.AIR);
             assert world.getBlockType(footPos) == this.block; // made sure from above
+            world.setBlockType(headPos, Blocks.AIR);
             world.setBlockType(footPos, Blocks.AIR);
 
-            // prevents dupe... tho the dupe would need to be pretty funny to work
-            world.notifyBlockChange(headPos, Blocks.AIR); 
-            world.notifyBlockChange(footPos, Blocks.AIR); 
+            world.markBlockNeedsUpdate(headPos);
+            world.markBlockNeedsUpdate(footPos);
+            world.notifyBlocksInCapsuleOfNeighborChange(f2hDir, footPos, this.block);
 
             player.addStat(Achievements.SLEEP_NETHER, 1);
             world.createExplosion(null, tilePos.x() + 0.5F, tilePos.y() + 0.5F, tilePos.z() + 0.5F, 5F, true, false);
@@ -130,7 +132,7 @@ public class BlockLogicBed
     public static @Nullable TilePos otherHalf(final @NotNull World world, @NotNull TilePosc tilePos, int data) {
         final var isHead = IS_HEAD.bool(data);
         final var dir = DIRECTION.get(data);
-        final var direction = headBlockToFootBlockMap[DIRECTION.get(data)].getDirection();
+        final var direction = footToHeadMap[DIRECTION.get(data)].direction();
         final var otherPos = (isHead)
             ? tilePos.sub(direction, new TilePos())
             : tilePos.add(direction, new TilePos());
@@ -157,9 +159,9 @@ public class BlockLogicBed
 
         for (int d = 0; d <= 1; d++) {
             final @NotNull TilePos minPos = new TilePos(
-                tilePos.x - headBlockToFootBlockMap[direction].getOffsetX() * d - 1,
+                tilePos.x - footToHeadMap[direction].offsetX() * d - 1,
                 tilePos.y,
-                tilePos.z - headBlockToFootBlockMap[direction].getOffsetZ() * d - 1
+                tilePos.z - footToHeadMap[direction].offsetZ() * d - 1
             );
             final @NotNull TilePos maxPos = minPos.add(2, 0, 2, new TilePos());
 
@@ -186,10 +188,15 @@ public class BlockLogicBed
         return null;
     }
 
-    public static final @NotNull Side @NotNull [] headBlockToFootBlockMap = {
+    public static final @NotNull Side @NotNull [] footToHeadMap = {
         Side.SOUTH,
         Side.WEST,
         Side.NORTH,
         Side.EAST
     };
+
+    @Override
+    public @NotNull ISupport getSupportConstraint(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        return FullSupport.INSTANCE;
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBoneShale.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBoneShale.java
index 761c1bcd3..deba70176 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicBoneShale.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicBoneShale.java
@@ -20,7 +20,7 @@ public class BlockLogicBoneShale
         final @NotNull Direction direction = BlockLogicFullyRotatable.metaToDirection(data);
 
         float blockHardness = this.block.getHardness();
-        if (side != direction.getSide()) {
+        if (side != direction.side()) {
             blockHardness *= 66.6f;
         }
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicButton.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicButton.java
index 100b70b02..68835d3a5 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicButton.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicButton.java
@@ -16,7 +16,6 @@ import net.minecraft.core.util.helper.DyeColor;
 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;
 import org.jetbrains.annotations.Nullable;
@@ -69,7 +68,7 @@ public class BlockLogicButton
 
     @Override
     public boolean canPlaceOnSide(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side) {
-        final @NotNull Side checkSide = side.getOpposite();
+        final @NotNull Side checkSide = side.opposite();
         return this.isSupported(world, tilePos, checkSide);
     }
 
@@ -131,7 +130,7 @@ public class BlockLogicButton
     public void onPlacedOnSide(final @NotNull World world, final @NotNull TilePosc tilePos, @NotNull Side side, final double xHit, final double yHit) {
         int orientation = -1;
         if (side.isHorizontal()) {
-            side = side.getOpposite();
+            side = side.opposite();
         }
         switch (side) {
             case BOTTOM:
@@ -254,6 +253,18 @@ public class BlockLogicButton
         return this.press(world, tilePos, player);
     }
 
+    private static @NotNull Direction getDirection(int directionWeird) {
+        return switch (directionWeird) {
+            default -> Direction.NONE;
+            case SIDE_WEST -> Direction.WEST;
+            case SIDE_EAST -> Direction.EAST;
+            case SIDE_NORTH -> Direction.NORTH;
+            case SIDE_SOUTH -> Direction.SOUTH;
+            case SIDE_TOP -> Direction.UP;
+            case SIDE_BOTTOM -> Direction.DOWN;
+        };
+    }
+
     private boolean press(final @NotNull World world, final @NotNull TilePosc tilePos, final @Nullable Player player) {
         final int meta = world.getBlockData(tilePos);
         final int direction = meta & MASK_DIRECTION;
@@ -261,31 +272,11 @@ public class BlockLogicButton
         if (isPowered) {
             return false;
         }
-        world.setBlockDataNotify(tilePos, (meta & ~MASK_DIRECTION) | direction | MASK_POWERED);
-        world.markBlockDirty(tilePos);
+        world.setBlockData(tilePos, (meta & ~MASK_DIRECTION) | direction | MASK_POWERED);
+        world.markBlockNeedsUpdate(tilePos);
         world.playSoundEffect(player, SoundCategory.WORLD_SOUNDS, tilePos.x() + 0.5D, tilePos.y() + 0.5D, tilePos.z() + 0.5D, "random.click", 0.3F, 0.6F);
-        world.notifyBlocksOfNeighborChange(tilePos, this.block);
 
-        switch (direction) {
-            case SIDE_WEST:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.WEST, new TilePos()), this.block);
-                break;
-            case SIDE_EAST:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.EAST, new TilePos()), this.block);
-                break;
-            case SIDE_NORTH:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.NORTH, new TilePos()), this.block);
-                break;
-            case SIDE_SOUTH:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.SOUTH, new TilePos()), this.block);
-                break;
-            case SIDE_TOP:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.UP, new TilePos()), this.block);
-                break;
-            case SIDE_BOTTOM:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.DOWN, new TilePos()), this.block);
-                break;
-        }
+        world.notifyBlocksInCapsuleOfNeighborChange(getDirection(direction), tilePos, this.block);
         world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay());
         return true;
     }
@@ -298,28 +289,8 @@ public class BlockLogicButton
     @Override
     public void onRemoved(final @NotNull World world, final @NotNull TilePosc tilePos, final int data) {
         if ((data & MASK_POWERED) != 0) {
-            world.notifyBlocksOfNeighborChange(tilePos, this.block);
             final int direction = data & MASK_DIRECTION;
-            switch (direction) {
-                case SIDE_WEST:
-                    world.notifyBlocksOfNeighborChange(tilePos.add(Direction.WEST, new TilePos()), this.block);
-                    break;
-                case SIDE_EAST:
-                    world.notifyBlocksOfNeighborChange(tilePos.add(Direction.EAST, new TilePos()), this.block);
-                    break;
-                case SIDE_NORTH:
-                    world.notifyBlocksOfNeighborChange(tilePos.add(Direction.NORTH, new TilePos()), this.block);
-                    break;
-                case SIDE_SOUTH:
-                    world.notifyBlocksOfNeighborChange(tilePos.add(Direction.SOUTH, new TilePos()), this.block);
-                    break;
-                case SIDE_TOP:
-                    world.notifyBlocksOfNeighborChange(tilePos.add(Direction.UP, new TilePos()), this.block);
-                    break;
-                case SIDE_BOTTOM:
-                    world.notifyBlocksOfNeighborChange(tilePos.add(Direction.DOWN, new TilePos()), this.block);
-                    break;
-            }
+            world.notifyBlocksInCapsuleOfNeighborChange(getDirection(direction), tilePos, this.block);
         }
         super.onRemoved(world, tilePos, data);
     }
@@ -361,32 +332,12 @@ public class BlockLogicButton
         if ((meta & MASK_POWERED) == 0) {
             return;
         }
-        world.setBlockDataNotify(tilePos, meta & ~MASK_POWERED);
-        world.notifyBlocksOfNeighborChange(tilePos, this.block);
         final int direction = meta & MASK_DIRECTION;
-        switch (direction) {
-            case SIDE_WEST:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.WEST, new TilePos()), this.block);
-                break;
-            case SIDE_EAST:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.EAST, new TilePos()), this.block);
-                break;
-            case SIDE_NORTH:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.NORTH, new TilePos()), this.block);
-                break;
-            case SIDE_SOUTH:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.SOUTH, new TilePos()), this.block);
-                break;
-            case SIDE_TOP:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.UP, new TilePos()), this.block);
-                break;
-            case SIDE_BOTTOM:
-                world.notifyBlocksOfNeighborChange(tilePos.add(Direction.DOWN, new TilePos()), this.block);
-                break;
-        }
+        world.setBlockData(tilePos, meta & ~MASK_POWERED);
+        world.markBlockNeedsUpdate(tilePos);
+        world.notifyBlocksInCapsuleOfNeighborChange(getDirection(direction), tilePos, this.block);
 
         world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, tilePos.x() + 0.5D, tilePos.y() + 0.5D, tilePos.z() + 0.5D, "random.click", 0.3F, 0.5F);
-        world.markBlockDirty(tilePos);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicCactus.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicCactus.java
index 983adec37..62d75eadb 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicCactus.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicCactus.java
@@ -52,8 +52,11 @@ public class BlockLogicCactus
             if (cactusHeight < 3) {
                 final int data = world.getBlockData(tilePos);
                 if (data == 15) {
-                    world.setBlockTypeNotify(tilePos.add(Direction.UP, queryPos), this.block);
-                    world.setBlockDataNotify(tilePos, 0);
+                    world.setBlockData(tilePos, 0);
+                    world.setBlockType(tilePos.add(Direction.UP, queryPos), this.block);
+                    world.markBlockNeedsUpdate(tilePos);
+                    world.markBlockNeedsUpdate(queryPos);
+                    world.notifyBlocksInCapsuleOfNeighborChange(Direction.UP, tilePos, this.block);
                 } else {
                     world.setBlockDataNotify(tilePos, data + 1);
                 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicChest.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicChest.java
index fc75af867..690af327d 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicChest.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicChest.java
@@ -27,12 +27,12 @@ public class BlockLogicChest
 
     @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) {
-        @NotNull Direction direction = mob.getHorizontalPlacementDirection(side).getOpposite();
+        @NotNull Direction direction = mob.getHorizontalPlacementDirection(side).opposite();
         @NotNull Type type = Type.SINGLE;
 
         TilePos queryPos = new TilePos();
-        if (mob.isSneaking() && side.isHorizontal() && (mob.rotationLockHorizontal == null || mob.rotationLockHorizontal == Direction.NONE)) {
-            final @NotNull TilePos placedOn = tilePos.sub(side.getDirection(), new TilePos());
+        if (mob.isSneaking() && side.isHorizontal() && mob.rotationLockHorizontal == null) {
+            final @NotNull TilePos placedOn = tilePos.sub(side.direction(), new TilePos());
 
             if (isSingleChest(world, placedOn)) {
                 final @NotNull Direction direction2 = getDirectionFromMeta(world.getBlockData(placedOn));
@@ -137,7 +137,7 @@ public class BlockLogicChest
 
     @Override
     public void onPlacedOnSide(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final double xHit, final double yHit) {
-        final Direction direction = side.getDirection();
+        final Direction direction = side.direction();
         Type type = Type.SINGLE;
         TilePos queryPos = new TilePos();
         if (direction == Direction.NORTH) {
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 448a650a7..adad84a0d 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
@@ -136,21 +136,21 @@ public class BlockLogicConduit extends BlockLogic {
      */
     @Override
     public void onPlacedByMob(final @NotNull World world, final @NotNull TilePosc cTilePos, final @NotNull Side sidePlacedOnto, final @NotNull Mob mob, final double xHit, final double yHit) {
-        Side sideFacingPlayer = Direction.getDirection(mob).getOpposite().getSide();
-        Side sideClickedAgainst = sidePlacedOnto.getOpposite();
+        Side sideFacingPlayer = Direction.getLockable(mob).opposite().side();
+        Side sideClickedAgainst = sidePlacedOnto.opposite();
 
         int stateToPlaceAs = 0;
 
         if (mob instanceof Player player) {
-            if (player.rotationLock != null && player.rotationLock != Direction.NONE) {
-                Side lockSide = player.rotationLock.getSide();
-                stateToPlaceAs = getStateFromTwoSides(lockSide, lockSide.getOpposite());
+            if (player.rotationLock != null) {
+                Side lockSide = player.rotationLock.side();
+                stateToPlaceAs = getStateFromTwoSides(lockSide, lockSide.opposite());
             }
         }
 
         if (stateToPlaceAs == 0 && mob.isSneaking()) {
             boolean openedClosedFace = false;
-            TilePos clickedPos = new TilePos(cTilePos.x() + sideClickedAgainst.getOffsetX(), cTilePos.y() + sideClickedAgainst.getOffsetY(), cTilePos.z() + sideClickedAgainst.getOffsetZ());
+            TilePos clickedPos = new TilePos(cTilePos.x() + sideClickedAgainst.offsetX(), cTilePos.y() + sideClickedAgainst.offsetY(), cTilePos.z() + sideClickedAgainst.offsetZ());
 
             if (world.getBlockType(clickedPos) == this.block) {
                 int clickedData = world.getBlockData(clickedPos);
@@ -170,12 +170,12 @@ public class BlockLogicConduit extends BlockLogic {
             }
 
             if (openedClosedFace) {
-                stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideClickedAgainst.getOpposite());
+                stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideClickedAgainst.opposite());
             } else {
                 if (sideClickedAgainst != sideFacingPlayer && sideFacingPlayer != Side.NONE) {
                     stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideFacingPlayer);
                 } else {
-                    stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideClickedAgainst.getOpposite());
+                    stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideClickedAgainst.opposite());
                 }
             }
         }
@@ -183,12 +183,12 @@ public class BlockLogicConduit extends BlockLogic {
             List<Side> validSides = new ArrayList<>();
 
             for (Side side : Side.values()) {
-                TilePos nTilePos = new TilePos(cTilePos.x() + side.getOffsetX(), cTilePos.y() + side.getOffsetY(), cTilePos.z() + side.getOffsetZ());
+                TilePos nTilePos = new TilePos(cTilePos.x() + side.offsetX(), cTilePos.y() + side.offsetY(), cTilePos.z() + side.offsetZ());
 
                 if (world.getBlockType(nTilePos) == this.block) {
                     int nData = world.getBlockData(nTilePos);
 
-                    if (isSideOpen(nData, side.getOpposite())) {
+                    if (isSideOpen(nData, side.opposite())) {
                         validSides.add(side);
                     }
                 }
@@ -202,7 +202,7 @@ public class BlockLogicConduit extends BlockLogic {
                 if (sideFacingPlayer != autoConnectedSide && sideFacingPlayer != Side.NONE) {
                     stateToPlaceAs = getStateFromTwoSides(autoConnectedSide, sideFacingPlayer);
                 } else {
-                    stateToPlaceAs = getStateFromTwoSides(autoConnectedSide, autoConnectedSide.getOpposite());
+                    stateToPlaceAs = getStateFromTwoSides(autoConnectedSide, autoConnectedSide.opposite());
                 }
             } else if (numOfValidSides == 2) {
                 stateToPlaceAs = getStateFromTwoSides(validSides.get(0), validSides.get(1));
@@ -216,7 +216,7 @@ public class BlockLogicConduit extends BlockLogic {
                     if (sideFacingPlayer != sideClickedAgainst && sideFacingPlayer != Side.NONE) {
                         stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideFacingPlayer);
                     } else {
-                        stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideClickedAgainst.getOpposite());
+                        stateToPlaceAs = getStateFromTwoSides(sideClickedAgainst, sideClickedAgainst.opposite());
                     }
                 }
             }
@@ -311,7 +311,7 @@ public class BlockLogicConduit extends BlockLogic {
     public static byte packEventConduitData(Side side, int strength) {
         if (side == null) side = Side.NONE;
         int s = Math.max(0, Math.min(PROPAGATION_LENGTH, strength));
-        int sideId = side.getId() & 0x07;
+        int sideId = side.id & 0x07;
         return (byte) ((sideId << POWER_SHIFT) | (s & POWER_MASK));
     }
 
@@ -331,17 +331,17 @@ public class BlockLogicConduit extends BlockLogic {
 
         if (currentPower == 0) return false;
 
-        TilePos receiverPos = new TilePos(tilePos).add(side.getOpposite());
+        TilePos receiverPos = new TilePos(tilePos).add(side.opposite());
         if (world.getBlockType(receiverPos) == Blocks.WIRE_REDSTONE) {
             return false;
         }
 
-        return isSideOpen(data, side.getOpposite());
+        return isSideOpen(data, side.opposite());
     }
 
     private int getNextState(int currentState, @NotNull Side sideQueried) {
         if (currentState == 0) {
-            return getStateFromTwoSides(sideQueried, sideQueried.getOpposite());
+            return getStateFromTwoSides(sideQueried, sideQueried.opposite());
         }
 
         Side[] currentSides = STATE_TABLE[currentState];
@@ -383,7 +383,7 @@ public class BlockLogicConduit extends BlockLogic {
      */
     public static int getAndConvertNeighborSignal(@NotNull WorldSource world, @NotNull TilePosc cTilePos, @NotNull Side side) {
 
-        TilePos nTilePos = new TilePos(cTilePos.x() + side.getOffsetX(), cTilePos.y() + side.getOffsetY(), cTilePos.z() + side.getOffsetZ());
+        TilePos nTilePos = new TilePos(cTilePos.x() + side.offsetX(), cTilePos.y() + side.offsetY(), cTilePos.z() + side.offsetZ());
         Block<?> nBlockType = world.getBlockType(nTilePos);
 
         int cStrength = (world.getBlockData(cTilePos) >> POWER_SHIFT) & POWER_MASK;
@@ -396,7 +396,7 @@ public class BlockLogicConduit extends BlockLogic {
 
         if (nBlockType == Blocks.CONDUIT) {
             int nData = world.getBlockData(nTilePos);
-            if (isSideOpen(nData, side.getOpposite())) {
+            if (isSideOpen(nData, side.opposite())) {
                 int nStrength = (nData >> POWER_SHIFT) & POWER_MASK;
 
                 if (nStrength <= cStrength) return 0;
@@ -411,7 +411,7 @@ public class BlockLogicConduit extends BlockLogic {
 
             if (world1.isBlockNormalCube(nTilePos) && !isPowerCube) {
                 for (Side s : Side.sides) {
-                    TilePos queryPos = new TilePos(nTilePos.x() + s.getOffsetX(), nTilePos.y() + s.getOffsetY(), nTilePos.z() + s.getOffsetZ());
+                    TilePos queryPos = new TilePos(nTilePos.x() + s.offsetX(), nTilePos.y() + s.offsetY(), nTilePos.z() + s.offsetZ());
                     Block<?> queryBlock = world1.getBlockType(queryPos);
 
                     if (queryBlock != Blocks.WIRE_REDSTONE && queryBlock != Blocks.CONDUIT) {
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 223ca8dbc..47ef77a98 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
@@ -47,9 +47,9 @@ public class BlockLogicDispenser extends BlockLogicVeryRotatable {
 
     private void dispenseItem(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Random random) {
         final @NotNull Direction direction = BlockLogicVeryRotatable.metaToDirection(world.getBlockData(tilePos));
-        final int xOffset = direction.getOffsetX();
-        final int yOffset = direction.getOffsetY();
-        final int zOffset = direction.getOffsetZ();
+        final int xOffset = direction.offsetX();
+        final int yOffset = direction.offsetY();
+        final int zOffset = direction.offsetZ();
 
         final @NotNull TileEntityDispenser tileEntity = (TileEntityDispenser) Objects.requireNonNull(world.getTileEntity(tilePos));
         int stackIndex = tileEntity.getRandomStackIndexFromInventory();
@@ -97,7 +97,7 @@ public class BlockLogicDispenser extends BlockLogicVeryRotatable {
                 }
                 world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_ITEM, 0);
             }
-            world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_PARTICLES, direction.getId());
+            world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_PARTICLES, direction.id);
         }
         if (stackIndex >= 0 && itemStack != null && itemStack.stackSize <= 0) {
             tileEntity.setItem(stackIndex, null);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicDoor.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicDoor.java
index d7163bb7e..86a67a49d 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicDoor.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicDoor.java
@@ -56,12 +56,13 @@ public class BlockLogicDoor
     public void onPlacedOnSide(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final double xHit, final double yHit) {
         final int meta = world.getBlockData(tilePos);
         if (world.hasNeighborSignal(tilePos)) {
-            world.setBlockDataNotify(tilePos, meta | MASK_OPENED);
-            if (this.isTop) {
-                world.setBlockDataNotify(tilePos.down(new TilePos()), meta | MASK_OPENED);
-            } else {
-                world.setBlockDataNotify(tilePos.up(new TilePos()), meta | MASK_OPENED);
-            }
+            world.setBlockData(tilePos, meta | MASK_OPENED);
+            final var dir = this.isTop ? Direction.DOWN : Direction.UP;
+            world.setBlockData(tilePos.add(dir, new TilePos()), meta | MASK_OPENED);
+
+            world.markBlockNeedsUpdate(tilePos);
+            world.markBlockNeedsUpdate(tilePos.add(dir, new TilePos()));
+            world.notifyBlocksInCapsuleOfNeighborChange(dir, tilePos, this.block);
             world.playBlockEvent(tilePos, LevelListener.EVENT_DOOR_SOUND, 0);
         }
     }
@@ -163,7 +164,7 @@ public class BlockLogicDoor
 
     @Override
     public void onActivatorInteracted(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull TileEntityActivator activator, final @NotNull Direction direction) {
-        this.onInteracted(world, tilePos, null, direction.getSide(), 0.5, 0.5);
+        this.onInteracted(world, tilePos, null, direction.side(), 0.5, 0.5);
     }
 
     public void onPoweredBlockChange(final @NotNull World world, final @NotNull TilePosc tilePos, boolean isPowered) {
@@ -300,22 +301,22 @@ public class BlockLogicDoor
                 switch (this.getRotation(data)) {
                     case 0: {
                         side = Side.WEST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
                     case 1: {
                         side = Side.NORTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
                     case 2: {
                         side = Side.EAST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
                     case 3: {
                         side = Side.SOUTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
 
@@ -324,22 +325,22 @@ public class BlockLogicDoor
                 switch (this.getRotation(data)) {
                     case 0: {
                         side = Side.NORTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                     case 1: {
                         side = Side.EAST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                     case 2: {
                         side = Side.SOUTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                     case 3: {
                         side = Side.WEST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                 }
@@ -349,22 +350,22 @@ public class BlockLogicDoor
                 switch (this.getRotation(data)) {
                     case 0: {
                         side = Side.NORTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
                     case 1: {
                         side = Side.EAST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
                     case 2: {
                         side = Side.SOUTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
                     case 3: {
                         side = Side.WEST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.right(), side);
                     }
 
@@ -373,22 +374,22 @@ public class BlockLogicDoor
                 switch (this.getRotation(data)) {
                     case 0: {
                         side = Side.EAST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                     case 1: {
                         side = Side.SOUTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.up(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                     case 2: {
                         side = Side.WEST;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.right(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                     case 3: {
                         side = Side.NORTH;
-                        hingeSupport = world.getSupport(tilePos.add(side.getDirection(), queryPos), side.getOpposite());
+                        hingeSupport = world.getSupport(tilePos.add(side.direction(), queryPos), side.opposite());
                         return sBelow.canSupport(PartialSupport.INSTANCE.left(), Side.BOTTOM) || sBelow.canSupport(PartialSupport.INSTANCE.down(), Side.BOTTOM) || hingeSupport.canSupport(PartialSupport.INSTANCE.left(), side);
                     }
                 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFence.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFence.java
index 9df34cb76..315461b50 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFence.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFence.java
@@ -25,11 +25,6 @@ public class BlockLogicFence
         super(block, Materials.WOOD);
     }
 
-    @Override
-    public boolean canPlaceOnSurface() {
-        return true;
-    }
-
     @Override
     public @NotNull ISupport getSupport(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         if (side.isVertical()) {
@@ -89,8 +84,8 @@ public class BlockLogicFence
 
     public boolean canConnectTo(final @NotNull WorldSource source, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         TilePos queryPos = new TilePos();
-        final @NotNull Block<?> b = source.getBlockType(tilePos.add(side.getDirection(), queryPos));
-        return Blocks.hasTag(b, BlockTags.FENCES_CONNECT) || b.getAttachedSide(source, tilePos.add(side.getDirection(), queryPos)) == side.getOpposite();
+        final @NotNull Block<?> b = source.getBlockType(tilePos.add(side.direction(), queryPos));
+        return Blocks.hasTag(b, BlockTags.FENCES_CONNECT) || b.getAttachedSide(source, tilePos.add(side.direction(), queryPos)) == side.opposite();
     }
 
     @Deprecated
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceChainlink.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceChainlink.java
index 072597a2c..806039014 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceChainlink.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceChainlink.java
@@ -18,8 +18,8 @@ public class BlockLogicFenceChainlink extends BlockLogicFenceThin {
     @Override
     public boolean canConnectTo(final @NotNull WorldSource source, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         TilePos queryPos = new TilePos();
-        final @Nullable Block<?> b = source.getBlockType(tilePos.add(side.getDirection(), queryPos));
-        return Blocks.hasTag(b, BlockTags.CHAINLINK_FENCES_CONNECT) || (b != null && b.getAttachedSide(source, tilePos.add(side.getDirection(), queryPos)) == side.getOpposite());
+        final @Nullable Block<?> b = source.getBlockType(tilePos.add(side.direction(), queryPos));
+        return Blocks.hasTag(b, BlockTags.CHAINLINK_FENCES_CONNECT) || (b != null && b.getAttachedSide(source, tilePos.add(side.direction(), queryPos)) == side.opposite());
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceGate.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceGate.java
index c74ebaa0f..eefc919c8 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceGate.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceGate.java
@@ -74,7 +74,7 @@ public class BlockLogicFenceGate
 
     @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) {
-        final int direction = mob.getHorizontalPlacementDirection(side).getHorizontalIndex();
+        final int direction = mob.getHorizontalPlacementDirection(side).legacyHorizontalIndex();
         final int meta = world.getBlockData(tilePos);
         world.setBlockDataNotify(tilePos, direction | (meta & ~MASK_DIRECTION));
     }
@@ -84,7 +84,7 @@ public class BlockLogicFenceGate
         if (!side.isHorizontal()) {
             side = Side.NORTH;
         }
-        final int direction = side.getDirection().getHorizontalIndex();
+        final int direction = side.direction().legacyHorizontalIndex();
         final int meta = world.getBlockData(tilePos);
         world.setBlockDataNotify(tilePos, direction | (meta & ~MASK_DIRECTION));
     }
@@ -134,7 +134,7 @@ public class BlockLogicFenceGate
 
     @Override
     public void onActivatorInteracted(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull TileEntityActivator activator, final @NotNull Direction direction) {
-        this.onInteracted(world, tilePos, null, direction.getSide(), 0.5, 0.5);
+        this.onInteracted(world, tilePos, null, direction.side(), 0.5, 0.5);
     }
 
     public static boolean isOpen(final int meta) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceSteel.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceSteel.java
index 1493510e1..cdc450111 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceSteel.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceSteel.java
@@ -18,7 +18,7 @@ public class BlockLogicFenceSteel
     @Override
     public boolean canConnectTo(final @NotNull WorldSource source, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         TilePos queryPos = new TilePos();
-        final @Nullable Block<?> b = source.getBlockType(tilePos.add(side.getDirection(), queryPos));
-        return Blocks.hasTag(b, BlockTags.CHAINLINK_FENCES_CONNECT) || (b != null && b.getAttachedSide(source, tilePos.add(side.getDirection(), queryPos)) == side.getOpposite());
+        final @Nullable Block<?> b = source.getBlockType(tilePos.add(side.direction(), queryPos));
+        return Blocks.hasTag(b, BlockTags.CHAINLINK_FENCES_CONNECT) || (b != null && b.getAttachedSide(source, tilePos.add(side.direction(), queryPos)) == side.opposite());
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceThin.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceThin.java
index 14058f6be..1613ad4f4 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceThin.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceThin.java
@@ -21,11 +21,6 @@ public abstract class BlockLogicFenceThin
         super(block, material);
     }
 
-    @Override
-    public boolean canPlaceOnSurface() {
-        return true;
-    }
-
     @Override
     public @NotNull ISupport getSupport(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         if (side.isVertical()) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceWallPaper.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceWallPaper.java
index a4c258f97..a1655ba49 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceWallPaper.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFenceWallPaper.java
@@ -18,7 +18,7 @@ public class BlockLogicFenceWallPaper
     @Override
     public boolean canConnectTo(final @NotNull WorldSource source, final @NotNull TilePosc tilePos, final @NotNull Side side) {
         TilePos queryPos = new TilePos();
-        final @Nullable Block<?> b = source.getBlockType(tilePos.add(side.getDirection(), queryPos));
-        return Blocks.hasTag(b, BlockTags.FENCES_CONNECT) || (b != null && b.getAttachedSide(source, tilePos.add(side.getDirection(), queryPos)) == side.getOpposite());
+        final @Nullable Block<?> b = source.getBlockType(tilePos.add(side.direction(), queryPos));
+        return Blocks.hasTag(b, BlockTags.FENCES_CONNECT) || (b != null && b.getAttachedSide(source, tilePos.add(side.direction(), queryPos)) == side.opposite());
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java
index 5a927c736..55dd2555a 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java
@@ -69,7 +69,7 @@ public class BlockLogicFluidFlowing extends BlockLogicFluid {
                 } else {
                     world.setBlockDataNotify(tilePos, localFlowDecay);
                     world.scheduleBlockUpdate(tilePos, this.block, tickDelay());
-                    world.notifyBlocksOfNeighborChange(tilePos, this.block); // ?
+                    world.notifyBlocksOfNeighborChange(tilePos, this.block);
                 }
             } else {
                 setFluidStill(world, tilePos);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFullyRotatable.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFullyRotatable.java
index 3d67c7024..2313016d4 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFullyRotatable.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFullyRotatable.java
@@ -16,20 +16,20 @@ public class BlockLogicFullyRotatable extends BlockLogic {
 
     @Override
     public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
-        final Direction direction = mob.getPlacementDirection(side).getOpposite();
+        final Direction direction = mob.getPlacementDirection(side).opposite();
         world.setBlockDataNotify(tilePos, directionToMeta(direction));
     }
 
     @Override
     public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
-        world.setBlockDataNotify(tilePos, directionToMeta(side.getDirection()));
+        world.setBlockDataNotify(tilePos, directionToMeta(side.direction()));
     }
 
     public static int directionToMeta(Direction direction) {
-        return direction.getId() & MASK_DIRECTION;
+        return direction.id;
     }
 
     public static @NotNull Direction metaToDirection(final int data) {
-        return Direction.getDirectionById(data & MASK_DIRECTION);
+        return Direction.fromId(data & MASK_DIRECTION);
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLadder.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLadder.java
index 7969695e9..3d43989b9 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLadder.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLadder.java
@@ -78,7 +78,7 @@ public class BlockLogicLadder extends BlockLogic implements ISupportable {
     public @Nullable Side getSideForPlacement(@NotNull World world, @NotNull TilePos tilePos, @NotNull Side clickedSide) {
         TilePosc p = tilePos;
         if (!world.canPlaceInsideBlock(tilePos)) {
-            p = p.add(clickedSide.getDirection(), new TilePos());
+            p = p.add(clickedSide.direction(), new TilePos());
         }
 
         if (canExistAt(world, p, getMetaForSide(clickedSide))) return clickedSide;
@@ -91,7 +91,7 @@ public class BlockLogicLadder extends BlockLogic implements ISupportable {
     }
 
     public boolean canExistAt(@NotNull World world, @NotNull TilePosc tilePos, int meta) {
-        Side side = getSideFromMeta(meta).getOpposite();
+        Side side = getSideFromMeta(meta).opposite();
         return isSupported(world, tilePos, side);
     }
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerAsh.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerAsh.java
index 06b608fd0..64492b0b8 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerAsh.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerAsh.java
@@ -25,8 +25,7 @@ public class BlockLogicLayerAsh extends BlockLogicLayerBase
         int myMetadata = world.getBlockData(tilePos);
         if (myMetadata == 0x7) return;
 
-        for (int i = 0; i < Direction.horizontalDirections.length; i++) {
-            Direction dir = Direction.horizontalDirections[i];
+        for (final var dir : Direction.horizontal) {
             TilePos dirPos = tilePos.add(dir, new TilePos());
             if (!((world.isBlockOpaqueCube(dirPos)) || (world.getBlockType(dirPos) == this.block && world.getBlockData(dirPos) >= myMetadata))) return;
         }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerLeaves.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerLeaves.java
index 52b510ea7..07cc209bc 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerLeaves.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerLeaves.java
@@ -35,8 +35,8 @@ public class BlockLogicLayerLeaves extends BlockLogicLayerBase {
         int myMetadata = world.getBlockData(tilePos);
         if (myMetadata == 0x7) return;
 
-        for (int i = 0; i < Direction.horizontalDirections.length; i++) {
-            TilePos p = tilePos.add(Direction.horizontalDirections[i], new TilePos());
+        for (final var dir : Direction.horizontal) {
+            TilePos p = tilePos.add(dir, new TilePos());
 
             if (!((world.isBlockOpaqueCube(p)) || (world.getBlockType(p) == this.block && world.getBlockData(p) >= myMetadata))) {
                 return;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerSnow.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerSnow.java
index c4d25e101..a853eafd6 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerSnow.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLayerSnow.java
@@ -32,8 +32,7 @@ public class BlockLogicLayerSnow extends BlockLogicLayerBase {
         int myMetadata = world.getBlockData(tilePos);
         if (myMetadata == 0x7) return;
 
-        for (int i = 0; i < Direction.horizontalDirections.length; i++) {
-            Direction dir = Direction.horizontalDirections[i];
+        for (final var dir : Direction.horizontal) {
             TilePos dirPos = tilePos.add(dir, new TilePos());
             if (!((world.isBlockOpaqueCube(dirPos)) || (world.getBlockType(dirPos) == this.block && world.getBlockData(dirPos) >= myMetadata)))
                 return;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLever.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLever.java
index d0a5d984c..b14ad73dd 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLever.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLever.java
@@ -61,7 +61,7 @@ public class BlockLogicLever extends BlockLogic implements ISupportable {
 
     @Override
     public boolean canPlaceOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
-        Side checkSide = side.getOpposite();
+        Side checkSide = side.opposite();
         return isSupported(world, tilePos, checkSide);
     }
 
@@ -84,12 +84,12 @@ public class BlockLogicLever extends BlockLogic implements ISupportable {
         switch (side) {
             case BOTTOM:
                 if (isSupported(world, tilePos, Side.TOP)) {
-                    rotation = ROTATION_BOTTOM_NS + (mob.getHorizontalPlacementDirection(side).getAxis() == Axis.Z ? 1 : 0);
+                    rotation = ROTATION_BOTTOM_NS + (mob.getHorizontalPlacementDirection(side).axis() == Axis.Z ? 1 : 0);
                 }
                 break;
             case TOP:
                 if (isSupported(world, tilePos, Side.BOTTOM)) {
-                    rotation = ROTATION_TOP_NS + (mob.getHorizontalPlacementDirection(side).getAxis() == Axis.Z ? 1 : 0);
+                    rotation = ROTATION_TOP_NS + (mob.getHorizontalPlacementDirection(side).axis() == Axis.Z ? 1 : 0);
                 }
                 break;
             case NORTH:
@@ -127,7 +127,7 @@ public class BlockLogicLever extends BlockLogic implements ISupportable {
         int meta = world.getBlockData(tilePos);
         boolean isPowered = isPowered(meta);
         int rotation = -1;
-        switch (side.getOpposite()) {
+        switch (side.opposite()) {
             case BOTTOM -> {
                 if (isSupported(world, tilePos, Side.TOP)) {
                     rotation = ROTATION_BOTTOM_NS;
@@ -226,29 +226,31 @@ public class BlockLogicLever extends BlockLogic implements ISupportable {
         return true;
     }
 
+    private static @NotNull Direction getDirection(int rotation) {
+        return switch (rotation) {
+            default -> Direction.NONE;
+            case ROTATION_EAST -> Direction.WEST;
+            case ROTATION_WEST -> Direction.EAST;
+            case ROTATION_SOUTH -> Direction.NORTH;
+            case ROTATION_NORTH -> Direction.SOUTH;
+            case ROTATION_TOP_NS, ROTATION_TOP_WE -> Direction.DOWN;
+            case ROTATION_BOTTOM_NS, ROTATION_BOTTOM_WE -> Direction.UP;
+        };
+    }
+
     public void flip(@NotNull World world, @NotNull TilePosc tilePos, @Nullable Player player) {
         int meta = world.getBlockData(tilePos);
         int rotation = getRotation(meta);
         if (isPowered(meta)) {
-            world.setBlockDataNotify(tilePos, meta & ~MASK_POWERED);
+            world.setBlockData(tilePos, meta & ~MASK_POWERED);
         } else {
-            world.setBlockDataNotify(tilePos, meta | MASK_POWERED);
+            world.setBlockData(tilePos, meta | MASK_POWERED);
         }
 
-        world.markBlocksDirty(tilePos, tilePos);
+        world.markBlockNeedsUpdate(tilePos);
         world.playSoundEffect(player, SoundCategory.WORLD_SOUNDS, (double) tilePos.x() + 0.5D, (double) tilePos.y() + 0.5D, (double) tilePos.z() + 0.5D, "random.click", 0.3F, isPowered(meta) ? 0.5F : 0.6F);
-        world.notifyBlocksOfNeighborChange(tilePos, this.block);
 
-        TilePos queryPos = new TilePos();
-        switch (rotation) {
-            case ROTATION_EAST -> world.notifyBlocksOfNeighborChange(tilePos.west(queryPos), this.block);
-            case ROTATION_WEST -> world.notifyBlocksOfNeighborChange(tilePos.east(queryPos), this.block);
-            case ROTATION_SOUTH -> world.notifyBlocksOfNeighborChange(tilePos.north(queryPos), this.block);
-            case ROTATION_NORTH -> world.notifyBlocksOfNeighborChange(tilePos.south(queryPos), this.block);
-            case ROTATION_TOP_NS, ROTATION_TOP_WE ->
-                world.notifyBlocksOfNeighborChange(tilePos.down(queryPos), this.block);
-            default -> world.notifyBlocksOfNeighborChange(tilePos.up(queryPos), this.block);
-        }
+        world.notifyBlocksInCapsuleOfNeighborChange(getDirection(rotation), tilePos, this.block);
     }
 
     @Override
@@ -259,18 +261,8 @@ public class BlockLogicLever extends BlockLogic implements ISupportable {
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
         if (isPowered(data)) {
-            world.notifyBlocksOfNeighborChange(tilePos, this.block);
-            TilePos queryPos = new TilePos();
             int rotation = getRotation(data);
-            switch (rotation) {
-                case ROTATION_EAST -> world.notifyBlocksOfNeighborChange(tilePos.west(queryPos), this.block);
-                case ROTATION_WEST -> world.notifyBlocksOfNeighborChange(tilePos.east(queryPos), this.block);
-                case ROTATION_SOUTH -> world.notifyBlocksOfNeighborChange(tilePos.north(queryPos), this.block);
-                case ROTATION_NORTH -> world.notifyBlocksOfNeighborChange(tilePos.south(queryPos), this.block);
-                case ROTATION_TOP_NS, ROTATION_TOP_WE ->
-                    world.notifyBlocksOfNeighborChange(tilePos.down(queryPos), this.block);
-                default -> world.notifyBlocksOfNeighborChange(tilePos.up(queryPos), this.block);
-            }
+            world.notifyBlocksInCapsuleOfNeighborChange(getDirection(rotation), tilePos, this.block);
         }
         super.onRemoved(world, tilePos, data);
     }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMagma.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMagma.java
index 33229309b..448ef8f85 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMagma.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMagma.java
@@ -52,7 +52,7 @@ public class BlockLogicMagma extends BlockLogic {
     public boolean canMelt(@NotNull World world, @NotNull TilePosc tilePos) {
         boolean canMelt = false;
         TilePos queryPos = new TilePos();
-        for (Direction dir : Direction.directions) {
+        for (final var dir : Direction.all) {
             Block<?> block = world.getBlockType(tilePos.add(dir, queryPos));
             Material adjacentMaterial = block == null ? Materials.AIR : block.getMaterial();
             if (adjacentMaterial == Materials.WATER) {
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 639ef05cc..d247ec354 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
@@ -42,7 +42,7 @@ public class BlockLogicMatcher extends BlockLogicAxisAligned {
 
     @Override
     public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
-        Axis axis = mob.getPlacementDirection(side, PlacementMode.FACING).getAxis();
+        Axis axis = mob.getPlacementDirection(side, PlacementMode.FACING).axis();
 
         world.setBlockDataNotify(tilePos, axisToMeta(axis));
         world.scheduleBlockUpdate(tilePos, this.block, tickDelay());
@@ -111,7 +111,7 @@ public class BlockLogicMatcher extends BlockLogicAxisAligned {
 
     @Override
     public boolean isEmittingSignal(@NotNull WorldSource source, @NotNull TilePosc tilePos, @NotNull Side side) {
-        return this.isActive && (BlockLogicAxisAligned.metaToAxis(source.getBlockData(tilePos)) != side.getAxis());
+        return this.isActive && (BlockLogicAxisAligned.metaToAxis(source.getBlockData(tilePos)) != side.axis());
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java
index 99ea73638..e413fb88e 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java
@@ -23,12 +23,6 @@ public class BlockLogicMesh extends BlockLogicTransparent
         return false;
     }
 
-    @Override
-    public boolean canPlaceOnSurface()
-    {
-        return true;
-    }
-
     @Override
     public boolean collidesWithEntity(@NotNull Entity entity, @NotNull World world, @NotNull TilePosc tilePos) {
         return !(entity instanceof EntityItem);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMobSpawnerDeactivated.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMobSpawnerDeactivated.java
index e2058485e..1792e738d 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMobSpawnerDeactivated.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMobSpawnerDeactivated.java
@@ -23,12 +23,6 @@ public class BlockLogicMobSpawnerDeactivated extends BlockLogicTransparent {
         return false;
     }
 
-    @Override
-    public boolean canPlaceOnSurface()
-    {
-        return true;
-    }
-
     @Override
     public boolean collidesWithEntity(@NotNull Entity entity, @NotNull World world, @NotNull TilePosc tilePos) {
         return !(entity instanceof EntityItem);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMoss.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMoss.java
index 54cd8e48f..3e0fc96f6 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMoss.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMoss.java
@@ -36,7 +36,7 @@ public class BlockLogicMoss extends BlockLogic implements IBonemealable {
 
         if (world.areBlocksLoaded(tilePos, 16)) {
             if (rand.nextInt(20) == 0 && airExposed(world, tilePos)) {
-                Direction rDir = Direction.getDirectionById(rand.nextInt(6));
+                Direction rDir = Direction.ID_MAP[rand.nextInt(6)];
                 TilePos randomPos = tilePos.add(rDir, new TilePos());
                 if (canMossSpread(world, randomPos)) {
                     @Nullable Block<?> mossStone = getMossBlock(world.getBlockType(randomPos));
@@ -54,8 +54,7 @@ public class BlockLogicMoss extends BlockLogic implements IBonemealable {
         int maxBlockLight = 0;
         int maxSkyLight = 0;
         TilePos queryPos = new TilePos();
-        for (int i = 0; i < Direction.directions.length; i++) {
-            Direction dir = Direction.directions[i];
+        for (final var dir : Direction.all) {
             TilePos offsetPos = tilePos.add(dir, queryPos);
             int blockLight = world.getBlockLightValue(offsetPos);
             int skyLight = world.getSavedLightValue(LightLayer.Sky, offsetPos);
@@ -134,8 +133,7 @@ public class BlockLogicMoss extends BlockLogic implements IBonemealable {
 
     public boolean airExposed(@NotNull World world, @NotNull TilePosc tilePos) {
         TilePos queryPos = new TilePos();
-        for (int i = 0; i < Direction.directions.length; i++) {
-            Direction dir = Direction.directions[i];
+        for (final var dir : Direction.all) {
             if (world.isAirBlock(tilePos.add(dir, queryPos))) return true;
         }
         return false;
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 7b2c23008..794d81e3e 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
@@ -35,8 +35,7 @@ public class BlockLogicMotionSensor extends BlockLogicVeryRotatable {
         TilePos queryPos = new TilePos();
         final int brightness = 10;
         double d = 0.0625;
-        for (int i = 0; i < Direction.directions.length; i++) {
-            Direction dir = Direction.directions[i];
+        for (final var dir : Direction.all) {
             double px = (double) tilePos.x() + rand.nextFloat();
             double py = (double) tilePos.y() + rand.nextFloat();
             double pz = (double) tilePos.z() + rand.nextFloat();
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 8e9237aae..28070ac04 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
@@ -111,8 +111,7 @@ public class BlockLogicOreRedstone extends BlockLogic {
 
         final double d = 0.0625;
         TilePos queryPos = new TilePos();
-        for (int i = 0; i < Direction.directions.length; i++) {
-            Direction dir = Direction.directions[i];
+        for (final var dir : Direction.all) {
             double px = (double) tilePos.x() + random.nextFloat();
             double py = (double) tilePos.y() + random.nextFloat();
             double pz = (double) tilePos.z() + random.nextFloat();
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicOverlayPebbles.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicOverlayPebbles.java
index c9032d5f7..abacf19c0 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicOverlayPebbles.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicOverlayPebbles.java
@@ -2,7 +2,9 @@ package net.minecraft.core.block;
 
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
+import net.minecraft.core.block.support.ISupportable;
 import net.minecraft.core.block.support.PartialSupport;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.Items;
@@ -16,7 +18,9 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.joml.primitives.AABBdc;
 
-public class BlockLogicOverlayPebbles extends BlockLogic {
+public class BlockLogicOverlayPebbles extends BlockLogic
+    implements ISupportable
+{
     public static final int MAX_PEBBLES = 3;
     public static final int MASK_PEBBLE_AMOUNT = 0b0000_0011;
     public BlockLogicOverlayPebbles(@NotNull Block<?> block, @NotNull Material material) {
@@ -37,8 +41,7 @@ public class BlockLogicOverlayPebbles extends BlockLogic {
 
     @Override
     public boolean canPlaceAt(@NotNull World world, @NotNull TilePosc tilePos) {
-        Block<?> block = world.getBlockType(tilePos.down(new TilePos()));
-        return block.getMaterial().isSolid();
+        return this.isSupported(world, tilePos, Side.BOTTOM);
     }
 
     @Override
@@ -89,4 +92,9 @@ public class BlockLogicOverlayPebbles extends BlockLogic {
     public static int getStackCount(int data) {
         return data & MASK_PEBBLE_AMOUNT;
     }
+
+    @Override
+    public @NotNull ISupport getSupportConstraint(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        return FullSupport.INSTANCE;
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPressurePlate.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPressurePlate.java
index 32902034e..10a3271ae 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPressurePlate.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPressurePlate.java
@@ -109,7 +109,7 @@ public class BlockLogicPressurePlate<T extends Entity> extends BlockLogic implem
     private void updateState(@NotNull World world, @NotNull TilePosc tilePos) {
         int meta = world.getBlockData(tilePos);
         boolean isPressed = isPressed(meta);
-        Direction dir = sideFromMeta(meta).getDirection();
+        Direction dir = sideFromMeta(meta).direction();
         boolean isSteppedOn = false;
         float pixel = 0.125F;
         List<T> list = switch (sideFromMeta(meta)) {
@@ -134,15 +134,15 @@ public class BlockLogicPressurePlate<T extends Entity> extends BlockLogic implem
             }
         }
         if (isSteppedOn && !isPressed) {
-            world.setBlockDataNotify(tilePos, meta | MASK_POWERED);
-            // world.notifyBlocksOfNeighborChange(tilePos, this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.add(dir, new TilePos()), this.block);
+            world.setBlockData(tilePos, meta | MASK_POWERED);
+            world.markBlockNeedsUpdate(tilePos);
+            world.notifyBlocksInCapsuleOfNeighborChange(dir, tilePos, this.block);
             world.markBlocksDirty(tilePos, tilePos);
             world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) tilePos.x() + 0.5D, (double) tilePos.y() + 0.1D, (double) tilePos.z() + 0.5D, "random.click", 0.3F, 0.6F);
         } else if (!isSteppedOn && isPressed) {
-            world.setBlockDataNotify(tilePos, meta & ~MASK_POWERED);
-            // world.notifyBlocksOfNeighborChange(tilePos, this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.add(dir, new TilePos()), this.block);
+            world.setBlockData(tilePos, meta & ~MASK_POWERED);
+            world.markBlockNeedsUpdate(tilePos);
+            world.notifyBlocksInCapsuleOfNeighborChange(dir, tilePos, this.block);
             world.markBlocksDirty(tilePos, tilePos);
             world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) tilePos.x() + 0.5D, (double) tilePos.y() + 0.1D, (double) tilePos.z() + 0.5D, "random.click", 0.3F, 0.5F);
         }
@@ -155,8 +155,7 @@ public class BlockLogicPressurePlate<T extends Entity> extends BlockLogic implem
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
         if (isPressed(data)) {
             Side side = sideFromMeta(world.getBlockData(tilePos));
-            world.notifyBlocksOfNeighborChange(tilePos, this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.add(side.getDirection(), new TilePos()), this.block);
+            world.notifyBlocksInCapsuleOfNeighborChange(side.direction(), tilePos, this.block);
         }
         super.onRemoved(world, tilePos, data);
     }
@@ -185,7 +184,7 @@ public class BlockLogicPressurePlate<T extends Entity> extends BlockLogic implem
     public boolean isEmittingDirectSignal(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
         int meta = world.getBlockData(tilePos);
         if (isPressed(meta)) {
-            return side == sideFromMeta(meta).getOpposite();
+            return side == sideFromMeta(meta).opposite();
         } else {
             return false;
         }
@@ -196,11 +195,11 @@ public class BlockLogicPressurePlate<T extends Entity> extends BlockLogic implem
     }
 
     public static int setSide(int meta, @NotNull Side side) {
-        return (meta & ~MASK_SIDE) | (side.getId() << 1);
+        return (meta & ~MASK_SIDE) | (side.id << 1);
     }
 
     public static @NotNull Side sideFromMeta(int meta) {
-        return Side.getSideById((meta & MASK_SIDE) >> 1);
+        return Side.fromId((meta & MASK_SIDE) >> 1);
     }
 
     @Override
@@ -216,7 +215,7 @@ public class BlockLogicPressurePlate<T extends Entity> extends BlockLogic implem
     @Override
     public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
         int meta = world.getBlockData(tilePos);
-        side = side.getOpposite();
+        side = side.opposite();
         if (!isSupported(world, tilePos, side)) {
             side = getDefaultSide(world, tilePos);
         }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumice.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumice.java
index df4f52456..b69ebfa67 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumice.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumice.java
@@ -89,7 +89,7 @@ public class BlockLogicPumice extends BlockLogic {
     @Override
     public void animationTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand) {
         if (this.isWet) {
-            Direction direction = Direction.directions[rand.nextInt(6)]; // Get random side
+            Direction direction = Direction.ID_MAP[rand.nextInt(6)]; // Get random side
             if (direction == Direction.UP || world.isBlockOpaqueCube(tilePos.add(direction, new TilePos()))) return;
             float off1 = rand.nextFloat() * 0.9f + 0.05f;
             float off2 = rand.nextFloat() * 0.9f + 0.05f;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkin.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkin.java
index b54fa7cf8..bab15fb27 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkin.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicPumpkin.java
@@ -30,7 +30,7 @@ public class BlockLogicPumpkin extends BlockLogicVeryRotatable
         ItemStack heldItem = player.getHeldItem();
         if (heldItem != null && heldItem.getItem() instanceof ItemToolSword){
             heldItem.damageItem(1,player);
-            world.setBlockTypeDataNotify(tilePos, Blocks.PUMPKIN_CARVED_IDLE, player.getHorizontalPlacementDirection(Direction.getHorizontalDirection(player).getSide()).getOpposite().getId());
+            world.setBlockTypeDataNotify(tilePos, Blocks.PUMPKIN_CARVED_IDLE, player.getHorizontalPlacementDirection(Direction.getHorizontalLockable(player).side()).opposite().id);
             return true;
         }
         return false;
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 381ab4251..3fe46ef80 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
@@ -18,16 +18,13 @@ public class BlockLogicPumpkinRedstone extends BlockLogicVeryRotatable {
 
     @Override
     public void onPlacedByWorld(@NotNull World world, @NotNull TilePosc tilePos) {
-        TilePos queryPos = new TilePos();
-        for (Direction dir : Direction.directions){
-            world.notifyBlocksOfNeighborChange(tilePos.add(dir, queryPos), this.block);
-        }
+        world.notifyShellBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
         TilePos queryPos = new TilePos();
-        for (Direction dir : Direction.directions){
+        for (final var dir : Direction.all){
             world.notifyBlocksOfNeighborChange(tilePos.add(dir, queryPos), this.block);
         }
     }
@@ -39,8 +36,8 @@ public class BlockLogicPumpkinRedstone extends BlockLogicVeryRotatable {
 
     @Override
     public boolean isEmittingSignal(@NotNull WorldSource source, @NotNull TilePosc tilePos, @NotNull Side side) {
-        Side mySide = Side.getSideById(source.getBlockData(tilePos));
-        return mySide == side.getOpposite();
+        Side mySide = Side.fromId(source.getBlockData(tilePos));
+        return mySide == side.opposite();
     }
 
     @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 029969654..b55e3f37e 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
@@ -9,6 +9,7 @@ import net.minecraft.core.block.support.ISupportable;
 import net.minecraft.core.block.support.PartialSupport;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.util.helper.Axis;
+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.WorldSource;
@@ -134,18 +135,25 @@ public class BlockLogicRail extends BlockLogic implements ISupportable {
             boolean changedMeta = false;
             if (gettingPower && !isPoweredFlagSet) {
                 // Set powered bit
-                world.setBlockDataNotify(tilePos, railDirection.meta | MASK_POWERED);
+                world.setBlockData(tilePos, railDirection.meta | MASK_POWERED);
                 changedMeta = true;
             } else if (!gettingPower && isPoweredFlagSet) {
                 // Remove powered bit
-                world.setBlockDataNotify(tilePos, railDirection.meta);
+                world.setBlockData(tilePos, railDirection.meta);
                 changedMeta = true;
             }
 
+            world.markBlockNeedsUpdate(tilePos);
+
             if (changedMeta) {
-                world.notifyBlocksOfNeighborChange(tilePos.down(queryPos), this.block);
-                if (railDirection.isSloped()) {
-                    world.notifyBlocksOfNeighborChange(tilePos.up(queryPos), this.block);
+                if (!railDirection.isSloped()) {
+                    world.notifyBlocksInCapsuleOfNeighborChange(Direction.DOWN, tilePos, this.block);
+                } else {
+                    world.notifyBlocksOfNeighborChanges(new TilePosc[] {
+                        tilePos,
+                        tilePos.down(queryPos),
+                        tilePos.up(new TilePos()),
+                    }, this.block);
                 }
             }
         } else if (block != null && block.isSignalSource() && new Rail(world, tilePos).getAdjacentTracks() == 3) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRailDetector.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRailDetector.java
index c20b3a53b..ebaf81c12 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRailDetector.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRailDetector.java
@@ -5,6 +5,7 @@ import java.util.Random;
 
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.vehicle.EntityMinecart;
+import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.World;
@@ -76,15 +77,15 @@ public class BlockLogicRailDetector extends BlockLogicRail {
         }
         if (minecartPresent && !isPowered) {
             // Set rail to powered
-            world.setBlockDataNotify(tilePos, meta | MASK_POWERED);
-            world.notifyBlocksOfNeighborChange(tilePos, this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.down(new TilePos()), this.block);
+            world.setBlockData(tilePos, meta | MASK_POWERED);
+            world.markBlockNeedsUpdate(tilePos);
+            world.notifyBlocksInCapsuleOfNeighborChange(Direction.DOWN, tilePos, this.block);
             world.markBlockDirty(tilePos);
         } else if (!minecartPresent && isPowered) {
             // Set rail to unpowered
-            world.setBlockDataNotify(tilePos, meta & ~MASK_POWERED);
-            world.notifyBlocksOfNeighborChange(tilePos, this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.down(new TilePos()), this.block);
+            world.setBlockData(tilePos, meta & ~MASK_POWERED);
+            world.markBlockNeedsUpdate(tilePos);
+            world.notifyBlocksInCapsuleOfNeighborChange(Direction.DOWN, tilePos, this.block);
             world.markBlockDirty(tilePos);
         }
         if (minecartPresent) {
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 3b548f2df..b5c8862f7 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
@@ -33,18 +33,12 @@ public class BlockLogicRedstone extends BlockLogic {
 
     @Override
     public void onPlacedByWorld(@NotNull World world, @NotNull TilePosc tilePos) {
-        TilePos queryPos = new TilePos();
-        for (Direction dir : Direction.directions) {
-            world.notifyBlocksOfNeighborChange(tilePos.add(dir, queryPos), this.block);
-        }
+        world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
-        TilePos queryPos = new TilePos();
-        for (Direction dir : Direction.directions) {
-            world.notifyBlocksOfNeighborChange(tilePos.add(dir, queryPos), this.block);
-        }
+        world.notifyBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
 
@@ -56,38 +50,38 @@ public class BlockLogicRedstone extends BlockLogic {
 
         final double d = 0.0625;
         TilePos queryPos = new TilePos();
-        for (Direction dir : Direction.directions) {
+        for (final var dir : Direction.all) {
             double px = (double) tilePos.x() + random.nextFloat();
             double py = (double) tilePos.y() + random.nextFloat();
             double pz = (double) tilePos.z() + random.nextFloat();
             switch (dir) {
                 case DOWN:
-                    if (!world.isBlockOpaqueCube(tilePos.add(dir.getOpposite(), queryPos))) {
+                    if (!world.isBlockOpaqueCube(tilePos.add(dir.opposite(), queryPos))) {
                         py = (tilePos.y() + 1) + d;
                     }
                     break;
                 case UP:
-                    if (!world.isBlockOpaqueCube(tilePos.add(dir.getOpposite(), queryPos))) {
+                    if (!world.isBlockOpaqueCube(tilePos.add(dir.opposite(), queryPos))) {
                         py = (tilePos.y() + 0) - d;
                     }
                     break;
                 case NORTH:
-                    if (!world.isBlockOpaqueCube(tilePos.add(dir.getOpposite(), queryPos))) {
+                    if (!world.isBlockOpaqueCube(tilePos.add(dir.opposite(), queryPos))) {
                         pz = (tilePos.z() + 1) + d;
                     }
                     break;
                 case SOUTH:
-                    if (!world.isBlockOpaqueCube(tilePos.add(dir.getOpposite(), queryPos))) {
+                    if (!world.isBlockOpaqueCube(tilePos.add(dir.opposite(), queryPos))) {
                         pz = (tilePos.z() + 0) - d;
                     }
                     break;
                 case WEST:
-                    if (!world.isBlockOpaqueCube(tilePos.add(dir.getOpposite(), queryPos))) {
+                    if (!world.isBlockOpaqueCube(tilePos.add(dir.opposite(), queryPos))) {
                         px = (tilePos.x() + 1) + d;
                     }
                     break;
                 case EAST:
-                    if (!world.isBlockOpaqueCube(tilePos.add(dir.getOpposite(), queryPos))) {
+                    if (!world.isBlockOpaqueCube(tilePos.add(dir.opposite(), queryPos))) {
                         px = (tilePos.x() + 0) - d;
                     }
                     break;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRepeater.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRepeater.java
index b4a081d48..7fa8e6d3f 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRepeater.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRepeater.java
@@ -67,17 +67,22 @@ public class BlockLogicRepeater extends BlockLogic implements ISupportable {
 
     @Override
     public boolean canStay(@NotNull World world, @NotNull TilePosc tilePos) {
-        return world.canPlaceOnSurfaceOfBlock(tilePos.down(new TilePos())) && super.canStay(world, tilePos);
+        return this.isSupported(world, tilePos, Side.BOTTOM) && super.canStay(world, tilePos);
     }
 
     @Override
     public void updateTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand, boolean isRandomTick) {
         int meta = world.getBlockData(tilePos);
         boolean powered = isGettingPower(world, tilePos, meta);
+        var side = getSideFromMeta(meta);
         if (this.isRepeaterPowered && !powered) {
-            world.setBlockTypeDataNotify(tilePos, Blocks.REPEATER_IDLE, meta);
+            world.setBlockTypeDataRaw(tilePos, Blocks.REPEATER_IDLE, meta);
+            world.markBlockNeedsUpdate(tilePos);
+            world.notifyBlocksInCapsuleOfNeighborChange(side.opposite(), tilePos, Blocks.REPEATER_IDLE);
         } else if (!this.isRepeaterPowered) {
-            world.setBlockTypeDataNotify(tilePos, Blocks.REPEATER_ACTIVE, meta);
+            world.setBlockTypeDataRaw(tilePos, Blocks.REPEATER_ACTIVE, meta);
+            world.markBlockNeedsUpdate(tilePos);
+            world.notifyBlocksInCapsuleOfNeighborChange(side.opposite(), tilePos, Blocks.REPEATER_IDLE);
             if (!powered) {
                 int tickDelay = (meta & MASK_TICK_DELAY) >> 2;
                 world.scheduleBlockUpdate(tilePos, Blocks.REPEATER_ACTIVE, tickDelayMap[tickDelay] * 2);
@@ -96,8 +101,8 @@ public class BlockLogicRepeater extends BlockLogic implements ISupportable {
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
         Side back = getSideFromMeta(data);
-        Side front = back.getOpposite();
-        world.notifyBlocksOfNeighborChange(tilePos.add(front.getDirection(), new TilePos()), this.block);
+        Side front = back.opposite();
+        world.notifyBlocksOfNeighborChange(tilePos.add(front.direction(), new TilePos()), this.block);
     }
 
     @Override
@@ -144,7 +149,7 @@ public class BlockLogicRepeater extends BlockLogic implements ISupportable {
         Side direction = getSideFromMeta(meta);
         if (direction.isVertical()) return false;
 
-        TilePos p = tilePos.add(direction.getDirection(), new TilePos());
+        TilePos p = tilePos.add(direction.direction(), new TilePos());
         return world.hasSignal(p, direction) || (world.getBlockType(p) == Blocks.WIRE_REDSTONE && (world.getBlockData(p) & BlockLogicWireRedstone.MASK_POWER) != 0);
     }
 
@@ -168,7 +173,7 @@ public class BlockLogicRepeater extends BlockLogic implements ISupportable {
 
     @Override
     public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
-        int l = mob.getHorizontalPlacementDirection(side).index;
+        int l = mob.getHorizontalPlacementDirection(side).legacyIndex();
         world.setBlockDataNotify(tilePos, l);
         if (isGettingPower(world, tilePos, l)) {
             world.scheduleBlockUpdate(tilePos, this.block, 1);
@@ -178,7 +183,7 @@ public class BlockLogicRepeater extends BlockLogic implements ISupportable {
     @Override
     public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
         if (!side.isHorizontal()) side = Side.NORTH;
-        int l = side.getDirection().index;
+        int l = side.direction().legacyIndex();
         world.setBlockDataNotify(tilePos, l);
         if (isGettingPower(world, tilePos, l)) {
             world.scheduleBlockUpdate(tilePos, this.block, 1);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java
index b446cf34a..df176e776 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java
@@ -17,22 +17,22 @@ public abstract class BlockLogicRotatable extends BlockLogic {
     }
 
     public static @NotNull Direction getDirectionFromMeta(int meta) {
-        return Direction.getDirectionById(meta & MASK_DIRECTION);
+        return Direction.fromId(meta & MASK_DIRECTION);
     }
 
     public static int setDirection(int meta, Direction direction) {
-        return (meta & ~MASK_DIRECTION) | direction.getId();
+        return (meta & ~MASK_DIRECTION) | direction.id;
     }
 
     @Override
     public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
-        world.setBlockDataNotify(tilePos, mob.getHorizontalPlacementDirection(side).getOpposite().getId());
+        world.setBlockDataNotify(tilePos, mob.getHorizontalPlacementDirection(side).opposite().id);
     }
 
     @Override
     public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
         if (!side.isHorizontal()) side = Side.SOUTH;
-        world.setBlockDataNotify(tilePos, BlockLogicRotatable.setDirection(0, side.getDirection()));
+        world.setBlockDataNotify(tilePos, BlockLogicRotatable.setDirection(0, side.direction()));
     }
 
     public static void setDefaultDirection(@NotNull World world, @NotNull TilePosc tilePos) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglass.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglass.java
index 73e0d2fa8..79e2988bf 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglass.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglass.java
@@ -36,13 +36,12 @@ public class BlockLogicRubyglass extends BlockLogic
         }
 
         if (rand.nextInt(10) == 0) {
-            final int dir = rand.nextInt(6);
-            final @NotNull Direction direction = Direction.getDirectionById(dir);
+            final @NotNull Direction direction = Direction.ID_MAP[rand.nextInt(6)];
 
             final @NotNull TilePos checkPos = tilePos.add(direction, new TilePos());
             final @NotNull Block<?> checkBlock = world.getBlockType(checkPos);
             if (checkBlock == Blocks.AIR) {
-                world.spawnParticle("rubyglassLightning", tilePos.x(), tilePos.y(), tilePos.z(), 0, 0, 0, dir, 1600d, true);
+                world.spawnParticle("rubyglassLightning", tilePos.x(), tilePos.y(), tilePos.z(), 0, 0, 0, direction.id, 1600d, true);
             }
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassSprout.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassSprout.java
index 35fff8255..026c98085 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassSprout.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassSprout.java
@@ -32,7 +32,7 @@ public class BlockLogicRubyglassSprout extends BlockLogic {
             || (side == Side.WEST && world.isBlockNormalCube(tilePos.east(temp)))
             || (side == Side.BOTTOM && world.isBlockNormalCube(tilePos.up(temp)))
             || (side == Side.TOP && world.isBlockNormalCube(tilePos.down(temp)))) {
-            world.setBlockDataNotify(tilePos, BlockLogicRotatable.setDirection(0, side.getDirection()));
+            world.setBlockDataNotify(tilePos, BlockLogicRotatable.setDirection(0, side.direction()));
         } else {
             world.setBlockDataNotify(tilePos, BlockLogicRotatable.setDirection(0, getOrientation(world, tilePos)));
         }
@@ -43,26 +43,26 @@ public class BlockLogicRubyglassSprout extends BlockLogic {
         final @NotNull TilePos temp = new TilePos();
         if(world.isBlockNormalCube(tilePos.west(temp)))
         {
-            return Side.EAST.getDirection();
+            return Side.EAST.direction();
         }
         if(world.isBlockNormalCube(tilePos.east(temp)))
         {
-            return Side.WEST.getDirection();
+            return Side.WEST.direction();
         }
         if(world.isBlockNormalCube(tilePos.south(temp)))
         {
-            return Side.NORTH.getDirection();
+            return Side.NORTH.direction();
         }
         if (world.isBlockNormalCube(tilePos.north(temp))){
-            return  Side.SOUTH.getDirection();
+            return  Side.SOUTH.direction();
         }
         if (world.isBlockNormalCube(tilePos.down(temp))){
-            return  Side.TOP.getDirection();
+            return  Side.TOP.direction();
         }
         if (world.isBlockNormalCube(tilePos.up(temp))){
-            return  Side.BOTTOM.getDirection();
+            return  Side.BOTTOM.direction();
         }
-        return Side.TOP.getDirection();
+        return Side.TOP.direction();
     }
 
     @Override
@@ -170,7 +170,7 @@ public class BlockLogicRubyglassSprout extends BlockLogic {
         {
             direction = Direction.DOWN;
         }
-        world.setBlockDataNotify(tilePos, direction.getId());
+        world.setBlockDataNotify(tilePos, direction.id);
     }
 
     @Override
@@ -190,7 +190,7 @@ public class BlockLogicRubyglassSprout extends BlockLogic {
     }
 
     public static Direction metaToDirection(int meta) {
-        return Direction.getDirectionById(meta & MASK_DIRECTION);
+        return Direction.fromId(meta & MASK_DIRECTION);
     }
 
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSlab.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSlab.java
index 9dfd95b90..cd85f0a6f 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSlab.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSlab.java
@@ -82,12 +82,6 @@ public class BlockLogicSlab extends BlockLogic
         return (meta & MASK_STATE) == STATE_DOUBLE;
     }
 
-    @Override
-    public boolean canPlaceOnSurfaceOnCondition(@NotNull World world, @NotNull TilePosc tilePos) {
-        int meta = world.getBlockData(tilePos);
-        return (meta & MASK_STATE) != STATE_LOWER;
-    }
-
     @Override
     public @NotNull ISupport getSupport(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
         int state = world.getBlockData(tilePos) & MASK_STATE;
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 185255109..8236bc71d 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
@@ -46,16 +46,16 @@ public class BlockLogicSpikes extends BlockLogic
             meta |= MASK_POWERED;
         }
 
-        Direction placeDir = sidePlacedOnto.getOpposite().getDirection();
+        Direction placeDir = sidePlacedOnto.opposite().direction();
 
         if (mob instanceof Player player) {
-            if (player.rotationLock != null && player.rotationLock != Direction.NONE) {
+            if (player.rotationLock != null) {
                 placeDir = player.rotationLock;
             }
         }
 
         meta &= ~MASK_DIRECTION;
-        meta |= ((placeDir.getId() << 1) & MASK_DIRECTION);
+        meta |= placeDir.id << 1;
 
         world.setBlockDataNotify(tilePos, meta);
     }
@@ -145,7 +145,7 @@ public class BlockLogicSpikes extends BlockLogic
     }
 
     public static @NotNull Direction getDirectionFromData(int data) {
-        return Direction.getDirectionById((data & MASK_DIRECTION) >> 1);
+        return Direction.fromId((data & MASK_DIRECTION) >> 1);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSponge.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSponge.java
index 171e60515..2a1c03195 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSponge.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSponge.java
@@ -51,8 +51,7 @@ public class BlockLogicSponge extends BlockLogic {
 
     public boolean inWater(@NotNull World world, @NotNull TilePosc tilePos) {
         TilePos queryPos = new TilePos();
-        for (int i = 0; i < Direction.directions.length; i++) {
-            Direction dir = Direction.directions[i];
+        for (final var dir : Direction.all) {
             Block<?> b = world.getBlockType(tilePos.add(dir, queryPos));
             if (Blocks.hasTag(b, BlockTags.IS_WATER)) return true;
         }
@@ -61,8 +60,7 @@ public class BlockLogicSponge extends BlockLogic {
 
     public boolean inHot(@NotNull World world, @NotNull TilePosc tilePos) {
         TilePos queryPos = new TilePos();
-        for (int i = 0; i < Direction.directions.length; i++) {
-            Direction dir = Direction.directions[i];
+        for (final var dir : Direction.all) {
             Block<?> b = world.getBlockType(tilePos.add(dir, queryPos));
             if (b == Blocks.MAGMA || Blocks.hasTag(b, BlockTags.IS_LAVA)) return true;
         }
@@ -89,7 +87,7 @@ public class BlockLogicSponge extends BlockLogic {
     public void animationTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand) {
         if (this.isWet) {
             Side side = Side.sides[rand.nextInt(6)]; // Get random side
-            if (side == Side.TOP || world.isBlockOpaqueCube(tilePos.add(side.getDirection(), new TilePos()))) return;
+            if (side == Side.TOP || world.isBlockOpaqueCube(tilePos.add(side.direction(), new TilePos()))) return;
             float off1 = rand.nextFloat() * 0.9f + 0.05f;
             float off2 = rand.nextFloat() * 0.9f + 0.05f;
             float lOff = 0.01f; // Dumb offset to fix bad lighting
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicStairs.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicStairs.java
index e4379a2b9..3ce6bd769 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicStairs.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicStairs.java
@@ -200,19 +200,14 @@ public class BlockLogicStairs extends BlockLogic {
         return false;
     }
 
-    @Override
-    public boolean canPlaceOnSurfaceOnCondition(@NotNull World world, @NotNull TilePosc tilePos) {
-        return (world.getBlockData(tilePos) & MASK_ROTATION_VERTICAL) != 0;
-    }
-
     @Override
     public @NotNull ISupport getSupport(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
         int meta = world.getBlockData(tilePos);
         int hRot = meta & MASK_ROTATION_HORIZONTAL;
         if ((meta & MASK_ROTATION_VERTICAL) != 0) {
-            return upperStairsSupportMapping[side.getId() * 4 + hRot];
+            return upperStairsSupportMapping[side.id * 4 + hRot];
         } else {
-            return lowerStairsSupportMapping[side.getId() * 4 + hRot];
+            return lowerStairsSupportMapping[side.id * 4 + hRot];
         }
     }
 
@@ -340,7 +335,7 @@ public class BlockLogicStairs extends BlockLogic {
     public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
         int meta = world.getBlockData(tilePos) & 0xF0;
 
-        Direction hRotation = mob.getHorizontalPlacementDirection(side).getOpposite();
+        Direction hRotation = mob.getHorizontalPlacementDirection(side).opposite();
         if (hRotation == Direction.NORTH) meta |= H_ROTATION_NORTH;
         if (hRotation == Direction.EAST) meta |= H_ROTATION_EAST;
         if (hRotation == Direction.SOUTH) meta |= H_ROTATION_SOUTH;
@@ -357,16 +352,16 @@ public class BlockLogicStairs extends BlockLogic {
     public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
         int meta = world.getBlockData(tilePos) & 0xF0;
 
-        side = side.getOpposite();
+        side = side.opposite();
         Side hSide = !side.isHorizontal() ? Side.NORTH : side;
-        Direction hRotation = hSide.getOpposite().getDirection();
+        Direction hRotation = hSide.opposite().direction();
         if (hRotation == Direction.NORTH) meta |= H_ROTATION_NORTH;
         if (hRotation == Direction.EAST) meta |= H_ROTATION_EAST;
         if (hRotation == Direction.SOUTH) meta |= H_ROTATION_SOUTH;
         if (hRotation == Direction.WEST) meta |= H_ROTATION_WEST;
 
         Side vSide = !side.isVertical() ? Side.BOTTOM : side;
-        Direction vRotation = vSide.getDirection();
+        Direction vRotation = vSide.direction();
 //        if (vRotation == Direction.DOWN) meta |= 0b0000;
         if (vRotation == Direction.UP) meta |= MASK_ROTATION_VERTICAL;
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTimer.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTimer.java
new file mode 100644
index 000000000..21648dc61
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTimer.java
@@ -0,0 +1,214 @@
+package net.minecraft.core.block;
+
+import net.minecraft.core.block.entity.TileEntity;
+import net.minecraft.core.block.entity.TileEntityActivator;
+import net.minecraft.core.block.entity.TileEntityTimer;
+import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.support.FullSupport;
+import net.minecraft.core.block.support.ISupport;
+import net.minecraft.core.block.support.ISupportable;
+import net.minecraft.core.block.support.PartialSupport;
+import net.minecraft.core.entity.Mob;
+import net.minecraft.core.entity.player.Player;
+import net.minecraft.core.enums.EnumDropCause;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.item.Items;
+import net.minecraft.core.util.helper.Axis;
+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.WorldSource;
+import net.minecraft.core.world.pos.TilePos;
+import net.minecraft.core.world.pos.TilePosc;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Random;
+
+public class BlockLogicTimer extends BlockLogic implements ISupportable {
+    public static final double[] torchPosOffset = {
+        -0.0625D, 0.0625D, 0.1875D, 0.3125D
+    };
+    public static final int[] tickDelayMap = {
+        4, 8, 12, 16
+    };
+
+    public static final int MASK_AXIS = 0b0000_0001;
+    public static final int AXIS_SOUTH_NORTH = 0;
+    public static final int AXIS_WEST_EAST = 1;
+
+    public static final int MASK_OUTPUT = 0b0000_0010;
+    public static final int OUTPUT_A = 0 << 1;
+    public static final int OUTPUT_B = 1 << 1;
+
+    public static final int MASK_DIRECTION = MASK_AXIS | MASK_OUTPUT;
+    public static final int DIRECTION_SOUTH = OUTPUT_A | AXIS_SOUTH_NORTH;
+    public static final int DIRECTION_WEST = OUTPUT_A | AXIS_WEST_EAST;
+    public static final int DIRECTION_NORTH = OUTPUT_B | AXIS_SOUTH_NORTH;
+    public static final int DIRECTION_EAST = OUTPUT_B | AXIS_WEST_EAST;
+
+    public static final int MASK_TICK_DELAY = 0b0000_1100;
+
+    public BlockLogicTimer(@NotNull Block<?> block) {
+        super(block, Materials.DECORATION);
+        setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 0.125F, 1.0F);
+        block.withEntity(TileEntityTimer::new);
+    }
+
+    @Override
+    public boolean isCubeShaped() {
+        return false;
+    }
+
+    @Override
+    public @NotNull ISupport getSupport(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        return PartialSupport.INSTANCE;
+    }
+
+    @Override
+    public boolean canPlaceAt(@NotNull World world, @NotNull TilePosc tilePos) {
+        return this.isSupported(world, tilePos, Side.BOTTOM);
+    }
+
+    @Override
+    public ItemStack[] getBreakResult(@NotNull World world, @NotNull EnumDropCause dropCause, int data, @Nullable TileEntity tileEntity) {
+        return new ItemStack[]{new ItemStack(Items.TIMER)};
+    }
+
+    @Override
+    public boolean canStay(@NotNull World world, @NotNull TilePosc tilePos) {
+        return this.isSupported(world, tilePos, Side.BOTTOM) && super.canStay(world, tilePos);
+    }
+
+    // Ensure that timers can activate doors, tnt, and other redstone components through powering a neighboring block.
+    @Override
+    public boolean isSignalSource() {
+        return true;
+    }
+
+    // Cause block updates on the target block when removing a timer,
+    // which prevents redstone components on the other wide of the target block from staying powered.
+    @Override
+    public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
+        Side back = getSideFromMeta(data);
+        Side front = back.opposite();
+        world.notifyBlocksOfNeighborChange(tilePos.add(front.direction(), new TilePos()), this.block);
+    }
+
+    @Override
+    public boolean isEmittingDirectSignal(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        return isEmittingSignal(world, tilePos, side);
+    }
+
+    @Override
+    public boolean isEmittingSignal(@NotNull WorldSource source, @NotNull TilePosc tilePos, @NotNull Side side) {
+        return side == getSideFromMeta(source.getBlockData(tilePos));
+    }
+
+    public static @NotNull Side getSideFromMeta(final int meta) {
+        return switch (meta & MASK_DIRECTION) {
+            case DIRECTION_SOUTH -> Side.SOUTH;
+            case DIRECTION_WEST -> Side.WEST;
+            case DIRECTION_NORTH -> Side.NORTH;
+            case DIRECTION_EAST -> Side.EAST;
+            default -> throw new RuntimeException("No side for direction " + (meta & 0b11) + "!");
+        };
+    }
+
+    @Override
+    public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
+        if (!canStay(world, tilePos)) {
+            dropWithCause(world, EnumDropCause.WORLD, tilePos, world.getBlockData(tilePos), null, null);
+            world.setBlockTypeNotify(tilePos, Blocks.AIR);
+        }
+    }
+
+    @Override
+    public boolean onInteracted(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Player player, @Nullable Side side, double xHit, double yHit) {
+        int metadata = world.getBlockData(tilePos);
+        int tickDelay = (metadata & MASK_TICK_DELAY) >> 2;
+        tickDelay = ((tickDelay + 1) << 2) & MASK_TICK_DELAY;
+        world.setBlockDataNotify(tilePos, tickDelay | metadata & MASK_DIRECTION);
+        return true;
+    }
+
+    @Override
+    public void onActivatorInteracted(@NotNull World world, @NotNull TilePosc tilePos, @NotNull TileEntityActivator activator, @NotNull Direction direction) {
+        int metadata = world.getBlockData(tilePos);
+        int tickDelay = (metadata & MASK_TICK_DELAY) >> 2;
+        tickDelay = ((tickDelay + 1) << 2) & MASK_TICK_DELAY;
+        world.setBlockDataNotify(tilePos, tickDelay | metadata & MASK_DIRECTION);
+    }
+
+    @Override
+    public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
+        final @NotNull Axis axis = mob.getHorizontalPlacementDirection(side).axis();
+        assert(axis == Axis.X || axis == Axis.Z);
+        world.setBlockDataNotify(tilePos, axis == Axis.Z ? AXIS_SOUTH_NORTH : AXIS_WEST_EAST);
+    }
+
+    @Override
+    public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
+        if (!side.isHorizontal()) side = Side.NORTH;
+        final @NotNull Axis axis = side.direction().axis();
+        assert(axis == Axis.X || axis == Axis.Z);
+        world.setBlockDataNotify(tilePos, axis == Axis.Z ? AXIS_SOUTH_NORTH : AXIS_WEST_EAST);
+    }
+
+    @Override
+    public boolean isSolidRender() {
+        return false;
+    }
+
+    @Override
+    public void animationTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand) {
+        int meta = world.getBlockData(tilePos);
+        double px = (tilePos.x() + 0.5D) + (rand.nextFloat() - 0.5D) * 0.2D;
+        double py = (tilePos.y() + 0.4D) + (rand.nextFloat() - 0.5D) * 0.2D;
+        double pz = (tilePos.z() + 0.5D) + (rand.nextFloat() - 0.5D) * 0.2D;
+
+        double xOffset = 0.0;
+        double zOffset = 0.0;
+
+        final int redstoneBrightness = 15;
+        int tickDelay = (meta & MASK_TICK_DELAY) >> 2;
+
+        if (rand.nextInt(2) == 0) {
+            switch (getSideFromMeta(meta)) {
+                case SOUTH:
+                    zOffset = -torchPosOffset[tickDelay];
+                    break;
+                case NORTH:
+                    zOffset = torchPosOffset[tickDelay];
+                    break;
+                case EAST:
+                    xOffset = -torchPosOffset[tickDelay];
+                    break;
+                case WEST:
+                    xOffset = torchPosOffset[tickDelay];
+                    break;
+            }
+        } else {
+            switch (getSideFromMeta(meta)) {
+                case SOUTH:
+                    zOffset = torchPosOffset[tickDelay];
+                    break;
+                case NORTH:
+                    zOffset = -torchPosOffset[tickDelay];
+                    break;
+                case EAST:
+                    xOffset = torchPosOffset[tickDelay];
+                    break;
+                case WEST:
+                    xOffset = -torchPosOffset[tickDelay];
+                    break;
+            }
+        }
+        world.spawnParticle("reddust", px + xOffset, py, pz + zOffset, 0, 0, 0, redstoneBrightness, false);
+    }
+
+    @Override
+    public @NotNull ISupport getSupportConstraint(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        return FullSupport.INSTANCE;
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorch.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorch.java
index 3480259ad..540fa475e 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorch.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorch.java
@@ -74,14 +74,14 @@ public class BlockLogicTorch extends BlockLogic implements ISupportable {
 
     @Override
     public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
-        if (side.isHorizontal()) side = side.getOpposite();
+        if (side.isHorizontal()) side = side.opposite();
         onPlacedOnSide(world, tilePos, side, xHit, yHit);
     }
 
     @Override
     public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
         int orientation = SIDE_NONE;
-        if (side.isHorizontal()) side = side.getOpposite();
+        if (side.isHorizontal()) side = side.opposite();
         switch (side) {
             case TOP:
                 if (isSupported(world, tilePos, Side.BOTTOM)) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorchRedstone.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorchRedstone.java
index 11ee242b9..f1b94382d 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorchRedstone.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicTorchRedstone.java
@@ -32,28 +32,12 @@ public class BlockLogicTorchRedstone extends BlockLogicTorch {
         if (world.getBlockData(tilePos) == SIDE_NONE) {
             super.onPlacedByWorld(world, tilePos);
         }
-        TilePos queryPos = new TilePos();
-        if (this.torchActive) {
-            world.notifyBlocksOfNeighborChange(tilePos.down(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.up(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.west(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.east(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.north(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.south(queryPos), this.block);
-        }
+        world.notifyShellBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
     public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
-        TilePos queryPos = new TilePos();
-        if (this.torchActive) {
-            world.notifyBlocksOfNeighborChange(tilePos.down(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.up(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.west(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.east(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.north(queryPos), this.block);
-            world.notifyBlocksOfNeighborChange(tilePos.south(queryPos), this.block);
-        }
+        world.notifyShellBlocksInRadiusOfNeighborChange(2, tilePos, this.block);
     }
 
     @Override
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 a78b3754a..c9d86983b 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
@@ -150,7 +150,7 @@ public class BlockLogicTrapDoor extends BlockLogic implements IPaintable {
 
     @Override
     public int getPlacedData(@Nullable Player player, @NotNull ItemStack itemStack, @NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
-        int meta = getMetaForDirection(player == null ? side.getDirection().getOpposite() : player.getHorizontalPlacementDirection(side, PlacementMode.SIDE));
+        int meta = getMetaForDirection(player == null ? side.direction().opposite() : player.getHorizontalPlacementDirection(side, PlacementMode.SIDE));
         if (player != null && player.getVerticalPlacementDirection(side, yHit) == Direction.UP) {
             meta = setUpperHalf(meta, true);
         }
@@ -165,7 +165,7 @@ public class BlockLogicTrapDoor extends BlockLogic implements IPaintable {
         if (isTrapdoorOpen(meta) && (block != null && block.getLogic() instanceof BlockLogicLadder ladder)) {
             Side ladderSide = ladder.getSideFromMeta(world.getBlockData(tilePos.down(queryPos)));
             Direction trapdoorDirection = getDirectionForMeta(meta);
-            return ladderSide.getDirection() == trapdoorDirection.getOpposite();
+            return ladderSide.direction() == trapdoorDirection.opposite();
         }
         return false;
     }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicVeryRotatable.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicVeryRotatable.java
index 886abf81b..ef39e7d8e 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicVeryRotatable.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicVeryRotatable.java
@@ -18,22 +18,22 @@ public abstract class BlockLogicVeryRotatable
 
     @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) {
-        final @NotNull Direction direction = mob.getPlacementDirection(side).getOpposite();
+        final @NotNull Direction direction = mob.getPlacementDirection(side).opposite();
 
         world.setBlockDataNotify(tilePos, setDirection(0, direction));
     }
 
     @Override
     public void onPlacedOnSide(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final double xHit, final double yHit) {
-        world.setBlockDataNotify(tilePos, setDirection(0, side.getDirection()));
+        world.setBlockDataNotify(tilePos, setDirection(0, side.direction()));
     }
 
     public static int setDirection(final int meta, final @NotNull Direction direction) {
-        return (meta & ~MASK_DIRECTION) | (direction.getId() & MASK_DIRECTION);
+        return (meta & ~MASK_DIRECTION) | direction.id;
     }
 
     public static @NotNull Direction metaToDirection(final int meta) {
-        return Direction.getDirectionById(meta & MASK_DIRECTION);
+        return Direction.fromId(meta & MASK_DIRECTION);
     }
 
 }
\ No newline at end of file
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicWireRedstone.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicWireRedstone.java
index 0754aeb82..3621bd2a6 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicWireRedstone.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicWireRedstone.java
@@ -369,7 +369,7 @@ public class BlockLogicWireRedstone extends BlockLogic implements ISupportable {
 
         boolean isXConnected = posXShouldConnectTo || negXShouldConnectTo;
         boolean isZConnected = posZShouldConnectTo || negZShouldConnectTo;
-        return !isZConnected && !isXConnected && side.getAxis() != Axis.Y // Default single dust
+        return !isZConnected && !isXConnected && side.axis() != Axis.Y // Default single dust
             // When the signal request comes from a block that dust connects to (usually power sources)
             || side == Side.SOUTH && negZShouldConnectTo
             || side == Side.NORTH && posZShouldConnectTo
@@ -418,17 +418,21 @@ public class BlockLogicWireRedstone extends BlockLogic implements ISupportable {
         }
         if (block == Blocks.PUMPKIN_REDSTONE) {
             if (side == Side.NONE || side.isVertical()) return false;
-            return Side.getSideById(source.getBlockData(tilePos)).getOpposite() == side;
+            return Side.fromId(source.getBlockData(tilePos)).opposite() == side;
         }
         if (block == Blocks.MATCHER || block == Blocks.MATCHER_ACTIVE) {
-            return BlockLogicAxisAligned.metaToAxis(source.getBlockData(tilePos)) != side.getAxis();
+            return BlockLogicAxisAligned.metaToAxis(source.getBlockData(tilePos)) != side.axis();
         }
         if (block == Blocks.REPEATER_IDLE || block == Blocks.REPEATER_ACTIVE) {
             final Side direction = BlockLogicRepeater.getSideFromMeta(source.getBlockData(tilePos));
-            return side == direction || side == direction.getOpposite();
+            return side == direction || side == direction.opposite();
+        }
+        if (block == Blocks.TIMER) {
+            final Side direction = BlockLogicTimer.getSideFromMeta(source.getBlockData(tilePos));
+            return side == direction || side == direction.opposite();
         }
         if (block == Blocks.CONDUIT) {
-            return BlockLogicConduit.isSideOpen(source.getBlockData(tilePos), side.getOpposite());
+            return BlockLogicConduit.isSideOpen(source.getBlockData(tilePos), side.opposite());
         }
         return block.isSignalSource();
     }
@@ -453,7 +457,7 @@ public class BlockLogicWireRedstone extends BlockLogic implements ISupportable {
         if (side2 == Side.BOTTOM) {
             //top wire looking down at bottom
             if (side1 != Side.NONE) {
-                TilePos wallTilePos = new TilePos(tilePos).add(side1.getOpposite());
+                TilePos wallTilePos = new TilePos(tilePos).add(side1.opposite());
                 if (isBlockingConduit(worldSource, wallTilePos, side1)) return false;
 
                 //transparent diagonal connection
@@ -468,7 +472,7 @@ public class BlockLogicWireRedstone extends BlockLogic implements ISupportable {
             } else {
                 //fallback
                 for (Side fSide : new Side[]{Side.NORTH, Side.SOUTH, Side.EAST, Side.WEST}) {
-                    TilePos wallTilePos = new TilePos(tilePos).add(fSide.getOpposite());
+                    TilePos wallTilePos = new TilePos(tilePos).add(fSide.opposite());
                     if (worldSource.getBlockType(new TilePos(wallTilePos).up()) == Blocks.WIRE_REDSTONE) {
                         if (isBlockingConduit(worldSource, wallTilePos, fSide)) return false;
 
@@ -487,11 +491,11 @@ public class BlockLogicWireRedstone extends BlockLogic implements ISupportable {
             //bottom wire is looking up at top
             TilePos wallTilePos = new TilePos(tilePos).down();
             if (side1 != Side.NONE) {
-                if (isBlockingConduit(worldSource, wallTilePos, side1.getOpposite())) return false;
+                if (isBlockingConduit(worldSource, wallTilePos, side1.opposite())) return false;
 
                 //transparent diagonal connection
-                if (!isSolidWallForWire(worldSource, wallTilePos, side1.getOpposite())) {
-                    TilePos bottomWireTilePos = new TilePos(wallTilePos).add(side1.getOpposite());
+                if (!isSolidWallForWire(worldSource, wallTilePos, side1.opposite())) {
+                    TilePos bottomWireTilePos = new TilePos(wallTilePos).add(side1.opposite());
                     int bottomPower = worldSource.getBlockData(bottomWireTilePos) & MASK_POWER;
                     int topPower = worldSource.getBlockData(tilePos) & MASK_POWER;
 
@@ -546,7 +550,7 @@ public class BlockLogicWireRedstone extends BlockLogic implements ISupportable {
             return true;
         }
         if (world.getBlockType(wallTilePos) == Blocks.CONDUIT) {
-            return !BlockLogicConduit.isSideOpen(world.getBlockData(wallTilePos), directionToWall.getOpposite());
+            return !BlockLogicConduit.isSideOpen(world.getBlockData(wallTilePos), directionToWall.opposite());
         }
         return false;
     }
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 197abfabe..0f412e6ec 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
@@ -7,8 +7,7 @@ import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.block.piston.BlockLogicPistonBase;
 import net.minecraft.core.block.piston.BlockLogicPistonHead;
 import net.minecraft.core.block.piston.BlockLogicPistonMoving;
-import net.minecraft.core.block.piston.BlockLogicPistonBaseSteel;
-import net.minecraft.core.block.piston.BlockLogicPistonBaseSticky;
+import net.minecraft.core.block.piston.PistonCommon;
 import net.minecraft.core.block.tag.BlockTags;
 import net.minecraft.core.data.tag.Tag;
 import net.minecraft.core.entity.Entity;
@@ -16,6 +15,7 @@ import net.minecraft.core.entity.Mob;
 import net.minecraft.core.entity.animal.MobButterfly;
 import net.minecraft.core.entity.animal.MobFireflyCluster;
 import net.minecraft.core.entity.player.Player;
+import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.Item;
 import net.minecraft.core.item.Items;
 import net.minecraft.core.item.block.*;
@@ -802,20 +802,38 @@ public final class Blocks {
     public static final @NotNull Block<BlockLogicRepeater> REPEATER_ACTIVE = register("repeater.active", "minecraft:block/repeater_active", 511, (BlockLogicSupplier<BlockLogicRepeater>)b -> new BlockLogicRepeater(b, true))
         .withSound(BlockSounds.STONE).withHardness(0.0F).withLightEmission(0.625F).withDisabledStats().withDisabledNeighborNotifyOnMetadataChange().withOverrideColor(MaterialColor.redstone).setStatParent(() -> REPEATER_IDLE)
         .withTags(BlockTags.MINEABLE_BY_PICKAXE, BlockTags.BROKEN_BY_FLUIDS, BlockTags.NOT_IN_CREATIVE_MENU, BlockTags.PREVENT_MOB_SPAWNS);
+    public static final @NotNull Block<BlockLogicTimer> TIMER = register("timer", "minecraft:block/timer", 512, BlockLogicTimer::new)
+        .withSound(BlockSounds.STONE).withHardness(0.0F).withDisabledStats().withDisabledNeighborNotifyOnMetadataChange().withOverrideColor(MaterialColor.redstone).setStatParent(() -> Items.TIMER)
+        .withTags(BlockTags.MINEABLE_BY_PICKAXE, BlockTags.BROKEN_BY_FLUIDS, BlockTags.NOT_IN_CREATIVE_MENU, BlockTags.PREVENT_MOB_SPAWNS);
 
-    public static final @NotNull Block<BlockLogicPistonBase> PISTON_BASE = register("piston.base", "minecraft:block/piston_base", 520, (BlockLogicSupplier<BlockLogicPistonBase>)b -> new BlockLogicPistonBase(b, 12))
+    public static final @NotNull Block<BlockLogicPistonBase> PISTON_BASE = register("piston.base", "minecraft:block/piston_base", 520, (BlockLogicSupplier<BlockLogicPistonBase>)b -> 
+        new BlockLogicPistonBase(
+            b, PistonCommon.TYPE_NORMAL,
+            (byte)12, (byte)1, EnumDropCause.SILK_TOUCH, false, Double.NaN
+        ))
         .withSound(BlockSounds.STONE).withDisabledNeighborNotifyOnMetadataChange()
         .withTags(BlockTags.MINEABLE_BY_PICKAXE);
-    public static final @NotNull Block<BlockLogicPistonBase> PISTON_BASE_STICKY = register("piston.base.sticky", "minecraft:block/piston_base_sticky", 521, (BlockLogicSupplier<BlockLogicPistonBase>)b -> new BlockLogicPistonBaseSticky(b, 12))
+    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
+        ))
         .withSound(BlockSounds.STONE).withDisabledNeighborNotifyOnMetadataChange()
         .withTags(BlockTags.MINEABLE_BY_PICKAXE);
+
     public static final @NotNull Block<BlockLogicPistonHead> PISTON_HEAD = register("piston.head", "minecraft:block/piston_head", 522, (BlockLogicSupplier<BlockLogicPistonHead>)(b) -> new BlockLogicPistonHead(b, 4/16d, 4/16d))
         .withSound(BlockSounds.WOOD).withDisabledNeighborNotifyOnMetadataChange().withDisabledStats().withImmovableFlagSet()
         .withTags(BlockTags.NOT_IN_CREATIVE_MENU, BlockTags.MINEABLE_BY_PICKAXE);
+
     public static final @NotNull Block<BlockLogicPistonMoving> PISTON_MOVING = register("piston.moving", "minecraft:block/piston_moving", 523, BlockLogicPistonMoving::new)
         .withSound(BlockSounds.STONE).withDisabledStats()
         .withTags(BlockTags.NOT_IN_CREATIVE_MENU);
-    public static final @NotNull Block<BlockLogicPistonBase> PISTON_BASE_STEEL = register("piston.base.steel", "minecraft:block/piston_base_steel", 524, (BlockLogicSupplier<BlockLogicPistonBase>)b -> new BlockLogicPistonBaseSteel(b, 24))
+
+    public static final @NotNull Block<BlockLogicPistonBase> PISTON_BASE_STEEL = register("piston.base.steel", "minecraft:block/piston_base_steel", 524, (BlockLogicSupplier<BlockLogicPistonBase>)b -> 
+        new BlockLogicPistonBase(
+            b, PistonCommon.TYPE_STEEL,
+            (byte)24, (byte)1, EnumDropCause.PISTON_CRUSH, false, 2.0
+        ))
         .withSound(BlockSounds.METAL)
         .withHardness(5F)
         .withBlastResistance(2000F)
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntity.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntity.java
index b7a2a0605..535d28847 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntity.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntity.java
@@ -37,15 +37,8 @@ public abstract class TileEntity implements ICarriable
 
     public void setChanged()
     {
-        if(worldObj != null)
-        {
-            int id = getBlockId();
-            if (id != 0 && Blocks.getBlock(id).isSignalSource()) {
-                for (Side s : Side.sides){
-                    worldObj.notifyBlocksOfNeighborChange(tilePos.x + s.getOffsetX(), tilePos.y + s.getOffsetY(), tilePos.z + s.getOffsetZ(), getBlockId());
-                }
-            }
-            worldObj.updateTileEntityChunkAndSendToPlayer(tilePos.x, tilePos.y, tilePos.z, this);
+        if(worldObj != null) {
+            worldObj.updateTileEntityChunkAndSendToPlayer(tilePos, this);
         }
     }
     
@@ -143,9 +136,9 @@ public abstract class TileEntity implements ICarriable
     @Override
     public boolean tryPlace(World world, Entity holder, int blockX, int blockY, int blockZ, Side side, double xPlaced, double yPlaced) {
         CarriedBlock carriedBlock = this.carriedBlock;
-        tilePos.x = blockX + side.getOffsetX();
-        tilePos.y = blockY + side.getOffsetY();
-        tilePos.z = blockZ + side.getOffsetZ();
+        tilePos.x = blockX + side.offsetX();
+        tilePos.y = blockY + side.offsetY();
+        tilePos.z = blockZ + side.offsetZ();
 
         Block<?> currentBlock = world.getBlock(tilePos.x, tilePos.y, tilePos.z);
         if (currentBlock != null && !currentBlock.hasTag(BlockTags.PLACE_OVERWRITES)) return false;
@@ -159,12 +152,8 @@ public abstract class TileEntity implements ICarriable
         if (b != null && holder instanceof Mob) {
             b.onBlockPlacedByMob(world, tilePos.x, tilePos.y, tilePos.z, side, (Mob) holder, xPlaced, yPlaced);
         }
-        world.notifyBlockChange(tilePos.x, tilePos.y, tilePos.z, carriedBlock.blockId);
-        if (carriedBlock.blockId != 0 && Blocks.getBlock(carriedBlock.blockId).isSignalSource()) {
-            for (Side s : Side.sides){
-                world.notifyBlocksOfNeighborChange(tilePos.x + s.getOffsetX(), tilePos.y + s.getOffsetY(), tilePos.z + s.getOffsetZ(), getBlockId());
-            }
-        }
+        final boolean isSignal = Blocks.getBlock(carriedBlock.blockId).isSignalSource();
+        world.notifyBlocksInRadiusOfNeighborChange((isSignal) ? 2 : 1, tilePos, currentBlock);
         return true;
     }
 
@@ -193,13 +182,8 @@ public abstract class TileEntity implements ICarriable
         int currentMeta = world.getBlockMetadata(tilePos.x, tilePos.y, tilePos.z);
         world.removeBlockTileEntity(tilePos.x, tilePos.y, tilePos.z);
         world.setBlockRaw(tilePos.x, tilePos.y, tilePos.z, 0);
-        world.notifyBlockChange(tilePos.x, tilePos.y, tilePos.z, 0);
-        int id = getBlockId();
-        if (currentBlock.isSignalSource()) {
-            for (Side s : Side.sides){
-                worldObj.notifyBlocksOfNeighborChange(tilePos.x + s.getOffsetX(), tilePos.y + s.getOffsetY(), tilePos.z + s.getOffsetZ(), getBlockId());
-            }
-        }
+        final boolean isSignal = this.getBlock().isSignalSource();
+        world.notifyBlocksInRadiusOfNeighborChange((isSignal) ? 2 : 1, tilePos, currentBlock);
         this.worldObj = null;
         this.carriedBlock = getCarriedEntry(world, holder, currentBlock, currentMeta);
         return carriedBlock;
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityBasket.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityBasket.java
index 67f4f074d..0dc111172 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityBasket.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityBasket.java
@@ -4,7 +4,6 @@ import java.util.*;
 
 import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
-import net.minecraft.core.block.Blocks;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.EntityItem;
 import net.minecraft.core.entity.player.Player;
@@ -33,7 +32,8 @@ public class TileEntityBasket extends TileEntity implements ICarrySource {
         int currentNumInside = calcNumUnitsInside();
         if (currentNumInside != numUnitsInside && worldObj != null) {
             this.numUnitsInside = currentNumInside;
-            worldObj.notifyBlockChange(tilePos.x, tilePos.y, tilePos.z, worldObj.getBlockId(tilePos.x, tilePos.y, tilePos.z));
+            worldObj.markBlockNeedsUpdate(tilePos);
+            worldObj.notifyBlocksInRadiusOfNeighborChange(2, tilePos, getBlock());
             setChanged();
         }
     }
@@ -109,7 +109,6 @@ public class TileEntityBasket extends TileEntity implements ICarrySource {
         }
 
         updateNumUnits();
-        world.notifyBlockChange(this.tilePos.x, this.tilePos.y, this.tilePos.z, Blocks.BASKET.id());
     }
 
     @Override
@@ -168,12 +167,7 @@ public class TileEntityBasket extends TileEntity implements ICarrySource {
             }
         }
 
-        if (shouldUpdate) {
-            if (this.worldObj != null) {
-                this.worldObj.notifyBlockChange(this.tilePos.x, this.tilePos.y, this.tilePos.z, Blocks.BASKET.id());
-            }
-            updateNumUnits();
-        }
+        if (shouldUpdate) updateNumUnits();
     }
 
     private boolean importItemStack(final ItemStack stack) {
@@ -232,7 +226,6 @@ public class TileEntityBasket extends TileEntity implements ICarrySource {
 
         this.contents.clear();
 
-        world.notifyBlockChange(x, y, z, Blocks.BASKET.id());
         updateNumUnits();
     }
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityDispatcher.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityDispatcher.java
index 6137d822f..c43ced3f4 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityDispatcher.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityDispatcher.java
@@ -90,6 +90,7 @@ public class TileEntityDispatcher {
         addMapping(TileEntityMesh.class, NamespaceID.fromPool("minecraft", "mesh"));
         addMapping(TileEntityMeshGold.class, NamespaceID.fromPool("minecraft", "mesh_gold"));
         addMapping(TileEntityStatue.class, NamespaceID.fromPool("minecraft", "statue_stone"));
+        addMapping(TileEntityTimer.class, NamespaceID.fromPool("minecraft", "timer"));
     }
 
     @Deprecated
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java
index 73fac3ed1..6f08d6bd5 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java
@@ -72,11 +72,11 @@ public class TileEntityMesh extends TileEntity {
             if (entity instanceof EntityItem item && entity.isInWater())
             {
                 if(item.meshSiphoning == null && item.siphonDirection == Direction.NONE){
-                    item.siphonDirection = direction.getOpposite();
+                    item.siphonDirection = direction.opposite();
                     item.meshSiphoning = this;
                 }
 
-                if(direction.getOpposite().equals(item.siphonDirection) && item.meshSiphoning == this) {
+                if(direction.opposite().equals(item.siphonDirection) && item.meshSiphoning == this) {
 
                     if (!this.worldObj.isClientSide) {
                         Player player = entity.world.getClosestPlayer(tilePos.x, tilePos.y, tilePos.z, 12);
@@ -148,14 +148,14 @@ public class TileEntityMesh extends TileEntity {
         double centerZ = tilePos.z + 0.5;
 
         // move the center to the edge of the block face
-        centerX += facing.getOffsetX() * 0.5;
-        centerY += facing.getOffsetY() * 0.5;
-        centerZ += facing.getOffsetZ() * 0.5;
+        centerX += facing.offsetX() * 0.5;
+        centerY += facing.offsetY() * 0.5;
+        centerZ += facing.offsetZ() * 0.5;
 
         // move height distance further in the facing direction
-        double farX = centerX + (facing.getOffsetX() * siphonHeight);
-        double farY = centerY + (facing.getOffsetY() * siphonHeight);
-        double farZ = centerZ + (facing.getOffsetZ() * siphonHeight);
+        double farX = centerX + (facing.offsetX() * siphonHeight);
+        double farY = centerY + (facing.offsetY() * siphonHeight);
+        double farZ = centerZ + (facing.offsetZ() * siphonHeight);
 
 
         // need a half width since in middle of block
@@ -171,15 +171,15 @@ public class TileEntityMesh extends TileEntity {
         double maxZ = Math.max(centerZ, farZ);
 
         // expand based on the axis we are NOT facing
-        if (facing.getAxis() != Axis.X) {
+        if (facing.axis() != Axis.X) {
             minX -= hw;
             maxX += hw;
         }
-        if (facing.getAxis() != Axis.Y) {
+        if (facing.axis() != Axis.Y) {
             minY -= hw;
             maxY += hw;
         }
-        if (facing.getAxis() != Axis.Z) {
+        if (facing.axis() != Axis.Z) {
             minZ -= hw;
             maxZ += hw;
         }
@@ -227,10 +227,10 @@ public class TileEntityMesh extends TileEntity {
 
             //axis check to make sure the swirl behaves correctly depending on facing direction
             double sX = 0, sY = 0, sZ = 0;
-            if (direction.getAxis() == Axis.Y) {
+            if (direction.axis() == Axis.Y) {
                 sX = -diffZ;
                 sZ = diffX;
-            } else if (direction.getAxis() == Axis.Z) {
+            } else if (direction.axis() == Axis.Z) {
                 sX = -diffY;
                 sY = diffX;
             } else {
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java
index 0ad61f978..557c0796f 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java
@@ -105,10 +105,10 @@ public class TileEntityMeshGold extends TileEntityMesh {
             if (entity instanceof EntityItem item && entity.isInWater())
             {
                 if(item.meshSiphoning == null && item.siphonDirection == Direction.NONE){
-                    item.siphonDirection = direction.getOpposite();
+                    item.siphonDirection = direction.opposite();
                     item.meshSiphoning = this;
                 }
-                if(direction.getOpposite().equals(item.siphonDirection) && item.meshSiphoning == this) {
+                if(direction.opposite() == item.siphonDirection && item.meshSiphoning == this) {
                     if (item.item.getItem() == this.filterItem.getItem() && item.item.getMetadata() == this.filterItem.getMetadata()) {
 
                         Player player = entity.world.getClosestPlayer(tilePos.x, tilePos.y, tilePos.z, 12);
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 b22191f02..81624ba86 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
@@ -25,15 +25,15 @@ public class TileEntitySensor extends TileEntity {
         }
 
         int range = 4;
-        int blockInFront = world.getBlockId(MathHelper.round(x + facing.getOffsetX()), MathHelper.round(y + facing.getOffsetY()), MathHelper.round(z + facing.getOffsetZ()));
+        int blockInFront = world.getBlockId(MathHelper.round(x + facing.offsetX()), MathHelper.round(y + facing.offsetY()), MathHelper.round(z + facing.offsetZ()));
         if (Blocks.hasTag(blockInFront, BlockTags.EXTENDS_MOTION_SENSOR_RANGE)) {
             range = 8;
         }
 
         for (int i = 1; i <= range; i++) {
-            int x1 = MathHelper.round(x + facing.getOffsetX() * i);
-            int y1 = MathHelper.round(y + facing.getOffsetY() * i);
-            int z1 = MathHelper.round(z + facing.getOffsetZ() * i);
+            int x1 = MathHelper.round(x + facing.offsetX() * i);
+            int y1 = MathHelper.round(y + facing.offsetY() * i);
+            int z1 = MathHelper.round(z + facing.offsetZ() * i);
             int id = world.getBlockId(x1, y1, z1);
             if (Blocks.solid[id] && !Blocks.hasTag(id, BlockTags.EXTENDS_MOTION_SENSOR_RANGE)) {
                 return i - 1;
@@ -44,13 +44,13 @@ public class TileEntitySensor extends TileEntity {
     }
 
     private @NotNull AABBdc getDetectionBox(double x, double y, double z, @NotNull Direction facing, int range) {
-        double x1 = x + facing.getOffsetX();
-        double y1 = y + facing.getOffsetY();
-        double z1 = z + facing.getOffsetZ();
+        double x1 = x + facing.offsetX();
+        double y1 = y + facing.offsetY();
+        double z1 = z + facing.offsetZ();
 
-        double x2 = x + facing.getOffsetX() * range;
-        double y2 = y + facing.getOffsetY() * range;
-        double z2 = z + facing.getOffsetZ() * range;
+        double x2 = x + facing.offsetX() * range;
+        double y2 = y + facing.offsetY() * range;
+        double z2 = z + facing.offsetZ() * range;
 
         double minX = Math.min(x1, x2);
         double minY = Math.min(y1, y2);
@@ -154,7 +154,9 @@ public class TileEntitySensor extends TileEntity {
             final var block = (active)
                 ? Blocks.MOTION_SENSOR_ACTIVE
                 : Blocks.MOTION_SENSOR_IDLE;
-            world.setBlockTypeDataNotify(this.tilePos, block, meta);
+            world.setBlockTypeData(this.tilePos, block, meta);
+            world.markBlockNeedsUpdate(this.tilePos);
+            world.notifyBlocksInRadiusOfNeighborChange(2, this.tilePos, block);
         }
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTimer.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTimer.java
new file mode 100644
index 000000000..6c5434e25
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTimer.java
@@ -0,0 +1,56 @@
+package net.minecraft.core.block.entity;
+
+import com.mojang.nbt.tags.CompoundTag;
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.BlockLogicTimer;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.util.helper.Axis;
+import net.minecraft.core.util.helper.Side;
+import net.minecraft.core.world.pos.TilePos;
+import org.jetbrains.annotations.NotNull;
+
+public class TileEntityTimer extends TileEntity {
+    // This has got to be one of the simplest tile entities of all time.
+
+    @Override
+    public void readAdditionalData(@NotNull CompoundTag compoundTag) {
+        // Do nothing
+    }
+
+    @Override
+    public void writeAdditionalData(@NotNull CompoundTag compoundTag) {
+        // Do nothing
+    }
+
+    @Override
+    public void tick() {
+        if (this.worldObj == null || this.worldObj.isClientSide) {
+            return;
+        }
+
+        Block<?> block = this.worldObj.getBlockType(this.tilePos);
+        if (block != Blocks.TIMER) {
+            return;
+        }
+
+        final int currentMeta = this.worldObj.getBlockData(this.tilePos);
+        final int currentAxis = currentMeta & BlockLogicTimer.MASK_AXIS;
+        final int currentDelay = currentMeta & BlockLogicTimer.MASK_TICK_DELAY;
+
+        final long currentTick = this.worldObj.getTotalWorldTime();
+        final long interval = BlockLogicTimer.tickDelayMap[currentDelay >> 2];
+        if (currentTick % interval == 0) {
+            final int newOutput =
+                ((currentTick % (interval * 2) == 0) ? BlockLogicTimer.OUTPUT_A : BlockLogicTimer.OUTPUT_B);
+            final int newMeta = currentDelay | newOutput | currentAxis;
+
+            this.worldObj.setBlockData(this.tilePos, newMeta);
+            this.worldObj.markBlockNeedsUpdate(this.tilePos);
+
+            final Side side = BlockLogicTimer.getSideFromMeta(currentMeta);
+
+            this.worldObj.notifyBlocksInCapsuleOfNeighborChange(side, this.tilePos, Blocks.TIMER);
+            this.worldObj.notifyBlocksInCapsuleOfNeighborChange(side.opposite(), this.tilePos, Blocks.TIMER);
+        }
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTrommel.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTrommel.java
index 21f4555e3..691d2cbe5 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTrommel.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityTrommel.java
@@ -312,10 +312,12 @@ public class TileEntityTrommel extends TileEntity
         if(itemResult!=null)
         {
         	// Check if chest is adjacent
-
-        	int adjacentId = this.worldObj != null ? this.worldObj.getBlockId(this.tilePos.x + direction.getOffsetX(), this.tilePos.y + direction.getOffsetY(), this.tilePos.z + direction.getOffsetZ()) : 0;
+            final var queryPos = new TilePos();
+        	int adjacentId = this.worldObj != null 
+                ? this.worldObj.getBlockType(queryPos.set(this.tilePos).add(direction)).id() 
+                : 0;
         	if (this.worldObj != null && Block.hasLogicClass(Blocks.blocksList[adjacentId], BlockLogicChest.class)) {
-                Container chest = BlockLogicChest.getInventory(this.worldObj, new TilePos(this.tilePos.x + direction.getOffsetX(), this.tilePos.y + direction.getOffsetY(), this.tilePos.z + direction.getOffsetZ()));
+                Container chest = BlockLogicChest.getInventory(this.worldObj, queryPos);
 
                 for (int i = 0; i < chest.getContainerSize(); i++)
                 {
@@ -346,12 +348,12 @@ public class TileEntityTrommel extends TileEntity
             if(itemResult.stackSize > 0) {
                 if (this.worldObj != null) {
                     if (this.worldObj.isAirBlock(new TilePos(this.tilePos.x, this.tilePos.y, this.tilePos.z).add(direction))) {
-                        EntityItem droppedItem = new EntityItem(this.worldObj, this.tilePos.x + 0.5 + direction.getOffsetX() * 0.5, this.tilePos.y + 0.5 + direction.getOffsetY() * 0.5, this.tilePos.z + 0.5 + direction.getOffsetZ() * 0.5, itemResult);
+                        EntityItem droppedItem = new EntityItem(this.worldObj, this.tilePos.x + 0.5 + direction.offsetX() * 0.5, this.tilePos.y + 0.5 + direction.offsetY() * 0.5, this.tilePos.z + 0.5 + direction.offsetZ() * 0.5, itemResult);
                         droppedItem.pickupDelay = 10;
-                        droppedItem.fling(direction.getOffsetX() * 0.3125, direction.getOffsetY() * 0.3125, direction.getOffsetZ() * 0.3125, 1);
+                        droppedItem.fling(direction.offsetX() * 0.3125, direction.offsetY() * 0.3125, direction.offsetZ() * 0.3125, 1);
                         this.worldObj.entityJoinedWorld(droppedItem);
                     } else {
-                        this.worldObj.dropItem(this.tilePos.x, this.tilePos.y, this.tilePos.z, itemResult);
+                        this.worldObj.dropItem(this.tilePos, itemResult);
                     }
                 } else if (this.carriedBlock != null) {
                     this.carriedBlock.world.dropItem(MathHelper.floor(this.carriedBlock.holder.x), MathHelper.floor(this.carriedBlock.holder.y), MathHelper.floor(this.carriedBlock.holder.z), itemResult);
@@ -371,8 +373,8 @@ public class TileEntityTrommel extends TileEntity
             mobSlime.zd = (float) this.random.nextGaussian() * f3;
             if (this.worldObj != null) {
                 if (this.worldObj.isAirBlock(new TilePos(this.tilePos.x, this.tilePos.y, this.tilePos.z).add(direction))) {
-                    mobSlime.moveTo(this.tilePos.x + 0.5 + direction.getOffsetX(), this.tilePos.y + 0.5 + direction.getOffsetY(), this.tilePos.z + 0.5 + direction.getOffsetZ(), this.random.nextFloat() * 360F, 0.0F);
-                    mobSlime.fling(direction.getOffsetX() * 0.75, direction.getOffsetY() * 0.75, direction.getOffsetZ() * 0.75, 1);
+                    mobSlime.moveTo(this.tilePos.x + 0.5 + direction.offsetX(), this.tilePos.y + 0.5 + direction.offsetY(), this.tilePos.z + 0.5 + direction.offsetZ(), this.random.nextFloat() * 360F, 0.0F);
+                    mobSlime.fling(direction.offsetX() * 0.75, direction.offsetY() * 0.75, direction.offsetZ() * 0.75, 1);
                 } else {
                     mobSlime.moveTo(this.tilePos.x + (double)f, this.tilePos.y + 1D, this.tilePos.z + (double)f1, this.random.nextFloat() * 360F, 0.0F);
                 }
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 88391617f..f7a2ded33 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
@@ -60,9 +60,9 @@ public class CarriedBlock implements ICarriable, ICarriedBlock {
         if (entity != null) {
             return entity.tryPlace(world, holder, blockX, blockY, blockZ, side, xPlaced, yPlaced);
         } else {
-            int x = blockX + side.getOffsetX();
-            int y = blockY + side.getOffsetY();
-            int z = blockZ + side.getOffsetZ();
+            int x = blockX + side.offsetX();
+            int y = blockY + side.offsetY();
+            int z = blockZ + side.offsetZ();
 
             Block<?> currentBlock = world.getBlock(x, y, z);
             if (currentBlock != null && !currentBlock.hasTag(BlockTags.PLACE_OVERWRITES)) return false;
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 88cecea89..d066e7073 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
@@ -3,388 +3,573 @@ package net.minecraft.core.block.piston;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.Global;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.BlockLogic;
 import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.block.piston.PistonCommon.MovingType;
 import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.block.support.PartialSupport;
 import net.minecraft.core.block.tag.BlockTags;
-import net.minecraft.core.entity.Mob;
+import net.minecraft.core.entity.EntityFallingBlock;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.sound.SoundCategory;
+import net.minecraft.core.util.PackedField;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.Side;
+import net.minecraft.core.world.IVehicle;
 import net.minecraft.core.world.LevelListener;
 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;
 import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3d;
 import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
 
+import static net.minecraft.core.block.piston.PistonCommon.*;
+
 public class BlockLogicPistonBase extends BlockLogic {
-    public static final int MASK_DIRECTION = 0b0000_0111;
-    public static final int MASK_POWERED = 0b0000_1000;
-
-    public static final int EVENT_EXTEND = 0;
-    public static final int EVENT_RETRACT = 1;
-
-    public static final ISupport[] pistonSupportLookupTable = {
-        // Bottom
-        PartialSupport.INSTANCE,
-        FullSupport.INSTANCE,
-        PartialSupport.INSTANCE.center().up(),
-        PartialSupport.INSTANCE.center().down(),
-        PartialSupport.INSTANCE.center().right(),
-        PartialSupport.INSTANCE.center().left(),
-        // Top
-        FullSupport.INSTANCE,
-        PartialSupport.INSTANCE,
-        PartialSupport.INSTANCE.center().down(),
-        PartialSupport.INSTANCE.center().up(),
-        PartialSupport.INSTANCE.center().right(),
-        PartialSupport.INSTANCE.center().left(),
-        // North
-        PartialSupport.INSTANCE.center().up(),
-        PartialSupport.INSTANCE.center().down(),
-        PartialSupport.INSTANCE,
-        FullSupport.INSTANCE,
-        PartialSupport.INSTANCE.center().right(),
-        PartialSupport.INSTANCE.center().left(),
-        // South
-        PartialSupport.INSTANCE.center().up(),
-        PartialSupport.INSTANCE.center().down(),
-        FullSupport.INSTANCE,
-        PartialSupport.INSTANCE,
-        PartialSupport.INSTANCE.center().right(),
-        PartialSupport.INSTANCE.center().left(),
-        // West
-        PartialSupport.INSTANCE.center().up(),
-        PartialSupport.INSTANCE.center().down(),
-        PartialSupport.INSTANCE.center().left(),
-        PartialSupport.INSTANCE.center().right(),
-        PartialSupport.INSTANCE,
-        FullSupport.INSTANCE,
-        // East
-        PartialSupport.INSTANCE.center().up(),
-        PartialSupport.INSTANCE.center().down(),
-        PartialSupport.INSTANCE.center().left(),
-        PartialSupport.INSTANCE.center().right(),
-        FullSupport.INSTANCE,
-        PartialSupport.INSTANCE,
-    };
-
-    private final int maxPushedBlocks;
-
-    public BlockLogicPistonBase(@NotNull Block<?> block, int maxPushedBlocks) {
+	public final byte typeId;
+
+    final byte pushLimit, crushLimit;
+    final @Nullable EnumDropCause crushAction;
+    final boolean canPull;
+    final double flingStrength;
+
+    public BlockLogicPistonBase(
+        @NotNull Block<? extends BlockLogicPistonBase> block, 
+        byte typeId,
+        byte pushLimit, byte crushLimit,
+        @Nullable EnumDropCause crushAction, boolean canPull, double flingStrength
+    ) {
         super(block, Materials.PISTON);
-        this.maxPushedBlocks = maxPushedBlocks;
         block.withHardness(0.5F);
+		this.typeId = typeId;
+
+        assert typeId >= 0 && typeId < 3;
+
+        this.pushLimit = pushLimit;
+        this.crushLimit = crushLimit;
+        this.crushAction = crushAction;
+        this.canPull = canPull;
+        this.flingStrength = flingStrength;
     }
 
-    @Override
-    public boolean isSolidRender() {
-        return false;
+    // Piston updates:
+    // =================
+
+    private final boolean signalUpdate(@NotNull World world, @NotNull TilePosc tilePos) {
+        return signalUpdate(world, tilePos, world.getBlockData(tilePos));
     }
 
-    @Override
-    public void onPlacedByMob(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, @NotNull Mob mob, double xHit, double yHit) {
-        Direction placementDirection = mob.getPlacementDirection(side).getOpposite();
-        world.setBlockDataNotify(tilePos, placementDirection.getId());
-        if (!world.isClientSide) {
-            checkIfExtend(world, tilePos);
-        }
+    private final boolean signalUpdate(@NotNull World world, @NotNull TilePosc tilePos, int data) {
+        data = validateBaseData(data, this);
+        
+        final boolean powered = hasNeighborSignal(world, tilePos, Direction.fromId(DIRECTION.get(data)));
+        final boolean extended = IS_EXTENDED.bool(data);
+        final boolean detached = IS_DETACHED.bool(data);
+        
+        if (powered == extended) return false;
+
+        int event = (!detached) ? EVENT_EXTEND : EVENT_DETACH_EXTEND;
+        event |= (powered) ? 0 : 1;
+
+        final var lineInfo = (powered && !detached)
+            ? testLine(world, tilePos, Direction.fromId(DIRECTION.get(data)))
+            : 0;
+        assert (lineInfo & ~0xFF) == 0;
+
+        if (LINE_OBSTRUCTED.bool(lineInfo)) return false;
+        
+        world.triggerEvent(tilePos, event, lineInfo);
+        return true;
     }
 
     @Override
-    public void onPlacedOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
-        Direction placementDirection = side.getDirection();
-        world.setBlockDataNotify(tilePos, placementDirection.getId());
-        if (!world.isClientSide) {
-            checkIfExtend(world, tilePos);
+    public void triggerEvent(@NotNull World world, @NotNull TilePosc tilePos, int index, int eventData) {
+        assert (eventData & ~0xFF) == 0;
+        final double x, y, z;
+        x = tilePos.x() + 0.5D;
+        y = tilePos.y() + 0.5D;
+        z = tilePos.z() + 0.5D;
+
+        final int data = getValidatedBaseData(world, tilePos, this);
+        final boolean isOut = (index & 1) == 0;
+
+        switch (index & ~1) {
+        default:
+            return;
+        case EVENT_DETACH:
+            break;
+        case EVENT_DETACH_EXTEND:
+            world.setBlockData(tilePos, IS_EXTENDED.set(data, isOut ? 1 : 0));
+            break;
+        case EVENT_EXTEND:
+            if (isOut) {
+                if (!this.extend(world, tilePos, data, eventData)) return;
+            } else {
+                if (world.isClientSide) {
+                    // serverside / singleplayer should have already fixed this 
+                    fixLegacyBaseData(world, tilePos);
+                }
+                this.retract(world, tilePos, data);
+            }
+            break;
         }
-    }
 
-    @Override
-    public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        checkIfExtend(world, tilePos);
-    }
+        // we let client side compute effects
+        if (Global.isServer) return;
 
-    @Override
-    public void onPlacedByWorld(@NotNull World world, @NotNull TilePosc tilePos) {
-        if (!world.isClientSide && world.getTileEntity(tilePos) == null) {
-            checkIfExtend(world, tilePos);
+        final @NotNull String sound;
+        final float volumn, pitch;
+
+        switch (index & ~1) {
+        default: 
+            assert false;
+            return;
+        case EVENT_DETACH:
+            world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_PARTICLES, DIRECTION.get(data));
+            sound = (isOut) ? "tile.piston.detached" : "tile.piston.attached";
+            volumn = 1.0F;
+            pitch  = 0.6F;
+            break;
+        case EVENT_DETACH_EXTEND:
+            world.playBlockEvent(tilePos, LevelListener.EVENT_DISPENSER_PARTICLES, DIRECTION.get(data));
+            sound = (isOut) ? "tile.piston.detached.out" : "tile.piston.detached.in";
+            volumn = pitch = 1.0F;
+            break;
+        case EVENT_EXTEND:
+            sound = (isOut) ? "tile.piston.out" : "tile.piston.in";
+            volumn = 0.5F;
+            pitch = world.rand.nextFloat() * 0.25F + 0.6F;
+            break;
         }
+
+        world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, x, y, z, sound, volumn, pitch);
     }
+    
+    private static boolean hasNeighborSignal(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction dir) {
+        final var queryPos = new TilePos();
+        for (final var search : Direction.all) {
+            queryPos.set(tilePos).add(search);
+            if (search != dir && world.hasSignal(queryPos, search.side())) return true;
 
-    private void checkIfExtend(@NotNull World world, @NotNull TilePosc tilePos) {
-        int data = world.getBlockData(tilePos);
-        Direction direction = getDirection(data);
-        boolean hasNeighborSignal = getNeighborSignal(world, tilePos, direction);
-        if (data == MASK_DIRECTION) {
-            return;
+            // QC
+            if (search == Direction.DOWN) continue;
+            if (dir == Direction.UP && search == Direction.UP) continue;
+            queryPos.up();
+            if (world.hasSignal(queryPos, search.side())) return true;
         }
-        if (hasNeighborSignal && !isPowered(data)) {
-            if (canPushLine(world, tilePos, direction, this.maxPushedBlocks)) {
-                world.setBlockData(tilePos, direction.getId() | MASK_POWERED);
-                world.triggerEvent(tilePos, EVENT_EXTEND, direction.getId());
+        return false;
+    }
+
+    // Piston tests:
+    // =================
+
+    // only uses 8 bits since only 8 bits of PacketBlockEvent's data field
+    private static final PackedField LINE_LENGTH     = new PackedField(0, 6);
+    private static final PackedField CRUSH_EOL       = new PackedField(6, 1);
+    private static final PackedField LINE_OBSTRUCTED = new PackedField(7, 1);
+
+    /**
+     * Returns line length: <br>
+     * {@code LINE_LENGTH.get(n)} encodes length of line; <br>
+     * if {@code LINE_OBSTRUCTED.bool(n)}, line can't be pushed; <br>
+     * if {@code CRUSH_EOL.bool(n)}, end of line should be crushed; <br>
+     */
+    public int testLine(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction dir) {
+        final var pos = new TilePos(tilePos).add(dir);
+        final int maxY = world.getHeightBlocks();
+        final int pushLimit = LINE_LENGTH.get(this.pushLimit);
+
+        if (pushLimit == 0) return 0;
+        for (int i = 0; i <= pushLimit; i++, pos.add(dir)) {
+            final int nope = LINE_OBSTRUCTED.mask | i;
+
+            if (pos.y < 0 || pos.y >= maxY) return nope;
+
+            switch (Movability.test(world, pos)) {
+            case BREAKS: 
+                return i;
+            case MOVES:
+                if (i >= pushLimit) return nope;
+                continue;
+            case CRUSHES:
+                if (this.crushAction == null) return nope; 
+                if (i == 0) return nope;
+                if (i > this.crushLimit) return nope;
+                return CRUSH_EOL.mask | i - 1;
+            case OBSTRUCTED:
+                return nope;
             }
-        } else if (!hasNeighborSignal && isPowered(data)) {
-            world.setBlockData(tilePos, direction.getId());
-            world.triggerEvent(tilePos, EVENT_RETRACT, direction.getId());
+            assert false : "unreachable";
         }
+        assert false : "unreachable"; return 0;
     }
 
-    private boolean getNeighborSignal(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction) {
-        TilePos queryPos = new TilePos();
-        if (direction != Direction.DOWN && world.hasSignal(tilePos.add(0, -1, 0, queryPos), Side.BOTTOM)) {
-            return true;
-        }
-        if (direction != Direction.UP && world.hasSignal(tilePos.add(0, 1, 0, queryPos), Side.TOP)) {
-            return true;
-        }
-        if (direction != Direction.NORTH && world.hasSignal(tilePos.add(0, 0, -1, queryPos), Side.NORTH)) {
-            return true;
-        }
-        if (direction != Direction.SOUTH && world.hasSignal(tilePos.add(0, 0, 1, queryPos), Side.SOUTH)) {
-            return true;
-        }
-        if (direction != Direction.EAST && world.hasSignal(tilePos.add(1, 0, 0, queryPos), Side.EAST)) {
-            return true;
-        }
-        if (direction != Direction.WEST && world.hasSignal(tilePos.add(-1, 0, 0, queryPos), Side.WEST)) {
-            return true;
-        }
-        if (world.hasSignal(tilePos, Side.BOTTOM)) {
-            return true;
-        }
-        // QC
-        if (direction != Direction.UP && world.hasSignal(tilePos.add(0, 2, 0, queryPos), Side.TOP)) {
-            return true;
+    public static enum Movability {
+        OBSTRUCTED, MOVES, BREAKS, CRUSHES;
+
+        public static @NotNull Movability test(@NotNull World world, @NotNull TilePosc tilePos) {
+            final var block = world.getBlockType(tilePos);
+            if (block == Blocks.AIR) return BREAKS;
+            if (block.hasTag(BlockTags.PISTON_CRUSHING)) return CRUSHES;
+            switch (block.getPistonPushReaction(world, tilePos)) {
+                case Material.PISTON_DESTROY_ON_PUSH: return BREAKS;
+                case Material.PISTON_CANT_PUSH: return OBSTRUCTED;
+            }
+            if (block.getImmovable() || block.getHardness() == -1F) return OBSTRUCTED;
+            return MOVES;
         }
-        if (world.hasSignal(tilePos.add(0, 1, -1, queryPos), Side.NORTH)) {
-            return true;
+    }
+
+    // Piston Events:
+    // ==================
+    //  ├─ extend 
+    //  │  ├─ push
+    //  │  ├─ destroy (breakTail)
+    //  │  ├─ ?crush (breakTail)
+    //  │  ├─ ?flingEntity
+    //  │  └─ ?flingBlock
+    //  └─ retract
+    //     └─ ?pull
+    // 
+
+    private boolean extend(@NotNull World world, @NotNull TilePosc tilePos, int data, int lineInfo) {
+        data = validateBaseData(data, this);
+        final var dir = Direction.fromId(DIRECTION.get(data));
+        final var headPos = new TilePos(tilePos).add(dir);
+
+        world.setBlockData(tilePos, IS_EXTENDED.set(data, 1));
+        world.markBlockNeedsUpdate(tilePos);
+
+        final boolean canFling = !Double.isNaN(this.flingStrength);
+        if (canFling) flingEntity(world, headPos, dir);
+
+        final int len = LINE_LENGTH.get(lineInfo);
+        final boolean shouldCrush = CRUSH_EOL.bool(lineInfo);
+        final boolean shouldFling = !shouldCrush && canFling && len == 1;
+        if (shouldFling) {
+            flingBlock(world, headPos, dir);
+        } else {
+            final var tailAction = (shouldCrush) ? this.crushAction : EnumDropCause.WORLD;
+            breakTail(tailAction, world, headPos, dir, len);
+            push(world, headPos, dir, len);
         }
-        if (world.hasSignal(tilePos.add(0, 1, 1, queryPos), Side.SOUTH)) {
-            return true;
+
+        final var headData = DIRECTION.set(0, dir.id) | TYPE.getNoShift(data);
+        final var headMoving = new TileEntityMovingPistonBlock(
+            MovingType.EXTENDING_PISTON,
+            HEAD_BLOCK_MAP[TYPE.get(data)], headData, null, dir);
+
+        world.setBlockTypeDataEntity(headPos, Blocks.PISTON_MOVING, headData, headMoving);
+
+        // extension updates base immediately, and head 3 ticks after 
+        world.notifyBlocksOfNeighborChange(tilePos, this.block);
+
+        return true;
+    }
+
+    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);
+            }
+
+            world.removeTileEntity(src);
+            final var moving = new TileEntityMovingPistonBlock(
+                MovingType.EXTENDING,
+                block, data, entity, dir);
+            world.setBlockTypeDataEntity(tar, Blocks.PISTON_MOVING, data, moving);
+
+            if (i >= 1) world.setBlockTypeRaw(src, Blocks.AIR);
         }
-        if (world.hasSignal(tilePos.add(-1, 1, 0, queryPos), Side.WEST)) {
-            return true;
+    }
+
+    private void breakTail(
+        @NotNull EnumDropCause cause, @NotNull World world, @NotNull TilePosc headPos,
+        @NotNull Direction dir, int off
+    ) {
+        final var target = dir.offsetScaled(new TilePos(headPos), off);
+
+        final var block = world.getBlockType(target);
+        if (block.id() > 0) {
+            world.playBlockEvent(null, target, LevelListener.EVENT_BLOCK_BREAK, block.id());
         }
-        return world.hasSignal(tilePos.add(1, 1, 0, queryPos), Side.EAST);
+
+        block.dropWithCause(world, cause, target, world.getBlockData(target), world.getTileEntity(target), null);
+        world.setBlockType(target, Blocks.AIR);
     }
 
-    @Override
-    public void triggerEvent(@NotNull World world, @NotNull TilePosc tilePos, int index, int data) {
-        Direction direction = getDirection(data);
-        if (direction == Direction.NONE) return;
-        if (index == EVENT_EXTEND) {
-            extendEvent(world, tilePos, data, direction);
-        } else if (index == EVENT_RETRACT) {
-            retractEvent(world, tilePos, data, direction);
+    private void flingEntity(@NotNull World world, @NotNull TilePosc headPos, @NotNull Direction dir) {
+        if (world.isClientSide) return;
+        final double vx = dir.offsetX() * this.flingStrength;
+        final double vy = dir.offsetY() * this.flingStrength;
+        final double vz = dir.offsetZ() * this.flingStrength;
+        final var entities = world.getEntitiesWithinAABBExcludingEntity(null,
+            new AABBd(new Vector3d(headPos.vec()), new Vector3d(headPos.vec()).add(1, 1, 1)));
+        // note that flingEntity is performed before flingBlock, therefore no need to 
+        // check for flingBlock
+        for (final var entity : entities) {
+            entity.fling(vx, vy, vz, 1f);
         }
     }
 
-    public void extendEvent(@NotNull World world, @NotNull TilePosc tilePos, int data, @NotNull Direction direction) {
-        if (tryExtend(world, tilePos, direction, this.maxPushedBlocks)) {
-            world.setBlockDataNotify(tilePos, direction.getId() | MASK_POWERED);
-            world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) tilePos.x() + 0.5D, (double) tilePos.y() + 0.5D, (double) tilePos.z() + 0.5D, "tile.piston.out", 0.5F, world.rand.nextFloat() * 0.25F + 0.6F);
+    private void flingBlock(@NotNull World world, @NotNull TilePosc headPos, @NotNull Direction dir) {
+        final double vx = dir.offsetX() * this.flingStrength;
+        final double vy = dir.offsetY() * this.flingStrength;
+        final double vz = dir.offsetZ() * this.flingStrength;
+
+        final var block = world.getBlockType(headPos);
+        if (block == Blocks.AIR) return;
+
+        final int blockMeta = world.getBlockData(headPos);
+        final var tileEntity = world.getTileEntity(headPos);
+        world.removeTileEntity(headPos);
+        world.setBlockTypeNotify(headPos, Blocks.AIR);
+        
+        if (world.isClientSide) return;
+        
+        var flungBlock = new EntityFallingBlock(world, 
+            headPos.x() + 0.5, headPos.y() + 0.5, headPos.z() + 0.5,
+            block.id(), blockMeta, tileEntity);
+        flungBlock.hasRemovedBlock = true;
+
+        if (tileEntity instanceof IVehicle vehicle) {
+            var rider = vehicle.ejectRider();
+            if (rider != null) rider.startRiding(flungBlock); // TODO: ride packet after entity packet
         }
+
+        world.entityJoinedWorld(flungBlock);
+        flungBlock.fling(vx, vy, vz, 1f);
     }
 
-    public void retractEvent(@NotNull World world, @NotNull TilePosc tilePos, int data, @NotNull Direction direction) {
-        TilePos shiftedPos = tilePos.add(direction, new TilePos());
-        TileEntity tileEntity = world.getTileEntity(shiftedPos);
-        if ((tileEntity instanceof TileEntityMovingPistonBlock moving)) {
-            if (!moving.isExtending()) {
+    private void retract(@NotNull World world, @NotNull TilePosc tilePos, int data) {
+        data = validateBaseData(data, this);
+        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(shiftedPos, Blocks.AIR);
+                world.setBlockType(headPos, Blocks.AIR);
             }
         }
-        world.setBlockTypeData(tilePos, Blocks.PISTON_MOVING, direction.getId());
-        world.replaceTileEntity(tilePos, BlockLogicPistonMoving.createTileEntity(this.block, direction.getId(), null, direction, false, true));
-        world.setBlockTypeNotify(shiftedPos, Blocks.AIR);
-        world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) tilePos.x() + 0.5D, (double) tilePos.y() + 0.5D, (double) tilePos.z() + 0.5D, "tile.piston.in", 0.5F, world.rand.nextFloat() * 0.15F + 0.6F);
-    }
 
-    @Override
-    public @NotNull AABBdc getBoundsFromState(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
-        int data = source.getBlockData(tilePos);
-        if (isPowered(data)) {
-            return switch (getDirection(data)) {
-                case DOWN -> new AABBd(0.0F, 0.25F, 0.0F, 1.0F, 1.0F, 1.0F);
-                case UP -> new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 0.75F, 1.0F);
-                case NORTH -> new AABBd(0.0F, 0.0F, 0.25F, 1.0F, 1.0F, 1.0F);
-                case SOUTH -> new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 0.75F);
-                case WEST -> new AABBd(0.25F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
-                default -> new AABBd(0.0F, 0.0F, 0.0F, 0.75F, 1.0F, 1.0F);
-            };
+        final var moving = new TileEntityMovingPistonBlock(
+            MovingType.RETRACTING_PISTON,
+            this.block, dir.id, null, dir);
+        world.setBlockTypeDataEntity(tilePos, Blocks.PISTON_MOVING, data, moving);
+
+        if (this.canPull) {
+            pull(world, headPos, dir);
         } else {
-            return new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
+            world.setBlockType(headPos, Blocks.AIR);
+            world.removeTileEntity(headPos);
         }
+
+        // logically, retraction should only update head immediately, and base 3 ticks after
+        // but this emulates the old behavior
+        world.notifyBlocksOfNeighborChange(tilePos, this.block);
+        world.notifyBlocksOfNeighborChange(headPos, Blocks.AIR);
+    }
+
+    private static void pull(@NotNull World world, @NotNull TilePosc headPos, @NotNull Direction dir) {
+        final var tar = headPos;
+        final var src = new TilePos(tar).add(dir);
+
+        final var entity = world.getTileEntity(src);
+
+        if (entity instanceof TileEntityMovingPistonBlock moving) position_moving: {
+            if (moving.direction != dir) break position_moving;
+            if (!moving.isExtending) break position_moving;
+            assert world.getBlockType(src) == Blocks.PISTON_MOVING;
+
+            // 0t
+            moving.finalTick();
+            return;
+        }
+
+        if (Movability.test(world, src) != Movability.MOVES) {
+            world.removeTileEntity(tar);
+            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);
     }
 
+    // BlockLogic impl:
+    // ====================
+
     @Override
-    public boolean isCubeShaped() {
+    public boolean isSolidRender() {
         return false;
     }
-
+    
     @Override
-    public @NotNull ISupport getSupport(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
-        int meta = world.getBlockData(tilePos);
-        if ((meta & MASK_POWERED) == 0) return FullSupport.INSTANCE;
-        Direction dir = getDirection(meta);
-        return pistonSupportLookupTable[side.getId() * 6 + dir.getId()];
+    public int getPlacedData(
+        @Nullable Player player, @NotNull ItemStack itemStack, @NotNull World world,
+        @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit
+    ) {
+        int meta = 0;
+        meta |= TYPE.set(meta, this.typeId);
+        meta |= NEW_FORMAT_SIGNATURE.mask;
+
+        meta |= IS_DETACHED_OR_EXTENDED.setNoShift(meta, itemStack.getMetadata());
+        final var dir = (player != null)
+            ? player.getPlacementDirection(side).opposite().id
+            : side.id;
+        meta |= DIRECTION.set(meta, dir);
+
+        return meta;
     }
 
-    public static Direction getDirection(int data) {
-        return Direction.getDirectionById(data & MASK_DIRECTION);
+    @Override
+    public void onPlacedByWorld(@NotNull World world, @NotNull TilePosc tilePos) {
+        if (world.isClientSide) return;
+        final var d = NEW_FORMAT_SIGNATURE.set(world.getBlockData(tilePos), 1);
+        world.setBlockData(tilePos, d);
+        this.signalUpdate(world, tilePos);
     }
 
-    public static boolean isPowered(int data) {
-        return (data & MASK_POWERED) != 0;
+    @Override
+    public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
+        if (world.isClientSide) return;
+        final var data = fixLegacyBaseData(world, tilePos);
+        this.signalUpdate(world, tilePos, data);
     }
 
-    protected boolean isPushable(final @NotNull Block<?> block, @NotNull World world, @NotNull TilePos tilePos, boolean canDestroy) {
-        int pushReaction = block.getPistonPushReaction(world, tilePos);
-        return !block.hasTag(BlockTags.PISTON_CRUSHING) &&
-            !block.getImmovable() &&
-            pushReaction != Material.PISTON_CANT_PUSH &&
-            (canDestroy || pushReaction != Material.PISTON_DESTROY_ON_PUSH) &&
-            block.getHardness() != -1F;
+    @Override
+    public @NotNull AABBdc getBoundsFromState(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
+        int data = getValidatedBaseData(source, tilePos, this);
+        if (!IS_DETACHED_OR_EXTENDED.bool(data)) return new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
+
+        final var dir = Direction.fromId(DIRECTION.get(data));
+        final var thickness = HEAD_BLOCK_MAP[this.typeId].getLogic().headThickness;
+
+        final var min = new Vector3d(0);
+        final var max = new Vector3d(1);
+
+        dir.offsetScaled(dir.isPositive() ? max : min, (dir.isNegative()) ? thickness : -thickness);
+        return new AABBd(min, max);
     }
 
     @Override
-    public int getPistonPushReaction(@NotNull World world, @NotNull TilePosc tilePos) {
-        if (isPowered(world.getBlockData(tilePos))) {
-            return Material.PISTON_CANT_PUSH;
-        }
-        return super.getPistonPushReaction(world, tilePos);
+    public boolean isCubeShaped() {
+        return false;
     }
 
-    protected boolean canPushLine(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction, int maxPushedBlocks) {
-        TilePos posO = tilePos.add(direction, new TilePos());
-        int blocks = 0;
-        while (true) {
-            if (blocks >= maxPushedBlocks + 1) {
-                break;
-            }
-            if (posO.y < 0 || posO.y >= world.getHeightBlocks()) {
-                return false;
-            }
-            final @NotNull Block<?> block = world.getBlockType(posO);
-            if (block == Blocks.AIR) {
-                break;
-            }
-            if (!isPushable(block, world, posO, true)) {
-                if (blocks == 1 && block.hasTag(BlockTags.PISTON_CRUSHING) && tryCrush(world, tilePos, direction)) {
-                    break;
-                }
-                return false;
-            }
-            if (block.getPistonPushReaction(world, posO) == Material.PISTON_DESTROY_ON_PUSH) {
-                break;
-            }
-            if (blocks == maxPushedBlocks) {
-                return false;
-            }
-            posO.x += direction.getOffsetX();
-            posO.y += direction.getOffsetY();
-            posO.z += direction.getOffsetZ();
-            blocks++;
-        }
-        return true;
+    private static final ISupport[] SUPPORT_LUT;
+    static {
+        final var F = FullSupport.INSTANCE;
+        final var P = PartialSupport.INSTANCE;
+        final PartialSupport U,D,L,R;
+        U = P.center().up();
+        D = P.center().down();
+        L = P.center().left();
+        R = P.center().right();
+        
+        SUPPORT_LUT = new ISupport[] {
+            P,F, U,D, R,L, // Bottom
+            F,P, D,U, R,L, // Top
+            U,D, P,F, R,L, // North
+            U,D, F,P, R,L, // South
+            U,D, L,R, P,F, // West
+            U,D, L,R, F,P, // East
+        };
     }
 
-    public boolean tryCrush(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction) {
-        TilePos pos2 = tilePos.add(direction, new TilePos());
-        Block<?> block = world.getBlockType(pos2);
-        world.playBlockEvent(null, pos2, LevelListener.EVENT_BLOCK_BREAK, world.getBlockType(pos2).id());
-        block.dropWithCause(world, EnumDropCause.SILK_TOUCH, pos2, world.getBlockData(pos2), null, null);
-        world.setBlockTypeNotify(pos2, Blocks.AIR);
-        return true;
+    @Override
+    public @NotNull ISupport getSupport(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
+        final int data = getValidatedBaseData(world, tilePos, this);
+        final int dir = Direction.fromId(DIRECTION.get(data)).id; // ensures < 6
+
+        return (!IS_DETACHED_OR_EXTENDED.bool(data))
+            ? FullSupport.INSTANCE
+            : SUPPORT_LUT[side.id * 6 + dir];
     }
 
-    protected boolean tryExtend(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction, int maxPushedBlocks) {
-        int data = world.getBlockData(tilePos);
-        TilePos posO = tilePos.add(direction, new TilePos());
-        int blocks = 0;
-        while (true) {
-            if (blocks >= maxPushedBlocks + 1) {
-                break;
-            }
-            if (posO.y < 0 || posO.y >= world.getHeightBlocks()) {
-                return false;
-            }
-            final @NotNull Block<?> block = world.getBlockType(posO);
-            if (block == Blocks.AIR) {
-                break;
-            }
-            if (!isPushable(block, world, posO, true)) {
-                return false;
-            }
-            if (block.getPistonPushReaction(world, posO) == Material.PISTON_DESTROY_ON_PUSH) {
-                block.dropWithCause(world, EnumDropCause.WORLD, posO, world.getBlockData(posO), world.getTileEntity(posO), null);
-                break;
-            }
-            if (blocks == maxPushedBlocks) {
-                return false;
-            }
-            posO.x += direction.getOffsetX();
-            posO.y += direction.getOffsetY();
-            posO.z += direction.getOffsetZ();
-            blocks++;
-        }
-        TilePos posP = new TilePos(posO);
-        while (!posO.equals(tilePos)) {
-            posP.set(posO.x - direction.getOffsetX(), posO.y - direction.getOffsetY(), posO.z - direction.getOffsetZ());
-            @NotNull Block<?> pushBlock = world.getBlockType(posP);
-            int pushMeta = world.getBlockData(posP);
-            TileEntity pushEntity = world.getTileEntity(posP);
-            if (pushEntity instanceof TileEntityMovingPistonBlock && posP.equals(tilePos)) {
-                break;
-            }
-            if (pushBlock == this.block && posP.equals(tilePos)) {
-                createPistonHeadAt(world, posO, data, direction);
-            } else if (pushBlock == Blocks.PISTON_MOVING) {
-                TileEntity old = world.getTileEntity(posP);
-                if (old instanceof TileEntityMovingPistonBlock moving) {
-                    if (!moving.isSourcePiston()) {
-                        pushBlock = moving.getMovedBlock();
-                        pushMeta = moving.getMovedData();
-                        pushEntity = moving.getMovedEntity();
-                    }
-                }
-                world.removeTileEntity(posP);
-                world.setBlockTypeData(posO, Blocks.PISTON_MOVING, pushMeta);
-                world.replaceTileEntity(posO, BlockLogicPistonMoving.createTileEntity(pushBlock, pushMeta, pushEntity, direction, true, false));
-            } else {
-                world.removeTileEntity(posP);
-                world.setBlockTypeData(posO, Blocks.PISTON_MOVING, pushMeta);
-                world.replaceTileEntity(posO, BlockLogicPistonMoving.createTileEntity(pushBlock, pushMeta, pushEntity, direction, true, false));
-            }
-            if (!posP.equals(tilePos)) {
-                world.setBlockTypeRaw(posP, Blocks.AIR);
-            }
-            posO.set(posP.x, posP.y, posP.z);
-        }
+    @Override
+    public int getPistonPushReaction(@NotNull World world, @NotNull TilePosc tilePos) {
+        final var data = fixLegacyBaseData(world, tilePos);
 
-        return true;
+        return (IS_DETACHED_OR_EXTENDED.bool(data))
+            ? Material.PISTON_CANT_PUSH
+            : super.getPistonPushReaction(world, tilePos);
     }
 
-    public void createPistonHeadAt(@NotNull World world, @NotNull TilePos tilePos, int data, @NotNull Direction direction) {
-        world.setBlockTypeData(tilePos, Blocks.PISTON_MOVING, BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_NORMAL, direction.getId()));
-        world.replaceTileEntity(tilePos, BlockLogicPistonMoving.createTileEntity(Blocks.PISTON_HEAD, BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_NORMAL, direction.getId()), null, direction, true, false));
+    @Override
+    public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
+        data = validateBaseData(data, this);
+        if (IS_DETACHED.bool(data)) return;
+
+        final var dir = Direction.fromId(DIRECTION.get(data));
+        final var hpos = new TilePos(tilePos).add(dir);
+        final var head = world.getBlockType(hpos).getLogic();
+
+        if (head instanceof BlockLogicPistonHead) {
+            final int hdat = getValidatedHeadData(world, hpos);
+            if (hdat == DATA_INVALID) return;
+            if (PAIR_COMPARISON.get(data ^ hdat) != 0) return;
+            world.setBlockTypeNotify(hpos, Blocks.AIR);
+        }
+        else if (head instanceof BlockLogicPistonMoving) {
+            if (!(world.getTileEntity(hpos) instanceof TileEntityMovingPistonBlock moving)) return;
+            if (!moving.isSourcePiston) return;
+            if (moving.direction != dir) return;
+            world.removeTileEntity(hpos);
+            world.setBlockTypeNotify(hpos, Blocks.AIR);
+        }
     }
 
     @Override
-    public int getPlacedData(@Nullable Player player, @NotNull ItemStack itemStack, @NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, double xHit, double yHit) {
-        return MASK_DIRECTION;
+    public @NotNull ItemStack @Nullable [] getBreakResult(
+        @NotNull World world, @NotNull EnumDropCause dropCause,
+        int data, @Nullable TileEntity tileEntity
+    ) {
+        class ctx {
+            static final ItemStack[] buf = new ItemStack[1];
+        }
+
+        data = validateBaseData(data, this);
+        data = DIRECTION.set(data, 0) & IS_EXTENDED.set(data, 0);
+
+        ctx.buf[0] = new ItemStack(this.block, 1, data);
+        return ctx.buf;
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBaseSteel.java b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBaseSteel.java
deleted file mode 100644
index 6a9281cf1..000000000
--- a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBaseSteel.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package net.minecraft.core.block.piston;
-
-import net.minecraft.core.block.Blocks;
-import net.minecraft.core.block.entity.TileEntity;
-import net.minecraft.core.block.material.Material;
-import net.minecraft.core.block.Block;
-import net.minecraft.core.block.tag.BlockTags;
-import net.minecraft.core.entity.Entity;
-import net.minecraft.core.entity.EntityFallingBlock;
-import net.minecraft.core.entity.player.Player;
-import net.minecraft.core.enums.EnumDropCause;
-import net.minecraft.core.util.helper.Direction;
-import net.minecraft.core.world.IVehicle;
-import net.minecraft.core.world.LevelListener;
-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;
-import org.joml.primitives.AABBd;
-import org.joml.primitives.AABBdc;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class BlockLogicPistonBaseSteel extends BlockLogicPistonBase {
-    public static final double HEAD_THICKNESS_STEEL = 6 / 16d;
-
-    private Entity flungBlock = null;
-
-    public BlockLogicPistonBaseSteel(@NotNull Block<?> container, int maxPushedBlocks) {
-        super(container, maxPushedBlocks);
-    }
-
-    @Override
-    public @NotNull AABBdc getBoundsFromState(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
-        int data = source.getBlockData(tilePos);
-        if (isPowered(data)) {
-            return switch (getDirection(data)) {
-                case DOWN -> new AABBd(0.0F, HEAD_THICKNESS_STEEL, 0.0F, 1.0F, 1.0F, 1.0F);
-                case UP -> new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1 - HEAD_THICKNESS_STEEL, 1.0F);
-                case NORTH -> new AABBd(0.0F, 0.0F, HEAD_THICKNESS_STEEL, 1.0F, 1.0F, 1.0F);
-                case SOUTH -> new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1 - HEAD_THICKNESS_STEEL);
-                case WEST -> new AABBd(HEAD_THICKNESS_STEEL, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
-                default -> new AABBd(0.0F, 0.0F, 0.0F, 1 - HEAD_THICKNESS_STEEL, 1.0F, 1.0F);
-            };
-        } else {
-            return new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
-        }
-    }
-
-    @Override
-    protected boolean canPushLine(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction, int maxPushedBlocks) {
-        TilePos posO = tilePos.add(direction, new TilePos());
-        int blocks = 0;
-        boolean didCrush = false;
-        while (true) {
-            if (blocks >= maxPushedBlocks + 1) {
-                break;
-            }
-            if (posO.y < 0 || posO.y >= world.getHeightBlocks()) {
-                return false;
-            }
-            final @NotNull Block<?> block = world.getBlockType(posO);
-            if (block == Blocks.AIR) {
-                break;
-            }
-            if (!isPushable(block, world, posO, true)) {
-                if (blocks == 1 && block.hasTag(BlockTags.PISTON_CRUSHING) && tryCrush(world, tilePos, direction)) {
-                    didCrush = true;
-                    break;
-                }
-                return false;
-            }
-            if (block.getPistonPushReaction(world, posO) == Material.PISTON_DESTROY_ON_PUSH) {
-                break;
-            }
-            if (blocks == maxPushedBlocks) {
-                return false;
-            }
-            posO.x += direction.getOffsetX();
-            posO.y += direction.getOffsetY();
-            posO.z += direction.getOffsetZ();
-            blocks++;
-        }
-        if (!didCrush && blocks == 1) { // Block Flinging
-            tilePos.add(direction, posO);
-            Block<?> block = world.getBlockType(posO);
-            if (block == null) return true;
-            int blockMeta = world.getBlockData(posO);
-            TileEntity tileEntity = world.getTileEntity(posO);
-            world.removeTileEntity(posO);
-            world.setBlockTypeNotify(posO, Blocks.AIR);
-            if (!world.isClientSide) {
-                EntityFallingBlock entityFallingBlock = new EntityFallingBlock(world, posO.x + 0.5, posO.y + 0.5, posO.z + 0.5, block.id(), blockMeta, tileEntity);
-                entityFallingBlock.hasRemovedBlock = true;
-                if (tileEntity instanceof IVehicle) {
-                    Entity rider = ((IVehicle) tileEntity).getPassenger();
-                    ((IVehicle) tileEntity).ejectRider();
-                    if (rider != null) rider.startRiding(entityFallingBlock);
-                }
-                world.entityJoinedWorld(entityFallingBlock);
-                final double speed = 2;
-                entityFallingBlock.fling(direction.getOffsetX() * speed, direction.getOffsetY() * speed, direction.getOffsetZ() * speed, 1f);
-                this.flungBlock = entityFallingBlock;
-            }
-        }
-        return true;
-    }
-
-    @Override
-    public boolean tryCrush(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction) {
-        TilePos pos2 = tilePos.add(direction, new TilePos());
-        Block<?> block = world.getBlockType(pos2);
-        world.playBlockEvent(null, pos2, LevelListener.EVENT_BLOCK_BREAK, world.getBlockType(pos2).id());
-        block.dropWithCause(world, EnumDropCause.PISTON_CRUSH, pos2, world.getBlockData(pos2), null, null);
-        world.setBlockTypeNotify(pos2, Blocks.AIR);
-        return true;
-    }
-
-    @Override
-    protected boolean tryExtend(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction, int maxPushedBlocks) {
-        if (world.isClientSide) { // "Fixes" steel pistons appearing to push the block they just flung
-            int data = world.getBlockData(tilePos);
-            TilePos posO = tilePos.add(direction, new TilePos());
-            int blocks = 0;
-            while (true) {
-                if (blocks >= maxPushedBlocks + 1) {
-                    break;
-                }
-                if (posO.y < 0 || posO.y >= world.getHeightBlocks()) {
-                    return false;
-                }
-                final @NotNull Block<?> block = world.getBlockType(posO);
-                if (block == Blocks.AIR) {
-                    break;
-                }
-                if (!isPushable(block, world, posO, true)) {
-                    return false;
-                }
-                if (block.getPistonPushReaction(world, posO) == Material.PISTON_DESTROY_ON_PUSH) {
-                    block.dropWithCause(world, EnumDropCause.WORLD, posO, world.getBlockData(posO), world.getTileEntity(posO), null);
-                    break;
-                }
-                if (blocks == maxPushedBlocks) {
-                    return false;
-                }
-                posO.x += direction.getOffsetX();
-                posO.y += direction.getOffsetY();
-                posO.z += direction.getOffsetZ();
-                blocks++;
-            }
-            if (blocks == 1) {
-                TilePos posP = new TilePos(posO);
-                while (!posO.equals(tilePos)) {
-                    posP.set(posO.x - direction.getOffsetX(), posO.y - direction.getOffsetY(), posO.z - direction.getOffsetZ());
-                    @NotNull Block<?> pushBlock = world.getBlockType(posP);
-                    TileEntity pushEntity = world.getTileEntity(posP);
-                    if (pushEntity instanceof TileEntityMovingPistonBlock && posP.equals(tilePos)) {
-                        break;
-                    }
-                    if (pushBlock == this.block && posP.equals(tilePos)) {
-                        createPistonHeadAt(world, posO, data, direction);
-                    }
-                    if (!posP.equals(tilePos))
-                        world.setBlockTypeRaw(posP, Blocks.AIR);
-                    posO.x = posP.x;
-                    posO.y = posP.y;
-                    posO.z = posP.z;
-                }
-                return true;
-            }
-        }
-        return super.tryExtend(world, tilePos, direction, maxPushedBlocks);
-    }
-
-    @Override
-    public void extendEvent(@NotNull World world, @NotNull TilePosc tilePos, int data, @NotNull Direction direction) {
-        List<Entity> entityList = new ArrayList<>(world.getEntitiesWithinAABBExcludingEntity(null, new AABBd((double) tilePos.x() + direction.getOffsetX(), (double) tilePos.y() + direction.getOffsetY(), (double) tilePos.z() + direction.getOffsetZ(), (double) tilePos.x() + direction.getOffsetX() + 1, (double) tilePos.y() + direction.getOffsetY() + 1, (double) tilePos.z() + direction.getOffsetZ() + 1)));
-        for (Entity entity : entityList) {
-            /*Fix for 1 tick extensions not working properly*/
-            if (entity == null || entity.noPhysics || entity == this.flungBlock || entity.world.isClientSide && entity instanceof Player)
-                continue;
-            final double speed = 2;
-            entity.fling(direction.getOffsetX() * speed, direction.getOffsetY() * speed, direction.getOffsetZ() * speed, 1f);
-        }
-        this.flungBlock = null;
-        super.extendEvent(world, tilePos, data, direction);
-    }
-
-    @Override
-    public void createPistonHeadAt(@NotNull World world, @NotNull TilePos tilePos, int data, @NotNull Direction direction) {
-        world.setBlockTypeData(tilePos, Blocks.PISTON_MOVING, BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_STEEL, direction.getId()));
-        world.replaceTileEntity(tilePos, BlockLogicPistonMoving.createTileEntity(Blocks.PISTON_HEAD_STEEL, BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_STEEL, direction.getId()), null, direction, true, false));
-    }
-
-}
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBaseSticky.java b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBaseSticky.java
deleted file mode 100644
index 890266124..000000000
--- a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBaseSticky.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package net.minecraft.core.block.piston;
-
-import net.minecraft.core.block.Blocks;
-import net.minecraft.core.block.entity.TileEntity;
-import net.minecraft.core.block.material.Material;
-import net.minecraft.core.block.Block;
-import net.minecraft.core.sound.SoundCategory;
-import net.minecraft.core.util.helper.Direction;
-import net.minecraft.core.world.World;
-import net.minecraft.core.world.pos.TilePos;
-import net.minecraft.core.world.pos.TilePosc;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Objects;
-
-public class BlockLogicPistonBaseSticky extends BlockLogicPistonBase {
-    public BlockLogicPistonBaseSticky(@NotNull Block<?> block, int maxPushedBlocks) {
-        super(block, maxPushedBlocks);
-    }
-
-    @Override
-    public void retractEvent(@NotNull World world, @NotNull TilePosc tilePos, int data, @NotNull Direction direction) {
-        TilePos posDir = tilePos.add(direction, new TilePos());
-        TileEntity tileEntity = world.getTileEntity(posDir);
-        if ((tileEntity instanceof TileEntityMovingPistonBlock moving)) {
-            if (!moving.isExtending()) {
-                moving.finalTick();
-            } else {
-                world.setBlockType(posDir, Blocks.AIR);
-            }
-        }
-        world.setBlockTypeData(tilePos, Blocks.PISTON_MOVING, direction.getId());
-        world.replaceTileEntity(tilePos, BlockLogicPistonMoving.createTileEntity(this.block, direction.getId(), null, direction, false, true));
-        TilePos pos1 = new TilePos(tilePos.x() + direction.getOffsetX() * 2, tilePos.y() + direction.getOffsetY() * 2, tilePos.z() + direction.getOffsetZ() * 2);
-        @NotNull Block<?> retractBlock = world.getBlockType(pos1);
-        int retactMeta = world.getBlockData(pos1);
-        TileEntity retractEntity = world.getTileEntity(pos1);
-        boolean pulledMoving = false;
-        if (retractBlock == Blocks.PISTON_MOVING) {
-            if ((world.getTileEntity(pos1) instanceof TileEntityMovingPistonBlock movingPistonBlock)) {
-                if (movingPistonBlock.getDirection() == direction && movingPistonBlock.isExtending()) {
-                    movingPistonBlock.finalTick();
-                    retractBlock = movingPistonBlock.getMovedBlock();
-                    retactMeta = movingPistonBlock.getMovedData();
-                    retractEntity = movingPistonBlock.getMovedEntity();
-                    pulledMoving = true;
-                }
-            }
-        }
-        TilePos pos2 = new TilePos(tilePos);
-        if (!pulledMoving &&
-            isPushable(retractBlock, world, pos1, false) &&
-            (retractBlock.getPistonPushReaction(world, pos1) == Material.PISTON_PUSHABLE || (retractBlock.getLogic() instanceof BlockLogicPistonBase))) {
-            if (!(retractEntity instanceof TileEntityMovingPistonBlock)) {
-                world.removeTileEntity(pos1);
-            }
-            world.setBlockType(pos1, Blocks.AIR);
-            pos2.x += direction.getOffsetX();
-            pos2.y += direction.getOffsetY();
-            pos2.z += direction.getOffsetZ();
-            world.setBlockTypeData(pos2, Blocks.PISTON_MOVING, retactMeta);
-            world.replaceTileEntity(pos2, BlockLogicPistonMoving.createTileEntity(retractBlock, retactMeta, retractEntity, direction, false, false));
-            world.notifyBlockChange(pos1, Blocks.AIR);
-        } else if (!pulledMoving) {
-            world.setBlockTypeNotify(posDir, Blocks.AIR);
-        }
-        world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) pos2.x + 0.5D, (double) pos2.y + 0.5D, (double) pos2.z + 0.5D, "tile.piston.in", 0.5F, world.rand.nextFloat() * 0.15F + 0.6F);
-    }
-
-    @Override
-    public void createPistonHeadAt(@NotNull World world, @NotNull TilePos tilePos, int data, @NotNull Direction direction) {
-        world.setBlockTypeData(tilePos, Blocks.PISTON_MOVING, BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_STICKY, direction.getId()));
-        world.replaceTileEntity(tilePos, BlockLogicPistonMoving.createTileEntity(Blocks.PISTON_HEAD, BlockLogicPistonHead.setPistonType(BlockLogicPistonHead.TYPE_STICKY, direction.getId()), null, direction, true, false));
-    }
-}
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonHead.java b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonHead.java
index a1ee5acc8..95d6d5b68 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonHead.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonHead.java
@@ -11,28 +11,32 @@ import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.block.support.FullSupport;
 import net.minecraft.core.block.support.ISupport;
 import net.minecraft.core.block.support.PartialSupport;
+import net.minecraft.core.entity.Entity;
+import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
+import net.minecraft.core.item.Item;
 import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.item.Items;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.Side;
+import net.minecraft.core.world.ICarriable;
+import net.minecraft.core.world.ICarrySource;
 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;
 import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3d;
 import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
 
-public class BlockLogicPistonHead extends BlockLogic {
-    public static final int MASK_DIRECTION = 0b0000_0111;
-    public static final int MASK_TYPE = 0b1111_1000;
-    public static final int TYPE_NORMAL = 0;
-    public static final int TYPE_STICKY = 1;
-    public static final int TYPE_STEEL = 2;
+import static net.minecraft.core.block.piston.PistonCommon.*;
 
-    private final double headThickness;
-    private final double shaftThickness;
+public class BlockLogicPistonHead extends BlockLogic implements ICarrySource {
+
+    // "Good lord. This really is our most NSFW update" -- Jonk
+    final double headThickness, shaftThickness;
 
     public BlockLogicPistonHead(final Block<?> block, final double headThickness, final double shaftThickness) {
         super(block, Materials.PISTON);
@@ -41,25 +45,6 @@ public class BlockLogicPistonHead extends BlockLogic {
         this.shaftThickness = shaftThickness;
     }
 
-    @Override
-    public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
-        super.onRemoved(world, tilePos, data);
-        Direction direction = getDirectionFromMeta(data);
-        if (direction == Direction.NONE) {
-            return;
-        }
-        Direction orientation = direction.getOpposite();
-        TilePos nextPos = tilePos.add(orientation, new TilePos());
-        Block<?> block = world.getBlockType(nextPos);
-        if (block != null && block.getLogic() instanceof BlockLogicPistonBase) {
-            int data2 = world.getBlockData(nextPos);
-            if (BlockLogicPistonBase.isPowered(data2)) {
-                block.dropWithCause(world, EnumDropCause.PROPER_TOOL, nextPos, data2, null, null);
-                world.setBlockTypeNotify(nextPos, Blocks.AIR);
-            }
-        }
-    }
-
     @Override
     public boolean isSolidRender() {
         return false;
@@ -72,106 +57,174 @@ public class BlockLogicPistonHead extends BlockLogic {
 
     @Override
     public @NotNull ISupport getSupport(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
-        int meta = world.getBlockData(tilePos);
-        return getDirectionFromMeta(meta).getSide() == side ? FullSupport.INSTANCE : PartialSupport.INSTANCE;
+        int data = getValidatedHeadData(world, tilePos);
+        return (DIRECTION.get(data) == side.id)
+        ? FullSupport.INSTANCE 
+        : PartialSupport.INSTANCE;
     }
 
     @Override
     public boolean canPlaceAt(@NotNull World world, @NotNull TilePosc tilePos) {
         return false;
     }
-
+    
     @Override
     public boolean canPlaceOnSide(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side) {
         return false;
     }
-
-
+    
     @Override
     public void getCollisionAABBs(@NotNull World world, @NotNull TilePosc tilePos, @NotNull AABBdc aabb, @NotNull List<@NotNull AABBdc> aabbList) {
-        switch (getDirectionFromMeta(world.getBlockData(tilePos))) {
-            case DOWN -> {
-                addIntersectingBoundingBox(aabb, new AABBd(0.0F, 0.0F, 0.0F, 1.0F, this.headThickness, 1.0F).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-                addIntersectingBoundingBox(aabb, new AABBd(0.5 - this.shaftThickness / 2, this.headThickness, 0.5 - this.shaftThickness / 2, 0.5 + this.shaftThickness / 2, 1 + this.headThickness, 0.5 + this.shaftThickness / 2).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-            }
-            case UP -> {
-                addIntersectingBoundingBox(aabb, new AABBd(0.0F, 1 - this.headThickness, 0.0F, 1.0F, 1.0F, 1.0F).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-                addIntersectingBoundingBox(aabb, new AABBd(0.5 - this.shaftThickness / 2, -this.headThickness, 0.5 - this.shaftThickness / 2, 0.5 + this.shaftThickness / 2, 1 - this.headThickness, 0.5 + this.shaftThickness / 2).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-            }
-            case NORTH -> {
-                addIntersectingBoundingBox(aabb, new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, this.headThickness).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-                addIntersectingBoundingBox(aabb, new AABBd(0.5 - this.shaftThickness / 2, 0.5 - this.shaftThickness / 2, this.headThickness, 0.5 + this.shaftThickness / 2, 0.5 + this.shaftThickness / 2, 1 + this.headThickness).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-            }
-            case SOUTH -> {
-                addIntersectingBoundingBox(aabb, new AABBd(0.0F, 0.0F, 1 - this.headThickness, 1.0F, 1.0F, 1.0F).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-                addIntersectingBoundingBox(aabb, new AABBd(0.5 - this.shaftThickness / 2, 0.5 - this.shaftThickness / 2, -this.headThickness, 0.5 + this.shaftThickness / 2, 0.5 + this.shaftThickness / 2, 1 - this.headThickness).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-            }
-            case WEST -> {
-                addIntersectingBoundingBox(aabb, new AABBd(0.0F, 0.0F, 0.0F, this.headThickness, 1.0F, 1.0F).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-                addIntersectingBoundingBox(aabb, new AABBd(this.headThickness, 0.5 - this.shaftThickness / 2, 0.5 - this.shaftThickness / 2, 1 + this.headThickness, 0.5 + this.shaftThickness / 2, 0.5 + this.shaftThickness / 2).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-            }
-            case EAST -> {
-                addIntersectingBoundingBox(aabb, new AABBd(1 - this.headThickness, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-                addIntersectingBoundingBox(aabb, new AABBd(-this.headThickness, 0.5 - this.shaftThickness / 2, 0.5 - this.shaftThickness / 2, 1 - this.headThickness, 0.5 + this.shaftThickness / 2, 0.5 + this.shaftThickness / 2).translate(tilePos.x(), tilePos.y(), tilePos.z()), aabbList);
-            }
-        }
+        final var halvedShaft = shaftThickness / 2;
+        
+        final Vector3d hmin = new Vector3d(0), hmax = new Vector3d(1);
+        final Vector3d smin = new Vector3d(0.5 - halvedShaft), smax = new Vector3d(0.5 + halvedShaft);
+        
+        final var dir = Direction.fromId(DIRECTION.get(getValidatedHeadData(world, tilePos)));
+        final int cidx = dir.componentIndex();
+        final boolean neg = dir.isNegative();
+
+        if (neg) hmax.setComponent(cidx, headThickness);
+            else hmin.setComponent(cidx, 1 - headThickness);
+          
+        final var sstart = (neg) ? this.headThickness : -this.headThickness;
+        smin.setComponent(cidx, sstart);
+        smax.setComponent(cidx, sstart + 1);
+        
+        final var pos = new Vector3d(tilePos.vec());
+        addIntersectingBoundingBox(aabb, new AABBd(hmin, hmax).translate(pos), aabbList);
+        addIntersectingBoundingBox(aabb, new AABBd(smin, smax).translate(pos), aabbList);
+    }
+
+    @Override
+    public @NotNull AABBdc getSelectionAABB(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
+        final Vector3d min = new Vector3d(0), max = new Vector3d(1);
+        // if (!isBaseless(source.getBlockData(tilePos))) {
+        //     final int i = getDirectionFromMeta(source.getBlockData(tilePos)).getId();
+        //     final int j = (i/2 + 1) % 3;
+        //     final boolean n = (i & 1) == 0;
+        //     if (n) max.setComponent(j, 1 + headThickness);
+        //       else min.setComponent(j, 0 - headThickness);
+        // }
+        return new AABBd(min, max).translate(new Vector3d(tilePos.vec()));
     }
 
     @Override
     public @NotNull AABBdc getBoundsFromState(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
-        return switch (getDirectionFromMeta(source.getBlockData(tilePos))) {
-            case DOWN -> new AABBd(0.0F, 0.0F, 0.0F, 1.0F, this.headThickness, 1.0F);
-            case UP -> new AABBd(0.0F, 1 - this.headThickness, 0.0F, 1.0F, 1.0F, 1.0F);
-            case NORTH -> new AABBd(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, this.headThickness);
-            case SOUTH -> new AABBd(0.0F, 0.0F, 1 - this.headThickness, 1.0F, 1.0F, 1.0F);
-            case WEST -> new AABBd(0.0F, 0.0F, 0.0F, this.headThickness, 1.0F, 1.0F);
-            case EAST -> new AABBd(1 - this.headThickness, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
-            default -> super.getBoundsFromState(source, tilePos);
-        };
+        final Vector3d min = new Vector3d(0), max = new Vector3d(1);
+
+        final var dir = Direction.fromId(DIRECTION.get(getValidatedHeadData(source, tilePos)));
+        final int cidx = dir.componentIndex();
+        final boolean neg = dir.isNegative();
+
+        if (neg) max.setComponent(cidx, headThickness);
+            else min.setComponent(cidx, 1 - headThickness);
+
+        return new AABBd(min, max);
     }
 
     @Override
     public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        Direction direction = getDirectionFromMeta(world.getBlockData(tilePos));
-        if (direction == Direction.NONE) {
-            return;
-        }
-        TilePos posOffset = tilePos.sub(direction, new TilePos());
-        Block<?> offsetBlock = world.getBlockType(posOffset);
-        if (offsetBlock.getLogic() instanceof BlockLogicPistonBase) {
-            offsetBlock.onNeighborChanged(world, posOffset, block);
-        } else {
-            world.setBlockTypeNotify(tilePos, Blocks.AIR);
-        }
+        final var data = getValidatedHeadData(world, tilePos);
+        if (IS_DETACHED.bool(data)) return;
+
+        final var bpos = new TilePos(tilePos).sub(Direction.fromId(DIRECTION.get(data)));
+        if (!(world.getBlockType(bpos).getLogic() instanceof BlockLogicPistonBase)) return;
+        int bdat = fixLegacyBaseData(world, bpos);
+        if (PAIR_COMPARISON.get(data ^ bdat) != 0) return;
+
+        world.getBlockType(bpos).onNeighborChanged(world, bpos, block);
     }
 
     @Override
     public int getPistonPushReaction(@NotNull World world, @NotNull TilePosc tilePos) {
-        return Material.PISTON_CANT_PUSH;
-    }
-
-    public static Direction getDirectionFromMeta(int meta) {
-        return Direction.getDirectionById(meta & MASK_DIRECTION);
+        return (IS_DETACHED.bool(getValidatedHeadData(world, tilePos)))
+            ? Material.PISTON_PUSHABLE 
+            : Material.PISTON_CANT_PUSH;
     }
 
-    public static int getPistonType(int data) {
-        return ((data & 0xFF) & MASK_TYPE) >> 3;
+    @Override
+    public void onRemoved(@NotNull World world, @NotNull TilePosc tilePos, int data) {
+        data = validateHeadData(data);
+        if (IS_DETACHED.bool(data)) return;
+
+        final var bpos = new TilePos(tilePos).sub(Direction.fromId(DIRECTION.get(data)));
+        if (!(world.getBlockType(bpos).getLogic() instanceof BlockLogicPistonBase)) return;
+        int bdat = fixLegacyBaseData(world, bpos);
+        if (PAIR_COMPARISON.get(data ^ bdat) != 0) return;
+        bdat = IS_DETACHED.set(bdat, 1);
+        if (TYPE.get(bdat) == TYPE_STICKY) {
+            bdat = TYPE.set(bdat, TYPE_NORMAL);
+            world.setBlockTypeData(bpos, Blocks.PISTON_BASE, bdat);
+        } else {
+            world.setBlockData(bpos, bdat);
+        }
+        world.markBlockDirty(bpos);
+        world.triggerEvent(bpos, EVENT_DETACH, 0);
     }
 
-    public static int setPistonType(int type, int data) {
-        return ((data & ~MASK_TYPE) | (type << 3)) & 0xFF;
+    @Override
+    public void onDestroyedByPlayer(
+        @NotNull World world, @NotNull TilePosc tilePos, @NotNull Side side, int data,
+        @NotNull Player player, @Nullable Item item
+    ) {
+        data = validateHeadData(data);
+        if (IS_DETACHED.bool(data)) return;
+        
+        final var bpos = new TilePos(tilePos).sub(Direction.fromId(DIRECTION.get(data)));
+        if (!(world.getBlockType(bpos).getLogic() instanceof BlockLogicPistonBase)) return;
+        int bdat = fixLegacyBaseData(world, bpos);
+        bdat = IS_DETACHED.set(bdat, 0);
+        if (PAIR_COMPARISON.get(data ^ bdat) != 0) return;
+        
+        final var b = world.getBlockType(bpos).getLogic();
+        world.setBlockTypeNotify(bpos, Blocks.AIR); // onRemoved
+        
+        if (player.gamemode.hasItemDrops()) {
+            b.dropWithCause(world, EnumDropCause.WORLD, bpos, bdat, null, null);
+        }
     }
 
     @Override
-    public ItemStack[] getBreakResult(@NotNull World world, @NotNull EnumDropCause dropCause, int data, @Nullable TileEntity tileEntity) {
+    public @NotNull ItemStack @Nullable [] getBreakResult(
+        @NotNull World world, @NotNull EnumDropCause dropCause, @NotNull TilePosc tilePos,
+        int data, @Nullable TileEntity tileEntity
+    ) {
+        // handle middle click pick block properly
         if (dropCause == EnumDropCause.PICK_BLOCK) {
-            return switch (getPistonType(data)) {
-                case TYPE_NORMAL -> new ItemStack[]{new ItemStack(Blocks.PISTON_BASE)};
-                case TYPE_STICKY -> new ItemStack[]{new ItemStack(Blocks.PISTON_BASE_STICKY)};
-                case TYPE_STEEL -> new ItemStack[]{new ItemStack(Blocks.PISTON_BASE_STEEL)};
-                default -> null;
-            };
+            if (IS_DETACHED.bool(data)) return null;
+            
+            // we assume head is attached to an actual matching base without performing tilepos lookup
+            int baseData = IS_EXTENDED.set(data, 1);
+            return BASE_BLOCK_MAP[TYPE.get(data)].getBreakResult(world, dropCause, baseData, null);
         }
-        return null;
+
+        return this.getBreakResult(world, dropCause, data, tileEntity);
+    }
+    
+    @Override
+    public @NotNull ItemStack @Nullable [] getBreakResult(
+        @NotNull World world, @NotNull EnumDropCause dropCause,
+        int data, @Nullable TileEntity tileEntity
+    ) {
+        class Result { final static ItemStack[][] LUT = {
+            { new ItemStack(Blocks.PLANKS_OAK, 3) },
+            { new ItemStack(Blocks.PLANKS_OAK, 3), new ItemStack(Items.SLIMEBALL) },
+            { new ItemStack(Items.INGOT_STEEL_CRUDE, 3) },
+        }; }
+        
+        if (dropCause == EnumDropCause.PICK_BLOCK) return null;
+        if (!IS_DETACHED.bool(data)) return null;
+        return Result.LUT[TYPE.get(data)];
+    }
+
+    @Override
+    public @NotNull ICarriable pickup(@NotNull World world, @NotNull Entity holder, @NotNull TilePosc tilePos) {
+        final var hdat = getValidatedHeadData(world, tilePos);
+        
+        world.setBlockType(tilePos, Blocks.AIR); // onRemoved
+        world.notifyBlockChange(tilePos, this.block);
+
+        return new CarriedPistonHead((byte)TYPE.get(hdat));
     }
 }
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 192fc82fd..9cf4d026d 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
@@ -8,11 +8,13 @@ import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.ItemStack;
-import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePosc;
+
+import java.util.List;
+
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.joml.primitives.AABBd;
@@ -54,20 +56,6 @@ public class BlockLogicPistonMoving extends BlockLogic {
         return false;
     }
 
-    @Override
-    public boolean onInteracted(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Player player, @Nullable Side side, double xHit, double yHit) {
-        if (!world.isClientSide && world.getTileEntity(tilePos) == null) {
-            @NotNull Block<?> block = Blocks.AIR;
-            if (world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock piston) {
-                block = piston.getMovedBlock();
-            }
-            world.setBlockTypeNotify(tilePos, block);
-            return true;
-        } else {
-            return false;
-        }
-    }
-
     @Override
     public ItemStack[] getBreakResult(@NotNull World world, @NotNull EnumDropCause dropCause, int data, @Nullable TileEntity tileEntity) {
         return null;
@@ -75,62 +63,52 @@ 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.isClientSide) return;
         if (world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock piston) {
-            piston.getMovedBlock().dropWithCause(world, EnumDropCause.WORLD, tilePos, data, null, null);
+            piston.movingBlock().dropWithCause(world, EnumDropCause.WORLD, tilePos, data, null, null);
         }
     }
-
+    
     @Override
     public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        if (!world.isClientSide) {
-            world.getTileEntity(tilePos); // Im not sure if this actually does anything anymore??
+        if (world.isClientSide) return;
+        if (!(world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock)) {
+            world.setBlockType(tilePos, Blocks.AIR);
         }
     }
 
-    public static TileEntity createTileEntity(final @NotNull Block<?> movedBlock, int movedData, @Nullable TileEntity movedEntity, @NotNull Direction direction, boolean extending, boolean isSourcePiston) {
-        assert !(movedEntity instanceof TileEntityMovingPistonBlock) : "Moving piston entity should not be able to be put into another moving piston!";
-        return new TileEntityMovingPistonBlock(movedBlock, movedData, movedEntity, direction, extending, isSourcePiston);
+    @Override
+    public void getCollisionAABBs(
+        @NotNull World world, @NotNull TilePosc tilePos,
+        @NotNull AABBdc aabb, @NotNull List<@NotNull AABBdc> aabbList
+    ) {
+        if (!(world.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock moving)) {
+            super.getCollisionAABBs(world, tilePos, aabb, aabbList);
+            return;
+        }
+        if (moving.isExtending || !moving.isSourcePiston) {
+            super.getCollisionAABBs(world, tilePos, aabb, aabbList);
+            return;
+        }
+        aabbList.add(this.getCollisionAABB(world, tilePos));
+        // prevents entities from phasing into piston base
+        aabbList.add(new AABBd(0, 0, 0, 1, 1, 1).translate(tilePos.x(), tilePos.y(), tilePos.z()));
+        return;
     }
 
     @Override
     public @Nullable AABBdc getCollisionAABB(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
-        if (source.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock piston) {
-            float progress = piston.getProgress(0.0F);
-            if (piston.isExtending()) {
-                progress = 1.0F - progress;
-            }
-            return getCollisionShapeFromTileEntity(source, tilePos, piston.getMovedBlock(), progress, piston.getDirection());
+        if (!(source.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock moving)) {
+            return null;
         }
-        return null;
+        return moving.collision;
     }
 
     @Override
     public @NotNull AABBdc getBoundsFromState(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
         if (source.getTileEntity(tilePos) instanceof TileEntityMovingPistonBlock piston) {
-            Block<?> block = piston.getMovedBlock();
-            if (block == this.block) {
-                return super.getBoundsFromState(source, tilePos);
-            }
-            float directionStretch = piston.getProgress(0.0F);
-            if (piston.isExtending()) {
-                directionStretch = 1.0F - directionStretch;
-            }
-            Direction direction = piston.getDirection();
-            AABBdc otherBounds = block.getBoundsFromState(source, tilePos);
-            return otherBounds.translate(-direction.getOffsetX() * directionStretch, -direction.getOffsetY() * directionStretch, -direction.getOffsetZ() * directionStretch, new AABBd());
+            return piston.bounds;
         }
         return super.getBoundsFromState(source, tilePos);
     }
-
-    public @Nullable AABBdc getCollisionShapeFromTileEntity(@NotNull WorldSource source, @NotNull TilePosc tilePos, final @NotNull Block<?> block, float directionStretch, @NotNull Direction direction) {
-        AABBdc aabb = block.getCollisionAABB(source, tilePos);
-        if (aabb == null) {
-            return null;
-        } else {
-            return aabb.translate(-direction.getOffsetX() * directionStretch, -direction.getOffsetY() * directionStretch, -direction.getOffsetZ() * directionStretch, new AABBd());
-        }
-    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/CarriedPistonHead.java b/game/core/src/main/java/net/minecraft/core/block/piston/CarriedPistonHead.java
new file mode 100644
index 000000000..a67feeed1
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/CarriedPistonHead.java
@@ -0,0 +1,107 @@
+package net.minecraft.core.block.piston;
+
+import org.jetbrains.annotations.NotNull;
+
+import com.mojang.nbt.tags.CompoundTag;
+
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.motion.ICarriedBlock;
+import net.minecraft.core.entity.Entity;
+import net.minecraft.core.entity.Mob;
+import net.minecraft.core.enums.PlacementMode;
+import net.minecraft.core.util.helper.Direction;
+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.TilePos;
+
+import static net.minecraft.core.block.piston.PistonCommon.*;
+
+public class CarriedPistonHead implements ICarriable, ICarriedBlock {
+    private @NotNull byte type;
+
+    public CarriedPistonHead(byte type) {
+        this.type = type;
+    }
+
+	@Override
+    public @NotNull Block<? extends BlockLogicPistonHead> block() {
+        return PistonCommon.HEAD_BLOCK_MAP[type];
+    }
+
+    @Override
+    public int metadata() {
+        return metadata(Direction.UP.id);
+    }
+    
+    private int metadata(int dir) {
+        int d = 0;
+        d = IS_DETACHED.set(d, 1);
+        d = TYPE.set(d, this.type);
+        d = DIRECTION.set(d, dir);
+        d = validateHeadData(d);
+        return d;
+    }
+
+    @Override
+    public boolean tryPlace(World world, Entity holder, int blockX, int blockY, int blockZ,
+        Side side, double xPlaced, double yPlaced
+    ) {
+        final var hpos = new TilePos(blockX, blockY, blockZ).add(side.direction());
+        if (!world.canPlaceInsideBlock(hpos)) return false;
+
+        final int dir = (holder instanceof Mob mob)
+            ? mob.getPlacementDirection(side.opposite(), PlacementMode.SIDE).opposite().id
+            : Direction.UP.id;
+
+        int hdat = metadata(dir);
+        world.setBlockType(hpos, block());
+
+        sync_base: {
+            final var bpos = new TilePos();
+            int bdat = headGetBase(hdat, bpos, world, hpos);
+            if (bdat == DATA_INVALID) break sync_base;
+            if (!IS_DETACHED_OR_EXTENDED.bool(bdat)) break sync_base;
+            if (PAIR_COMPARISON.get(hdat ^ bdat) != 0) break sync_base;
+
+            hdat = IS_DETACHED.set(hdat, 0);
+            bdat = IS_DETACHED.set(bdat, 0);
+            bdat = IS_EXTENDED.set(hdat, 1);
+
+            if (IS_STEEL.bool(hdat)) {
+                world.setBlockData(bpos, bdat);
+            } else {
+                bdat = TYPE.setNoShift(bdat, hdat);
+                world.setBlockTypeDataRaw(bpos, BASE_BLOCK_MAP[TYPE.get(hdat)], bdat);
+            }
+            world.triggerEvent(bpos, EVENT_ATTACH, 0);
+        }
+
+        world.setBlockDataNotify(hpos, hdat);
+        return true;
+    }
+
+    @Override
+    public void drop(World world, Entity holder) {
+    }
+
+    @Override
+    public void writeToNBT(CompoundTag tag) {
+        tag.putByte("head_type", type);
+        tag.putString("type", TYPE_PISTON_HEAD);
+    }
+
+    @Override
+    public void readFromNBT(CompoundTag tag) {
+        type = tag.getByte("head_type");
+    }
+
+    public static CarriedPistonHead createAndLoad(CompoundTag tag) {
+        final var carriedBlock = new CarriedPistonHead((byte)0);
+        carriedBlock.readFromNBT(tag);
+        return carriedBlock;
+    }
+
+    @Override
+    public void heldTick(World world, Entity holder) {}
+}
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
new file mode 100644
index 000000000..ebf72e971
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/PistonCommon.java
@@ -0,0 +1,186 @@
+package net.minecraft.core.block.piston;
+
+import org.jetbrains.annotations.NotNull;
+
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.util.PackedField;
+import net.minecraft.core.util.helper.Direction;
+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;
+
+/**
+ * NOTE: this class should only be accessed after the Blocks clinit logic is complete, i.e. 
+ * not from within BlockLogic constructors!
+ */
+public abstract class PistonCommon {
+    private PistonCommon() {}
+
+    public static final PackedField DIRECTION   = new PackedField(0, 3);
+    public static final PackedField TYPE        = new PackedField(3, 2);
+    public static final PackedField IS_DETACHED = new PackedField(5, 1);
+    public static final PackedField IS_EXTENDED = new PackedField(6, 1);
+    
+    public static final PackedField NEW_FORMAT_SIGNATURE = new PackedField(7, 1);
+
+    public static final PackedField IS_STICKY   = new PackedField(4, 1);
+    public static final PackedField IS_STEEL    = new PackedField(5, 1);
+
+    public static final PackedField IS_DETACHED_OR_EXTENDED = new PackedField(
+        IS_DETACHED.mask | IS_EXTENDED.mask
+    );
+    
+    public static final PackedField PAIR_COMPARISON = new PackedField(
+        DIRECTION.mask | IS_DETACHED.mask | IS_STEEL.mask
+    );
+
+    // TODO using type id is poorly extensible,
+    // TODO   in the future we should probably just represent piston types with separate Blocks
+    public static final byte TYPE_NORMAL = 0b00;
+    public static final byte TYPE_STICKY = 0b01;
+    public static final byte TYPE_STEEL  = 0b10;
+
+    /**
+     * Event data encodes line length.
+     * @see BlockLogicPistonBase#testLine(World, TilePosc, int)
+     */ 
+    public static final byte EVENT_EXTEND         = 0b00_0; 
+    public static final byte EVENT_RETRACT        = 0b00_1;
+    public static final byte EVENT_DETACH         = 0b01_0;
+    public static final byte EVENT_ATTACH         = 0b01_1;
+    public static final byte EVENT_DETACH_EXTEND  = 0b10_0;
+    public static final byte EVENT_DETACH_RETRACT = 0b10_1;
+
+    public static final @NotNull Block<? extends BlockLogicPistonBase>[] 
+    BASE_BLOCK_MAP = (Block<BlockLogicPistonBase>[]) new Block[] {
+        Blocks.PISTON_BASE,
+        Blocks.PISTON_BASE_STICKY,
+        Blocks.PISTON_BASE_STEEL,
+    };
+    
+    public static final @NotNull Block<? extends BlockLogicPistonHead>[]
+    HEAD_BLOCK_MAP = (Block<BlockLogicPistonHead>[]) new Block[] {
+        Blocks.PISTON_HEAD,
+        Blocks.PISTON_HEAD, // TODO this here is the only reason why we need typeid in block metadata
+        Blocks.PISTON_HEAD_STEEL,
+    };
+
+    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);
+        if (NEW_FORMAT_SIGNATURE.bool(baseData)) return baseData;
+
+        baseData = TYPE.set(baseData, base.typeId);
+        baseData = NEW_FORMAT_SIGNATURE.set(baseData, 1);
+
+        // Direction bit address is the same as before
+        var baseDir = Direction.fromIdLenient(DIRECTION.get(baseData));
+        
+        // on a healthy world this shouldn't happen
+        if (baseDir == Direction.NONE) return DIRECTION.set(baseData, 0);
+
+        // old data format should not have set those bits
+        if (!IS_DETACHED_OR_EXTENDED.bool(baseData)) set_extended_if_head_matches: {
+            final var headPos = new TilePos(basePos).add(baseDir);
+            final var headBlock = world.getBlockType(headPos);
+            final var headData = world.getBlockData(headPos);
+
+            final boolean isMovingHead = headBlock.getLogic() instanceof BlockLogicPistonMoving;
+
+            if (isMovingHead) {
+                if (!(world.getTileEntity(headPos) instanceof TileEntityMovingPistonBlock moving)) {
+                    break set_extended_if_head_matches;
+                }
+                if (!moving.isSourcePiston) break set_extended_if_head_matches;
+                if (moving.direction != baseDir) break set_extended_if_head_matches;
+            } else if (!(headBlock.getLogic() instanceof BlockLogicPistonHead)) {
+                break set_extended_if_head_matches;
+            } 
+
+            // old head data format should not have set this bit
+            if (IS_DETACHED.bool(headData)) break set_extended_if_head_matches;
+            if (DIRECTION.get(headData) != baseDir.id) break set_extended_if_head_matches;
+
+            if (TYPE.get(headData) != base.typeId) break set_extended_if_head_matches;
+
+            baseData = IS_EXTENDED.set(baseData, 1);
+        }
+
+        world.setBlockData(basePos, baseData);
+        return baseData;
+    }
+
+    public static int validateBaseData(int data, @NotNull BlockLogicPistonBase base) {
+        data = TYPE.set(data, base.typeId);
+        if (DIRECTION.get(data) > 5) data = DIRECTION.set(data, 0);
+        // if (IS_DETACHED.bool(data)) data = IS_EXTENDED.set(data, 1);
+        return data;
+    }
+
+    public static int getValidatedBaseData(@NotNull WorldSource world, @NotNull TilePosc pos, @NotNull BlockLogicPistonBase base) {
+        return validateBaseData(world.getBlockData(pos), base);
+    }
+
+    public static int validateHeadData(int data) {
+        data = IS_EXTENDED.set(data, 0);
+        if (DIRECTION.get(data) > 5) data = DIRECTION.set(data, 0);
+        return data;
+    }
+    
+    public static int getValidatedHeadData(@NotNull WorldSource world, @NotNull TilePosc pos) {
+        return validateHeadData(world.getBlockData(pos));
+    }
+
+    public static int baseGetHead(int bdata, @NotNull TilePos hpos, @NotNull World world, @NotNull TilePosc bpos) {
+        hpos.set(bpos).add(Direction.fromId(DIRECTION.get(bdata)));
+        return (world.getBlockType(hpos).getLogic() instanceof BlockLogicPistonHead)
+            ? getValidatedHeadData(world, hpos)
+            : DATA_INVALID; 
+    }
+
+    public static int headGetBase(int hdata, @NotNull TilePos bpos, @NotNull World world, @NotNull TilePosc hpos) {
+        bpos.set(hpos).sub(Direction.fromId(DIRECTION.get(hdata)));
+        return (world.getBlockType(bpos).getLogic() instanceof BlockLogicPistonBase base)
+            ? getValidatedBaseData(world, bpos, base)
+            : DATA_INVALID; 
+    }
+
+    public static enum MovingType {
+        EXTENDING(true, false),
+        EXTENDING_PISTON(true, true),
+        RETRACTING(false, false),
+        RETRACTING_PISTON(false, true);
+
+        private MovingType(boolean isExtending, boolean isSourcePiston) {
+            this.isExtending = isExtending;
+            this.isSourcePiston = isSourcePiston;
+        }
+
+        public final boolean isExtending;
+        public final boolean isSourcePiston;
+    }
+
+    public static @NotNull ItemStack baseItem(int type, boolean isExteneded, boolean isDetached) {
+        int data = 0;
+        data |= TYPE.set(data, type);
+        data |= IS_EXTENDED.set(data, isExteneded ? 1 : 0);
+        data |= IS_DETACHED.set(data, isDetached ? 1 : 0);
+        return new ItemStack(BASE_BLOCK_MAP[type], 1, data);
+    }
+
+    public static @NotNull ItemStack headItem(int type, boolean isDetached) {
+        int data = 0;
+        data |= TYPE.set(data, type);
+        data |= IS_DETACHED.set(data, isDetached ? 1 : 0);
+        return new ItemStack(BASE_BLOCK_MAP[type], 1, 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 0b7d5745b..8822b3578 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
@@ -3,217 +3,210 @@ 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.Block;
-import net.minecraft.core.entity.Entity;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.util.helper.Direction;
 import com.mojang.nbt.tags.CompoundTag;
-import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePos;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
 
 import java.util.*;
+import static net.minecraft.core.block.piston.PistonCommon.*;
 
 public class TileEntityMovingPistonBlock extends TileEntity
 {
     public static final int TICKS_TO_EXTEND = 2;
-    private static final List<Entity> entityBuffer = new ArrayList<>();
-
-    private @NotNull Block<?> movedBlock;
-    private int movedData;
-    private @Nullable TileEntity movedEntity;
-    private Direction direction;
-    private boolean extending;
-    private boolean isSourcePiston;
-    private float progress;
-    private float progressO;
-
-    @SuppressWarnings("unused")
-    public TileEntityMovingPistonBlock()
-    {
-    }
-
-    public TileEntityMovingPistonBlock(final @NotNull Block<?> movedBlock, int movedData, @Nullable TileEntity movedEntity, Direction direction, boolean extending, boolean isSourcePiston)
-    {
-        this.movedBlock = movedBlock;
-        this.movedData = movedData;
-        this.movedEntity = movedEntity;
+    private static final float PROGRESS_DELTA = 1f / TICKS_TO_EXTEND;
+
+    @NotNull Block<?> movingBlock;
+    int movingBlockData;
+    @Nullable TileEntity movingTileEntity;
+
+    @NotNull Direction direction;
+    boolean isExtending;
+    boolean isSourcePiston;
+    float progressCur = 0F, progressPrev = 0F;
+
+    // these would otherwise be reallocated each tick, might as well store them here
+    @NotNull AABBd collision = new AABBd(); 
+    @NotNull AABBd bounds = new AABBd();
+
+    
+    public @NotNull Block<?> movingBlock() { return this.movingBlock; }
+    public int movingBlockData() { return this.movingBlockData; }
+    public @Nullable TileEntity movingTileEntity() { return this.movingTileEntity; }
+    
+    public @NotNull Direction direction() { return this.direction; }
+    public @NotNull Direction movingDirection() {
+        return (this.isExtending) ? this.direction : this.direction.opposite();
+    }
+
+    public boolean isExtending() { return this.isExtending; }
+    public boolean isSourcePiston() { return this.isSourcePiston; }
+
+    public @NotNull AABBdc collision() { return this.collision; }
+    public @NotNull AABBdc bounds() { return this.bounds; }
+
+
+    // @SuppressWarnings("unused")
+    public TileEntityMovingPistonBlock() {}
+
+    public TileEntityMovingPistonBlock(
+        @NotNull MovingType type,
+        @NotNull Block<?> movingBlock, int movingData, @Nullable TileEntity movingEntity, 
+        @NotNull Direction direction
+    ) {
+        assert movingBlock != Blocks.PISTON_MOVING;
+        this.movingBlock = movingBlock;
+        this.movingBlockData = movingData;
+        this.movingTileEntity = movingEntity;
         this.direction = direction;
-        this.extending = extending;
-        this.isSourcePiston = isSourcePiston;
-        this.progress = this.progressO = 0.0F;
+        this.isExtending = type.isExtending;
+        this.isSourcePiston = type.isSourcePiston;
     }
 
-    public @NotNull Block<?> getMovedBlock()
-    {
-        return this.movedBlock;
-    }
+    private final void initAABBs() {
+        double dx,dy,dz;
+        final var sign = (this.isExtending) ? -1 : 1;
+        dx = sign * this.direction.offsetX();
+        dy = sign * this.direction.offsetY();
+        dz = sign * this.direction.offsetZ();
 
-	public int getMovedData()
-    {
-        return this.movedData;
-    }
+        this.bounds
+            .set(this.movingBlock.getBoundsFromState(this.worldObj, tilePos))
+            .translate(dx, dy, dz);
 
-    public @Nullable TileEntity getMovedEntity() {
-        return this.movedEntity;
-    }
+        var collision = this.movingBlock.getCollisionAABB(this.worldObj, this.tilePos);
+        if (collision == null) collision = new AABBd(0,0,0,0,0,0);
 
-    public boolean isExtending()
-    {
-        return this.extending;
+        this.collision
+            .set(collision)
+            .translate(dx, dy, dz);
     }
 
-    public Direction getDirection()
-    {
-        return this.direction;
+    private final void translateAABBs(float scale) {
+        final float dx,dy,dz;
+        scale = (this.isExtending) ? scale : -scale;
+        dx = scale * this.direction.offsetX();
+        dy = scale * this.direction.offsetY();
+        dz = scale * this.direction.offsetZ();
+        this.bounds.translate(dx, dy, dz);
+        this.collision.translate(dx, dy, dz);
     }
 
-    public boolean isSourcePiston()
-    {
-        return this.isSourcePiston;
+    @Override
+    public void tick() {
+        this.progressPrev = this.progressCur;
+        if (this.progressCur == 0.0f) this.initAABBs();
+        if (this.progressCur > (1.0F - 0x1p-12F)) {
+            this.finalTick();
+        } else {
+            this.progressCur += PROGRESS_DELTA;
+            this.translateAABBs(PROGRESS_DELTA);
+            this.moveCollidedEntities(PROGRESS_DELTA + 0x1p-7F); 
+        }
     }
 
-    public float getProgress(float partialTick)
-    {
-        if(partialTick > 1.0F)
-        {
-            partialTick = 1.0F;
+    private final void moveCollidedEntities(float scale) {
+        assert this.worldObj != null;
+
+        final var dir = this.movingDirection();
+        final float dx,dy,dz;
+        dx = dir.offsetX() * scale;
+        dy = dir.offsetY() * scale;
+        dz = dir.offsetZ() * scale;
+
+        final int axis = dir.id >> 1; // y z x
+        final boolean neg = (dir.id & 1) == 0;
+        final double lim = 0x1p-4;
+
+        final var entities = this.worldObj.getEntitiesWithinAABBExcludingEntity(null, this.collision);
+        for (final @NotNull var e : entities) {
+            if (e.noPhysics) continue;
+            e.move(dx, dy, dz);
+            switch (axis) {
+                case 2 -> e.xd = (neg) ? Math.min(e.xd, -lim) : Math.max(e.xd, lim);
+                case 0 -> e.yd = (neg) ? Math.min(e.yd, -lim) : Math.max(e.yd, lim);
+                case 1 -> e.zd = (neg) ? Math.min(e.zd, -lim) : Math.max(e.zd, lim);
+            }
         }
-        return this.progressO + (this.progress - this.progressO) * partialTick;
     }
 
-    public float getXOff(float partialTick)
-    {
-        if(this.extending)
-        {
-            return (getProgress(partialTick) - 1.0F) * (float) this.direction.getOffsetX();
-        } else
-        {
-            return (1.0F - getProgress(partialTick)) * (float) this.direction.getOffsetX();
-        }
+    public float progress(float partialTick) {
+        partialTick = Math.min(partialTick, 1.0F);
+        return this.progressPrev + (this.progressCur - this.progressPrev) * partialTick;
     }
 
-    public float getYOff(float partialTick)
-    {
-        if(this.extending)
-        {
-            return (getProgress(partialTick) - 1.0F) * (float) this.direction.getOffsetY();
-        } else
-        {
-            return (1.0F - getProgress(partialTick)) * (float) this.direction.getOffsetY();
-        }
+    public float progressSigned(float partialTick) {
+        final var p = this.progress(partialTick);
+        return (this.isExtending)
+        ? p - 1.0F
+        : 1.0F - p;
     }
 
-    public float getZOff(float partialTick)
-    {
-        if(this.extending)
-        {
-            return (getProgress(partialTick) - 1.0F) * (float) this.direction.getOffsetZ();
-        } else
-        {
-            return (1.0F - getProgress(partialTick)) * (float) this.direction.getOffsetZ();
-        }
+    public float xOffset(float partialTick) {
+        return this.progressSigned(partialTick) * (float) this.direction.offsetX();
     }
 
-    private void moveCollidedEntities(float stretch, float force) {
-        assert this.worldObj != null;
-        if(!this.extending) {
-            stretch--;
-        } else {
-            stretch = 1.0F - stretch;
-        }
-        AABBdc aabb = Blocks.PISTON_MOVING.getLogic().getCollisionShapeFromTileEntity(this.worldObj, new TilePos(this.tilePos.x, this.tilePos.y, this.tilePos.z), this.movedBlock, stretch, this.direction);
-        if (aabb == null) return;
-
-        List<Entity> list = this.worldObj.getEntitiesWithinAABBExcludingEntity(null, aabb);
-        if(!list.isEmpty())
-        {
-            entityBuffer.addAll(list);
-            for (Entity entity : entityBuffer) {
-                if (entity == null || entity.noPhysics) continue;
-                entity.move(force * (float) this.direction.getOffsetX(), force * (float) this.direction.getOffsetY(), force * (float) this.direction.getOffsetZ());
-            }
-            entityBuffer.clear();
-        }
+    public float yOffset(float partialTick) {
+        return this.progressSigned(partialTick) * (float) this.direction.offsetY();
+    }
+    
+    public float zOffset(float partialTick) {
+        return this.progressSigned(partialTick) * (float) this.direction.offsetZ();
     }
 
     public void finalTick() {
-        TilePos selfPos = new TilePos(this.tilePos.x, this.tilePos.y, this.tilePos.z);
+        final var selfPos = new TilePos(this.tilePos);
         assert this.worldObj != null;
 
-        this.progressO = this.progress = 1.0F;
+        this.progressPrev = this.progressCur = 1.0F;
         this.worldObj.removeTileEntity(selfPos);
         invalidate();
-        if(this.worldObj.getBlockType(selfPos) == Blocks.PISTON_MOVING) {
-            this.worldObj.setBlockTypeDataRaw(selfPos, this.movedBlock, this.movedData);
-            if (this.movedEntity != null) {
-                TileEntity oldEnt = this.worldObj.getTileEntity(selfPos);
-                if (oldEnt != null) {
-                    oldEnt.invalidate();
-                }
-                this.movedEntity.validate();
-                this.movedEntity.tilePos.x = this.tilePos.x;
-                this.movedEntity.tilePos.y = this.tilePos.y;
-                this.movedEntity.tilePos.z = this.tilePos.z;
-                this.worldObj.replaceTileEntity(selfPos, this.movedEntity);
-            }
+        if (this.worldObj.getBlockType(selfPos) != Blocks.PISTON_MOVING) return;
+        this.worldObj.setBlockTypeDataRaw(selfPos, this.movingBlock, this.movingBlockData);
 
-            Block.disableNormalEntityLogic = true;
-            this.movedBlock.onPlacedByWorld(this.worldObj, selfPos);
-            Block.disableNormalEntityLogic = false;
-
-            assert this.movedEntity == null || this.worldObj.getTileEntity(selfPos) == this.movedEntity : "Piston failed to actually move entity!";
-            this.worldObj.notifyBlockChange(selfPos, this.movedBlock);
+        if (this.movingTileEntity != null) {
+            this.movingTileEntity.validate();
+            this.worldObj.replaceTileEntity(selfPos, this.movingTileEntity);
         }
-    }
+        
+        Block.disableNormalEntityLogic = true;
+        this.movingBlock.onPlacedByWorld(this.worldObj, selfPos);
+        Block.disableNormalEntityLogic = false;
 
-    @Override
-	public void tick()
-    {
-        this.progressO = this.progress;
-        if(this.progressO >= 1.0F)
-        {
-            moveCollidedEntities(1.0F, 0.25F);
-            finalTick();
-            return;
-        }
-        this.progress += 1f/TICKS_TO_EXTEND;
-        if(this.progress >= 0.999F)
-        {
-            this.progress = 1.0F;
-        }
-        if(this.extending)
-        {
-            moveCollidedEntities(this.progress, (this.progress - this.progressO) + 0.0625F);
-        }
+        assert this.movingTileEntity == null || this.worldObj.getTileEntity(selfPos) == this.movingTileEntity 
+            : "Piston failed to actually move entity!";
+
+        this.worldObj.notifyBlockChange(selfPos, this.movingBlock);
     }
 
     @Override
     public void readAdditionalData(@NotNull CompoundTag compoundTag) {
-        this.movedBlock = Objects.requireNonNullElse(Blocks.getBlock(compoundTag.getInteger("blockId")), Blocks.AIR);
-        this.movedData = compoundTag.getInteger("blockData");
+        this.movingBlock = Objects.requireNonNullElse(Blocks.getBlock(compoundTag.getInteger("blockId")), Blocks.AIR);
+        this.movingBlockData = compoundTag.getInteger("blockData");
         if (compoundTag.containsKey("tileEntity")) {
             CompoundTag entityTag = compoundTag.getCompound("tileEntity");
-            this.movedEntity = TileEntityDispatcher.createAndLoadEntity(entityTag);
+            this.movingTileEntity = TileEntityDispatcher.createAndLoadEntity(entityTag);
         }
-        this.direction = Direction.getDirectionById(compoundTag.getInteger("facing"));
-        this.progressO = this.progress = compoundTag.getFloat("progress");
-        this.extending = compoundTag.getBoolean("extending");
+        this.direction = Direction.fromId(compoundTag.getInteger("facing"));
+        this.progressPrev = this.progressCur = compoundTag.getFloat("progress");
+        this.isExtending = compoundTag.getBoolean("extending");
     }
 
     @Override
-    public void writeAdditionalData(@NotNull CompoundTag compoundTag)
-    {
-        compoundTag.putInt("blockId", this.movedBlock.id());
-        compoundTag.putInt("blockData", this.movedData);
-        if (this.movedEntity != null) {
+    public void writeAdditionalData(@NotNull CompoundTag compoundTag) {
+        compoundTag.putInt("blockId", this.movingBlock.id());
+        compoundTag.putInt("blockData", this.movingBlockData);
+        if (this.movingTileEntity != null) {
             CompoundTag entityTag = new CompoundTag();
-            this.movedEntity.writeToNBT(entityTag);
+            this.movingTileEntity.writeToNBT(entityTag);
             compoundTag.put("tileEntity", entityTag);
         }
-        compoundTag.putInt("facing", this.direction.getId());
-        compoundTag.putFloat("progress", this.progressO);
-        compoundTag.putBoolean("extending", this.extending);
+        compoundTag.putInt("facing", this.direction.id);
+        compoundTag.putFloat("progress", this.progressPrev);
+        compoundTag.putBoolean("extending", this.isExtending);
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/support/ISupportable.java b/game/core/src/main/java/net/minecraft/core/block/support/ISupportable.java
index 909a160f3..660fc08ea 100644
--- a/game/core/src/main/java/net/minecraft/core/block/support/ISupportable.java
+++ b/game/core/src/main/java/net/minecraft/core/block/support/ISupportable.java
@@ -6,19 +6,14 @@ import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 public interface ISupportable {
     @NotNull ISupport getSupportConstraint(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side);
 
     default boolean isSupported(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side) {
-        final @Nullable Block<?> supportingBlock = world.getBlockType(tilePos.add(side.getDirection(), new TilePos()));
-        if (supportingBlock == null) {
-            return false;
-        }
-
-        final @NotNull ISupport supporting = supportingBlock.getSupport(world, tilePos.add(side.getDirection(), new TilePos()), side.getOpposite());
+        final @NotNull Block<?> supportingBlock = world.getBlockType(tilePos.add(side.direction(), new TilePos()));
+        final @NotNull ISupport supporting = supportingBlock.getSupport(world, tilePos.add(side.direction(), new TilePos()), side.opposite());
         final @NotNull ISupport supported = this.getSupportConstraint(world, tilePos, side);
-        return supporting.canSupport(supported, side.getOpposite());
+        return supporting.canSupport(supported, side.opposite());
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/tag/BlockTags.java b/game/core/src/main/java/net/minecraft/core/block/tag/BlockTags.java
index 2a8d1432f..5f4b11937 100644
--- a/game/core/src/main/java/net/minecraft/core/block/tag/BlockTags.java
+++ b/game/core/src/main/java/net/minecraft/core/block/tag/BlockTags.java
@@ -178,7 +178,7 @@ public abstract class BlockTags
      */
     public static final Tag<Block<?>> NETHER_MOBS_SPAWN = Tag.of("nether_mobs_spawn");
     /**
-     * Will drop block when piston extends into it
+     * Will drop block when piston push blocks into it
      */
     public static final Tag<Block<?>> PISTON_CRUSHING = Tag.of("piston_crushing");
     /**
diff --git a/game/core/src/main/java/net/minecraft/core/current/util/BlockState.java b/game/core/src/main/java/net/minecraft/core/current/util/BlockState.java
index a85af8cec..044e534f0 100644
--- a/game/core/src/main/java/net/minecraft/core/current/util/BlockState.java
+++ b/game/core/src/main/java/net/minecraft/core/current/util/BlockState.java
@@ -62,11 +62,11 @@ public class BlockState {
 	}
 
 	public boolean hasSignal(World world, TilePosc pos, Direction dir) {
-		return block.isEmittingSignal(world, pos, dir.getSide());
+		return block.isEmittingSignal(world, pos, dir.side());
 	}
 
 	public boolean hasDirectSignal(World world, TilePosc pos, Direction dir) {
-		return block.isEmittingDirectSignal(world, pos, dir.getSide());
+		return block.isEmittingDirectSignal(world, pos, dir.side());
 	}
 
 	public boolean canSurvive(World world, TilePosc pos) {
diff --git a/game/core/src/main/java/net/minecraft/core/current/wire/ConduitHandler.java b/game/core/src/main/java/net/minecraft/core/current/wire/ConduitHandler.java
index 6db86a5c8..2e53c3f8d 100644
--- a/game/core/src/main/java/net/minecraft/core/current/wire/ConduitHandler.java
+++ b/game/core/src/main/java/net/minecraft/core/current/wire/ConduitHandler.java
@@ -89,7 +89,7 @@ public class ConduitHandler {
                 for (Side side : BlockLogicConduit.getOpenSides(data)) {
                     TilePos nPos = new TilePos(pos).add(side);
                     if (world.getBlockType(nPos) == Blocks.CONDUIT &&
-                            BlockLogicConduit.isSideOpen(world.getBlockData(nPos), side.getOpposite())) {
+                            BlockLogicConduit.isSideOpen(world.getBlockData(nPos), side.opposite())) {
                         toSearch.add(nPos);
                     }
                 }
@@ -162,10 +162,8 @@ public class ConduitHandler {
             if (oldPower != newPower) {
                 int state = oldData & BlockLogicConduit.STATE_MASK;
 
-                //suppress recursive neighbor updates while data is being set
-                world.noNeighborUpdate = true;
-                world.setBlockDataNotify(tilePos, (newPower << BlockLogicConduit.POWER_SHIFT) | state);
-                world.noNeighborUpdate = false;
+                world.setBlockData(tilePos, (newPower << BlockLogicConduit.POWER_SHIFT) | state);
+                world.markBlockNeedsUpdate(tilePos);
 
                 //queue updates ONLY for blocks facing the open sides that ARE NOT conduits in this network
                 for (Side side : BlockLogicConduit.getOpenSides(state)) {
diff --git a/game/core/src/main/java/net/minecraft/core/current/wire/WireHandler.java b/game/core/src/main/java/net/minecraft/core/current/wire/WireHandler.java
index ec81ad39e..9565b40a6 100644
--- a/game/core/src/main/java/net/minecraft/core/current/wire/WireHandler.java
+++ b/game/core/src/main/java/net/minecraft/core/current/wire/WireHandler.java
@@ -808,7 +808,7 @@ public class WireHandler {
             default -> wireSide;
         };
 
-        return wireSide.getOpposite();
+        return wireSide.opposite();
     }
 
     /**
diff --git a/game/core/src/main/java/net/minecraft/core/current/wire/WorldHelper.java b/game/core/src/main/java/net/minecraft/core/current/wire/WorldHelper.java
index 5c61fc059..efc3de215 100644
--- a/game/core/src/main/java/net/minecraft/core/current/wire/WorldHelper.java
+++ b/game/core/src/main/java/net/minecraft/core/current/wire/WorldHelper.java
@@ -53,10 +53,11 @@ public class WorldHelper {
 
         world.setBlockData(pos, metadata);
 
-        // notify clients of the BlockState change
-        world.notifyBlockChange(pos, block);
-        // mark the chunk for saving
-        world.markBlockDirty(pos);
+        // // notify clients of the BlockState change
+        // world.notifyBlockChange(pos, block);
+        world.markBlockNeedsUpdate(pos);
+        // // mark the chunk for saving // is this actually true?
+        // world.markBlockDirty(pos);
 
         return true;
     }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/Entity.java b/game/core/src/main/java/net/minecraft/core/entity/Entity.java
index cfaba6e12..0f22991d0 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/Entity.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/Entity.java
@@ -866,7 +866,7 @@ public abstract class Entity
         double dx = this.x - x;
         double dy = this.y - y;
         double dz = this.z - z;
-        return MathHelper.sqrt(dx * dx + dy * dy + dz * dz);
+        return Math.sqrt(dx * dx + dy * dy + dz * dz);
     }
 
     public double distanceToSqr(@NotNull Entity entity) {
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 4b483ea9f..d6577933a 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
@@ -85,17 +85,20 @@ public class EntityFallingBlock extends Entity
     @Override
 	public void tick()
     {
-        if(this.carriedBlock.blockId == 0)
-        {
+        final boolean isClient = this.world.isClientSide;
+
+        if(this.carriedBlock.blockId == 0) {
             remove();
             return;
         }
+
         this.pushesThisTick = 0;
         this.pushTime *= 0.98f;
         if (this.pushTime < 0.05f || (this.pushTime < 0.25 && this.onGround)){
             this.pushTime = 0;
         }
-        if (isOnFire() && this.carriedBlock.blockId == Blocks.TNT.id()) {
+        
+        if (!isClient && this.carriedBlock.blockId == Blocks.TNT.id() && isOnFire()) {
             remove();
             EntityPrimedTNT entityPrimedTNT = new EntityPrimedTNT(this.world, this.x, this.y, this.z);
             entityPrimedTNT.xd = this.xd;
@@ -105,6 +108,7 @@ public class EntityFallingBlock extends Entity
             this.world.entityJoinedWorld(entityPrimedTNT);
             return;
         }
+
         this.xo = this.x;
         this.yo = this.y;
         this.zo = this.z;
@@ -116,12 +120,16 @@ public class EntityFallingBlock extends Entity
         this.xd *= 0.98D;
         this.yd *= 0.98D;
         this.zd *= 0.98D;
-        TilePosc blockPos = roundedPos();
-        if(this.world.getBlockType(blockPos).id() == this.carriedBlock.blockId && !this.hasRemovedBlock)
-        {
+
+        final TilePosc blockPos = this.tilePosApprox();
+
+        // TODO tbh why are we using this hacky way of removing the block, should probably 
+        // TODO   just remove directly from within block logic :\
+        if (!isClient && this.world.getBlockType(blockPos).id() == this.carriedBlock.blockId && !this.hasRemovedBlock) {
             this.world.setBlockTypeNotify(blockPos, Blocks.AIR);
             this.hasRemovedBlock = true;
         }
+
         if (this.onGround) {
             Block<?> selfBlock = this.carriedBlock.block();
             Block<?> blockBelow = this.world.getBlockType(blockPos.down(new TilePos()));
@@ -140,53 +148,70 @@ public class EntityFallingBlock extends Entity
             this.pushTime *= (float) friction;
         }
 
-        double v = Math.hypot(this.xd, this.zd);
-        if (v < 0.001 || isInWall()) {
-            if(this.onGround || isInWall())
-            {
-                Entity rider = getPassenger();
-                ejectRider();
-                remove();
-                if(
-                    (
-                        this.fallChecker.canFallInto(this.world, this.carriedBlock, blockPos.down(new TilePos())) ||
-                        !this.world.setBlockType(blockPos, Blocks.getBlock(this.carriedBlock.blockId))
-                    ) && !this.world.isClientSide
-                ) {
-                    if(this.hasRemovedBlock) {
-                        drop();
-                    }
-                } else if (!this.world.isClientSide) {
-                    this.world.setBlockData(blockPos, this.carriedBlock.metadata);
-                    if (this.carriedBlock.entity != null) {
-                        TileEntity oldEnt = this.world.getTileEntity(blockPos);
-                        if (oldEnt != null) {
-                            oldEnt.invalidate();
-                        }
-                        this.carriedBlock.entity.validate();
-                        this.carriedBlock.entity.tilePos.x = blockPos.x();
-                        this.carriedBlock.entity.tilePos.y = blockPos.y();
-                        this.carriedBlock.entity.tilePos.z = blockPos.z();
-                        this.carriedBlock.entity.worldObj = this.world;
-                        this.carriedBlock.entity.carriedBlock = null;
-                        this.world.replaceTileEntity(blockPos, this.carriedBlock.entity);
-                    }
-                    this.world.notifyBlockChange(blockPos, Blocks.getBlock(this.carriedBlock.blockId));
+        if (!isClient) check_removal: {
+            final boolean isInWall = this.isInWall();
+            if (!isInWall) {
+                if (Math.hypot(this.xd, this.zd) >= 0x1p-4D) break check_removal;
+                if (!this.onGround) {
+                    if (this.fallTime <= 600) break check_removal;
+                    if (this.hasRemovedBlock) this.drop();
+                    this.ejectRider();
+                    this.remove();
+                    break check_removal;
                 }
-                if (rider != null) {
-                    TileEntity te = this.world.getTileEntity(blockPos);
-                    if (te instanceof IVehicle vehicle) rider.startRiding(vehicle);
+            }
+            final var passenger = getPassenger();
+            this.ejectRider();
+            this.remove();
+
+            // moving .hasRemovedBlock here fixes gravity block dupe
+            // note that it still creates duplicated falling block entity, but it cannot be
+            // turned into actual block (or item) anymore
+            if (this.hasRemovedBlock) fall_or_drop: {
+
+                // if can be placed at position, AND cannot replace position bellow (floored), try to place; 
+                // otherwise drop instead.
+                // 
+                // this causes the interesting behavior of falling block when falling onto 
+                // fence(gate) / boat, where they drop if the block above the fence, or the block
+                // the boat is in, is air, and place even if there's a torch above the fence / in the boat.
+                // 
+                // this should be changed if we want something more intuitive.
+                final boolean shoudPlaceHere = 
+                    !this.fallChecker.canFallInto(this.world, this.carriedBlock, blockPos.down(new TilePos()));
+
+                final boolean isPlaced = shoudPlaceHere &&
+                    this.world.setBlockType(blockPos, Blocks.getBlock(this.carriedBlock.blockId));
+
+                if (!isPlaced) {
+                    this.drop();
+                    break fall_or_drop;
                 }
-            } else if(this.fallTime > 600 && !this.world.isClientSide) {
-                if(this.hasRemovedBlock) {
-                    drop();
+
+                this.world.setBlockData(blockPos, this.carriedBlock.metadata);
+
+                final var tileEntity = this.carriedBlock.entity;
+                if (tileEntity != null) {
+                    tileEntity.validate();
+                    tileEntity.tilePos.set(blockPos);
+                    tileEntity.worldObj = this.world;
+                    tileEntity.carriedBlock = null;
+                    this.world.replaceTileEntity(blockPos, tileEntity);
+
+                    if (passenger != null && tileEntity instanceof IVehicle vehicle) {
+                        passenger.startRiding(vehicle);
+                    }
                 }
-                ejectRider();
-                remove();
+
+                this.world.notifyBlockChange(blockPos, this.carriedBlock.block());
             }
 
-            this.carriedBlock.heldTick(this.world, this);
+            // we used to call heldTick even after falling block is removed.
+            // was that _intended_?
+            return;
         }
+
+        this.carriedBlock.heldTick(this.world, this);
     }
 
     public void drop() {
@@ -199,10 +224,8 @@ public class EntityFallingBlock extends Entity
                 }
             }
         }
-        if (this.carriedBlock.entity != null) {
-            TilePosc blockPos = roundedPos();
-            this.carriedBlock.entity.dropContents(this.world, blockPos.x(), blockPos.y(), blockPos.z());
-        }
+        final var bpos = this.tilePosApprox();
+        if (this.carriedBlock.entity != null) this.carriedBlock.entity.dropContents(world, bpos.x(), bpos.y(), bpos.z());;
     }
 
     @Override
@@ -272,7 +295,7 @@ public class EntityFallingBlock extends Entity
         return 0.0D;
     }
 
-    public @NotNull TilePosc roundedPos() {
+    public @NotNull TilePosc tilePosApprox() {
         return new TilePos(MathHelper.round(this.x - 0.5), MathHelper.floor(this.y), MathHelper.round(this.z - 0.5));
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/EntityPainting.java b/game/core/src/main/java/net/minecraft/core/entity/EntityPainting.java
index 6cbeeb3fa..7913026e0 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/EntityPainting.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/EntityPainting.java
@@ -272,9 +272,9 @@ public class EntityPainting extends Entity {
                 break;
             }
         }
-        final double x = this.x + d.getOffsetX() / 4d;
-        final double y = this.y + d.getOffsetY() / 4d;
-        final double z = this.z + d.getOffsetZ() / 4d;
+        final double x = this.x + d.offsetX() / 4d;
+        final double y = this.y + d.offsetY() / 4d;
+        final double z = this.z + d.offsetZ() / 4d;
         this.world.entityJoinedWorld(new EntityItem(this.world, x, y, z, new ItemStack(Items.PAINTING)));
         if (getBorderStack() != null) {
             this.world.entityJoinedWorld(new EntityItem(this.world, x, y, z, getBorderStack()));
diff --git a/game/core/src/main/java/net/minecraft/core/entity/Mob.java b/game/core/src/main/java/net/minecraft/core/entity/Mob.java
index 6b58551ce..6f73ebaa1 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/Mob.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/Mob.java
@@ -1195,9 +1195,9 @@ public abstract class Mob extends Entity {
             mode = this.placementModeOverride;
         }
         if (mode == PlacementMode.SIDE && side != null && side.isHorizontal()) {
-            return side.getDirection().getOpposite();
+            return side.direction().opposite();
         }
-        return Direction.getHorizontalDirection(this);
+        return Direction.getHorizontalLockable(this);
     }
 
     public @NotNull Direction getPlacementDirection(final @NotNull Side side) {
@@ -1212,9 +1212,9 @@ public abstract class Mob extends Entity {
             mode = this.placementModeOverride;
         }
         if (mode == PlacementMode.SIDE && side != null) {
-            return side.getDirection();
+            return side.direction();
         }
-        return Direction.getDirection(this);
+        return Direction.getLockable(this);
     }
 
     public Direction getVerticalPlacementDirection(Side side, double sideHeight) {
@@ -1230,7 +1230,7 @@ public abstract class Mob extends Entity {
         }
         if (mode == PlacementMode.SIDE) {
             if (side.isVertical()) {
-                return side.getDirection().getOpposite();
+                return side.direction().opposite();
             } else {
                 return sideHeight > 0.5 ? Direction.UP : Direction.DOWN;
             }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileCannonballBouncy.java b/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileCannonballBouncy.java
index 6eaeced84..94e8901d1 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileCannonballBouncy.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileCannonballBouncy.java
@@ -58,19 +58,19 @@ public class ProjectileCannonballBouncy extends ProjectileCannonball {
                     this.zd = 0f;
                     return;
                 }
-                if (side.getAxis() == Axis.Y) {
+                if (side.axis() == Axis.Y) {
                     this.yd = -this.yd * bounceAxisScale;
                     this.xd *= otherAxisScale;
                     this.zd *= otherAxisScale;
                     this.world.playSoundAtEntity(null, this, "random.explode", 0.25f * velocity, 2.0f);
                 }
-                if (side.getAxis() == Axis.X) {
+                if (side.axis() == Axis.X) {
                     this.xd = -this.xd * bounceAxisScale;
                     this.yd *= otherAxisScale;
                     this.zd *= otherAxisScale;
                     this.world.playSoundAtEntity(null, this, "random.explode", 0.25f * velocity, 2.0f);
                 }
-                if (side.getAxis() == Axis.Z) {
+                if (side.axis() == Axis.Z) {
                     this.zd = -this.zd * bounceAxisScale;
                     this.xd *= otherAxisScale;
                     this.yd *= otherAxisScale;
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemBed.java b/game/core/src/main/java/net/minecraft/core/item/ItemBed.java
index 23cdecf3c..4f8df4f8c 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemBed.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemBed.java
@@ -23,33 +23,34 @@ public class ItemBed extends Item {
         if (selfStack.stackSize <= 0) return false;
         TilePosc pos1 = blockPos;
         if (!world.canPlaceInsideBlock(pos1)) {
-            pos1 = new TilePos(pos1).add(side.getDirection());
+            pos1 = new TilePos(pos1).add(side.direction());
         }
-        Direction dir = player == null ? Direction.NORTH : player.getHorizontalPlacementDirection(null).getOpposite();
+        Direction dir = player == null ? Direction.NORTH : player.getHorizontalPlacementDirection(null).opposite();
         final TilePos pos2 = new TilePos(pos1).sub(dir);
         
         if (!world.canPlaceInsideBlock(pos1) || !world.canPlaceInsideBlock(pos2)) return false;
         
-        final TilePos qpos = new TilePos();
-        if (!world.canPlaceOnSurfaceOfBlock(qpos.set(pos1).down())) return false;
-        if (!world.canPlaceOnSurfaceOfBlock(qpos.set(pos2).down())) return false;
+        final var bed = Blocks.BED.getLogic();
+        if (!bed.isSupported(world, pos1, Side.BOTTOM)) return false;
+        if (!bed.isSupported(world, pos2, Side.BOTTOM)) return false;
         
         if (!world.canBlockIdBePlacedAt(Blocks.BED.id(), pos1, false, side)) return false;
-        // if (!world.canBlockIdBePlacedAt(Blocks.BED.id(), pos2, false, side)) return false;
+        if (!world.canBlockIdBePlacedAt(Blocks.BED.id(), pos2, false, side)) return false;
         
         
         final boolean isPlaced1 = 
-        world.setBlockTypeData(pos1, Blocks.BED, dir.getHorizontalIndex());
+        world.setBlockTypeData(pos1, Blocks.BED, dir.legacyHorizontalIndex());
         final boolean isPlaced2 = isPlaced1 &&
-        world.setBlockTypeData(pos2, Blocks.BED, dir.getHorizontalIndex() | BlockLogicBed.IS_HEAD.set(0, 1));
+        world.setBlockTypeData(pos2, Blocks.BED, dir.legacyHorizontalIndex() | BlockLogicBed.IS_HEAD.set(0, 1));
         if (!isPlaced2) {
             if (isPlaced1) world.setBlockType(pos1, Blocks.AIR);
             return false;
         }
         selfStack.consumeItem(player);
 
-        world.notifyBlockChange(pos1, Blocks.BED);
-        world.notifyBlockChange(pos2, Blocks.BED);
+        world.markBlockNeedsUpdate(pos1);
+        world.markBlockNeedsUpdate(pos2);
+        world.notifyBlocksInCapsuleOfNeighborChange(dir.opposite(), pos1, Blocks.BED);
 
         world.playBlockSoundEffect(player, pos1.x() + 0.5F, pos1.y() + 0.5F, pos1.z() + 0.5F, Blocks.BED, EnumBlockSoundEffectType.PLACE);
         return true;
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemBoat.java b/game/core/src/main/java/net/minecraft/core/item/ItemBoat.java
index 95329ef10..0d0fcfcc6 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemBoat.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemBoat.java
@@ -44,9 +44,9 @@ public class ItemBoat extends Item {
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        double x = blockPos.x() + offX + direction.getOffsetX();
-        double y = blockPos.y() + offY + direction.getOffsetY();
-        double z = blockPos.z() + offZ + direction.getOffsetZ();
+        double x = blockPos.x() + offX + direction.offsetX();
+        double y = blockPos.y() + offY + direction.offsetY();
+        double z = blockPos.z() + offZ + direction.offsetZ();
         selfStack.consumeItem(null);
         EntityBoat boat = new EntityBoat(world, x, y, z);
         world.entityJoinedWorld(boat);
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java b/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java
index 8d9bcfeb3..201bb20eb 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java
@@ -248,9 +248,9 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
         Block<?> blockAtPos = world.getBlockType(rayCastPos);
         if (!blockAtPos.hasTag(BlockTags.PLACE_OVERWRITES) && !blockAtPos.hasTag(BlockTags.BROKEN_BY_FLUIDS)) {
             Side side = hitTile.side;
-            rayCastPos.x += side.getOffsetX();
-            rayCastPos.y += side.getOffsetY();
-            rayCastPos.z += side.getOffsetZ();
+            rayCastPos.x += side.offsetX();
+            rayCastPos.y += side.offsetY();
+            rayCastPos.z += side.offsetZ();
         }
 
         if (rayCastPos.y < 0 || rayCastPos.y >= world.getHeightBlocks()) return itemStack;
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemDoor.java b/game/core/src/main/java/net/minecraft/core/item/ItemDoor.java
index d6fb92c73..fc9c59284 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemDoor.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemDoor.java
@@ -33,22 +33,24 @@ public class ItemDoor extends Item {
     public boolean onUseOnBlock(@NotNull ItemStack selfStack, @NotNull World world, @Nullable Player player, @NotNull TilePosc blockPos, @NotNull Side side, double xHit, double yHit) {
         if (player == null) return false;
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
 
-        Direction leftDir = player.getHorizontalPlacementDirection(side).rotate(3);
-        int meta = leftDir.getHorizontalIndex();
+        if (!world.canBlockIdBePlacedAt(this.doorBlockBottom.id(), blockPos, false, side)) return false;
+
+        Direction leftDir = player.getHorizontalPlacementDirection(side).rotateY(1);
+        int meta = leftDir.legacyHorizontalIndex();
 
         int isSolidBlockLeft = (isSupported(world, blockPos, leftDir, false) ? 1 : 0) + (isSupported(world, blockPos.up(new TilePos()), leftDir, false) ? 1 : 0);
-        int isSolidBlockRight = (isSupported(world, blockPos, leftDir.getOpposite(), true) ? 1 : 0) + (isSupported(world, blockPos.up(new TilePos()), leftDir.getOpposite(), true) ? 1 : 0);
+        int isSolidBlockRight = (isSupported(world, blockPos, leftDir.opposite(), true) ? 1 : 0) + (isSupported(world, blockPos.up(new TilePos()), leftDir.opposite(), true) ? 1 : 0);
         boolean isDoorLeft = world.getBlockType(blockPos.add(leftDir, new TilePos())) == this.doorBlockBottom || world.getBlockType(blockPos.add(leftDir, new TilePos()).up(new TilePos())) == this.doorBlockTop;
         boolean isDoorRight = world.getBlockType(blockPos.sub(leftDir, new TilePos())) == this.doorBlockBottom || world.getBlockType(blockPos.sub(leftDir, new TilePos()).up(new TilePos())) == this.doorBlockTop;
         boolean isMirrored = false;
 
         if (isDoorLeft && !isDoorRight) {
             isMirrored = (world.getBlockData(blockPos.add(leftDir, new TilePos())) & BlockLogicDoor.MASK_HINGE) == 0;
-        } else if (isSolidBlockLeft <= 0 || side.getDirection() != leftDir.getOpposite()) {
-            if (isSolidBlockRight > 0 && side.getDirection() == leftDir) {
+        } else if (isSolidBlockLeft <= 0 || side.direction() != leftDir.opposite()) {
+            if (isSolidBlockRight > 0 && side.direction() == leftDir) {
                 isMirrored = true;
             } else if (isSolidBlockRight > isSolidBlockLeft) {
                 isMirrored = true;
@@ -72,16 +74,17 @@ public class ItemDoor extends Item {
         this.doorBlockBottom.onPlacedByMob(world, blockPos, side, player, xHit, yHit);
         this.doorBlockTop.onPlacedByMob(world, blockPos.up(new TilePos()), side, player, xHit, yHit);
 
-        world.notifyBlockChange(blockPos, this.doorBlockBottom);
-        world.notifyBlockChange(blockPos.up(new TilePos()), this.doorBlockTop);
+        // this is technically incorrect as both positions are updated as bottom door,
+        // but does it really matter?
+        world.notifyBlocksInCapsuleOfNeighborChange(Direction.UP, blockPos, this.doorBlockBottom);
 
         world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, this.doorBlockBottom, EnumBlockSoundEffectType.PLACE);
         return true;
     }
 
     public boolean isSupported(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Direction direction, boolean mirrored) {
-        ISupport support = world.getSupport(tilePos.add(direction, new TilePos()), direction.getSide().getOpposite());
-        return support.canSupport(mirrored ? PartialSupport.INSTANCE.right() : PartialSupport.INSTANCE.left(), direction.getSide());
+        ISupport support = world.getSupport(tilePos.add(direction, new TilePos()), direction.opposite().side());
+        return support.canSupport(mirrored ? PartialSupport.INSTANCE.right() : PartialSupport.INSTANCE.left(), direction.side());
     }
 
     @Override
@@ -91,18 +94,18 @@ public class ItemDoor extends Item {
         }
 
         if (!direction.isHorizontal()) direction = Direction.NORTH;
-        Direction lefDir = direction.rotate(3);
-        int meta = lefDir.getHorizontalIndex();
+        Direction lefDir = direction.rotateY(1);
+        int meta = lefDir.legacyHorizontalIndex();
 
         int isSolidBlockLeft = (isSupported(world, blockPos, lefDir, false) ? 1 : 0) + (isSupported(world, blockPos.up(new TilePos()), lefDir, false) ? 1 : 0);
-        int isSolidBlockRight = (isSupported(world, blockPos, lefDir.getOpposite(), true) ? 1 : 0) + (isSupported(world, blockPos.up(new TilePos()), lefDir.getOpposite(), true) ? 1 : 0);
+        int isSolidBlockRight = (isSupported(world, blockPos, lefDir.opposite(), true) ? 1 : 0) + (isSupported(world, blockPos.up(new TilePos()), lefDir.opposite(), true) ? 1 : 0);
         boolean isDoorLeft = world.getBlockType(blockPos.add(lefDir, new TilePos())) == this.doorBlockBottom || world.getBlockType(blockPos.add(lefDir, new TilePos()).up(new TilePos())) == this.doorBlockTop;
         boolean isDoorRight = world.getBlockType(blockPos.sub(lefDir, new TilePos())) == this.doorBlockBottom || world.getBlockType(blockPos.sub(lefDir, new TilePos()).up(new TilePos())) == this.doorBlockTop;
         boolean isMirrored = false;
 
         if (isDoorLeft && !isDoorRight) {
             isMirrored = (world.getBlockData(blockPos.add(lefDir, new TilePos())) & BlockLogicDoor.MASK_HINGE) == 0;
-        } else if (isSolidBlockLeft <= 0 || direction != lefDir.getOpposite()) {
+        } else if (isSolidBlockLeft <= 0 || direction != lefDir.opposite()) {
             if (isSolidBlockRight > 0 && direction == lefDir) {
                 isMirrored = true;
             } else if (isSolidBlockRight > isSolidBlockLeft) {
@@ -121,13 +124,15 @@ public class ItemDoor extends Item {
 
         world.setBlockTypeData(blockPos, this.doorBlockBottom, meta);
         world.setBlockTypeData(blockPos.up(new TilePos()), this.doorBlockTop, meta);
+        world.markBlockNeedsUpdate(blockPos);
+        world.markBlockNeedsUpdate(blockPos.up(new TilePos()));
+
         selfStack.consumeItem(null);
 
-        this.doorBlockBottom.onPlacedOnSide(world, blockPos, direction.getSide(), 0.5, 0.5);
-        this.doorBlockTop.onPlacedOnSide(world, blockPos.up(new TilePos()), direction.getSide(), 0.5, 0.5);
+        this.doorBlockBottom.onPlacedOnSide(world, blockPos, direction.side(), 0.5, 0.5);
+        this.doorBlockTop.onPlacedOnSide(world, blockPos.up(new TilePos()), direction.side(), 0.5, 0.5);
 
-        world.notifyBlockChange(blockPos, this.doorBlockBottom);
-        world.notifyBlockChange(blockPos.up(new TilePos()), this.doorBlockTop);
+        world.notifyBlocksInCapsuleOfNeighborChange(Direction.UP, blockPos, this.doorBlockBottom);
         
         world.playBlockSoundEffect(null, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, this.doorBlockBottom, EnumBlockSoundEffectType.PLACE);
     }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemDye.java b/game/core/src/main/java/net/minecraft/core/item/ItemDye.java
index 6a748984f..fd5015b4f 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemDye.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemDye.java
@@ -142,7 +142,7 @@ public class ItemDye extends Item {
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        onUseOnBlock(selfStack, world, null, blockPos.add(direction, new TilePos()), direction.getSide(), 0.5, 0.5);
+        onUseOnBlock(selfStack, world, null, blockPos.add(direction, new TilePos()), direction.side(), 0.5, 0.5);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemEgg.java b/game/core/src/main/java/net/minecraft/core/item/ItemEgg.java
index 387d2b673..b4db79d49 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemEgg.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemEgg.java
@@ -31,7 +31,7 @@ public class ItemEgg extends Item implements IDispensable {
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
         ProjectileEgg projectileEgg = new ProjectileEgg(world, blockPos.x() + offX, blockPos.y() + offY, blockPos.z() + offZ);
-        projectileEgg.setHeading(direction.getOffsetX() * 0.6, direction.getOffsetY() == 0 ? 0.1 : direction.getOffsetY() * 0.6, direction.getOffsetZ() * 0.6f, 1.1F, 6F);
+        projectileEgg.setHeading(direction.offsetX() * 0.6, direction.offsetY() == 0 ? 0.1 : direction.offsetY() * 0.6, direction.offsetZ() * 0.6f, 1.1F, 6F);
         world.entityJoinedWorld(projectileEgg);
         selfStack.stackSize -= 1;
     }
@@ -40,7 +40,7 @@ public class ItemEgg extends Item implements IDispensable {
     public void onDispensed(@NotNull ItemStack selfStack, @NotNull World world, @NotNull Random random, @NotNull Direction direction, double x, double y, double z) {
         if (selfStack.consumeItem(null)) {
             ProjectileEgg projectileEgg = new ProjectileEgg(world, x, y, z);
-            projectileEgg.setHeading(direction.getOffsetX(), direction.getOffsetY() + 0.1D, direction.getOffsetZ(), 1.1F, 6F);
+            projectileEgg.setHeading(direction.offsetX(), direction.offsetY() + 0.1D, direction.offsetZ(), 1.1F, 6F);
             world.entityJoinedWorld(projectileEgg);
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemFireStriker.java b/game/core/src/main/java/net/minecraft/core/item/ItemFireStriker.java
index d4204c0b5..da56e8899 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemFireStriker.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemFireStriker.java
@@ -29,7 +29,7 @@ public class ItemFireStriker extends Item {
 
     @Override
     public boolean onUseOnBlock(@NotNull ItemStack selfStack, @NotNull World world, @Nullable Player player, @NotNull TilePosc blockPos, @NotNull Side side, double xHit, double yHit) {
-        blockPos = blockPos.add(side.getDirection(), new TilePos());
+        blockPos = blockPos.add(side.direction(), new TilePos());
         if (world.isAirBlock(blockPos)) {
             if (world.setBlockTypeNotify(blockPos, Blocks.FIRE)) {
                 world.playSoundEffect(player, SoundCategory.WORLD_SOUNDS, (double) blockPos.x() + 0.5D, (double) blockPos.y() + 0.5D, (double) blockPos.z() + 0.5D, "fire.ignite", 1.0F, itemRand.nextFloat() * 0.4F + 0.8F);
@@ -43,7 +43,7 @@ public class ItemFireStriker extends Item {
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        if (!onUseOnBlock(selfStack, world, null, blockPos, direction.getSide(), 0.5, 0.5)) {
+        if (!onUseOnBlock(selfStack, world, null, blockPos, direction.side(), 0.5, 0.5)) {
             blockPos = blockPos.add(direction, new TilePos());
             TilePos queryPos = new TilePos();
             Block<?> b = world.getBlockType(blockPos);
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemFlag.java b/game/core/src/main/java/net/minecraft/core/item/ItemFlag.java
index c00177e8f..6dfaea5f8 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemFlag.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemFlag.java
@@ -57,7 +57,7 @@ public class ItemFlag extends Item {
         }
 
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
         if (blockPos.y() < 0 || blockPos.y() >= world.getHeightBlocks()) {
             return false;
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemMinecart.java b/game/core/src/main/java/net/minecraft/core/item/ItemMinecart.java
index ffa78f320..bbb77458e 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemMinecart.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemMinecart.java
@@ -41,6 +41,6 @@ public class ItemMinecart extends Item {
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        onUseOnBlock(selfStack, world, null, blockPos.add(direction, new TilePos()), direction.getSide(), 0.5, 0.5);
+        onUseOnBlock(selfStack, world, null, blockPos.add(direction, new TilePos()), direction.side(), 0.5, 0.5);
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemPaintBrush.java b/game/core/src/main/java/net/minecraft/core/item/ItemPaintBrush.java
index 8b969fc60..4fbc6bc9b 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemPaintBrush.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemPaintBrush.java
@@ -66,7 +66,7 @@ public class ItemPaintBrush extends Item {
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
         blockPos = blockPos.add(direction, new TilePos());
-        if (!onUseOnBlock(selfStack, world, null, blockPos, direction.getSide(), 0.5, 0.5)) {
+        if (!onUseOnBlock(selfStack, world, null, blockPos, direction.side(), 0.5, 0.5)) {
             AABBdc box = new AABBd(blockPos.x(), blockPos.y(), blockPos.z(), blockPos.x() + 1, blockPos.y() + 1, blockPos.z() + 1);
             List<MobSheep> entities = world.getEntitiesWithinAABB(MobSheep.class, box);
             if (!entities.isEmpty()) {
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemPebble.java b/game/core/src/main/java/net/minecraft/core/item/ItemPebble.java
index 663c88b78..1cecb614d 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemPebble.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemPebble.java
@@ -53,12 +53,12 @@ public class ItemPebble extends Item implements IDispensable {
             return false;
         }
 
+        final var pebble = Blocks.OVERLAY_PEBBLES.getLogic();
+
         // Check if we're placing "inside" a layer
         if (block == Blocks.OVERLAY_PEBBLES && side == Side.TOP) {
             final int newMeta = meta + 1;
-            if (!world.canPlaceOnSurfaceOfBlock(blockPos.down(new TilePos()))) {
-                return false;
-            }
+            if (!pebble.isSupported(world, blockPos, Side.BOTTOM)) return false;
 
             if (newMeta < BlockLogicOverlayPebbles.MAX_PEBBLES) {
                 world.setBlockTypeDataNotify(blockPos, Blocks.OVERLAY_PEBBLES, newMeta);
@@ -70,7 +70,7 @@ public class ItemPebble extends Item implements IDispensable {
 
         // Offset position
         if (block != Blocks.AIR) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
             block = world.getBlockType(blockPos);
             meta = world.getBlockData(blockPos);
         }
@@ -80,7 +80,7 @@ public class ItemPebble extends Item implements IDispensable {
             final int newMeta = meta + 1;
 
             final AABBdc bbBox = new AABBd(blockPos.x(), blockPos.y(), blockPos.z(), blockPos.x() + 1f, blockPos.y() + (2 * (newMeta + 1)) / 16.0f, blockPos.z() + 1f);
-            if (!world.checkIfAABBIsClear(bbBox) || !world.canPlaceOnSurfaceOfBlock(blockPos.down(new TilePos()))) {
+            if (!world.checkIfAABBIsClear(bbBox) || !pebble.isSupported(world, blockPos, Side.BOTTOM)) {
                 return false;
             }
 
@@ -94,7 +94,7 @@ public class ItemPebble extends Item implements IDispensable {
         }
 
         // Regular placement, as all else fails
-        if (world.canBlockIdBePlacedAt(Blocks.OVERLAY_PEBBLES.id(), blockPos, false, side) && world.canPlaceOnSurfaceOfBlock(blockPos.down(new TilePos()))) {
+        if (world.canBlockIdBePlacedAt(Blocks.OVERLAY_PEBBLES.id(), blockPos, false, side) && !pebble.isSupported(world, blockPos, Side.BOTTOM)) {
             if (world.setBlockTypeNotify(blockPos, Blocks.OVERLAY_PEBBLES)) {
                 Blocks.OVERLAY_PEBBLES.onPlacedByMob(world, blockPos, side, player, xHit, yHit);
                 world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, Blocks.OVERLAY_PEBBLES, EnumBlockSoundEffectType.PLACE);
@@ -108,7 +108,7 @@ public class ItemPebble extends Item implements IDispensable {
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
         ProjectilePebble projectilePebble = new ProjectilePebble(world, blockPos.x() + offX, blockPos.y() + offY, blockPos.z() + offZ);
-        projectilePebble.setHeading(direction.getOffsetX() * 0.6, direction.getOffsetY() == 0 ? 0.1 : direction.getOffsetY() * 0.6, direction.getOffsetZ() * 0.6f, 1.1F, 6F);
+        projectilePebble.setHeading(direction.offsetX() * 0.6, direction.offsetY() == 0 ? 0.1 : direction.offsetY() * 0.6, direction.offsetZ() * 0.6f, 1.1F, 6F);
         world.entityJoinedWorld(projectilePebble);
         selfStack.stackSize -= 1;
     }
@@ -117,7 +117,7 @@ public class ItemPebble extends Item implements IDispensable {
     public void onDispensed(@NotNull ItemStack selfStack, @NotNull World world, @NotNull Random random, @NotNull Direction direction, double x, double y, double z) {
         if (selfStack.consumeItem(null)) {
             ProjectilePebble projectilePebble = new ProjectilePebble(world, x, y, z);
-            projectilePebble.setHeading(direction.getOffsetX(), direction.getOffsetY() + 0.1D, direction.getOffsetZ(), 1.1F, 6F);
+            projectilePebble.setHeading(direction.offsetX(), direction.offsetY() + 0.1D, direction.offsetZ(), 1.1F, 6F);
             world.entityJoinedWorld(projectilePebble);
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemPlaceable.java b/game/core/src/main/java/net/minecraft/core/item/ItemPlaceable.java
index bbe148998..652c21498 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemPlaceable.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemPlaceable.java
@@ -32,33 +32,48 @@ public class ItemPlaceable extends Item {
             return false;
         }
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
         if (blockPos.y() < 0 || blockPos.y() >= world.getHeightBlocks()) {
             return false;
         }
-        if (world.canBlockIdBePlacedAt(this.blockToPlace.id(), blockPos, false, side) && selfStack.consumeItem(player)) {
-            if (world.setBlockType(blockPos, this.blockToPlace)) {
-                if (player == null) {
-                    this.blockToPlace.onPlacedOnSide(world, blockPos, side, xHit, yHit);
-                } else {
-                    this.blockToPlace.onPlacedByMob(world, blockPos, side, player, xHit, yHit);
-                }
-                if (this.blockToPlace.isEntityTile && selfStack.getData().containsKey("tileEntityData")) {
-                    TileEntity tileEntity = world.getTileEntity(blockPos);
-                    if (tileEntity != null) {
-                        tileEntity.readAdditionalData(selfStack.getData().getCompound("tileEntityData"));
-                    }
-                }
-                // notify self change after onPlacedByMob
-                // this should fix almost all bugs related to piston intercepting placement logic,
-                // with out change to individual onPlaced<_> logics.
-                world.notifyBlockChange(blockPos, this.blockToPlace);
-                world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, this.blockToPlace, EnumBlockSoundEffectType.PLACE);
-                return true;
+        if (!world.canBlockIdBePlacedAt(this.blockToPlace.id(), blockPos, false, side)) return false;
+
+        final var data = this.blockToPlace.getPlacedData(player, selfStack, world, blockPos, side, xHit, yHit);
+
+        final var pbloc = world.getBlockType(blockPos);
+        final int pdata = world.getBlockData(blockPos);
+
+        if (!world.setBlockTypeDataRaw(blockPos, this.blockToPlace, data)) return false;
+
+        if (pbloc != this.blockToPlace || pdata != data) {
+            pbloc.onRemoved(world, blockPos, pdata);
+        }
+
+        selfStack.consumeItem(player);
+
+        if (player == null) {
+            this.blockToPlace.onPlacedOnSide(world, blockPos, side, xHit, yHit);
+        } else {
+            this.blockToPlace.onPlacedByMob(world, blockPos, side, player, xHit, yHit);
+        }
+
+        // TODO before or after tileentity?
+        this.blockToPlace.onPlacedByWorld(world, blockPos);
+
+        if (this.blockToPlace.isEntityTile && selfStack.getData().containsKey("tileEntityData")) {
+            TileEntity tileEntity = world.getTileEntity(blockPos);
+            if (tileEntity != null) {
+                tileEntity.readAdditionalData(selfStack.getData().getCompound("tileEntityData"));
             }
         }
-        return false;
+
+        // notify self change after onPlacedByMob
+        // this should fix almost all bugs related to piston intercepting placement logic,
+        // with out change to individual onPlaced<_> logics.
+        world.notifyBlockChange(blockPos, this.blockToPlace);
+        world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, this.blockToPlace, EnumBlockSoundEffectType.PLACE);
+        return true;
     }
 
     @Override
@@ -66,7 +81,7 @@ public class ItemPlaceable extends Item {
         blockPos = blockPos.add(direction, new TilePos());
         Block<?> b = world.getBlockType(blockPos);
         if (b == null || BlockTags.PLACE_OVERWRITES.appliesTo(b)) {
-            onUseOnBlock(selfStack, world, null, blockPos, direction.getSide(), 0.5, 0.5);
+            onUseOnBlock(selfStack, world, null, blockPos, direction.side(), 0.5, 0.5);
         }
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemQuiver.java b/game/core/src/main/java/net/minecraft/core/item/ItemQuiver.java
index be24c7702..0c863de97 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemQuiver.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemQuiver.java
@@ -124,7 +124,7 @@ public class ItemQuiver extends Item implements IArmorItem<HumanArmorShape>, IDi
         int count = getArrowCount(selfStack);
         if (count > 0) {
             final @NotNull ProjectileArrow arrow = new ProjectileArrow(world, x, y, z, 0);
-            arrow.setHeading(direction.getOffsetX(), direction.getOffsetY() + 0.1D, direction.getOffsetZ(), 1.1F, 6F);
+            arrow.setHeading(direction.offsetX(), direction.offsetY() + 0.1D, direction.offsetZ(), 1.1F, 6F);
             arrow.setDoesArrowBelongToPlayer(true);
             world.entityJoinedWorld(arrow);
             setArrowCount(selfStack, count - 1);
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemQuiverEndless.java b/game/core/src/main/java/net/minecraft/core/item/ItemQuiverEndless.java
index e1e5fa509..a7a3d2668 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemQuiverEndless.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemQuiverEndless.java
@@ -40,7 +40,7 @@ public class ItemQuiverEndless extends Item implements IArmorItem<HumanArmorShap
     @Override
     public void onDispensed(@NotNull ItemStack selfStack, @NotNull World world, @NotNull Random random, @NotNull Direction direction, double x, double y, double z) {
         final @NotNull ProjectileArrowPurple arrow = new ProjectileArrowPurple(world, x, y, z);
-        arrow.setHeading(direction.getOffsetX(), direction.getOffsetY() + 0.1D, direction.getOffsetZ(), 1.1F, 6F);
+        arrow.setHeading(direction.offsetX(), direction.offsetY() + 0.1D, direction.offsetZ(), 1.1F, 6F);
         arrow.setDoesArrowBelongToPlayer(true);
         world.entityJoinedWorld(arrow);
     }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemRope.java b/game/core/src/main/java/net/minecraft/core/item/ItemRope.java
index 53bf94e5f..fc0c9432c 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemRope.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemRope.java
@@ -42,7 +42,7 @@ public class ItemRope extends ItemPlaceable {
             bp.set(tempPos);
         }
         if (!world.canPlaceInsideBlock(bp)) {
-            bp = bp.add(side.getDirection(), new TilePos());
+            bp = bp.add(side.direction(), new TilePos());
         }
         if (bp.y < 0 || bp.y >= world.getHeightBlocks()) {
             return false;
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemSeeds.java b/game/core/src/main/java/net/minecraft/core/item/ItemSeeds.java
index 37e8518f8..2340932c8 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemSeeds.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemSeeds.java
@@ -29,7 +29,7 @@ public class ItemSeeds extends Item {
     @Override
     public boolean onUseOnBlock(@NotNull ItemStack selfStack, @NotNull World world, @Nullable Player player, @NotNull TilePosc blockPos, @NotNull Side side, double xHit, double yHit) {
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
         if (world.getBlockType(blockPos.down(new TilePos())) == Blocks.FARMLAND_DIRT && world.canPlaceInsideBlock(blockPos)) {
             if (world.setBlockTypeNotify(blockPos, this.cropsBlock)) {
@@ -47,7 +47,7 @@ public class ItemSeeds extends Item {
         Block<?> b = world.getBlockType(blockPos);
         boolean isFarmland = Block.hasLogicClass(b, BlockLogicFarmland.class);
         if (b == null || isFarmland || BlockTags.PLACE_OVERWRITES.appliesTo(b)) {
-            onUseOnBlock(selfStack, world, null, blockPos.add(0, isFarmland ? 1 : 0, 0, new TilePos()), direction.getSide(), 0.5, 0.5);
+            onUseOnBlock(selfStack, world, null, blockPos.add(0, isFarmland ? 1 : 0, 0, new TilePos()), direction.side(), 0.5, 0.5);
         }
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemSign.java b/game/core/src/main/java/net/minecraft/core/item/ItemSign.java
index 66a35f6c4..4b6c2a2eb 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemSign.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemSign.java
@@ -27,7 +27,7 @@ public class ItemSign extends Item {
         }
 
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
 
         if (blockPos.y() < 0 || blockPos.y() >= world.getHeightBlocks() || !Blocks.SIGN_POST_PLANKS_OAK.canPlaceAt(world, blockPos)) {
@@ -40,7 +40,7 @@ public class ItemSign extends Item {
             world.setBlockTypeData(blockPos, blockPlaced, MathHelper.floor((double) (((player.yRot + 180F) * 16F) / 360F) + 0.5D) & 0xf);
         } else {
             blockPlaced = Blocks.SIGN_WALL_PLANKS_OAK;
-            world.setBlockTypeData(blockPos, blockPlaced, side.getId());
+            world.setBlockTypeData(blockPos, blockPlaced, side.id);
         }
         selfStack.consumeItem(player);
         world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, blockPlaced, EnumBlockSoundEffectType.PLACE);
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemSignPainted.java b/game/core/src/main/java/net/minecraft/core/item/ItemSignPainted.java
index f73ba4d5a..d92c19ad6 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemSignPainted.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemSignPainted.java
@@ -26,7 +26,7 @@ public class ItemSignPainted extends ItemSign {
         }
 
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
 
         if (blockPos.y() < 0 || blockPos.y() >= world.getHeightBlocks() || !Blocks.SIGN_POST_PLANKS_OAK_PAINTED.canPlaceAt(world, blockPos)) {
@@ -40,7 +40,7 @@ public class ItemSignPainted extends ItemSign {
             world.setBlockTypeData(blockPos, blockPlaced, Blocks.SIGN_POST_PLANKS_OAK_PAINTED.getLogic().toMetadata(DyeColor.colorFromItemMeta(selfStack.getMetadata())) | direction);
         } else {
             blockPlaced = Blocks.SIGN_WALL_PLANKS_OAK_PAINTED;
-            world.setBlockTypeData(blockPos, blockPlaced, Blocks.SIGN_POST_PLANKS_OAK_PAINTED.getLogic().toMetadata(DyeColor.colorFromItemMeta(selfStack.getMetadata())) | side.getId());
+            world.setBlockTypeData(blockPos, blockPlaced, Blocks.SIGN_POST_PLANKS_OAK_PAINTED.getLogic().toMetadata(DyeColor.colorFromItemMeta(selfStack.getMetadata())) | side.id);
         }
         selfStack.consumeItem(player);
         world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, blockPlaced, EnumBlockSoundEffectType.PLACE);
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemSlimeball.java b/game/core/src/main/java/net/minecraft/core/item/ItemSlimeball.java
new file mode 100644
index 000000000..e95545d96
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemSlimeball.java
@@ -0,0 +1,100 @@
+package net.minecraft.core.item;
+
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.entity.TileEntityActivator;
+import net.minecraft.core.block.piston.PistonCommon;
+import net.minecraft.core.entity.player.Player;
+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;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static net.minecraft.core.block.piston.PistonCommon.IS_DETACHED;
+
+import java.util.Random;
+
+public class ItemSlimeball extends Item {
+
+    public ItemSlimeball(@NotNull String name, @NotNull String namespaceId, int id) {
+        super(name, namespaceId, id);
+        this.maxStackSize = 16;
+    }
+
+    private static boolean useOnBlock(
+        @NotNull ItemStack selfStack, @NotNull World world, @Nullable Player player,
+        @NotNull TilePosc blockPos, @NotNull Direction direction
+    ) {
+        if (selfStack.stackSize <= 0) return false;
+        final var block = world.getBlockType(blockPos);
+        final boolean isBase = block == Blocks.PISTON_BASE;
+        final boolean isHead = !isBase && block == Blocks.PISTON_HEAD;
+        if (!isBase && !isHead) return false;
+        assert isBase || isHead;
+
+        int data = world.getBlockData(blockPos);
+        if (PistonCommon.TYPE.get(data) != PistonCommon.TYPE_NORMAL) return false;
+
+        // base can only be slimed while not extended and on the correct side
+        if (isBase) {
+            if (PistonCommon.IS_EXTENDED.bool(data)) return false;
+            final var faceDir = Direction.fromId(PistonCommon.DIRECTION.get(data));
+            if (faceDir != direction) return false;
+        }
+        
+        data = PistonCommon.TYPE.set(data, PistonCommon.TYPE_STICKY);
+        assert selfStack.consumeItem(player);
+
+        final var baseBlock = PistonCommon.BASE_BLOCK_MAP[PistonCommon.TYPE_STICKY];
+        final var headBlock = PistonCommon.HEAD_BLOCK_MAP[PistonCommon.TYPE_STICKY];
+
+        final var newBlock = (isBase) ? baseBlock : headBlock;
+
+        if (isBase || IS_DETACHED.bool(data)) {
+            world.setBlockTypeDataNotify(blockPos, newBlock, data);
+            return true;
+        }
+
+        // head need to update base type as well
+
+        final TilePos bpos = new TilePos();
+        int bdat = PistonCommon.headGetBase(data, bpos, world, blockPos);
+        
+        System.err.println(bpos);
+        System.err.println(Integer.toBinaryString(bdat));
+
+        final boolean isExtendedButNotDetached  = 
+            PistonCommon.IS_DETACHED_OR_EXTENDED.get(bdat) == PistonCommon.IS_EXTENDED.mask;
+        if (!isExtendedButNotDetached) {
+            world.setBlockTypeDataNotify(blockPos, newBlock, data);
+            return true;
+        }
+        
+        world.setBlockTypeDataRaw(bpos, baseBlock, bdat);
+        world.setBlockTypeDataRaw(blockPos, newBlock, data);
+        world.markBlockNeedsUpdate(bpos);
+        world.markBlockNeedsUpdate(blockPos);
+        world.notifyBlocksInCapsuleOfNeighborChange(direction.opposite(), blockPos, newBlock);
+
+        return true;
+    }
+
+    @Override
+    public boolean onUseOnBlock(
+        @NotNull ItemStack selfStack, @NotNull World world, @Nullable Player player,
+        @NotNull TilePosc blockPos, @NotNull Side side, double xHit, double yHit
+    ) {
+        return useOnBlock(selfStack, world, player, blockPos, side.direction());
+    }
+
+    @Override
+    public void onUseByActivator(
+        @NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, 
+        @NotNull Random random, @NotNull TilePosc blockPos, 
+        @NotNull Direction direction, double offX, double offY, double offZ
+    ) {
+        useOnBlock(selfStack, world, null, blockPos, direction);
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemSnowball.java b/game/core/src/main/java/net/minecraft/core/item/ItemSnowball.java
index 3a7e3922b..00f3d3262 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemSnowball.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemSnowball.java
@@ -31,7 +31,7 @@ public class ItemSnowball extends Item implements IDispensable {
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
         ProjectileSnowball snowball = new ProjectileSnowball(world, blockPos.x() + offX, blockPos.y() + offY, blockPos.z() + offZ);
-        snowball.setHeading(direction.getOffsetX() * 0.6, direction.getOffsetY() == 0 ? 0.1 : direction.getOffsetY() * 0.6, direction.getOffsetZ() * 0.6f, 1.1F, 6F);
+        snowball.setHeading(direction.offsetX() * 0.6, direction.offsetY() == 0 ? 0.1 : direction.offsetY() * 0.6, direction.offsetZ() * 0.6f, 1.1F, 6F);
         world.entityJoinedWorld(snowball);
         selfStack.stackSize -= 1;
     }
@@ -40,7 +40,7 @@ public class ItemSnowball extends Item implements IDispensable {
     public void onDispensed(@NotNull ItemStack selfStack, @NotNull World world, @NotNull Random random, @NotNull Direction direction, double x, double y, double z) {
         if (selfStack.consumeItem(null)) {
             ProjectileSnowball snowball = new ProjectileSnowball(world, x, y, z);
-            snowball.setHeading(direction.getOffsetX(), direction.getOffsetY() + 0.1D, direction.getOffsetZ(), 1.1F, 6F);
+            snowball.setHeading(direction.offsetX(), direction.offsetY() + 0.1D, direction.offsetZ(), 1.1F, 6F);
             world.entityJoinedWorld(snowball);
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemStatue.java b/game/core/src/main/java/net/minecraft/core/item/ItemStatue.java
index 59e3381a7..09e183355 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemStatue.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemStatue.java
@@ -30,15 +30,17 @@ public class ItemStatue extends Item {
     @Override
     public boolean onUseOnBlock(@NotNull ItemStack selfStack, @NotNull World world, @Nullable Player player, @NotNull TilePosc blockPos, @NotNull Side side, double xHit, double yHit) {
         if (!world.canPlaceInsideBlock(blockPos)) {
-            blockPos = blockPos.add(side.getDirection(), new TilePos());
+            blockPos = blockPos.add(side.direction(), new TilePos());
         }
         if (!this.statueBlockBottom.canPlaceAt(world, blockPos)) {
             return false;
         }
 
+        selfStack.consumeItem(player);
         world.setBlockTypeData(blockPos, this.statueBlockBottom, MathHelper.floor((double) (((player.yRot + 180F) * 16F) / 360F) + 0.5D) & 0xf);
         world.setBlockTypeData(blockPos.up(new TilePos()), this.statueBlockTop, MathHelper.floor((double) (((player.yRot + 180F) * 16F) / 360F) + 0.5D) & 0xf);
-        selfStack.consumeItem(player);
+        world.markBlockNeedsUpdate(blockPos);
+        world.markBlockNeedsUpdate(blockPos.up(new TilePos()));
         
         this.statueBlockBottom.onPlacedByMob(world, blockPos, side, player, xHit, yHit);
         this.statueBlockTop.onPlacedByMob(world, blockPos.up(new TilePos()), side, player, xHit, yHit);
@@ -49,8 +51,7 @@ public class ItemStatue extends Item {
                 tileEntity.readAdditionalData(selfStack.getData().getCompound("tileEntityData"));
             }
         }
-        world.notifyBlockChange(blockPos, this.statueBlockBottom);
-        world.notifyBlockChange(blockPos.up(new TilePos()), this.statueBlockTop);
+        world.notifyBlocksInCapsuleOfNeighborChange(Direction.UP, blockPos, this.statueBlockBottom);
         
         world.playBlockSoundEffect(player, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, this.statueBlockBottom, EnumBlockSoundEffectType.PLACE);
 
@@ -66,15 +67,15 @@ public class ItemStatue extends Item {
             return;
         }
 
-        world.noNeighborUpdate = true;
-        world.setBlockTypeNotify(blockPos, this.statueBlockBottom);
-        world.setBlockTypeNotify(blockPos.up(new TilePos()), this.statueBlockTop);
-        world.noNeighborUpdate = false;
-        world.notifyBlocksOfNeighborChange(blockPos, this.statueBlockBottom);
-        world.notifyBlocksOfNeighborChange(blockPos.up(new TilePos()), this.statueBlockTop);
+        world.setBlockType(blockPos, this.statueBlockBottom);
+        world.setBlockType(blockPos.up(new TilePos()), this.statueBlockTop);
+        world.markBlockNeedsUpdate(blockPos);
+        world.markBlockNeedsUpdate(blockPos.up(new TilePos()));
+        world.notifyBlocksInCapsuleOfNeighborChange(Direction.UP, blockPos, this.statueBlockBottom);
+
         world.playBlockSoundEffect(null, blockPos.x() + 0.5F, blockPos.y() + 0.5F, blockPos.z() + 0.5F, this.statueBlockBottom, EnumBlockSoundEffectType.PLACE);
-        this.statueBlockBottom.onPlacedOnSide(world, blockPos, direction.getSide(), 0.5, 0.5);
-        this.statueBlockTop.onPlacedOnSide(world, blockPos.up(new TilePos()), direction.getSide(), 0.5, 0.5);
+        this.statueBlockBottom.onPlacedOnSide(world, blockPos, direction.side(), 0.5, 0.5);
+        this.statueBlockTop.onPlacedOnSide(world, blockPos.up(new TilePos()), direction.side(), 0.5, 0.5);
 
         if (this.statueBlockBottom.isEntityTile && selfStack.getData().containsKey("tileEntityData")) {
             TileEntity tileEntity = world.getTileEntity(blockPos);
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemWandSpawner.java b/game/core/src/main/java/net/minecraft/core/item/ItemWandSpawner.java
index 8979d9b70..15b0765d7 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemWandSpawner.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemWandSpawner.java
@@ -68,11 +68,11 @@ public class ItemWandSpawner extends Item implements IDispensable
 
     @Override
     public void onDispensed(@NotNull ItemStack selfStack, @NotNull World world, @NotNull Random random, @NotNull Direction direction, double x, double y, double z) {
-        spawnEntity(selfStack, world, x + direction.getOffsetX() * 0.5, y + direction.getOffsetY() * 0.5, z + direction.getOffsetZ() * 0.5);
+        spawnEntity(selfStack, world, x + direction.offsetX() * 0.5, y + direction.offsetY() * 0.5, z + direction.offsetZ() * 0.5);
     }
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        spawnEntity(selfStack, world, blockPos.x() + offX + direction.getOffsetX() * 0.5, blockPos.y() + offY + direction.getOffsetY() * 0.5, blockPos.z() + offZ + direction.getOffsetZ() * 0.5);
+        spawnEntity(selfStack, world, blockPos.x() + offX + direction.offsetX() * 0.5, blockPos.y() + offY + direction.offsetY() * 0.5, blockPos.z() + offZ + direction.offsetZ() * 0.5);
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/item/Items.java b/game/core/src/main/java/net/minecraft/core/item/Items.java
index 895ae1556..3141a025f 100644
--- a/game/core/src/main/java/net/minecraft/core/item/Items.java
+++ b/game/core/src/main/java/net/minecraft/core/item/Items.java
@@ -211,6 +211,7 @@ public final class Items {
     public static Item WAND_NBT;
 //    public static Item RUBYGLASS_DUST;
     public static Item RUBYGLASS_CRYSTAL;
+    public static Item TIMER;
 
     public static Item ARMOR_WOLF_LEATHER;
     public static Item ARMOR_WOLF_CHAINMAIL;
@@ -344,7 +345,7 @@ public final class Items {
         SUGARCANE = (new ItemPlaceable("sugarcane", "minecraft:item/sugarcane", 16466, Blocks.SUGARCANE));
         PAPER = (new Item("paper", "minecraft:item/paper", 16467));
         BOOK = (new Item("book", "minecraft:item/book", 16468));
-        SLIMEBALL = (new Item("slimeball", "minecraft:item/slimeball", 16469));
+        SLIMEBALL = (new ItemSlimeball("slimeball", "minecraft:item/slimeball", 16469));
         MINECART_CHEST = (new ItemMinecart("minecart.chest", "minecraft:item/minecart_chest", 16470, EntityMinecart.CHEST_CART));
         MINECART_FURNACE = (new ItemMinecart("minecart.furnace", "minecraft:item/minecart_furnace", 16471, EntityMinecart.FURNACE_CART));
         EGG_CHICKEN = (new ItemEgg("egg.chicken", "minecraft:item/egg_chicken", 16472));
@@ -413,7 +414,7 @@ public final class Items {
         LANTERN_FIREFLY_BLUE = (new ItemPlaceable("lantern.firefly.blue", "minecraft:item/lantern_firefly_blue", 16521, Blocks.LANTERN_FIREFLY_BLUE));
         LANTERN_FIREFLY_ORANGE = (new ItemPlaceable("lantern.firefly.orange", "minecraft:item/lantern_firefly_orange", 16522, Blocks.LANTERN_FIREFLY_ORANGE));
         LANTERN_FIREFLY_RED = (new ItemPlaceable("lantern.firefly.red", "minecraft:item/lantern_firefly_red", 16523, Blocks.LANTERN_FIREFLY_RED));
-        //    candleSoulwax = new ItemPlaceable("candle.soulwax", 16524, Block.candleSoulwax).setIconCoord(5, 10);
+        TIMER = (new ItemPlaceable("timer", "minecraft:item/timer", 16524, Blocks.TIMER));
         BASKET = new ItemPlaceable("basket", "minecraft:item/basket", 16525, Blocks.BASKET);
         FLAG = new ItemFlag("flag", "minecraft:item/flag", 16526);
         ARMOR_BOOTS_ICESKATES = new ItemIceSkates("armor.boots.iceskates", "minecraft:item/armor_boots_iceskates", 16527);
@@ -446,6 +447,7 @@ public final class Items {
 //        RUBYGLASS_DUST = (new Item("rubyglass.crystal.cracked", "minecraft:item/rubyglass_crystal_cracked", 16554));
         RUBYGLASS_CRYSTAL = (new Item("rubyglass.crystal.pure", "minecraft:item/rubyglass_crystal_pure", 16555));
 
+
         ARMOR_WOLF_LEATHER = new ItemArmor<>("armor.wolf.leather", "minecraft:item/armor_wolf_leather", 16556, ArmorMaterial.LEATHER, WolfArmorShape.BODY);
         ARMOR_WOLF_CHAINMAIL = new ItemArmor<>("armor.wolf.chainmail", "minecraft:item/armor_wolf_chainmail", 16557, ArmorMaterial.CHAINMAIL, WolfArmorShape.BODY);
         ARMOR_WOLF_IRON = new ItemArmor<>("armor.wolf.iron", "minecraft:item/armor_wolf_iron", 16558, ArmorMaterial.IRON, WolfArmorShape.BODY);
diff --git a/game/core/src/main/java/net/minecraft/core/item/block/ItemBlock.java b/game/core/src/main/java/net/minecraft/core/item/block/ItemBlock.java
index 7169216c3..b49f0c2e6 100644
--- a/game/core/src/main/java/net/minecraft/core/item/block/ItemBlock.java
+++ b/game/core/src/main/java/net/minecraft/core/item/block/ItemBlock.java
@@ -52,12 +52,20 @@ public class ItemBlock<T extends BlockLogic> extends Item {
         final boolean placeNextToExisting = !world.canPlaceInsideBlock(blockPos) ||
             (this.block.hasTag(BlockTags.PLACE_OVERWRITES) && this.block == world.getBlockType(blockPos));
 
-        if (placeNextToExisting) blockPos = blockPos.add(side.getDirection(), new TilePos());
+        if (placeNextToExisting) blockPos = blockPos.add(side.direction(), new TilePos());
 
         if (!world.canBlockIdBePlacedAt(this.block.id(), blockPos, false, side)) return false;
-        
-        if (!world.setBlockTypeData(blockPos, this.block, meta)) return false;
-        if (!selfStack.consumeItem(player)) return false; // should always be true
+
+        final var pbloc = world.getBlockType(blockPos);
+        final int pdata = world.getBlockData(blockPos);
+
+        if (!world.setBlockTypeDataRaw(blockPos, this.block, meta)) return false;
+
+        if (pbloc != this.block || pdata != meta) {
+            pbloc.onRemoved(world, blockPos, pdata);
+        }
+
+        selfStack.consumeItem(player);
 
         if (player == null) {
             this.block.onPlacedOnSide(world, blockPos, side, xHit, yHit);
@@ -65,6 +73,8 @@ public class ItemBlock<T extends BlockLogic> extends Item {
             this.block.onPlacedByMob(world, blockPos, side, player, xHit, yHit);
         }
 
+        this.block.onPlacedByWorld(world, blockPos);
+
         if (this.block.isEntityTile && selfStack.getData().containsKey("tileEntityData")) {
             final TileEntity tileEntity = world.getTileEntity(blockPos);
             if (tileEntity != null) {
@@ -86,7 +96,7 @@ public class ItemBlock<T extends BlockLogic> extends Item {
         blockPos = new TilePos(blockPos).add(direction);
         @NotNull Block<?> b = world.getBlockType(blockPos);
         if (b == Blocks.AIR || BlockTags.PLACE_OVERWRITES.appliesTo(b)) {
-            onUseOnBlock(selfStack, world, null, blockPos, direction.getSide(), 0.5, 0.5);
+            onUseOnBlock(selfStack, world, null, blockPos, direction.side(), 0.5, 0.5);
         }
     }
 
diff --git a/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockLadder.java b/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockLadder.java
index e7a7a2cca..4f2dfce00 100644
--- a/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockLadder.java
+++ b/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockLadder.java
@@ -24,7 +24,7 @@ public class ItemBlockLadder<T extends BlockLogicLadder> extends ItemBlock<T> {
 
         final Side pside;
         if (player == null || player.isSneaking() || world.getBlockType(bp) != this.block) {
-            if (!world.canPlaceInsideBlock(bp)) bp.add(side.getDirection());
+            if (!world.canPlaceInsideBlock(bp)) bp.add(side.direction());
             pside = ladder.getSideForPlacement(world, bp, side);
         } else {
             xHit = 0.5;
diff --git a/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockSlab.java b/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockSlab.java
index 7ba51df41..8544fd687 100644
--- a/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockSlab.java
+++ b/game/core/src/main/java/net/minecraft/core/item/block/ItemBlockSlab.java
@@ -49,7 +49,7 @@ public class ItemBlockSlab<T extends BlockLogic> extends ItemBlock<T> {
                 };
             if (canPlaceInto) break;
             shiftPos.add(side);
-            yHit -= side.getOffsetY();
+            yHit -= side.offsetY();
         } if (i == 2) { // for else
             return super.onUseOnBlock(selfStack, world, player, blockPos, side, xHit, yHit);
         }
diff --git a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolHoe.java b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolHoe.java
index 27389ac8b..8a48d1c90 100644
--- a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolHoe.java
+++ b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolHoe.java
@@ -39,7 +39,7 @@ public class ItemToolHoe extends ItemTool {
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        till(selfStack, null, world, blockPos.add(direction, new TilePos()), direction.getSide());
+        till(selfStack, null, world, blockPos.add(direction, new TilePos()), direction.side());
     }
 
     public boolean till(@NotNull ItemStack selfStack, @Nullable Player player, @NotNull World world, @NotNull TilePosc blockPos, @NotNull Side side) {
diff --git a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolShovel.java b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolShovel.java
index 895b76bff..aa60ed8df 100644
--- a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolShovel.java
+++ b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolShovel.java
@@ -36,7 +36,7 @@ public class ItemToolShovel extends ItemTool {
 
     @Override
     public void onUseByActivator(@NotNull ItemStack selfStack, @NotNull World world, @NotNull TileEntityActivator activator, @NotNull Random random, @NotNull TilePosc blockPos, @NotNull Direction direction, double offX, double offY, double offZ) {
-        shovelBlock(selfStack, world, null, blockPos.add(direction, new TilePos()), direction.getSide());
+        shovelBlock(selfStack, world, null, blockPos.add(direction, new TilePos()), direction.side());
     }
 
     public boolean shovelBlock(@NotNull ItemStack selfStack, @NotNull World world, @Nullable Player entityplayer, @NotNull TilePosc blockPos, @NotNull Side side) {
diff --git a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolSword.java b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolSword.java
index 3d99468b8..7bb9605f1 100644
--- a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolSword.java
+++ b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolSword.java
@@ -55,7 +55,7 @@ public class ItemToolSword extends Item
         blockPos = blockPos.add(direction, new TilePos());
         Block<?> b = world.getBlockType(blockPos);
         if (b == Blocks.PUMPKIN) {
-            world.setBlockTypeData(blockPos, Blocks.PUMPKIN_CARVED_IDLE, direction.getOpposite().getId());
+            world.setBlockTypeData(blockPos, Blocks.PUMPKIN_CARVED_IDLE, direction.opposite().id);
             selfStack.damageItem(1, null);
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/net/entity/entries/NetEntryFallingBlock.java b/game/core/src/main/java/net/minecraft/core/net/entity/entries/NetEntryFallingBlock.java
index 473a2dd87..2f094e98d 100644
--- a/game/core/src/main/java/net/minecraft/core/net/entity/entries/NetEntryFallingBlock.java
+++ b/game/core/src/main/java/net/minecraft/core/net/entity/entries/NetEntryFallingBlock.java
@@ -27,7 +27,7 @@ public class NetEntryFallingBlock implements IVehicleEntry<EntityFallingBlock>,
 
     @Override
     public int getMovementPacketDelay() {
-        return 20;
+        return 3;
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/net/packet/PacketBlockEvent.java b/game/core/src/main/java/net/minecraft/core/net/packet/PacketBlockEvent.java
index 182a0f0a8..91f0698a9 100644
--- a/game/core/src/main/java/net/minecraft/core/net/packet/PacketBlockEvent.java
+++ b/game/core/src/main/java/net/minecraft/core/net/packet/PacketBlockEvent.java
@@ -9,14 +9,18 @@ public class PacketBlockEvent extends Packet
     public int xLocation;
     public int yLocation;
     public int zLocation;
-    public int index;
-    public int data;
+    public byte index;
+    public byte data;
 
     public PacketBlockEvent()
     {
     }
 
-    public PacketBlockEvent(int x, int y, int z, int index, int data)
+    public PacketBlockEvent(int x, int y, int z, int index, int data) {
+        this(x,y,z, (byte)(0xFF & index), (byte)(0xff & data));
+    }
+
+    public PacketBlockEvent(int x, int y, int z, byte index, byte data)
     {
         xLocation = x;
         yLocation = y;
@@ -32,8 +36,8 @@ public class PacketBlockEvent extends Packet
         xLocation = dis.readInt();
         yLocation = dis.readShort();
         zLocation = dis.readInt();
-        index = dis.read();
-        data = dis.read();
+        index = dis.readByte();
+        data = dis.readByte();
     }
 
     @Override
@@ -43,8 +47,8 @@ public class PacketBlockEvent extends Packet
         dos.writeInt(xLocation);
         dos.writeShort(yLocation);
         dos.writeInt(zLocation);
-        dos.write(index);
-        dos.write(data);
+        dos.writeByte(index);
+        dos.writeByte(data);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/net/packet/PacketPlayerAction.java b/game/core/src/main/java/net/minecraft/core/net/packet/PacketPlayerAction.java
index bc3288510..53a2a78da 100644
--- a/game/core/src/main/java/net/minecraft/core/net/packet/PacketPlayerAction.java
+++ b/game/core/src/main/java/net/minecraft/core/net/packet/PacketPlayerAction.java
@@ -50,7 +50,7 @@ public class PacketPlayerAction extends Packet
         xPosition = dis.readInt();
         yPosition = dis.read();
         zPosition = dis.readInt();
-        side = Side.getSideById(dis.read());
+        side = Side.fromId(dis.read());
         xHit = dis.readDouble();
         yHit = dis.readDouble();
     }
@@ -63,7 +63,7 @@ public class PacketPlayerAction extends Packet
         dos.writeInt(xPosition);
         dos.write(yPosition);
         dos.writeInt(zPosition);
-        dos.write(side.getId());
+        dos.write(side.id);
         dos.writeDouble(xHit);
         dos.writeDouble(yHit);
     }
diff --git a/game/core/src/main/java/net/minecraft/core/net/packet/PacketUseOrPlaceItemStack.java b/game/core/src/main/java/net/minecraft/core/net/packet/PacketUseOrPlaceItemStack.java
index 21c0c9a58..7a21858aa 100644
--- a/game/core/src/main/java/net/minecraft/core/net/packet/PacketUseOrPlaceItemStack.java
+++ b/game/core/src/main/java/net/minecraft/core/net/packet/PacketUseOrPlaceItemStack.java
@@ -80,7 +80,7 @@ public class PacketUseOrPlaceItemStack extends Packet
         xPosition = dis.readInt();
         yPosition = dis.read();
         zPosition = dis.readInt();
-        direction = Direction.getDirectionById(dis.read());
+        direction = Direction.fromId(dis.read());
         xPlaced = dis.readDouble();
         yPlaced = dis.readDouble();
         type = dis.readByte();
@@ -103,7 +103,7 @@ public class PacketUseOrPlaceItemStack extends Packet
         dos.writeInt(xPosition);
         dos.write(yPosition);
         dos.writeInt(zPosition);
-        dos.write(direction.getId());
+        dos.write(direction.id);
         dos.writeDouble(xPlaced);
         dos.writeDouble(yPlaced);
         dos.writeByte(type);
diff --git a/game/core/src/main/java/net/minecraft/core/util/PackedField.java b/game/core/src/main/java/net/minecraft/core/util/PackedField.java
index 9b7791a95..98fc084eb 100644
--- a/game/core/src/main/java/net/minecraft/core/util/PackedField.java
+++ b/game/core/src/main/java/net/minecraft/core/util/PackedField.java
@@ -5,7 +5,7 @@ public final class PackedField {
     public final int mask;
 
     public PackedField(int offset, int size) {
-        assert offset >= 0 && size >= 0 && offset + size < 32;
+        assert offset >= 0 && size >= 0 && offset + size <= 32;
         this.offset = offset;
         this.mask = ((1 << size) - 1) << offset;
     }
diff --git a/game/core/src/main/java/net/minecraft/core/util/helper/Axis.java b/game/core/src/main/java/net/minecraft/core/util/helper/Axis.java
index 53f2d815c..c72b69aa9 100644
--- a/game/core/src/main/java/net/minecraft/core/util/helper/Axis.java
+++ b/game/core/src/main/java/net/minecraft/core/util/helper/Axis.java
@@ -2,10 +2,23 @@ package net.minecraft.core.util.helper;
 
 public enum Axis {
 	
-	X, Y, Z, NONE;
+	X(0), Y(1), Z(2), 
 	
+	@Deprecated
+	NONE(-1, -1);
+	
+	private Axis(int xyzIndex) {
+		this(xyzIndex, (xyzIndex + 1) % 3);
+	}
+	private Axis(int xyzIndex, int yzxIndex) {
+		this.xyzIndex = xyzIndex;
+		this.yzxIndex = yzxIndex;
+	}
+
 	public boolean isVertical() {
 		return this == Y;
 	}
-	
+
+	public int xyzIndex;
+	public int yzxIndex;
 }
diff --git a/game/core/src/main/java/net/minecraft/core/util/helper/BlockParticleHelper.java b/game/core/src/main/java/net/minecraft/core/util/helper/BlockParticleHelper.java
index 3e5f86a2f..b6b3061b1 100644
--- a/game/core/src/main/java/net/minecraft/core/util/helper/BlockParticleHelper.java
+++ b/game/core/src/main/java/net/minecraft/core/util/helper/BlockParticleHelper.java
@@ -2,7 +2,7 @@ package net.minecraft.core.util.helper;
 
 public class BlockParticleHelper {
     public static int encodeBlockData(int blockId, int meta, Side side) {
-        return blockId & 0x3FFF | (meta & 0xFF) << 14 | (side.getId() << 22);
+        return blockId & 0x3FFF | (meta & 0xFF) << 14 | (side.id << 22);
     }
 
     public static int decodeBlockID(int data) {
@@ -14,6 +14,6 @@ public class BlockParticleHelper {
     }
 
     public static Side decodeBlockSide(int data) {
-        return Side.getSideById(data >> 22 & 0xF);
+        return Side.fromId(data >> 22 & 0xF);
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/util/helper/Direction.java b/game/core/src/main/java/net/minecraft/core/util/helper/Direction.java
index bc8f0e509..a34f6a9d6 100644
--- a/game/core/src/main/java/net/minecraft/core/util/helper/Direction.java
+++ b/game/core/src/main/java/net/minecraft/core/util/helper/Direction.java
@@ -1,221 +1,600 @@
 package net.minecraft.core.util.helper;
 
-import net.minecraft.core.entity.Mob;
+import java.util.Arrays;
+
 import org.jetbrains.annotations.NotNull;
+import org.joml.Vector3d;
+import org.joml.Vector3f;
+import org.joml.Vector3i;
+
+import net.minecraft.core.entity.Mob;
+import net.minecraft.core.world.pos.TilePos;
 
 public enum Direction {
-	
-	NORTH(Axis.Z, 0, 0, -1, "north"),
-	EAST(Axis.X, 1, 0, 0, "east"),
-	SOUTH(Axis.Z, 0, 0, 1, "south"),
-	WEST(Axis.X, -1, 0, 0, "west"),
-	UP(Axis.Y, 0, 1, 0, "up"),
-	DOWN(Axis.Y, 0, -1, 0, "down"),
-	NONE(Axis.NONE, 0, 0, 0, "none");
-	
-	public static final @NotNull Direction @NotNull [] directions = new Direction[6];
-	public static final @NotNull Direction @NotNull [] horizontalDirections = new Direction[4];
-	
-	static {
-		setId(DOWN, 0);
-		setId(UP, 1);
-		setId(NORTH, 2);
-		setId(SOUTH, 3);
-		setId(WEST, 4);
-		setId(EAST, 5);
-		NONE.id = -1;
-		
-		setHorizontal(NORTH, 0);
-		setHorizontal(EAST, 1);
-		setHorizontal(SOUTH, 2);
-		setHorizontal(WEST, 3);
-		
-		setOpposites(EAST, WEST);
-		setOpposites(UP, DOWN);
-		setOpposites(NORTH, SOUTH);
-		NONE.opposite = NONE;
-		
-		UP.side = Side.TOP;
-		DOWN.side = Side.BOTTOM;
-		NORTH.side = Side.NORTH;
-		EAST.side = Side.EAST;
-		SOUTH.side = Side.SOUTH;
-		WEST.side = Side.WEST;
-		NONE.side = Side.NONE;
-		
-		NORTH.index = 0;
-		EAST.index = 1;
-		SOUTH.index = 2;
-		WEST.index = 3;
-		UP.index = 4;
-		DOWN.index = 5;
-	}
-	
-	private static void setId(Direction side, int id) {
-		directions[id] = side;
-		side.id = id;
-	}
-	
-	private static void setHorizontal(Direction direction, int id) {
-		horizontalDirections[id] = direction;
-		direction.horizontalIndex = id;
-	}
-	
-	private static void setOpposites(Direction side1, Direction side2) {
-		side1.opposite = side2;
-		side2.opposite = side1;
-	}
-
-	/////////////////////
-	
-	public static @NotNull Direction getDirectionById(int i) {
-		if(i >= 0 && i < directions.length) {
-			return directions[i];
-		}else {
-			return NONE;
-		}
-	}
-
-	/////////////////////
-
-	private final Axis axis;
-	private int id;
-	private int horizontalIndex;
-	private Direction opposite;
-	private Side side;
-	
-	private final int offsetX;
-	private final int offsetY;
-	private final int offsetZ;
-	private final String translationKey;
-	
-	@Deprecated
-	public int index;
-	
-	Direction(Axis axis, int offsetX, int offsetY, int offsetZ, String translationKey) {
-		this.axis = axis;
-		this.offsetX = offsetX;
-		this.offsetY = offsetY;
-		this.offsetZ = offsetZ;
-		this.translationKey = "direction." + translationKey;
-	}
-	
-	public int getOffsetX() {
-		return offsetX;
-	}
-	
-	public int getOffsetY() {
-		return offsetY;
-	}
-	
-	public int getOffsetZ() {
-		return offsetZ;
-	}
-
-	public String getTranslationKey() {
-		return translationKey;
-	}
-	
-	public @NotNull Direction getOpposite() {
-		return opposite;
-	}
-	
-	public Side getSide() {
-		return side;
-	}
-	
-	public @NotNull Axis getAxis() {
-		return axis;
-	}
-	
-	public int getId() {
-		return id;
-	}
-
-	public boolean isVertical() {
-		return getAxis().isVertical();
-	}
-	
-	public boolean isHorizontal() {
-		return !isVertical();
-	}
-
-	public Direction rotate(int amount) {
-		if(this == UP) return UP;
-		if(this == DOWN) return DOWN;
-		return horizontalDirections[(getHorizontalIndex() + amount) & 3];
-	}
-	
-	public int getHorizontalIndex() {
-		return horizontalIndex;
-	}
-	
-	/////////////////////
-	
-	public static Direction getVerticalDirection(double rotationPitch) {
-		if(rotationPitch < 0) return UP;
-		else return DOWN;
-	}
-	
-	public static Direction getVerticalDirection(Mob entity) {
-		if(entity.rotationLockVertical != null && entity.rotationLockVertical != NONE) return entity.rotationLockVertical;
-		else return getVerticalDirection(entity.xRot);
-	}
-	
-	public static @NotNull Direction getHorizontalDirection(final double rotationYaw) {
-		return horizontalDirections[(MathHelper.floor(rotationYaw / 90f + 0.5) + 2) & 3];
-	}
-	
-	public static @NotNull Direction getHorizontalDirection(final @NotNull Mob mob) {
-		if (mob.rotationLockHorizontal != null && mob.rotationLockHorizontal != NONE) return mob.rotationLockHorizontal;
-		return getHorizontalDirection(mob.yRot);
-	}
-	
-	public static @NotNull Direction getDirection(final @NotNull Mob mob) {
-		if (mob.rotationLock != null && mob.rotationLock != NONE) return mob.rotationLock;
-		if (mob.xRot < -45) return UP;
-		if (mob.xRot > 45) return DOWN;
-		return getHorizontalDirection(mob.yRot);
-	}
-
-	//////////////////
-
-	public static @NotNull Direction[] rotXMap = new Direction[] {
-		Direction.DOWN, Direction.NORTH, Direction.UP, Direction.SOUTH,
-		Direction.UP, Direction.SOUTH, Direction.DOWN, Direction.NORTH,
-		Direction.NORTH, Direction.UP, Direction.SOUTH, Direction.DOWN,
-		Direction.SOUTH, Direction.DOWN, Direction.NORTH, Direction.UP,
-		Direction.WEST, Direction.WEST, Direction.WEST, Direction.WEST,
-		Direction.EAST, Direction.EAST, Direction.EAST, Direction.EAST,
-	};
-
-	public static @NotNull Direction rotateX(@NotNull Direction direction, int amount) {
-		return rotXMap[direction.getId() * 4 + (amount & 0b11)];
-	}
-
-	public static @NotNull Direction[] rotYMap = new Direction[] {
-		Direction.DOWN, Direction.DOWN, Direction.DOWN, Direction.DOWN,
-		Direction.UP, Direction.UP, Direction.UP, Direction.UP,
-		Direction.NORTH, Direction.WEST, Direction.SOUTH, Direction.EAST,
-		Direction.SOUTH, Direction.EAST, Direction.NORTH, Direction.WEST,
-		Direction.WEST, Direction.SOUTH, Direction.EAST, Direction.NORTH,
-		Direction.EAST, Direction.NORTH, Direction.WEST, Direction.SOUTH,
-	};
-
-	public static @NotNull Direction rotateY(@NotNull Direction direction, int amount) {
-		return rotYMap[direction.getId() * 4 + (amount & 0b11)];
-	}
-
-	public static @NotNull Direction[] rotZMap = new Direction[] {
-		Direction.DOWN, Direction.EAST, Direction.UP, Direction.WEST,
-		Direction.UP, Direction.WEST, Direction.DOWN, Direction.EAST,
-		Direction.NORTH, Direction.NORTH, Direction.NORTH, Direction.NORTH,
-		Direction.SOUTH, Direction.SOUTH, Direction.SOUTH, Direction.SOUTH,
-		Direction.WEST, Direction.DOWN, Direction.EAST, Direction.UP,
-		Direction.EAST, Direction.UP, Direction.WEST, Direction.DOWN,
-	};
-
-	public static @NotNull Direction rotateZ(@NotNull Direction direction, int amount) {
-		return rotZMap[direction.getId() * 4 + (amount & 0b11)];
-	}
+    DOWN  (0b000, "north"),
+    UP    (0b001, "east"),
+    NORTH (0b010, "south"),
+    SOUTH (0b011, "west"),
+    WEST  (0b100, "up"),
+    EAST  (0b101, "down"),
+
+    /**
+     * Method implementations might not guarantee correct behaviour (error safety) when passed
+     * NONE. Prefer passing {@code null} if method accepts {@code @Nullable Direction},
+     * or an acceptable default direction, or avoid calling the method entirely!
+     */
+    @Deprecated
+    NONE  (0b000, "none"); // NONE will always index as down to simplify things :|
+
+    public final int id;
+    public final String translationKey;
+
+    private Direction(int id, String translationKey) {
+        this.id = id;
+        this.translationKey = "direction." + translationKey;
+    }
+
+    public static final int count = 6;
+    public static final int horizontalCount = 4;
+
+    // Lookup:
+    // ============================
+
+    /**
+     * returns a valid direction when id >= 0 && < 6, otherwise NONE. <br><br>
+     * NOTE:
+     */
+    public static @NotNull Direction fromId(int id) {
+        return ((id & ~0b111) == 0)
+            ? OPTIONAL_MAP[id] // NONE if >= 6
+            : NONE;
+    }
+
+    public static @NotNull Direction fromIdOrDefault(int id) {
+        if (id >= 0 && id < 6) id = 0;
+        return ID_MAP[id];
+    }
+
+    public static Direction fromIdOrDefault(Direction direction, int id) {
+        return (id >= 0 && id < 6)
+            ? ID_MAP[id]
+            : direction;
+    }
+
+    /**
+     * simpler lookup logic, but can yield non-NONE result for index < 0 || >= 6
+     */
+    public static @NotNull Direction fromIdLenient(int id) {
+        return OPTIONAL_MAP[id & 0b111];
+    }
+
+    /**
+     * Perfer using {@code fromId(id)}.
+     * Index directly only if you are certain that the index is in bound;
+     * alternatively, if you want an always valid but may be incorrest result,
+     * use {@code ID_MAP[id % 6]}. <br><br>
+     *
+     * Directions follow the order [ -y, +y, -z, +z, -x, +x ]
+     *
+     * @see Direction#fromId(int)
+     */
+    public static final @NotNull Direction @NotNull[] ID_MAP = {
+        DOWN, UP,
+        NORTH, SOUTH,
+        WEST, EAST,
+    };
+
+    public static final @NotNull Direction @NotNull[] all = ID_MAP;
+
+    private static final Direction[] OPTIONAL_MAP = {
+        DOWN, UP,
+        NORTH, SOUTH,
+        WEST, EAST,
+        NONE, NONE, // [6..8]
+    };
+
+    public static final @NotNull Direction @NotNull[] horizontal = { NORTH, EAST, SOUTH, WEST };
+
+    @Deprecated
+    public int legacyIndex() {
+        return switch (this) {
+            default -> 0;
+            case NORTH -> 0;
+            case EAST -> 1;
+            case SOUTH -> 2;
+            case WEST -> 3;
+            case UP -> 4;
+            case DOWN -> 5;
+        };
+    }
+
+    @Deprecated
+    public int legacyHorizontalIndex() {
+        return switch (this) {
+            default -> 0;
+            case NORTH -> 0;
+            case EAST -> 1;
+            case SOUTH -> 2;
+            case WEST -> 3;
+        };
+    }
+
+    // Friends:
+    // ============================
+
+    public @NotNull Side side() {
+        return Side.fromId(this.id);
+    }
+
+    private static final Axis[] AXIS_YZX_MAP = { Axis.Y, Axis.Z, Axis.X };
+    // private static final Axis[] AXIS_XYZ_MAP = { Axis.X, Axis.Y, Axis.Z };
+
+    public @NotNull Axis axis() {
+        return AXIS_YZX_MAP[this.id >> 1];
+    }
+
+
+    // Rotation:
+    // ============================
+
+    public boolean isVertical() {
+        if (this == NONE) return false;
+        return isVerticle(this.id);
+    }
+    
+    // we don't care about NONE in the int version
+    public static boolean isVerticle(int id) {
+        return (id & 0b110) == 0;
+    }
+    
+    public boolean isHorizontal() {
+        if (this == NONE) return false;
+        return isHorizontal(this.id);
+    }
+
+    public static boolean isHorizontal(int id) {
+        return (id & 0b110) != 0;
+    }
+
+    public static @NotNull Direction fromPitch(double pitch) {
+        return (fpSign(pitch) == 0) ? DOWN : UP;
+    }
+
+    // handles +-0 correctly
+    // does not return meaningful output for NaN however!
+    private static int fpSign(double x) {
+        final long raw = Double.doubleToRawLongBits(x);
+        final int sign = (int) (raw >>> 63);
+        assert sign == 0 || sign == 1;
+        return sign;
+    }
+
+    public static @NotNull Direction fromYaw(double yaw) {
+        final double yawRightHanded = -yaw;
+        final int quarterTurnsFromNegX = MathHelper.floor(yawRightHanded / 90D + 0.5f);
+        final int quarterTurnsFromPosX = quarterTurnsFromNegX + 2;
+
+        return ID_MAP[ROT_MAPS[1][quarterTurnsFromPosX & 0b11]];
+    }
+
+    public static @NotNull Direction getVerticalLockable(@NotNull Mob mob) {
+        return (mob.rotationLockVertical != null)
+            ? mob.rotationLockVertical
+            : fromPitch(mob.xRot);
+    }
+
+    public static @NotNull Direction getHorizontalLockable(@NotNull Mob mob) {
+        return (mob.rotationLockHorizontal != null)
+            ? mob.rotationLockHorizontal
+            : fromYaw(mob.yRot);
+    }
+
+    public static @NotNull Direction getLockable(@NotNull Mob mob) {
+        if (mob.rotationLock != null) return mob.rotationLock;
+
+        // both of these would be false if NaN
+        if (mob.xRot < -45) return UP;
+        if (mob.xRot > 45) return DOWN;
+
+        return fromYaw(mob.yRot);
+    }
+
+    private static final int[][] ROT_MAPS = {
+        // X: -y, -z, +y, +z
+        { DOWN.id, NORTH.id, UP.id, SOUTH.id },
+
+        // Y: -x, +z, +x, -z
+        { NORTH.id, WEST.id, SOUTH.id, EAST.id },
+
+        // Z: -y, +x, +y, -x
+        { DOWN.id, EAST.id, UP.id, WEST.id },
+    };
+
+    private static final int[][] ROT_INDEX_MAPS = new int[3][6];
+    static {
+        for (int axis = 0; axis < 3; axis ++) {
+            Arrays.fill(ROT_INDEX_MAPS[axis], -1);
+            for (int i = 0; i < 4; i++) {
+                final int dir = ROT_MAPS[axis][i];
+                ROT_INDEX_MAPS[axis][dir] = i;
+            }
+        }
+    }
+
+    /**
+     * Right-handed around the +X axis
+     */
+    public @NotNull Direction rotateX(int quarterTurns) {
+        if (this == NONE) return this;
+        return ID_MAP[rotate(this.id, 0, quarterTurns)];
+    }
+
+    /**
+     * Right-handed around the +X axis
+     */
+    public static int rotateX(int id, int quarterTurns) {
+        return rotate(id, 0, quarterTurns);
+    }
+
+    /**
+     * Right-handed rotation around the +Y axis
+     */
+    public @NotNull Direction rotateY(int quarterTurns) {
+        if (this == NONE) return this;
+        return ID_MAP[rotate(this.id, 1, quarterTurns)];
+    }
+
+    /**
+     * Right-handed rotation around the +Y axis
+     */
+    public static int rotateY(int id, int quarterTurns) {
+        return rotate(id, 1, quarterTurns);
+    }
+
+    /**
+     * Right-handed rotation around the +Z axis
+     */
+    public @NotNull Direction rotateZ(int quarterTurns) {
+        if (this == NONE) return this;
+        return ID_MAP[rotate(this.id, 2, quarterTurns)];
+    }
+
+    /**
+     * Right-handed rotation around the +Z axis
+     */
+    public static int rotateZ(int id, int quarterTurns) {
+        return rotate(id, 2, quarterTurns);
+    }
+
+    /**
+     * Right-handed rotation around the given positive axis
+     */
+    public @NotNull Direction rotate(@NotNull Axis axis, int quarterTurns) {
+        if (this == NONE) return this;
+        return ID_MAP[rotate(this.id, axis.xyzIndex, quarterTurns)];
+    }
+
+    /**
+     * Right-handed rotation around the given positive axis
+     */
+    public static int rotate(int id, @NotNull Axis axis, int quarterTurns) {
+        return rotate(id, axis.xyzIndex, quarterTurns);
+    }
+
+    /**
+     * Right-handed rotation around the given positive axis,
+     * where +X, +Y, +Z are represented as 0, 1, 2 respectively
+     */
+    public static int rotate(int id, int axis, int quarterTurns) {
+        axis &= 0b11; assert axis < 3;
+        id &= 0b111; assert id < 6;
+
+        
+        final int ridx = ROT_INDEX_MAPS[axis][id];
+        if (ridx < 0) return id;
+
+        return ROT_MAPS[axis][(ridx + quarterTurns) & 0b11];
+    }
+
+
+    // Offset:
+    // ============================
+
+    private static final int[] CMP_MAP = { 1, 2, 0 };
+
+    public int componentIndex() {
+        if (this == NONE) return 0; // instead of -1 :|
+        final int res = componentIndex(this.id);
+        assert res >= 0;
+        return res;
+    }
+
+    public static int componentIndex(int id) {
+        id = (id >>> 1) & 0b11;
+        assert id != 0b11;
+        return CMP_MAP[id];
+    }
+
+    public int sign() {
+        if (this == NONE) return 0;
+        return sign(this.id);
+    }
+
+    public static int sign(int id) {
+        return ~id & 1;
+    }
+
+    public @NotNull Direction opposite() {
+        if (this == NONE) return this;
+        final var id = opposite(this.id);
+        assert id >= 0 && id < 6;
+        return ID_MAP[id];
+    }
+
+    public static int opposite(int id) {
+        return id ^ 1;
+    }
+
+    public boolean isPositive() {
+        if (this == NONE) return false;
+        return this.sign() == 0;
+    }
+
+    public boolean isNegative() {
+        if (this == NONE) return false;
+        return this.sign() == 1;
+    }
+
+    public static final int[][] OFFSETS = new int[3][6];
+    static {
+        for (int i = 0; i < 6; i++) {
+            OFFSETS[componentIndex(i)][i] = 1 - (sign(i) << 1);
+        }
+    }
+
+    public int offsetX() {
+        if (this == NONE) return 0;
+        return offsetX(this.id);
+    }
+    public int offsetY() {
+        if (this == NONE) return 0;
+        return offsetY(this.id);
+    }
+    public int offsetZ() {
+        if (this == NONE) return 0;
+        return offsetZ(this.id);
+    }
+
+    public static int offsetX(int id) {
+        id &= 0b111; assert id < 6;
+        return OFFSETS[0][id];
+    }
+
+    public static int offsetY(int id) {
+        id &= 0b111; assert id < 6;
+        return OFFSETS[1][id];
+    }
+
+    public static int offsetZ(int id) {
+        id &= 0b111; assert id < 6;
+        return OFFSETS[2][id];
+    }
+
+    // ---------
+
+    // all these api to ensure conveniency while avoid redundant allocation ...
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public @NotNull Vector3i getOffset(@NotNull Vector3i out) {
+        if (this == NONE) return out.set(0,0,0);
+        return getOffset(this.id, out);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public @NotNull Vector3d getOffset(@NotNull Vector3d out) {
+        if (this == NONE) return out.set(0,0,0);
+        return getOffset(this.id, out);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public @NotNull Vector3i getOffsetScaled(@NotNull Vector3i out, int scale) {
+        if (this == NONE) return out.set(0,0,0);
+        return getOffsetScaled(this.id, out, scale);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public @NotNull Vector3d getOffsetScaled(@NotNull Vector3d out, double scale) {
+        if (this == NONE) return out.set(0,0,0);
+        return getOffsetScaled(this.id, out, scale);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public @NotNull Vector3f getOffsetScaled(@NotNull Vector3f out, float scale) {
+        if (this == NONE) return out.set(0,0,0);
+        return getOffsetScaled(this.id, out, scale);
+    }
+
+    // ---------
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public static @NotNull Vector3i getOffset(int id, @NotNull Vector3i out) {
+        id &= 0b111; assert id < 6;
+        return out.set(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public static @NotNull Vector3d getOffset(int id, @NotNull Vector3d out) {
+        id &= 0b111; assert id < 6;
+        return out.set(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public static @NotNull Vector3f getOffset(int id, @NotNull Vector3f out) {
+        id &= 0b111; assert id < 6;
+        return out.set(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public static @NotNull Vector3i getOffsetScaled(int id, @NotNull Vector3i out, int scale) {
+        id &= 0b111; assert id < 6;
+        return out.set(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public static @NotNull Vector3d getOffsetScaled(int id, @NotNull Vector3d out, double scale) {
+        id &= 0b111; assert id < 6;
+        return out.set(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
+
+    /**
+     * @param out the location where the offset is written to.
+     */
+    public static @NotNull Vector3f getOffsetScaled(int id, @NotNull Vector3f out, float scale) {
+        id &= 0b111; assert id < 6;
+        return out.set(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
+
+    // ---------
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull TilePos offset(@NotNull TilePos ref) {
+        if (this == NONE) return ref;
+        return offset(this.id, ref);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull Vector3i offset(@NotNull Vector3i ref) {
+        if (this == NONE) return ref;
+        return offset(this.id, ref);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull Vector3d offset(@NotNull Vector3d ref) {
+        if (this == NONE) return ref;
+        return offset(this.id, ref);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull TilePos offsetScaled(@NotNull TilePos ref, int scale) {
+        if (this == NONE) return ref;
+        return offsetScaled(this.id, ref, scale);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull Vector3i offsetScaled(@NotNull Vector3i ref, int scale) {
+        if (this == NONE) return ref;
+        return offsetScaled(this.id, ref, scale);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull Vector3d offsetScaled(@NotNull Vector3d ref, double scale) {
+        if (this == NONE) return ref;
+        return offsetScaled(this.id, ref, scale);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public @NotNull Vector3f offsetScaled(@NotNull Vector3f ref, float scale) {
+        if (this == NONE) return ref;
+        return offsetScaled(this.id, ref, scale);
+    }
+
+    // ---------
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull TilePos offset(int id, @NotNull TilePos ref) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull Vector3i offset(int id, @NotNull Vector3i ref) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull Vector3d offset(int id, @NotNull Vector3d ref) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull Vector3f offset(int id, @NotNull Vector3f ref) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id], OFFSETS[1][id], OFFSETS[2][id]);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull Vector3d offsetScaled(int id, @NotNull Vector3d ref, double scale) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull TilePos offsetScaled(int id, @NotNull TilePos ref, int scale) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull Vector3i offsetScaled(int id, @NotNull Vector3i ref, int scale) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
+
+    /**
+     * @param ref the location where the offset is applied to.
+     */
+    public static @NotNull Vector3f offsetScaled(int id, @NotNull Vector3f ref, float scale) {
+        id &= 0b111; assert id < 6;
+        return ref.add(OFFSETS[0][id] * scale, OFFSETS[1][id] * scale, OFFSETS[2][id] * scale);
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/util/helper/NeighborUpdates.java b/game/core/src/main/java/net/minecraft/core/util/helper/NeighborUpdates.java
new file mode 100644
index 000000000..70b8a9291
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/util/helper/NeighborUpdates.java
@@ -0,0 +1,351 @@
+package net.minecraft.core.util.helper;
+
+import java.util.Set;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.minecraft.core.block.Block;
+import net.minecraft.core.world.World;
+import net.minecraft.core.world.pos.TilePos;
+import net.minecraft.core.world.pos.TilePosc;
+
+public abstract class NeighborUpdates {
+    private NeighborUpdates() {}
+
+    private static final Direction Xn = Direction.WEST;
+    private static final Direction Xp = Direction.EAST;
+    private static final Direction Yn = Direction.DOWN;
+    private static final Direction Yp = Direction.UP;
+    private static final Direction Zn = Direction.NORTH;
+    private static final Direction Zp = Direction.SOUTH;
+
+    private static final @NotNull Direction[] D1_OFFSETS = { Xn, Xp, Yn, Yp, Zn, Zp };
+
+    /**
+     * updates blocks adjascent to the origin with the order -x, +x, -y, +y, -z, +z
+     */
+    public static void diamondD1(@NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block) {
+        diamondD1(null, world, pos, block);
+    }
+
+    public static void diamondD1(
+        @Nullable Set<@NotNull TilePosc> updatedPositions,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        final boolean dedupe = updatedPositions != null;
+        if (block == null) block = world.getBlockType(pos);
+
+        final var qpos = new TilePos();
+        for (var d : D1_OFFSETS) {
+            qpos.set(pos).add(d);
+
+            if (dedupe) {
+                if (updatedPositions.contains(qpos)) continue;
+                updatedPositions.add(new TilePos(qpos));
+            }
+
+            final var block1 = world.getBlockType(qpos);
+            block1.onNeighborChanged(world, qpos, block);
+        }
+    }
+
+    private static final @NotNull Direction[] @NotNull[] D2_OFFSETS = {
+        /* Xn */ { Xn, Yp, Yn, Zp, Zn },
+        /* Xp */ { Xp, Yp, Yn, Zp, Zn },
+        /* Yn */ { Yn, Zp, Zn },
+        /* Yp */ { Yp, Zp, Zn },
+        /* Zn */ { Zn },
+        /* Zp */ { Zp },
+    };
+
+    /**
+     * updates each block within the range |x| + |y| + |z| <= 2, excluding the origin.
+     * blocks in the inner shell are updated with the origin as the source,
+     * blocks in the outer shell are updated with block from the inner shell as the source,
+     * prioritizing inner shell blocks from -x, +x, -y, +y, -z, +z.
+     */ 
+    public static void diamondD2(@NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block) {
+        diamondD2(null, world, pos, block);
+    }
+
+    public static synchronized void diamondD2(
+        @Nullable Set<@NotNull TilePosc> updatedPositions,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        // class ctx { static final Block<?>[] D1_BLOCKS = new Block<?>[6]; };
+
+        final boolean dedupe = updatedPositions != null;
+        if (block == null) block = world.getBlockType(pos);
+
+        final var qpos1 = new TilePos();
+        for (int i = 0; i < 6; i++) {
+            qpos1.set(pos).add(D1_OFFSETS[i]);
+            // ctx.D1_BLOCKS[i] = world.getBlockType(qpos1);
+
+            if (dedupe) {
+                if (updatedPositions.contains(qpos1)) continue;
+                updatedPositions.add(new TilePos(qpos1));
+            }
+
+            // ctx.D1_BLOCKS[i].onNeighborChanged(world, qpos1, block);
+            world.getBlockType(qpos1).onNeighborChanged(world, qpos1, block);
+        }
+        final var qpos2 = new TilePos();
+        for (int i = 0; i < 6; i++) {
+            qpos1.set(pos).add(D1_OFFSETS[i]);
+            for (final var d : D2_OFFSETS[i]) {
+                qpos2.set(qpos1).add(d);
+
+                if (dedupe) {
+                    if (updatedPositions.contains(qpos2)) continue;
+                    updatedPositions.add(new TilePos(qpos2));
+                }
+
+                // world.getBlockType(qpos2).onNeighborChanged(world, qpos2, ctx.D1_BLOCKS[i]);
+                world.getBlockType(qpos2).onNeighborChanged(world, qpos2, block);
+            }
+        }
+    }
+
+    public static void diamondD2Shell(
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        diamondD2Shell(null, world, pos, block);
+    }
+
+    public static synchronized void diamondD2Shell(
+        @Nullable Set<@NotNull TilePosc> updatedPositions,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        // class ctx { static final Block<?>[] D1_BLOCKS = new Block<?>[6]; };
+
+        final boolean dedupe = updatedPositions != null;
+        if (block == null) block = world.getBlockType(pos);
+
+        final var qpos1 = new TilePos();
+        // for (int i = 0; i < 6; i++) {
+        //     qpos1.set(pos).add(D1_OFFSETS[i]);
+        //     // ctx.D1_BLOCKS[i] = world.getBlockType(qpos1);
+
+        //     if (dedupe) {
+        //         if (updatedPositions.contains(qpos1)) continue;
+        //         updatedPositions.add(new TilePos(qpos1));
+        //     }
+        // }
+        final var qpos2 = new TilePos();
+        for (int i = 0; i < 6; i++) {
+            qpos1.set(pos).add(D1_OFFSETS[i]);
+            for (final var d : D2_OFFSETS[i]) {
+                qpos2.set(qpos1).add(d);
+
+                if (dedupe) {
+                    if (updatedPositions.contains(qpos2)) continue;
+                    updatedPositions.add(new TilePos(qpos2));
+                }
+
+                // world.getBlockType(qpos2).onNeighborChanged(world, qpos2, ctx.D1_BLOCKS[i]);
+                world.getBlockType(qpos2).onNeighborChanged(world, qpos2, block);
+            }
+        }
+    }
+
+    /**
+     * similar to diamondUpdateD2, but only the blocks from the outer shell, that are adjascent
+     * to the block in the inner shell on the given direction, are updated.
+     */
+    public static void diamondD2SingleDir(
+        @NotNull Direction direction,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        diamondD2SingleDir(null, direction, world, pos, block);
+    }
+
+    public static void diamondD2SingleDir(
+        @Nullable Set<@NotNull TilePosc> updatedPositions,
+        @NotNull Direction direction,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        if (direction == Direction.NONE) {
+            diamondD1(updatedPositions, world, pos, block);
+            return;
+        }
+
+        final boolean dedupe = updatedPositions != null;
+        if (block == null) block = world.getBlockType(pos);
+
+        final var opposite = direction.opposite();
+        final var qpos = new TilePos();
+        
+        // Block<?> sideBlock = null;
+        for (int i = 0; i < 6; i++) {
+            final var d = D1_OFFSETS[i];
+            qpos.set(pos).add(d);
+
+            if (dedupe) {
+                if (updatedPositions.contains(qpos)) continue;
+                updatedPositions.add(new TilePos(qpos));
+            }
+
+            final var b = world.getBlockType(qpos);
+            // if (d == direction) sideBlock = b;
+
+            b.onNeighborChanged(world, qpos, block);
+        }
+        
+        // assert sideBlock != null;
+        for (int i = 0; i < 6; i++) {
+            final var d = D1_OFFSETS[i];
+            if (d == opposite) continue;
+            qpos.set(pos).add(direction).add(d);
+
+            if (dedupe) {
+                if (updatedPositions.contains(qpos)) continue;
+                updatedPositions.add(new TilePos(qpos));
+            }
+
+            final var b = world.getBlockType(qpos);
+            b.onNeighborChanged(world, qpos, block);
+            // b.onNeighborChanged(world, qpos, sideBlock);
+        }
+    }
+
+    /**
+     * updates blocks within the range |x| + |y| + |z| <= radius, excluding the origin.
+     * for every N's layer is updated with the N-1's layer as the source.
+     * for layers >= 3, the order no longer folows vanilla :(
+     */
+    public static void diamondDn(
+        int radius,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        diamondDn(null, radius, world, pos, block);
+    }
+
+    public static void diamondDn(
+        @Nullable Set<@NotNull TilePosc> updatedPositions,
+        int radius,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        if (radius <= 0) return;
+        if (block == null) block = world.getBlockType(pos);
+
+        switch (radius) {
+        case 1:
+            diamondD1(updatedPositions, world, pos, block);
+            break;
+        case 2:
+            diamondD2(updatedPositions, world, pos, block);
+            break;
+        default:
+            diamondD2(updatedPositions, world, pos, block);
+            final boolean dedupe = updatedPositions != null;
+            for (int shell = 3; shell <= radius; shell++) {
+                if (dedupe) {
+                    diamondDnShell(updatedPositions, shell, world, pos, block);
+                } else {
+                    diamondDnShell(shell, world, pos, block);
+                }
+            }
+            break;
+        }
+    }
+
+    public static record Diamond(@NotNull TilePos origin, int radius, @Nullable Block<?> block) {
+        public @NotNull Block<?> block(@NotNull World world) {
+            return (this.block != null)
+                ? this.block
+                : world.getBlockType(this.origin);
+        }
+    }
+
+    public static synchronized void diamondDnShell(
+        int radius,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        class ctx { static final ObjectOpenHashSet<TilePosc> POS_UPDATED = new ObjectOpenHashSet<>(); }
+        if (radius <= 0) return;
+
+        switch (radius) {
+        case 1:
+            diamondD1(world, pos, null);
+            return;
+        case 2:
+            diamondD2Shell(world, pos, null);
+            return;
+        }
+
+        final int LEN = 3 * radius * (radius - 1);
+        ctx.POS_UPDATED.clear();
+        ctx.POS_UPDATED.ensureCapacity(LEN);
+        
+        diamondDnShell(ctx.POS_UPDATED, radius, world, pos, block);
+        
+        ctx.POS_UPDATED.clear();
+        if (LEN >= 1024) ctx.POS_UPDATED.trim(512);
+    }
+
+    private static int sign(int n) { return (n == 0) ? 0 : (n < 0) ? -1 : 1; }
+
+    // TODO now that I know diamond updates should always pass the origin block
+    // TODO   as the update source, this can be made much more efficient
+    public static void diamondDnShell(
+        // This one cannot be null
+        @NotNull Set<@NotNull TilePosc> updatedPositions,
+        int radius,
+        @NotNull World world, @NotNull TilePosc pos, @Nullable Block<?> block
+    ) {
+        switch (radius) {
+        case 1:
+            diamondD1(updatedPositions, world, pos, null);
+            return;
+        case 2:
+            diamondD2Shell(updatedPositions, world, pos, null);
+            return;
+        }
+
+        radius --;
+        if (radius <= 0) return;
+
+        if (block == null) block = world.getBlockType(pos);
+
+        final TilePos s_query = new TilePos();
+        final TilePos t_query = new TilePos();
+
+        for (int x = -radius; x <= radius; x ++) {
+            final int x_sign = sign(x);
+            final int y_radius = radius - Math.abs(x); // 0, 1, 2, ... targetR, ... 2, 1, 0
+
+            for (int y = -y_radius; y <= y_radius; y++) {
+                final int y_sign = sign(y);
+                final int z_radius = y_radius - Math.abs(y);
+
+                for (int z = -z_radius; z <= z_radius; z++) { // [-1 0 1] or [0]
+                    final int z_sign = sign(z);
+
+                    s_query.set(pos).add(x, y, z_radius);
+
+                    for (final var dir : D1_OFFSETS) {
+                        final int dx_sign = sign(dir.offsetX());
+                        final int dy_sign = sign(dir.offsetY());
+                        final int dz_sign = sign(dir.offsetZ());
+
+                        if (Math.abs(x_sign - dx_sign) > 1) continue;
+                        if (Math.abs(y_sign - dy_sign) > 1) continue;
+                        if (Math.abs(z_sign - dz_sign) > 1) continue;
+
+                        // t_query.set(s_query).add(dir);
+
+                        if (updatedPositions.contains(t_query)) continue;
+                        updatedPositions.add(new TilePos(t_query));
+
+                        // final var sblock = world.getBlockType(s_query);
+                        final var tblock = world.getBlockType(t_query);
+                        tblock.onNeighborChanged(world, t_query, block);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/util/helper/Side.java b/game/core/src/main/java/net/minecraft/core/util/helper/Side.java
index 84e23ab63..3c497cc9b 100644
--- a/game/core/src/main/java/net/minecraft/core/util/helper/Side.java
+++ b/game/core/src/main/java/net/minecraft/core/util/helper/Side.java
@@ -4,13 +4,20 @@ import org.jetbrains.annotations.NotNull;
 
 public enum Side {
 	
-	TOP(Axis.Y, 0, 1, 0, "top"),
-	BOTTOM(Axis.Y, 0, -1, 0, "bottom"),
-	NORTH(Axis.Z, 0, 0, -1, "north"),
-	EAST(Axis.X, 1, 0, 0, "east"),
-	SOUTH(Axis.Z, 0, 0, 1, "south"),
-	WEST(Axis.X, -1, 0, 0, "west"),
-	NONE(Axis.NONE, 0, 0, 0, "none");
+	BOTTOM(Direction.DOWN, "bottom"),
+	TOP(Direction.UP, "top"),
+	NORTH(Direction.NORTH, "north"),
+	SOUTH(Direction.SOUTH, "south"),
+	WEST(Direction.WEST, "west"),
+	EAST(Direction.EAST, "east"),
+
+    /**
+     * Method implementations might not guarantee correct behaviour (error safety) when passed
+     * Side.NONE. Prefer passing {@code null} if method accepts {@code @Nullable Side},
+     * or an acceptable default side, or avoid calling the method entirely!
+     */
+	@Deprecated
+	NONE(Direction.NONE, "none");
 	
 	public static final Side posX = EAST;
 	public static final Side negX = WEST;
@@ -19,106 +26,64 @@ public enum Side {
 	public static final Side posZ = SOUTH;
 	public static final Side negZ = NORTH;
 	
-	public static final Side[] sides = new Side[6];
-	
-	static {
-		setId(BOTTOM, 0);
-		setId(TOP, 1);
-		setId(NORTH, 2);
-		setId(SOUTH, 3);
-		setId(WEST, 4);
-		setId(EAST, 5);
-		NONE.id = -1;
-		
-		setOpposites(EAST, WEST);
-		setOpposites(TOP, BOTTOM);
-		setOpposites(NORTH, SOUTH);
-		NONE.opposite = NONE;
-		
-		TOP.direction = Direction.UP;
-		BOTTOM.direction = Direction.DOWN;
-		NORTH.direction = Direction.NORTH;
-		EAST.direction = Direction.EAST;
-		SOUTH.direction = Direction.SOUTH;
-		WEST.direction = Direction.WEST;
-		NONE.direction = Direction.NONE;
-	}
-	
-	private static void setId(Side side, int id) {
-		sides[id] = side;
-		side.id = id;
-	}
-	
-	private static void setOpposites(Side side1, Side side2) {
-		side1.opposite = side2;
-		side2.opposite = side1;
-	}
+	public static final Side[] sides = {
+		BOTTOM, TOP,
+		NORTH, SOUTH,
+		WEST, EAST,
+	};
 	
 	/////////////////////
 	
-	public static Side getSideById(int i) {
-		if(i >= 0 && i < sides.length) {
-			return sides[i];
-		}else {
-			return NONE;
-		}
+	public static Side fromId(int i) {
+		return (i >= 0 && i < 6)
+			? sides[i]
+			: Side.NONE;
 	}
-	
+
 	/////////////////////
 
-	private final Axis axis;
-	private int id;
-	private Side opposite;
-	private Direction direction;
-	
-	private final int offsetX;
-	private final int offsetY;
-	private final int offsetZ;
+	public final int id;
+	public final @NotNull Direction direction;
 
-	private final String translationKey;
+	// Why arent we using this
+	private final @NotNull String translationKey;
 	
-	Side(Axis axis, int offsetX, int offsetY, int offsetZ, String translationKey) {
-		this.axis = axis;
-		this.offsetX = offsetX;
-		this.offsetY = offsetY;
-		this.offsetZ = offsetZ;
+	Side(@NotNull Direction direction, @NotNull String translationKey) {
+		this.direction = direction;
+		this.id = direction.id;
 		this.translationKey = translationKey;
 	}
 	
-	public int getOffsetX() {
-		return offsetX;
+	public int offsetX() {
+		return this.direction.offsetX();
 	}
 	
-	public int getOffsetY() {
-		return offsetY;
+	public int offsetY() {
+		return this.direction.offsetY();
 	}
 	
-	public int getOffsetZ() {
-		return offsetZ;
+	public int offsetZ() {
+		return this.direction.offsetZ();
 	}
 	
-	public @NotNull Side getOpposite() {
-		return opposite;
+	public @NotNull Side opposite() {
+		return sides[this.id ^ 1];
 	}
 	
-	public @NotNull Direction getDirection() {
-		return direction;
+	public @NotNull Direction direction() {
+		return this.direction;
 	}
 	
-	public @NotNull Axis getAxis() {
-		return axis;
+	public @NotNull Axis axis() {
+		return this.direction.axis();
 	}
 	
-	public int getId() {
-		return id;
-	}
-
 	public boolean isVertical() {
-		return getAxis().isVertical();
+		return this.direction.isVertical();
 	}
 	
 	public boolean isHorizontal() {
-		return !isVertical();
+		return this.direction.isHorizontal();
 	}
 	
 }
diff --git a/game/core/src/main/java/net/minecraft/core/util/helper/Sides.java b/game/core/src/main/java/net/minecraft/core/util/helper/Sides.java
index a282369be..98c668058 100644
--- a/game/core/src/main/java/net/minecraft/core/util/helper/Sides.java
+++ b/game/core/src/main/java/net/minecraft/core/util/helper/Sides.java
@@ -16,6 +16,7 @@ public class Sides
 	public static final int negX = 4;
 	public static final int posX = 5;
 
+	// TODO replace with Direction.rotate<X | Y | Z>
 	public static final int[] orientationLookUpHorizontal = {
 			bottom, top, south, north, west, east,
 			bottom, top, south, north, west, east,
diff --git a/game/core/src/main/java/net/minecraft/core/world/ICarriable.java b/game/core/src/main/java/net/minecraft/core/world/ICarriable.java
index d2a19fd8d..da753bdeb 100644
--- a/game/core/src/main/java/net/minecraft/core/world/ICarriable.java
+++ b/game/core/src/main/java/net/minecraft/core/world/ICarriable.java
@@ -3,6 +3,7 @@ package net.minecraft.core.world;
 import com.mojang.logging.LogUtils;
 import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.block.motion.CarriedBlock;
+import net.minecraft.core.block.piston.CarriedPistonHead;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.util.helper.Side;
 import org.slf4j.Logger;
@@ -11,9 +12,10 @@ public interface ICarriable {
     Logger LOGGER = LogUtils.getLogger();
     /**
      * Called every tick an entity holds this object
-     */
-    void heldTick(World world, Entity holder);
-
+    */
+   void heldTick(World world, Entity holder);
+   
+    // TODO TilePos refactor
     /**
      * Called by the entity when trying to place the held object at some position
      * @return True if carried object could place, false otherwise
@@ -33,13 +35,17 @@ public interface ICarriable {
 
     void readFromNBT(CompoundTag tag);
 
-    String TYPE_BLOCK = "block";
+    static final String TYPE_BLOCK = "block";
+    static final String TYPE_PISTON_HEAD = "block/piston_head";
 
     static ICarriable createAndLoadCarriable(Entity holder, CompoundTag tag) {
         String type = tag.getString("type");
         if (type.equalsIgnoreCase(TYPE_BLOCK)) {
             return CarriedBlock.createAndLoadCarriedBlock(holder, tag);
         }
+        if (type.equalsIgnoreCase(TYPE_PISTON_HEAD)) {
+            return CarriedPistonHead.createAndLoad(tag);
+        }
         LOGGER.warn("Could not identify carriable for type {}!", type);
         return null;
     }
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 96650f788..5f83f8b14 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
@@ -2,6 +2,7 @@ package net.minecraft.core.world;
 
 import com.mojang.logging.LogUtils;
 import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
 import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
 import net.minecraft.core.Global;
 import net.minecraft.core.NextTickListEntry;
@@ -32,6 +33,7 @@ import net.minecraft.core.util.debug.Debug;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.LightIndexHelper;
 import net.minecraft.core.util.helper.MathHelper;
+import net.minecraft.core.util.helper.NeighborUpdates;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.util.phys.HitResult;
 import net.minecraft.core.world.biome.Biome;
@@ -634,6 +636,22 @@ public abstract class World implements MutableWorldSource {
         }
     }
 
+    public boolean setBlockTypeDataEntity(final @NotNull TilePosc tilePos, final @NotNull Block<?> block, final int data, final @NotNull TileEntity entity) {
+        if (!tilePos.inBounds(this)) return false;
+        
+        final var c = this.getChunk(tilePos);
+        final var ctp = new ChunkTilePos(tilePos);
+        final var prevBlock = c.getBlock(ctp);
+        final var prevData = c.getBlockData(ctp);
+        if (!c.setBlockIdDataRaw(ctp, block.id(), data)) return false;
+
+        this.replaceTileEntity(tilePos, entity);
+        prevBlock.getLogic().onRemoved(this, tilePos, prevData);
+        block.getLogic().onPlacedByWorld(this, tilePos);
+
+        return true;
+    }
+
     public boolean setBlockTypeRaw(final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
         return this.setBlockTypeDataRaw(tilePos, block, this.getBlockData(tilePos));
     }
@@ -656,6 +674,10 @@ public abstract class World implements MutableWorldSource {
         return this.setBlockTypeDataRaw(new TilePos(x, y, z), Objects.requireNonNull(Blocks.getBlock(id)), metadata);
     }
 
+    public boolean setBlockTypeEntity(final @NotNull TilePosc tilePos, final @NotNull Block<?> block, final @NotNull TileEntity entity) {
+        return this.setBlockTypeDataEntity(tilePos, block, this.getBlockData(tilePos), entity);
+    }
+
     @Override
     public boolean setBlockType(final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
         if (!tilePos.inBounds(this)) {
@@ -708,6 +730,15 @@ public abstract class World implements MutableWorldSource {
         }
     }
 
+    public boolean setBlockTypeEntityNotify(final @NotNull TilePosc tilePos, final @NotNull Block<?> block, final @NotNull TileEntity entity) {
+        if (this.setBlockTypeEntity(tilePos, block, entity)) {
+            this.notifyBlockChange(tilePos, block);
+            return true;
+        }
+
+        return false;
+    }
+
     public boolean setBlockTypeNotify(final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
         if (this.setBlockType(tilePos, block)) {
             this.notifyBlockChange(tilePos, block);
@@ -722,6 +753,15 @@ public abstract class World implements MutableWorldSource {
         return this.setBlockTypeNotify(new TilePos(x, y, z), Objects.requireNonNull(Blocks.getBlock(id)));
     }
 
+    public boolean setBlockTypeDataEntityNotify(final @NotNull TilePosc tilePos, final @NotNull Block<?> block, final int data, final @NotNull TileEntity entity) {
+        if (this.setBlockTypeDataEntity(tilePos, block, data, entity)) {
+            this.notifyBlockChange(tilePos, block);
+            return true;
+        }
+
+        return false;
+    }
+
     public boolean setBlockTypeDataNotify(final @NotNull TilePosc tilePos, final @NotNull Block<?> block, final int data) {
         if (this.setBlockTypeData(tilePos, block, data)) {
             this.notifyBlockChange(tilePos, block);
@@ -790,27 +830,114 @@ public abstract class World implements MutableWorldSource {
         this.markBlocksDirty(new TilePos(minX, minY, minZ), new TilePos(maxX, maxY, maxZ));
     }
 
-    public void notifyBlocksOfNeighborChange(final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        final TilePos queryPos = new TilePos();
-        this.notifyBlockOfNeighborChange(tilePos.add(Direction.WEST, queryPos), block);
-        this.notifyBlockOfNeighborChange(tilePos.add(Direction.EAST, queryPos), block);
-        this.notifyBlockOfNeighborChange(tilePos.add(Direction.DOWN, queryPos), block);
-        this.notifyBlockOfNeighborChange(tilePos.add(Direction.UP, queryPos), block);
-        this.notifyBlockOfNeighborChange(tilePos.add(Direction.NORTH, queryPos), block);
-        this.notifyBlockOfNeighborChange(tilePos.add(Direction.SOUTH, queryPos), block);
+    /**
+     * Notify blocks adjacent to the given position
+     * of neighbor update originated from the given block,
+     * or the block at the given position if null.
+     */
+    public void notifyBlocksOfNeighborChange(final @NotNull TilePosc tilePos, final @Nullable Block<?> block) {
+        if (this.noNeighborUpdate || this.isClientSide) return;
+        NeighborUpdates.diamondD1(this, tilePos, block);
     }
 
-    @Deprecated
-    public final void notifyBlocksOfNeighborChange(final int x, final int y, final int z, final int id) {
-        this.notifyBlocksOfNeighborChange(new TilePos(x, y, z), Objects.requireNonNull(Blocks.getBlock(id)));
+    /**
+     * Notify blocks in the provided radius -- such that (|dx| + |dy| + |dz| <= radius) --
+     * from the given position of neighbor update originated from the given block,
+     * or the block at the given position if null.
+     */
+    public void notifyBlocksInRadiusOfNeighborChange(
+        final int radius, 
+        final @NotNull TilePosc tilePos, final @Nullable Block<?> block
+    ) {
+        if (this.noNeighborUpdate || this.isClientSide) return;
+        NeighborUpdates.diamondDn(radius, this, tilePos, block);
     }
 
-    private void notifyBlockOfNeighborChange(final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
-        if (this.noNeighborUpdate || this.isClientSide) {
-            return;
+    /**
+     * Notify blocks with exactly the provided radius -- such that (|dx| + |dy| + |dz| == radius) --
+     * from the given position of neighbor update originated from the given block,
+     * or the block at the given position if null.
+     */
+    public void notifyShellBlocksInRadiusOfNeighborChange(
+        final int radius, final @NotNull TilePosc tilePos, @Nullable Block<?> block
+    ) {
+        if (this.noNeighborUpdate || this.isClientSide) return;
+        NeighborUpdates.diamondDnShell(radius, this, tilePos, block);
+    }
+
+    /**
+     * Notify adjacent blocks from the given position,
+     * and from the adjacent position on the given direction of the position,
+     * of neighbor update originated from the given block,
+     * or the block at the given position if null (avoiding duplicated updates).
+     */
+    public void notifyBlocksInCapsuleOfNeighborChange(
+        @NotNull Direction direction, final @NotNull TilePosc tilePos,
+        final @Nullable Block<?> block
+    ) {
+        if (this.noNeighborUpdate || this.isClientSide) return;
+        NeighborUpdates.diamondD2SingleDir(direction, this, tilePos, block);
+    }
+
+    /**
+     * Notify adjacent blocks from the given position,
+     * and from the position on the given side of the position,
+     * of neighbor update originated from the given block,
+     * or the block at the given position if null (avoiding duplicated updates).
+     */
+    public void notifyBlocksInCapsuleOfNeighborChange(
+        @NotNull Side side, final @NotNull TilePosc tilePos,
+        final @Nullable Block<?> block
+    ) {
+        this.notifyBlocksInCapsuleOfNeighborChange(side.direction(), tilePos, block);
+    }
+
+    /**
+     * Notify blocks adjacent to every provided positions,
+     * of neighbor update originated from the given block,
+     * or the block at *each* positions if null (avoiding duplicated updates).
+     */
+    public synchronized void notifyBlocksOfNeighborChanges(
+        final @NotNull TilePosc @NotNull[] tilePositions, @Nullable Block<?> block
+    ) {
+        class ctx { static final ObjectOpenHashSet<TilePosc> CACHE = new ObjectOpenHashSet<>(); }
+        if (this.noNeighborUpdate || this.isClientSide) return;
+        ctx.CACHE.clear();
+        ctx.CACHE.ensureCapacity(tilePositions.length * (6 / 2));
+
+        for (var pos : tilePositions) {
+            NeighborUpdates.diamondD1(ctx.CACHE, this, pos, block);
         }
 
-        this.getBlockType(tilePos).onNeighborChanged(this, tilePos, block);
+        final var size = ctx.CACHE.size();
+        ctx.CACHE.clear();
+        if (size >= 1024) ctx.CACHE.trim(512);
+    }
+
+    /**
+     * Notify blocks adjacent to every provided positions,
+     * of neighbor update originated from the given block,
+     * or the block at *each* positions if null (avoiding duplicated updates).
+     */
+    public synchronized <T extends TilePosc> void notifyBlocksOfNeighborChanges(
+        final @NotNull Iterable<@NotNull T> tilePositions, @Nullable Block<?> block
+    ) {
+        class ctx { static final ObjectOpenHashSet<TilePosc> CACHE = new ObjectOpenHashSet<>(); }
+        if (this.noNeighborUpdate || this.isClientSide) return;
+        ctx.CACHE.clear();
+
+        for (var pos : tilePositions) {
+            NeighborUpdates.diamondD1(ctx.CACHE, this, pos, block);
+        }
+
+        final var size = ctx.CACHE.size();
+        ctx.CACHE.clear();
+        if (size >= 1024) ctx.CACHE.trim(512);
+    }
+
+    @Deprecated
+    public final void notifyBlocksOfNeighborChange(final int x, final int y, final int z, final int id) {
+        this.notifyBlocksOfNeighborChange(new TilePos(x, y, z), Objects.requireNonNull(Blocks.getBlock(id)));
     }
 
     public boolean canBlockSeeSky(final @NotNull TilePosc tilePos) {
@@ -1824,7 +1951,7 @@ public abstract class World implements MutableWorldSource {
     }
 
     public void onBlockHit(final @NotNull Player player, final @NotNull TilePosc tilePos, final @NotNull Side side) {
-        final @NotNull TilePos offsetPos = tilePos.add(side.getDirection(), new TilePos());
+        final @NotNull TilePos offsetPos = tilePos.add(side.direction(), new TilePos());
         final @Nullable Block<?> block = this.getBlockType(offsetPos);
 
         if (block == Blocks.FIRE || block == Blocks.FIRE_COLD) {
@@ -1893,16 +2020,6 @@ public abstract class World implements MutableWorldSource {
         return block != null && block.isSolidRender();
     }
 
-    public boolean canPlaceOnSurfaceOfBlock(final @NotNull TilePos tilePos) {
-        final @Nullable Block<?> block = this.getBlockType(tilePos);
-        return block != null && block.canPlaceOnSurfaceOnCondition(this, tilePos.x, tilePos.y, tilePos.z);
-    }
-
-    @Deprecated
-    public final boolean canPlaceOnSurfaceOfBlock(final int x, final int y, final int z) {
-        return this.canPlaceOnSurfaceOfBlock(new TilePos(x, y, z));
-    }
-
     public @NotNull ISupport getSupport(final @NotNull TilePos tilePos, final @NotNull Side side) {
         return this.getBlockType(tilePos).getSupport(this, tilePos, side);
     }
@@ -2215,38 +2332,37 @@ public abstract class World implements MutableWorldSource {
     }
 
     public <T extends Entity> @NotNull List<@NotNull T> getEntitiesWithinAABB(@NotNull final Class<T> ofClass, @NotNull final AABBdc aabb) {
+        this.entityBuffer.clear();
         final int minX = MathHelper.floor((aabb.minX() - 2D) / Chunk.CHUNK_SIZE_X);
         final int maxX = MathHelper.floor((aabb.maxX() + 2D) / Chunk.CHUNK_SIZE_X);
         final int minZ = MathHelper.floor((aabb.minZ() - 2D) / Chunk.CHUNK_SIZE_Z);
         final int maxZ = MathHelper.floor((aabb.maxZ() + 2D) / Chunk.CHUNK_SIZE_Z);
-        final List<@NotNull T> entities = new ArrayList<>();
         for (int x = minX; x <= maxX; x++) {
             for (int z = minZ; z <= maxZ; z++) {
                 if (this.isChunkLoaded(x, z)) {
-                    this.getChunkFromChunkCoords(x, z).getEntitiesWithin(ofClass, aabb, entities);
+                    this.getChunkFromChunkCoords(x, z).getEntitiesWithin(ofClass, aabb, (List<T>)this.entityBuffer);
                 }
             }
-
         }
 
-        return entities;
+        return (List<T>)this.entityBuffer;
     }
 
     public <T extends Entity> @NotNull List<@NotNull T> getEntitiesWithinRadius(@NotNull final Class<T> ofClass, final double x, final double y, final double z, final double radius) {
+        this.entityBuffer.clear();
         final int minX = MathHelper.floor(x - radius);
         final int minZ = MathHelper.floor(z - radius);
         final int maxX = MathHelper.ceil(x + radius);
         final int maxZ = MathHelper.ceil(z + radius);
-        final List<@NotNull T> entities = new ArrayList<>();
         for (int bx = minX; bx <= maxX; bx++) {
             for (int bz = minZ; bz <= maxZ; bz++) {
                 if (this.isChunkLoaded(bx, bz)) {
-                    this.getChunkFromChunkCoords(bx, bz).getEntitiesWithinRadius(ofClass, x, y, z, radius, entities);
+                    this.getChunkFromChunkCoords(bx, bz).getEntitiesWithinRadius(ofClass, x, y, z, radius, (List<T>)this.entityBuffer);
                 }
             }
         }
 
-        return entities;
+        return (List<T>)this.entityBuffer;
     }
 
     public @NotNull List<@NotNull Entity> getLoadedEntityList() {
@@ -2377,7 +2493,7 @@ public abstract class World implements MutableWorldSource {
     public boolean hasDirectSignal(final @NotNull TilePosc tilePos) {
         final TilePos queryPos = new TilePos();
         for (final @NotNull Side side : Side.sides) {
-            if (this.hasDirectSignal(tilePos.add(side.getDirection(), queryPos), side)) {
+            if (this.hasDirectSignal(tilePos.add(side.direction(), queryPos), side)) {
                 return true;
             }
         }
@@ -2411,10 +2527,10 @@ public abstract class World implements MutableWorldSource {
     // Special version of hasDirectSignal that excludes the front side of the redstone lantern.
     private boolean pumpkinHasDirectSignal(final @NotNull TilePosc tilePos) {
         for (final @NotNull Side side : Side.sides) {
-            if (Side.getSideById(this.getBlockData(tilePos)) == side) {
+            if (Side.fromId(this.getBlockData(tilePos)) == side) {
                 continue;
             }
-            if (this.hasDirectSignal(tilePos.add(side.getDirection(), new TilePos()), side)) {
+            if (this.hasDirectSignal(tilePos.add(side.direction(), new TilePos()), side)) {
                 return true;
             }
         }
@@ -2424,7 +2540,7 @@ public abstract class World implements MutableWorldSource {
 
     public boolean hasNeighborSignal(final @NotNull TilePosc tilePos) {
         for (final @NotNull Side side : Side.sides) {
-            if (this.hasSignal(tilePos.add(side.getDirection(), new TilePos()), side)) {
+            if (this.hasSignal(tilePos.add(side.direction(), new TilePos()), side)) {
                 return true;
             }
         }
@@ -2703,10 +2819,8 @@ public abstract class World implements MutableWorldSource {
     }
 
     public void triggerEvent(final @NotNull TilePosc tilePos, final int index, final int data) {
-        final @Nullable Block<?> block = this.getBlockType(tilePos);
-        if (block != null) {
-            block.triggerEvent(this, tilePos.x(), tilePos.y(), tilePos.z(), index, data);
-        }
+        final @NotNull Block<?> block = this.getBlockType(tilePos);
+        block.triggerEvent(this, tilePos, index, data);
     }
 
     @Deprecated
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureGlowstone.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureGlowstone.java
index 3005f2033..c9e4b03e7 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureGlowstone.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureGlowstone.java
@@ -30,8 +30,7 @@ public class WorldFeatureGlowstone implements WorldFeatureInterface
                 continue;
             }
             int adjacentGlowstone = 0;
-            for(int d = 0; d < Direction.directions.length; d++) {
-                Direction dir = Direction.getDirectionById(d);
+            for(final var dir : Direction.all) {
                 if(world.getBlockType(queryPos.set(rx, ry, rz).add(dir)) == Blocks.GLOWSTONE) {
                     adjacentGlowstone++;
                 }
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureMoss.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureMoss.java
index 5169d0125..41a5856f9 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureMoss.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureMoss.java
@@ -104,7 +104,7 @@ public class WorldFeatureMoss extends WorldFeature
     public boolean airExposed(@NotNull World world, int x, int y, int z) {
         for (int i = 0; i < Side.sides.length; i++) {
             Side s = Side.sides[i];
-            if (world.isAirBlock(x + s.getOffsetX(), y + s.getOffsetY(), z + s.getOffsetZ())) return true;
+            if (world.isAirBlock(x + s.offsetX(), y + s.offsetY(), z + s.offsetZ())) return true;
         }
         return false;
     }
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureTerrace.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureTerrace.java
index 7023836ab..20d342f65 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureTerrace.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureTerrace.java
@@ -83,7 +83,7 @@ public class WorldFeatureTerrace extends WorldFeature
             return false;
         }
 
-        for (Direction dir : Direction.directions) {
+        for (final var dir : Direction.all) {
             cursor.add(dir, query);
             if(dir == Direction.UP){
                 if (!world.isAirBlock(query)) {
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreePalm.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreePalm.java
index 55b4711d8..a00989b78 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreePalm.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreePalm.java
@@ -101,7 +101,7 @@ public class WorldFeatureTreePalm extends WorldFeature {
 		}else {
 			bend = random.nextFloat() * 3.0f + 2.0f;
 		}
-		Direction direction = Direction.horizontalDirections[random.nextInt(4)];
+		Direction direction = Direction.horizontal[random.nextInt(4)];
 		
 		return generateTrunkIfPossible(height, angle, bend, direction);
 	}
@@ -115,9 +115,9 @@ public class WorldFeatureTreePalm extends WorldFeature {
 			float yFactor = i / (float) height;
 			int offset = MathHelper.floor(Math.pow(yFactor, angle + 1.0f) * bend);
 			
-			int offsetX = direction.getOffsetX() * offset;
+			int offsetX = direction.offsetX() * offset;
 			int offsetY = i;
-			int offsetZ = direction.getOffsetZ() * offset;
+			int offsetZ = direction.offsetZ() * offset;
 			
 			intList.add(encode(offsetX, offsetY, offsetZ));
 		}
diff --git a/game/core/src/main/java/net/minecraft/core/world/pos/ChunkPos.java b/game/core/src/main/java/net/minecraft/core/world/pos/ChunkPos.java
index 102c2c9f1..b8d865082 100644
--- a/game/core/src/main/java/net/minecraft/core/world/pos/ChunkPos.java
+++ b/game/core/src/main/java/net/minecraft/core/world/pos/ChunkPos.java
@@ -75,7 +75,7 @@ public final class ChunkPos implements Externalizable, Cloneable, ChunkPosc {
 
     @Override
     public @NotNull ChunkPos add(@NotNull Direction dir, @NotNull ChunkPos dest) {
-        return add(dir.getOffsetX(), dir.getOffsetZ(), dest);
+        return add(dir.offsetX(), dir.offsetZ(), dest);
     }
 
     public @NotNull ChunkPos add(int x, int z) {
@@ -102,7 +102,7 @@ public final class ChunkPos implements Externalizable, Cloneable, ChunkPosc {
 
     @Override
     public @NotNull ChunkPos sub(@NotNull Direction dir, @NotNull ChunkPos dest) {
-        return sub(dir.getOffsetX(), dir.getOffsetZ(), dest);
+        return sub(dir.offsetX(), dir.offsetZ(), dest);
     }
 
     public @NotNull ChunkPos sub(int x, int z) {
diff --git a/game/core/src/main/java/net/minecraft/core/world/pos/ChunkSectionTilePos.java b/game/core/src/main/java/net/minecraft/core/world/pos/ChunkSectionTilePos.java
index 9fd7f0933..a1b5f637b 100644
--- a/game/core/src/main/java/net/minecraft/core/world/pos/ChunkSectionTilePos.java
+++ b/game/core/src/main/java/net/minecraft/core/world/pos/ChunkSectionTilePos.java
@@ -94,7 +94,7 @@ public class ChunkSectionTilePos implements Externalizable, Cloneable, ChunkSect
 
     @Override
     public @NotNull ChunkSectionTilePos add(@NotNull Direction dir, @NotNull ChunkSectionTilePos dest) {
-        return add(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), dest);
+        return add(dir.offsetX(), dir.offsetY(), dir.offsetZ(), dest);
     }
 
     public @NotNull ChunkSectionTilePos add(int x, int y, int z) {
@@ -106,7 +106,7 @@ public class ChunkSectionTilePos implements Externalizable, Cloneable, ChunkSect
     }
 
     public @NotNull ChunkSectionTilePos add(@NotNull Direction dir) {
-        return add(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), this);
+        return add(dir.offsetX(), dir.offsetY(), dir.offsetZ(), this);
     }
 
     @Override
@@ -121,7 +121,7 @@ public class ChunkSectionTilePos implements Externalizable, Cloneable, ChunkSect
 
     @Override
     public @NotNull ChunkSectionTilePos sub(@NotNull Direction dir, @NotNull ChunkSectionTilePos dest) {
-        return sub(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), dest);
+        return sub(dir.offsetX(), dir.offsetY(), dir.offsetZ(), dest);
     }
 
     public @NotNull ChunkSectionTilePos sub(int x, int y, int z) {
@@ -133,7 +133,7 @@ public class ChunkSectionTilePos implements Externalizable, Cloneable, ChunkSect
     }
 
     public @NotNull ChunkSectionTilePos sub(@NotNull Direction dir) {
-        return sub(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), this);
+        return sub(dir.offsetX(), dir.offsetY(), dir.offsetZ(), this);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/world/pos/ChunkTilePos.java b/game/core/src/main/java/net/minecraft/core/world/pos/ChunkTilePos.java
index 97182c78c..dfe28ea69 100644
--- a/game/core/src/main/java/net/minecraft/core/world/pos/ChunkTilePos.java
+++ b/game/core/src/main/java/net/minecraft/core/world/pos/ChunkTilePos.java
@@ -94,7 +94,7 @@ public class ChunkTilePos implements Externalizable, Cloneable, ChunkTilePosc {
 
     @Override
     public @NotNull ChunkTilePos add(@NotNull Direction dir, @NotNull ChunkTilePos dest) {
-        return add(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), dest);
+        return add(dir.offsetX(), dir.offsetY(), dir.offsetZ(), dest);
     }
 
     public @NotNull ChunkTilePos add(int x, int y, int z) {
@@ -106,7 +106,7 @@ public class ChunkTilePos implements Externalizable, Cloneable, ChunkTilePosc {
     }
 
     public @NotNull ChunkTilePos add(@NotNull Direction dir) {
-        return add(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), this);
+        return add(dir.offsetX(), dir.offsetY(), dir.offsetZ(), this);
     }
 
     @Override
@@ -121,7 +121,7 @@ public class ChunkTilePos implements Externalizable, Cloneable, ChunkTilePosc {
 
     @Override
     public @NotNull ChunkTilePos sub(@NotNull Direction dir, @NotNull ChunkTilePos dest) {
-        return sub(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), dest);
+        return sub(dir.offsetX(), dir.offsetY(), dir.offsetZ(), dest);
     }
 
     public @NotNull ChunkTilePos sub(int x, int y, int z) {
@@ -133,7 +133,7 @@ public class ChunkTilePos implements Externalizable, Cloneable, ChunkTilePosc {
     }
 
     public @NotNull ChunkTilePos sub(@NotNull Direction dir) {
-        return sub(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), this);
+        return sub(dir.offsetX(), dir.offsetY(), dir.offsetZ(), this);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/world/pos/TilePos.java b/game/core/src/main/java/net/minecraft/core/world/pos/TilePos.java
index aedd08e22..58453ec8f 100644
--- a/game/core/src/main/java/net/minecraft/core/world/pos/TilePos.java
+++ b/game/core/src/main/java/net/minecraft/core/world/pos/TilePos.java
@@ -134,11 +134,11 @@ public final class TilePos implements Externalizable, Cloneable, TilePosc {
 
     @Override
     public @NotNull TilePos add(@NotNull Direction dir, @NotNull TilePos dest) {
-        return add(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), dest);
+        return dir.offset(dest.set(this));
     }
 
     public @NotNull TilePos add(@NotNull Side side, @NotNull TilePos dest) {
-        return add(side.getOffsetX(), side.getOffsetY(), side.getOffsetZ(), dest);
+        return add(side.offsetX(), side.offsetY(), side.offsetZ(), dest);
     }
 
     public @NotNull TilePos add(int x, int y, int z) {
@@ -169,12 +169,12 @@ public final class TilePos implements Externalizable, Cloneable, TilePosc {
 
     @Override
     public @NotNull TilePos sub(@NotNull Direction dir, @NotNull TilePos dest) {
-        return sub(dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ(), dest);
+        return dir.offsetScaled(dest.set(this), -1);
     }
 
     @Override
     public @NotNull TilePos sub(@NotNull Side side, @NotNull TilePos dest) {
-        return sub(side.getOffsetX(), side.getOffsetY(), side.getOffsetZ(), dest);
+        return sub(side.offsetX(), side.offsetY(), side.offsetZ(), dest);
     }
 
     public @NotNull TilePos sub(int x, int y, int z) {
diff --git a/game/core/src/main/java/net/minecraft/core/world/weather/WeatherSnow.java b/game/core/src/main/java/net/minecraft/core/world/weather/WeatherSnow.java
index 2d3e063ba..29b900c7b 100644
--- a/game/core/src/main/java/net/minecraft/core/world/weather/WeatherSnow.java
+++ b/game/core/src/main/java/net/minecraft/core/world/weather/WeatherSnow.java
@@ -112,9 +112,8 @@ public class WeatherSnow
                 }
             } else if (blockIdBelow == Blocks.FLUID_WATER_STILL.id() && world.getBlockMetadata(x, y - 1, z) == 0) {
                 if (rand.nextFloat() < world.getWeatherManager().getWeatherPower() * world.getWeatherManager().getWeatherIntensity()) {
-                    for (int i = 0; i < 4; i++) {
-                        Direction direction = Direction.horizontalDirections[i];
-                        Block<?> block = world.getBlock(x + direction.getOffsetX(), y - 1, z + direction.getOffsetZ());
+                    for (final var dir : Direction.horizontal) {
+                        Block<?> block = world.getBlock(x + dir.offsetX(), y - 1, z + dir.offsetZ());
                         if (block == Blocks.ICE || block != null && block.isSolidRender()) {
                             world.setBlockWithNotify(x, y - 1, z, Blocks.ICE.id());
                             break;
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/item.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/item.lang
index 1783d0edc..939eabe0b 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/item.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/item.lang
@@ -324,6 +324,8 @@ item.bed.name=Bed
 item.bed.desc=Take a nap overnight.
 item.repeater.name=Redstone Repeater
 item.repeater.desc=Delays a redstone signal.
+item.timer.name=Redstone Timer
+item.timer.desc=Continuously switches between two outputs.
 item.map.name=Map
 item.map.blank.name=Blank Map
 item.map.desc=Chart your land!
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/subtitles.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/subtitles.lang
index 28d05bfd8..a61904b28 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/subtitles.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/subtitles.lang
@@ -91,6 +91,10 @@ subtitles.ambient.weather.rain=Rainfall
 
 subtitles.tile.piston.out=Piston extended
 subtitles.tile.piston.in=Piston retracted
+subtitles.tile.piston.detached=Piston detached
+subtitles.tile.piston.attached=Piston attached
+subtitles.tile.piston.detached.out=Piston failed to extend
+subtitles.tile.piston.detached.in=Piston failed to retract
 
 subtitles.tile.tnt.fuse=TNT lit
 subtitles.tile.sensor_block.sense=Mob sense
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang
index 58413130c..1f2511f1a 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang
@@ -803,6 +803,8 @@ tile.repeater.idle.name=Redstone Repeater
 tile.repeater.idle.desc=Repeats redstone signals over a distance.
 tile.repeater.active.name=Redstone Repeater
 tile.repeater.active.desc=Repeats redstone signals over a distance.
+tile.timer.name=Redstone Timer
+tile.timer.desc=Continuously switches between two outputs.
 
 tile.piston.base.name=Piston
 tile.piston.base.desc=Pushes blocks but does not pull them.
diff --git a/game/core/src/main/resources/assets/minecraft/sounds/sounds.json b/game/core/src/main/resources/assets/minecraft/sounds/sounds.json
index bc67d6a65..0aad36ab1 100644
--- a/game/core/src/main/resources/assets/minecraft/sounds/sounds.json
+++ b/game/core/src/main/resources/assets/minecraft/sounds/sounds.json
@@ -704,6 +704,38 @@
       "tile/piston/in.ogg"
     ]
   },
+  "tile.piston.detached": {
+    "subtitle": "subtitles.tile.piston.detached",
+    "sounds": [
+      "tile/piston/out.ogg"
+    ]
+  },
+  "tile.piston.attached": {
+    "subtitle": "subtitles.tile.piston.attached",
+    "sounds": [
+      "tile/piston/in.ogg"
+    ]
+  },
+  "tile.piston.detached.in": {
+    "subtitle": "subtitles.tile.piston.detached.in",
+    "sounds": [
+      {
+        "name": "random.click",
+        "type": "event",
+        "pitch": 0.85
+      }
+    ]
+  },
+  "tile.piston.detached.out": {
+    "subtitle": "subtitles.tile.piston.detached.out",
+    "sounds": [
+      {
+        "name": "random.click",
+        "type": "event",
+        "pitch": 0.8
+      }
+    ]
+  },
   "mob.cow": {
     "subtitle": "subtitles.mob.cow",
     "sounds": [
diff --git a/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java b/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java
index e8cf4ceeb..f537cb748 100644
--- a/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java
+++ b/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java
@@ -386,12 +386,12 @@ public class PacketHandlerServer extends PacketHandler
                     j1 = i1;
                 }
                 if (this.hasMoved && this.playerEntity.distanceToSqr((double) x + 0.5D, (double) y + 0.5D, (double) z + 0.5D) < 64D && (j1 > this.mcServer.spawnProtectionRange || ignoreSpawnProtection)) {
-                    this.playerEntity.playerController.useOrPlaceItemStackOnTile(this.playerEntity, worldserver, itemstack, x, y, z, direction.getSide(), xPlaced, yPlaced);
+                    this.playerEntity.playerController.useOrPlaceItemStackOnTile(this.playerEntity, worldserver, itemstack, x, y, z, direction.side(), xPlaced, yPlaced);
                 }
                 this.playerEntity.playerNetServerHandler.sendPacket(new PacketBlockUpdate(x, y, z, worldserver));
-                x += direction.getOffsetX();
-                y += direction.getOffsetY();
-                z += direction.getOffsetZ();
+                x += direction.offsetX();
+                y += direction.offsetY();
+                z += direction.offsetZ();
                 this.playerEntity.playerNetServerHandler.sendPacket(new PacketBlockUpdate(x, y, z, worldserver));
                 break;
             }
@@ -416,12 +416,12 @@ public class PacketHandlerServer extends PacketHandler
                     j1 = i1;
                 }
                 if (this.hasMoved && this.playerEntity.distanceToSqr((double) x + 0.5D, (double) y + 0.5D, (double) z + 0.5D) < 64D && (j1 > this.mcServer.spawnProtectionRange || ignoreSpawnProtection)) {
-                    this.playerEntity.playerController.placeItemStackOnTile(this.playerEntity, worldserver, itemstack, x, y, z, direction.getSide(), xPlaced, yPlaced);
+                    this.playerEntity.playerController.placeItemStackOnTile(this.playerEntity, worldserver, itemstack, x, y, z, direction.side(), xPlaced, yPlaced);
                 }
                 this.playerEntity.playerNetServerHandler.sendPacket(new PacketBlockUpdate(x, y, z, worldserver));
-                x += direction.getOffsetX();
-                y += direction.getOffsetY();
-                z += direction.getOffsetZ();
+                x += direction.offsetX();
+                y += direction.offsetY();
+                z += direction.offsetZ();
                 this.playerEntity.playerNetServerHandler.sendPacket(new PacketBlockUpdate(x, y, z, worldserver));
                 break;
             }
@@ -738,9 +738,9 @@ public class PacketHandlerServer extends PacketHandler
     public void handleCustomPayload(@NotNull PacketCustomPayload packetCustomPayload) {
         if (PacketCustomPayload.CHANNEL_ROTATION_LOCK.equals(packetCustomPayload.channel)) {
             if (packetCustomPayload.data.length == 4) {
-                this.playerEntity.rotationLock = Direction.getDirectionById(packetCustomPayload.data[0]);
-                this.playerEntity.rotationLockHorizontal = Direction.getDirectionById(packetCustomPayload.data[1]);
-                this.playerEntity.rotationLockVertical = Direction.getDirectionById(packetCustomPayload.data[2]);
+                this.playerEntity.rotationLock = Direction.fromId(packetCustomPayload.data[0]);
+                this.playerEntity.rotationLockHorizontal = Direction.fromId(packetCustomPayload.data[1]);
+                this.playerEntity.rotationLockVertical = Direction.fromId(packetCustomPayload.data[2]);
                 this.playerEntity.placementModeOverride = PlacementMode.get(packetCustomPayload.data[3]);
             }
         } else if (PacketCustomPayload.CHANNEL_FLAG.equals(packetCustomPayload.channel)) {
diff --git a/game/server/src/main/java/net/minecraft/server/world/WorldServer.java b/game/server/src/main/java/net/minecraft/server/world/WorldServer.java
index eb17bae0a..fe99dacb7 100644
--- a/game/server/src/main/java/net/minecraft/server/world/WorldServer.java
+++ b/game/server/src/main/java/net/minecraft/server/world/WorldServer.java
@@ -39,7 +39,6 @@ public class WorldServer extends World {
     public @NotNull MinecraftServer mcServer;
     private long lastWeatherSend = System.currentTimeMillis();
 
-
     public WorldServer(
         final @NotNull MinecraftServer server,
         final @NotNull LevelStorage levelStorage,
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java b/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
index 15b788c19..82a1a4cab 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
@@ -7,6 +7,7 @@ import net.minecraft.core.item.ItemBucket;
 import net.minecraft.datagen.recipeBuilders.RecipeBuilder;
 import net.minecraft.datagen.recipeBuilders.RecipeBuilderShaped;
 import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.piston.PistonCommon;
 import net.minecraft.core.data.registry.Registries;
 import net.minecraft.core.data.registry.recipe.RecipeSymbol;
 import net.minecraft.core.item.ItemStack;
@@ -1226,6 +1227,14 @@ class WorkbenchGenerator {
             .addInput('R', Items.DUST_REDSTONE)
             .create("piston", new ItemStack(Blocks.PISTON_BASE));
 
+        RecipeBuilder.Shaped(CORE_NAMESPACE)
+            .setShape(
+                "XXX",
+                " B ")
+            .addInput('X', "minecraft:planks")
+            .addInput('B', PistonCommon.baseItem(PistonCommon.TYPE_NORMAL, false, true))
+            .create("piston_reattach", new ItemStack(Blocks.PISTON_BASE));
+
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
                 "S",
@@ -1236,14 +1245,32 @@ class WorkbenchGenerator {
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
+                " S ",
                 "XXX",
-                "#I#",
-                "#R#")
+                " B ")
+            .addInput('S', Items.SLIMEBALL)
+            .addInput('X', "minecraft:planks")
+            .addInput('B', PistonCommon.baseItem(PistonCommon.TYPE_STICKY, false, true))
+            .create("sticky_piston_reattach", Blocks.PISTON_BASE_STICKY.getDefaultStack());
+        
+        RecipeBuilder.Shaped(CORE_NAMESPACE)
+        .setShape(
+            "XXX",
+            "#I#",
+            "#R#")
             .addInput('X', Items.INGOT_STEEL_CRUDE)
             .addInput('#', "minecraft:stones")
             .addInput('I', Items.INGOT_STEEL)
             .addInput('R', Items.DUST_REDSTONE)
             .create("steel_piston", Blocks.PISTON_BASE_STEEL.getDefaultStack());
+            
+        RecipeBuilder.Shaped(CORE_NAMESPACE)
+            .setShape(
+                "XXX",
+                " B ")
+            .addInput('X', Items.INGOT_STEEL_CRUDE)
+            .addInput('B', PistonCommon.baseItem(PistonCommon.TYPE_STEEL, false, true))
+            .create("steel_piston_reattach", Blocks.PISTON_BASE_STEEL.getDefaultStack());
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -1295,6 +1322,15 @@ class WorkbenchGenerator {
             .addInput('I', "minecraft:stones")
             .create("redstone_repeater", new ItemStack(Items.REPEATER, 1));
 
+        RecipeBuilder.Shaped(CORE_NAMESPACE)
+            .setShape(
+                "#X#",
+                "III")
+            .addInput('#', Blocks.TORCH_REDSTONE_ACTIVE)
+            .addInput('X', Items.QUARTZ)
+            .addInput('I', "minecraft:stones")
+            .create("redstone_timer", new ItemStack(Items.TIMER, 1));
+
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
                 "X",
