v1.2.4: 寻路重试耗尽先往边上走两步再重试原目标,仍失败才回城或切换目标

这个提交包含在:
lyizai
2026-08-21 16:25:00 +08:00
父节点 08e15ffc9b
当前提交 a0963747b6
@@ -33,6 +33,7 @@ import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.component.DataComponentTypes; import net.minecraft.component.DataComponentTypes;
import net.minecraft.text.Text; import net.minecraft.text.Text;
@@ -99,6 +100,18 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
private long consecutiveTimeoutMillis; private long consecutiveTimeoutMillis;
/** 发送 /u return 回城指令的时间戳(RETURNING 状态使用,用于等待传送完成) */ /** 发送 /u return 回城指令的时间戳(RETURNING 状态使用,用于等待传送完成) */
private long returnStartMillis; private long returnStartMillis;
/** 当前目标是否已使用过"往边上走两步"策略(重试耗尽时先用它,避免重复;再次耗尽才切换目标) */
private boolean sideStepAttempted;
/** 是否正在"往边上走两步"阶段(手动向目标连线垂直方向走开约 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) { public ExpansionController(TerritoryDataService dataService) {
this.dataService = dataService; this.dataService = dataService;
@@ -216,6 +229,8 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
this.completedClaims = 0; this.completedClaims = 0;
this.retryCount = 0; this.retryCount = 0;
this.consecutiveTimeoutMillis = 0L; this.consecutiveTimeoutMillis = 0L;
this.sideStepAttempted = false;
this.sideStepping = false;
this.queue.clear(); this.queue.clear();
this.claimedSet.clear(); this.claimedSet.clear();
this.target = null; this.target = null;
@@ -239,6 +254,10 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
return; return;
} }
this.baritoneBridge.cancel(); this.baritoneBridge.cancel();
// 若正处于"往边上走两步"阶段,先释放手动按下的前进键
if (this.sideStepping) {
this.stopSideStep(false);
}
this.eating = false; this.eating = false;
this.setState(State.STOPPED, "\u5df2\u624b\u52a8\u505c\u6b62"); this.setState(State.STOPPED, "\u5df2\u624b\u52a8\u505c\u6b62");
this.log("\u5df2\u624b\u52a8\u505c\u6b62"); this.log("\u5df2\u624b\u52a8\u505c\u6b62");
@@ -278,6 +297,11 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
return; return;
} }
this.tickAutoSwim(client); this.tickAutoSwim(client);
// 处于"往边上走两步"阶段:每帧驱动手动横向行走,走开后自动重试原目标
if (this.sideStepping) {
this.tickSideStep();
return;
}
switch (this.state.ordinal()) { switch (this.state.ordinal()) {
case 1: { case 1: {
this.plan(); this.plan();
@@ -346,6 +370,7 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
/** 让 Baritone 寻路前往当前目标区块,并进入 PATHING 状态 */ /** 让 Baritone 寻路前往当前目标区块,并进入 PATHING 状态 */
private void startPathing() { private void startPathing() {
this.retryCount = 0; this.retryCount = 0;
this.sideStepAttempted = false;
this.stageStartedMillis = System.currentTimeMillis(); this.stageStartedMillis = System.currentTimeMillis();
this.lastProgress = 0; this.lastProgress = 0;
this.baritoneBridge.pathTo(this.target); this.baritoneBridge.pathTo(this.target);
@@ -355,7 +380,7 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
/** /**
* 寻路阶段每帧逻辑:到达目标区块后进入等待开垦状态; * 寻路阶段每帧逻辑:到达目标区块后进入等待开垦状态;
* 若寻路超时则重新寻路(而非回城),超过重试次数则跳过该目标。 * 若寻路超时则重新寻路;重试耗尽先"往边上走两步"再重试,仍失败则回城(连续超时达阈值)或跳过该目标。
*/ */
private void tickPathing() { private void tickPathing() {
if (this.target == null) { if (this.target == null) {
@@ -373,8 +398,12 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
this.pathCalcFailed = false; this.pathCalcFailed = false;
++this.retryCount; ++this.retryCount;
if (this.retryCount >= this.config.retries) { if (this.retryCount >= this.config.retries) {
this.log("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09\uff0c\u91cd\u8bd5\u8017\u5c3d\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); // 重试耗尽:不直接切换目标,先"往边上走两步"再重试一次;仍失败才交给回城/切换处理
this.setState(State.FAIL, "\u5bfb\u8def\u5931\u8d25"); if (!this.sideStepAttempted) {
this.startSideStep();
} else {
this.giveUpTarget("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09");
}
} else { } 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.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.dataService.refreshAsync();
@@ -401,18 +430,18 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
// 主动刷新地图,尽快同步最新领土状态 // 主动刷新地图,尽快同步最新领土状态
this.dataService.refreshAsync(); this.dataService.refreshAsync();
// 累计"连续寻路超时"时长:连续多次寻路都超时说明玩家被地形/缝隙卡死走不到目标, // 累计"连续寻路超时"时长:连续多次寻路都超时说明玩家被地形/缝隙卡死走不到目标,
// 累计达到阈值后发送 /u return 回城(约 10 秒后传送到可寻路位置),传送后重新规划。 // 达到阈值后作为发送 /u return 回城(约 10 秒后传送到可寻路位置)的最后手段,平时不打断重试/走两步。
this.consecutiveTimeoutMillis += (long)this.config.waitTimeoutSeconds * 1000L; this.consecutiveTimeoutMillis += (long)this.config.waitTimeoutSeconds * 1000L;
if (this.consecutiveTimeoutMillis >= (long)this.config.returnThresholdSeconds * 1000L) { if (this.retryCount < this.config.retries) {
this.sendReturnCommand(); // 未耗尽:重新寻路(而非回城),取消旧路径并重新计算,往往"在边上走走"就能走出困境
} else 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.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"); this.retrySameTarget("\u5bfb\u8def\u8d85\u65f6\uff0c\u91cd\u65b0\u5bfb\u8def");
} else if (!this.sideStepAttempted) {
// 重试耗尽:先不切换目标,用"往边上走两步"再重试一次(走开障碍后重新寻路原目标)
this.startSideStep();
} else {
// 走两步也未能脱困:连续超时达到阈值则回城传送重新开始,否则切换目标
this.giveUpTarget("\u5bfb\u8def\u8d85\u65f6");
} }
} }
} }
@@ -454,6 +483,93 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
this.plan(); this.plan();
} }
/**
* 目标寻路重试耗尽时的脱困策略入口:
* 取消当前寻路,改为手动朝"目标连线垂直方向"走开约 2.5 格(左右各试一侧),
* 走开后自动重新寻路原目标;走不开/超时则交给 {@link #giveUpTarget} 回城或切换目标。
* 每个目标最多触发一次(sideStepAttempted 标志在 startPathing 时重置)。
*/
private void startSideStep() {
this.baritoneBridge.cancel();
MinecraftClient client = MinecraftClient.getInstance();
this.sideStepping = true;
this.sideStepAttempted = true;
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");
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() { private void tickWaiting() {
if (this.target == null) { if (this.target == null) {
@@ -805,10 +921,10 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
return -1; return -1;
} }
/** 进食完成后恢复任务:若仍在寻路阶段则重新发起寻路 */ /** 进食完成后恢复任务:若仍在寻路阶段则重新发起寻路(走两步阶段不抢路) */
private void resumeAfterEating() { private void resumeAfterEating() {
this.eating = false; this.eating = false;
if (this.state == State.PATHING && this.target != null) { if (this.state == State.PATHING && this.target != null && !this.sideStepping) {
this.stageStartedMillis = System.currentTimeMillis(); this.stageStartedMillis = System.currentTimeMillis();
this.baritoneBridge.pathTo(this.target); this.baritoneBridge.pathTo(this.target);
} }