- Baritone设置:限制下落高度(10格)与轻微挖方块惩罚,减少走入矿洞,同时保留峡谷/高山通行 - 寻路超时且玩家在地下时自动执行 /u return 回城自救,等待10秒传送后重新规划 - 新增目标有效性校验:寻路/等待/重试前实时校验目标是否仍与己方领土接壤,不接壤立即跳过并重新规划,修复走到不接壤区块死等bug
754 行
31 KiB
Java
754 行
31 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 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.minecraft.block.BlockState;
|
|
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)
|
|
/**
|
|
* 自动扩地控制器:扩地任务的核心状态机。
|
|
*
|
|
* <p>工作流程:规划(PLANNING)→ 寻路(PATHING)→ 等待开垦(WAITING)→ 成功/失败(SUCCESS/FAIL)→ 下一个目标(NEXT)→ 完成(COMPLETE)。
|
|
* 每帧由 {@link #tick()} 驱动,通过 Baritone 自动寻路到目标区块,
|
|
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
|
|
*/
|
|
public final class ExpansionController
|
|
implements SubtitleChatMonitor.Listener {
|
|
/** 日志环形缓冲区的最大条数 */
|
|
private static final int MAX_LOGS = 80;
|
|
/** 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;
|
|
/** 回城自救后重新规划时保留重试计数,避免无限循环 */
|
|
private boolean keepRetryCount;
|
|
|
|
public ExpansionController(TerritoryDataService dataService) {
|
|
this.dataService = dataService;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* 启动自动扩地任务。
|
|
*
|
|
* <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;
|
|
}
|
|
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);
|
|
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;
|
|
}
|
|
case 9: {
|
|
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.keepRetryCount) {
|
|
this.retryCount = 0;
|
|
}
|
|
this.keepRetryCount = false;
|
|
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());
|
|
}
|
|
|
|
/**
|
|
* 寻路阶段每帧逻辑:到达目标区块后进入等待开垦状态;
|
|
* 若寻路超时则重试,超过重试次数则跳过该目标。
|
|
*/
|
|
private void tickPathing() {
|
|
if (this.target == null) {
|
|
this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a");
|
|
return;
|
|
}
|
|
// 目标已失效(被占领或不再与己方领土接壤):立即跳过,避免继续寻路到无效区块
|
|
if (this.skipIfTargetInvalid()) {
|
|
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");
|
|
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 if (this.isUnderground()) {
|
|
// 玩家被困地下(矿洞等):执行 /u return 回城自救,避免被困
|
|
this.log("\u5bfb\u8def\u8d85\u65f6\u4e14\u73a9\u5bb6\u5728\u5730\u4e0b\uff0c\u6267\u884c /u return \u56de\u57ce\u81ea\u6551 " + this.retryCount + "/" + this.config.retries);
|
|
this.returnToCapital("\u56de\u57ce\u540e\u91cd\u65b0\u5bfb\u8def " + this.target.x() + "," + this.target.z());
|
|
} else {
|
|
this.log("\u5bfb\u8def\u8d85\u65f6\uff0c\u91cd\u8bd5 " + this.retryCount + "/" + this.config.retries);
|
|
this.retrySameTarget("\u91cd\u8bd5\u524d\u5f80 " + this.target.x() + "," + this.target.z());
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 等待开垦阶段每帧逻辑:玩家离开目标区块则重新寻路;等待超时则重试或跳过当前目标 */
|
|
private void tickWaiting() {
|
|
if (this.target == null) {
|
|
this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a");
|
|
return;
|
|
}
|
|
// 目标已失效(被占领或不再与己方领土接壤):立即跳过,避免被困在无效区块
|
|
if (this.skipIfTargetInvalid()) {
|
|
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.skipIfTargetInvalid()) {
|
|
return;
|
|
}
|
|
this.baritoneBridge.cancel();
|
|
this.stageStartedMillis = System.currentTimeMillis();
|
|
this.baritoneBridge.pathTo(this.target);
|
|
this.setState(State.PATHING, statusText);
|
|
}
|
|
|
|
/**
|
|
* 若当前目标已失效(被其他邦国占领,或不再与己方领土接壤),
|
|
* 则取消寻路、记录日志、跳过该目标并进入 NEXT 状态,返回 true。
|
|
* 用于修复"寻路走到与领土不接壤区块后死等"的 bug:
|
|
* 地图数据刷新后,若目标周围被他人占领导致不再接壤,立即放弃该目标并重新规划。
|
|
*/
|
|
private boolean skipIfTargetInvalid() {
|
|
if (this.target == null) {
|
|
return false;
|
|
}
|
|
if (!this.isTargetStillValid()) {
|
|
this.baritoneBridge.cancel();
|
|
this.log("\u76ee\u6807\u5df2\u5931\u6548\uff08\u88ab\u5360\u9886\u6216\u4e0d\u518d\u4e0e\u9886\u571f\u63a5\u58e4\uff09\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
|
|
this.setState(State.NEXT, "\u76ee\u6807\u5931\u6548\uff0c\u91cd\u65b0\u89c4\u5212");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 判断当前目标是否仍然有效:
|
|
* 未被任何邦国占领,且与己方领土(含本次会话新占领)至少一个区块接壤。
|
|
* 使用实时地图数据校验,避免寻路到与领土不接壤的孤立区块。
|
|
*/
|
|
private boolean isTargetStillValid() {
|
|
if (this.target == null) {
|
|
return false;
|
|
}
|
|
TerritoryModel m = this.dataService.model();
|
|
if (m.allClaimedChunks().contains(this.target)) {
|
|
return false;
|
|
}
|
|
HashSet<ChunkPos> own = new HashSet<ChunkPos>(m.sessionClaims());
|
|
TerritoryState self = m.selfState();
|
|
if (self != null) {
|
|
own.addAll(self.chunks());
|
|
}
|
|
return this.ownNeighbors(this.target, own) > 0;
|
|
}
|
|
|
|
/** 统计区块周围(上下左右)属于给定集合的邻居数量 */
|
|
private int ownNeighbors(ChunkPos chunk, Set<ChunkPos> own) {
|
|
int count = 0;
|
|
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
|
|
if (own.contains(neighbor)) {
|
|
++count;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/** 判断区块是否属于己方领土(含本次会话新占领) */
|
|
private boolean isInOwnTerritory(ChunkPos chunk) {
|
|
TerritoryModel m = this.dataService.model();
|
|
if (m.sessionClaims().contains(chunk)) {
|
|
return true;
|
|
}
|
|
TerritoryState self = m.selfState();
|
|
return self != null && self.chunks().contains(chunk);
|
|
}
|
|
|
|
/**
|
|
* 判断玩家是否被困在地下(头顶上方连续数格被实心方块遮挡)。
|
|
* 用于寻路超时时识别"走进矿洞"的情况,从而触发回城自救。
|
|
*/
|
|
private boolean isUnderground() {
|
|
MinecraftClient client = MinecraftClient.getInstance();
|
|
if (client == null || client.player == null || client.world == null) {
|
|
return false;
|
|
}
|
|
BlockPos pos = client.player.getBlockPos();
|
|
for (int y = pos.getY() + 1; y <= pos.getY() + 3; ++y) {
|
|
BlockPos above = new BlockPos(pos.getX(), y, pos.getZ());
|
|
BlockState state = client.world.getBlockState(above);
|
|
if (state.isOpaqueFullCube()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 回城自救:取消当前寻路,发送 /u return 命令回到国家王城区块,
|
|
* 并进入 RETURNING 状态等待回城完成后重新规划扩地任务。
|
|
* 用于避免玩家被困在地下矿洞或无效区块。
|
|
*/
|
|
private void returnToCapital(String statusText) {
|
|
this.baritoneBridge.cancel();
|
|
this.stageStartedMillis = System.currentTimeMillis();
|
|
this.keepRetryCount = true;
|
|
this.baritoneBridge.sendCommand("u return");
|
|
this.setState(State.RETURNING, "\u56de\u57ce\u81ea\u6551\u4e2d");
|
|
this.log("\u6267\u884c /u return \u56de\u57ce\uff0c\u7b49\u5f85\u8fd4\u56de\u738b\u57ce\u540e\u7ee7\u7eed");
|
|
}
|
|
|
|
/**
|
|
* 回城阶段每帧逻辑:等待玩家回到己方领土(含王城)后重新规划。
|
|
* /u return 传送需要约 10 秒,因此前 10 秒不检查位置,避免传送未完成就误判;
|
|
* 若等待超时(60 秒)仍未回到领土,则跳过当前目标继续,避免死等。
|
|
*/
|
|
private void tickReturning() {
|
|
long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L;
|
|
if (elapsedSeconds >= 60) {
|
|
this.log("\u56de\u57ce\u7b49\u5f85\u8d85\u65f6\uff0c\u8df3\u8fc7\u5f53\u524d\u76ee\u6807\u7ee7\u7eed");
|
|
this.setState(State.FAIL, "\u56de\u57ce\u8d85\u65f6\uff0c\u8df3\u8fc7\u76ee\u6807");
|
|
return;
|
|
}
|
|
// /u return 传送约需 10 秒,等待传送完成后才检查是否回到己方领土
|
|
if (elapsedSeconds >= 10 && this.isInOwnTerritory(this.playerChunk)) {
|
|
this.log("\u5df2\u56de\u5230\u5df1\u65b9\u9886\u571f\uff0c\u91cd\u65b0\u89c4\u5212\u6269\u5730\u4efb\u52a1");
|
|
this.setState(State.NEXT, "\u5df2\u56de\u57ce\uff0c\u91cd\u65b0\u89c4\u5212");
|
|
}
|
|
}
|
|
|
|
/** 成功后的过渡:进入 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;
|
|
}
|
|
|
|
/** 监听器回调:开垦进度更新(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<ChunkPos> 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<String> deque = this.logs;
|
|
synchronized (deque) {
|
|
this.logs.addLast(System.currentTimeMillis() + " " + message);
|
|
while (this.logs.size() > 80) {
|
|
this.logs.removeFirst();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 扩地任务状态机枚举:
|
|
* <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,
|
|
COMPLETE,
|
|
STOPPED,
|
|
RETURNING;
|
|
|
|
}
|
|
}
|
|
|