v1.1.9: 增加运行时接壤校验与寻路失败监听,修复不接壤目标与大山寻路超时;新增日志导出

这个提交包含在:
lyizai
2026-08-18 20:40:04 +08:00
父节点 7a45e6f676
当前提交 e08dbb8104
共修改 4 个文件,包含 276 行新增和 43 行删除
@@ -12,6 +12,13 @@ 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;
@@ -20,6 +27,7 @@ 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;
@@ -37,9 +45,11 @@ import net.minecraft.component.DataComponentTypes;
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
*/
public final class ExpansionController
implements SubtitleChatMonitor.Listener {
/** 日志环形缓冲区的最大条数 */
private static final int MAX_LOGS = 80;
implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
/** 日志环形缓冲区的最大条数(含导出用详细日志,故调大) */
private static final int MAX_LOGS = 2000;
/** 日志时间戳格式(时:分:秒) */
private static final DateTimeFormatter LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
/** Baritone 寻路桥接(负责自动移动) */
private final BaritoneBridge baritoneBridge = new BaritoneBridge();
/** 扩地规划器(决定下一个目标区块) */
@@ -82,9 +92,13 @@ implements SubtitleChatMonitor.Listener {
private final Set<ChunkPos> 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() {
@@ -283,6 +297,7 @@ implements SubtitleChatMonitor.Listener {
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");
}
/**
@@ -318,10 +333,22 @@ implements SubtitleChatMonitor.Listener {
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;
@@ -350,6 +377,10 @@ implements SubtitleChatMonitor.Listener {
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());
@@ -411,6 +442,44 @@ implements SubtitleChatMonitor.Listener {
return false;
}
/**
* 若当前目标区块不再与己方领土(含本次会话已占领)的四个正方向接壤,
* 则跳过并进入 NEXT 状态,返回 true。
*
* <p>这是解决“走到不接壤区块”的关键:规划时目标必然接壤,但寻路/等待过程中
* 领土数据会异步刷新(边界变化、假占领被剔除等),导致目标可能变得不接壤。
* 在寻路与等待阶段每帧校验,一旦发现不接壤立即放弃,避免在无效区块上白费时间。</p>
*/
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;
}
/**
* 判断区块是否与己方领土(己方邦国地图区块 + 本次会话新占领区块)接壤。
*
* <p>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。</p>
*/
private boolean isAdjacentToOwn(ChunkPos chunk) {
TerritoryModel model = this.dataService.model();
Set<ChunkPos> own = new HashSet<ChunkPos>();
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"));
@@ -471,6 +540,26 @@ implements SubtitleChatMonitor.Listener {
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) {
@@ -645,13 +734,42 @@ implements SubtitleChatMonitor.Listener {
private void log(String message) {
Deque<String> deque = this.logs;
synchronized (deque) {
this.logs.addLast(System.currentTimeMillis() + " " + message);
while (this.logs.size() > 80) {
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;
}
}
/**
* 扩地任务状态机枚举:
* <ul>