- 新增 GoalXZAltitude 寻路目标:启发式成本 = 水平距离 - 高度权重×y,y 越大优先级越高、y 越小施惩罚,引导寻路优先走高 - ExpansionConfig 新增 altitudePreference/altitudeWeight 配置(默认开/0.1),BaritoneBridge 按配置选择原生 GoalXZ 或高度偏好 Goal - 寻路/重试前先检查目标区块是否仍与己方领土接壤(startPathing/retrySameTarget),不接壤立即跳过 - 寻路超时不再直接传送:重试耗尽后先往边上走两步自救(每轮重试原目标,最多 sideStepMaxRounds=3 轮),多轮无效才按连续超时阈值回城或切换目标
1016 行
48 KiB
Java
1016 行
48 KiB
Java
/*
|
||
* 本文件由 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.util.math.Vec3d;
|
||
import net.minecraft.client.MinecraftClient;
|
||
import net.minecraft.component.DataComponentTypes;
|
||
import net.minecraft.text.Text;
|
||
|
||
@Environment(value=EnvType.CLIENT)
|
||
/**
|
||
* 自动扩地控制器:扩地任务的核心状态机。
|
||
*
|
||
* <p>工作流程:规划(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<ChunkPos> queue = new ArrayDeque<ChunkPos>();
|
||
/** 运行日志队列(线程安全) */
|
||
private final Deque<String> logs = new ArrayDeque<String>();
|
||
/** 当前扩地配置(启动时从请求配置复制) */
|
||
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<ChunkPos> claimedSet = new HashSet<>();
|
||
/** 是否正在自动进食 */
|
||
private boolean eating;
|
||
/** Baritone 最近一次路径计算是否失败(目标不可达);由事件监听设置,寻路阶段消费 */
|
||
private volatile boolean pathCalcFailed;
|
||
/** 等待开拓时段自动开始:不在 11:00-22:00 时点击开始会暂存配置,进入时段后自动启动(null 表示无等待任务) */
|
||
private ExpansionConfig pendingStart;
|
||
/** 连续寻路超时累计时长(毫秒):多次寻路超时逐次累加,达到阈值触发 /u return 回城传送 */
|
||
private long consecutiveTimeoutMillis;
|
||
/** 发送 /u return 回城指令的时间戳(RETURNING 状态使用,用于等待传送完成) */
|
||
private long returnStartMillis;
|
||
/** 当前目标已触发"往边上走两步"的次数(每次重试耗尽先走两步再试,达上限才切换目标/回城) */
|
||
private int sideStepCount;
|
||
/** 是否正在"往边上走两步"阶段(手动向目标连线垂直方向走开约 2.5 格再重试) */
|
||
private boolean sideStepping;
|
||
/** 边走两步的方向符号(+1 左侧 / -1 右侧),被挡时换另一侧 */
|
||
private int sideStepSign = 1;
|
||
/** 边走两步的整体超时截止时间戳(毫秒) */
|
||
private long sideStepDeadlineMillis;
|
||
/** 最近一次换向/开始的时间戳(毫秒,用于判断该方向是否卡住) */
|
||
private long sideStepSinceMillis;
|
||
/** 边走两步开始时的玩家方块坐标(用于判断是否已走开约 2.5 格) */
|
||
private BlockPos sideStepStartPos;
|
||
|
||
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<String> logs() {
|
||
Deque<String> deque = this.logs;
|
||
synchronized (deque) {
|
||
return new ArrayList<String>(this.logs);
|
||
}
|
||
}
|
||
|
||
public boolean isRunning() {
|
||
return this.state != State.IDLE && this.state != State.COMPLETE && this.state != State.STOPPED;
|
||
}
|
||
|
||
/** @return 是否处于"等待开拓时段自动开始"状态 */
|
||
public boolean isWaitingForClaimTime() {
|
||
return this.pendingStart != null;
|
||
}
|
||
|
||
/**
|
||
* 判断当前现实时间(本地时区)是否在允许开拓的时间段内。
|
||
* 仅 11:00(含)到 22:00(不含)允许开拓,其余时段服务器会拒绝开拓。
|
||
*
|
||
* @return true 表示当前在允许开拓时段内
|
||
*/
|
||
public static boolean isWithinClaimTime() {
|
||
int hour = LocalTime.now().getHour();
|
||
return hour >= CLAIM_START_HOUR && hour < CLAIM_END_HOUR;
|
||
}
|
||
|
||
/**
|
||
* 启动自动扩地任务。
|
||
*
|
||
* <p>前置校验:必须在服务器世界中、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 允许开拓,其余时段服务器会拒绝开拓(等待超时)。
|
||
// 不在时段时不再直接拒绝,而是登记"等待自动开始":进入时段后由 tick() 自动启动。
|
||
if (!ExpansionController.isWithinClaimTime()) {
|
||
this.pendingStart = requestedConfig.copy();
|
||
this.setState(State.IDLE, "\u7b49\u5f85\u5f00\u62d3\u65f6\u6bb5\uff0c11:00 \u81ea\u52a8\u5f00\u59cb");
|
||
this.log("\u4e0d\u5728\u5f00\u62d3\u65f6\u6bb5\u5185\uff0811:00-22:00\uff09\uff0c\u5df2\u767b\u8bb0\u81ea\u52a8\u5f00\u59cb\uff0c\u5f53\u524d\u73b0\u5b9e\u65f6\u95f4 " + LocalTime.now().format(LOG_TIME_FORMAT));
|
||
// 给玩家发送聊天栏提示,避免玩家未打开 GUI 时看不到原因
|
||
client.player.sendMessage(Text.literal("[AutoStore] \u4e0d\u5728\u5f00\u62d3\u65f6\u6bb5\u5185\uff0811:00-22:00\uff09\uff0c\u5df2\u767b\u8bb0\uff1a\u8fdb\u5165\u65f6\u6bb5\u540e\u81ea\u52a8\u5f00\u59cb"), false);
|
||
return true;
|
||
}
|
||
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.consecutiveTimeoutMillis = 0L;
|
||
this.sideStepCount = 0;
|
||
this.sideStepping = false;
|
||
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.pendingStart != null) {
|
||
this.pendingStart = null;
|
||
this.setState(State.IDLE, "\u5df2\u53d6\u6d88\u81ea\u52a8\u5f00\u59cb");
|
||
this.log("\u5df2\u53d6\u6d88\u7b49\u5f85\u81ea\u52a8\u5f00\u59cb");
|
||
return;
|
||
}
|
||
if (this.state == State.IDLE || this.state == State.COMPLETE || this.state == State.STOPPED) {
|
||
return;
|
||
}
|
||
this.baritoneBridge.cancel();
|
||
// 若正处于"往边上走两步"阶段,先释放手动按下的前进键
|
||
if (this.sideStepping) {
|
||
this.stopSideStep(false);
|
||
}
|
||
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.pendingStart != null && ExpansionController.isWithinClaimTime()) {
|
||
ExpansionConfig pending = this.pendingStart;
|
||
this.pendingStart = null;
|
||
this.log("\u5df2\u8fdb\u5165\u5f00\u62d3\u65f6\u6bb5\uff0c\u81ea\u52a8\u5f00\u59cb");
|
||
this.start(pending);
|
||
}
|
||
// 现实时间限制:运行中超出 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");
|
||
client.player.sendMessage(Text.literal("[AutoStore] \u5df2\u5230\u5f00\u62d3\u65f6\u9650\u5916\uff0811:00-22:00\uff09\uff0c\u4efb\u52a1\u505c\u6b62"), false);
|
||
return;
|
||
}
|
||
if (this.tickAutoFood(client)) {
|
||
return;
|
||
}
|
||
this.tickAutoSwim(client);
|
||
// 处于"往边上走两步"阶段:每帧驱动手动横向行走,走开后自动重试原目标
|
||
if (this.sideStepping) {
|
||
this.tickSideStep();
|
||
return;
|
||
}
|
||
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;
|
||
}
|
||
case 7: {
|
||
this.tickReturning();
|
||
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<ChunkPos> 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() {
|
||
// 寻路前先校验目标区块是否仍与己方领土接壤(地图异步刷新后可能已不接壤),不接壤则直接跳过
|
||
if (this.skipIfTargetNotAdjacent()) {
|
||
return;
|
||
}
|
||
this.retryCount = 0;
|
||
this.sideStepCount = 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 报告路径计算失败(目标不可达,如被悬崖/高山/水体完全隔断)。
|
||
// 不立即跳过:CALC_FAILED 常因瞬时能力限制或起点卡位误报,先重新寻路一次
|
||
//(相当于“往边上走两步再算一遍”),重试耗尽才跳过当前目标。
|
||
if (this.pathCalcFailed) {
|
||
this.pathCalcFailed = false;
|
||
++this.retryCount;
|
||
if (this.retryCount >= this.config.retries) {
|
||
// 重试耗尽:不直接切换目标/回城,先"往边上走两步"再重试原目标;可反复多轮,达上限才交给回城/切换
|
||
if (this.sideStepCount < this.config.sideStepMaxRounds) {
|
||
this.startSideStep();
|
||
} else {
|
||
this.giveUpTarget("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09");
|
||
}
|
||
} else {
|
||
this.log("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09\uff0c\u91cd\u65b0\u5bfb\u8def " + this.retryCount + "/" + this.config.retries);
|
||
this.dataService.refreshAsync();
|
||
this.retrySameTarget("\u5bfb\u8def\u5931\u8d25\uff0c\u91cd\u65b0\u5bfb\u8def");
|
||
}
|
||
return;
|
||
}
|
||
// 目标已不再与己方领土接壤(地图刷新后边界变化):立即跳过,避免走到无效区块
|
||
if (this.skipIfTargetNotAdjacent()) {
|
||
return;
|
||
}
|
||
if (this.playerChunk.equals(this.target)) {
|
||
this.baritoneBridge.cancel();
|
||
// 成功到达目标:寻路不再卡死,清零连续超时计数
|
||
this.consecutiveTimeoutMillis = 0L;
|
||
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();
|
||
// 累计"连续寻路超时"时长:连续多次寻路都超时说明玩家被地形/缝隙卡死走不到目标,
|
||
// 达到阈值后作为发送 /u return 回城(约 10 秒后传送到可寻路位置)的最后手段,平时不打断重试/走两步。
|
||
this.consecutiveTimeoutMillis += (long)this.config.waitTimeoutSeconds * 1000L;
|
||
if (this.retryCount < this.config.retries) {
|
||
// 未耗尽:重新寻路(而非回城),取消旧路径并重新计算,往往"在边上走走"就能走出困境
|
||
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");
|
||
} else if (this.sideStepCount < this.config.sideStepMaxRounds) {
|
||
// 重试耗尽:先不切换目标/回城,用"往边上走两步"再重试一次(走开障碍后重新寻路原目标);可反复多轮
|
||
this.startSideStep();
|
||
} else {
|
||
// 多轮走两步也未能脱困:连续超时达到阈值则回城传送重新开始,否则切换目标
|
||
this.giveUpTarget("\u5bfb\u8def\u8d85\u65f6");
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 连续寻路超时达到阈值时触发回城:
|
||
* 取消当前寻路,向服务器发送 /u return 指令(约 10 秒后自动传送回可寻路的安全位置),
|
||
* 随后进入 RETURNING 状态等待传送完成,传送后由 {@link #tickReturning()} 重新规划并从头开始。
|
||
*/
|
||
private void sendReturnCommand() {
|
||
this.baritoneBridge.cancel();
|
||
MinecraftClient client = MinecraftClient.getInstance();
|
||
if (client != null && client.player != null) {
|
||
this.log("\u8fde\u7eed\u5bfb\u8def\u8d85\u65f6\u8d85\u8fc7 " + this.config.returnThresholdSeconds + " \u79d2\uff0c\u53d1\u9001 /u return \u56de\u57ce\uff0c10 \u79d2\u540e\u81ea\u52a8\u4f20\u9001");
|
||
// 通过玩家网络处理器发送聊天指令 /u return(Yarn 映射:sendChatCommand 自动带上前缀斜杠)
|
||
client.player.networkHandler.sendChatCommand("u return");
|
||
client.player.sendMessage(Text.literal("[AutoStore] \u8fde\u7eed\u5bfb\u8def\u8d85\u65f6\uff0c\u5df2\u53d1\u9001 /u return\uff0c\u7b49\u5f85\u4f20\u9001\u540e\u91cd\u65b0\u6269\u5730"), false);
|
||
} else {
|
||
this.log("\u8fde\u7eed\u5bfb\u8def\u8d85\u65f6\uff0c\u4f46\u73a9\u5bb6\u4e0d\u5728\u4e16\u754c\uff0c\u8df3\u8fc7");
|
||
}
|
||
this.returnStartMillis = System.currentTimeMillis();
|
||
this.setState(State.RETURNING, "\u7b49\u5f85\u56de\u57ce\u4f20\u9001\uff0810 \u79d2\u540e\u81ea\u52a8\u4f20\u9001\uff09");
|
||
}
|
||
|
||
/**
|
||
* 回城传送等待阶段每帧逻辑:
|
||
* 发送 /u return 后服务器约 10 秒把玩家传送回安全位置。等待 {@code returnWaitSeconds}
|
||
* 秒(覆盖传送延迟并留余量)后,清零连续超时计数并重新规划扩地计划,
|
||
* 从最高优先级区块(队首)重新开始寻路。
|
||
*/
|
||
private void tickReturning() {
|
||
long elapsedMillis = System.currentTimeMillis() - this.returnStartMillis;
|
||
if (elapsedMillis < (long)this.config.returnWaitSeconds * 1000L) {
|
||
return;
|
||
}
|
||
this.consecutiveTimeoutMillis = 0L;
|
||
this.log("\u56de\u57ce\u4f20\u9001\u5b8c\u6210\uff0c\u91cd\u65b0\u89c4\u5212\u5e76\u4ece\u6700\u9ad8\u4f18\u5148\u7ea7\u533a\u5757\u5f00\u59cb");
|
||
// 重新规划:清空队列重新生成计划,从队首(最高优先级)区块开始寻路
|
||
this.plan();
|
||
}
|
||
|
||
/**
|
||
* 目标寻路重试耗尽时的脱困策略入口:
|
||
* 取消当前寻路,改为手动朝"目标连线垂直方向"走开约 2.5 格(左右各试一侧),
|
||
* 走开后自动重新寻路原目标;走不开/超时则再走两步或交给 {@link #giveUpTarget} 回城/切换目标。
|
||
* 每个目标最多触发 {@code sideStepMaxRounds} 轮(sideStepCount 在 startPathing 时重置)。
|
||
*/
|
||
private void startSideStep() {
|
||
this.baritoneBridge.cancel();
|
||
MinecraftClient client = MinecraftClient.getInstance();
|
||
this.sideStepping = true;
|
||
++this.sideStepCount;
|
||
this.sideStepSign = 1; // 默认先朝目标连线左侧走
|
||
this.sideStepStartPos = client != null && client.player != null ? client.player.getBlockPos() : null;
|
||
this.sideStepDeadlineMillis = System.currentTimeMillis() + 10000L; // 整体最多走 10 秒
|
||
this.sideStepSinceMillis = System.currentTimeMillis();
|
||
this.log("\u5bfb\u8def\u91cd\u8bd5\u8017\u5c3d\uff0c\u5f00\u59cb\u5f80\u8fb9\u4e0a\u8d70\u4e24\u6b65\u518d\u91cd\u8bd5\u539f\u76ee\u6807\uff08\u7b2c " + this.sideStepCount + "/" + this.config.sideStepMaxRounds + " \u8f6e\uff09");
|
||
this.setState(State.PATHING, "\u5f80\u8fb9\u4e0a\u8d70\u4e24\u6b65...");
|
||
}
|
||
|
||
/** 走两步阶段每帧逻辑:朝目标连线垂直方向按住前进键行走,走开约 2.5 格后重试原目标 */
|
||
private void tickSideStep() {
|
||
MinecraftClient client = MinecraftClient.getInstance();
|
||
if (client == null || client.player == null || this.sideStepStartPos == null || this.target == null) {
|
||
this.stopSideStep(false);
|
||
return;
|
||
}
|
||
// 整体超时:往边上走两步也未能脱困
|
||
if (System.currentTimeMillis() >= this.sideStepDeadlineMillis) {
|
||
this.stopSideStep(false);
|
||
this.giveUpTarget("\u5f80\u8fb9\u4e0a\u8d70\u4e24\u6b65\u8d85\u65f6");
|
||
return;
|
||
}
|
||
// 目标连线方向(水平),取垂直向量作为横向移动方向(左为 +,右为 -)
|
||
Vec3d playerPos = client.player.getPos();
|
||
Vec3d dirToTarget = new Vec3d(this.target.centerBlockX() + 0.5, playerPos.y, this.target.centerBlockZ() + 0.5)
|
||
.subtract(playerPos).normalize();
|
||
Vec3d sideDir = new Vec3d(-dirToTarget.z, 0.0, dirToTarget.x).multiply(this.sideStepSign);
|
||
// 面朝横向移动方向并按住前进键
|
||
float yaw = (float)Math.toDegrees(Math.atan2(sideDir.z, sideDir.x)) - 90.0f;
|
||
client.player.setYaw(yaw);
|
||
client.options.forwardKey.setPressed(true);
|
||
// 已走开足够距离(约 2.5 格):停止并重试原目标
|
||
if (client.player.getBlockPos().getSquaredDistance(this.sideStepStartPos) >= 6.25) {
|
||
this.stopSideStep(true);
|
||
this.retrySameTarget("\u5f80\u8fb9\u4e0a\u8d70\u4e24\u6b65\u5b8c\u6210\uff0c\u91cd\u65b0\u5bfb\u8def");
|
||
return;
|
||
}
|
||
// 卡住检查:同一方向 5 秒没移动就换另一侧;两侧都试过仍不动则放弃
|
||
if (System.currentTimeMillis() - this.sideStepSinceMillis > 5000L) {
|
||
if (this.sideStepSign != -1) {
|
||
this.sideStepSign = -1;
|
||
this.sideStepSinceMillis = System.currentTimeMillis();
|
||
this.log("\u5f80\u8fb9\u4e0a\u8d70\u88ab\u6321\uff0c\u53d6\u53cd\u65b9\u5411\u7ee7\u7eed");
|
||
} else {
|
||
this.stopSideStep(false);
|
||
this.giveUpTarget("\u5f80\u8fb9\u4e0a\u8d70\u4e24\u4fa7\u5747\u88ab\u6321");
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 结束走两步阶段:释放手动按下的前进键;success 表示已走开并准备重试原目标 */
|
||
private void stopSideStep(boolean success) {
|
||
this.sideStepping = false;
|
||
MinecraftClient client = MinecraftClient.getInstance();
|
||
if (client != null && client.options != null) {
|
||
client.options.forwardKey.setPressed(false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 放弃当前目标(重试与走两步均已尝试)的统一出口:
|
||
* 若连续寻路超时累计已达到阈值,则发送 /u return 回城传送并重新规划;
|
||
* 否则记录日志并进入 FAIL 状态切换下一个目标。
|
||
*/
|
||
private void giveUpTarget(String reason) {
|
||
if (this.consecutiveTimeoutMillis >= (long)this.config.returnThresholdSeconds * 1000L) {
|
||
this.sendReturnCommand();
|
||
return;
|
||
}
|
||
if (this.target != null) {
|
||
this.log(reason + "\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
|
||
} else {
|
||
this.log(reason);
|
||
}
|
||
this.setState(State.FAIL, reason);
|
||
}
|
||
|
||
/** 等待开垦阶段每帧逻辑:玩家离开目标区块则重新寻路;等待超时则重试或跳过当前目标 */
|
||
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;
|
||
}
|
||
if (this.skipIfTargetNotAdjacent()) {
|
||
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。
|
||
*
|
||
* <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>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。
|
||
* 实现上直接查询四个邻居是否命中集合,零集合复制——
|
||
* 旧实现每帧把数千个己方区块复制进临时 HashSet,是持续卡顿与内存压力的来源之一。</p>
|
||
*/
|
||
private boolean isAdjacentToOwn(ChunkPos chunk) {
|
||
TerritoryModel model = this.dataService.model();
|
||
TerritoryState self = model.selfState();
|
||
Set<ChunkPos> ownChunks = self == null ? null : self.chunks();
|
||
Set<ChunkPos> sessionClaims = model.sessionClaims();
|
||
int x = chunk.x();
|
||
int z = chunk.z();
|
||
return this.isOwnAt(ownChunks, sessionClaims, x + 1, z)
|
||
|| this.isOwnAt(ownChunks, sessionClaims, x - 1, z)
|
||
|| this.isOwnAt(ownChunks, sessionClaims, x, z + 1)
|
||
|| this.isOwnAt(ownChunks, sessionClaims, x, z - 1);
|
||
}
|
||
|
||
/** 判断指定坐标的区块是否属于己方领土或本次会话新占领 */
|
||
private boolean isOwnAt(Set<ChunkPos> ownChunks, Set<ChunkPos> sessionClaims, int x, int z) {
|
||
ChunkPos probe = new ChunkPos(x, z);
|
||
return ownChunks != null && ownChunks.contains(probe) || sessionClaims.contains(probe);
|
||
}
|
||
|
||
/** 成功后的过渡:进入 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;
|
||
}
|
||
// 仅在任务运行中记录关键事件,避免空闲时 Baritone 高频 CANCELED/中间态刷屏
|
||
if (!this.isRunning()) {
|
||
return;
|
||
}
|
||
// 只记录关键事件,且跳过高频的 CANCELED(空闲/重寻路时每秒触发多次,无排查价值)
|
||
if (event == PathEvent.CALC_STARTED || event == PathEvent.CALC_FINISHED_NOW_EXECUTING
|
||
|| event == PathEvent.CALC_FAILED || event == PathEvent.AT_GOAL) {
|
||
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.dataService.refreshAsync();
|
||
++this.completedClaims;
|
||
// 开垦成功:寻路/等待恢复正常,清零连续超时计数
|
||
this.consecutiveTimeoutMillis = 0L;
|
||
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;
|
||
}
|
||
// 开垦只能发生在玩家所在区块:校验玩家确实位于目标区块上。
|
||
// 通用成功文案采用模糊匹配,可能被服务器广播的其他玩家开垦消息误触发;
|
||
// 不校验会把未占领的目标错误标记为"已占领"并跳过,直到很久之后才被地图数据纠正(v1.2.1 修复)。
|
||
if (!this.playerChunk.equals(this.target)) {
|
||
return;
|
||
}
|
||
// 去重:同一区块只计一次
|
||
if (!this.claimedSet.add(this.target)) {
|
||
return;
|
||
}
|
||
this.model.addSessionClaim(this.target);
|
||
this.dataService.addSessionClaim(this.target);
|
||
// 占领成功后立即触发一次地图刷新,尽快确认该区块
|
||
this.dataService.refreshAsync();
|
||
++this.completedClaims;
|
||
// 开垦成功:寻路/等待恢复正常,清零连续超时计数
|
||
this.consecutiveTimeoutMillis = 0L;
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 自动游泳逻辑(每帧调用):
|
||
* 仅在玩家完全没入水中(有溺水风险)时自动跳跃,保持浮在水面。
|
||
* 玩家在水面上正常游泳时不做任何干预,避免干扰 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.sideStepping) {
|
||
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<String> 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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 扩地任务状态机枚举:
|
||
* <ul>
|
||
* <li>IDLE:空闲(未启动)</li>
|
||
* <li>PLANNING:正在生成扩地计划</li>
|
||
* <li>PATHING:正在寻路前往目标区块</li>
|
||
* <li>WAITING:已到达,等待开垦完成</li>
|
||
* <li>SUCCESS:当前区块开垦成功</li>
|
||
* <li>FAIL:当前区块开垦失败/超时</li>
|
||
* <li>NEXT:准备处理下一个目标</li>
|
||
* <li>COMPLETE:全部目标完成</li>
|
||
* <li>STOPPED:被手动停止</li>
|
||
* </ul>
|
||
*/
|
||
@Environment(value=EnvType.CLIENT)
|
||
public static enum State {
|
||
IDLE,
|
||
PLANNING,
|
||
PATHING,
|
||
WAITING,
|
||
SUCCESS,
|
||
FAIL,
|
||
NEXT,
|
||
RETURNING,
|
||
COMPLETE,
|
||
STOPPED;
|
||
|
||
}
|
||
}
|
||
|