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 行删除
@@ -0,0 +1,140 @@
package autodig.client.service;
import autodig.client.config.DigConfig;
import autodig.client.config.MineTask;
import java.util.ArrayList;
import java.util.HashMap;
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.BlockItem;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.slot.SlotActionType;
/**
* 背包垃圾自动清理服务。
*
* <p>背包满时,把 {@code trashItems} 清单里的垃圾物品丢出背包(圆石、凝灰岩、
* 深板岩等挖掘副产物)。方块类垃圾保留 {@code trashReserve} 个供 Baritone
* 搭路/垫脚使用;若满背时额度之外没有别的垃圾可丢,则只丢一组腾出空间,
* 尽量保住搭路方块。丢弃通过 {@code clickSlot(THROW)} 走正规网络包,
* 单人/多人均可用。</p>
*/
public final class AutoTrash {
/** 每 tick 最多丢弃的物品组数,避免一次产生过多数据包 */
private static final int MAX_DROPS_PER_TICK = 4;
/** 每帧调用:背包满时自动丢弃垃圾物品。 */
public void tick(DigConfig config) {
MinecraftClient client = MinecraftClient.getInstance();
if (client.player == null || client.world == null || client.interactionManager == null) {
return;
}
if (!config.autoTrash || config.task == MineTask.NONE) {
return;
}
if (client.currentScreen != null) {
return; // 有界面(箱子/死亡界面等)打开时不操作背包
}
PlayerEntity player = client.player;
ScreenHandler handler = player.currentScreenHandler;
if (handler != player.playerScreenHandler) {
return;
}
if (!handler.getCursorStack().isEmpty()) {
return;
}
if (!this.inventoryFull(player)) {
return;
}
Set<String> trash = new HashSet<>(config.getTrashItemList());
if (trash.isEmpty()) {
return;
}
// 收集背包里的垃圾,按物品 id 分组(并记录是否为可放置的方块)
Map<String, List<ItemStack>> groups = new HashMap<>();
Map<String, Boolean> blockLike = new HashMap<>();
for (int i = 0; i < 36; i++) {
ItemStack s = player.getInventory().main.get(i);
if (s.isEmpty()) {
continue;
}
String id = this.itemId(s);
if (!trash.contains(id)) {
continue;
}
groups.computeIfAbsent(id, k -> new ArrayList<>()).add(s);
blockLike.putIfAbsent(id, s.getItem() instanceof BlockItem);
}
if (groups.isEmpty()) {
return;
}
// 决定丢弃哪些:方块类垃圾保留 trashReserve 个(搭路用),非方块全丢
List<ItemStack> drops = new ArrayList<>();
for (Map.Entry<String, List<ItemStack>> e : groups.entrySet()) {
int keep = Boolean.TRUE.equals(blockLike.get(e.getKey())) ? config.trashReserve : 0;
for (ItemStack s : e.getValue()) {
if (keep > 0) {
keep -= s.getCount();
continue; // 还在保留额度内
}
drops.add(s);
}
}
if (drops.isEmpty()) {
// 满背但垃圾全在保留额度内:只丢一组腾出空间,尽量保住搭路方块
for (List<ItemStack> list : groups.values()) {
if (!list.isEmpty()) {
drops.add(list.get(0));
break;
}
}
}
// 执行丢弃(每 tick 限量,剩余下 tick 继续)
int dropped = 0;
for (ItemStack s : drops) {
if (dropped >= MAX_DROPS_PER_TICK) {
break;
}
int slot = this.screenSlotOf(handler, s);
if (slot < 0) {
continue;
}
// THROW + 按钮 1 = 丢出整组
client.interactionManager.clickSlot(handler.syncId, slot, 1, SlotActionType.THROW, player);
dropped++;
}
}
private boolean inventoryFull(PlayerEntity player) {
for (int i = 0; i < 36; i++) {
if (player.getInventory().main.get(i).isEmpty()) {
return false;
}
}
return true;
}
/** 按对象引用在 ScreenHandler 里找槽位下标。 */
private int screenSlotOf(ScreenHandler handler, ItemStack stack) {
for (int i = 0; i < handler.slots.size(); i++) {
if (handler.getSlot(i).getStack() == stack) {
return i;
}
}
return -1;
}
private String itemId(ItemStack stack) {
return Registries.ITEM.getId(stack.getItem()).toString();
}
}