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 新增两个开关与垃圾清单编辑区,切换任务/开关时保留输入框未应用的编辑 - 装备低耐久检查的手持工具仅限真正的工具,不再误伤钓鱼竿等杂物
这个提交包含在:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
在新工单中引用
屏蔽一个用户