diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java
index 75bdd9021..65662bbe4 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/FishingHookEntity.java
@@ -99,19 +99,9 @@ public class FishingHookEntity extends ThrowableEntity {
                 }
             }
 
-            int waterLevel = BlockStateValues.getWaterLevel(blockID);
-            if (BlockRegistries.WATERLOGGED.get().contains(blockID)) {
-                waterLevel = 0;
-            }
-            if (waterLevel >= 0) {
-                double waterMaxY = iter.getY() + 1 - (waterLevel + 1) / 9.0;
-                // Falling water is a full block
-                if (waterLevel >= 8) {
-                    waterMaxY = iter.getY() + 1;
-                }
-                if (position.getY() <= waterMaxY) {
-                    touchingWater = true;
-                }
+            double waterHeight = BlockStateValues.getWaterHeight(blockID);
+            if (waterHeight != -1 && position.getY() <= (iter.getY() + waterHeight)) {
+                touchingWater = true;
             }
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
index adb81761f..e4772df80 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
@@ -77,6 +77,8 @@ public final class BlockStateValues {
     public static int JAVA_SPAWNER_ID;
     public static int JAVA_WATER_ID;
 
+    public static final int NUM_WATER_LEVELS = 9;
+
     /**
      * Determines if the block state contains Bedrock block information
      *
@@ -449,7 +451,6 @@ public final class BlockStateValues {
 
     /**
      * Get the level of water from the block state.
-     * This is used in FishingHookEntity to create splash sounds when the hook hits the water.
      *
      * @param state BlockState of the block
      * @return The water level or -1 if the block isn't water
@@ -458,6 +459,30 @@ public final class BlockStateValues {
         return WATER_LEVEL.getOrDefault(state, -1);
     }
 
+    /**
+     * Get the height of water from the block state
+     * This is used in FishingHookEntity to create splash sounds when the hook hits the water. In addition,
+     * CollisionManager uses this to determine if the player's eyes are in water.
+     *
+     * @param state BlockState of the block
+     * @return The water height or -1 if the block does not contain water
+     */
+    public static double getWaterHeight(int state) {
+        int waterLevel = BlockStateValues.getWaterLevel(state);
+        if (BlockRegistries.WATERLOGGED.get().contains(state)) {
+            waterLevel = 0;
+        }
+        if (waterLevel >= 0) {
+            double waterHeight = 1 - (waterLevel + 1) / ((double) NUM_WATER_LEVELS);
+            // Falling water is a full block
+            if (waterLevel >= 8) {
+                waterHeight = 1;
+            }
+            return waterHeight;
+        }
+        return -1;
+    }
+
     /**
      * Get the slipperiness of a block.
      * This is used in ItemEntity to calculate the friction on an item as it slides across the ground
diff --git a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
index 2617b2750..a91c2b083 100644
--- a/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
+++ b/core/src/main/java/org/geysermc/geyser/level/physics/CollisionManager.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.geyser.level.physics;
 
+import com.nukkitx.math.GenericMath;
 import com.nukkitx.math.vector.Vector3d;
 import com.nukkitx.math.vector.Vector3f;
 import com.nukkitx.math.vector.Vector3i;
@@ -405,6 +406,18 @@ public class CollisionManager {
         return session.getGeyser().getWorldManager().getBlockAt(session, session.getPlayerEntity().getPosition().toInt()) == BlockStateValues.JAVA_WATER_ID;
     }
 
+    public boolean isWaterInEyes() {
+        double eyeX = playerBoundingBox.getMiddleX();
+        double eyeY = playerBoundingBox.getMiddleY() - playerBoundingBox.getSizeY() / 2d + session.getEyeHeight();
+        double eyeZ = playerBoundingBox.getMiddleZ();
+
+        eyeY -= 1 / ((double) BlockStateValues.NUM_WATER_LEVELS); // Subtract the height of one water layer
+        int blockID = session.getGeyser().getWorldManager().getBlockAt(session, GenericMath.floor(eyeX), GenericMath.floor(eyeY), GenericMath.floor(eyeZ));
+        double waterHeight = BlockStateValues.getWaterHeight(blockID);
+
+        return waterHeight != -1 && eyeY < (Math.floor(eyeY) + waterHeight);
+    }
+
     /**
      * Updates scaffolding entity flags
      * Scaffolding needs to be checked per-move since it's a flag in Bedrock but Java does it client-side
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index 705ee6416..93ef62c56 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -100,6 +100,7 @@ import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.api.connection.GeyserConnection;
 import org.geysermc.geyser.command.CommandSender;
 import org.geysermc.geyser.configuration.EmoteOffhandWorkaroundOption;
+import org.geysermc.geyser.entity.EntityDefinitions;
 import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
 import org.geysermc.geyser.entity.type.Entity;
 import org.geysermc.geyser.entity.type.ItemFrameEntity;
@@ -1767,6 +1768,17 @@ public class GeyserSession implements GeyserConnection, CommandSender {
         sendUpstreamPacket(packet);
     }
 
+    public float getEyeHeight() {
+        return switch (pose) {
+            case SNEAKING -> 1.27f;
+            case SWIMMING,
+                    FALL_FLYING, // Elytra
+                    SPIN_ATTACK -> 0.4f; // Trident spin attack
+            case SLEEPING -> 0.2f;
+            default -> EntityDefinitions.PLAYER.offset();
+        };
+    }
+
     public MinecraftCodecHelper getCodecHelper() {
         return (MinecraftCodecHelper) this.downstream.getCodecHelper();
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
index d9603ae8e..1adf123bf 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
@@ -201,7 +201,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
 
                         // CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch
                         Vector3f playerPosition = session.getPlayerEntity().getPosition();
-                        playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - getEyeHeight(session));
+                        playerPosition = playerPosition.down(EntityDefinitions.PLAYER.offset() - session.getEyeHeight());
 
                         boolean creative = session.getGameMode() == GameMode.CREATIVE;
 
@@ -608,7 +608,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
         // Use the bounding box's position since we need the player's position seen by the Java server
         Vector3d playerPosition = session.getCollisionManager().getPlayerBoundingBox().getBottomCenter();
         float xDiff = (float) (target.getX() - playerPosition.getX());
-        float yDiff = (float) (target.getY() - (playerPosition.getY() + getEyeHeight(session)));
+        float yDiff = (float) (target.getY() - (playerPosition.getY() + session.getEyeHeight()));
         float zDiff = (float) (target.getZ() - playerPosition.getZ());
 
         // First triangle on the XZ plane
@@ -637,15 +637,4 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
             }, 150, TimeUnit.MILLISECONDS));
         }
     }
-
-    private float getEyeHeight(GeyserSession session) {
-        return switch (session.getPose()) {
-            case SNEAKING -> 1.27f;
-            case SWIMMING,
-                FALL_FLYING, // Elytra
-                SPIN_ATTACK -> 0.4f; // Trident spin attack
-            case SLEEPING -> 0.2f;
-            default -> EntityDefinitions.PLAYER.offset();
-        };
-    }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
index 7059c9a8b..c0d484919 100644
--- a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
@@ -36,6 +36,8 @@ import org.geysermc.geyser.registry.type.ItemMapping;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.collision.BlockCollision;
 
+import javax.annotation.Nullable;
+
 public final class BlockUtils {
 
     private static boolean correctTool(GeyserSession session, BlockMapping blockMapping, String itemToolType) {
@@ -101,7 +103,7 @@ public final class BlockUtils {
     // https://minecraft.gamepedia.com/Breaking
     private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock,
                                              String toolType, boolean isShearsEffective, int toolEfficiencyLevel, int hasteLevel, int miningFatigueLevel,
-                                             boolean insideOfWaterWithoutAquaAffinity, boolean outOfWaterButNotOnGround, boolean insideWaterAndNotOnGround) {
+                                             boolean insideOfWaterWithoutAquaAffinity, boolean onGround) {
         double baseTime = (((correctTool && canTierMineBlock) || canHarvestWithHand) ? 1.5 : 5.0) * blockHardness;
         double speed = 1.0 / baseTime;
 
@@ -129,12 +131,11 @@ public final class BlockUtils {
         }
 
         if (insideOfWaterWithoutAquaAffinity) speed *= 0.2;
-        if (outOfWaterButNotOnGround) speed *= 0.2;
-        if (insideWaterAndNotOnGround) speed *= 0.2;
+        if (!onGround) speed *= 0.2;
         return 1.0 / speed;
     }
 
-    public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, CompoundTag nbtData, boolean isSessionPlayer) {
+    public static double getBreakTime(GeyserSession session, BlockMapping blockMapping, ItemMapping item, @Nullable CompoundTag nbtData, boolean isSessionPlayer) {
         boolean isShearsEffective = session.getTagCache().isShearsEffective(blockMapping); //TODO called twice
         boolean canHarvestWithHand = blockMapping.isCanBreakWithHand();
         String toolType = "";
@@ -154,36 +155,28 @@ public final class BlockUtils {
         if (!isSessionPlayer) {
             // Another entity is currently mining; we have all the information we know
             return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
-                    toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false,
-                    false, false);
+                    toolEfficiencyLevel, hasteLevel, miningFatigueLevel, false, true);
         }
 
         hasteLevel = Math.max(session.getEffectCache().getHaste(), session.getEffectCache().getConduitPower());
         miningFatigueLevel = session.getEffectCache().getMiningFatigue();
 
-        boolean isInWater = session.getCollisionManager().isPlayerInWater();
-
-        boolean insideOfWaterWithoutAquaAffinity = isInWater &&
+        boolean waterInEyes = session.getCollisionManager().isWaterInEyes();
+        boolean insideOfWaterWithoutAquaAffinity = waterInEyes &&
                 ItemUtils.getEnchantmentLevel(session.getPlayerInventory().getItem(5).getNbt(), "minecraft:aqua_affinity") < 1;
 
-        boolean outOfWaterButNotOnGround = (!isInWater) && (!session.getPlayerEntity().isOnGround());
-        boolean insideWaterNotOnGround = isInWater && !session.getPlayerEntity().isOnGround();
         return calculateBreakTime(blockMapping.getHardness(), toolTier, canHarvestWithHand, correctTool, toolCanBreak, toolType, isShearsEffective,
-                toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity,
-                outOfWaterButNotOnGround, insideWaterNotOnGround);
+                toolEfficiencyLevel, hasteLevel, miningFatigueLevel, insideOfWaterWithoutAquaAffinity, session.getPlayerEntity().isOnGround());
     }
 
     public static double getSessionBreakTime(GeyserSession session, BlockMapping blockMapping) {
         PlayerInventory inventory = session.getPlayerInventory();
         GeyserItemStack item = inventory.getItemInHand();
-        ItemMapping mapping;
-        CompoundTag nbtData;
+        ItemMapping mapping = ItemMapping.AIR;
+        CompoundTag nbtData = null;
         if (item != null) {
             mapping = item.getMapping(session);
             nbtData = item.getNbt();
-        } else {
-            mapping = ItemMapping.AIR;
-            nbtData = new CompoundTag("");
         }
         return getBreakTime(session, blockMapping, mapping, nbtData, true);
     }