文件
AutoDig/src/main/java/autodig/client/controller/DigController.java
T
lyizai 248505deee feat: v2.2.2 无路时自动搭方块(可开关)
- Baritone allowPlace/allowParkourPlace 改为配置驱动(默认开)
- 新增 AutoScaffold 服务:快捷栏搭路方块不足时自动从背包补给
  (只进空位/只与更小的搭路方块堆交换,不碰工具食物)
- 搭路方块清单喂给 Baritone acceptableThrowawayItems(放置时从中挑选)
- AutoTrash 永不丢弃快捷栏物品,与搭路补给协同
- GUI 新增开关与搭路方块清单设置;版本升至 2.2.2
2026-08-31 13:37:59 +08:00

437 行
18 KiB
Java

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.AutoScaffold;
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 AutoScaffold autoScaffold = new AutoScaffold();
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.autoScaffold.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 -> "装备保护中";
};
}
}