文件
AutoDig/src/main/java/autodig/client/gui/AutoDigGui.java
T
lyizai a297d2714d 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 新增两个开关与垃圾清单编辑区,切换任务/开关时保留输入框未应用的编辑
- 装备低耐久检查的手持工具仅限真正的工具,不再误伤钓鱼竿等杂物
2026-08-30 13:12:26 +08:00

436 行
20 KiB
Java
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package autodig.client.gui;
import autodig.client.AutoDigModClient;
import autodig.client.config.DigConfig;
import autodig.client.config.MineTask;
import autodig.client.controller.DigController;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.IntSupplier;
import java.util.function.Supplier;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.CheckboxWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.text.Text;
import net.minecraft.util.math.MathHelper;
/**
* AutoDig 游戏内 GUI(响应式 + 滚动布局)。
*
* <p>结构:顶部标题与任务选择、底部操作按钮(开始/停止/仅关闭)固定不动,
* 中间为可滚动设置区(滚轮滚动,右侧有滚动条)。设置区内容按当前任务动态
* 显示:主世界只显示主世界相关项,下界只显示下界相关项,避免无关选项干扰。
* 面板宽度随窗口自适应,标签过长时复选框自动改为上下堆叠,避免文字重叠。</p>
*/
public final class AutoDigGui extends Screen {
private final AutoDigModClient mod;
private final DigConfig config;
private final DigController controller;
/** 关闭界面时是否把改动写回 mod 配置("仅关闭"会置 false) */
private boolean applyOnClose = true;
// ---- 布局 ----
private int panelLeft;
private int panelW;
/** 滚动区可视范围(屏幕坐标) */
private int viewportTop;
private int viewportBottom;
// ---- 滚动状态 ----
private double scrollOffset;
private int contentHeight;
/** 可滚动控件与其虚拟坐标(真实 y = virtualY - scrollOffset) */
private record ScrollEntry(ClickableWidget widget, int x, int virtualY) {}
private final List<ScrollEntry> scrollable = new ArrayList<>();
/** 可滚动的纯文本(分区标题、数值、字段标签),渲染时按滚动偏移绘制 */
private record TextEntry(int x, int virtualY, Supplier<String> text, int color, boolean rightAlign, boolean underline) {}
private final List<TextEntry> texts = new ArrayList<>();
private TextFieldWidget overworldOresField;
private TextFieldWidget netherOresField;
private TextFieldWidget trashField;
public AutoDigGui(AutoDigModClient mod) {
super(Text.literal("AutoDig"));
this.mod = mod;
this.config = mod.config().copy();
this.controller = mod.controller();
}
@Override
protected void init() {
this.clearChildren();
this.scrollable.clear();
this.texts.clear();
this.scrollOffset = 0;
this.overworldOresField = null;
this.netherOresField = null;
this.trashField = null;
// 面板宽度自适应窗口,最小 200 保证 +/- 与输入框可用
this.panelW = MathHelper.clamp(this.width - 16, 200, 400);
this.panelLeft = (this.width - this.panelW) / 2;
// ===== 固定区:任务选择 =====
int taskY = 24;
int w3 = (this.panelW - 8) / 3;
this.addDrawableChild(this.taskButton("空闲", MineTask.NONE, this.panelLeft, taskY, w3));
this.addDrawableChild(this.taskButton("主世界", MineTask.OVERWORLD, this.panelLeft + w3 + 4, taskY, w3));
this.addDrawableChild(this.taskButton("下界", MineTask.NETHER, this.panelLeft + (w3 + 4) * 2, taskY, w3));
// ===== 固定区:底部操作按钮(永远可见,不随内容滚动) =====
int actionY = this.height - 44;
this.addDrawableChild(this.textButton("开始", b -> this.start(), this.panelLeft, actionY, w3));
this.addDrawableChild(this.textButton("停止", b -> this.stop(), this.panelLeft + w3 + 4, actionY, w3));
this.addDrawableChild(this.textButton("仅关闭", b -> this.closeOnly(), this.panelLeft + (w3 + 4) * 2, actionY, w3));
this.viewportTop = taskY + 26;
this.viewportBottom = actionY - 8;
// ===== 滚动区内容(虚拟坐标从 0 开始) =====
int y = 0;
if (this.config.task == MineTask.NONE) {
this.texts.add(new TextEntry(this.panelLeft, 4, () -> "请先在上方选择挖矿任务", 0xBBBBBB, false, false));
this.texts.add(new TextEntry(this.panelLeft, 18, () -> "主世界:鱼骨巷道挖矿 / 下界:进传送门挖矿并自动返程", 0x888888, false, false));
y = 40;
} else {
y = this.header(y, "通用设置");
y = this.checkPair(y, "保命优先", () -> this.config.survivalFirst, v -> this.config.survivalFirst = v,
"优先绕开岩浆", () -> this.config.avoidLava, v -> this.config.avoidLava = v);
y = this.checkPair(y, "自动进食", () -> this.config.autoEat, v -> this.config.autoEat = v,
"自动切换工具", () -> this.config.autoTool, v -> this.config.autoTool = v);
y = this.checkPair(y, "重生后回死亡点", () -> this.config.respawnReturn, v -> this.config.respawnReturn = v,
"杀戮光环", () -> this.config.killAura, v -> this.config.killAura = v);
y = this.checkPair(y, "杀戮光环不冒进", () -> this.config.killAuraSafe, v -> this.config.killAuraSafe = v,
"下界协同杀戮光环", () -> 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, "装备耐久阈值(低于即停,百分比)");
y = this.numberRow(y, "护甲耐久", () -> this.config.armorDurabilityThreshold,
v -> this.config.armorDurabilityThreshold = v, 1, 100);
y = this.numberRow(y, "工具耐久", () -> this.config.toolDurabilityThreshold,
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,
v -> this.config.overworldLevel = v, -64, 320);
y = this.numberRow(y, "巷道长度", () -> this.config.tunnelLength,
v -> this.config.tunnelLength = v, 4, 512);
y = this.fieldRow(y, "目标矿物(逗号分隔,如 diamond_ore,deepslate_diamond_ore)",
this.config.overworldOres, tf -> this.overworldOresField = tf);
y += 10;
} else if (this.config.task == MineTask.NETHER) {
y = this.header(y, "下界设置");
y = this.numberRow(y, "目标数量 (-1 无限)", () -> this.config.netherTargetCount,
v -> this.config.netherTargetCount = v, -1, 512);
y = this.checkPair(y, "挖完/背包满/血量低/耐久低 自动回主世界",
() -> this.config.netherAutoReturn, v -> this.config.netherAutoReturn = v,
null, null, null);
y = this.numberRow(y, "返程血量阈值", () -> this.config.netherFleeHealthThreshold,
v -> this.config.netherFleeHealthThreshold = v, 0, 100);
y = this.fieldRow(y, "目标矿物(逗号分隔,如 ancient_debris,nether_gold_ore)",
this.config.netherOres, tf -> this.netherOresField = tf);
y += 10;
}
}
this.contentHeight = y;
this.clampScroll();
this.applyScroll();
}
// ================= 布局辅助 =================
/** 分区标题(金色文本 + 下划线)。 */
private int header(int y, String text) {
this.texts.add(new TextEntry(this.panelLeft, y, () -> text, 0xFFD700, false, true));
return y + 16;
}
/**
* 一行复选框:两列放得下则并排;放不下(窄屏/长文本)自动上下堆叠,
* 保证文字不会互相重叠。label2 为 null 时独占一行。
*/
private int checkPair(int y, String label1, BooleanSupplier get1, Consumer<Boolean> set1,
String label2, BooleanSupplier get2, Consumer<Boolean> set2) {
CheckboxWidget c1 = this.checkbox(label1, get1, set1);
if (label2 == null) {
this.addScrollable(c1, this.panelLeft, y);
return y + 22;
}
CheckboxWidget c2 = this.checkbox(label2, get2, set2);
if (c1.getWidth() + c2.getWidth() + 12 <= this.panelW) {
this.addScrollable(c1, this.panelLeft, y);
this.addScrollable(c2, this.panelLeft + this.panelW - c2.getWidth(), y);
return y + 22;
}
// 放不下:上下堆叠
this.addScrollable(c1, this.panelLeft, y);
this.addScrollable(c2, this.panelLeft, y + 22);
return y + 44;
}
private CheckboxWidget checkbox(String label, BooleanSupplier getter, Consumer<Boolean> setter) {
return CheckboxWidget.builder(Text.literal(label), this.textRenderer)
.pos(0, 0).checked(getter.getAsBoolean())
.callback((b, v) -> setter.accept(v)).build();
}
/** 数值行:[-] 标签 ..... 值 [+],标签左对齐、数值右对齐,自适应面板宽度。 */
private int numberRow(int y, String label, IntSupplier getter, Consumer<Integer> setter, int min, int max) {
int btn = 20;
this.addScrollable(ButtonWidget.builder(Text.literal("-"), b ->
setter.accept(MathHelper.clamp(getter.getAsInt() - 1, min, max)))
.dimensions(this.panelLeft, 0, btn, btn).build(), this.panelLeft, y);
this.addScrollable(ButtonWidget.builder(Text.literal("+"), b ->
setter.accept(MathHelper.clamp(getter.getAsInt() + 1, min, max)))
.dimensions(this.panelLeft + this.panelW - btn, 0, btn, btn).build(),
this.panelLeft + this.panelW - btn, y);
this.texts.add(new TextEntry(this.panelLeft + btn + 6, y + 6, () -> label, 0xFFFFFF, false, false));
this.texts.add(new TextEntry(this.panelLeft + this.panelW - btn - 6, y + 6,
() -> String.valueOf(getter.getAsInt()), 0x55FF55, true, false));
return y + 24;
}
/** 文本输入行:标签 + 输入框(占满面板宽度)。 */
private int fieldRow(int y, String label, String initial, Consumer<TextFieldWidget> ref) {
this.texts.add(new TextEntry(this.panelLeft, y + 3, () -> label, 0xBBBBBB, false, false));
y += 14;
TextFieldWidget tf = new TextFieldWidget(this.textRenderer, this.panelLeft, 0, this.panelW, 18, Text.literal(""));
tf.setMaxLength(256);
tf.setText(initial);
ref.accept(tf);
this.addScrollable(tf, this.panelLeft, y);
return y + 24;
}
/** 注册可滚动控件(真实位置在 applyScroll 中按偏移计算)。 */
private void addScrollable(ClickableWidget widget, int x, int virtualY) {
widget.setX(x);
widget.setY(virtualY);
this.scrollable.add(new ScrollEntry(widget, x, virtualY));
this.addDrawableChild(widget);
}
private ButtonWidget taskButton(String label, MineTask task, int x, int y, int w) {
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));
}
private ButtonWidget textButton(String label, ButtonWidget.PressAction action, int x, int y, int w) {
return ButtonWidget.builder(Text.literal(label), action).dimensions(x, y, w, 20).build();
}
// ================= 滚动逻辑 =================
private double maxScroll() {
return Math.max(0, this.contentHeight - (this.viewportBottom - this.viewportTop));
}
private void clampScroll() {
this.scrollOffset = MathHelper.clamp(this.scrollOffset, 0, this.maxScroll());
}
/** 按滚动偏移重排控件位置,并把滚出可视区的控件隐藏(不渲染、不可点)。 */
private void applyScroll() {
for (ScrollEntry e : this.scrollable) {
int dy = e.virtualY() - (int) this.scrollOffset;
e.widget().setX(e.x());
e.widget().setY(dy);
e.widget().visible = dy + e.widget().getHeight() > this.viewportTop
&& dy < this.viewportBottom;
}
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
if (super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
return true;
}
double before = this.scrollOffset;
this.scrollOffset = MathHelper.clamp(
this.scrollOffset - verticalAmount * 24.0, 0, this.maxScroll());
if (this.scrollOffset != before) {
this.applyScroll();
return true;
}
return false;
}
// ================= 渲染 =================
/**
* GUI 打开时不允许游戏暂停:否则客户端 tick 不执行,控制器停摆。
*/
@Override
public boolean shouldPause() {
return false;
}
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
this.renderBackground(context, mouseX, mouseY, delta);
// 滚动区底色,增强可读性
context.fill(this.panelLeft - 6, this.viewportTop - 4,
this.panelLeft + this.panelW + 6, this.viewportBottom + 4, 0x88000000);
super.render(context, mouseX, mouseY, delta);
context.drawCenteredTextWithShadow(this.textRenderer, "AutoDig 自动挖矿",
this.width / 2, 8, 0xFFFFFF);
// 滚动区文本(裁剪绘制,防止溢出到固定区)
int offset = (int) this.scrollOffset;
context.enableScissor(this.panelLeft - 6, this.viewportTop - 4,
this.panelLeft + this.panelW + 6, this.viewportBottom + 4);
for (TextEntry t : this.texts) {
String text = t.text().get();
if (text == null || text.isEmpty()) {
continue;
}
int dy = t.virtualY() - offset;
int x = t.rightAlign() ? t.x() - this.textRenderer.getWidth(text) : t.x();
context.drawTextWithShadow(this.textRenderer, text, x, dy, t.color());
if (t.underline()) {
context.fill(this.panelLeft, dy + 11, this.panelLeft + this.panelW, dy + 12, 0x55FFFFFF);
}
}
context.disableScissor();
// 滚动条
double max = this.maxScroll();
if (max > 0) {
int trackTop = this.viewportTop - 4;
int trackBottom = this.viewportBottom + 4;
int trackH = trackBottom - trackTop;
int thumbH = Math.max(18, (int) (trackH * (double) trackH / Math.max(1, this.contentHeight)));
int thumbY = trackTop + (int) ((trackH - thumbH) * (this.scrollOffset / max));
int barX = this.panelLeft + this.panelW + 8;
context.fill(barX, trackTop, barX + 3, trackBottom, 0x40FFFFFF);
context.fill(barX, thumbY, barX + 3, thumbY + thumbH, 0xB0FFFFFF);
}
// 底部状态栏
context.drawTextWithShadow(this.textRenderer,
"任务: " + this.config.task.label() + " 状态: " + this.controller.statusText(),
this.panelLeft, this.height - 14, 0xAAAAAA);
}
// ================= 应用与操作 =================
/** 同步文本输入框的最新值到本地配置副本。 */
private void syncFields() {
if (this.overworldOresField != null) {
this.config.overworldOres = this.overworldOresField.getText();
}
if (this.netherOresField != null) {
this.config.netherOres = this.netherOresField.getText();
}
if (this.trashField != null) {
this.config.trashItems = this.trashField.getText();
}
}
/** 把界面临时配置写回 mod 全局配置并保存(供 controller 实时读取)。 */
private void applyToMod() {
DigConfig target = this.mod.config();
target.task = this.config.task;
target.survivalFirst = this.config.survivalFirst;
target.avoidLava = this.config.avoidLava;
target.autoEat = this.config.autoEat;
target.autoTool = this.config.autoTool;
target.killAura = this.config.killAura;
target.killAuraSafe = this.config.killAuraSafe;
target.killAuraInNether = this.config.killAuraInNether;
target.respawnReturn = this.config.respawnReturn;
target.netherAutoReturn = this.config.netherAutoReturn;
target.overworldLevel = this.config.overworldLevel;
target.tunnelLength = this.config.tunnelLength;
target.overworldOres = this.config.overworldOres;
target.netherOres = this.config.netherOres;
target.netherTargetCount = this.config.netherTargetCount;
target.netherFleeHealthThreshold = this.config.netherFleeHealthThreshold;
target.armorDurabilityProtection = this.config.armorDurabilityProtection;
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();
}
private void start() {
this.syncFields();
if (this.config.task == MineTask.NONE) {
this.mod.sendToast("AutoDig: 请先选择一个挖矿任务(主世界 / 下界)");
return;
}
List<String> ores = this.config.task == MineTask.OVERWORLD
? this.config.getOverworldOreList()
: this.config.getNetherOreList();
if (ores.isEmpty()) {
this.mod.sendToast("AutoDig: 目标矿物列表为空,请填写至少一个矿物 ID");
return;
}
this.applyToMod();
this.mod.sendToast("AutoDig: 已开始 " + this.config.task.label() + " 任务");
this.close();
}
private void stop() {
// 先把本地副本的任务置空再写回,避免 close() 二次应用时把旧任务写回去
this.config.task = MineTask.NONE;
this.applyToMod();
this.controller.bridge().cancelAll();
this.mod.sendToast("AutoDig: 已停止");
this.close();
}
/** 仅关闭:丢弃未应用的改动。 */
private void closeOnly() {
this.applyOnClose = false;
this.close();
}
@Override
public void close() {
if (this.applyOnClose) {
this.syncFields();
this.applyToMod();
}
super.close();
}
}