feat: 自动换备用装备 + 背包满自动丢垃圾 (v2.1.0)

新增 AutoGear 服务:
- 护甲/工具耐久过低时自动从背包换上同类备用装备(工具按镐/斧/锹/锄/剑类别,
  护甲按头/胸/腿/脚部位匹配),选剩余耐久最高的候选
- 通过 clickSlot 三段点击交换(拿起旧装备->与背包新装备互换->就位),
  走正规网络包,客户端/服务端同步,单人/多人均可用
- 控制器改为换装优先:能换就不打断挖矿,无备用才暂停/下界返程
- 装备保护状态下持续尝试换装,换上后自动恢复任务

新增 AutoTrash 服务:
- 背包满时自动丢弃 trashItems 清单里的垃圾(默认:圆石/深板岩/凝灰岩/
  泥土/花岗岩/闪长岩/安山岩/下界岩/黑石/玄武岩,GUI 可改)
- 方块类垃圾保留 trashReserve 个(默认 64)供 Baritone 搭路垫脚;
  满背且无其他垃圾可丢时只丢一组腾位,尽量保住搭路方块
- clickSlot(THROW) 丢整组,每 tick 限 4 组防数据包风暴

配套修复:
- NetherMiner: 修复血量低时每帧 cancelAll 导致回程寻路反复被取消卡死的 bug;
  markGearBroken 现在真正触发 RETURN_PORTAL 阶段;产物计数改用注册表 id +
  矿物->掉落物映射 + 任务开始基线(老库存不再被误计为战果)
- 两个 Miner 的背包满判定加 20 tick 去抖,给自动丢垃圾留出腾位时间
- GUI 新增两个开关与垃圾清单编辑区,切换任务/开关时保留输入框未应用的编辑
- 装备低耐久检查的手持工具仅限真正的工具,不再误伤钓鱼竿等杂物
这个提交包含在:
lyizai
2026-08-30 13:12:26 +08:00
父节点 49c6297c41
当前提交 a297d2714d
共修改 10 个文件,包含 543 行新增和 64 行删除
@@ -2,9 +2,14 @@ package autodig.client.miner;
import autodig.client.bridge.BaritoneBridge;
import autodig.client.config.DigConfig;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.world.World;
@@ -16,7 +21,8 @@ import net.minecraft.world.World;
* 死亡重生的返回逻辑由控制器统一处理,本类每次 tick 依据当前所在维度
* 自适应地执行对应阶段。</p>
*
* <p>下界无死亡不掉落,比主世界更"惜命":血量低或装备耐久低时主动触发返程。</p>
* <p>下界无死亡不掉落,比主世界更"惜命":血量低或装备耐久低时主动触发返程。
* 背包满采用去抖判定(连续 1 秒都满才算真满),给自动丢垃圾服务留出腾位时间。</p>
*/
public final class NetherMiner {
@@ -33,6 +39,31 @@ public final class NetherMiner {
RETURN_PORTAL
}
/** 背包满去抖阈值(tick):连续这么久都满才认定为真满 */
private static final int FULL_DEBOUNCE_TICKS = 20;
/** 常见矿物方块 id -> 掉落物品 id(挖掘产物与方块本身不同的情况) */
private static final Map<String, String> 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;
/** 结束原因是否为「背包已满」 */
@@ -41,12 +72,19 @@ public final class NetherMiner {
private boolean fled;
/** 结束原因是否为「装备耐久过低触发返程」 */
private boolean gearBroken;
/** 外部(控制器)要求的强制返程 */
private boolean forceReturn;
/** 是否已派发过挖矿指令(用于区分「尚未开始」和「已经挖完」) */
private boolean miningStarted;
/** 派发节流 */
private long lastIssue;
/** 已挖到的目标矿物数量(通过扫描背包近似) */
/** 已挖到的目标矿物数量(按背包产物计数,扣除任务开始时的基线) */
private int minedCount;
/** 任务开始时背包里已有的目标产物数量(避免老库存被误计为战果) */
private int baselineCount;
private boolean baselineSet;
/** 背包满连续持续的 tick 数(去抖) */
private int fullTicks;
public NetherMiner(BaritoneBridge bridge) {
this.bridge = bridge;
@@ -59,8 +97,12 @@ public final class NetherMiner {
this.full = false;
this.fled = false;
this.gearBroken = false;
this.forceReturn = false;
this.miningStarted = false;
this.minedCount = 0;
this.baselineCount = 0;
this.baselineSet = false;
this.fullTicks = 0;
}
/**
@@ -79,13 +121,14 @@ public final class NetherMiner {
boolean inOverworld = dim == World.OVERWORLD;
boolean inNether = dim == World.NETHER;
// 下界惜命:血量过低时触发返程
if (inNether && config.netherFleeHealthThreshold > 0
// 下界惜命:血量过低时触发返程(只触发一次,避免反复取消回程寻路)
if (inNether && this.stage != Stage.RETURN_PORTAL && !this.done
&& config.netherFleeHealthThreshold > 0
&& this.healthPercent(player) < config.netherFleeHealthThreshold) {
this.fled = true;
this.miningStarted = false;
this.bridge.cancelAll();
if (config.netherAutoReturn) {
this.bridge.cancelAll();
this.stage = Stage.RETURN_PORTAL;
} else {
this.done = true;
@@ -93,6 +136,13 @@ public final class NetherMiner {
return this.done;
}
// 外部标记(装备耐久过低等):直接进入返程
if (this.forceReturn && inNether && this.stage != Stage.RETURN_PORTAL) {
this.miningStarted = false;
this.bridge.cancelAll();
this.stage = Stage.RETURN_PORTAL;
}
switch (this.stage) {
case GO_COMMIT -> {
// 选择去哪个维度
@@ -114,7 +164,20 @@ public final class NetherMiner {
// 意外回到主世界,重走流程
this.stage = Stage.GO_COMMIT;
this.miningStarted = false;
} else if (this.inventoryFull(player)) {
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;
this.miningStarted = false;
@@ -123,7 +186,7 @@ public final class NetherMiner {
} else {
this.done = true;
}
} else if (this.minedEnough(config)) {
} else if (this.minedEnough(config, player)) {
// 已挖够目标数量
this.miningStarted = false;
if (config.netherAutoReturn) {
@@ -132,7 +195,7 @@ public final class NetherMiner {
this.done = true;
}
} else if (this.miningStarted && !this.bridge.isActive()) {
// 挖矿进程已跑完(附近无矿可挖)
// 挖矿进程已跑完(挖够数量或附近无矿可挖)
this.miningStarted = false;
if (config.netherAutoReturn) {
this.stage = Stage.RETURN_PORTAL;
@@ -163,35 +226,40 @@ public final class NetherMiner {
return this.done;
}
/** 判断是否已挖够目标矿物数量 */
private boolean minedEnough(DigConfig config) {
/** 判断是否已挖够目标矿物数量(按背包产物计数,扣除任务开始时的基线)。 */
private boolean minedEnough(DigConfig config, PlayerEntity player) {
if (config.netherTargetCount <= 0) {
return false; // -1 或 0 表示无限挖
}
// 通过扫描背包中目标矿物数量近似判断
MinecraftClient client = MinecraftClient.getInstance();
if (client.player == null) {
return false;
this.minedCount = this.countProducts(config, player) - this.baselineCount;
return this.minedCount >= config.netherTargetCount;
}
/** 统计背包中目标矿物产物的总数量(方块 id 映射到掉落物 id)。 */
private int countProducts(DigConfig config, PlayerEntity player) {
List<String> ores = config.getNetherOreList();
if (ores.isEmpty()) {
return 0;
}
int count = 0;
var ores = config.getNetherOreList();
Set<String> 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++) {
var stack = client.player.getInventory().main.get(i);
ItemStack stack = 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;
}
if (products.contains(itemId(stack))) {
total += stack.getCount();
}
}
this.minedCount = count;
return count >= config.netherTargetCount;
return total;
}
private static String itemId(ItemStack stack) {
return Registries.ITEM.getId(stack.getItem()).toString();
}
private void issue(Runnable action) {
@@ -234,13 +302,14 @@ public final class NetherMiner {
return this.gearBroken;
}
/** 标记装备耐久过低,触发返程 */
/** 标记装备耐久过低,触发强制返程 */
public void markGearBroken() {
this.gearBroken = true;
this.forceReturn = true;
this.miningStarted = false;
}
/** @return 已挖到的目标矿物数量 */
/** @return 本次任务新挖到的目标矿物数量 */
public int getMinedCount() {
return this.minedCount;
}