From a297d2714df2929a3b939c3062449500cba844ee Mon Sep 17 00:00:00 2001
From: lyizai <3945107889@qq.com>
Date: Sun, 30 Aug 2026 13:12:26 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E6=8D=A2=E5=A4=87?=
=?UTF-8?q?=E7=94=A8=E8=A3=85=E5=A4=87=20+=20=E8=83=8C=E5=8C=85=E6=BB=A1?=
=?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=B8=A2=E5=9E=83=E5=9C=BE=20(v2.1.0)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增 AutoGear 服务:
- 护甲/工具耐久过低时自动从背包换上同类备用装备(工具按镐/斧/锹/锄/剑类别,
护甲按头/胸/腿/脚部位匹配),选剩余耐久最高的候选
- 通过 clickSlot 三段点击交换(拿起旧装备->与背包新装备互换->就位),
走正规网络包,客户端/服务端同步,单人/多人均可用
- 控制器改为换装优先:能换就不打断挖矿,无备用才暂停/下界返程
- 装备保护状态下持续尝试换装,换上后自动恢复任务
新增 AutoTrash 服务:
- 背包满时自动丢弃 trashItems 清单里的垃圾(默认:圆石/深板岩/凝灰岩/
泥土/花岗岩/闪长岩/安山岩/下界岩/黑石/玄武岩,GUI 可改)
- 方块类垃圾保留 trashReserve 个(默认 64)供 Baritone 搭路垫脚;
满背且无其他垃圾可丢时只丢一组腾位,尽量保住搭路方块
- clickSlot(THROW) 丢整组,每 tick 限 4 组防数据包风暴
配套修复:
- NetherMiner: 修复血量低时每帧 cancelAll 导致回程寻路反复被取消卡死的 bug;
markGearBroken 现在真正触发 RETURN_PORTAL 阶段;产物计数改用注册表 id +
矿物->掉落物映射 + 任务开始基线(老库存不再被误计为战果)
- 两个 Miner 的背包满判定加 20 tick 去抖,给自动丢垃圾留出腾位时间
- GUI 新增两个开关与垃圾清单编辑区,切换任务/开关时保留输入框未应用的编辑
- 装备低耐久检查的手持工具仅限真正的工具,不再误伤钓鱼竿等杂物
---
gradle.properties | 2 +-
.../autodig/client/config/ConfigStore.java | 4 +
.../java/autodig/client/config/DigConfig.java | 19 ++
.../client/controller/DigController.java | 71 ++++---
.../java/autodig/client/gui/AutoDigGui.java | 25 +++
.../autodig/client/miner/NetherMiner.java | 127 +++++++++---
.../autodig/client/miner/OverworldMiner.java | 21 +-
.../java/autodig/client/service/AutoGear.java | 196 ++++++++++++++++++
.../autodig/client/service/AutoTrash.java | 140 +++++++++++++
src/main/resources/fabric.mod.json | 2 +-
10 files changed, 543 insertions(+), 64 deletions(-)
create mode 100644 src/main/java/autodig/client/service/AutoGear.java
create mode 100644 src/main/java/autodig/client/service/AutoTrash.java
diff --git a/gradle.properties b/gradle.properties
index 01ad493..0f563a9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8
loader_version=0.19.3
# Mod Properties
-mod_version=2.0.1
+mod_version=2.1.0
maven_group=autodig
archives_base_name=autodig
diff --git a/src/main/java/autodig/client/config/ConfigStore.java b/src/main/java/autodig/client/config/ConfigStore.java
index 076547c..f5aa7cc 100644
--- a/src/main/java/autodig/client/config/ConfigStore.java
+++ b/src/main/java/autodig/client/config/ConfigStore.java
@@ -84,6 +84,10 @@ public final class ConfigStore {
if (c.netherOres == null || c.netherOres.isBlank()) {
c.netherOres = "ancient_debris";
}
+ c.trashReserve = clamp(c.trashReserve, 0, 2304);
+ if (c.trashItems == null || c.trashItems.isBlank()) {
+ c.trashItems = "cobblestone,cobbled_deepslate,deepslate,tuff,dirt,granite,diorite,andesite,netherrack,blackstone,basalt";
+ }
return c;
}
diff --git a/src/main/java/autodig/client/config/DigConfig.java b/src/main/java/autodig/client/config/DigConfig.java
index b418a23..94709b0 100644
--- a/src/main/java/autodig/client/config/DigConfig.java
+++ b/src/main/java/autodig/client/config/DigConfig.java
@@ -35,6 +35,16 @@ public class DigConfig {
/** 工具耐久阈值(百分比 0-100),低于此值停止挖矿 */
public int toolDurabilityThreshold = 15;
+ // ---- 装备与背包管理 ----
+ /** 装备耐久过低时自动从背包换上同类备用装备(工具按类别、护甲按部位),无备用才暂停 */
+ public boolean autoGearSwap = true;
+ /** 背包满时自动丢弃垃圾物品(按 trashItems 清单) */
+ public boolean autoTrash = true;
+ /** 垃圾物品清单(逗号分隔的物品 id,如 cobblestone,tuff,deepslate) */
+ public String trashItems = "cobblestone,cobbled_deepslate,deepslate,tuff,dirt,granite,diorite,andesite,netherrack,blackstone,basalt";
+ /** 方块类垃圾保留数量(供 Baritone 搭路/垫脚),0 为不保留 */
+ public int trashReserve = 64;
+
// ---- 岩浆/障碍 ----
/** 优先绕开岩浆池(给岩浆方块额外寻路代价) */
public boolean avoidLava = true;
@@ -81,6 +91,11 @@ public class DigConfig {
return parseOreList(this.netherOres);
}
+ /** 获取垃圾物品清单 */
+ public List getTrashItemList() {
+ return parseOreList(this.trashItems);
+ }
+
private static List parseOreList(String str) {
if (str == null || str.isBlank()) {
return List.of();
@@ -103,6 +118,10 @@ public class DigConfig {
c.armorDurabilityThreshold = this.armorDurabilityThreshold;
c.toolDurabilityProtection = this.toolDurabilityProtection;
c.toolDurabilityThreshold = this.toolDurabilityThreshold;
+ c.autoGearSwap = this.autoGearSwap;
+ c.autoTrash = this.autoTrash;
+ c.trashItems = this.trashItems;
+ c.trashReserve = this.trashReserve;
c.avoidLava = this.avoidLava;
c.killAura = this.killAura;
c.killAuraSafe = this.killAuraSafe;
diff --git a/src/main/java/autodig/client/controller/DigController.java b/src/main/java/autodig/client/controller/DigController.java
index 15dcd58..bd7f374 100644
--- a/src/main/java/autodig/client/controller/DigController.java
+++ b/src/main/java/autodig/client/controller/DigController.java
@@ -6,11 +6,12 @@ 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.AutoTrash;
import autodig.client.service.DeathRecorder;
import autodig.client.service.KillAura;
import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
@@ -18,7 +19,8 @@ import net.minecraft.world.Heightmap;
/**
* 挖矿总控制器:把配置、Baritone 桥接、自动进食、杀戮光环、死亡记录、装备耐久
- * 保护和各任务专用挖矿器串起来,驱动一个简单的状态机。
+ * 保护(自动换备用装备)、背包垃圾清理和各任务专用挖矿器串起来,
+ * 驱动一个简单的状态机。
*/
public final class DigController {
@@ -30,6 +32,8 @@ public final class DigController {
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 DeathRecorder deathRecorder = new DeathRecorder();
private final OverworldMiner overworld = new OverworldMiner(this.bridge);
private final NetherMiner nether = new NetherMiner(this.bridge);
@@ -52,9 +56,11 @@ public final class DigController {
}
PlayerEntity player = client.player;
- // 常驻能力:杀戮光环、自动进食
+ // 常驻能力:杀戮光环、自动进食、背包满丢垃圾、换装冷却
this.killAura.tick(config);
this.autoEat.tick(config);
+ this.autoTrash.tick(config);
+ this.autoGear.tick();
// 死亡处理:死亡瞬间暂停任务,重生后按需回到死亡坐标
this.deathRecorder.tick(config, this::onRespawn);
@@ -64,30 +70,37 @@ public final class DigController {
}
// 装备耐久保护(仅在挖矿任务进行中检查)
- if (config.task != MineTask.NONE && this.state != State.GEAR_PROTECTION) {
- if (this.isGearLowDurability(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, "装备耐久过低,暂停挖矿保装备");
+ 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);
}
- return;
- }
- }
-
- // 装备耐久保护恢复
- if (this.state == State.GEAR_PROTECTION) {
- if (!this.isGearLowDurability(config, player)) {
- this.setState(this.preGearState != null ? this.preGearState : State.IDLE,
- "装备耐久已恢复,继续挖矿");
- this.preGearState = null;
- } else {
- // 仍在装备保护中,等待玩家换装备
- return;
+ 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;
+ }
+ // 换装成功:继续正常流程
}
}
@@ -318,7 +331,7 @@ public final class DigController {
/**
* 检查装备耐久是否过低。
* 护甲:任意一件护甲耐久百分比低于阈值
- * 工具:手持工具耐久百分比低于阈值
+ * 工具:手持工具(镐/斧/锹/锄/剑)耐久百分比低于阈值
*/
private boolean isGearLowDurability(DigConfig config, PlayerEntity player) {
// 护甲耐久检查
@@ -335,10 +348,10 @@ public final class DigController {
}
}
}
- // 手持工具耐久检查
+ // 手持工具耐久检查(只关心真正的工具,不误伤钓鱼竿/打火石等杂物)
if (config.toolDurabilityProtection) {
ItemStack mainHand = player.getMainHandStack();
- if (!mainHand.isEmpty() && mainHand.isDamageable()) {
+ if (AutoGear.isTool(mainHand) && mainHand.isDamageable()) {
int maxDmg = mainHand.getMaxDamage();
int remaining = maxDmg - mainHand.getDamage();
double percent = (double) remaining / maxDmg * 100.0;
diff --git a/src/main/java/autodig/client/gui/AutoDigGui.java b/src/main/java/autodig/client/gui/AutoDigGui.java
index ca54fb4..e8e593f 100644
--- a/src/main/java/autodig/client/gui/AutoDigGui.java
+++ b/src/main/java/autodig/client/gui/AutoDigGui.java
@@ -56,6 +56,7 @@ public final class AutoDigGui extends Screen {
private TextFieldWidget overworldOresField;
private TextFieldWidget netherOresField;
+ private TextFieldWidget trashField;
public AutoDigGui(AutoDigModClient mod) {
super(Text.literal("AutoDig"));
@@ -72,6 +73,7 @@ public final class AutoDigGui extends Screen {
this.scrollOffset = 0;
this.overworldOresField = null;
this.netherOresField = null;
+ this.trashField = null;
// 面板宽度自适应窗口,最小 200 保证 +/- 与输入框可用
this.panelW = MathHelper.clamp(this.width - 16, 200, 400);
@@ -111,6 +113,12 @@ public final class AutoDigGui extends Screen {
"下界协同杀戮光环", () -> this.config.killAuraInNether, v -> this.config.killAuraInNether = v);
y = this.checkPair(y, "护甲耐久保护", () -> this.config.armorDurabilityProtection, v -> this.config.armorDurabilityProtection = v,
"工具耐久保护", () -> this.config.toolDurabilityProtection, v -> this.config.toolDurabilityProtection = v);
+ y = this.checkPair(y, "自动换备用装备", () -> this.config.autoGearSwap, v -> this.config.autoGearSwap = v,
+ "背包满丢垃圾", () -> this.config.autoTrash, v -> {
+ this.syncFields(); // 保留输入框中未应用的编辑
+ this.config.autoTrash = v;
+ this.init(); // 切换后显示/隐藏垃圾设置区
+ });
y += 10;
y = this.header(y, "装备耐久阈值(低于即停,百分比)");
@@ -120,6 +128,15 @@ public final class AutoDigGui extends Screen {
v -> this.config.toolDurabilityThreshold = v, 1, 100);
y += 10;
+ if (this.config.autoTrash) {
+ y = this.header(y, "垃圾丢弃设置");
+ y = this.numberRow(y, "方块垃圾保留量", () -> this.config.trashReserve,
+ v -> this.config.trashReserve = v, 0, 2304);
+ y = this.fieldRow(y, "垃圾物品(逗号分隔的物品 id)",
+ this.config.trashItems, tf -> this.trashField = tf);
+ y += 10;
+ }
+
if (this.config.task == MineTask.OVERWORLD) {
y = this.header(y, "主世界设置");
y = this.numberRow(y, "挖掘层 (Y)", () -> this.config.overworldLevel,
@@ -226,6 +243,7 @@ public final class AutoDigGui extends Screen {
boolean selected = this.config.task == task;
String text = selected ? "■ " + label : "□ " + label;
return this.addDrawableChild(this.textButton(text, b -> {
+ this.syncFields(); // 保留输入框中未应用的编辑
this.config.task = task;
this.init(); // 切换任务:重建滚动区,只显示相关设置
}, x, y, w));
@@ -339,6 +357,9 @@ public final class AutoDigGui extends Screen {
if (this.netherOresField != null) {
this.config.netherOres = this.netherOresField.getText();
}
+ if (this.trashField != null) {
+ this.config.trashItems = this.trashField.getText();
+ }
}
/** 把界面临时配置写回 mod 全局配置并保存(供 controller 实时读取)。 */
@@ -364,6 +385,10 @@ public final class AutoDigGui extends Screen {
target.armorDurabilityThreshold = this.config.armorDurabilityThreshold;
target.toolDurabilityProtection = this.config.toolDurabilityProtection;
target.toolDurabilityThreshold = this.config.toolDurabilityThreshold;
+ target.autoGearSwap = this.config.autoGearSwap;
+ target.autoTrash = this.config.autoTrash;
+ target.trashItems = this.config.trashItems;
+ target.trashReserve = this.config.trashReserve;
this.mod.saveConfig();
}
diff --git a/src/main/java/autodig/client/miner/NetherMiner.java b/src/main/java/autodig/client/miner/NetherMiner.java
index a3f0f5f..2697fb8 100644
--- a/src/main/java/autodig/client/miner/NetherMiner.java
+++ b/src/main/java/autodig/client/miner/NetherMiner.java
@@ -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 依据当前所在维度
* 自适应地执行对应阶段。
*
- * 下界无死亡不掉落,比主世界更"惜命":血量低或装备耐久低时主动触发返程。
+ * 下界无死亡不掉落,比主世界更"惜命":血量低或装备耐久低时主动触发返程。
+ * 背包满采用去抖判定(连续 1 秒都满才算真满),给自动丢垃圾服务留出腾位时间。
*/
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 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 ores = config.getNetherOreList();
+ if (ores.isEmpty()) {
+ return 0;
}
- int count = 0;
- var ores = config.getNetherOreList();
+ 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++) {
- 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;
}
diff --git a/src/main/java/autodig/client/miner/OverworldMiner.java b/src/main/java/autodig/client/miner/OverworldMiner.java
index 43f7ad6..27f50b9 100644
--- a/src/main/java/autodig/client/miner/OverworldMiner.java
+++ b/src/main/java/autodig/client/miner/OverworldMiner.java
@@ -35,6 +35,11 @@ public final class OverworldMiner {
private long lastIssue;
/** 上一次打印诊断日志的时间戳,用于节流 */
private long lastLog;
+ /** 背包满连续持续的 tick 数(去抖:给自动丢垃圾留出腾位时间) */
+ private int fullTicks;
+
+ /** 背包满去抖阈值(tick):连续这么久都满才认定为真满 */
+ private static final int FULL_DEBOUNCE_TICKS = 20;
public OverworldMiner(BaritoneBridge bridge) {
this.bridge = bridge;
@@ -64,6 +69,7 @@ public final class OverworldMiner {
this.step = 0;
this.done = false;
this.full = false;
+ this.fullTicks = 0;
}
private List buildPlan(BlockPos base, int y, int trunkLen, int branchLen, int branchGap, int axis, int sign) {
@@ -105,11 +111,17 @@ public final class OverworldMiner {
if (player == null) {
return true;
}
+ // 背包满去抖:连续一段时间都满才认定为真满(给自动丢垃圾留出腾位时间)
if (this.inventoryFull(player)) {
- this.done = true;
- this.full = true;
- System.out.println("[AutoDig] overworld 结束:背包已满");
- return true;
+ 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;
@@ -194,6 +206,7 @@ public final class OverworldMiner {
this.step = best;
this.done = false;
this.full = false;
+ this.fullTicks = 0;
}
/**
diff --git a/src/main/java/autodig/client/service/AutoGear.java b/src/main/java/autodig/client/service/AutoGear.java
new file mode 100644
index 0000000..0b0fd97
--- /dev/null
+++ b/src/main/java/autodig/client/service/AutoGear.java
@@ -0,0 +1,196 @@
+package autodig.client.service;
+
+import autodig.client.config.DigConfig;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerInteractionManager;
+import net.minecraft.entity.EquipmentSlot;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.item.ArmorItem;
+import net.minecraft.item.AxeItem;
+import net.minecraft.item.HoeItem;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.PickaxeItem;
+import net.minecraft.item.ShovelItem;
+import net.minecraft.item.SwordItem;
+import net.minecraft.screen.ScreenHandler;
+import net.minecraft.screen.slot.SlotActionType;
+
+/**
+ * 装备自动更换服务。
+ *
+ * 当护甲或手持工具耐久过低时,从背包里寻找同类别(工具:镐/斧/锹/锄/剑;
+ * 护甲:同部位头/胸/腿/脚)且剩余耐久更高的备用装备,通过对玩家背包
+ * ScreenHandler 的三次点击完成交换:拿起旧装备 → 与背包中的新装备互换 →
+ * 新装备就位。交换走正规网络包(clickSlot),客户端/服务端自动同步,
+ * 单人/多人均可用。找不到备用装备时返回 false,由控制器走"暂停保装备"或
+ * "下界返程"逻辑。
+ */
+public final class AutoGear {
+
+ /** 每次换装后的冷却 tick 数:等待客户端预测/服务端确认生效,避免连续误判 */
+ private static final int SWAP_COOLDOWN_TICKS = 10;
+
+ private int coolTicks;
+
+ /** 每帧推进冷却。 */
+ public void tick() {
+ if (this.coolTicks > 0) {
+ this.coolTicks--;
+ }
+ }
+
+ /**
+ * 尝试处理低耐久装备:用背包里的备用装备换下低耐久装备。
+ *
+ * @return true 表示本次(或最近一次)已成功换装,调用方可继续挖矿;
+ * false 表示当前没有可用的备用装备
+ */
+ public boolean handleLowGear(DigConfig config, PlayerEntity player) {
+ if (this.coolTicks > 0) {
+ return true; // 刚换过装,等待生效
+ }
+ MinecraftClient client = MinecraftClient.getInstance();
+ if (client.interactionManager == null) {
+ return false;
+ }
+ // 只在玩家背包界面(未打开箱子等容器)且鼠标上没拿着物品时操作
+ if (player.currentScreenHandler != player.playerScreenHandler) {
+ return false;
+ }
+ if (!player.currentScreenHandler.getCursorStack().isEmpty()) {
+ return false;
+ }
+ PlayerInventory inv = player.getInventory();
+
+ // 1. 手持工具:同类别里找剩余耐久最高的备用工具
+ ItemStack held = player.getMainHandStack();
+ if (isTool(held) && held.isDamageable()
+ && this.durabilityPct(held) < config.toolDurabilityThreshold) {
+ ItemStack repl = this.findToolReplacement(inv, held);
+ if (repl != null && this.swap(player, held, repl)) {
+ this.coolTicks = SWAP_COOLDOWN_TICKS;
+ return true;
+ }
+ }
+
+ // 2. 护甲:逐件找同部位、剩余耐久更高的备用护甲
+ for (int i = 0; i < 4; i++) {
+ ItemStack piece = inv.armor.get(i);
+ if (piece.isEmpty() || !piece.isDamageable()) {
+ continue;
+ }
+ if (this.durabilityPct(piece) >= config.armorDurabilityThreshold) {
+ continue;
+ }
+ ItemStack repl = this.findArmorReplacement(player, inv, i, piece);
+ if (repl != null && this.swap(player, piece, repl)) {
+ this.coolTicks = SWAP_COOLDOWN_TICKS;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** 手持工具的替换候选:同类别中剩余耐久最高且高于当前手持的。 */
+ private ItemStack findToolReplacement(PlayerInventory inv, ItemStack held) {
+ String kind = toolKind(held.getItem());
+ if (kind == null) {
+ return null;
+ }
+ ItemStack best = null;
+ for (int i = 0; i < 36; i++) {
+ if (i == inv.selectedSlot) {
+ continue; // 跳过当前手持槽位
+ }
+ ItemStack s = inv.main.get(i);
+ if (s.isEmpty() || !s.isDamageable() || !kind.equals(toolKind(s.getItem()))) {
+ continue;
+ }
+ if (best == null || this.remaining(s) > this.remaining(best)) {
+ best = s;
+ }
+ }
+ return best != null && this.remaining(best) > this.remaining(held) ? best : null;
+ }
+
+ /** 护甲替换候选:同部位中剩余耐久最高且高于当前穿戴的。 */
+ private ItemStack findArmorReplacement(PlayerEntity player, PlayerInventory inv, int armorIndex, ItemStack current) {
+ ItemStack best = null;
+ for (int i = 0; i < 36; i++) {
+ ItemStack s = inv.main.get(i);
+ if (s.isEmpty() || !(s.getItem() instanceof ArmorItem)) {
+ continue;
+ }
+ if (this.armorSlotIndex(player, s) != armorIndex) {
+ continue;
+ }
+ if (best == null || this.remaining(s) > this.remaining(best)) {
+ best = s;
+ }
+ }
+ return best != null && this.remaining(best) > this.remaining(current) ? best : null;
+ }
+
+ /** 背包里的护甲对应的护甲槽位下标(0=鞋 1=腿 2=胸 3=头),不可穿戴返回 -1。 */
+ private int armorSlotIndex(PlayerEntity player, ItemStack stack) {
+ EquipmentSlot slot = player.getPreferredEquipmentSlot(stack);
+ if (slot != EquipmentSlot.HEAD && slot != EquipmentSlot.CHEST
+ && slot != EquipmentSlot.LEGS && slot != EquipmentSlot.FEET) {
+ return -1;
+ }
+ return slot.getEntitySlotId();
+ }
+
+ /**
+ * 三次点击完成装备交换:
+ * 点击旧装备槽(拿起)→ 点击背包新装备槽(互换)→ 再点旧装备槽(新装备就位)。
+ */
+ private boolean swap(PlayerEntity player, ItemStack equipped, ItemStack replacement) {
+ ScreenHandler handler = player.currentScreenHandler;
+ int equippedSlot = this.screenSlotOf(handler, equipped);
+ int bagSlot = this.screenSlotOf(handler, replacement);
+ if (equippedSlot < 0 || bagSlot < 0 || equippedSlot == bagSlot) {
+ return false;
+ }
+ ClientPlayerInteractionManager im = MinecraftClient.getInstance().interactionManager;
+ int syncId = handler.syncId;
+ im.clickSlot(syncId, equippedSlot, 0, SlotActionType.PICKUP, player); // 拿起旧装备
+ im.clickSlot(syncId, bagSlot, 0, SlotActionType.PICKUP, player); // 与背包新装备互换
+ im.clickSlot(syncId, equippedSlot, 0, SlotActionType.PICKUP, player); // 新装备就位
+ 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;
+ }
+
+ /** 是否为受耐久保护的工具(镐/斧/锹/锄/剑)。 */
+ public static boolean isTool(ItemStack stack) {
+ return !stack.isEmpty() && toolKind(stack.getItem()) != null;
+ }
+
+ private static String toolKind(Item item) {
+ if (item instanceof PickaxeItem) return "pickaxe";
+ if (item instanceof AxeItem) return "axe";
+ if (item instanceof ShovelItem) return "shovel";
+ if (item instanceof HoeItem) return "hoe";
+ if (item instanceof SwordItem) return "sword";
+ return null;
+ }
+
+ private static int remaining(ItemStack stack) {
+ return stack.getMaxDamage() - stack.getDamage();
+ }
+
+ private static double durabilityPct(ItemStack stack) {
+ return stack.getMaxDamage() <= 0 ? 100.0 : (double) remaining(stack) / stack.getMaxDamage() * 100.0;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/autodig/client/service/AutoTrash.java b/src/main/java/autodig/client/service/AutoTrash.java
new file mode 100644
index 0000000..14147a9
--- /dev/null
+++ b/src/main/java/autodig/client/service/AutoTrash.java
@@ -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;
+
+/**
+ * 背包垃圾自动清理服务。
+ *
+ * 背包满时,把 {@code trashItems} 清单里的垃圾物品丢出背包(圆石、凝灰岩、
+ * 深板岩等挖掘副产物)。方块类垃圾保留 {@code trashReserve} 个供 Baritone
+ * 搭路/垫脚使用;若满背时额度之外没有别的垃圾可丢,则只丢一组腾出空间,
+ * 尽量保住搭路方块。丢弃通过 {@code clickSlot(THROW)} 走正规网络包,
+ * 单人/多人均可用。
+ */
+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 trash = new HashSet<>(config.getTrashItemList());
+ if (trash.isEmpty()) {
+ return;
+ }
+
+ // 收集背包里的垃圾,按物品 id 分组(并记录是否为可放置的方块)
+ Map> groups = new HashMap<>();
+ Map 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 drops = new ArrayList<>();
+ for (Map.Entry> 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 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();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index a0145d6..07edc65 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -3,7 +3,7 @@
"id": "autodig",
"version": "${version}",
"name": "AutoDig",
- "description": "自动挖矿模组(内置 Baritone 1.13.1,无需另行安装):主世界模式与下界模式,均可自定义目标矿物。主世界鱼骨巷道挖矿,下界自动进出传送门挖矿并支持返程。支持自动切换工具、自动进食、杀戮光环、保命优先、优先绕开岩浆、装备耐久保护、死亡重生回点。",
+ "description": "自动挖矿模组(内置 Baritone 1.13.1,无需另行安装):主世界模式与下界模式,均可自定义目标矿物。主世界鱼骨巷道挖矿,下界自动进出传送门挖矿并支持返程。支持自动切换工具、自动进食、杀戮光环、保命优先、优先绕开岩浆、装备耐久保护(自动换备用装备)、背包满自动丢垃圾、死亡重生回点。",
"authors": [],
"contact": {},
"license": "All-Rights-Reserved",