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",