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.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.registry.Registries; import net.minecraft.util.Identifier; import net.minecraft.util.math.BlockPos; /** * 主世界鱼骨巷道挖矿器。 * *

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

* *

智能找矿:推进巷道的同时,周期性扫描玩家周围(默认 10 格)的目标矿物; * 发现后临时绕道前往该矿物将其挖掉,再回到巷道计划继续推进。 * 这样巷道壁旁暴露的钻石等目标矿不会漏挖。

* *

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

*/ public final class OverworldMiner { private final BaritoneBridge bridge; private BlockPos start; private int yLevel; private List plan = new ArrayList<>(); private int step; /** 是否已完成 */ private boolean done; /** 完成原因是否为「背包已满」 */ private boolean full; /** 上一次朝向目标派发寻路的时间戳,用于失败重试节流 */ private long lastIssue; /** 上一次打印诊断日志的时间戳,用于节流 */ private long lastLog; /** 背包满连续持续的 tick 数(去抖:给自动丢垃圾留出腾位时间) */ private int fullTicks; /** 矿物绕道目标;非 null 时优先前往挖掘该矿物 */ private BlockPos detourTarget; /** 绕道已持续的 tick 数(超时放弃,避免卡死) */ private int detourTicks; /** 上一次扫描附近矿物的时间戳(节流) */ private long lastOreScan; /** 上次放弃的绕道点与时间(同一点短时间内不再重试,避免卡在不可达矿上) */ private BlockPos abandonedTarget; private long abandonedAt; /** 背包满去抖阈值(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 = 10; /** 单次绕道超时(tick):超过则放弃该矿点回到巷道计划 */ private static final int DETOUR_TIMEOUT_TICKS = 400; /** 同一矿点放弃后的重试冷却(毫秒) */ private static final long ABANDON_COOLDOWN_MS = 30_000; public OverworldMiner(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.overworldLevel; BlockPos stop = player.getBlockPos(); this.start = new BlockPos(stop.getX(), this.yLevel, stop.getZ()); int branchLen = Math.max(1, config.branchLength); int branchGap = 8; // 用玩家水平朝向(而非移动方向)确定挖矿方向,站着不动时也稳定 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; this.fullTicks = 0; this.detourTarget = null; this.detourTicks = 0; this.abandonedTarget = null; this.abandonedAt = 0; } 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 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)); } } } } return plan; } /** * 每帧推进一次。背包已满或走完全程则返回已完成。 * * @param config 挖矿配置 * @param canMineOres 手持工具是否足以挖目标矿物(false 时跳过矿物绕道,避免白挖不掉落) */ public boolean tick(DigConfig config, boolean canMineOres) { if (this.done) { return true; } MinecraftClient client = MinecraftClient.getInstance(); PlayerEntity player = client.player; if (player == null) { return true; } // 背包满去抖:连续一段时间都满才认定为真满(给自动丢垃圾留出腾位时间) if (this.inventoryFull(player)) { this.fullTicks++; if (this.fullTicks >= FULL_DEBOUNCE_TICKS) { this.done = true; this.full = true; System.out.println("[AutoDig] overworld 结束:背包已满"); return true; } } else { this.fullTicks = 0; } if (this.step >= this.plan.size()) { this.done = true; System.out.println("[AutoDig] overworld 结束:计划完成 step=" + this.step + " size=" + this.plan.size()); return true; } // ---- 矿物绕道挖掘:优先把附近暴露的目标矿物挖掉 ---- if (this.handleOreDetour(config, client, player, canMineOres)) { return false; // 绕道期间不推进巷道 } 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++; return this.step >= this.plan.size(); } // 距离太长或 Baritone 没在干活时重新派发 if (dist > 4.0 || !this.bridge.isActive()) { if (now - this.lastIssue > 250) { this.bridge.gotoBlock(target); this.lastIssue = now; } } return false; } /** * 处理矿物绕道挖掘。 * * @return true 表示当前正在绕道挖矿(调用方应暂停巷道推进) */ private boolean handleOreDetour(DigConfig config, MinecraftClient client, PlayerEntity player, boolean canMineOres) { 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) { // 太久没挖到(不可达等):放弃该点并冷却 System.out.println("[AutoDig] overworld 绕道超时,放弃矿点: " + this.detourTarget); this.abandonedTarget = this.detourTarget; this.abandonedAt = now; this.detourTarget = null; return false; } if (now - this.lastIssue > 250) { 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] overworld 发现目标矿物,绕道挖掘: " + 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.getOverworldOreList()) { 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 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; } /** @return 当前推进到的计划步数(从 0 开始) */ public int currentStep() { return this.step; } /** @return 鱼骨计划总步数 */ public int totalSteps() { return this.plan.size(); } /** @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; this.fullTicks = 0; } }