commit 95c4e97db05dbf1e989bcfac75b2412168ae2f47 Author: lyizai <3945107889@qq.com> Date: Thu Aug 20 21:04:27 2026 +0800 首次提交 AutoDig Mod diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7fdf419 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +build/ +out/ +run/ +*.iml +.idea/ +.vscode/ +.DS_Store +.env \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..10638f0 --- /dev/null +++ b/build.gradle @@ -0,0 +1,57 @@ +plugins { + id 'fabric-loom' version '1.17.19' + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +// 用于把 Baritone 原样打包进模组 jar 的资源(不放默认依赖里,避免 Gradle 误解析) +configurations { + nestedJar +} + +dependencies { + // Minecraft + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + // Baritone API (local jar, compile only) + modImplementation files("libs/baritone-api-fabric-1.13.1.jar") + + // 标记需要嵌套打包的 jar + nestedJar files("libs/baritone-api-fabric-1.13.1.jar") +} + +processResources { + inputs.property "version", project.version + filteringCharset "UTF-8" + + filesMatching("fabric.mod.json") { + expand "version": project.version + } + + // 将 Baritone 嵌套打包进 META-INF/jars,匹配 fabric.mod.json 的 jars 声明, + // 使玩家无需单独安装 baritone 依赖即可运行 + from(configurations.nestedJar) { + into "META-INF/jars" + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 21 +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ac80044 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,15 @@ +org.gradle.jvmargs=-Xmx2G +org.gradle.parallel=true + +# Fabric Properties +minecraft_version=1.21.4 +yarn_mappings=1.21.4+build.8 +loader_version=0.19.3 + +# Mod Properties +mod_version=1.0.0 +maven_group=autodig +archives_base_name=autodig + +# Fabric API +fabric_version=0.119.4+1.21.4 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c032084 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.7.0-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/libs/baritone-api-fabric-1.13.1.jar b/libs/baritone-api-fabric-1.13.1.jar new file mode 100644 index 0000000..27d7eca Binary files /dev/null and b/libs/baritone-api-fabric-1.13.1.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..afbea1c --- /dev/null +++ b/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = 'autodig' \ No newline at end of file diff --git a/src/main/java/autodig/AutoDigMod.java b/src/main/java/autodig/AutoDigMod.java new file mode 100644 index 0000000..894bc7d --- /dev/null +++ b/src/main/java/autodig/AutoDigMod.java @@ -0,0 +1,17 @@ +package autodig; + +import net.fabricmc.api.ModInitializer; + +/** + * AutoDig 模组主入口(服务端/共用初始化)。 + * + *

本模组为纯客户端玩法,Logic 都在客户端侧自动执行;此处保持空实现以符合 + * Fabric 对 ModInitializer 的约定。

+ */ +public final class AutoDigMod implements ModInitializer { + + @Override + public void onInitialize() { + // 客户端逻辑见 autodig.client.AutoDigModClient + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/AutoDigModClient.java b/src/main/java/autodig/client/AutoDigModClient.java new file mode 100644 index 0000000..6a87133 --- /dev/null +++ b/src/main/java/autodig/client/AutoDigModClient.java @@ -0,0 +1,111 @@ +package autodig.client; + +import autodig.client.bridge.BaritoneBridge; +import autodig.client.command.AutoDigCommands; +import autodig.client.config.ConfigStore; +import autodig.client.config.DigConfig; +import autodig.client.controller.DigController; +import autodig.client.gui.AutoDigGui; +import com.mojang.brigadier.CommandDispatcher; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import net.minecraft.text.Text; + +/** + * AutoDig 客户端主入口(Fabric ClientModInitializer)。 + * + *

负责:加载配置、注册一键唤出 GUI 的快捷键、每帧把配置同步给 Baritone 并 + * 驱动挖矿控制器、注册命令。通过单例 {@link #instance()} 向其他模块提供入口。

+ */ +@Environment(EnvType.CLIENT) +public final class AutoDigModClient implements ClientModInitializer { + + private static AutoDigModClient instance; + + private final ConfigStore configStore = new ConfigStore(); + private final DigController controller = new DigController(); + private DigConfig config; + private boolean openGuiRequested; + private KeyBinding guiKey; + + private static final int KEY_R = 82; + private static final int KEY_LEFT_CTRL = 341; + private static final int KEY_RIGHT_CTRL = 345; + + @Override + public void onInitializeClient() { + instance = this; + this.config = this.configStore.load(); + this.guiKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.autodig.open", KEY_R, "category.autodig")); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + // 同步模组配置到 Baritone(工具切换 / 避岩浆 / 避怪/保命) + this.bridge().applyConfig(this.config); + this.controller.tick(this.config); + while (this.guiKey.wasPressed()) { + if (this.isCtrlDown(client)) { + this.openGui(); + } + } + if (this.openGuiRequested) { + this.openGuiRequested = false; + try { + client.setScreen(new AutoDigGui(this)); + } catch (Throwable t) { + if (client.player != null) { + client.player.sendMessage(Text.literal("AutoDig GUI \u6253\u5f00\u5931\u8d25: " + t), false); + } + } + } + }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) + -> AutoDigCommands.register(dispatcher)); + } + + public static AutoDigModClient instance() { + return instance; + } + + public DigController controller() { + return this.controller; + } + + public DigConfig config() { + return this.config; + } + + public BaritoneBridge bridge() { + return this.controller.bridge(); + } + + public void saveConfig() { + this.configStore.save(this.config); + } + + /** 请求打开 AutoDig GUI(下一帧安全执行)。 */ + public void openGui() { + this.openGuiRequested = true; + } + + /** 向玩家聊天栏发送一条提示。 */ + public void sendToast(String message) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null) { + client.player.sendMessage(Text.literal(message), false); + } + } + + private boolean isCtrlDown(MinecraftClient client) { + long handle = client.getWindow().getHandle(); + return InputUtil.isKeyPressed(handle, KEY_LEFT_CTRL) + || InputUtil.isKeyPressed(handle, KEY_RIGHT_CTRL); + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/bridge/BaritoneBridge.java b/src/main/java/autodig/client/bridge/BaritoneBridge.java new file mode 100644 index 0000000..f072696 --- /dev/null +++ b/src/main/java/autodig/client/bridge/BaritoneBridge.java @@ -0,0 +1,133 @@ +package autodig.client.bridge; + +import autodig.client.config.DigConfig; +import baritone.api.BaritoneAPI; +import baritone.api.IBaritone; +import baritone.api.Settings; +import baritone.api.utils.BlockOptionalMeta; +import java.util.List; +import java.util.function.Predicate; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.BlockPos; + +/** + * Baritone API 封装(桥接层)。 + * + *

把模组对 Baritone 的所有调用集中在此类:同步用户配置(工具切换、避岩浆、 + * 避怪、保命优先)、按名字/数量挖矿、前往指定坐标、前往最近传送门、 + * 跟随并猎杀生物(杀戮光环之猎杀模式)、取消任务等。

+ * + *

所有调用都做空安全保护,Baritone 未加载时相关操作会安全地静默失败。

+ */ +public final class BaritoneBridge { + + /** + * 把模组配置同步到 Baritone 全局设置。 + * + * @param config 挖矿配置 + */ + public void applyConfig(DigConfig config) { + Settings settings = BaritoneAPI.getSettings(); + settings.allowSprint.value = true; + settings.allowBreak.value = true; // 挖矿必须允许挖方块 + settings.allowPlace.value = true; // 允许搭路/垫脚 + settings.allowParkour.value = true; + settings.allowParkourPlace.value = true; + settings.freeLook.value = true; + // 自动切换最合适工具 + settings.autoTool.value = config.autoTool; + // 保命优先:让 Baritone 寻路时主动绕开敌对生物 + settings.avoidance.value = config.survivalFirst; + settings.mobAvoidanceRadius.value = 5; + // 优先绕开岩浆池:给岩浆(含流动岩浆所在方块)额外的寻路代价 + settings.blocksToAvoid.value = config.avoidLava + ? List.of(Blocks.LAVA) + : List.of(); + } + + /** + * 按方块 id 挖指定数量的矿。 + * + * @param count 要挖的最大数量(-1 表示无穷,挖到为止) + * @param blockNames 方块 id,如 "diamond_ore"、"deepslate_diamond_ore" + */ + public void mineByName(int count, String... blockNames) { + IBaritone baritone = this.primary(); + if (baritone == null || blockNames.length == 0) { + return; + } + baritone.getMineProcess().mineByName(count, blockNames); + } + + /** 派遣 Baritone 前往指定坐标(自动挖掘途中障碍)。 */ + public void gotoBlock(BlockPos target) { + IBaritone baritone = this.primary(); + if (baritone == null || target == null) { + return; + } + baritone.getCustomGoalProcess().setGoalAndPath(new baritone.api.pathing.goals.GoalBlock(target)); + } + + /** 前往离玩家最近的传送门方块(用于进出下界)。 */ + public void toNetherPortal() { + IBaritone baritone = this.primary(); + if (baritone == null) { + return; + } + baritone.getGetToBlockProcess().getToBlock(new BlockOptionalMeta(Blocks.NETHER_PORTAL)); + } + + /** 是否正在采集一个收尾中的目标(传送门等)。 */ + public boolean gettingToBlock() { + IBaritone baritone = this.primary(); + return baritone != null && baritone.getGetToBlockProcess().isActive(); + } + + /** 跟随/猎杀所有满足条件的生物(杀戮光环之猎杀模式)。 */ + public void follow(Predicate filter) { + IBaritone baritone = this.primary(); + if (baritone == null) { + return; + } + baritone.getFollowProcess().follow(filter); + } + + /** 取消当前 Baritone 任务(挖矿/寻路/跟随)。 */ + public void cancelAll() { + IBaritone baritone = this.primary(); + if (baritone == null) { + return; + } + baritone.getPathingBehavior().cancelEverything(); + baritone.getMineProcess().cancel(); + baritone.getFollowProcess().cancel(); + } + + /** @return Baritone 当前是否正在寻路/执行任务 */ + public boolean isActive() { + IBaritone baritone = this.primary(); + if (baritone == null) { + return false; + } + return baritone.getPathingBehavior().isPathing() + || baritone.getMineProcess().isActive() + || baritone.getFollowProcess().isActive() + || baritone.getGetToBlockProcess().isActive(); + } + + /** @return Baritone 是否已加载(可获取主实例) */ + public boolean isAvailable() { + return this.primary() != null; + } + + /** 获取 Baritone 主实例;未加载或异常时返回 null。 */ + private IBaritone primary() { + try { + return BaritoneAPI.getProvider().getPrimaryBaritone(); + } catch (Throwable ignored) { + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/command/AutoDigCommands.java b/src/main/java/autodig/client/command/AutoDigCommands.java new file mode 100644 index 0000000..3e077a1 --- /dev/null +++ b/src/main/java/autodig/client/command/AutoDigCommands.java @@ -0,0 +1,65 @@ +package autodig.client.command; + +import autodig.client.AutoDigModClient; +import autodig.client.config.MineTask; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.text.Text; + +/** + * AutoDig 客户端命令。 + * + *

提供 {@code /autodig gui}(打开界面)、{@code /autodig start <任务>} + * (直接启动)、{@code /autodig stop}(停止当前任务)三个快捷入口, + * 方便在未绑定的快捷键场景下使用。

+ */ +public final class AutoDigCommands { + + private AutoDigCommands() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(com.mojang.brigadier.builder.LiteralArgumentBuilder + .literal("autodig") + .then(com.mojang.brigadier.builder.LiteralArgumentBuilder.literal("gui") + .executes(ctx -> { + AutoDigModClient.instance().openGui(); + return 1; + })) + .then(com.mojang.brigadier.builder.LiteralArgumentBuilder.literal("start") + .then(com.mojang.brigadier.builder.RequiredArgumentBuilder + .argument("task", StringArgumentType.word()) + .executes(ctx -> { + String name = StringArgumentType.getString(ctx, "task"); + MineTask task = parseTask(name); + if (task == null) { + ctx.getSource().sendFeedback(Text.literal("未知任务: " + name + "(可用 diamond / debris)")); + return 0; + } + AutoDigModClient mod = AutoDigModClient.instance(); + mod.config().task = task; + mod.saveConfig(); + mod.sendToast("AutoDig: 已选择任务 " + task.label()); + return 1; + }))) + .then(com.mojang.brigadier.builder.LiteralArgumentBuilder.literal("stop") + .executes(ctx -> { + AutoDigModClient mod = AutoDigModClient.instance(); + mod.config().task = MineTask.NONE; + mod.saveConfig(); + mod.bridge().cancelAll(); + mod.sendToast("AutoDig: 已停止"); + return 1; + }))); + } + + private static MineTask parseTask(String name) { + return switch (name.toLowerCase()) { + case "diamond", "钻石" -> MineTask.DIAMOND; + case "debris", "ancient_debris", "远古残骸" -> MineTask.ANCIENT_DEBRIS; + case "none", "stop", "空闲" -> MineTask.NONE; + default -> null; + }; + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/config/ConfigStore.java b/src/main/java/autodig/client/config/ConfigStore.java new file mode 100644 index 0000000..a5c539b --- /dev/null +++ b/src/main/java/autodig/client/config/ConfigStore.java @@ -0,0 +1,56 @@ +package autodig.client.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import net.fabricmc.loader.api.FabricLoader; + +/** + * 配置文件读写器。 + * + *

使用 Gson 将 {@link DigConfig} 序列化为 JSON, + * 配置文件位于 Fabric 配置目录下的 {@code autodig.json}。

+ */ +public final class ConfigStore { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public DigConfig load() { + Path path = this.path(); + if (!Files.exists(path, new LinkOption[0])) { + return new DigConfig(); + } + try { + String json = Files.readString(path, StandardCharsets.UTF_8); + DigConfig loaded = GSON.fromJson(json, DigConfig.class); + if (loaded == null) { + return new DigConfig(); + } + return loaded; + } catch (Exception e) { + return new DigConfig(); + } + } + + public void save(DigConfig config) { + Path path = this.path(); + try { + if (path.getParent() != null) { + Files.createDirectories(path.getParent(), new FileAttribute[0]); + } + Files.writeString(path, GSON.toJson(config), StandardCharsets.UTF_8, new OpenOption[0]); + } catch (IOException iOException) { + // 保存失败时静默忽略 + } + } + + private Path path() { + return FabricLoader.getInstance().getConfigDir().resolve("autodig.json"); + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/config/DigConfig.java b/src/main/java/autodig/client/config/DigConfig.java new file mode 100644 index 0000000..ffd4f69 --- /dev/null +++ b/src/main/java/autodig/client/config/DigConfig.java @@ -0,0 +1,75 @@ +package autodig.client.config; + +/** + * 挖矿任务的用户可配置项(可序列化为 JSON,由 GUI 直接读写)。 + */ +public class DigConfig { + + /** 当前选中的挖矿任务 */ + public MineTask task = MineTask.DIAMOND; + + // ---- 通用能力开关 ---- + /** 自动切换最合适的工具(钻石稿/铁镐等) */ + public boolean autoTool = true; + /** 自动进食(饥饿度低时自动吃背包/快捷栏食物) */ + public boolean autoEat = true; + /** 饥饿度低于该值时开始进食(20 为满值) */ + public int eatHungerThreshold = 8; + + // ---- 保命/安全 ---- + /** 保命优先:生命过低时暂停挖矿并向上撤离 */ + public boolean survivalFirst = true; + /** 生命值(半心百分比,0-100)低于该值时触发保命撤离 */ + public int healthFleeThreshold = 40; + + // ---- 岩浆/障碍 ---- + /** 优先绕开岩浆池(给岩浆方块额外寻路代价) */ + public boolean avoidLava = true; + + // ---- 杀戮光环 ---- + /** 杀戮光环:自动攻击附近敌对生物 */ + public boolean killAura = false; + /** 杀戮光环:仅在生命阈值内生效,生命过低时不冒进 */ + public boolean killAuraSafe = true; + /** 杀戮光环攻击范围(方块) */ + public double killRange = 4.0; + + // ---- 钻石(主世界)参数 ---- + /** 鱼骨巷道瞄准的 Y 层(钻石一般在 -60 附近) */ + public int diamondLevel = -58; + /** 鱼骨主干巷道长度(方块) */ + public int tunnelLength = 60; + + // ---- 远古残骸参数 ---- + /** 需要挖多少个远古残骸后视为完成 */ + public int debrisCount = 4; + /** 挖够数量后自动回到主世界 */ + public boolean debrisReturnHome = true; + /** 是否开启下界的自动猎杀/杀戮光环协同 */ + public boolean killAuraInNether = true; + + // ---- 死亡重生 ---- + /** 死亡重生后自动回到死亡地点继续任务 */ + public boolean respawnReturn = true; + + public DigConfig copy() { + DigConfig c = new DigConfig(); + c.task = this.task; + c.autoTool = this.autoTool; + c.autoEat = this.autoEat; + c.eatHungerThreshold = this.eatHungerThreshold; + c.survivalFirst = this.survivalFirst; + c.healthFleeThreshold = this.healthFleeThreshold; + c.avoidLava = this.avoidLava; + c.killAura = this.killAura; + c.killAuraSafe = this.killAuraSafe; + c.killRange = this.killRange; + c.diamondLevel = this.diamondLevel; + c.tunnelLength = this.tunnelLength; + c.debrisCount = this.debrisCount; + c.debrisReturnHome = this.debrisReturnHome; + c.killAuraInNether = this.killAuraInNether; + c.respawnReturn = this.respawnReturn; + return c; + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/config/MineTask.java b/src/main/java/autodig/client/config/MineTask.java new file mode 100644 index 0000000..6fe31ea --- /dev/null +++ b/src/main/java/autodig/client/config/MineTask.java @@ -0,0 +1,26 @@ +package autodig.client.config; + +/** + * 挖矿任务类型。 + * + *

不同任务对应不同的自动挖矿策略:主世界钻石走鱼骨巷道 + Baritone 矿脉探测, + * 远古残骸需要先进传送门下界、再矿脉探测、挖完/死亡后自动回主世界。

+ */ +public enum MineTask { + /** 空闲:不执行任何挖矿任务 */ + NONE("空闲"), + /** 主世界挖钻石(含深层钻石矿石) */ + DIAMOND("钻石"), + /** 下界挖远古残骸(自动进出传送门、死亡重生返回坐标) */ + ANCIENT_DEBRIS("远古残骸"); + + private final String label; + + MineTask(String label) { + this.label = label; + } + + public String label() { + return this.label; + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/controller/DigController.java b/src/main/java/autodig/client/controller/DigController.java new file mode 100644 index 0000000..d8e4f7e --- /dev/null +++ b/src/main/java/autodig/client/controller/DigController.java @@ -0,0 +1,235 @@ +package autodig.client.controller; + +import autodig.client.bridge.BaritoneBridge; +import autodig.client.config.DigConfig; +import autodig.client.config.MineTask; +import autodig.client.miner.DebrisHandler; +import autodig.client.miner.FishboneMiner; +import autodig.client.service.AutoEat; +import autodig.client.service.DeathRecorder; +import autodig.client.service.KillAura; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.Heightmap; + +/** + * 挖矿总控制器:把配置、Baritone 桥接、自动进食、杀戮光环、死亡记录和各任务 + * 专用挖矿器串起来,驱动一个简单的状态机。 + */ +public final class DigController { + + /** 内部状态机 */ + private enum State { + IDLE, FISHBONE, CLIMB, DEBRIS, FLEE, RETURN_TO_DEATH + } + + private final BaritoneBridge bridge = new BaritoneBridge(); + private final AutoEat autoEat = new AutoEat(); + private final KillAura killAura = new KillAura(); + private final DeathRecorder deathRecorder = new DeathRecorder(); + private final FishboneMiner fishbone = new FishboneMiner(this.bridge); + private final DebrisHandler debris = new DebrisHandler(this.bridge); + + private State state = State.IDLE; + private BlockPos resumeTarget; + private long lastIssue; + private boolean wasDead; + + /** 每帧驱动整个自动挖矿流程。 */ + public void tick(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) { + this.state = State.IDLE; + return; + } + PlayerEntity player = client.player; + + // 常驻能力:杀戮光环、自动进食 + this.killAura.tick(config); + this.autoEat.tick(config); + + // 死亡处理:死亡瞬间暂停任务,重生后按需回到死亡坐标 + this.deathRecorder.tick(config, this::onRespawn); + if (player.isDead()) { + this.wasDead = true; + this.bridge.cancelAll(); + return; + } + this.wasDead = false; + + // 保命优先:血线过低时向上撤离(不冒进挖矿) + if (config.survivalFirst && this.healthPercent(player) < config.healthFleeThreshold) { + this.fleeTick(config); + return; + } + + // 未选择任务时归位 + if (config.task == MineTask.NONE) { + if (this.state != State.IDLE) { + this.bridge.cancelAll(); + this.state = State.IDLE; + } + return; + } + + this.drive(config, player); + } + + private void drive(DigConfig config, PlayerEntity player) { + switch (this.state) { + case IDLE -> { + if (config.task == MineTask.DIAMOND) { + this.fishbone.init(config); + this.state = State.FISHBONE; + } else if (config.task == MineTask.ANCIENT_DEBRIS) { + this.debris.reset(); + this.state = State.DEBRIS; + } + } + case FISHBONE -> { + player.sendMessage(Text.literal("AutoDig: 主世界鱼骨挖矿进行中"), true); + if (this.fishbone.tick(config)) { + this.bridge.cancelAll(); + this.pathToSurface(player.getBlockPos()); + this.state = State.CLIMB; + } + } + case CLIMB -> { + if (this.nearSurface(player)) { + this.finish("鱼骨挖矿完成,已返回地表"); + } + } + case DEBRIS -> { + player.sendMessage(Text.literal("AutoDig: 下界远古残骸挖矿进行中"), true); + if (this.debris.tick(config)) { + this.finish("远古残骸任务完成,已回到主世界"); + } + } + case RETURN_TO_DEATH -> this.returnToDeathTick(player, config); + case FLEE -> { + // FLEE 在 tick() 中单独处理,正常情况下不会进到这里 + } + } + } + + /** 重生后先回到最近的死亡坐标,再续上原任务。 */ + private void onRespawn() { + BlockPos deathPos = this.deathRecorder.lastDeathPos(); + if (deathPos != null) { + this.bridge.cancelAll(); + this.resumeTarget = deathPos; + this.state = State.RETURN_TO_DEATH; + } + } + + private void returnToDeathTick(PlayerEntity player, DigConfig config) { + if (this.resumeTarget == null) { + this.resumeTask(config); + return; + } + double dist = this.distanceSq(player, this.resumeTarget); + if (dist < 9.0) { + this.resumeTarget = null; + this.resumeTask(config); + } else { + this.issue(() -> this.bridge.gotoBlock(this.resumeTarget)); + } + } + + /** 按当前任务与所在维度恢复挖矿。 */ + private void resumeTask(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + boolean inNether = client.world != null && client.world.getRegistryKey() == net.minecraft.world.World.NETHER; + if (config.task == MineTask.DIAMOND) { + this.fishbone.init(config); + this.state = State.FISHBONE; + } else if (config.task == MineTask.ANCIENT_DEBRIS) { + this.debris.reset(); + this.state = State.DEBRIS; + // 若已在下界,让残留处理机直接从挖矿阶段继续 + if (inNether) { + this.state = State.DEBRIS; + } + } else { + this.state = State.IDLE; + } + } + + /** 保命撤离:把玩家送到脚下地表,血线恢复后继续原任务。 */ + private void fleeTick(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + PlayerEntity player = client.player; + if (player == null) { + return; + } + this.bridge.cancelAll(); + this.pathToSurface(player.getBlockPos()); + if (this.healthPercent(player) >= config.healthFleeThreshold + 20) { + this.resumeTask(config); + } + } + + private void finish(String message) { + this.bridge.cancelAll(); + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null) { + client.player.sendMessage(Text.literal("AutoDig: " + message), true); + } + this.state = State.IDLE; + } + + private double healthPercent(PlayerEntity player) { + return player.getHealth() / Math.max(player.getMaxHealth(), 1.0) * 100.0; + } + + private double distanceSq(PlayerEntity player, BlockPos target) { + double dx = player.getX() - (target.getX() + 0.5); + double dz = player.getZ() - (target.getZ() + 0.5); + return dx * dx + dz * dz; + } + + private void issue(Runnable action) { + long now = System.currentTimeMillis(); + if (now - this.lastIssue > 250) { + action.run(); + this.lastIssue = now; + } + } + + /** 计算垂直方向上的地表坐标并派发前往(节流)。 */ + private void pathToSurface(BlockPos below) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world == null) { + return; + } + int x = below.getX(); + int z = below.getZ(); + int top = client.world.getTopY(Heightmap.Type.MOTION_BLOCKING, x, z); + BlockPos surface = new BlockPos(x, Math.max(top, below.getY()), z); + this.issue(() -> this.bridge.gotoBlock(surface)); + } + + /** 玩家是否已回到地表。 */ + private boolean nearSurface(PlayerEntity player) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world == null) { + return true; + } + int x = player.getBlockX(); + int z = player.getBlockZ(); + int top = client.world.getTopY(Heightmap.Type.MOTION_BLOCKING, x, z); + return Math.abs(player.getY() - (top + 1)) < 4.0; + } + + /** @return Baritone 桥接(供其他模块复用)。 */ + public BaritoneBridge bridge() { + return this.bridge; + } + + /** 供 GUI/调试获取当前状态文本。 */ + public String statusText() { + return this.state.name(); + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/gui/AutoDigGui.java b/src/main/java/autodig/client/gui/AutoDigGui.java new file mode 100644 index 0000000..bfbcff8 --- /dev/null +++ b/src/main/java/autodig/client/gui/AutoDigGui.java @@ -0,0 +1,174 @@ +package autodig.client.gui; + +import autodig.client.AutoDigModClient; +import autodig.client.config.DigConfig; +import autodig.client.config.MineTask; +import autodig.client.controller.DigController; +import java.util.function.Consumer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.CheckboxWidget; +import net.minecraft.text.Text; + +/** + * AutoDig 游戏内 GUI。 + * + *

用于选择挖什么(钻石 / 远古残骸),并快捷设置:保命优先、优先绕开岩浆、 + * 自动切换工具、自动进食、杀戮光环、重生后回死亡点、挖完回主世界, + * 以及钻石层数 / 巷道长度 / 残骸数量等参数。界面中的改动在"开始/停止/关闭"时 + * 同步到 mod 全局配置并落盘。

+ */ +public final class AutoDigGui extends Screen { + + private final AutoDigModClient mod; + private final DigConfig config; + private final DigController controller; + + // 动态数值按钮(点击 +/- 后刷新显示) + private ButtonWidget levelBtn; + private ButtonWidget tunnelBtn; + private ButtonWidget debrisBtn; + + public AutoDigGui(AutoDigModClient mod) { + super(Text.literal("AutoDig")); + this.mod = mod; + this.config = mod.config().copy(); + this.controller = mod.controller(); + } + + @Override + protected void init() { + this.clearChildren(); + int colX = 26; + int x = colX; + int y = 34; + // ===== 任务选择 ===== + this.taskButton("空闲", MineTask.NONE, x, y); + this.taskButton("钻石(鱼骨)", MineTask.DIAMOND, x + 110, y); + this.taskButton("远古残骸", MineTask.ANCIENT_DEBRIS, x + 220, y); + + // ===== 开关设置 ===== + y += 30; + this.check("保命优先", colX, y, () -> this.config.survivalFirst, v -> this.config.survivalFirst = v); + this.check("优先绕开岩浆", colX + 150, y, () -> this.config.avoidLava, v -> this.config.avoidLava = v); + y += 24; + this.check("自动进食", colX, y, () -> this.config.autoEat, v -> this.config.autoEat = v); + this.check("自动切换工具", colX + 150, y, () -> this.config.autoTool, v -> this.config.autoTool = v); + y += 24; + this.check("杀戮光环", colX, y, () -> this.config.killAura, v -> this.config.killAura = v); + this.check("重生后回死亡点", colX + 150, y, () -> this.config.respawnReturn, v -> this.config.respawnReturn = v); + y += 24; + this.check("挖完回主世界", colX, y, () -> this.config.debrisReturnHome, v -> this.config.debrisReturnHome = v); + this.check("避难时绝杀不冒进", colX + 150, y, () -> this.config.killAuraSafe, v -> this.config.killAuraSafe = v); + + // ===== 数值参数 ===== + y += 32; + this.levelBtn = this.numberRow("钻石层(Y)", colX, y, () -> this.config.diamondLevel, v -> this.config.diamondLevel = v); + y += 24; + this.tunnelBtn = this.numberRow("巷道长度", colX, y, () -> this.config.tunnelLength, v -> this.config.tunnelLength = v); + y += 24; + this.debrisBtn = this.numberRow("残骸数量", colX, y, () -> this.config.debrisCount, v -> this.config.debrisCount = v); + + // ===== 底部操作按钮 ===== + y += 40; + ButtonWidget start = this.textButton("开始 (保存并运行)", b -> this.start(), x, y, 140); + ButtonWidget stop = this.textButton("停止", b -> this.stop(), x + 146, y, 90); + ButtonWidget saveClose = this.textButton("仅关闭", b -> this.close(), x + 242, y, 80); + this.addDrawableChild(start); + this.addDrawableChild(stop); + this.addDrawableChild(saveClose); + } + + private ButtonWidget taskButton(String label, MineTask task, int x, int y) { + ButtonWidget b = this.textButton(label, button -> { + this.config.task = task; + this.init(); // 刷新选中高亮 + }, x, y, 104); + this.addDrawableChild(b); + return b; + } + + private void check(String label, int x, int y, java.util.function.BooleanSupplier getter, Consumer setter) { + CheckboxWidget box = CheckboxWidget.builder(Text.literal(label), this.textRenderer) + .pos(x, y).checked(getter.getAsBoolean()) + .callback((b, value) -> setter.accept(value)).build(); + this.addDrawableChild(box); + } + + private ButtonWidget numberRow(String label, int x, int y, java.util.function.IntSupplier getter, Consumer setter) { + this.addDrawableChild(ButtonWidget.builder(Text.literal("-"), b -> { + setter.accept(getter.getAsInt() - 1); + this.refreshNumberButtons(); + }).dimensions(x, y, 20, 20).build()); + int valueX = x + 24; + int valueW = 72; + ButtonWidget value = ButtonWidget.builder(Text.literal(label + " : " + getter.getAsInt()), b -> { + }).dimensions(valueX, y, valueW, 20).build(); + value.active = false; // 纯展示 + this.addDrawableChild(value); + this.addDrawableChild(ButtonWidget.builder(Text.literal("+"), b -> { + setter.accept(getter.getAsInt() + 1); + this.refreshNumberButtons(); + }).dimensions(valueX + valueW + 4, y, 20, 20).build()); + return value; + } + + private void refreshNumberButtons() { + this.levelBtn.setMessage(Text.literal("钻石层(Y) : " + this.config.diamondLevel)); + this.tunnelBtn.setMessage(Text.literal("巷道长度 : " + this.config.tunnelLength)); + this.debrisBtn.setMessage(Text.literal("残骸数量 : " + this.config.debrisCount)); + } + + private ButtonWidget textButton(String label, ButtonWidget.PressAction action, int x, int y, int w) { + return ButtonWidget.builder(Text.literal(label), action).dimensions(x, y, w, 20).build(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(this.textRenderer, "AutoDig \u81ea\u52a8\u6316\u77ff", this.width / 2, 10, 0xFFFFFF); + context.drawTextWithShadow(this.textRenderer, + "\u5f53\u524d\u4efb\u52a1: " + this.config.task.label() + " \u72b6\u6001: " + this.controller.statusText(), + 26, this.height - 24, 0xAAAAAA); + } + + /** 把界面临时配置写回 mod 全局配置并保存(供 controller 实时读取)。 */ + private void applyToMod() { + DigConfig target = this.mod.config(); + target.task = this.config.task; + target.survivalFirst = this.config.survivalFirst; + target.avoidLava = this.config.avoidLava; + target.autoEat = this.config.autoEat; + target.autoTool = this.config.autoTool; + target.killAura = this.config.killAura; + target.killAuraSafe = this.config.killAuraSafe; + target.respawnReturn = this.config.respawnReturn; + target.debrisReturnHome = this.config.debrisReturnHome; + target.diamondLevel = this.config.diamondLevel; + target.tunnelLength = this.config.tunnelLength; + target.debrisCount = this.config.debrisCount; + this.mod.saveConfig(); + } + + private void start() { + this.applyToMod(); + this.mod.sendToast("AutoDig: 已开始 " + this.config.task.label() + " 任务"); + this.close(); + } + + private void stop() { + this.applyToMod(); + this.mod.config().task = MineTask.NONE; + this.mod.saveConfig(); + this.controller.bridge().cancelAll(); + this.mod.sendToast("AutoDig: 已停止"); + this.close(); + } + + @Override + public void close() { + this.applyToMod(); + super.close(); + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/miner/DebrisHandler.java b/src/main/java/autodig/client/miner/DebrisHandler.java new file mode 100644 index 0000000..b9f3d3e --- /dev/null +++ b/src/main/java/autodig/client/miner/DebrisHandler.java @@ -0,0 +1,125 @@ +package autodig.client.miner; + +import autodig.client.bridge.BaritoneBridge; +import autodig.client.config.DigConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.registry.RegistryKey; +import net.minecraft.world.World; + +/** + * 远古残骸自动挖矿流转(下界)。 + * + *

流程:主世界 -> 走进最近传送门 -> 进入下界 -> 挖远古残骸 -> + * (背包满/挖够)-> 找下界传送门走回主世界。死亡重生的返回逻辑由控制器统一处理, + * 本类每次 tick 依据当前所在维度自适应地执行对应阶段。

+ */ +public final class DebrisHandler { + + private final BaritoneBridge bridge; + + private enum Stage { + /** 在下界从传送门前往主世界(或已在主世界即完成) */ + GO_COMMIT, + /** 在前一个维度前往传送门 */ + GO_PORTAL, + /** 挖远古残骸 */ + MINE, + /** 返回主世界 */ + RETURN_PORTAL + } + + private Stage stage = Stage.GO_COMMIT; + private boolean done; + /** 派发节流 */ + private long lastIssue; + + public DebrisHandler(BaritoneBridge bridge) { + this.bridge = bridge; + } + + /** 任务结束或需要切换任务时归还控制权。 */ + public void reset() { + this.stage = Stage.GO_COMMIT; + this.done = false; + } + + /** + * 每帧推进下界挖矿流转。 + * + * @param config 挖矿配置 + * @return 是否已全部完成(含回到主世界) + */ + public boolean tick(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) { + return this.done; + } + PlayerEntity player = client.player; + RegistryKey dim = client.world.getRegistryKey(); + boolean inOverworld = dim == World.OVERWORLD; + boolean inNether = dim == World.NETHER; + + switch (this.stage) { + case GO_COMMIT -> { + // 选择去哪个维度 + if (inNether) { + this.stage = Stage.MINE; + } else { + this.stage = Stage.GO_PORTAL; + } + } + case GO_PORTAL -> { + if (inNether) { + this.stage = Stage.MINE; // 已通过传送门进入下界 + } else { + this.issue(() -> this.bridge.toNetherPortal()); + } + } + case MINE -> { + if (!inNether) { + // 意外回到主世界,重走流程 + this.stage = Stage.GO_COMMIT; + } else if (this.inventoryFull(player) || this.bridge.isActive() == false) { + // 背包满或 Baritone 空闲即为本段结束;若期望回程则前往传送门 + if (config.debrisReturnHome) { + this.stage = Stage.RETURN_PORTAL; + } else { + this.stage = Stage.GO_COMMIT; // 停在下界挖(不返回) + } + } else { + this.issue(() -> this.bridge.mineByName(config.debrisCount, "ancient_debris")); + } + } + case RETURN_PORTAL -> { + if (inOverworld) { + this.done = true; // 已回到主世界,全流程完成 + } else { + this.issue(() -> this.bridge.toNetherPortal()); + } + } + } + return this.done; + } + + private void issue(Runnable action) { + long now = System.currentTimeMillis(); + if (now - this.lastIssue > 1000) { + action.run(); + this.lastIssue = now; + } + } + + private boolean inventoryFull(PlayerEntity player) { + for (int i = 0; i < 36; i++) { + if (player.getInventory().main.get(i).isEmpty()) { + return false; + } + } + return true; + } + + public boolean isDone() { + return this.done; + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/miner/FishboneMiner.java b/src/main/java/autodig/client/miner/FishboneMiner.java new file mode 100644 index 0000000..1281887 --- /dev/null +++ b/src/main/java/autodig/client/miner/FishboneMiner.java @@ -0,0 +1,153 @@ +package autodig.client.miner; + +import autodig.client.bridge.BaritoneBridge; +import autodig.client.config.DigConfig; +import java.util.ArrayList; +import java.util.List; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.math.BlockPos; + +/** + * 鱼骨巷道挖矿器(主世界钻石)。 + * + *

在指定的 Y 层,沿玩家朝向挖一条"主干隧道",并在主干上每隔 + * {@code branchGap} 格向两侧各开一条侧支巷道,形成鱼骨状分布。 + * 推进实现为:把鱼骨图案转成一串目标坐标 {@link GoalBlock},按顺序 + * 派 Baritone 逐个前往;因为允许挖方块,Baritone 会走直线把挡路的方块 + * 挖掉,从而挖出巷道。自动切换工具由 Baritone 的 {@code autoTool} 负责。

+ * + *

遇到背包已满时提前结束,把控制权交还控制器(用于返回地表)。

+ */ +public final class FishboneMiner { + + private final BaritoneBridge bridge; + + private BlockPos start; + private int yLevel; + private List plan = new ArrayList<>(); + private int step; + /** 是否已完成 */ + private boolean done; + /** 上一次朝向目标派发寻路的时间戳,用于失败重试节流 */ + private long lastIssue; + + public FishboneMiner(BaritoneBridge bridge) { + this.bridge = bridge; + } + + /** + * 以玩家当前位置为起点建立鱼骨巷道计划。 + * + * @param config 挖矿配置 + */ + public void init(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + PlayerEntity player = client.player; + if (player == null) { + return; + } + this.yLevel = config.diamondLevel; + BlockPos stop = player.getBlockPos(); + this.start = new BlockPos(stop.getX(), this.yLevel, stop.getZ()); + int branchLen = 6; + int branchGap = 8; + int axis = Math.abs(player.getMovementDirection().getOffsetX()) > Math.abs(player.getMovementDirection().getOffsetZ()) ? 0 : 1; + int sign = axis == 0 ? player.getMovementDirection().getOffsetX() : player.getMovementDirection().getOffsetZ(); + sign = sign == 0 ? 1 : sign; + this.plan = this.buildPlan(this.start, this.yLevel, config.tunnelLength, branchLen, branchGap, axis, sign); + this.step = 0; + this.done = false; + } + + private List buildPlan(BlockPos base, int y, int trunkLen, int branchLen, int branchGap, int axis, int sign) { + List plan = new ArrayList<>(); + int dx = axis == 0 ? sign : 0; + int dz = axis == 0 ? 0 : sign; + // 侧向单位向量 + int sx = axis == 0 ? 0 : 1; + int sz = axis == 0 ? 1 : 0; + for (int t = 0; t <= trunkLen; t++) { + BlockPos trunk = base.add(dx * t, 0, dz * t); + plan.add(trunk); + // 每 branchGap 格开一对侧支(去程+回程) + if (t > 0 && t % branchGap == 0) { + for (int b = 1; b <= branchLen; b++) { + plan.add(trunk.add(sx * b, 0, sz * b)); + } + for (int b = branchLen - 1; b >= 1; b--) { + plan.add(trunk.add(sx * b, 0, sz * b)); + } + } + } + return plan; + } + + /** + * 每帧推进一次。背包已满或走完全程则返回已完成。 + * + * @param config 挖矿配置 + */ + public boolean tick(DigConfig config) { + if (this.done) { + return true; + } + MinecraftClient client = MinecraftClient.getInstance(); + PlayerEntity player = client.player; + if (player == null) { + return true; + } + if (this.inventoryFull(player)) { + this.done = true; + return true; + } + if (this.step >= this.plan.size()) { + this.done = true; + return true; + } + BlockPos target = this.plan.get(this.step); + double dist = this.distanceSq(player, target); + if (dist <= 2.0) { + // 已到达当前目标,推进到下一个 + this.step++; + return this.step >= this.plan.size(); + } + // 距离太长或 Baritone 没在干活时重新派发 + if (dist > 4.0 || !this.bridge.isActive()) { + long now = System.currentTimeMillis(); + if (now - this.lastIssue > 250) { + this.bridge.gotoBlock(target); + this.lastIssue = now; + } + } + return false; + } + + private double distanceSq(PlayerEntity player, BlockPos target) { + double dx = player.getX() - (target.getX() + 0.5); + double dy = player.getY() - target.getY(); + double dz = player.getZ() - (target.getZ() + 0.5); + return dx * dx + dy * dy + dz * dz; + } + + private boolean inventoryFull(PlayerEntity player) { + for (int i = 0; i < 36; i++) { + if (player.getInventory().main.get(i).isEmpty()) { + return false; + } + } + // 主背包全满(含快捷栏),无空位可存放 + return true; + } + + /** @return 是否已挖完 */ + public boolean isDone() { + return this.done; + } + + /** 重新允许继续(死亡重生/保命恢复后复用)。 */ + public void abort() { + this.done = true; + this.plan = new ArrayList<>(); + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/service/AutoEat.java b/src/main/java/autodig/client/service/AutoEat.java new file mode 100644 index 0000000..4b0235c --- /dev/null +++ b/src/main/java/autodig/client/service/AutoEat.java @@ -0,0 +1,53 @@ +package autodig.client.service; + +import autodig.client.config.DigConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.item.consume.UseAction; +import net.minecraft.util.Hand; + +/** + * 自动进食服务。 + * + *

饥饿度低于阈值时,自动从快捷栏挑选一件食物并持续"使用手指"进食。 + * 已在使用物品(正在咀嚼)时不会重复触发,避免打断进食节奏。

+ */ +public final class AutoEat { + + /** + * 每帧调用。饥饿不够时在快捷栏里找食物并吃掉。 + * + * @param config 挖矿配置 + */ + public void tick(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null) { + return; + } + net.minecraft.entity.player.PlayerEntity player = client.player; + // 已在进食(用于吞咽)时等待,避免反复打断 + if (player.isUsingItem()) { + return; + } + int food = player.getHungerManager().getFoodLevel(); + if (food >= config.eatHungerThreshold) { + return; + } + PlayerInventory inv = player.getInventory(); + // 优先在快捷栏找食物 + for (int slot = 0; slot < 9; slot++) { + ItemStack stack = inv.getStack(slot); + if (stack.isEmpty() || stack.getUseAction() != UseAction.EAT) { + continue; + } + // 先切到该槽位,下一帧再触发进食 + if (inv.selectedSlot != slot) { + inv.selectedSlot = slot; + return; + } + client.interactionManager.interactItem(player, Hand.MAIN_HAND); + return; + } + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/service/DeathRecorder.java b/src/main/java/autodig/client/service/DeathRecorder.java new file mode 100644 index 0000000..7590e14 --- /dev/null +++ b/src/main/java/autodig/client/service/DeathRecorder.java @@ -0,0 +1,64 @@ +package autodig.client.service; + +import autodig.client.config.DigConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.BlockPos; + +/** + * 死亡记录服务。 + * + *

在玩家死亡的那一刻记录其坐标;重生(isDead 由真转假)后, + * 若开启了"重生返回",通过回调通知控制器派 Baritone 回到死亡地点继续任务。

+ */ +public final class DeathRecorder { + + private boolean wasDead; + /** 最近一次死亡的坐标 */ + private BlockPos deathPos; + private boolean pendingReturn; + + /** + * 每帧调用。 + * + * @param config 挖矿配置 + * @param onRespawn 重生且需要返回死亡点时触发的回调 + */ + public void tick(DigConfig config, Runnable onRespawn) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) { + return; + } + boolean dead = client.player.isDead(); + if (dead && !this.wasDead) { + // 刚死亡:记录死亡坐标(死亡界面时玩家仍停在该格) + this.deathPos = client.player.getBlockPos(); + this.pendingReturn = config.respawnReturn; + this.wasDead = true; + return; + } + if (this.wasDead && !dead) { + // 刚重生 + this.wasDead = false; + if (this.pendingReturn && this.deathPos != null) { + this.pendingReturn = false; + if (onRespawn != null) { + onRespawn.run(); + } + } + } + if (!dead) { + this.wasDead = false; + } + } + + /** @return 最近一次死亡的坐标,或 null */ + public BlockPos lastDeathPos() { + return this.deathPos; + } + + /** 清除死亡记录。 */ + public void clear() { + this.deathPos = null; + this.pendingReturn = false; + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/service/KillAura.java b/src/main/java/autodig/client/service/KillAura.java new file mode 100644 index 0000000..815d48f --- /dev/null +++ b/src/main/java/autodig/client/service/KillAura.java @@ -0,0 +1,95 @@ +package autodig.client.service; + +import autodig.client.config.DigConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.Monster; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; + +/** + * 杀戮光环服务。 + * + *

在开关开启时,自动攻击以玩家为中心、指定半径内最近的敌对生物 + * ({@link Monster})。攻击前会转向目标方向;"安全模式"开启时, + * 玩家生命过低则停止攻击、保命优先。

+ */ +public final class KillAura { + + /** 攻击节流:每 N tick 才允许挥一刀,避免疯狂连点 */ + private int attackCooldownTicks; + + /** + * 每帧调用。执行杀戮光环逻辑(开关关闭时直接返回)。 + * + * @param config 挖矿配置 + */ + public void tick(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null || client.interactionManager == null) { + return; + } + if (!config.killAura) { + return; + } + net.minecraft.entity.player.PlayerEntity player = client.player; + if (player.isDead()) { + return; + } + // 安全模式:血线过低不出击 + if (config.killAuraSafe) { + double health = player.getHealth() / Math.max(player.getMaxHealth(), 1.0) * 100.0; + if (health < config.healthFleeThreshold) { + return; + } + } + // 挥刀节流 + if (this.attackCooldownTicks > 0) { + this.attackCooldownTicks--; + return; + } + // 攻击冷却未转好则等待 + if (player.getAttackCooldownProgress(0.5f) < 1.0f) { + return; + } + Entity target = this.findNearestHostile(client, player, config.killRange); + if (target == null) { + return; + } + // 转向目标 + this.faceEntity(player, target); + client.interactionManager.attackEntity(player, target); + this.attackCooldownTicks = 4; // 约 4 tick 一刀 + } + + /** 在玩家周围范围内找最近的敌对生物。 */ + private Entity findNearestHostile(MinecraftClient client, LivingEntity player, double range) { + range = Math.max(2.0, range); + Box box = player.getBoundingBox().expand(range); + Entity best = null; + double bestDist = Double.MAX_VALUE; + for (Entity e : client.world.getEntitiesByClass(Entity.class, box, e -> e instanceof Monster && e.isAlive())) { + double d = e.squaredDistanceTo(player); + if (d < bestDist) { + bestDist = d; + best = e; + } + } + return best; + } + + /** 让玩家面朝向实体。 */ + private void faceEntity(LivingEntity player, Entity target) { + Vec3d p = player.getEyePos(); + Vec3d t = target.getEyePos(); + double dx = t.x - p.x; + double dy = t.y - p.y; + double dz = t.z - p.z; + double d = Math.sqrt(dx * dx + dz * dz); + float yaw = (float) (Math.toDegrees(Math.atan2(dz, dx)) - 90.0); + float pitch = (float) (-Math.toDegrees(Math.atan2(dy, Math.max(d, 0.0001)))); + player.setYaw(yaw); + player.setPitch(pitch); + } +} \ No newline at end of file diff --git a/src/main/resources/assets/autodig/lang/en_us.json b/src/main/resources/assets/autodig/lang/en_us.json new file mode 100644 index 0000000..c39eac4 --- /dev/null +++ b/src/main/resources/assets/autodig/lang/en_us.json @@ -0,0 +1,4 @@ +{ + "key.autodig.open": "Open AutoDig GUI", + "category.autodig": "AutoDig" +} \ No newline at end of file diff --git a/src/main/resources/assets/autodig/lang/zh_cn.json b/src/main/resources/assets/autodig/lang/zh_cn.json new file mode 100644 index 0000000..2209b2a --- /dev/null +++ b/src/main/resources/assets/autodig/lang/zh_cn.json @@ -0,0 +1,4 @@ +{ + "key.autodig.open": "打开 AutoDig 界面", + "category.autodig": "AutoDig" +} \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..033b37d --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "id": "autodig", + "version": "${version}", + "name": "AutoDig", + "description": "基于 Baritone API 的自动挖矿模组:可选钻石/远古残骸,支持鱼骨挖矿、自动切换工具、自动进食、杀戮光环、保命优先、优先绕开岩浆、进出传送门与死亡重生回点。", + "authors": [], + "contact": {}, + "license": "All-Rights-Reserved", + "environment": "client", + "entrypoints": { + "client": [ + "autodig.client.AutoDigModClient" + ], + "main": [ + "autodig.AutoDigMod" + ] + }, + "depends": { + "fabricloader": ">=0.16.14", + "fabric-api": "*", + "minecraft": "1.21.4" + }, + "jars": [ + { + "file": "META-INF/jars/baritone-api-fabric-1.13.1.jar" + } + ] +} \ No newline at end of file