/* * 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152), * 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。 */ package autostore.client.controller; import autostore.client.bridge.BaritoneBridge; import autostore.client.config.ExpansionConfig; import autostore.client.monitor.SubtitleChatMonitor; import autostore.client.planner.ExpansionPlanner; import autostore.client.territory.ChunkPos; import autostore.client.territory.TerritoryDataService; import autostore.client.territory.TerritoryModel; import autostore.client.territory.TerritoryState; import baritone.api.event.events.PathEvent; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.util.Hand; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; import net.minecraft.util.math.BlockPos; import net.minecraft.client.MinecraftClient; import net.minecraft.component.DataComponentTypes; @Environment(value=EnvType.CLIENT) /** * 自动扩地控制器:扩地任务的核心状态机。 * *

工作流程:规划(PLANNING)→ 寻路(PATHING)→ 等待开垦(WAITING)→ 成功/失败(SUCCESS/FAIL)→ 下一个目标(NEXT)→ 完成(COMPLETE)。 * 每帧由 {@link #tick()} 驱动,通过 Baritone 自动寻路到目标区块, * 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。 */ public final class ExpansionController implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener { /** 日志环形缓冲区的最大条数(含导出用详细日志,故调大) */ private static final int MAX_LOGS = 2000; /** 日志时间戳格式(时:分:秒) */ private static final DateTimeFormatter LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss"); /** 允许开拓的现实时间起始小时(含,本地时区) */ private static final int CLAIM_START_HOUR = 11; /** 允许开拓的现实时间结束小时(不含,本地时区),即 22:00 起停止开拓 */ private static final int CLAIM_END_HOUR = 22; /** Baritone 寻路桥接(负责自动移动) */ private final BaritoneBridge baritoneBridge = new BaritoneBridge(); /** 扩地规划器(决定下一个目标区块) */ private final ExpansionPlanner planner = new ExpansionPlanner(); /** 聊天/字幕监听器(识别开垦进度与结果) */ private final SubtitleChatMonitor monitor = new SubtitleChatMonitor(this); /** 领土数据服务(提供实时领土模型) */ private final TerritoryDataService dataService; /** 待占领区块队列 */ private final Deque queue = new ArrayDeque(); /** 运行日志队列(线程安全) */ private final Deque logs = new ArrayDeque(); /** 当前扩地配置(启动时从请求配置复制) */ private ExpansionConfig config = new ExpansionConfig(); /** 当前领土模型 */ private TerritoryModel model = TerritoryModel.empty(); /** 当前状态机状态 */ private State state = State.IDLE; /** 当前状态描述文本(显示在 GUI/命令中) */ private String status = "\u672a\u542f\u52a8"; /** 当前目标区块 */ private ChunkPos target; /** 玩家当前所在区块(每帧更新) */ private ChunkPos playerChunk = new ChunkPos(0, 0); /** 已成功开垦的区块数量 */ private int completedClaims; /** 当前目标的连续重试次数 */ private int retryCount; /** 当前阶段(寻路/等待)的开始时间戳 */ private long stageStartedMillis; /** 最近一次开垦进度(0-100) */ private int lastProgress; /** 当前目标区块的哈希集合,用于去重(防止重复计数) */ private final Set claimedSet = new HashSet<>(); /** 是否正在自动进食 */ private boolean eating; /** Baritone 最近一次路径计算是否失败(目标不可达);由事件监听设置,寻路阶段消费 */ private volatile boolean pathCalcFailed; public ExpansionController(TerritoryDataService dataService) { this.dataService = dataService; // 注册路径事件监听,及时识别“目标不可达”并跳过(避免大山/悬崖地形傻等超时) this.baritoneBridge.addPathListener(this); } public SubtitleChatMonitor monitor() { return this.monitor; } public State state() { return this.state; } public String status() { return this.status; } public int completedClaims() { return this.completedClaims; } public int queuedTargets() { return this.queue.size(); } public ChunkPos target() { return this.target; } public int lastProgress() { return this.lastProgress; } /** 返回运行日志的副本(线程安全) */ public List logs() { Deque deque = this.logs; synchronized (deque) { return new ArrayList(this.logs); } } public boolean isRunning() { return this.state != State.IDLE && this.state != State.COMPLETE && this.state != State.STOPPED; } /** * 判断当前现实时间(本地时区)是否在允许开拓的时间段内。 * 仅 11:00(含)到 22:00(不含)允许开拓,其余时段服务器会拒绝开拓。 * * @return true 表示当前在允许开拓时段内 */ public static boolean isWithinClaimTime() { int hour = LocalTime.now().getHour(); return hour >= CLAIM_START_HOUR && hour < CLAIM_END_HOUR; } /** * 启动自动扩地任务。 * *

前置校验:必须在服务器世界中、Baritone 已就绪; * 邦国名称为空时自动识别玩家所在邦国,否则校验指定邦国是否存在。 * 校验通过后清空队列、生成扩地计划并进入 PLANNING 状态。 * * @param requestedConfig 本次任务的扩地配置 * @return 是否成功启动 */ public boolean start(ExpansionConfig requestedConfig) { MinecraftClient client = MinecraftClient.getInstance(); if (client == null || client.player == null || client.world == null) { this.status = "\u8bf7\u5148\u8fdb\u5165\u670d\u52a1\u5668\u4e16\u754c"; return false; } if (!this.baritoneBridge.isAvailable()) { this.status = "Baritone \u5c1a\u672a\u521d\u59cb\u5316\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5"; return false; } // 现实时间限制:仅 11:00-22:00 允许开拓,其余时段服务器会拒绝开拓(等待超时) if (!ExpansionController.isWithinClaimTime()) { this.status = "\u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\uff0811:00-22:00\uff09\uff0c\u65e0\u6cd5\u5f00\u62d3"; this.setState(State.IDLE, this.status); this.log("\u542f\u52a8\u5931\u8d25\uff1a\u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\uff0c\u5f53\u524d\u73b0\u5b9e\u65f6\u95f4 " + LocalTime.now().format(LOG_TIME_FORMAT)); return false; } this.config = requestedConfig.copy(); this.model = this.dataService.model(); this.playerChunk = this.playerChunk(client); if (this.config.stateName.isEmpty()) { TerritoryState auto = this.model.findContainingState(this.playerChunk); if (auto == null) { this.status = "\u65e0\u6cd5\u81ea\u52a8\u8bc6\u522b\u90a6\u56fd\uff0c\u8bf7\u5728 GUI \u4e2d\u624b\u52a8\u9009\u62e9"; this.state = State.IDLE; return false; } this.model.setSelfStateName(auto.name()); this.dataService.setSelectedStateName(auto.name()); } else { if (this.model.state(this.config.stateName) == null) { this.status = "\u6240\u9009\u90a6\u56fd\u4e0d\u5b58\u5728\uff1a" + this.config.stateName; this.state = State.IDLE; return false; } this.model.setSelfStateName(this.config.stateName); this.dataService.setSelectedStateName(this.config.stateName); } this.completedClaims = 0; this.retryCount = 0; this.queue.clear(); this.claimedSet.clear(); this.target = null; this.eating = false; this.baritoneBridge.applyConfig(this.config); this.setState(State.PLANNING, "\u6b63\u5728\u751f\u6210\u6269\u5f20\u8ba1\u5212"); this.log("\u5f00\u59cb\u81ea\u52a8\u6269\u5730\uff0c\u90a6\u56fd\uff1a" + this.model.selfStateName()); this.plan(); return true; } /** 手动停止任务:取消 Baritone 寻路并进入 STOPPED 状态 */ public void stop() { if (this.state == State.IDLE || this.state == State.COMPLETE || this.state == State.STOPPED) { return; } this.baritoneBridge.cancel(); this.eating = false; this.setState(State.STOPPED, "\u5df2\u624b\u52a8\u505c\u6b62"); this.log("\u5df2\u624b\u52a8\u505c\u6b62"); } /** * 每帧驱动逻辑(由客户端 tick 事件调用): * 更新玩家区块,优先处理自动进食,再按当前状态分发到对应的 tick 方法。 * 若已退出世界则自动停止任务。 */ public void tick() { MinecraftClient client = MinecraftClient.getInstance(); if (client == null || client.player == null || client.world == null) { if (this.isRunning()) { this.stop(); this.status = "\u4e16\u754c\u5df2\u9000\u51fa\uff0c\u4efb\u52a1\u505c\u6b62"; } return; } this.playerChunk = this.playerChunk(client); // 现实时间限制:运行中超出 11:00-22:00 则自动停止,避免服务器拒绝开拓导致超时 if (this.isRunning() && !ExpansionController.isWithinClaimTime()) { this.stop(); this.status = "\u5df2\u5230\u5f00\u62d3\u65f6\u9650\u5916\uff0811:00-22:00\uff09\uff0c\u4efb\u52a1\u505c\u6b62"; this.log("\u5df2\u5230\u5f00\u62d3\u65f6\u9650\u5916\uff0c\u505c\u6b62\u4efb\u52a1\uff08\u5f53\u524d\u73b0\u5b9e\u65f6\u95f4 " + LocalTime.now().format(LOG_TIME_FORMAT) + "\uff09"); return; } if (this.tickAutoFood(client)) { return; } this.tickAutoSwim(client); switch (this.state.ordinal()) { case 1: { this.plan(); break; } case 2: { this.tickPathing(); break; } case 3: { this.tickWaiting(); break; } case 4: { this.tickSuccess(); break; } case 5: { this.tickFail(); break; } case 6: { this.tickNext(); break; } } } /** * 生成扩地计划:调用规划器计算候选区块,按配置过滤圈数上限后入队。 * 若已达成目标数量或没有可用区块则直接完成。 */ private void plan() { this.model = this.dataService.model(); if (this.config.targetCount > 0 && this.completedClaims >= this.config.targetCount) { this.finish(); return; } this.queue.clear(); List planned = this.planner.generate(this.model, this.config, this.playerChunk); // 过滤掉规划后已被他人占领的区块,避免寻路到无效目标 for (ChunkPos chunk : planned) { if (!this.isClaimedByOthers(chunk)) { this.queue.add(chunk); } } // 矩形区域扩地时不受扩圈上限限制(玩家圈选的矩形即为明确边界,需完整填充) boolean rectangleMode = this.config.strategy == 2 && this.config.regionMaxX >= this.config.regionMinX; if (this.config.maxRings > 0 && !rectangleMode) { this.queue.removeIf(chunk -> this.ringDistance(chunk, this.model) > this.config.maxRings); } this.log("\u89c4\u5212\u5b8c\u6210\uff0c\u5171 " + this.queue.size() + " \u4e2a\u5019\u9009\u533a\u5757"); if (this.queue.isEmpty()) { this.setState(State.COMPLETE, "\u6ca1\u6709\u53ef\u7528\u7684\u76f8\u90bb\u65e0\u4e3b\u533a\u5757"); this.log("\u6ca1\u6709\u53ef\u7528\u7684\u76f8\u90bb\u65e0\u4e3b\u533a\u5757"); return; } this.target = this.queue.removeFirst(); this.startPathing(); } /** 让 Baritone 寻路前往当前目标区块,并进入 PATHING 状态 */ private void startPathing() { this.retryCount = 0; this.stageStartedMillis = System.currentTimeMillis(); this.lastProgress = 0; this.baritoneBridge.pathTo(this.target); this.setState(State.PATHING, "\u6b63\u5728\u524d\u5f80\u76ee\u6807\u533a\u5757 " + this.target.x() + "," + this.target.z()); this.log("\u5bfb\u8def\u524d\u5f80 " + this.target.x() + "," + this.target.z() + "\uff08\u73a9\u5bb6\u533a\u5757 " + this.playerChunk.x() + "," + this.playerChunk.z() + "\uff09"); } /** * 寻路阶段每帧逻辑:到达目标区块后进入等待开垦状态; * 若寻路超时则重新寻路(而非回城),超过重试次数则跳过该目标。 */ private void tickPathing() { if (this.target == null) { this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a"); return; } // 目标已被他人占领(地图刷新后发现):立即跳过,避免继续寻路到无效区块 if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) { return; } // Baritone 报告路径计算失败(目标不可达,如被悬崖/高山/水体完全隔断):立即跳过,不再傻等超时 if (this.pathCalcFailed) { this.pathCalcFailed = false; this.log("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); this.setState(State.FAIL, "\u5bfb\u8def\u5931\u8d25"); return; } // 目标已不再与己方领土接壤(地图刷新后边界变化):立即跳过,避免走到无效区块 if (this.skipIfTargetNotAdjacent()) { return; } if (this.playerChunk.equals(this.target)) { this.baritoneBridge.cancel(); this.stageStartedMillis = System.currentTimeMillis(); this.setState(State.WAITING, "\u5df2\u5230\u8fbe\uff0c\u7b49\u5f85\u9886\u5730\u5f00\u62d3"); this.log("\u5df2\u5230\u8fbe\u76ee\u6807 " + this.target.x() + "," + this.target.z() + "\uff0c\u5f00\u59cb\u7b49\u5f85\u5f00\u57a6"); return; } long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L; if (elapsedSeconds >= this.config.waitTimeoutSeconds) { ++this.retryCount; // 主动刷新地图,尽快同步最新领土状态 this.dataService.refreshAsync(); if (this.retryCount >= this.config.retries) { // 重试耗尽:跳过当前目标(不再回城),交给下一个目标 this.log("\u5bfb\u8def\u8d85\u65f6\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); this.setState(State.FAIL, "\u5bfb\u8def\u8d85\u65f6"); } else { // 重新寻路(而非回城):取消旧路径并重新计算,往往“在边上走走”就能走出困境 this.log("\u5bfb\u8def\u8d85\u65f6\uff0c\u91cd\u65b0\u5bfb\u8def " + this.retryCount + "/" + this.config.retries); this.retrySameTarget("\u5bfb\u8def\u8d85\u65f6\uff0c\u91cd\u65b0\u5bfb\u8def"); } } } /** 等待开垦阶段每帧逻辑:玩家离开目标区块则重新寻路;等待超时则重试或跳过当前目标 */ private void tickWaiting() { if (this.target == null) { this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a"); return; } // 目标已被他人占领(地图刷新后发现):立即跳过,避免被困在已占领区块 if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) { return; } // 目标已不再与己方领土接壤:立即跳过,避免在无效区块上干等开垦 if (this.skipIfTargetNotAdjacent()) { return; } // 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去 if (!this.playerChunk.equals(this.target)) { this.retrySameTarget("\u79bb\u5f00\u76ee\u6807\u533a\u5757\uff0c\u91cd\u65b0\u524d\u5f80 " + this.target.x() + "," + this.target.z()); return; } long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L; if (elapsedSeconds >= this.config.waitTimeoutSeconds) { ++this.retryCount; // 主动刷新地图,尽快同步"区块已被占领"的最新状态,避免玩家被困 this.dataService.refreshAsync(); if (this.retryCount >= this.config.retries) { this.log("\u7b49\u5f85\u8d85\u65f6\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); this.setState(State.FAIL, "\u7b49\u5f85\u9886\u5730\u5f00\u62d3\u8d85\u65f6"); } else { this.log("\u7b49\u5f85\u8d85\u65f6\uff0c\u91cd\u8bd5 " + this.retryCount + "/" + this.config.retries); this.retrySameTarget("\u7b49\u5f85\u8d85\u65f6\uff0c\u91cd\u65b0\u524d\u5f80 " + this.target.x() + "," + this.target.z()); } } } /** 重试当前目标:取消旧寻路,重新让 Baritone 前往同一目标区块 */ private void retrySameTarget(String statusText) { // 目标已被他人占领(地图刷新后发现):不再重试,直接跳过,避免玩家被困在已占领区块 if (this.skipIfTargetClaimed("\u91cd\u8bd5\u65f6\u53d1\u73b0\u76ee\u6807\u5df2\u88ab\u5360\u9886")) { return; } this.baritoneBridge.cancel(); this.stageStartedMillis = System.currentTimeMillis(); this.baritoneBridge.pathTo(this.target); this.setState(State.PATHING, statusText); } /** * 若当前目标区块已被任何邦国占领(含己方与本次会话), * 则记录日志、跳过该目标并进入 NEXT 状态,返回 true。 * 用于在地图数据异步刷新后及时识别"已被占领"的区块,避免死循环。 */ private boolean skipIfTargetClaimed(String reason) { if (this.target != null && this.isClaimedByOthers(this.target)) { this.log(reason + " " + this.target.x() + "," + this.target.z() + "\uff0c\u8df3\u8fc7"); this.setState(State.NEXT, "\u8df3\u8fc7\u5df2\u5360\u9886\u533a\u5757"); return true; } return false; } /** * 若当前目标区块不再与己方领土(含本次会话已占领)的四个正方向接壤, * 则跳过并进入 NEXT 状态,返回 true。 * *

这是解决“走到不接壤区块”的关键:规划时目标必然接壤,但寻路/等待过程中 * 领土数据会异步刷新(边界变化、假占领被剔除等),导致目标可能变得不接壤。 * 在寻路与等待阶段每帧校验,一旦发现不接壤立即放弃,避免在无效区块上白费时间。

*/ private boolean skipIfTargetNotAdjacent() { if (this.target != null && !this.isAdjacentToOwn(this.target)) { this.log("\u76ee\u6807\u4e0e\u5df1\u65b9\u9886\u571f\u4e0d\u76f8\u90bb\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); this.setState(State.NEXT, "\u8df3\u8fc7\u4e0d\u63a5\u58e4\u533a\u5757"); return true; } return false; } /** * 判断区块是否与己方领土(己方邦国地图区块 + 本次会话新占领区块)接壤。 * *

接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。

*/ private boolean isAdjacentToOwn(ChunkPos chunk) { TerritoryModel model = this.dataService.model(); Set own = new HashSet(); TerritoryState self = model.selfState(); if (self != null) { own.addAll(self.chunks()); } own.addAll(model.sessionClaims()); for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) { if (own.contains(neighbor)) { return true; } } return false; } /** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */ private void tickSuccess() { this.setState(State.NEXT, "\u5df2\u5b8c\u6210 " + this.completedClaims + (this.config.targetCount > 0 ? "/" + this.config.targetCount : "/\u6301\u7eed")); } /** 失败后的过渡:进入 NEXT 状态,跳过当前目标 */ private void tickFail() { this.setState(State.NEXT, "\u51c6\u5907\u4e0b\u4e00\u4e2a\u76ee\u6807"); } /** * 处理下一个目标:若已达成目标数量则完成; * 队列为空时重新规划,否则取出队首区块开始寻路。 */ private void tickNext() { this.target = null; if (this.config.targetCount > 0 && this.completedClaims >= this.config.targetCount) { this.finish(); return; } if (this.queue.isEmpty()) { this.plan(); return; } this.target = this.queue.removeFirst(); // 目标已被他人占领时跳过,避免无效寻路 if (this.isClaimedByOthers(this.target)) { this.log("\u76ee\u6807\u5df2\u88ab\u5360\u9886\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); this.setState(State.NEXT, "\u8df3\u8fc7\u5df2\u5360\u9886\u533a\u5757"); return; } this.startPathing(); } /** 完成任务:取消寻路、停止进食,进入 COMPLETE 状态 */ private void finish() { this.baritoneBridge.cancel(); this.eating = false; this.setState(State.COMPLETE, this.config.targetCount > 0 ? "\u5df2\u5b8c\u6210\u76ee\u6807\u6570\u91cf " + this.completedClaims + "/" + this.config.targetCount : "\u5df2\u65e0\u53ef\u6269\u533a\u5757\uff0c\u7d2f\u8ba1\u6269\u5730 " + this.completedClaims); this.log("\u81ea\u52a8\u6269\u5730\u5b8c\u6210"); } /** 计算区块到己方领土的"圈数"(切比雪夫距离的最小值),用于过滤扩圈上限 */ private int ringDistance(ChunkPos chunk, TerritoryModel model) { int distance; int min = Integer.MAX_VALUE; TerritoryState self = model.selfState(); if (self != null) { for (ChunkPos own : self.chunks()) { distance = Math.max(Math.abs(chunk.x() - own.x()), Math.abs(chunk.z() - own.z())); min = Math.min(min, distance); } } for (ChunkPos own : model.sessionClaims()) { distance = Math.max(Math.abs(chunk.x() - own.x()), Math.abs(chunk.z() - own.z())); min = Math.min(min, distance); } return min == Integer.MAX_VALUE ? 0 : min; } /** * Baritone 路径事件回调:记录关键事件,并在“计算失败”时置标志供寻路阶段消费。 * 本回调可能来自 Baritone 事件线程,只做轻量操作(置标志 + 写日志), * 具体处理放在 tick 的寻路阶段(主线程)完成,避免跨线程状态竞争。 */ @Override public void onPathEvent(PathEvent event) { if (event == PathEvent.CALC_STARTED) { this.pathCalcFailed = false; } else if (event == PathEvent.CALC_FAILED) { this.pathCalcFailed = true; } // 只记录关键事件,避免路径中间态刷屏 if (event == PathEvent.CALC_STARTED || event == PathEvent.CALC_FINISHED_NOW_EXECUTING || event == PathEvent.CALC_FAILED || event == PathEvent.AT_GOAL || event == PathEvent.CANCELED) { this.log("\u5bfb\u8def\u4e8b\u4ef6\uff1a" + event); } } /** 监听器回调:开垦进度更新(0-100%) */ @Override public void onProgress(int percent) { if (!this.isRunning()) { return; } this.lastProgress = percent; this.status = "\u5f00\u62d3\u4e2d " + percent + "%"; } /** * 监听器回调:开垦成功(带具体坐标)。 * 将新占领区块加入模型与数据服务,累计完成数并进入 SUCCESS 状态。 */ @Override public void onSuccess(ChunkPos chunk, int blockX, int blockZ) { if (!this.isRunning()) { return; } if (chunk == null) { chunk = this.target; } if (chunk == null) { return; } // 去重:同一区块只计一次 if (!this.claimedSet.add(chunk)) { return; } this.model.addSessionClaim(chunk); this.dataService.addSessionClaim(chunk); ++this.completedClaims; this.target = chunk; this.lastProgress = 100; this.log("\u5360\u9886\u6210\u529f " + chunk.x() + "," + chunk.z() + "\uff08\u533a\u5757 " + chunk.x() + "," + chunk.z() + " / \u4e16\u754c " + blockX + "," + blockZ + "\uff09"); this.setState(State.SUCCESS, "\u7586\u571f\u5df2\u5f00\u62d3 " + chunk.x() + "," + chunk.z()); } /** 监听器回调:开垦成功(无具体坐标的通用消息),按当前目标区块记录 */ @Override public void onGenericSuccess() { if (!this.isRunning()) { return; } if (this.target == null) { return; } // 去重:同一区块只计一次 if (!this.claimedSet.add(this.target)) { return; } this.model.addSessionClaim(this.target); this.dataService.addSessionClaim(this.target); ++this.completedClaims; this.lastProgress = 100; this.log("\u5360\u9886\u6210\u529f " + this.target.x() + "," + this.target.z()); this.setState(State.SUCCESS, "\u7586\u571f\u5df2\u5f00\u62d3 " + this.target.x() + "," + this.target.z()); } /** 获取玩家当前所在区块(由方块坐标换算) */ private ChunkPos playerChunk(MinecraftClient client) { BlockPos pos = client.player.getBlockPos(); return ChunkPos.fromBlock(pos.getX(), pos.getZ()); } /** 判断区块是否已被任何邦国占领(含己方与本次会话) */ private boolean isClaimedByOthers(ChunkPos chunk) { return this.dataService.model().allClaimedChunks().contains(chunk); } /** 返回区块的四个正方向邻居 */ private List cardinalNeighbors(ChunkPos chunk) { return List.of(new ChunkPos(chunk.x() + 1, chunk.z()), new ChunkPos(chunk.x() - 1, chunk.z()), new ChunkPos(chunk.x(), chunk.z() + 1), new ChunkPos(chunk.x(), chunk.z() - 1)); } /** * 自动游泳逻辑(每帧调用): * 仅在玩家完全没入水中(有溺水风险)时自动跳跃,保持浮在水面。 * 玩家在水面上正常游泳时不做任何干预,避免干扰 Baritone 的水平寻路。 */ private void tickAutoSwim(MinecraftClient client) { if (!this.isRunning()) { return; } PlayerEntity player = client.player; if (player == null) { return; } // 只有完全没入水中(头部在水面以下)才有溺水风险,此时才需要跳跃上浮 if (player.isSubmergedInWater()) { player.jump(); } } /** * 自动进食逻辑(每帧调用): * 开启 autoEat 且饥饿值低于阈值时暂停寻路并自动吃背包里的食物, * 吃饱后恢复任务。返回 true 表示本帧被进食占用。 */ private boolean tickAutoFood(MinecraftClient client) { if (!this.config.autoEat || !this.isRunning()) { this.eating = false; return false; } int foodLevel = client.player.getHungerManager().getFoodLevel(); if (this.eating) { if (client.player.isUsingItem()) { this.status = "\u81ea\u52a8\u8fdb\u98df\u4e2d"; return true; } if (foodLevel >= 20 || foodLevel > this.config.eatThreshold) { this.eating = false; this.resumeAfterEating(); this.log("\u81ea\u52a8\u8fdb\u98df\u5b8c\u6210"); return false; } return this.beginEating(client, foodLevel); } if (foodLevel > this.config.eatThreshold) { return false; } return this.beginEating(client, foodLevel); } /** 开始进食:在背包中找食物并切换主手使用;找不到食物则提示并返回 false */ private boolean beginEating(MinecraftClient client, int foodLevel) { if (client.player.isUsingItem()) { this.eating = true; this.status = "\u81ea\u52a8\u8fdb\u98df\u4e2d"; return true; } int slot = this.findFoodSlot(client.player.getInventory()); if (slot < 0) { this.status = "\u9965\u997f\u4f46\u80cc\u5305\u91cc\u6ca1\u6709\u98df\u7269"; return false; } this.baritoneBridge.cancel(); client.player.getInventory().selectedSlot = slot; client.interactionManager.interactItem(client.player, Hand.MAIN_HAND); this.eating = true; this.status = "\u81ea\u52a8\u8fdb\u98df\u4e2d"; this.log("\u81ea\u52a8\u8fdb\u98df\uff0c\u9965\u997f\u503c " + foodLevel); return true; } /** 在背包前 36 格中查找第一个可食用的物品槽位(含 FOOD 组件),找不到返回 -1 */ private int findFoodSlot(PlayerInventory inventory) { for (int slot = 0; slot < 36; ++slot) { ItemStack stack = inventory.getStack(slot); if (stack.isEmpty() || stack.getComponents().get(DataComponentTypes.FOOD) == null) continue; return slot; } return -1; } /** 进食完成后恢复任务:若仍在寻路阶段则重新发起寻路 */ private void resumeAfterEating() { this.eating = false; if (this.state == State.PATHING && this.target != null) { this.stageStartedMillis = System.currentTimeMillis(); this.baritoneBridge.pathTo(this.target); } } /** 更新状态与状态描述文本 */ private void setState(State newState, String newStatus) { this.state = newState; this.status = newStatus; } /** 追加一条带时间戳的日志,超出上限时移除最旧记录 */ private void log(String message) { Deque deque = this.logs; synchronized (deque) { this.logs.addLast("[" + LocalTime.now().format(LOG_TIME_FORMAT) + "] " + message); while (this.logs.size() > MAX_LOGS) { this.logs.removeFirst(); } } } /** * 导出运行日志到文件,返回文件路径;失败时返回 null。 * 文件写入 Fabric 配置目录下的 {@code autostore-debug.log}, * 内容包含当前状态摘要(状态、目标、玩家区块、队列、进度)与全部详细日志。 * * @return 导出文件的绝对路径,或 null(写入失败时) */ public Path exportLogs() { Path path = FabricLoader.getInstance().getConfigDir().resolve("autostore-debug.log"); try { StringBuilder sb = new StringBuilder(8192); sb.append("==== AutoStore 调试日志 ====\n"); sb.append("状态: ").append(this.state).append(" / ").append(this.status).append('\n'); sb.append("目标区块: ").append(this.target == null ? "无" : this.target.x() + "," + this.target.z()).append('\n'); sb.append("玩家区块: ").append(this.playerChunk.x()).append(',').append(this.playerChunk.z()).append('\n'); sb.append("队列: ").append(this.queue.size()).append(" | 已占领: ").append(this.completedClaims) .append(" | 重试: ").append(this.retryCount).append(" | 最近进度: ").append(this.lastProgress).append("%\n\n"); synchronized (this.logs) { for (String line : this.logs) { sb.append(line).append('\n'); } } Files.writeString(path, sb.toString(), StandardCharsets.UTF_8); return path; } catch (IOException e) { return null; } } /** * 扩地任务状态机枚举: *
    *
  • IDLE:空闲(未启动)
  • *
  • PLANNING:正在生成扩地计划
  • *
  • PATHING:正在寻路前往目标区块
  • *
  • WAITING:已到达,等待开垦完成
  • *
  • SUCCESS:当前区块开垦成功
  • *
  • FAIL:当前区块开垦失败/超时
  • *
  • NEXT:准备处理下一个目标
  • *
  • COMPLETE:全部目标完成
  • *
  • STOPPED:被手动停止
  • *
*/ @Environment(value=EnvType.CLIENT) public static enum State { IDLE, PLANNING, PATHING, WAITING, SUCCESS, FAIL, NEXT, COMPLETE, STOPPED; } }