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

这个提交包含在:
lyizai
2026-08-18 20:40:04 +08:00
父节点 7a45e6f676
当前提交 e08dbb8104
共修改 4 个文件,包含 276 行新增和 43 行删除
@@ -7,8 +7,12 @@ package autostore.client.bridge;
import baritone.api.BaritoneAPI; import baritone.api.BaritoneAPI;
import baritone.api.IBaritone; import baritone.api.IBaritone;
import baritone.api.Settings; import baritone.api.Settings;
import baritone.api.event.events.PathEvent;
import baritone.api.event.listener.AbstractGameEventListener;
import autostore.client.config.ExpansionConfig; import autostore.client.config.ExpansionConfig;
import autostore.client.territory.ChunkPos; import autostore.client.territory.ChunkPos;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
@@ -22,6 +26,25 @@ import net.fabricmc.api.Environment;
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
public final class BaritoneBridge { public final class BaritoneBridge {
/**
* 寻路事件回调:用于把 Baritone 的路径事件(如计算失败、到达目标)转发给控制器,
* 使控制器能及时识别“目标不可达”并跳过,而不是傻等到超时。
*/
public interface PathListener {
void onPathEvent(PathEvent event);
}
/** 已注册的路径事件监听器集合(线程安全) */
private final Set<PathListener> pathListeners = ConcurrentHashMap.newKeySet();
/** 是否已向 Baritone 注册事件监听器(只注册一次) */
private volatile boolean eventListenerRegistered;
/** 注册一个路径事件监听器;同时确保已向 Baritone 事件总线挂载转发器 */
public void addPathListener(PathListener listener) {
this.pathListeners.add(listener);
this.primary();
}
/** /**
* 将模组配置同步到 Baritone 的全局设置。 * 将模组配置同步到 Baritone 的全局设置。
* *
@@ -75,12 +98,30 @@ public final class BaritoneBridge {
/** /**
* 获取 Baritone 主实例;加载失败或异常时返回 null。 * 获取 Baritone 主实例;加载失败或异常时返回 null。
* 首次成功获取时,向 Baritone 事件总线注册一个转发器,
* 把路径事件分发给所有已注册的 {@link PathListener}。
* *
* @return 主 Baritone 实例,或 null * @return 主 Baritone 实例,或 null
*/ */
private IBaritone primary() { private IBaritone primary() {
try { try {
return BaritoneAPI.getProvider().getPrimaryBaritone(); IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone();
if (baritone != null && !this.eventListenerRegistered) {
this.eventListenerRegistered = true;
baritone.getGameEventHandler().registerEventListener(new AbstractGameEventListener() {
@Override
public void onPathEvent(PathEvent event) {
for (PathListener listener : BaritoneBridge.this.pathListeners) {
try {
listener.onPathEvent(event);
} catch (Throwable ignored) {
// 单个监听器异常不影响其他监听器
}
}
}
});
}
return baritone;
} catch (Throwable ignored) { } catch (Throwable ignored) {
return null; return null;
} }
@@ -1,6 +1,5 @@
/* /*
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152), * AutoStore 客户端命令:注册 /autostore 系列命令。
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
*/ */
package autostore.client.command; package autostore.client.command;
@@ -10,11 +9,9 @@ import autostore.client.controller.ExpansionController;
import autostore.client.territory.ChunkPos; import autostore.client.territory.ChunkPos;
import autostore.client.territory.TerritoryState; import autostore.client.territory.TerritoryState;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
@@ -37,6 +34,7 @@ import net.minecraft.text.Text;
* <li>/autostore state &lt;邦国名&gt; —— 设置目标邦国</li> * <li>/autostore state &lt;邦国名&gt; —— 设置目标邦国</li>
* <li>/autostore strategy &lt;1-4&gt; —— 设置扩地策略</li> * <li>/autostore strategy &lt;1-4&gt; —— 设置扩地策略</li>
* <li>/autostore rings &lt;层数&gt; —— 设置扩圈上限(0 为持续)</li> * <li>/autostore rings &lt;层数&gt; —— 设置扩圈上限(0 为持续)</li>
* <li>/autostore log —— 导出运行日志到文件</li>
* </ul> * </ul>
*/ */
public final class AutoStoreCommands { public final class AutoStoreCommands {
@@ -45,50 +43,108 @@ public final class AutoStoreCommands {
/** 注册全部 /autostore 子命令到命令分发器 */ /** 注册全部 /autostore 子命令到命令分发器 */
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, AutoStoreModClient mod) { public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, AutoStoreModClient mod) {
dispatcher.register((((((((((ClientCommandManager.literal((String)"autostore").then(ClientCommandManager.literal((String)"gui").executes(ctx -> { LiteralArgumentBuilder<FabricClientCommandSource> root = ClientCommandManager.literal("autostore");
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u6253\u5f00 AutoStore GUI"));
// 打开 GUI
root.then(ClientCommandManager.literal("gui").executes(ctx -> {
ctx.getSource().sendFeedback(Text.literal("已打开 AutoStore GUI"));
mod.openGui(); mod.openGui();
return 1; return 1;
}))).then((ClientCommandManager.literal((String)"start").executes(ctx -> AutoStoreCommands.start(ctx.getSource(), mod, mod.config().targetCount, mod.config().strategy))).then(ClientCommandManager.argument((String)"count", IntegerArgumentType.integer()).executes(ctx -> AutoStoreCommands.start(ctx.getSource(), mod, IntegerArgumentType.getInteger(ctx, (String)"count"), mod.config().strategy))))).then(ClientCommandManager.literal((String)"stop").executes(ctx -> { }));
// 开始扩地(可选数量参数)
root.then(ClientCommandManager.literal("start")
.executes(ctx -> start(ctx.getSource(), mod, mod.config().targetCount, mod.config().strategy))
.then(ClientCommandManager.argument("count", IntegerArgumentType.integer())
.executes(ctx -> start(ctx.getSource(), mod, IntegerArgumentType.getInteger(ctx, "count"), mod.config().strategy))));
// 停止扩地
root.then(ClientCommandManager.literal("stop").executes(ctx -> {
mod.controller().stop(); mod.controller().stop();
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u505c\u6b62\u81ea\u52a8\u6269\u5730")); ctx.getSource().sendFeedback(Text.literal("已停止自动扩地"));
return 1; return 1;
}))).then(ClientCommandManager.literal((String)"status").executes(ctx -> AutoStoreCommands.status(ctx.getSource(), mod)))).then(ClientCommandManager.literal((String)"refresh").executes(ctx -> { }));
// 查看状态
root.then(ClientCommandManager.literal("status").executes(ctx -> status(ctx.getSource(), mod)));
// 刷新地图
root.then(ClientCommandManager.literal("refresh").executes(ctx -> {
mod.dataService().refreshAsync(); mod.dataService().refreshAsync();
(ctx.getSource()).sendFeedback(Text.literal((String)"\u6b63\u5728\u5237\u65b0\u90a6\u56fd\u5730\u56fe")); ctx.getSource().sendFeedback(Text.literal("正在刷新邦国地图"));
return 1; return 1;
}))).then(ClientCommandManager.literal((String)"auto").executes(ctx -> { }));
TerritoryState state = mod.dataService().model().findContainingState(AutoStoreCommands.playerChunk(ctx.getSource()));
// 自动识别邦国
root.then(ClientCommandManager.literal("auto").executes(ctx -> {
TerritoryState state = mod.dataService().model().findContainingState(playerChunk(ctx.getSource()));
if (state == null) { if (state == null) {
(ctx.getSource()).sendError(Text.literal((String)"\u65e0\u6cd5\u8bc6\u522b\u5f53\u524d\u90a6\u56fd")); ctx.getSource().sendError(Text.literal("无法识别当前邦国"));
} else { } else {
mod.config().stateName = state.name(); mod.config().stateName = state.name();
mod.saveConfig(); mod.saveConfig();
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u81ea\u52a8\u8bc6\u522b\u90a6\u56fd\uff1a" + state.name()))); ctx.getSource().sendFeedback(Text.literal("已自动识别邦国:" + state.name()));
} }
return 1; return 1;
}))).then(ClientCommandManager.literal((String)"state").then(ClientCommandManager.argument((String)"name", StringArgumentType.greedyString()).executes(ctx -> { }));
String name;
mod.config().stateName = name = StringArgumentType.getString(ctx, (String)"name"); // 设置邦国
root.then(ClientCommandManager.literal("state").then(ClientCommandManager.argument("name", StringArgumentType.greedyString()).executes(ctx -> {
String name = StringArgumentType.getString(ctx, "name");
mod.config().stateName = name;
mod.saveConfig(); mod.saveConfig();
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u8bbe\u7f6e\u90a6\u56fd\uff1a" + name))); ctx.getSource().sendFeedback(Text.literal("已设置邦国:" + name));
return 1;
})))).then(ClientCommandManager.literal((String)"strategy").then(ClientCommandManager.argument((String)"strategy", IntegerArgumentType.integer((int)1, (int)4)).executes(ctx -> {
int strategy;
mod.config().strategy = strategy = IntegerArgumentType.getInteger(ctx, (String)"strategy");
mod.saveConfig();
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u8bbe\u7f6e\u6269\u5f20\u7b56\u7565\uff1a" + strategy)));
return 1;
})))).then(ClientCommandManager.literal((String)"rings").then(ClientCommandManager.argument((String)"rings", IntegerArgumentType.integer((int)0)).executes(ctx -> {
int rings;
mod.config().maxRings = rings = IntegerArgumentType.getInteger(ctx, (String)"rings");
mod.saveConfig();
(ctx.getSource()).sendFeedback(Text.literal((rings <= 0 ? "\u5df2\u8bbe\u7f6e\u6301\u7eed\u6269\u5730" : "\u5df2\u8bbe\u7f6e\u6269\u5708\u4e0a\u9650\uff1a" + rings)));
return 1;
})))).then(ClientCommandManager.literal((String)"help").executes(ctx -> {
(ctx.getSource()).sendFeedback(Text.literal((String)"/autostore help \u663e\u793a\u5e2e\u52a9\n/autostore gui \u6253\u5f00 GUI\uff08\u6216\u6309 F6 \u5feb\u6377\u952e\uff09\n/autostore start \u5f00\u59cb\u6269\u5730\n/autostore start <\u6570\u91cf> \u6307\u5b9a\u76ee\u6807\u6570\u91cf\uff0c-1 \u4e3a\u6301\u7eed\n/autostore stop \u505c\u6b62\n/autostore status \u67e5\u770b\u72b6\u6001\n/autostore refresh \u5237\u65b0\u5730\u56fe\n/autostore auto \u81ea\u52a8\u8bc6\u522b\u90a6\u56fd\n/autostore state <\u90a6\u56fd\u540d> \u8bbe\u7f6e\u90a6\u56fd\n/autostore strategy <1|2|3|4> \u8bbe\u7f6e\u7b56\u7565\n/autostore rings <\u5c42\u6570> \u8bbe\u7f6e\u5708\u6570\u4e0a\u9650"));
return 1; return 1;
}))); })));
// 设置策略
root.then(ClientCommandManager.literal("strategy").then(ClientCommandManager.argument("strategy", IntegerArgumentType.integer(1, 4)).executes(ctx -> {
int strategy = IntegerArgumentType.getInteger(ctx, "strategy");
mod.config().strategy = strategy;
mod.saveConfig();
ctx.getSource().sendFeedback(Text.literal("已设置扩张策略:" + strategy));
return 1;
})));
// 设置圈数上限
root.then(ClientCommandManager.literal("rings").then(ClientCommandManager.argument("rings", IntegerArgumentType.integer(0)).executes(ctx -> {
int rings = IntegerArgumentType.getInteger(ctx, "rings");
mod.config().maxRings = rings;
mod.saveConfig();
ctx.getSource().sendFeedback(Text.literal(rings <= 0 ? "已设置持续扩地" : "已设置扩圈上限:" + rings));
return 1;
})));
// 帮助
root.then(ClientCommandManager.literal("help").executes(ctx -> {
ctx.getSource().sendFeedback(Text.literal(
"/autostore help 显示帮助\n" +
"/autostore gui 打开 GUI(或按 Ctrl+R 快捷键)\n" +
"/autostore start 开始扩地\n" +
"/autostore start <数量> 指定目标数量,-1 为持续\n" +
"/autostore stop 停止\n" +
"/autostore status 查看状态\n" +
"/autostore refresh 刷新地图\n" +
"/autostore auto 自动识别邦国\n" +
"/autostore state <邦国名> 设置邦国\n" +
"/autostore strategy <1|2|3|4> 设置策略\n" +
"/autostore rings <层数> 设置圈数上限\n" +
"/autostore log 导出日志"));
return 1;
}));
// 导出日志
root.then(ClientCommandManager.literal("log").executes(ctx -> {
java.nio.file.Path path = mod.controller().exportLogs();
if (path != null) {
ctx.getSource().sendFeedback(Text.literal("日志已导出:" + path));
} else {
ctx.getSource().sendError(Text.literal("日志导出失败"));
}
return 1;
}));
dispatcher.register(root);
} }
/** /autostore start 命令处理:复制当前配置并覆盖目标数量与策略,然后启动扩地 */ /** /autostore start 命令处理:复制当前配置并覆盖目标数量与策略,然后启动扩地 */
@@ -100,15 +156,20 @@ public final class AutoStoreCommands {
mod.config().strategy = strategy; mod.config().strategy = strategy;
mod.saveConfig(); mod.saveConfig();
boolean started = mod.controller().start(config); boolean started = mod.controller().start(config);
source.sendFeedback(Text.literal((started ? "\u5f00\u59cb\u81ea\u52a8\u6269\u5730" : "\u542f\u52a8\u5931\u8d25\uff1a" + mod.controller().status()))); source.sendFeedback(Text.literal(started ? "开始自动扩地" : "启动失败:" + mod.controller().status()));
return started ? 1 : 0; return started ? 1 : 0;
} }
/** /autostore status 命令处理:汇总状态、进度、队列、策略与邦国信息并反馈 */ /** /autostore status 命令处理:汇总状态、进度、队列、策略与邦国信息并反馈 */
private static int status(FabricClientCommandSource source, AutoStoreModClient mod) { private static int status(FabricClientCommandSource source, AutoStoreModClient mod) {
ExpansionController controller = mod.controller(); ExpansionController controller = mod.controller();
String text = "\u72b6\u6001\uff1a" + controller.status() + " | \u8fdb\u5ea6\uff1a" + controller.completedClaims() + (mod.config().targetCount > 0 ? "/" + mod.config().targetCount : "/\u6301\u7eed") + " | \u961f\u5217\uff1a" + controller.queuedTargets() + " | \u7b56\u7565\uff1a" + mod.config().strategy + " | \u90a6\u56fd\uff1a" + (mod.config().stateName.isEmpty() ? "\u81ea\u52a8" : mod.config().stateName); String text = "状态:" + controller.status()
source.sendFeedback(Text.literal((String)text)); + " | 进度:" + controller.completedClaims()
+ (mod.config().targetCount > 0 ? "/" + mod.config().targetCount : "/持续")
+ " | 队列:" + controller.queuedTargets()
+ " | 策略:" + mod.config().strategy
+ " | 邦国:" + (mod.config().stateName.isEmpty() ? "自动" : mod.config().stateName);
source.sendFeedback(Text.literal(text));
return 1; return 1;
} }
@@ -118,4 +179,3 @@ public final class AutoStoreCommands {
return ChunkPos.fromBlock(pos.getX(), pos.getZ()); return ChunkPos.fromBlock(pos.getX(), pos.getZ());
} }
} }
@@ -12,6 +12,13 @@ import autostore.client.territory.ChunkPos;
import autostore.client.territory.TerritoryDataService; import autostore.client.territory.TerritoryDataService;
import autostore.client.territory.TerritoryModel; import autostore.client.territory.TerritoryModel;
import autostore.client.territory.TerritoryState; 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.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
@@ -20,6 +27,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.util.Hand; import net.minecraft.util.Hand;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerInventory;
@@ -37,9 +45,11 @@ import net.minecraft.component.DataComponentTypes;
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。 * 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
*/ */
public final class ExpansionController public final class ExpansionController
implements SubtitleChatMonitor.Listener { implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
/** 日志环形缓冲区的最大条数 */ /** 日志环形缓冲区的最大条数(含导出用详细日志,故调大) */
private static final int MAX_LOGS = 80; private static final int MAX_LOGS = 2000;
/** 日志时间戳格式(时:分:秒) */
private static final DateTimeFormatter LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
/** Baritone 寻路桥接(负责自动移动) */ /** Baritone 寻路桥接(负责自动移动) */
private final BaritoneBridge baritoneBridge = new BaritoneBridge(); private final BaritoneBridge baritoneBridge = new BaritoneBridge();
/** 扩地规划器(决定下一个目标区块) */ /** 扩地规划器(决定下一个目标区块) */
@@ -82,9 +92,13 @@ implements SubtitleChatMonitor.Listener {
private final Set<ChunkPos> claimedSet = new HashSet<>(); private final Set<ChunkPos> claimedSet = new HashSet<>();
/** 是否正在自动进食 */ /** 是否正在自动进食 */
private boolean eating; private boolean eating;
/** Baritone 最近一次路径计算是否失败(目标不可达);由事件监听设置,寻路阶段消费 */
private volatile boolean pathCalcFailed;
public ExpansionController(TerritoryDataService dataService) { public ExpansionController(TerritoryDataService dataService) {
this.dataService = dataService; this.dataService = dataService;
// 注册路径事件监听,及时识别“目标不可达”并跳过(避免大山/悬崖地形傻等超时)
this.baritoneBridge.addPathListener(this);
} }
public SubtitleChatMonitor monitor() { public SubtitleChatMonitor monitor() {
@@ -283,6 +297,7 @@ implements SubtitleChatMonitor.Listener {
this.lastProgress = 0; this.lastProgress = 0;
this.baritoneBridge.pathTo(this.target); this.baritoneBridge.pathTo(this.target);
this.setState(State.PATHING, "\u6b63\u5728\u524d\u5f80\u76ee\u6807\u533a\u5757 " + this.target.x() + "," + this.target.z()); 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")) { if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
return; 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)) { if (this.playerChunk.equals(this.target)) {
this.baritoneBridge.cancel(); this.baritoneBridge.cancel();
this.stageStartedMillis = System.currentTimeMillis(); this.stageStartedMillis = System.currentTimeMillis();
this.setState(State.WAITING, "\u5df2\u5230\u8fbe\uff0c\u7b49\u5f85\u9886\u5730\u5f00\u62d3"); 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; return;
} }
long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L; long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L;
@@ -350,6 +377,10 @@ implements SubtitleChatMonitor.Listener {
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) { if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
return; return;
} }
// 目标已不再与己方领土接壤:立即跳过,避免在无效区块上干等开垦
if (this.skipIfTargetNotAdjacent()) {
return;
}
// 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去 // 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去
if (!this.playerChunk.equals(this.target)) { 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()); 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; 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 状态,准备处理下一个目标 */ /** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */
private void tickSuccess() { private void tickSuccess() {
this.setState(State.NEXT, "\u5df2\u5b8c\u6210 " + this.completedClaims + (this.config.targetCount > 0 ? "/" + this.config.targetCount : "/\u6301\u7eed")); 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; 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%) */ /** 监听器回调:开垦进度更新(0-100%) */
@Override @Override
public void onProgress(int percent) { public void onProgress(int percent) {
@@ -645,13 +734,42 @@ implements SubtitleChatMonitor.Listener {
private void log(String message) { private void log(String message) {
Deque<String> deque = this.logs; Deque<String> deque = this.logs;
synchronized (deque) { synchronized (deque) {
this.logs.addLast(System.currentTimeMillis() + " " + message); this.logs.addLast("[" + LocalTime.now().format(LOG_TIME_FORMAT) + "] " + message);
while (this.logs.size() > 80) { while (this.logs.size() > MAX_LOGS) {
this.logs.removeFirst(); 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> * <ul>
@@ -106,6 +106,8 @@ extends Screen {
// 左上角外部链接按钮:Git 仓库与官网(点击在系统浏览器打开) // 左上角外部链接按钮:Git 仓库与官网(点击在系统浏览器打开)
this.addDrawableChild(ButtonWidget.builder(Text.literal((String)"Git \u4ed3\u5e93"), button -> this.openUrl(GIT_REPO_URL)).dimensions(this.mapX, 8, 70, 16).build()); this.addDrawableChild(ButtonWidget.builder(Text.literal((String)"Git \u4ed3\u5e93"), button -> this.openUrl(GIT_REPO_URL)).dimensions(this.mapX, 8, 70, 16).build());
this.addDrawableChild(ButtonWidget.builder(Text.literal((String)"\u5b98\u7f51"), button -> this.openUrl(HOMEPAGE_URL)).dimensions(this.mapX + 76, 8, 50, 16).build()); this.addDrawableChild(ButtonWidget.builder(Text.literal((String)"\u5b98\u7f51"), button -> this.openUrl(HOMEPAGE_URL)).dimensions(this.mapX + 76, 8, 50, 16).build());
// 导出日志按钮:把当前运行日志写入文件,方便排查寻路/坐标/状态问题
this.addDrawableChild(ButtonWidget.builder(Text.literal((String)"\u5bfc\u51fa\u65e5\u5fd7"), button -> this.exportLogs()).dimensions(this.mapX + 132, 8, 70, 16).build());
ButtonWidget startButton = ButtonWidget.builder(Text.literal((String)"\u5f00\u59cb\u81ea\u52a8\u6269\u5730"), button -> this.start()).dimensions(topX, 34, 154, 30).build(); ButtonWidget startButton = ButtonWidget.builder(Text.literal((String)"\u5f00\u59cb\u81ea\u52a8\u6269\u5730"), button -> this.start()).dimensions(topX, 34, 154, 30).build();
ButtonWidget stopButton = ButtonWidget.builder(Text.literal((String)"\u505c\u6b62"), button -> this.controller.stop()).dimensions(topX + 160, 34, 74, 30).build(); ButtonWidget stopButton = ButtonWidget.builder(Text.literal((String)"\u505c\u6b62"), button -> this.controller.stop()).dimensions(topX + 160, 34, 74, 30).build();
this.addDrawableChild(startButton); this.addDrawableChild(startButton);
@@ -174,6 +176,18 @@ extends Screen {
} }
} }
/** 导出运行日志到文件,并在聊天栏提示导出结果与路径 */
private void exportLogs() {
java.nio.file.Path path = this.controller.exportLogs();
if (this.client != null && this.client.player != null) {
if (path != null) {
this.client.player.sendMessage(Text.literal("\u65e5\u5fd7\u5df2\u5bfc\u51fa\uff1a" + path), false);
} else {
this.client.player.sendMessage(Text.literal("\u65e5\u5fd7\u5bfc\u51fa\u5931\u8d25"), false);
}
}
}
/** 创建整数输入框(初始值为 value,最多 12 位) */ /** 创建整数输入框(初始值为 value,最多 12 位) */
private TextFieldWidget numberField(int value, int x, int y, int width) { private TextFieldWidget numberField(int value, int x, int y, int width) {
TextFieldWidget field = new TextFieldWidget(this.textRenderer, x, y, width, 20, Text.literal((String)"")); TextFieldWidget field = new TextFieldWidget(this.textRenderer, x, y, width, 20, Text.literal((String)""));