diff --git a/build.gradle b/build.gradle
index d802ac566..6cf42af69 100755
--- a/build.gradle
+++ b/build.gradle
@@ -37,11 +37,11 @@ allprojects {
     }
     group = rootProject.maven_group
 
-    sourceCompatibility = targetCompatibility = JavaVersion.VERSION_21
+    sourceCompatibility = targetCompatibility = JavaVersion.toVersion(project.java_version)
     
     java {
         toolchain {
-            languageVersion.set(JavaLanguageVersion.of(21))
+            languageVersion.set(JavaLanguageVersion.of(sourceCompatibility.majorVersion.toInteger()))
         }
     }
 
@@ -103,7 +103,7 @@ allprojects {
     tasks.withType(JavaCompile).configureEach {
         it.options.encoding = "UTF-8"
 
-        def targetVersion = 17
+        def targetVersion = project.java_version.toInteger()
         if (JavaVersion.current().isJava9Compatible()) {
             it.options.release = targetVersion
         }
diff --git a/buildSrc/src/main/java/baritone/gradle/task/BaritoneGradleTask.java b/buildSrc/src/main/java/baritone/gradle/task/BaritoneGradleTask.java
index 2dad551f2..f27df9aaa 100644
--- a/buildSrc/src/main/java/baritone/gradle/task/BaritoneGradleTask.java
+++ b/buildSrc/src/main/java/baritone/gradle/task/BaritoneGradleTask.java
@@ -36,8 +36,8 @@
 class BaritoneGradleTask extends DefaultTask {
 
     protected static final String
-            PROGUARD_ZIP                    = "proguard.zip",
-            PROGUARD_JAR                    = "proguard.jar",
+            PROGUARD_ZIP                    = "proguard-%s.zip",
+            PROGUARD_JAR                    = "proguard-%s.jar",
             PROGUARD_CONFIG_TEMPLATE        = "scripts/proguard.pro",
             PROGUARD_CONFIG_DEST            = "template.pro",
             PROGUARD_API_CONFIG             = "api.pro",
diff --git a/buildSrc/src/main/java/baritone/gradle/task/ProguardTask.java b/buildSrc/src/main/java/baritone/gradle/task/ProguardTask.java
index a9c7f94ea..d5e05a19c 100644
--- a/buildSrc/src/main/java/baritone/gradle/task/ProguardTask.java
+++ b/buildSrc/src/main/java/baritone/gradle/task/ProguardTask.java
@@ -26,6 +26,9 @@
 import org.gradle.api.tasks.compile.ForkOptions;
 import org.gradle.api.tasks.compile.JavaCompile;
 import org.gradle.internal.jvm.Jvm;
+import org.gradle.jvm.toolchain.JavaLanguageVersion;
+import org.gradle.jvm.toolchain.JavaLauncher;
+import org.gradle.jvm.toolchain.JavaToolchainService;
 import xyz.wagyourtail.unimined.api.UniminedExtension;
 import xyz.wagyourtail.unimined.api.minecraft.MinecraftConfig;
 
@@ -47,17 +50,10 @@
 public class ProguardTask extends BaritoneGradleTask {
 
     @Input
-    private String url;
+    private String proguardVersion;
 
-    public String getUrl() {
-        return url;
-    }
-
-    @Input
-    private String extract;
-
-    public String getExtract() {
-        return extract;
+    public String getProguardVersion() {
+        return proguardVersion;
     }
 
     private List<String> requiredLibraries;
@@ -99,98 +95,33 @@ private void processArtifact() throws Exception {
     }
 
     private void downloadProguard() throws Exception {
-        Path proguardZip = getTemporaryFile(PROGUARD_ZIP);
+        Path proguardZip = getTemporaryFile(String.format(PROGUARD_ZIP, proguardVersion));
         if (!Files.exists(proguardZip)) {
-            write(new URL(this.url).openStream(), proguardZip);
+            write(new URL(String.format("https://github.com/Guardsquare/proguard/releases/download/v%s/proguard-%s.zip", proguardVersion, proguardVersion)).openStream(), proguardZip);
         }
     }
 
     private void extractProguard() throws Exception {
-        Path proguardJar = getTemporaryFile(PROGUARD_JAR);
+        Path proguardJar = getTemporaryFile(String.format(PROGUARD_JAR, proguardVersion));
         if (!Files.exists(proguardJar)) {
-            ZipFile zipFile = new ZipFile(getTemporaryFile(PROGUARD_ZIP).toFile());
-            ZipEntry zipJarEntry = zipFile.getEntry(this.extract);
+            ZipFile zipFile = new ZipFile(getTemporaryFile(String.format(PROGUARD_ZIP, proguardVersion)).toFile());
+            ZipEntry zipJarEntry = zipFile.getEntry(String.format("proguard-%s/lib/proguard.jar", proguardVersion));
             write(zipFile.getInputStream(zipJarEntry), proguardJar);
             zipFile.close();
         }
     }
 
-    private String getJavaBinPathForProguard() throws Exception {
-        String path;
-        try {
-            path = findJavaPathByGradleConfig();
-            if (path != null) return path;
-        } catch (Exception ex) {
-            System.err.println("Unable to find java by javaCompile options");
-            ex.printStackTrace();
-        }
-
-        path = findJavaByGradleCurrentRuntime();
-        if (path != null) return path;
-
-        try {
-            path = findJavaByJavaHome();
-            if (path != null) return path;
-        } catch (Exception ex) {
-            System.err.println("Unable to find java by JAVA_HOME");
-            ex.printStackTrace();
-        }
-
-        throw new Exception("Unable to find java to determine ProGuard libraryjars. Please specify forkOptions.executable in javaCompile," +
-                " JAVA_HOME environment variable, or make sure to run Gradle with the correct JDK (a v1.8 only)");
-    }
-
-    private String findJavaByGradleCurrentRuntime() {
-        String path = Jvm.current().getJavaExecutable().getAbsolutePath();
-        System.out.println("Using Gradle's runtime Java for ProGuard");
-        return path;
-    }
-
-    private String findJavaByJavaHome() {
-        final String javaHomeEnv = System.getenv("JAVA_HOME");
-        if (javaHomeEnv != null) {
-            String path = Jvm.forHome(new File(javaHomeEnv)).getJavaExecutable().getAbsolutePath();
-            System.out.println("Detected Java path by JAVA_HOME");
-            return path;
-        }
-        return null;
-    }
+    private JavaLauncher getJavaLauncherForProguard() {
+        var toolchains = getProject().getExtensions().getByType(JavaToolchainService.class);
+        var toolchain = toolchains.launcherFor((spec) -> {
+            spec.getLanguageVersion().set(JavaLanguageVersion.of(getProject().findProperty("java_version").toString()));
+        }).getOrNull();
 
-    private String findJavaPathByGradleConfig() {
-        final TaskCollection<JavaCompile> javaCompiles = super.getProject().getTasks().withType(JavaCompile.class);
-
-        final JavaCompile compileTask = javaCompiles.iterator().next();
-        final ForkOptions forkOptions = compileTask.getOptions().getForkOptions();
-
-        if (forkOptions != null) {
-            String javacPath = forkOptions.getExecutable();
-            if (javacPath != null) {
-                File javacFile = new File(javacPath);
-                if (javacFile.exists()) {
-                    File[] maybeJava = javacFile.getParentFile().listFiles((dir, name) -> name.equals("java"));
-                    if (maybeJava != null && maybeJava.length > 0) {
-                        String path = maybeJava[0].getAbsolutePath();
-                        System.out.println("Detected Java path by forkOptions");
-                        return path;
-                    }
-                }
-            }
+        if (toolchain == null) {
+            throw new IllegalStateException("Java toolchain not found");
         }
-        return null;
-    }
 
-    private boolean validateJavaVersion(String java) {
-        //TODO: fix for j16
-//        final JavaVersion javaVersion = new DefaultJvmVersionDetector(new DefaultExecActionFactory(new IdentityFileResolver())).getJavaVersion(java);
-//
-//        if (!javaVersion.getMajorVersion().equals("8")) {
-//            System.out.println("Failed to validate Java version " + javaVersion.toString() + " [" + java + "] for ProGuard libraryjars");
-//            // throw new RuntimeException("Java version incorrect: " + javaVersion.getMajorVersion() + " for " + java);
-//            return false;
-//        }
-//
-//        System.out.println("Validated Java version " + javaVersion.toString() + " [" + java + "] for ProGuard libraryjars");
-        return true;
+        return toolchain;
     }
 
     private void generateConfigs() throws Exception {
@@ -284,13 +215,8 @@ private void cleanup() {
         } catch (IOException ignored) {}
     }
 
-    public void setUrl(String url) {
-        this.url = url;
-    }
-
-
-    public void setExtract(String extract) {
-        this.extract = extract;
+    public void setProguardVersion(String url) {
+        this.proguardVersion = url;
     }
 
     private void runProguard(Path config) throws Exception {
@@ -299,39 +225,15 @@ private void runProguard(Path config) throws Exception {
             Files.delete(this.proguardOut);
         }
 
-        // Make paths relative to work directory; fixes spaces in path to config, @"" doesn't work
         Path workingDirectory = getTemporaryFile("");
-        Path proguardJar = workingDirectory.relativize(getTemporaryFile(PROGUARD_JAR));
-        config = workingDirectory.relativize(config);
-
-        // Honestly, if you still have spaces in your path at this point, you're SOL.
 
-        Process p = new ProcessBuilder("java", "-jar", proguardJar.toString(), "@" + config.toString())
-                .directory(workingDirectory.toFile()) // Set the working directory to the temporary folder]
-                .start();
+        getProject().javaexec(spec -> {
+            spec.workingDir(workingDirectory.toFile());
+            spec.args("@" + workingDirectory.relativize(config));
+            spec.classpath(getTemporaryFile(String.format(PROGUARD_JAR, proguardVersion)));
 
-        // We can't do output inherit process I/O with gradle for some reason and have it work, so we have to do this
-        this.printOutputLog(p.getInputStream(), System.out);
-        this.printOutputLog(p.getErrorStream(), System.err);
-
-        // Halt the current thread until the process is complete, if the exit code isn't 0, throw an exception
-        int exitCode = p.waitFor();
-        if (exitCode != 0) {
-            Thread.sleep(1000);
-            throw new IllegalStateException("Proguard exited with code " + exitCode);
-        }
+            spec.executable(getJavaLauncherForProguard().getExecutablePath().getAsFile());
+        }).assertNormalExitValue().rethrowFailure();
     }
 
-    private void printOutputLog(InputStream stream, PrintStream outerr) {
-        new Thread(() -> {
-            try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    outerr.println(line);
-                }
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }).start();
-    }
 }
diff --git a/buildSrc/src/main/java/baritone/gradle/util/Determinizer.java b/buildSrc/src/main/java/baritone/gradle/util/Determinizer.java
index 781aaec24..04bd74947 100644
--- a/buildSrc/src/main/java/baritone/gradle/util/Determinizer.java
+++ b/buildSrc/src/main/java/baritone/gradle/util/Determinizer.java
@@ -71,10 +71,10 @@ public static void determinize(String inputPath, String outputPath, List<File> t
                     ByteArrayOutputStream cancer = new ByteArrayOutputStream();
                     copy(jarFile.getInputStream(entry), cancer);
                     String manifest = new String(cancer.toByteArray());
-                    if (!manifest.contains("baritone.launch.BaritoneTweaker")) {
+                    if (!manifest.contains("baritone.launch.tweaker.BaritoneTweaker")) {
                         throw new IllegalStateException("unable to replace");
                     }
-                    manifest = manifest.replace("baritone.launch.BaritoneTweaker", "org.spongepowered.asm.launch.MixinTweaker");
+                    manifest = manifest.replace("baritone.launch.tweaker.BaritoneTweaker", "org.spongepowered.asm.launch.MixinTweaker");
                     jos.write(manifest.getBytes());
                 } else {
                     copy(jarFile.getInputStream(entry), jos);
diff --git a/fabric/build.gradle b/fabric/build.gradle
index bf3e6c38d..c607dbab2 100644
--- a/fabric/build.gradle
+++ b/fabric/build.gradle
@@ -78,8 +78,7 @@ components.java {
 }
 
 task proguard(type: ProguardTask) {
-    url 'https://github.com/Guardsquare/proguard/releases/download/v7.4.2/proguard-7.4.2.zip'
-    extract 'proguard-7.4.2/lib/proguard.jar'
+    proguardVersion "7.4.2"
     compType "fabric"
 }
 
diff --git a/forge/build.gradle b/forge/build.gradle
index 46c941a62..e7ba6a06c 100644
--- a/forge/build.gradle
+++ b/forge/build.gradle
@@ -95,8 +95,7 @@ components.java {
 }
 
 task proguard(type: ProguardTask) {
-    url 'https://github.com/Guardsquare/proguard/releases/download/v7.4.2/proguard-7.4.2.zip'
-    extract 'proguard-7.4.2/lib/proguard.jar'
+    proguardVersion "7.4.2"
     compType "forge"
 }
 
diff --git a/gradle.properties b/gradle.properties
index e25c8b569..94525c8ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,6 +6,8 @@ mod_version=1.10.2
 maven_group=baritone
 archives_base_name=baritone
 
+java_version=21
+
 minecraft_version=1.20.6
 
 forge_version=50.0.8
diff --git a/neoforge/build.gradle b/neoforge/build.gradle
index 6b97406fd..82dff3f4c 100644
--- a/neoforge/build.gradle
+++ b/neoforge/build.gradle
@@ -105,8 +105,7 @@ components.java {
 }
 
 task proguard(type: ProguardTask) {
-    url 'https://github.com/Guardsquare/proguard/releases/download/v7.4.2/proguard-7.4.2.zip'
-    extract 'proguard-7.4.2/lib/proguard.jar'
+    proguardVersion "7.4.2"
     compType "neoforge"
 }
 
diff --git a/src/api/java/baritone/api/Settings.java b/src/api/java/baritone/api/Settings.java
index 8609a0a88..1812fe486 100644
--- a/src/api/java/baritone/api/Settings.java
+++ b/src/api/java/baritone/api/Settings.java
@@ -387,7 +387,7 @@ public final class Settings {
 
     /**
      * How many ticks between breaking a block and starting to break the next block. Default in game is 6 ticks.
-     * Values under 2 will be clamped.
+     * Values under 1 will be clamped. The delay only applies to non-instant (1-tick) breaks.
      */
     public final Setting<Integer> blockBreakSpeed = new Setting<>(6);
 
@@ -973,6 +973,11 @@ public final class Settings {
      */
     public final Setting<Boolean> replantNetherWart = new Setting<>(false);
 
+    /**
+     * Farming will scan for at most this many blocks.
+     */
+    public final Setting<Integer> farmMaxScanSize = new Setting<>(256);
+
     /**
      * When the cache scan gives less blocks than the maximum threshold (but still above zero), scan the main world too.
      * <p>
diff --git a/src/api/java/baritone/api/utils/SettingsUtil.java b/src/api/java/baritone/api/utils/SettingsUtil.java
index 53283cd33..23c2acddb 100644
--- a/src/api/java/baritone/api/utils/SettingsUtil.java
+++ b/src/api/java/baritone/api/utils/SettingsUtil.java
@@ -149,7 +149,7 @@ public static <T> String settingValueToString(Settings.Setting<T> setting, T val
             throw new IllegalStateException("Missing " + setting.getValueClass() + " " + setting.getName());
         }
 
-        return io.toString(new ParserContext(setting), value);
+        return io.toString(setting.getType(), value);
     }
 
     public static String settingValueToString(Settings.Setting setting) throws IllegalArgumentException {
@@ -196,7 +196,7 @@ public static void parseAndApply(Settings settings, String settingName, String s
         }
         Class intendedType = setting.getValueClass();
         ISettingParser ioMethod = Parser.getParser(setting.getType());
-        Object parsed = ioMethod.parse(new ParserContext(setting), settingValue);
+        Object parsed = ioMethod.parse(setting.getType(), settingValue);
         if (!intendedType.isInstance(parsed)) {
             throw new IllegalStateException(ioMethod + " parser returned incorrect type, expected " + intendedType + " got " + parsed + " which is " + parsed.getClass());
         }
@@ -205,26 +205,13 @@ public static void parseAndApply(Settings settings, String settingName, String s
 
     private interface ISettingParser<T> {
 
-        T parse(ParserContext context, String raw);
+        T parse(Type type, String raw);
 
-        String toString(ParserContext context, T value);
+        String toString(Type type, T value);
 
         boolean accepts(Type type);
     }
 
-    private static class ParserContext {
-
-        private final Settings.Setting<?> setting;
-
-        private ParserContext(Settings.Setting<?> setting) {
-            this.setting = setting;
-        }
-
-        private Settings.Setting<?> getSetting() {
-            return this.setting;
-        }
-    }
-
     private enum Parser implements ISettingParser {
 
         DOUBLE(Double.class, Double::parseDouble),
@@ -256,21 +243,21 @@ private enum Parser implements ISettingParser {
         ),
         LIST() {
             @Override
-            public Object parse(ParserContext context, String raw) {
-                Type type = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
-                Parser parser = Parser.getParser(type);
+            public Object parse(Type type, String raw) {
+                Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+                Parser parser = Parser.getParser(elementType);
                 return Stream.of(raw.split(","))
-                        .map(s -> parser.parse(context, s))
+                        .map(s -> parser.parse(elementType, s))
                         .collect(Collectors.toList());
             }
 
             @Override
-            public String toString(ParserContext context, Object value) {
-                Type type = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
-                Parser parser = Parser.getParser(type);
+            public String toString(Type type, Object value) {
+                Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0];
+                Parser parser = Parser.getParser(elementType);
 
                 return ((List<?>) value).stream()
-                        .map(o -> parser.toString(context, o))
+                        .map(o -> parser.toString(elementType, o))
                         .collect(Collectors.joining(","));
             }
 
@@ -281,26 +268,26 @@ public boolean accepts(Type type) {
         },
         MAPPING() {
             @Override
-            public Object parse(ParserContext context, String raw) {
-                Type keyType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
-                Type valueType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[1];
+            public Object parse(Type type, String raw) {
+                Type keyType = ((ParameterizedType) type).getActualTypeArguments()[0];
+                Type valueType = ((ParameterizedType) type).getActualTypeArguments()[1];
                 Parser keyParser = Parser.getParser(keyType);
                 Parser valueParser = Parser.getParser(valueType);
 
                 return Stream.of(raw.split(",(?=[^,]*->)"))
                         .map(s -> s.split("->"))
-                        .collect(Collectors.toMap(s -> keyParser.parse(context, s[0]), s -> valueParser.parse(context, s[1])));
+                        .collect(Collectors.toMap(s -> keyParser.parse(keyType, s[0]), s -> valueParser.parse(valueType, s[1])));
             }
 
             @Override
-            public String toString(ParserContext context, Object value) {
-                Type keyType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[0];
-                Type valueType = ((ParameterizedType) context.getSetting().getType()).getActualTypeArguments()[1];
+            public String toString(Type type, Object value) {
+                Type keyType = ((ParameterizedType) type).getActualTypeArguments()[0];
+                Type valueType = ((ParameterizedType) type).getActualTypeArguments()[1];
                 Parser keyParser = Parser.getParser(keyType);
                 Parser valueParser = Parser.getParser(valueType);
 
                 return ((Map<?, ?>) value).entrySet().stream()
-                        .map(o -> keyParser.toString(context, o.getKey()) + "->" + valueParser.toString(context, o.getValue()))
+                        .map(o -> keyParser.toString(keyType, o.getKey()) + "->" + valueParser.toString(valueType, o.getValue()))
                         .collect(Collectors.joining(","));
             }
 
@@ -331,14 +318,14 @@ <T> Parser(Class<T> cla$$, Function<String, T> parser, Function<T, String> toStr
         }
 
         @Override
-        public Object parse(ParserContext context, String raw) {
+        public Object parse(Type type, String raw) {
             Object parsed = this.parser.apply(raw);
             Objects.requireNonNull(parsed);
             return parsed;
         }
 
         @Override
-        public String toString(ParserContext context, Object value) {
+        public String toString(Type type, Object value) {
             return this.toString.apply(value);
         }
 
diff --git a/src/launch/java/baritone/launch/mixins/MixinPlayerController.java b/src/launch/java/baritone/launch/mixins/MixinPlayerController.java
index 34f39aee9..7a77d4849 100644
--- a/src/launch/java/baritone/launch/mixins/MixinPlayerController.java
+++ b/src/launch/java/baritone/launch/mixins/MixinPlayerController.java
@@ -31,6 +31,10 @@ public abstract class MixinPlayerController implements IPlayerControllerMP {
     @Override
     public abstract void setIsHittingBlock(boolean isHittingBlock);
 
+    @Accessor("isDestroying")
+    @Override
+    public abstract boolean isHittingBlock();
+
     @Accessor("destroyBlockPos")
     @Override
     public abstract BlockPos getCurrentBlock();
@@ -38,4 +42,8 @@ public abstract class MixinPlayerController implements IPlayerControllerMP {
     @Invoker("ensureHasSentCarriedItem")
     @Override
     public abstract void callSyncCurrentPlayItem();
+
+    @Accessor("destroyDelay")
+    @Override
+    public abstract void setDestroyDelay(int destroyDelay);
 }
diff --git a/src/main/java/baritone/process/FarmProcess.java b/src/main/java/baritone/process/FarmProcess.java
index bccb021c6..b38ba767f 100644
--- a/src/main/java/baritone/process/FarmProcess.java
+++ b/src/main/java/baritone/process/FarmProcess.java
@@ -42,6 +42,7 @@
 import net.minecraft.world.item.Items;
 import net.minecraft.world.level.Level;
 import net.minecraft.world.level.block.AirBlock;
+import net.minecraft.world.level.block.BambooStalkBlock;
 import net.minecraft.world.level.block.Block;
 import net.minecraft.world.level.block.Blocks;
 import net.minecraft.world.level.block.BonemealableBlock;
@@ -95,6 +96,7 @@ public final class FarmProcess extends BaritoneProcessHelper implements IFarmPro
             Items.NETHER_WART,
             Items.COCOA_BEANS,
             Blocks.SUGAR_CANE.asItem(),
+            Blocks.BAMBOO.asItem(),
             Blocks.CACTUS.asItem()
     );
 
@@ -137,6 +139,15 @@ public boolean readyToHarvest(Level world, BlockPos pos, BlockState state) {
                 return true;
             }
         },
+        BAMBOO(Blocks.BAMBOO, null) {
+            @Override
+            public boolean readyToHarvest(Level world, BlockPos pos, BlockState state) {
+                if (Baritone.settings().replantCrops.value) {
+                    return world.getBlockState(pos.below()).getBlock() instanceof BambooStalkBlock;
+                }
+                return true;
+            }
+        },
         CACTUS(Blocks.CACTUS, null) {
             @Override
             public boolean readyToHarvest(Level world, BlockPos pos, BlockState state) {
@@ -191,20 +202,20 @@ private boolean isCocoa(ItemStack stack) {
 
     @Override
     public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
-        ArrayList<Block> scan = new ArrayList<>();
-        for (Harvest harvest : Harvest.values()) {
-            scan.add(harvest.block);
-        }
-        if (Baritone.settings().replantCrops.value) {
-            scan.add(Blocks.FARMLAND);
-            scan.add(Blocks.JUNGLE_LOG);
-            if (Baritone.settings().replantNetherWart.value) {
-                scan.add(Blocks.SOUL_SAND);
+        if (Baritone.settings().mineGoalUpdateInterval.value != 0 && tickCount++ % Baritone.settings().mineGoalUpdateInterval.value == 0) {
+            ArrayList<Block> scan = new ArrayList<>();
+            for (Harvest harvest : Harvest.values()) {
+                scan.add(harvest.block);
+            }
+            if (Baritone.settings().replantCrops.value) {
+                scan.add(Blocks.FARMLAND);
+                scan.add(Blocks.JUNGLE_LOG);
+                if (Baritone.settings().replantNetherWart.value) {
+                    scan.add(Blocks.SOUL_SAND);
+                }
             }
-        }
 
-        if (Baritone.settings().mineGoalUpdateInterval.value != 0 && tickCount++ % Baritone.settings().mineGoalUpdateInterval.value == 0) {
-            Baritone.getExecutor().execute(() -> locations = BaritoneAPI.getProvider().getWorldScanner().scanChunkRadius(ctx, scan, 256, 10, 10));
+            Baritone.getExecutor().execute(() -> locations = BaritoneAPI.getProvider().getWorldScanner().scanChunkRadius(ctx, scan, Baritone.settings().farmMaxScanSize.value, 10, 10));
         }
         if (locations == null) {
             return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE);
@@ -256,7 +267,12 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
         }
 
         baritone.getInputOverrideHandler().clearAllKeys();
+        BetterBlockPos playerPos = ctx.playerFeet();
+        double blockReachDistance = ctx.playerController().getBlockReachDistance();
         for (BlockPos pos : toBreak) {
+            if (playerPos.distSqr(pos) > blockReachDistance * blockReachDistance) {
+                continue;
+            }
             Optional<Rotation> rot = RotationUtils.reachable(ctx, pos);
             if (rot.isPresent() && isSafeToCancel) {
                 baritone.getLookBehavior().updateTarget(rot.get(), true);
@@ -270,10 +286,13 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
         ArrayList<BlockPos> both = new ArrayList<>(openFarmland);
         both.addAll(openSoulsand);
         for (BlockPos pos : both) {
+            if (playerPos.distSqr(pos) > blockReachDistance * blockReachDistance) {
+                continue;
+            }
             boolean soulsand = openSoulsand.contains(pos);
-            Optional<Rotation> rot = RotationUtils.reachableOffset(ctx, pos, new Vec3(pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5), ctx.playerController().getBlockReachDistance(), false);
+            Optional<Rotation> rot = RotationUtils.reachableOffset(ctx, pos, new Vec3(pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5), blockReachDistance, false);
             if (rot.isPresent() && isSafeToCancel && baritone.getInventoryBehavior().throwaway(true, soulsand ? this::isNetherWart : this::isPlantable)) {
-                HitResult result = RayTraceUtils.rayTraceTowards(ctx.player(), rot.get(), ctx.playerController().getBlockReachDistance());
+                HitResult result = RayTraceUtils.rayTraceTowards(ctx.player(), rot.get(), blockReachDistance);
                 if (result instanceof BlockHitResult && ((BlockHitResult) result).getDirection() == Direction.UP) {
                     baritone.getLookBehavior().updateTarget(rot.get(), true);
                     if (ctx.isLookingAt(pos)) {
@@ -284,14 +303,17 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
             }
         }
         for (BlockPos pos : openLog) {
+            if (playerPos.distSqr(pos) > blockReachDistance * blockReachDistance) {
+                continue;
+            }
             for (Direction dir : Direction.Plane.HORIZONTAL) {
                 if (!(ctx.world().getBlockState(pos.relative(dir)).getBlock() instanceof AirBlock)) {
                     continue;
                 }
                 Vec3 faceCenter = Vec3.atCenterOf(pos).add(Vec3.atLowerCornerOf(dir.getNormal()).scale(0.5));
-                Optional<Rotation> rot = RotationUtils.reachableOffset(ctx, pos, faceCenter, ctx.playerController().getBlockReachDistance(), false);
+                Optional<Rotation> rot = RotationUtils.reachableOffset(ctx, pos, faceCenter, blockReachDistance, false);
                 if (rot.isPresent() && isSafeToCancel && baritone.getInventoryBehavior().throwaway(true, this::isCocoa)) {
-                    HitResult result = RayTraceUtils.rayTraceTowards(ctx.player(), rot.get(), ctx.playerController().getBlockReachDistance());
+                    HitResult result = RayTraceUtils.rayTraceTowards(ctx.player(), rot.get(), blockReachDistance);
                     if (result instanceof BlockHitResult && ((BlockHitResult) result).getDirection() == dir) {
                         baritone.getLookBehavior().updateTarget(rot.get(), true);
                         if (ctx.isLookingAt(pos)) {
@@ -303,6 +325,9 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
             }
         }
         for (BlockPos pos : bonemealable) {
+            if (playerPos.distSqr(pos) > blockReachDistance * blockReachDistance) {
+                continue;
+            }
             Optional<Rotation> rot = RotationUtils.reachable(ctx, pos);
             if (rot.isPresent() && isSafeToCancel && baritone.getInventoryBehavior().throwaway(true, this::isBoneMeal)) {
                 baritone.getLookBehavior().updateTarget(rot.get(), true);
@@ -359,6 +384,14 @@ public PathingCommand onTick(boolean calcFailed, boolean isSafeToCancel) {
                 }
             }
         }
+        if (goalz.isEmpty()) {
+            logDirect("Farm failed");
+            if (Baritone.settings().notificationOnFarmFail.value) {
+                logNotification("Farm failed", true);
+            }
+            onLostControl();
+            return new PathingCommand(null, PathingCommandType.REQUEST_PAUSE);
+        }
         return new PathingCommand(new GoalComposite(goalz.toArray(new Goal[0])), PathingCommandType.SET_GOAL_AND_PATH);
     }
 
diff --git a/src/main/java/baritone/utils/BlockBreakHelper.java b/src/main/java/baritone/utils/BlockBreakHelper.java
index 3332aec76..0c5cf6f00 100644
--- a/src/main/java/baritone/utils/BlockBreakHelper.java
+++ b/src/main/java/baritone/utils/BlockBreakHelper.java
@@ -19,6 +19,7 @@
 
 import baritone.api.BaritoneAPI;
 import baritone.api.utils.IPlayerContext;
+import baritone.utils.accessor.IPlayerControllerMP;
 import net.minecraft.world.InteractionHand;
 import net.minecraft.world.phys.BlockHitResult;
 import net.minecraft.world.phys.HitResult;
@@ -29,10 +30,10 @@
  */
 public final class BlockBreakHelper {
     // base ticks between block breaks caused by tick logic
-    private static final int BASE_BREAK_DELAY = 2;
+    private static final int BASE_BREAK_DELAY = 1;
 
     private final IPlayerContext ctx;
-    private boolean didBreakLastTick;
+    private boolean wasHitting;
     private int breakDelayTimer = 0;
 
     BlockBreakHelper(IPlayerContext ctx) {
@@ -41,13 +42,10 @@ public final class BlockBreakHelper {
 
     public void stopBreakingBlock() {
         // The player controller will never be null, but the player can be
-        if (ctx.player() != null && didBreakLastTick) {
-            if (!ctx.playerController().hasBrokenBlock()) {
-                // insane bypass to check breaking succeeded
-                ctx.playerController().setHittingBlock(true);
-            }
+        if (ctx.player() != null && wasHitting) {
+            ctx.playerController().setHittingBlock(false);
             ctx.playerController().resetBlockRemoving();
-            didBreakLastTick = false;
+            wasHitting = false;
         }
     }
 
@@ -60,24 +58,30 @@ public void tick(boolean isLeftClick) {
         boolean isBlockTrace = trace != null && trace.getType() == HitResult.Type.BLOCK;
 
         if (isLeftClick && isBlockTrace) {
-            if (!didBreakLastTick) {
+            ctx.playerController().setHittingBlock(wasHitting);
+            if (ctx.playerController().hasBrokenBlock()) {
                 ctx.playerController().syncHeldItem();
                 ctx.playerController().clickBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection());
                 ctx.player().swing(InteractionHand.MAIN_HAND);
+            } else {
+                if (ctx.playerController().onPlayerDamageBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection())) {
+                    ctx.player().swing(InteractionHand.MAIN_HAND);
+                }
+                if (ctx.playerController().hasBrokenBlock()) { // block broken this tick
+                    // break delay timer only applies for multi-tick block breaks like vanilla
+                    breakDelayTimer = BaritoneAPI.getSettings().blockBreakSpeed.value - BASE_BREAK_DELAY;
+                    // must reset controller's destroy delay to prevent the client from delaying itself unnecessarily
+                    ((IPlayerControllerMP) ctx.minecraft().gameMode).setDestroyDelay(0);
+                }
             }
-
-            // Attempt to break the block
-            if (ctx.playerController().onPlayerDamageBlock(((BlockHitResult) trace).getBlockPos(), ((BlockHitResult) trace).getDirection())) {
-                ctx.player().swing(InteractionHand.MAIN_HAND);
-            }
-
+            // if true, we're breaking a block. if false, we broke the block this tick
+            wasHitting = !ctx.playerController().hasBrokenBlock();
+            // this value will be reset by the MC client handling mouse keys
+            // since we're not spoofing the click keybind to the client, the client will stop the break if isDestroyingBlock is true
+            // we store and restore this value on the next tick to determine if we're breaking a block
             ctx.playerController().setHittingBlock(false);
-
-            didBreakLastTick = true;
-        } else if (didBreakLastTick) {
-            stopBreakingBlock();
-            breakDelayTimer = BaritoneAPI.getSettings().blockBreakSpeed.value - BASE_BREAK_DELAY;
-            didBreakLastTick = false;
+        } else {
+            wasHitting = false;
         }
     }
 }
diff --git a/src/main/java/baritone/utils/GuiClick.java b/src/main/java/baritone/utils/GuiClick.java
index 113450d1c..48781e389 100644
--- a/src/main/java/baritone/utils/GuiClick.java
+++ b/src/main/java/baritone/utils/GuiClick.java
@@ -74,13 +74,11 @@ public void render(GuiGraphics stack, int mouseX, int mouseY, float partialTicks
         Vec3 far = toWorld(mx, my, 1); // "Use 0.945 that's what stack overflow says" - leijurv
 
         if (near != null && far != null) {
-            ///
             Vec3 viewerPos = new Vec3(PathRenderer.posX(), PathRenderer.posY(), PathRenderer.posZ());
             LocalPlayer player = BaritoneAPI.getProvider().getPrimaryBaritone().getPlayerContext().player();
             HitResult result = player.level().clip(new ClipContext(near.add(viewerPos), far.add(viewerPos), ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, player));
             if (result != null && result.getType() == HitResult.Type.BLOCK) {
                 currentMouseOver = ((BlockHitResult) result).getBlockPos();
-                System.out.println("currentMouseOver = " + currentMouseOver);
             }
         }
     }
diff --git a/src/main/java/baritone/utils/ToolSet.java b/src/main/java/baritone/utils/ToolSet.java
index ef83a2db1..e9b0ecf95 100644
--- a/src/main/java/baritone/utils/ToolSet.java
+++ b/src/main/java/baritone/utils/ToolSet.java
@@ -177,7 +177,13 @@ private double avoidanceMultiplier(Block b) {
      * @return how long it would take in ticks
      */
     public static double calculateSpeedVsBlock(ItemStack item, BlockState state) {
-        float hardness = state.getDestroySpeed(null, null);
+        float hardness;
+        try {
+            hardness = state.getDestroySpeed(null, null);
+        } catch (NullPointerException npe) {
+            // can't easily determine the hardness so treat it as unbreakable
+            return -1;
+        }
         if (hardness < 0) {
             return -1;
         }
diff --git a/src/main/java/baritone/utils/accessor/IPlayerControllerMP.java b/src/main/java/baritone/utils/accessor/IPlayerControllerMP.java
index 72e6f7ee3..55fa43316 100644
--- a/src/main/java/baritone/utils/accessor/IPlayerControllerMP.java
+++ b/src/main/java/baritone/utils/accessor/IPlayerControllerMP.java
@@ -23,7 +23,11 @@ public interface IPlayerControllerMP {
 
     void setIsHittingBlock(boolean isHittingBlock);
 
+    boolean isHittingBlock();
+
     BlockPos getCurrentBlock();
 
     void callSyncCurrentPlayItem();
+
+    void setDestroyDelay(int destroyDelay);
 }
diff --git a/src/main/java/baritone/utils/player/BaritonePlayerController.java b/src/main/java/baritone/utils/player/BaritonePlayerController.java
index 42ba49052..b7e729b70 100644
--- a/src/main/java/baritone/utils/player/BaritonePlayerController.java
+++ b/src/main/java/baritone/utils/player/BaritonePlayerController.java
@@ -20,7 +20,6 @@
 import baritone.api.utils.IPlayerController;
 import baritone.utils.accessor.IPlayerControllerMP;
 import net.minecraft.client.Minecraft;
-import net.minecraft.client.multiplayer.ClientLevel;
 import net.minecraft.client.player.LocalPlayer;
 import net.minecraft.core.BlockPos;
 import net.minecraft.core.Direction;
@@ -54,7 +53,7 @@ public void syncHeldItem() {
 
     @Override
     public boolean hasBrokenBlock() {
-        return ((IPlayerControllerMP) mc.gameMode).getCurrentBlock().getY() == -1;
+        return !((IPlayerControllerMP) mc.gameMode).isHittingBlock();
     }
 
     @Override
diff --git a/tweaker/build.gradle b/tweaker/build.gradle
index 71a025ca8..feb9dd039 100644
--- a/tweaker/build.gradle
+++ b/tweaker/build.gradle
@@ -26,7 +26,7 @@ plugins {
 unimined.minecraft {
     runs.client = {
         mainClass = "net.minecraft.launchwrapper.Launch"
-        args.addAll(["--tweakClass", "baritone.launch.BaritoneTweaker"])
+        args.addAll(["--tweakClass", "baritone.launch.tweaker.BaritoneTweaker"])
     }
 }
 
@@ -94,8 +94,7 @@ jar {
 }
 
 task proguard(type: ProguardTask) {
-    url 'https://github.com/Guardsquare/proguard/releases/download/v7.4.2/proguard-7.4.2.zip'
-    extract 'proguard-7.4.2/lib/proguard.jar'
+    proguardVersion "7.4.2"
 }
 
 task createDist(type: CreateDistTask, dependsOn: proguard)
diff --git a/tweaker/src/main/java/baritone/launch/BaritoneTweaker.java b/tweaker/src/main/java/baritone/launch/tweaker/BaritoneTweaker.java
similarity index 98%
rename from tweaker/src/main/java/baritone/launch/BaritoneTweaker.java
rename to tweaker/src/main/java/baritone/launch/tweaker/BaritoneTweaker.java
index b9db9b6a5..694aef533 100644
--- a/tweaker/src/main/java/baritone/launch/BaritoneTweaker.java
+++ b/tweaker/src/main/java/baritone/launch/tweaker/BaritoneTweaker.java
@@ -15,7 +15,7 @@
  * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package baritone.launch;
+package baritone.launch.tweaker;
 
 import io.github.impactdevelopment.simpletweaker.SimpleTweaker;
 import net.minecraft.launchwrapper.Launch;