package autodig.client.controller; import autodig.client.bridge.BaritoneBridge; import autodig.client.config.DigConfig; import autodig.client.config.MineTask; import autodig.client.miner.NetherMiner; import autodig.client.miner.OverworldMiner; import autodig.client.service.AutoEat; import autodig.client.service.AutoGear; import autodig.client.service.AutoTrash; import autodig.client.service.DeathRecorder; import autodig.client.service.KillAura; import autodig.client.service.SessionStats; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemStack; 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, 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 AutoGear autoGear = new AutoGear(); private final AutoTrash autoTrash = new AutoTrash(); private final DeathRecorder deathRecorder = new DeathRecorder(); private final SessionStats stats = new SessionStats(); 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 long lastWarn; /** 上一次"没有合适工具"提示的时间戳(节流) */ private long lastToolWarn; /** 返回死亡点的截止时间(超时后直接续任务,避免不可达时死循环) */ private long returnDeadline; /** 装备耐久保护触发前所处的状态,用于恢复后回到该状态 */ private State preGearState; /** 每帧驱动整个自动挖矿流程。 */ 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.autoTrash.tick(config); this.autoGear.tick(); // 死亡处理:记录死亡次数,暂停任务,重生后按需回到死亡坐标 boolean wasDead = player.isDead(); this.deathRecorder.tick(config, this::onRespawn); if (player.isDead()) { if (!wasDead) { this.stats.addDeath(); // 刚死亡,计一次 } this.bridge.cancelAll(); return; } // 装备耐久保护(仅在挖矿任务进行中检查) if (config.task != MineTask.NONE) { boolean gearLow = this.isGearLowDurability(config, player); if (this.state == State.GEAR_PROTECTION) { // 保护中持续尝试自动换上备用装备,换上后自动恢复任务 if (config.autoGearSwap && gearLow) { this.autoGear.handleLowGear(config, player); gearLow = this.isGearLowDurability(config, player); } if (!gearLow) { this.setState(this.preGearState != null ? this.preGearState : State.IDLE, "装备耐久已恢复,继续挖矿"); this.preGearState = null; } else { // 仍在装备保护中,等待换装生效或玩家手动处理 return; } } else if (gearLow) { // 先尝试自动换上背包里的备用装备,能换就不打断挖矿 if (!(config.autoGearSwap && this.autoGear.handleLowGear(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 (config.task == MineTask.NONE) { if (this.state != State.IDLE) { this.bridge.cancelAll(); this.stats.stop(); this.setState(State.IDLE, "任务已停止,进入空闲状态"); } return; } // 保命优先:仅在挖矿任务进行中生效,血线过低时向上撤离 if (config.survivalFirst && this.healthPercent(player) < config.healthFleeThreshold) { if (this.state != State.FLEE) { this.bridge.cancelAll(); } this.setState(State.FLEE, "血线过低,保命撤离中"); this.fleeTick(config); return; } // 工具适用性保障:确保手持工具能采掘目标矿物(如远古残骸必须钻石镐) boolean canMineOres = true; if (config.autoTool) { canMineOres = this.autoGear.ensureToolForOres(config, player); if (!canMineOres) { this.warnToolInsufficient(config); } } this.drive(config, player, canMineOres); } /** 背包中没有能采掘目标矿物的工具时,每 5 秒提示一次。 */ private void warnToolInsufficient(DigConfig config) { long now = System.currentTimeMillis(); if (now - this.lastToolWarn > 5000) { this.lastToolWarn = now; String ores = config.task == MineTask.NETHER ? config.netherOres : config.overworldOres; this.sendChat("背包中没有能采掘 " + ores + " 的工具(如需要钻石镐),矿物绕道挖掘已暂停;放入合适工具后自动恢复"); } } private void drive(DigConfig config, PlayerEntity player, boolean canMineOres) { switch (this.state) { case IDLE -> { // Baritone 未就绪时无法挖矿,节流提示并停留在空闲,避免静默无响应 if (!this.bridge.isAvailable()) { this.warnBaritoneUnavailable(); break; } if (config.task == MineTask.OVERWORLD) { this.stats.start(); this.overworld.init(config); this.setState(State.OVERWORLD, "开始主世界鱼骨挖矿(Y=" + config.overworldLevel + ",巷道 " + config.tunnelLength + " 格)"); } else if (config.task == MineTask.NETHER) { this.stats.start(); this.nether.reset(); this.setState(State.NETHER, "开始下界挖矿(目标 " + config.netherTargetCount + " 个)"); } } case OVERWORLD -> { if (this.overworld.tick(config, canMineOres)) { this.bridge.cancelAll(); if (this.overworld.isFull()) { // 背包已满无法继续,直接结束任务,避免空转循环 this.finish(config, "背包已满,任务已暂停(清空背包后重新开始)"); } else { this.pathToSurface(player.getBlockPos()); this.setState(State.CLIMB, "鱼骨巷道已挖完,正在返回地表"); } } } case CLIMB -> { if (this.nearSurface(player)) { this.finish(config, "主世界挖矿完成,已返回地表"); } else { // 周期性重发寻路指令,避免 Baritone 中途失败后卡死在该状态 this.pathToSurface(player.getBlockPos()); } } 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() 中单独处理(血线恢复后自动续任务) } case GEAR_PROTECTION -> { // 装备耐久保护在 tick() 中单独处理 } } } /** 重生后先回到最近的死亡坐标,再续上原任务。 */ private void onRespawn() { BlockPos deathPos = this.deathRecorder.lastDeathPos(); if (deathPos != null) { this.bridge.cancelAll(); this.resumeTarget = deathPos; this.returnDeadline = System.currentTimeMillis() + 120_000; this.setState(State.RETURN_TO_DEATH, "已重生,正在返回死亡点继续任务"); } } private void returnToDeathTick(PlayerEntity player, DigConfig config) { 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; } 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) { 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.setState(State.IDLE, "任务已结束"); } } /** 保命撤离:把玩家送到脚下地表,血线恢复后继续原任务。 */ private void fleeTick(DigConfig config) { MinecraftClient client = MinecraftClient.getInstance(); PlayerEntity player = client.player; if (player == null) { return; } this.pathToSurface(player.getBlockPos()); if (this.healthPercent(player) >= config.healthFleeThreshold + 20) { this.resumeTask(config); } } /** * 结束任务:取消 Baritone、把配置任务重置为空闲(避免 IDLE 分支再次自动 * 重开造成刷屏循环),并提示结果。 */ private void finish(DigConfig config, String message) { this.bridge.cancelAll(); config.task = MineTask.NONE; this.stats.stop(); 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), false); } } /** Baritone 未就绪时每 3 秒提示一次,避免刷屏,同时避免静默无响应。 */ private void warnBaritoneUnavailable() { long now = System.currentTimeMillis(); if (now - this.lastWarn > 3000) { this.lastWarn = now; this.sendChat("内置 Baritone 未能就绪,无法开始挖矿(请尝试重启游戏;若你另外安装了 Baritone,请先移除,避免与本模组内置版本冲突)"); } } 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; } /** * 检查装备耐久是否过低。 * 护甲:任意一件护甲耐久百分比低于阈值 * 工具:手持工具(镐/斧/锹/锄/剑)耐久百分比低于阈值 */ 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 (AutoGear.isTool(mainHand) && 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; } /** @return 会话统计(供 HUD/GUI 展示)。 */ public SessionStats stats() { return this.stats; } /** @return 主世界鱼骨巷道当前推进到的步数(HUD 进度显示用)。 */ public int overworldStep() { return this.overworld.currentStep(); } /** @return 主世界鱼骨巷道总步数(HUD 进度显示用)。 */ public int overworldTotal() { return this.overworld.totalSteps(); } /** @return 下界模式本次新挖到的目标矿物数量(HUD 进度显示用)。 */ public int netherMinedCount() { return this.nether.getMinedCount(); } /** 供 GUI/调试获取当前状态文本(中文描述)。 */ public String statusText() { return switch (this.state) { case IDLE -> "空闲"; case OVERWORLD -> "主世界鱼骨挖矿中"; case CLIMB -> "返回地表"; case NETHER -> "下界挖矿中"; case FLEE -> "保命撤离中"; case RETURN_TO_DEATH -> "返回死亡点"; case GEAR_PROTECTION -> "装备保护中"; }; } }