package autodig.client.miner; import autodig.client.bridge.BaritoneBridge; import autodig.client.config.DigConfig; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemStack; import net.minecraft.registry.Registries; import net.minecraft.registry.RegistryKey; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Direction; import net.minecraft.world.World; /** * 下界自动挖矿流转。 * *

流程:主世界 -> 走进最近传送门 -> 进入下界 -> 挖可配置矿物 -> * (挖够数量/背包满/血量低/装备耐久低)-> 找下界传送门走回主世界。 * 死亡重生的返回逻辑由控制器统一处理,本类每次 tick 依据当前所在维度 * 自适应地执行对应阶段。

* *

挖矿方式:与主世界相同采用巷道扫描式——在配置的 Y 层 * (远古残骸富集层,默认 15)沿玩家朝向挖直巷,逐点推进;巷道壁两侧 * 暴露的目标矿物由周期性扫描绕道挖掉。这样不依赖 Baritone 的矿物 * 远距离扫描,即使服务器反作弊隐藏矿物方块,主动挖巷道也能把路径 * 上的矿物挖出来。一条巷道挖完仍未挖够目标数量时,方向旋转 90° 转移 * 到新区域继续挖,避免漫无目的地乱走。

* *

下界无死亡不掉落,比主世界更"惜命":血量低或装备耐久低时主动触发返程。 * 背包满采用去抖判定(连续 1 秒都满才算真满),给自动丢垃圾服务留出腾位时间。

*/ public final class NetherMiner { private final BaritoneBridge bridge; private enum Stage { /** 在下界从传送门前往主世界(或已在主世界即完成) */ GO_COMMIT, /** 在前一个维度前往传送门 */ GO_PORTAL, /** 挖矿 */ MINE, /** 返回主世界 */ RETURN_PORTAL } /** 背包满去抖阈值(tick):连续这么久都满才认定为真满 */ private static final int FULL_DEBOUNCE_TICKS = 20; /** 矿物扫描间隔(毫秒) */ private static final long ORE_SCAN_INTERVAL_MS = 1000; /** 矿物扫描半径(方块) */ private static final int ORE_SCAN_RADIUS = 6; /** 单次绕道超时(tick):超过则放弃该矿点回到巷道计划 */ private static final int DETOUR_TIMEOUT_TICKS = 400; /** 同一矿点放弃后的重试冷却(毫秒) */ private static final long ABANDON_COOLDOWN_MS = 30_000; /** 巷道目标点不可达超时(毫秒):超过此时间仍未接近,放弃该点跳到下一个 */ private static final long WAYPOINT_STUCK_MS = 20_000; /** 转移寻路超时(毫秒):超过则放弃转移,原地重建巷道 */ private static final long RELOCATE_TIMEOUT_MS = 15_000; /** 转移距离(方块) */ private static final int RELOCATE_DISTANCE = 12; /** 常见矿物方块 id -> 掉落物品 id(挖掘产物与方块本身不同的情况) */ private static final Map ORE_TO_DROP = Map.ofEntries( Map.entry("diamond_ore", "diamond"), Map.entry("deepslate_diamond_ore", "diamond"), Map.entry("emerald_ore", "emerald"), Map.entry("deepslate_emerald_ore", "emerald"), Map.entry("lapis_ore", "lapis_lazuli"), Map.entry("deepslate_lapis_ore", "lapis_lazuli"), Map.entry("redstone_ore", "redstone"), Map.entry("deepslate_redstone_ore", "redstone"), Map.entry("coal_ore", "coal"), Map.entry("deepslate_coal_ore", "coal"), Map.entry("copper_ore", "raw_copper"), Map.entry("deepslate_copper_ore", "raw_copper"), Map.entry("iron_ore", "raw_iron"), Map.entry("deepslate_iron_ore", "raw_iron"), Map.entry("gold_ore", "raw_gold"), Map.entry("deepslate_gold_ore", "raw_gold"), Map.entry("nether_gold_ore", "gold_nugget"), Map.entry("nether_quartz_ore", "quartz")); // ancient_debris 挖掉落后就是 ancient_debris 物品,走默认映射 private Stage stage = Stage.GO_COMMIT; private boolean done; /** 结束原因是否为「背包已满」 */ private boolean full; /** 结束原因是否为「血量过低触发返程」 */ private boolean fled; /** 结束原因是否为「装备耐久过低触发返程」 */ private boolean gearBroken; /** 外部(控制器)要求的强制返程 */ private boolean forceReturn; /** 派发节流 */ private long lastIssue; /** 已挖到的目标矿物数量(按背包产物计数,扣除任务开始时的基线) */ private int minedCount; /** 任务开始时背包里已有的目标产物数量(避免老库存被误计为战果) */ private int baselineCount; private boolean baselineSet; /** 背包满连续持续的 tick 数(去抖) */ private int fullTicks; // ---- 巷道推进状态 ---- /** 下界巷道计划(直巷,y = config.netherLevel) */ private List plan = new ArrayList<>(); /** 当前推进到的计划步数 */ private int step; /** 当前巷道方向 */ private int dirX; private int dirZ; /** 上一次打印诊断日志的时间戳(节流) */ private long lastLog; /** 上一次成功下发的目标点(避免同一目标反复重发) */ private BlockPos lastIssuedTarget; /** 当前巷道目标点的最近进度时间戳与最佳距离(用于超时/不可达放弃) */ private long lastProgress; private double bestDist; // ---- 矿物绕道状态 ---- /** 绕道目标;非 null 时优先前往挖掘该矿物 */ private BlockPos detourTarget; /** 绕道已持续的 tick 数(超时放弃,避免卡死) */ private int detourTicks; /** 上一次扫描附近矿物的时间戳(节流) */ private long lastOreScan; /** 上次放弃的绕道点与时间(同一点短时间内不再重试) */ private BlockPos abandonedTarget; private long abandonedAt; // ---- 转移新区域状态 ---- /** 是否正在转移到新矿区 */ private boolean relocating; /** 转移目标点 */ private BlockPos relocateTarget; /** 转移开始时间戳 */ private long relocateStart; 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.forceReturn = false; this.minedCount = 0; this.baselineCount = 0; this.baselineSet = false; this.fullTicks = 0; this.plan.clear(); this.step = 0; this.lastIssuedTarget = null; this.lastProgress = System.currentTimeMillis(); this.bestDist = Double.MAX_VALUE; this.detourTarget = null; this.detourTicks = 0; this.abandonedTarget = null; this.abandonedAt = 0; this.relocating = false; this.relocateTarget = null; this.relocateStart = 0; } /** * 每帧推进下界挖矿流转。 * * @param config 挖矿配置 * @param canMineOres 手持工具是否足以挖目标矿物(false 时跳过矿物绕道,避免白挖不掉落) * @return 是否已全部完成(含回到主世界) */ public boolean tick(DigConfig config, boolean canMineOres) { 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 && this.stage != Stage.RETURN_PORTAL && !this.done && config.netherFleeHealthThreshold > 0 && this.healthPercent(player) < config.netherFleeHealthThreshold) { this.fled = true; this.bridge.cancelAll(); if (config.netherAutoReturn) { this.stage = Stage.RETURN_PORTAL; } else { this.done = true; } return this.done; } // 外部标记(装备耐久过低等):直接进入返程 if (this.forceReturn && inNether && this.stage != Stage.RETURN_PORTAL) { this.bridge.cancelAll(); this.stage = Stage.RETURN_PORTAL; } 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; return this.done; } // 记录任务开始时背包已有的产物数量作为基线 if (!this.baselineSet) { this.baselineCount = this.countProducts(config, player); this.baselineSet = true; } // 背包满去抖:连续一段时间都满才认定为真满(给自动丢垃圾留时间) if (this.inventoryFull(player)) { this.fullTicks++; } else { this.fullTicks = 0; } if (this.fullTicks >= FULL_DEBOUNCE_TICKS) { // 背包已满:期望回程则去传送门;不回程时无处可放,直接结束 this.full = true; if (config.netherAutoReturn) { this.stage = Stage.RETURN_PORTAL; } else { this.done = true; } } else if (this.minedEnough(config, player)) { // 已挖够目标数量 this.bridge.cancelAll(); if (config.netherAutoReturn) { this.stage = Stage.RETURN_PORTAL; } else { this.done = true; } } else { // 未挖够目标数量:巷道推进挖矿,绝不因 Baritone 空闲误判"任务完成" this.mineTick(config, player, canMineOres); } } case RETURN_PORTAL -> { if (inOverworld) { this.done = true; // 已回到主世界,全流程完成 } else { this.issue(() -> this.bridge.toNetherPortal()); } } } return this.done; } /** 判断是否已挖够目标矿物数量(按背包产物计数,扣除任务开始时的基线)。 */ private boolean minedEnough(DigConfig config, PlayerEntity player) { this.minedCount = this.countProducts(config, player) - this.baselineCount; if (config.netherTargetCount <= 0) { return false; // -1 或 0 表示无限挖 } return this.minedCount >= config.netherTargetCount; } /** * 巷道扫描式挖矿:建立/推进直巷计划,途中绕道挖掉暴露的目标矿物。 */ private void mineTick(DigConfig config, PlayerEntity player, boolean canMineOres) { MinecraftClient client = MinecraftClient.getInstance(); if (client.world == null) { return; } // 尚无巷道计划或刚转移完成:建立新计划 if (this.plan.isEmpty()) { this.initTunnel(config, player); } if (this.relocating) { double d = this.distanceSq(player, this.relocateTarget); if (d <= 4.0) { // 已到新矿区,重建巷道 this.relocating = false; this.relocateTarget = null; this.plan.clear(); } else if (System.currentTimeMillis() - this.relocateStart > RELOCATE_TIMEOUT_MS) { // 转移超时(目标点不可达):放弃转移,原地重建巷道 this.relocating = false; this.relocateTarget = null; this.bridge.cancelAll(); this.plan.clear(); } else if (!this.bridge.isActive()) { this.bridge.gotoBlock(this.relocateTarget); } return; } // 先处理矿物绕道(巷道壁暴露的目标矿物优先挖掉) if (this.handleOreDetour(config, player, canMineOres)) { return; } // 巷道走完:方向旋转 90° 转移到新区域继续挖 if (this.step >= this.plan.size()) { this.rotateAndRelocate(player); return; } 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] nether: 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++; this.lastIssuedTarget = null; this.lastProgress = now; this.bestDist = Double.MAX_VALUE; return; } // 进度跟踪:只要在接近目标就刷新"最近有进展"时间 if (dist < this.bestDist) { this.bestDist = dist; this.lastProgress = now; } // 长时间无进展(目标不可达)→ 放弃该点,跳到下一个 if (now - this.lastProgress > WAYPOINT_STUCK_MS) { System.out.println("[AutoDig] nether 目标长时间不可达,跳过 step=" + this.step + " target=" + target); this.bridge.cancelAll(); this.step++; this.lastIssuedTarget = null; this.lastProgress = now; this.bestDist = Double.MAX_VALUE; return; } // 下发寻路:换了新目标才立即下发;同一目标仅在 Baritone 空闲时低频重试 if (!target.equals(this.lastIssuedTarget)) { this.bridge.gotoBlock(target); this.lastIssuedTarget = target; this.lastIssue = now; this.lastProgress = now; this.bestDist = Double.MAX_VALUE; } else if (!this.bridge.isActive() && now - this.lastIssue > 2000) { this.bridge.gotoBlock(target); this.lastIssue = now; } } /** 建立一条直巷计划:以玩家水平坐标为起点,y = config.netherLevel,沿当前朝向推进。 */ private void initTunnel(DigConfig config, PlayerEntity player) { Direction facing = player.getHorizontalFacing(); int axis = facing.getAxis() == Direction.Axis.X ? 0 : 1; int sign = axis == 0 ? facing.getOffsetX() : facing.getOffsetZ(); this.dirX = axis == 0 ? sign : 0; this.dirZ = axis == 0 ? 0 : sign; BlockPos start = new BlockPos(player.getBlockPos().getX(), config.netherLevel, player.getBlockPos().getZ()); this.plan = new ArrayList<>(); for (int t = 0; t <= config.netherTunnelLength; t++) { this.plan.add(start.add(this.dirX * t, 0, this.dirZ * t)); } this.step = 0; this.lastIssuedTarget = null; this.lastProgress = System.currentTimeMillis(); this.bestDist = Double.MAX_VALUE; System.out.println("[AutoDig] nether 巷道建立: y=" + config.netherLevel + " 方向=(" + this.dirX + "," + this.dirZ + ") 长度=" + config.netherTunnelLength); } /** 巷道挖完仍未挖够:方向旋转 90°,转移到侧方新区域继续挖。 */ private void rotateAndRelocate(PlayerEntity player) { int nx = -this.dirZ; int nz = this.dirX; this.dirX = nx; this.dirZ = nz; this.relocateTarget = player.getBlockPos().add(this.dirX * RELOCATE_DISTANCE, 0, this.dirZ * RELOCATE_DISTANCE); this.relocateStart = System.currentTimeMillis(); this.relocating = true; this.plan.clear(); this.bridge.gotoBlock(this.relocateTarget); System.out.println("[AutoDig] nether 巷道挖完,转移到新区域: " + this.relocateTarget); } /** * 处理矿物绕道挖掘。 * * @return true 表示当前正在绕道挖矿(调用方应暂停巷道推进) */ private boolean handleOreDetour(DigConfig config, PlayerEntity player, boolean canMineOres) { MinecraftClient client = MinecraftClient.getInstance(); if (client.world == null) { return false; } long now = System.currentTimeMillis(); if (this.detourTarget != null) { BlockState state = client.world.getBlockState(this.detourTarget); if (!this.isTargetOre(config, state)) { // 已挖掉或目标失效:回到巷道计划 this.detourTarget = null; return false; } this.detourTicks++; if (this.detourTicks > DETOUR_TIMEOUT_TICKS) { // 太久没挖到(不可达等):放弃该点并冷却 this.abandonedTarget = this.detourTarget; this.abandonedAt = now; this.detourTarget = null; return false; } // 仅在 Baritone 空闲时低频重发,避免打断正在规划的路线 if (!this.bridge.isActive() && now - this.lastIssue > 1000) { this.bridge.gotoBlock(this.detourTarget); this.lastIssue = now; } return true; } // 节流扫描附近目标矿物 if (!canMineOres || now - this.lastOreScan < ORE_SCAN_INTERVAL_MS) { return false; } this.lastOreScan = now; BlockPos ore = this.findNearbyOre(config, client, player); if (ore != null) { this.detourTarget = ore; this.detourTicks = 0; this.bridge.gotoBlock(ore); this.lastIssue = now; System.out.println("[AutoDig] nether 发现目标矿物,绕道挖掘: " + ore); return true; } return false; } /** 在玩家周围扫描最近的目标矿物方块。 */ private BlockPos findNearbyOre(DigConfig config, MinecraftClient client, PlayerEntity player) { Set targets = this.targetOrePaths(config); if (targets.isEmpty()) { return null; } BlockPos best = null; double bestDist = Double.MAX_VALUE; for (BlockPos pos : BlockPos.iterateOutwards(player.getBlockPos(), ORE_SCAN_RADIUS, ORE_SCAN_RADIUS, ORE_SCAN_RADIUS)) { String path = Registries.BLOCK.getId(client.world.getBlockState(pos).getBlock()).getPath(); if (!targets.contains(path)) { continue; } // 最近放弃过的矿点在冷却期内不再尝试 if (pos.equals(this.abandonedTarget) && System.currentTimeMillis() - this.abandonedAt < ABANDON_COOLDOWN_MS) { continue; } double d = pos.getSquaredDistance(player.getBlockPos()); if (d < bestDist) { bestDist = d; best = pos.toImmutable(); } } return best; } /** 判断方块状态是否为配置中的目标矿物(按 path 匹配,兼容命名空间前缀)。 */ private boolean isTargetOre(DigConfig config, BlockState state) { if (state.isOf(Blocks.AIR)) { return false; } String path = Registries.BLOCK.getId(state.getBlock()).getPath(); return this.targetOrePaths(config).contains(path); } /** 目标矿物 path 集合(去命名空间,兼容 "minecraft:" 前缀写法)。 */ private Set targetOrePaths(DigConfig config) { Set set = new HashSet<>(); for (String ore : config.getNetherOreList()) { String path = ore.contains(":") ? ore.substring(ore.indexOf(':') + 1) : ore; if (!path.isBlank()) { set.add(path); } } return set; } private double distanceSq(PlayerEntity player, BlockPos target) { double dx = player.getX() - (target.getX() + 0.5); double dz = player.getZ() - (target.getZ() + 0.5); double dy = player.getY() - (target.getY() + 0.5); return dx * dx + dy * dy + dz * dz; } /** 统计背包中目标矿物产物的总数量(方块 id 映射到掉落物 id)。 */ private int countProducts(DigConfig config, PlayerEntity player) { List ores = config.getNetherOreList(); if (ores.isEmpty()) { return 0; } Set products = new HashSet<>(); for (String ore : ores) { products.add(ORE_TO_DROP.getOrDefault(ore, ore)); } int total = 0; for (int i = 0; i < 36; i++) { ItemStack stack = player.getInventory().main.get(i); if (stack.isEmpty()) { continue; } if (products.contains(itemId(stack))) { total += stack.getCount(); } } return total; } private static String itemId(ItemStack stack) { return Registries.ITEM.getId(stack.getItem()).toString(); } 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.forceReturn = true; this.bridge.cancelAll(); } /** @return 本次任务新挖到的目标矿物数量 */ public int getMinedCount() { return this.minedCount; } }