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 行删除
+1 -1
查看文件
@@ -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
@@ -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;
}
@@ -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<String> getTrashItemList() {
return parseOreList(this.trashItems);
}
private static List<String> 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;
@@ -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;
@@ -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();
}
@@ -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;
}
@@ -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<BlockPos> 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;
}
/**
@@ -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;
/**
* 装备自动更换服务。
*
* <p>当护甲或手持工具耐久过低时,从背包里寻找同类别(工具:镐/斧/锹/锄/剑;
* 护甲:同部位头/胸/腿/脚)且剩余耐久更高的备用装备,通过对玩家背包
* ScreenHandler 的三次点击完成交换:拿起旧装备 → 与背包中的新装备互换 →
* 新装备就位。交换走正规网络包(clickSlot),客户端/服务端自动同步,
* 单人/多人均可用。找不到备用装备时返回 false,由控制器走"暂停保装备"或
* "下界返程"逻辑。</p>
*/
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;
}
}
@@ -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();
}
}
+1 -1
查看文件
@@ -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",