diff --git a/build.gradle b/build.gradle index 10638f0..0d2d17e 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,9 @@ base { archivesName = project.archives_base_name } -// 用于把 Baritone 原样打包进模组 jar 的资源(不放默认依赖里,避免 Gradle 误解析) +// 独立配置,用于把完整的 Baritone 实现原样(不经过 Loom 重映射)复制进最终 jar。 +// 注意:libs 下的这个文件虽然命名为 *-api-*,但它实际上是 Baritone 的完整实现 +// (含混淆后的实现类、fabric.mod.json、Mixin 配置),并非只有接口。 configurations { nestedJar } @@ -24,10 +26,13 @@ dependencies { // Fabric API modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" - // Baritone API (local jar, compile only) + // Baritone 完整实现(本地 jar): + // - modImplementation 供编译期与开发环境(runClient)使用,Loom 会将其重映射到 yarn; + // - 生产 jar 中则通过下方 nestedJar 嵌入原始(intermediary 映射)文件, + // 由 Fabric Loader 在运行时自动作为嵌套模组加载,玩家无需另行安装 Baritone。 modImplementation files("libs/baritone-api-fabric-1.13.1.jar") - // 标记需要嵌套打包的 jar + // 标记需要原样嵌套打包的 jar(保持原始字节,不做重映射) nestedJar files("libs/baritone-api-fabric-1.13.1.jar") } @@ -39,8 +44,10 @@ processResources { expand "version": project.version } - // 将 Baritone 嵌套打包进 META-INF/jars,匹配 fabric.mod.json 的 jars 声明, - // 使玩家无需单独安装 baritone 依赖即可运行 + // 将完整的 Baritone 实现原样嵌套打包进 META-INF/jars, + // 与 fabric.mod.json 的 jars 声明对应。Fabric Loader 会在运行时把嵌套 jar + // 作为独立模组自动加载,因此玩家无需另行安装 Baritone。 + // 注意:不要对嵌套副本做重映射,Loader 要求生产环境的模组保持 intermediary 映射。 from(configurations.nestedJar) { into "META-INF/jars" } diff --git a/gradle.properties b/gradle.properties index ac80044..b765588 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8 loader_version=0.19.3 # Mod Properties -mod_version=1.0.0 +mod_version=2.0.0 maven_group=autodig archives_base_name=autodig diff --git a/src/main/java/autodig/client/AutoDigModClient.java b/src/main/java/autodig/client/AutoDigModClient.java index 6a87133..63eb3bc 100644 --- a/src/main/java/autodig/client/AutoDigModClient.java +++ b/src/main/java/autodig/client/AutoDigModClient.java @@ -4,6 +4,7 @@ import autodig.client.bridge.BaritoneBridge; import autodig.client.command.AutoDigCommands; import autodig.client.config.ConfigStore; import autodig.client.config.DigConfig; +import autodig.client.config.MineTask; import autodig.client.controller.DigController; import autodig.client.gui.AutoDigGui; import com.mojang.brigadier.CommandDispatcher; @@ -35,6 +36,8 @@ public final class AutoDigModClient implements ClientModInitializer { private DigConfig config; private boolean openGuiRequested; private KeyBinding guiKey; + /** 上一次同步的任务,用于检测 controller 内部改动(如任务完成)并落盘 */ + private MineTask lastTask; private static final int KEY_R = 82; private static final int KEY_LEFT_CTRL = 341; @@ -44,15 +47,23 @@ public final class AutoDigModClient implements ClientModInitializer { public void onInitializeClient() { instance = this; this.config = this.configStore.load(); + this.lastTask = this.config.task; 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); + // controller 可能因任务完成而把 task 置空,这里落盘,避免下次进游戏自动挖矿 + if (this.config.task != this.lastTask) { + this.lastTask = this.config.task; + this.saveConfig(); + } while (this.guiKey.wasPressed()) { if (this.isCtrlDown(client)) { this.openGui(); + } else { + this.sendToast("AutoDig: 请按住 Ctrl 再按此快捷键打开界面"); } } if (this.openGuiRequested) { diff --git a/src/main/java/autodig/client/bridge/BaritoneBridge.java b/src/main/java/autodig/client/bridge/BaritoneBridge.java index f072696..bbc123b 100644 --- a/src/main/java/autodig/client/bridge/BaritoneBridge.java +++ b/src/main/java/autodig/client/bridge/BaritoneBridge.java @@ -6,10 +6,8 @@ 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; /** @@ -17,34 +15,41 @@ import net.minecraft.util.math.BlockPos; * *

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

+ * 取消任务等。(杀戮光环由 KillAura 近战实现,不经 Baritone。)

* *

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

*/ public final class BaritoneBridge { + /** 上一次打印 gotoBlock 诊断日志的时间戳,用于节流 */ + private long lastGotoLog; + /** * 把模组配置同步到 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(); + try { + 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(); + } catch (Throwable ignored) { + // Baritone 未就绪时静默失败,避免抛出异常中断每帧逻辑 + } } /** @@ -65,9 +70,28 @@ public final class BaritoneBridge { public void gotoBlock(BlockPos target) { IBaritone baritone = this.primary(); if (baritone == null || target == null) { + System.out.println("[AutoDig] gotoBlock 失败: baritone=" + (baritone != null) + " target=" + target); return; } - baritone.getCustomGoalProcess().setGoalAndPath(new baritone.api.pathing.goals.GoalBlock(target)); + try { + boolean ctxOk = baritone.getPlayerContext() != null; + boolean worldOk = ctxOk && baritone.getPlayerContext().world() != null; + boolean playerOk = ctxOk && baritone.getPlayerContext().player() != null; + boolean wasPathing = baritone.getPathingBehavior().isPathing(); + baritone.getCustomGoalProcess().setGoalAndPath(new baritone.api.pathing.goals.GoalBlock(target)); + boolean nowPathing = baritone.getPathingBehavior().isPathing(); + boolean goalActive = baritone.getCustomGoalProcess().isActive(); + long now = System.currentTimeMillis(); + if (now - this.lastGotoLog > 5000) { + this.lastGotoLog = now; + System.out.println("[AutoDig] gotoBlock target=" + target + + " ctx=" + ctxOk + " world=" + worldOk + " player=" + playerOk + + " wasPathing=" + wasPathing + " nowPathing=" + nowPathing + + " goalActive=" + goalActive); + } + } catch (Throwable t) { + System.out.println("[AutoDig] gotoBlock 异常: " + t); + } } /** 前往离玩家最近的传送门方块(用于进出下界)。 */ @@ -79,21 +103,6 @@ public final class BaritoneBridge { 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(); diff --git a/src/main/java/autodig/client/command/AutoDigCommands.java b/src/main/java/autodig/client/command/AutoDigCommands.java index 3e077a1..da49f18 100644 --- a/src/main/java/autodig/client/command/AutoDigCommands.java +++ b/src/main/java/autodig/client/command/AutoDigCommands.java @@ -34,7 +34,7 @@ public final class AutoDigCommands { String name = StringArgumentType.getString(ctx, "task"); MineTask task = parseTask(name); if (task == null) { - ctx.getSource().sendFeedback(Text.literal("未知任务: " + name + "(可用 diamond / debris)")); + ctx.getSource().sendFeedback(Text.literal("未知任务: " + name + "(可用 overworld / nether / none)")); return 0; } AutoDigModClient mod = AutoDigModClient.instance(); @@ -56,8 +56,8 @@ public final class AutoDigCommands { private static MineTask parseTask(String name) { return switch (name.toLowerCase()) { - case "diamond", "钻石" -> MineTask.DIAMOND; - case "debris", "ancient_debris", "远古残骸" -> MineTask.ANCIENT_DEBRIS; + case "overworld", "主世界", "ow" -> MineTask.OVERWORLD; + case "nether", "下界" -> MineTask.NETHER; case "none", "stop", "空闲" -> MineTask.NONE; default -> null; }; diff --git a/src/main/java/autodig/client/config/ConfigStore.java b/src/main/java/autodig/client/config/ConfigStore.java index a5c539b..076547c 100644 --- a/src/main/java/autodig/client/config/ConfigStore.java +++ b/src/main/java/autodig/client/config/ConfigStore.java @@ -10,6 +10,8 @@ import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.attribute.FileAttribute; import net.fabricmc.loader.api.FabricLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 配置文件读写器。 @@ -19,6 +21,8 @@ import net.fabricmc.loader.api.FabricLoader; */ public final class ConfigStore { + private static final Logger LOGGER = LoggerFactory.getLogger("AutoDig"); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); public DigConfig load() { @@ -32,8 +36,9 @@ public final class ConfigStore { if (loaded == null) { return new DigConfig(); } - return loaded; + return sanitize(loaded); } catch (Exception e) { + LOGGER.warn("AutoDig: 读取配置失败,已回退到默认配置: {}", e.getMessage()); return new DigConfig(); } } @@ -45,12 +50,44 @@ public final class ConfigStore { Files.createDirectories(path.getParent(), new FileAttribute[0]); } Files.writeString(path, GSON.toJson(config), StandardCharsets.UTF_8, new OpenOption[0]); - } catch (IOException iOException) { - // 保存失败时静默忽略 + } catch (IOException e) { + LOGGER.warn("AutoDig: 保存配置失败: {}", e.getMessage()); } } private Path path() { return FabricLoader.getInstance().getConfigDir().resolve("autodig.json"); } + + /** + * 把磁盘上的配置夹回合法范围,避免手改 JSON 或将来字段重命名导致运行时异常。 + * Gson 对缺失字段会保留字段初始化值,对显式 null 会写入 null,所以这里两种都要处理。 + */ + private static DigConfig sanitize(DigConfig c) { + if (c.task == null) { + c.task = MineTask.NONE; + } + c.eatHungerThreshold = clamp(c.eatHungerThreshold, 1, 20); + c.healthFleeThreshold = clamp(c.healthFleeThreshold, 5, 100); + c.armorDurabilityThreshold = clamp(c.armorDurabilityThreshold, 1, 100); + c.toolDurabilityThreshold = clamp(c.toolDurabilityThreshold, 1, 100); + c.netherFleeHealthThreshold = clamp(c.netherFleeHealthThreshold, 0, 100); + if (Double.isNaN(c.killRange) || c.killRange < 2.0 || c.killRange > 8.0) { + c.killRange = 4.0; + } + c.overworldLevel = clamp(c.overworldLevel, -64, 320); + c.tunnelLength = clamp(c.tunnelLength, 4, 512); + c.netherTargetCount = clamp(c.netherTargetCount, -1, 512); + if (c.overworldOres == null || c.overworldOres.isBlank()) { + c.overworldOres = "diamond_ore,deepslate_diamond_ore"; + } + if (c.netherOres == null || c.netherOres.isBlank()) { + c.netherOres = "ancient_debris"; + } + return c; + } + + private static int clamp(int v, int min, int max) { + return Math.max(min, Math.min(max, v)); + } } \ 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 index ffd4f69..b418a23 100644 --- a/src/main/java/autodig/client/config/DigConfig.java +++ b/src/main/java/autodig/client/config/DigConfig.java @@ -1,12 +1,15 @@ package autodig.client.config; +import java.util.Arrays; +import java.util.List; + /** * 挖矿任务的用户可配置项(可序列化为 JSON,由 GUI 直接读写)。 */ public class DigConfig { - /** 当前选中的挖矿任务 */ - public MineTask task = MineTask.DIAMOND; + /** 当前选中的挖矿任务(默认空闲,需通过 GUI/命令手动开始,避免进游戏自动挖矿) */ + public MineTask task = MineTask.NONE; // ---- 通用能力开关 ---- /** 自动切换最合适的工具(钻石稿/铁镐等) */ @@ -22,6 +25,16 @@ public class DigConfig { /** 生命值(半心百分比,0-100)低于该值时触发保命撤离 */ public int healthFleeThreshold = 40; + // ---- 装备保护 ---- + /** 护甲耐久保护:任意护甲件耐久低于该百分比时暂停挖矿(主世界/下界均生效) */ + public boolean armorDurabilityProtection = true; + /** 护甲耐久阈值(百分比 0-100),低于此值停止挖矿 */ + public int armorDurabilityThreshold = 20; + /** 工具耐久保护:手持工具耐久低于该百分比时暂停挖矿 */ + public boolean toolDurabilityProtection = true; + /** 工具耐久阈值(百分比 0-100),低于此值停止挖矿 */ + public int toolDurabilityThreshold = 15; + // ---- 岩浆/障碍 ---- /** 优先绕开岩浆池(给岩浆方块额外寻路代价) */ public boolean avoidLava = true; @@ -33,25 +46,51 @@ public class DigConfig { public boolean killAuraSafe = true; /** 杀戮光环攻击范围(方块) */ public double killRange = 4.0; + /** 下界模式下启用杀戮光环协同(下界怪物多,建议开启) */ + public boolean killAuraInNether = true; - // ---- 钻石(主世界)参数 ---- + // ---- 主世界模式参数 ---- /** 鱼骨巷道瞄准的 Y 层(钻石一般在 -60 附近) */ - public int diamondLevel = -58; + public int overworldLevel = -58; /** 鱼骨主干巷道长度(方块) */ public int tunnelLength = 60; + /** 主世界要挖的矿物方块 ID 列表(逗号分隔,如 "diamond_ore,deepslate_diamond_ore") */ + public String overworldOres = "diamond_ore,deepslate_diamond_ore"; - // ---- 远古残骸参数 ---- - /** 需要挖多少个远古残骸后视为完成 */ - public int debrisCount = 4; - /** 挖够数量后自动回到主世界 */ - public boolean debrisReturnHome = true; - /** 是否开启下界的自动猎杀/杀戮光环协同 */ - public boolean killAuraInNether = true; + // ---- 下界模式参数 ---- + /** 下界要挖的矿物方块 ID 列表(逗号分隔,如 "ancient_debris,nether_gold_ore") */ + public String netherOres = "ancient_debris"; + /** 挖够数量后自动回主世界(-1 表示无限挖直到背包满/血量低/装备耐久低) */ + public int netherTargetCount = 4; + /** 下界模式:挖够/背包满/血量低/装备耐久低时自动回主世界 */ + public boolean netherAutoReturn = true; + /** 下界模式:血量低于该百分比时触发返程(0-100,0 关闭) */ + public int netherFleeHealthThreshold = 30; // ---- 死亡重生 ---- /** 死亡重生后自动回到死亡地点继续任务 */ public boolean respawnReturn = true; + /** 获取主世界矿物列表 */ + public List getOverworldOreList() { + return parseOreList(this.overworldOres); + } + + /** 获取下界矿物列表 */ + public List getNetherOreList() { + return parseOreList(this.netherOres); + } + + private static List parseOreList(String str) { + if (str == null || str.isBlank()) { + return List.of(); + } + return Arrays.stream(str.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } + public DigConfig copy() { DigConfig c = new DigConfig(); c.task = this.task; @@ -60,15 +99,22 @@ public class DigConfig { c.eatHungerThreshold = this.eatHungerThreshold; c.survivalFirst = this.survivalFirst; c.healthFleeThreshold = this.healthFleeThreshold; + c.armorDurabilityProtection = this.armorDurabilityProtection; + c.armorDurabilityThreshold = this.armorDurabilityThreshold; + c.toolDurabilityProtection = this.toolDurabilityProtection; + c.toolDurabilityThreshold = this.toolDurabilityThreshold; 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.overworldLevel = this.overworldLevel; + c.tunnelLength = this.tunnelLength; + c.overworldOres = this.overworldOres; + c.netherOres = this.netherOres; + c.netherTargetCount = this.netherTargetCount; + c.netherAutoReturn = this.netherAutoReturn; + c.netherFleeHealthThreshold = this.netherFleeHealthThreshold; c.respawnReturn = this.respawnReturn; return c; } diff --git a/src/main/java/autodig/client/config/MineTask.java b/src/main/java/autodig/client/config/MineTask.java index 6fe31ea..679d4b0 100644 --- a/src/main/java/autodig/client/config/MineTask.java +++ b/src/main/java/autodig/client/config/MineTask.java @@ -3,16 +3,17 @@ package autodig.client.config; /** * 挖矿任务类型。 * - *

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

+ *

主世界模式:在主世界按鱼骨巷道挖掘可配置的矿物(默认钻石/深层钻石),有死亡不掉落保护。 + * 下界模式:自动进传送门进入下界挖掘可配置的矿物(默认远古残骸),无死亡不掉落,需惜命, + * 提供返程功能(挖够/背包满/血量低/装备耐久低时自动回主世界),并珍惜装备耐久。

*/ public enum MineTask { /** 空闲:不执行任何挖矿任务 */ NONE("空闲"), - /** 主世界挖钻石(含深层钻石矿石) */ - DIAMOND("钻石"), - /** 下界挖远古残骸(自动进出传送门、死亡重生返回坐标) */ - ANCIENT_DEBRIS("远古残骸"); + /** 主世界模式:鱼骨巷道挖掘可配置矿物 */ + OVERWORLD("主世界"), + /** 下界模式:进传送门挖掘可配置矿物,支持自动返程与惜命/护装备逻辑 */ + NETHER("下界"); private final String label; diff --git a/src/main/java/autodig/client/controller/DigController.java b/src/main/java/autodig/client/controller/DigController.java index d8e4f7e..9eeabb5 100644 --- a/src/main/java/autodig/client/controller/DigController.java +++ b/src/main/java/autodig/client/controller/DigController.java @@ -3,39 +3,45 @@ 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.miner.NetherMiner; +import autodig.client.miner.OverworldMiner; 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.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; import net.minecraft.world.Heightmap; /** - * 挖矿总控制器:把配置、Baritone 桥接、自动进食、杀戮光环、死亡记录和各任务 - * 专用挖矿器串起来,驱动一个简单的状态机。 + * 挖矿总控制器:把配置、Baritone 桥接、自动进食、杀戮光环、死亡记录、装备耐久 + * 保护和各任务专用挖矿器串起来,驱动一个简单的状态机。 */ public final class DigController { /** 内部状态机 */ private enum State { - IDLE, FISHBONE, CLIMB, DEBRIS, FLEE, RETURN_TO_DEATH + IDLE, OVERWORLD, CLIMB, NETHER, FLEE, RETURN_TO_DEATH, GEAR_PROTECTION } 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 final OverworldMiner overworld = new OverworldMiner(this.bridge); + private final NetherMiner nether = new NetherMiner(this.bridge); private State state = State.IDLE; private BlockPos resumeTarget; private long lastIssue; - private boolean wasDead; + private long lastWarn; + /** 返回死亡点的截止时间(超时后直接续任务,避免不可达时死循环) */ + private long returnDeadline; + /** 装备耐久保护触发前所处的状态,用于恢复后回到该状态 */ + private State preGearState; /** 每帧驱动整个自动挖矿流程。 */ public void tick(DigConfig config) { @@ -53,14 +59,45 @@ public final class DigController { // 死亡处理:死亡瞬间暂停任务,重生后按需回到死亡坐标 this.deathRecorder.tick(config, this::onRespawn); if (player.isDead()) { - this.wasDead = true; this.bridge.cancelAll(); return; } - this.wasDead = false; - // 保命优先:血线过低时向上撤离(不冒进挖矿) + // 装备耐久保护(仅在挖矿任务进行中检查) + if (config.task != MineTask.NONE && this.state != State.GEAR_PROTECTION) { + if (this.isGearLowDurability(config, player)) { + this.preGearState = this.state; + this.bridge.cancelAll(); + // 下界模式:装备耐久低时直接返程回主世界 + if (config.task == MineTask.NETHER && config.netherAutoReturn) { + this.nether.markGearBroken(); + this.setState(State.NETHER, "装备耐久过低,下界返程中"); + } else { + this.setState(State.GEAR_PROTECTION, "装备耐久过低,暂停挖矿保装备"); + } + return; + } + } + + // 装备耐久保护恢复 + if (this.state == State.GEAR_PROTECTION) { + if (!this.isGearLowDurability(config, player)) { + this.setState(this.preGearState != null ? this.preGearState : State.IDLE, + "装备耐久已恢复,继续挖矿"); + this.preGearState = null; + } else { + // 仍在装备保护中,等待玩家换装备 + return; + } + } + + // 保命优先:血线过低时向上撤离(不冒进挖矿)。 + // cancelAll 只在刚进入 FLEE 时执行一次,避免每帧取消刚派发的寻路 if (config.survivalFirst && this.healthPercent(player) < config.healthFleeThreshold) { + if (this.state != State.FLEE) { + this.bridge.cancelAll(); + } + this.setState(State.FLEE, "血线过低,保命撤离中"); this.fleeTick(config); return; } @@ -69,7 +106,7 @@ public final class DigController { if (config.task == MineTask.NONE) { if (this.state != State.IDLE) { this.bridge.cancelAll(); - this.state = State.IDLE; + this.setState(State.IDLE, "任务已停止,进入空闲状态"); } return; } @@ -80,36 +117,59 @@ public final class DigController { 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; + // Baritone 未就绪时无法挖矿,节流提示并停留在空闲,避免静默无响应 + if (!this.bridge.isAvailable()) { + this.warnBaritoneUnavailable(); + break; + } + if (config.task == MineTask.OVERWORLD) { + this.overworld.init(config); + this.setState(State.OVERWORLD, + "开始主世界鱼骨挖矿(Y=" + config.overworldLevel + ",巷道 " + config.tunnelLength + " 格)"); + } else if (config.task == MineTask.NETHER) { + this.nether.reset(); + this.setState(State.NETHER, "开始下界挖矿(目标 " + config.netherTargetCount + " 个)"); } } - case FISHBONE -> { - player.sendMessage(Text.literal("AutoDig: 主世界鱼骨挖矿进行中"), true); - if (this.fishbone.tick(config)) { + case OVERWORLD -> { + if (this.overworld.tick(config)) { this.bridge.cancelAll(); - this.pathToSurface(player.getBlockPos()); - this.state = State.CLIMB; + if (this.overworld.isFull()) { + // 背包已满无法继续,直接结束任务,避免空转循环 + this.finish(config, "背包已满,任务已暂停(清空背包后重新开始)"); + } else { + this.pathToSurface(player.getBlockPos()); + this.setState(State.CLIMB, "鱼骨巷道已挖完,正在返回地表"); + } } } case CLIMB -> { if (this.nearSurface(player)) { - this.finish("鱼骨挖矿完成,已返回地表"); + this.finish(config, "主世界挖矿完成,已返回地表"); + } else { + // 周期性重发寻路指令,避免 Baritone 中途失败后卡死在该状态 + this.pathToSurface(player.getBlockPos()); } } - case DEBRIS -> { - player.sendMessage(Text.literal("AutoDig: 下界远古残骸挖矿进行中"), true); - if (this.debris.tick(config)) { - this.finish("远古残骸任务完成,已回到主世界"); + case NETHER -> { + if (this.nether.tick(config)) { + if (this.nether.isFull()) { + this.finish(config, "背包已满,下界任务已暂停(清空背包后重新开始)"); + } else if (this.nether.isFled()) { + this.finish(config, "血量过低触发返程,下界任务已完成(已回主世界)"); + } else if (this.nether.isGearBroken()) { + this.finish(config, "装备耐久过低触发返程,下界任务已完成(已回主世界,请修理装备)"); + } else { + this.finish(config, "下界任务完成,已回到主世界"); + } } } case RETURN_TO_DEATH -> this.returnToDeathTick(player, config); case FLEE -> { - // FLEE 在 tick() 中单独处理,正常情况下不会进到这里 + // FLEE 在 tick() 中单独处理(血线恢复后自动续任务) + } + case GEAR_PROTECTION -> { + // 装备耐久保护在 tick() 中单独处理 } } } @@ -120,12 +180,19 @@ public final class DigController { if (deathPos != null) { this.bridge.cancelAll(); this.resumeTarget = deathPos; - this.state = State.RETURN_TO_DEATH; + this.returnDeadline = System.currentTimeMillis() + 120_000; + this.setState(State.RETURN_TO_DEATH, "已重生,正在返回死亡点继续任务"); } } private void returnToDeathTick(PlayerEntity player, DigConfig config) { - if (this.resumeTarget == null) { + MinecraftClient client = MinecraftClient.getInstance(); + // 维度不一致(如下界死亡、主世界床上重生)或超时则放弃返回,直接续任务 + boolean dimOk = client.world != null + && client.world.getRegistryKey() == this.deathRecorder.lastDeathDim(); + if (this.resumeTarget == null || !dimOk + || System.currentTimeMillis() > this.returnDeadline) { + this.resumeTarget = null; this.resumeTask(config); return; } @@ -138,22 +205,16 @@ public final class DigController { } } - /** 按当前任务与所在维度恢复挖矿。 */ + /** 按当前任务恢复挖矿(尽量复用已有巷道计划)。 */ 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; - } + if (config.task == MineTask.OVERWORLD) { + this.overworld.continueOrInit(config); + this.setState(State.OVERWORLD, "继续主世界鱼骨挖矿"); + } else if (config.task == MineTask.NETHER) { + this.nether.reset(); + this.setState(State.NETHER, "继续下界挖矿"); } else { - this.state = State.IDLE; + this.setState(State.IDLE, "任务已结束"); } } @@ -164,20 +225,52 @@ public final class DigController { 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) { + /** + * 结束任务:取消 Baritone、把配置任务重置为空闲(避免 IDLE 分支再次自动 + * 重开造成刷屏循环),并提示结果。 + */ + private void finish(DigConfig config, String message) { this.bridge.cancelAll(); + config.task = MineTask.NONE; + this.setState(State.IDLE, message); + } + + /** + * 切换状态机状态,并在状态真正发生变化时向玩家发一条普通聊天提示 + * (非字幕 overlay)。仅变化时提示一次,避免每帧刷屏。 + * + * @param next 目标状态 + * @param message 提示内容;为 null 或空串时不提示 + */ + private void setState(State next, String message) { + boolean changed = this.state != next; + this.state = next; + if (changed && message != null && !message.isEmpty()) { + this.sendChat(message); + } + } + + /** 向玩家发送一条普通聊天提示(非字幕 overlay)。 */ + private void sendChat(String message) { MinecraftClient client = MinecraftClient.getInstance(); if (client.player != null) { - client.player.sendMessage(Text.literal("AutoDig: " + message), true); + client.player.sendMessage(Text.literal("AutoDig: " + message), false); + } + } + + /** Baritone 未就绪时每 3 秒提示一次,避免刷屏,同时避免静默无响应。 */ + private void warnBaritoneUnavailable() { + long now = System.currentTimeMillis(); + if (now - this.lastWarn > 3000) { + this.lastWarn = now; + this.sendChat("内置 Baritone 未能就绪,无法开始挖矿(请尝试重启游戏;若你另外安装了 Baritone,请先移除,避免与本模组内置版本冲突)"); } - this.state = State.IDLE; } private double healthPercent(PlayerEntity player) { @@ -223,13 +316,56 @@ public final class DigController { return Math.abs(player.getY() - (top + 1)) < 4.0; } + /** + * 检查装备耐久是否过低。 + * 护甲:任意一件护甲耐久百分比低于阈值 + * 工具:手持工具耐久百分比低于阈值 + */ + private boolean isGearLowDurability(DigConfig config, PlayerEntity player) { + // 护甲耐久检查 + if (config.armorDurabilityProtection) { + for (ItemStack armor : player.getInventory().armor) { + if (armor.isEmpty() || !armor.isDamageable()) { + continue; + } + int maxDmg = armor.getMaxDamage(); + int remaining = maxDmg - armor.getDamage(); + double percent = (double) remaining / maxDmg * 100.0; + if (percent < config.armorDurabilityThreshold) { + return true; + } + } + } + // 手持工具耐久检查 + if (config.toolDurabilityProtection) { + ItemStack mainHand = player.getMainHandStack(); + if (!mainHand.isEmpty() && mainHand.isDamageable()) { + int maxDmg = mainHand.getMaxDamage(); + int remaining = maxDmg - mainHand.getDamage(); + double percent = (double) remaining / maxDmg * 100.0; + if (percent < config.toolDurabilityThreshold) { + return true; + } + } + } + return false; + } + /** @return Baritone 桥接(供其他模块复用)。 */ public BaritoneBridge bridge() { return this.bridge; } - /** 供 GUI/调试获取当前状态文本。 */ + /** 供 GUI/调试获取当前状态文本(中文描述)。 */ public String statusText() { - return this.state.name(); + return switch (this.state) { + case IDLE -> "空闲"; + case OVERWORLD -> "主世界鱼骨挖矿中"; + case CLIMB -> "返回地表"; + case NETHER -> "下界挖矿中"; + case FLEE -> "保命撤离中"; + case RETURN_TO_DEATH -> "返回死亡点"; + case GEAR_PROTECTION -> "装备保护中"; + }; } } \ 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 index bfbcff8..c754ccd 100644 --- a/src/main/java/autodig/client/gui/AutoDigGui.java +++ b/src/main/java/autodig/client/gui/AutoDigGui.java @@ -14,27 +14,39 @@ import net.minecraft.text.Text; /** * AutoDig 游戏内 GUI。 * - *

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

+ *

用于选择挖矿模式(主世界 / 下界),并快捷设置:保命优先、优先绕开岩浆、 + * 自动切换工具、自动进食、杀戮光环(含下界协同)、重生后回死亡点、挖完回主世界, + * 装备耐久保护、矿物选择,以及鱼骨层数 / 巷道长度 / 目标数量等参数。 + * 界面中的改动在"开始/停止"或常规关闭(含 ESC)时同步到 mod 全局配置并落盘; + * "仅关闭"则丢弃未应用的改动。

*/ public final class AutoDigGui extends Screen { private final AutoDigModClient mod; private final DigConfig config; private final DigController controller; + /** 关闭界面时是否把改动写回 mod 配置("仅关闭"会置 false) */ + private boolean applyOnClose = true; // 动态数值按钮(点击 +/- 后刷新显示) private ButtonWidget levelBtn; private ButtonWidget tunnelBtn; - private ButtonWidget debrisBtn; + private ButtonWidget countBtn; + private ButtonWidget armorDurBtn; + private ButtonWidget toolDurBtn; + private ButtonWidget fleeHBtn; + // 矿物输入框状态 + private String overworldOresDraft; + private String netherOresDraft; + private int oreInputFocused = -1; // 0 = overworld, 1 = nether public AutoDigGui(AutoDigModClient mod) { super(Text.literal("AutoDig")); this.mod = mod; this.config = mod.config().copy(); this.controller = mod.controller(); + this.overworldOresDraft = this.config.overworldOres; + this.netherOresDraft = this.config.netherOres; } @Override @@ -45,8 +57,8 @@ public final class AutoDigGui extends Screen { int y = 34; // ===== 任务选择 ===== this.taskButton("空闲", MineTask.NONE, x, y); - this.taskButton("钻石(鱼骨)", MineTask.DIAMOND, x + 110, y); - this.taskButton("远古残骸", MineTask.ANCIENT_DEBRIS, x + 220, y); + this.taskButton("主世界", MineTask.OVERWORLD, x + 110, y); + this.taskButton("下界", MineTask.NETHER, x + 220, y); // ===== 开关设置 ===== y += 30; @@ -59,29 +71,102 @@ public final class AutoDigGui extends Screen { 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, y, () -> this.config.netherAutoReturn, v -> this.config.netherAutoReturn = v); this.check("避难时绝杀不冒进", colX + 150, y, () -> this.config.killAuraSafe, v -> this.config.killAuraSafe = v); + y += 24; + this.check("下界协同杀戮光环", colX, y, () -> this.config.killAuraInNether, v -> this.config.killAuraInNether = v); + this.check("护甲耐久保护", colX + 150, y, () -> this.config.armorDurabilityProtection, v -> this.config.armorDurabilityProtection = v); + y += 24; + this.check("工具耐久保护", colX, y, () -> this.config.toolDurabilityProtection, v -> this.config.toolDurabilityProtection = v); // ===== 数值参数 ===== y += 32; - this.levelBtn = this.numberRow("钻石层(Y)", colX, y, () -> this.config.diamondLevel, v -> this.config.diamondLevel = v); + this.levelBtn = this.numberRow("挖掘层(Y)", colX, y, () -> this.config.overworldLevel, v -> this.config.overworldLevel = v, -64, 320); y += 24; - this.tunnelBtn = this.numberRow("巷道长度", colX, y, () -> this.config.tunnelLength, v -> this.config.tunnelLength = v); + this.tunnelBtn = this.numberRow("巷道长度", colX, y, () -> this.config.tunnelLength, v -> this.config.tunnelLength = v, 4, 512); y += 24; - this.debrisBtn = this.numberRow("残骸数量", colX, y, () -> this.config.debrisCount, v -> this.config.debrisCount = v); + this.countBtn = this.numberRow("下界目标数量", colX, y, () -> this.config.netherTargetCount, v -> this.config.netherTargetCount = v, -1, 512); + y += 24; + this.armorDurBtn = this.numberRow("护甲耐久阈值%", colX, y, () -> this.config.armorDurabilityThreshold, v -> this.config.armorDurabilityThreshold = v, 1, 100); + y += 24; + this.toolDurBtn = this.numberRow("工具耐久阈值%", colX, y, () -> this.config.toolDurabilityThreshold, v -> this.config.toolDurabilityThreshold = v, 1, 100); + y += 24; + this.fleeHBtn = this.numberRow("下界返程血量%", colX, y, () -> this.config.netherFleeHealthThreshold, v -> this.config.netherFleeHealthThreshold = v, 0, 100); + + // ===== 矿物选择 ===== + y += 32; + this.addDrawableChild(ButtonWidget.builder(Text.literal("主世界矿物: " + this.overworldOresDraft), b -> { + // 循环切换预设矿物组合 + this.overworldOresDraft = nextOverworldOres(this.overworldOresDraft); + this.refreshOreButtons(); + }).dimensions(colX, y, 300, 20).build()); + y += 24; + this.addDrawableChild(ButtonWidget.builder(Text.literal("下界矿物: " + this.netherOresDraft), b -> { + // 循环切换预设矿物组合 + this.netherOresDraft = nextNetherOres(this.netherOresDraft); + this.refreshOreButtons(); + }).dimensions(colX, y, 300, 20).build()); // ===== 底部操作按钮 ===== - y += 40; + y += 32; 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); + ButtonWidget saveClose = this.textButton("仅关闭", b -> this.closeOnly(), x + 242, y, 80); this.addDrawableChild(start); this.addDrawableChild(stop); this.addDrawableChild(saveClose); } + /** 主世界矿物预设循环 */ + private static String[] OVERWORLD_ORE_PRESETS = { + "diamond_ore,deepslate_diamond_ore", + "diamond_ore,deepslate_diamond_ore,iron_ore,deepslate_iron_ore,coal_ore,deepslate_coal_ore", + "diamond_ore,deepslate_diamond_ore,emerald_ore,deepslate_emerald_ore", + "gold_ore,deepslate_gold_ore", + "lapis_ore,deepslate_lapis_ore", + "redstone_ore,deepslate_redstone_ore", + "copper_ore,deepslate_copper_ore", + "iron_ore,deepslate_iron_ore", + "coal_ore,deepslate_coal_ore" + }; + + /** 下界矿物预设循环 */ + private static String[] NETHER_ORE_PRESETS = { + "ancient_debris", + "ancient_debris,nether_gold_ore", + "nether_gold_ore", + "nether_quartz_ore", + "ancient_debris,nether_gold_ore,nether_quartz_ore" + }; + + private String nextOverworldOres(String current) { + for (int i = 0; i < OVERWORLD_ORE_PRESETS.length; i++) { + if (OVERWORLD_ORE_PRESETS[i].equals(current)) { + return OVERWORLD_ORE_PRESETS[(i + 1) % OVERWORLD_ORE_PRESETS.length]; + } + } + return OVERWORLD_ORE_PRESETS[0]; + } + + private String nextNetherOres(String current) { + for (int i = 0; i < NETHER_ORE_PRESETS.length; i++) { + if (NETHER_ORE_PRESETS[i].equals(current)) { + return NETHER_ORE_PRESETS[(i + 1) % NETHER_ORE_PRESETS.length]; + } + } + return NETHER_ORE_PRESETS[0]; + } + + private void refreshOreButtons() { + // 按钮在 init() 中创建,文本更新需要重新 init + this.init(); + } + private ButtonWidget taskButton(String label, MineTask task, int x, int y) { - ButtonWidget b = this.textButton(label, button -> { + // 选中的任务用 [ ] 包裹,形成高亮提示 + boolean selected = this.config.task == task; + String text = selected ? "[" + label + "]" : label; + ButtonWidget b = this.textButton(text, button -> { this.config.task = task; this.init(); // 刷新选中高亮 }, x, y, 104); @@ -96,9 +181,9 @@ public final class AutoDigGui extends Screen { this.addDrawableChild(box); } - private ButtonWidget numberRow(String label, int x, int y, java.util.function.IntSupplier getter, Consumer setter) { + private ButtonWidget numberRow(String label, int x, int y, java.util.function.IntSupplier getter, Consumer setter, int min, int max) { this.addDrawableChild(ButtonWidget.builder(Text.literal("-"), b -> { - setter.accept(getter.getAsInt() - 1); + setter.accept(this.clamp(getter.getAsInt() - 1, min, max)); this.refreshNumberButtons(); }).dimensions(x, y, 20, 20).build()); int valueX = x + 24; @@ -108,22 +193,39 @@ public final class AutoDigGui extends Screen { value.active = false; // 纯展示 this.addDrawableChild(value); this.addDrawableChild(ButtonWidget.builder(Text.literal("+"), b -> { - setter.accept(getter.getAsInt() + 1); + setter.accept(this.clamp(getter.getAsInt() + 1, min, max)); this.refreshNumberButtons(); }).dimensions(valueX + valueW + 4, y, 20, 20).build()); return value; } + private int clamp(int v, int min, int max) { + return Math.max(min, Math.min(max, v)); + } + private void refreshNumberButtons() { - this.levelBtn.setMessage(Text.literal("钻石层(Y) : " + this.config.diamondLevel)); + this.levelBtn.setMessage(Text.literal("挖掘层(Y) : " + this.config.overworldLevel)); this.tunnelBtn.setMessage(Text.literal("巷道长度 : " + this.config.tunnelLength)); - this.debrisBtn.setMessage(Text.literal("残骸数量 : " + this.config.debrisCount)); + this.countBtn.setMessage(Text.literal("下界目标数量 : " + this.config.netherTargetCount)); + this.armorDurBtn.setMessage(Text.literal("护甲耐久阈值% : " + this.config.armorDurabilityThreshold)); + this.toolDurBtn.setMessage(Text.literal("工具耐久阈值% : " + this.config.toolDurabilityThreshold)); + this.fleeHBtn.setMessage(Text.literal("下界返程血量% : " + this.config.netherFleeHealthThreshold)); } 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(); } + /** + * GUI 打开时不允许游戏暂停:否则客户端 tick 不执行,{@link DigController} + * 每帧驱动逻辑停摆,玩家会停在原地不挖矿。这里返回 false,让挖矿在 GUI + * 打开状态下也能持续进行。 + */ + @Override + public boolean shouldPause() { + return false; + } + @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { super.render(context, mouseX, mouseY, delta); @@ -143,32 +245,52 @@ public final class AutoDigGui extends Screen { target.autoTool = this.config.autoTool; target.killAura = this.config.killAura; target.killAuraSafe = this.config.killAuraSafe; + target.killAuraInNether = this.config.killAuraInNether; target.respawnReturn = this.config.respawnReturn; - target.debrisReturnHome = this.config.debrisReturnHome; - target.diamondLevel = this.config.diamondLevel; + target.netherAutoReturn = this.config.netherAutoReturn; + target.overworldLevel = this.config.overworldLevel; target.tunnelLength = this.config.tunnelLength; - target.debrisCount = this.config.debrisCount; + target.overworldOres = this.overworldOresDraft; + target.netherOres = this.netherOresDraft; + target.netherTargetCount = this.config.netherTargetCount; + target.netherFleeHealthThreshold = this.config.netherFleeHealthThreshold; + target.armorDurabilityProtection = this.config.armorDurabilityProtection; + target.armorDurabilityThreshold = this.config.armorDurabilityThreshold; + target.toolDurabilityProtection = this.config.toolDurabilityProtection; + target.toolDurabilityThreshold = this.config.toolDurabilityThreshold; this.mod.saveConfig(); } private void start() { + if (this.config.task == MineTask.NONE) { + this.mod.sendToast("AutoDig: 请先选择一个挖矿任务(主世界 / 下界)"); + return; + } this.applyToMod(); this.mod.sendToast("AutoDig: 已开始 " + this.config.task.label() + " 任务"); this.close(); } private void stop() { + // 先把本地副本的任务置空再写回,避免 close() 二次应用时把旧任务写回去 + this.config.task = MineTask.NONE; this.applyToMod(); - this.mod.config().task = MineTask.NONE; - this.mod.saveConfig(); this.controller.bridge().cancelAll(); this.mod.sendToast("AutoDig: 已停止"); this.close(); } + /** 仅关闭:丢弃未应用的改动。 */ + private void closeOnly() { + this.applyOnClose = false; + this.close(); + } + @Override public void close() { - this.applyToMod(); + if (this.applyOnClose) { + 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 deleted file mode 100644 index b9f3d3e..0000000 --- a/src/main/java/autodig/client/miner/DebrisHandler.java +++ /dev/null @@ -1,125 +0,0 @@ -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/NetherMiner.java b/src/main/java/autodig/client/miner/NetherMiner.java new file mode 100644 index 0000000..a3f0f5f --- /dev/null +++ b/src/main/java/autodig/client/miner/NetherMiner.java @@ -0,0 +1,247 @@ +package autodig.client.miner; + +import autodig.client.bridge.BaritoneBridge; +import autodig.client.config.DigConfig; +import java.util.List; +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 NetherMiner { + + private final BaritoneBridge bridge; + + private enum Stage { + /** 在下界从传送门前往主世界(或已在主世界即完成) */ + GO_COMMIT, + /** 在前一个维度前往传送门 */ + GO_PORTAL, + /** 挖矿 */ + MINE, + /** 返回主世界 */ + RETURN_PORTAL + } + + private Stage stage = Stage.GO_COMMIT; + private boolean done; + /** 结束原因是否为「背包已满」 */ + private boolean full; + /** 结束原因是否为「血量过低触发返程」 */ + private boolean fled; + /** 结束原因是否为「装备耐久过低触发返程」 */ + private boolean gearBroken; + /** 是否已派发过挖矿指令(用于区分「尚未开始」和「已经挖完」) */ + private boolean miningStarted; + /** 派发节流 */ + private long lastIssue; + /** 已挖到的目标矿物数量(通过扫描背包近似) */ + private int minedCount; + + public NetherMiner(BaritoneBridge bridge) { + this.bridge = bridge; + } + + /** 任务结束或需要切换任务时归还控制权。 */ + public void reset() { + this.stage = Stage.GO_COMMIT; + this.done = false; + this.full = false; + this.fled = false; + this.gearBroken = false; + this.miningStarted = false; + this.minedCount = 0; + } + + /** + * 每帧推进下界挖矿流转。 + * + * @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; + + // 下界惜命:血量过低时触发返程 + if (inNether && config.netherFleeHealthThreshold > 0 + && this.healthPercent(player) < config.netherFleeHealthThreshold) { + this.fled = true; + this.miningStarted = false; + this.bridge.cancelAll(); + if (config.netherAutoReturn) { + this.stage = Stage.RETURN_PORTAL; + } else { + this.done = true; + } + return this.done; + } + + 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; + this.miningStarted = false; + } else if (this.inventoryFull(player)) { + // 背包已满:期望回程则去传送门;不回程时无处可放,直接结束 + this.full = true; + this.miningStarted = false; + if (config.netherAutoReturn) { + this.stage = Stage.RETURN_PORTAL; + } else { + this.done = true; + } + } else if (this.minedEnough(config)) { + // 已挖够目标数量 + this.miningStarted = false; + if (config.netherAutoReturn) { + this.stage = Stage.RETURN_PORTAL; + } else { + this.done = true; + } + } else if (this.miningStarted && !this.bridge.isActive()) { + // 挖矿进程已跑完(附近无矿可挖) + this.miningStarted = false; + if (config.netherAutoReturn) { + this.stage = Stage.RETURN_PORTAL; + } else { + this.stage = Stage.GO_COMMIT; // 停在下界继续挖(不返回) + } + } else { + // 先派发挖矿指令;派发成功后 miningStarted 置位, + // 之后才能用 isActive()==false 判定「挖完了」 + this.issue(() -> { + List ores = config.getNetherOreList(); + if (!ores.isEmpty()) { + int count = config.netherTargetCount > 0 ? config.netherTargetCount : -1; + this.bridge.mineByName(count, ores.toArray(String[]::new)); + this.miningStarted = true; + } + }); + } + } + case RETURN_PORTAL -> { + if (inOverworld) { + this.done = true; // 已回到主世界,全流程完成 + } else { + this.issue(() -> this.bridge.toNetherPortal()); + } + } + } + return this.done; + } + + /** 判断是否已挖够目标矿物数量 */ + private boolean minedEnough(DigConfig config) { + if (config.netherTargetCount <= 0) { + return false; // -1 或 0 表示无限挖 + } + // 通过扫描背包中目标矿物数量近似判断 + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null) { + return false; + } + int count = 0; + var ores = config.getNetherOreList(); + for (int i = 0; i < 36; i++) { + var stack = client.player.getInventory().main.get(i); + if (stack.isEmpty()) { + continue; + } + // 通过物品的 toString 粗略匹配矿物名 + String itemName = stack.getItem().toString(); + for (String ore : ores) { + // ancient_debris -> ancient_debris, nether_gold_ore -> nether_gold_ore + if (itemName.contains(ore) || ore.contains(itemName)) { + count += stack.getCount(); + break; + } + } + } + this.minedCount = count; + return count >= config.netherTargetCount; + } + + 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; + } + + private double healthPercent(PlayerEntity player) { + return player.getHealth() / Math.max(player.getMaxHealth(), 1.0) * 100.0; + } + + public boolean isDone() { + return this.done; + } + + /** @return 结束原因是否为「背包已满」 */ + public boolean isFull() { + return this.full; + } + + /** @return 结束原因是否为「血量过低触发返程」 */ + public boolean isFled() { + return this.fled; + } + + /** @return 结束原因是否为「装备耐久过低触发返程」 */ + public boolean isGearBroken() { + return this.gearBroken; + } + + /** 标记装备耐久过低,触发返程 */ + public void markGearBroken() { + this.gearBroken = true; + this.miningStarted = false; + } + + /** @return 已挖到的目标矿物数量 */ + public int getMinedCount() { + return this.minedCount; + } +} \ No newline at end of file diff --git a/src/main/java/autodig/client/miner/FishboneMiner.java b/src/main/java/autodig/client/miner/OverworldMiner.java similarity index 57% rename from src/main/java/autodig/client/miner/FishboneMiner.java rename to src/main/java/autodig/client/miner/OverworldMiner.java index 1281887..43f7ad6 100644 --- a/src/main/java/autodig/client/miner/FishboneMiner.java +++ b/src/main/java/autodig/client/miner/OverworldMiner.java @@ -9,7 +9,7 @@ import net.minecraft.entity.player.PlayerEntity; import net.minecraft.util.math.BlockPos; /** - * 鱼骨巷道挖矿器(主世界钻石)。 + * 主世界鱼骨巷道挖矿器。 * *

在指定的 Y 层,沿玩家朝向挖一条"主干隧道",并在主干上每隔 * {@code branchGap} 格向两侧各开一条侧支巷道,形成鱼骨状分布。 @@ -19,7 +19,7 @@ import net.minecraft.util.math.BlockPos; * *

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

*/ -public final class FishboneMiner { +public final class OverworldMiner { private final BaritoneBridge bridge; @@ -29,10 +29,14 @@ public final class FishboneMiner { private int step; /** 是否已完成 */ private boolean done; + /** 完成原因是否为「背包已满」 */ + private boolean full; /** 上一次朝向目标派发寻路的时间戳,用于失败重试节流 */ private long lastIssue; + /** 上一次打印诊断日志的时间戳,用于节流 */ + private long lastLog; - public FishboneMiner(BaritoneBridge bridge) { + public OverworldMiner(BaritoneBridge bridge) { this.bridge = bridge; } @@ -47,17 +51,19 @@ public final class FishboneMiner { if (player == null) { return; } - this.yLevel = config.diamondLevel; + this.yLevel = config.overworldLevel; 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; + // 用玩家水平朝向(而非移动方向)确定挖矿方向,站着不动时也稳定 + net.minecraft.util.math.Direction facing = player.getHorizontalFacing(); + int axis = facing.getAxis() == net.minecraft.util.math.Direction.Axis.X ? 0 : 1; + int sign = axis == 0 ? facing.getOffsetX() : facing.getOffsetZ(); this.plan = this.buildPlan(this.start, this.yLevel, config.tunnelLength, branchLen, branchGap, axis, sign); this.step = 0; this.done = false; + this.full = false; } private List buildPlan(BlockPos base, int y, int trunkLen, int branchLen, int branchGap, int axis, int sign) { @@ -70,13 +76,15 @@ public final class FishboneMiner { for (int t = 0; t <= trunkLen; t++) { BlockPos trunk = base.add(dx * t, 0, dz * t); plan.add(trunk); - // 每 branchGap 格开一对侧支(去程+回程) + // 每 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)); + for (int side = -1; side <= 1; side += 2) { + for (int b = 1; b <= branchLen; b++) { + plan.add(trunk.add(sx * b * side, 0, sz * b * side)); + } + for (int b = branchLen - 1; b >= 1; b--) { + plan.add(trunk.add(sx * b * side, 0, sz * b * side)); + } } } } @@ -99,14 +107,24 @@ public final class FishboneMiner { } if (this.inventoryFull(player)) { this.done = true; + this.full = true; + System.out.println("[AutoDig] overworld 结束:背包已满"); return true; } if (this.step >= this.plan.size()) { this.done = true; + System.out.println("[AutoDig] overworld 结束:计划完成 step=" + this.step + " size=" + this.plan.size()); return true; } BlockPos target = this.plan.get(this.step); double dist = this.distanceSq(player, target); + long now = System.currentTimeMillis(); + if (now - this.lastLog > 5000) { + this.lastLog = now; + System.out.println("[AutoDig] overworld: step=" + this.step + "/" + this.plan.size() + + " player=" + player.getBlockPos() + " target=" + target + + " dist=" + String.format("%.1f", dist) + " active=" + this.bridge.isActive()); + } if (dist <= 2.0) { // 已到达当前目标,推进到下一个 this.step++; @@ -114,7 +132,6 @@ public final class FishboneMiner { } // 距离太长或 Baritone 没在干活时重新派发 if (dist > 4.0 || !this.bridge.isActive()) { - long now = System.currentTimeMillis(); if (now - this.lastIssue > 250) { this.bridge.gotoBlock(target); this.lastIssue = now; @@ -145,9 +162,47 @@ public final class FishboneMiner { return this.done; } - /** 重新允许继续(死亡重生/保命恢复后复用)。 */ - public void abort() { - this.done = true; - this.plan = new ArrayList<>(); + /** @return 完成原因是否为「背包已满」 */ + public boolean isFull() { + return this.full; + } + + /** + * 恢复挖掘(死亡重生/保命恢复后复用):已有未完成计划时复用原计划, + * 把指针回退到离玩家最近的目标点;否则按当前位置重新建立计划。 + * + * @param config 挖矿配置 + */ + public void continueOrInit(DigConfig config) { + MinecraftClient client = MinecraftClient.getInstance(); + PlayerEntity player = client.player; + if (this.plan.isEmpty() || this.done || player == null) { + this.init(config); + return; + } + int from = Math.max(0, this.step - 8); + int to = Math.min(this.plan.size() - 1, this.step + 8); + int best = this.step; + double bestDist = Double.MAX_VALUE; + for (int i = from; i <= to; i++) { + double d = this.distanceSq(player, this.plan.get(i)); + if (d < bestDist) { + bestDist = d; + best = i; + } + } + this.step = best; + this.done = false; + this.full = false; + } + + /** + * 派发矿脉挖掘指令(用于在鱼骨巷道推进过程中遇到目标矿物时挖掉)。 + * 由控制器在合适时机调用。 + */ + public void mineOres(DigConfig config) { + if (!config.getOverworldOreList().isEmpty()) { + this.bridge.mineByName(-1, config.getOverworldOreList().toArray(String[]::new)); + } } } \ 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 index 4b0235c..1a7011c 100644 --- a/src/main/java/autodig/client/service/AutoEat.java +++ b/src/main/java/autodig/client/service/AutoEat.java @@ -10,13 +10,14 @@ import net.minecraft.util.Hand; /** * 自动进食服务。 * - *

饥饿度低于阈值时,自动从快捷栏挑选一件食物并持续"使用手指"进食。 + *

饥饿度低于阈值时,自动从快捷栏/主背包挑选一件食物并持续"使用手指"进食。 + * 食物在主背包时,先将其与当前快捷栏槽位交换,下一帧再吃。 * 已在使用物品(正在咀嚼)时不会重复触发,避免打断进食节奏。

*/ public final class AutoEat { /** - * 每帧调用。饥饿不够时在快捷栏里找食物并吃掉。 + * 每帧调用。饥饿不够时在快捷栏/主背包里找食物并吃掉。 * * @param config 挖矿配置 */ @@ -35,12 +36,17 @@ public final class AutoEat { return; } PlayerInventory inv = player.getInventory(); - // 优先在快捷栏找食物 - for (int slot = 0; slot < 9; slot++) { + // 快捷栏 + 主背包共 36 格找食物 + for (int slot = 0; slot < 36; slot++) { ItemStack stack = inv.getStack(slot); if (stack.isEmpty() || stack.getUseAction() != UseAction.EAT) { continue; } + if (slot >= 9) { + // 主背包的食物先换到当前选中的快捷栏槽位,下一帧再吃 + inv.swapSlotWithHotbar(slot); + return; + } // 先切到该槽位,下一帧再触发进食 if (inv.selectedSlot != slot) { inv.selectedSlot = slot; diff --git a/src/main/java/autodig/client/service/DeathRecorder.java b/src/main/java/autodig/client/service/DeathRecorder.java index 7590e14..f12cfd3 100644 --- a/src/main/java/autodig/client/service/DeathRecorder.java +++ b/src/main/java/autodig/client/service/DeathRecorder.java @@ -2,12 +2,14 @@ package autodig.client.service; import autodig.client.config.DigConfig; import net.minecraft.client.MinecraftClient; +import net.minecraft.registry.RegistryKey; import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; /** * 死亡记录服务。 * - *

在玩家死亡的那一刻记录其坐标;重生(isDead 由真转假)后, + *

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

*/ public final class DeathRecorder { @@ -15,6 +17,8 @@ public final class DeathRecorder { private boolean wasDead; /** 最近一次死亡的坐标 */ private BlockPos deathPos; + /** 最近一次死亡时所在的维度 */ + private RegistryKey deathDim; private boolean pendingReturn; /** @@ -30,8 +34,9 @@ public final class DeathRecorder { } boolean dead = client.player.isDead(); if (dead && !this.wasDead) { - // 刚死亡:记录死亡坐标(死亡界面时玩家仍停在该格) + // 刚死亡:记录死亡坐标与维度(死亡界面时玩家仍停在该格) this.deathPos = client.player.getBlockPos(); + this.deathDim = client.world.getRegistryKey(); this.pendingReturn = config.respawnReturn; this.wasDead = true; return; @@ -56,9 +61,8 @@ public final class DeathRecorder { return this.deathPos; } - /** 清除死亡记录。 */ - public void clear() { - this.deathPos = null; - this.pendingReturn = false; + /** @return 最近一次死亡时所在的维度,或 null */ + public RegistryKey lastDeathDim() { + return this.deathDim; } } \ 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 index 815d48f..2adb73f 100644 --- a/src/main/java/autodig/client/service/KillAura.java +++ b/src/main/java/autodig/client/service/KillAura.java @@ -5,8 +5,11 @@ 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.Hand; import net.minecraft.util.math.Box; import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import java.util.concurrent.ThreadLocalRandom; /** * 杀戮光环服务。 @@ -33,6 +36,10 @@ public final class KillAura { if (!config.killAura) { return; } + // 下界协同开关:未开启时在下界不主动出击 + if (!config.killAuraInNether && client.world.getRegistryKey() == World.NETHER) { + return; + } net.minecraft.entity.player.PlayerEntity player = client.player; if (player.isDead()) { return; @@ -60,7 +67,9 @@ public final class KillAura { // 转向目标 this.faceEntity(player, target); client.interactionManager.attackEntity(player, target); - this.attackCooldownTicks = 4; // 约 4 tick 一刀 + player.swingHand(Hand.MAIN_HAND); // 视觉摆动 + // 3-6 tick 抖动,避免固定间隔被反作弊识别 + this.attackCooldownTicks = 3 + ThreadLocalRandom.current().nextInt(4); } /** 在玩家周围范围内找最近的敌对生物。 */ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 033b37d..a0145d6 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "autodig", "version": "${version}", "name": "AutoDig", - "description": "基于 Baritone API 的自动挖矿模组:可选钻石/远古残骸,支持鱼骨挖矿、自动切换工具、自动进食、杀戮光环、保命优先、优先绕开岩浆、进出传送门与死亡重生回点。", + "description": "自动挖矿模组(内置 Baritone 1.13.1,无需另行安装):主世界模式与下界模式,均可自定义目标矿物。主世界鱼骨巷道挖矿,下界自动进出传送门挖矿并支持返程。支持自动切换工具、自动进食、杀戮光环、保命优先、优先绕开岩浆、装备耐久保护、死亡重生回点。", "authors": [], "contact": {}, "license": "All-Rights-Reserved",