比较提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
385e0d13f1 | ||
|
|
1e99ff39e0 | ||
|
|
c569065054 | ||
|
|
3601bcd40e | ||
|
|
e08dbb8104 | ||
|
|
7a45e6f676 | ||
|
|
d2a52199ae | ||
|
|
1f817199f7 | ||
|
|
9ee4b41630 |
@@ -6,17 +6,21 @@
|
||||
|
||||
- 从远程 `markers.json` 拉取各国领土边界
|
||||
- 四种扩地策略:方形外扩、区域扩地、玩家周围、绕国一圈
|
||||
- 区域扩地采用 S 形扫描:从与现有领土接壤的区块开始,横向/纵向 S 形扫过整个矩形,不沿邦国边界,但保证每个新占领区块都与已占领领土接壤
|
||||
- 自动寻路(Baritone)前往目标区块,寻路优先走地面减少走入矿洞,超时自动 `/u return` 回城自救
|
||||
- 目标有效性校验:寻路/等待/重试前实时校验目标是否仍与己方领土接壤,不接壤立即跳过并重新规划
|
||||
- 区域扩地采用 S 形扫描:从与己方领土接壤的区块开始,横向/纵向 S 形扫过整个矩形,不沿邦国边界,但保证每个新占领区块都与**己方领土**接壤(仅四个正方向,不把其他邦国领土计入接壤)
|
||||
- 自动寻路(Baritone)前往目标区块;监听 Baritone 路径事件,目标不可达(CALC_FAILED,如被悬崖/高山/水体隔断)时立即跳过;寻路超时则重新寻路(不立即回城),重试耗尽才跳过当前目标
|
||||
- 运行时接壤校验:寻路/等待阶段每帧校验目标是否仍与己方领土接壤,地图刷新导致边界变化时立即跳过
|
||||
- 目标有效性校验:目标若已被其他邦国占领则立即跳过
|
||||
- 现实时间限制:仅 11:00-22:00(本地现实时间)允许开拓,其余时段启动会被拒绝、运行中到点自动停止
|
||||
- 自动进食(饥饿时自动吃背包食物)
|
||||
- 实时地图 GUI(通过 Ctrl+R 打开)
|
||||
- 监听聊天/字幕消息判断开垦结果
|
||||
- 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞
|
||||
- 日志导出:GUI 右上角“导出日志”按钮,或 `/autostore log` 命令,记录寻路、坐标、状态、路径事件等
|
||||
|
||||
## 安装
|
||||
|
||||
1. 安装 Fabric Loader 0.16.14+ 和 Fabric API
|
||||
2. 将 `autostore-1.1.7.jar` 放入 `.minecraft/mods/` 目录
|
||||
2. 将 `autostore-1.1.9.jar` 放入 `.minecraft/mods/` 目录
|
||||
3. 启动游戏,按 `Ctrl+R` 打开 GUI
|
||||
|
||||
## 构建
|
||||
@@ -26,4 +30,4 @@
|
||||
./build_mod.sh
|
||||
```
|
||||
|
||||
输出文件:`autostore-1.1.7.jar`
|
||||
输出文件:`autostore-1.1.9.jar`
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8
|
||||
loader_version=0.19.3
|
||||
|
||||
# Mod Properties
|
||||
mod_version=1.1.7
|
||||
mod_version=1.1.9
|
||||
maven_group=autostore
|
||||
archives_base_name=autostore
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ package autostore.client.bridge;
|
||||
import baritone.api.BaritoneAPI;
|
||||
import baritone.api.IBaritone;
|
||||
import baritone.api.Settings;
|
||||
import baritone.api.pathing.goals.Goal;
|
||||
import baritone.api.pathing.goals.GoalXZ;
|
||||
import baritone.api.event.events.PathEvent;
|
||||
import baritone.api.event.listener.AbstractGameEventListener;
|
||||
import autostore.client.config.ExpansionConfig;
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
|
||||
/**
|
||||
* Baritone API 封装(桥接层)。
|
||||
@@ -25,6 +26,25 @@ import net.minecraft.client.MinecraftClient;
|
||||
@Environment(EnvType.CLIENT)
|
||||
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 的全局设置。
|
||||
*
|
||||
@@ -37,16 +57,10 @@ public final class BaritoneBridge {
|
||||
settings.allowPlace.value = config.allowPlace;
|
||||
settings.allowParkour.value = config.allowJump;
|
||||
settings.avoidance.value = config.avoidMobs;
|
||||
// 减少走入地下矿洞:限制无水源时的下落高度,避免掉进深坑/矿洞。
|
||||
// 放宽到 10 格,仍允许穿越峡谷、下坡等正常地形,同时避免掉入深坑
|
||||
settings.maxFallHeightNoWater.value = 10;
|
||||
// 轻微提高挖掘方块的额外代价,让寻路尽量绕行地面而非挖洞穿山,
|
||||
// 但不会阻止必要挖掘(高山、峡谷等仍可通行)
|
||||
settings.blockBreakAdditionalPenalty.value = 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 让 Baritone 寻路前往指定区块的中心。
|
||||
* 让 Baritone 寻路前往指定区块(以“到达区块内”为完成条件)。
|
||||
*
|
||||
* @param chunk 目标区块
|
||||
*/
|
||||
@@ -55,9 +69,11 @@ public final class BaritoneBridge {
|
||||
if (baritone == null) {
|
||||
return;
|
||||
}
|
||||
// 使用 GoalXZ 前往区块中心:Baritone 的 GoalXZ 自带约 10 格容差,
|
||||
// 只需到达区块中心附近即可,避免中心点不可达导致寻路卡死
|
||||
baritone.getCustomGoalProcess().setGoalAndPath(new GoalXZ(chunk.centerBlockX(), chunk.centerBlockZ()));
|
||||
// 使用自定义 ChunkGoal:完成条件为“玩家脚下方块落在目标区块内”,
|
||||
// 与控制器 playerChunk.equals(target) 的判定一致。避免 Baritone 自带
|
||||
// GoalXZ(容差 0,精确到区块中心单格)在中心不可达时停在相邻区块,
|
||||
// 造成目标区块错位与反复寻路。
|
||||
baritone.getCustomGoalProcess().setGoalAndPath(new ChunkGoal(chunk));
|
||||
}
|
||||
|
||||
/** 强制取消当前寻路任务。 */
|
||||
@@ -69,18 +85,6 @@ public final class BaritoneBridge {
|
||||
baritone.getPathingBehavior().forceCancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务器聊天命令(如 /u return 回城),command 参数不含前导斜杠。
|
||||
* 用于寻路超时后的回城自救,避免玩家被困在地下矿洞等位置。
|
||||
*/
|
||||
public void sendCommand(String command) {
|
||||
MinecraftClient client = MinecraftClient.getInstance();
|
||||
if (client == null || client.player == null || client.getNetworkHandler() == null) {
|
||||
return;
|
||||
}
|
||||
client.getNetworkHandler().sendChatCommand(command);
|
||||
}
|
||||
|
||||
/** @return 当前是否正在寻路 */
|
||||
public boolean isPathing() {
|
||||
IBaritone baritone = this.primary();
|
||||
@@ -94,12 +98,30 @@ public final class BaritoneBridge {
|
||||
|
||||
/**
|
||||
* 获取 Baritone 主实例;加载失败或异常时返回 null。
|
||||
* 首次成功获取时,向 Baritone 事件总线注册一个转发器,
|
||||
* 把路径事件分发给所有已注册的 {@link PathListener}。
|
||||
*
|
||||
* @return 主 Baritone 实例,或 null
|
||||
*/
|
||||
private IBaritone primary() {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 本文件为 AutoStore 模组自定义的 Baritone 寻路目标。
|
||||
*/
|
||||
package autostore.client.bridge;
|
||||
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import baritone.api.pathing.goals.Goal;
|
||||
|
||||
/**
|
||||
* 以“到达目标区块内”为完成条件的寻路目标。
|
||||
*
|
||||
* <p>修复“目标区块错位”问题:Baritone 自带的 {@code GoalXZ} 容差为 0,
|
||||
* 会要求玩家精确站在区块中心的单个方块上(如 {@code x*16+8, z*16+8})。
|
||||
* 一旦该方块不可达(被树、石头、水、建筑等占据),玩家会被停在区块边缘甚至
|
||||
* 相邻区块,导致玩家所在区块与目标区块错位、反复寻路。</p>
|
||||
*
|
||||
* <p>本目标把完成条件放宽为“玩家脚下方块落在目标区块内”,与控制器中
|
||||
* {@code playerChunk.equals(target)} 的判定完全一致,从而彻底消除
|
||||
* 中心点不可达带来的错位。</p>
|
||||
*/
|
||||
public final class ChunkGoal implements Goal {
|
||||
|
||||
/** 目标区块 X 坐标 */
|
||||
private final int targetX;
|
||||
/** 目标区块 Z 坐标 */
|
||||
private final int targetZ;
|
||||
/** 目标区块中心的方块 X 坐标(用于启发式距离估算) */
|
||||
private final int centerX;
|
||||
/** 目标区块中心的方块 Z 坐标(用于启发式距离估算) */
|
||||
private final int centerZ;
|
||||
|
||||
public ChunkGoal(ChunkPos chunk) {
|
||||
this.targetX = chunk.x();
|
||||
this.targetZ = chunk.z();
|
||||
this.centerX = chunk.centerBlockX();
|
||||
this.centerZ = chunk.centerBlockZ();
|
||||
}
|
||||
|
||||
/** 判断玩家脚下方块是否落在目标区块内 */
|
||||
@Override
|
||||
public boolean isInGoal(int x, int y, int z) {
|
||||
return Math.floorDiv(x, 16) == this.targetX && Math.floorDiv(z, 16) == this.targetZ;
|
||||
}
|
||||
|
||||
/** 启发式:到目标区块中心的水平欧几里得距离(可采纳,保证寻路效率) */
|
||||
@Override
|
||||
public double heuristic(int x, int y, int z) {
|
||||
double dx = x - this.centerX;
|
||||
double dz = z - this.centerZ;
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
|
||||
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
|
||||
* AutoStore 客户端命令:注册 /autostore 系列命令。
|
||||
*/
|
||||
package autostore.client.command;
|
||||
|
||||
@@ -10,11 +9,9 @@ import autostore.client.controller.ExpansionController;
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import autostore.client.territory.TerritoryState;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
|
||||
@@ -37,6 +34,7 @@ import net.minecraft.text.Text;
|
||||
* <li>/autostore state <邦国名> —— 设置目标邦国</li>
|
||||
* <li>/autostore strategy <1-4> —— 设置扩地策略</li>
|
||||
* <li>/autostore rings <层数> —— 设置扩圈上限(0 为持续)</li>
|
||||
* <li>/autostore log —— 导出运行日志到文件</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class AutoStoreCommands {
|
||||
@@ -45,50 +43,108 @@ public final class AutoStoreCommands {
|
||||
|
||||
/** 注册全部 /autostore 子命令到命令分发器 */
|
||||
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, AutoStoreModClient mod) {
|
||||
dispatcher.register((((((((((ClientCommandManager.literal((String)"autostore").then(ClientCommandManager.literal((String)"gui").executes(ctx -> {
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u6253\u5f00 AutoStore GUI"));
|
||||
LiteralArgumentBuilder<FabricClientCommandSource> root = ClientCommandManager.literal("autostore");
|
||||
|
||||
// 打开 GUI
|
||||
root.then(ClientCommandManager.literal("gui").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(Text.literal("已打开 AutoStore GUI"));
|
||||
mod.openGui();
|
||||
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();
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u505c\u6b62\u81ea\u52a8\u6269\u5730"));
|
||||
ctx.getSource().sendFeedback(Text.literal("已停止自动扩地"));
|
||||
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();
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"\u6b63\u5728\u5237\u65b0\u90a6\u56fd\u5730\u56fe"));
|
||||
ctx.getSource().sendFeedback(Text.literal("正在刷新邦国地图"));
|
||||
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) {
|
||||
(ctx.getSource()).sendError(Text.literal((String)"\u65e0\u6cd5\u8bc6\u522b\u5f53\u524d\u90a6\u56fd"));
|
||||
ctx.getSource().sendError(Text.literal("无法识别当前邦国"));
|
||||
} else {
|
||||
mod.config().stateName = state.name();
|
||||
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;
|
||||
}))).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();
|
||||
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u8bbe\u7f6e\u90a6\u56fd\uff1a" + 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"));
|
||||
ctx.getSource().sendFeedback(Text.literal("已设置邦国:" + name));
|
||||
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 命令处理:复制当前配置并覆盖目标数量与策略,然后启动扩地 */
|
||||
@@ -100,15 +156,20 @@ public final class AutoStoreCommands {
|
||||
mod.config().strategy = strategy;
|
||||
mod.saveConfig();
|
||||
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;
|
||||
}
|
||||
|
||||
/** /autostore status 命令处理:汇总状态、进度、队列、策略与邦国信息并反馈 */
|
||||
private static int status(FabricClientCommandSource source, AutoStoreModClient mod) {
|
||||
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);
|
||||
source.sendFeedback(Text.literal((String)text));
|
||||
String text = "状态:" + controller.status()
|
||||
+ " | 进度:" + 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;
|
||||
}
|
||||
|
||||
@@ -118,4 +179,3 @@ public final class AutoStoreCommands {
|
||||
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.TerritoryModel;
|
||||
import autostore.client.territory.TerritoryState;
|
||||
import baritone.api.event.events.PathEvent;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
@@ -20,7 +27,7 @@ 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.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.util.Hand;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.entity.player.PlayerInventory;
|
||||
@@ -28,6 +35,7 @@ import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.component.DataComponentTypes;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
@Environment(value=EnvType.CLIENT)
|
||||
/**
|
||||
@@ -38,9 +46,15 @@ import net.minecraft.component.DataComponentTypes;
|
||||
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
|
||||
*/
|
||||
public final class ExpansionController
|
||||
implements SubtitleChatMonitor.Listener {
|
||||
/** 日志环形缓冲区的最大条数 */
|
||||
private static final int MAX_LOGS = 80;
|
||||
implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
|
||||
/** 日志环形缓冲区的最大条数(含导出用详细日志,故调大) */
|
||||
private static final int MAX_LOGS = 2000;
|
||||
/** 日志时间戳格式(时:分:秒) */
|
||||
private static final DateTimeFormatter LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
/** 允许开拓的现实时间起始小时(含,本地时区) */
|
||||
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();
|
||||
/** 扩地规划器(决定下一个目标区块) */
|
||||
@@ -77,11 +91,13 @@ implements SubtitleChatMonitor.Listener {
|
||||
private final Set<ChunkPos> claimedSet = new HashSet<>();
|
||||
/** 是否正在自动进食 */
|
||||
private boolean eating;
|
||||
/** 回城自救后重新规划时保留重试计数,避免无限循环 */
|
||||
private boolean keepRetryCount;
|
||||
/** Baritone 最近一次路径计算是否失败(目标不可达);由事件监听设置,寻路阶段消费 */
|
||||
private volatile boolean pathCalcFailed;
|
||||
|
||||
public ExpansionController(TerritoryDataService dataService) {
|
||||
this.dataService = dataService;
|
||||
// 注册路径事件监听,及时识别“目标不可达”并跳过(避免大山/悬崖地形傻等超时)
|
||||
this.baritoneBridge.addPathListener(this);
|
||||
}
|
||||
|
||||
public SubtitleChatMonitor monitor() {
|
||||
@@ -124,6 +140,17 @@ implements SubtitleChatMonitor.Listener {
|
||||
return this.state != State.IDLE && this.state != State.COMPLETE && this.state != State.STOPPED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前现实时间(本地时区)是否在允许开拓的时间段内。
|
||||
* 仅 11:00(含)到 22:00(不含)允许开拓,其余时段服务器会拒绝开拓。
|
||||
*
|
||||
* @return true 表示当前在允许开拓时段内
|
||||
*/
|
||||
public static boolean isWithinClaimTime() {
|
||||
int hour = LocalTime.now().getHour();
|
||||
return hour >= CLAIM_START_HOUR && hour < CLAIM_END_HOUR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动扩地任务。
|
||||
*
|
||||
@@ -144,6 +171,15 @@ implements SubtitleChatMonitor.Listener {
|
||||
this.status = "Baritone \u5c1a\u672a\u521d\u59cb\u5316\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5";
|
||||
return false;
|
||||
}
|
||||
// 现实时间限制:仅 11:00-22:00 允许开拓,其余时段服务器会拒绝开拓(等待超时)
|
||||
if (!ExpansionController.isWithinClaimTime()) {
|
||||
this.status = "\u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\uff0811:00-22:00\uff09\uff0c\u65e0\u6cd5\u5f00\u62d3";
|
||||
this.setState(State.IDLE, this.status);
|
||||
this.log("\u542f\u52a8\u5931\u8d25\uff1a\u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\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\u9650\u5185\uff0811:00-22:00\uff09\uff0c\u65e0\u6cd5\u5f00\u62d3"), false);
|
||||
return false;
|
||||
}
|
||||
this.config = requestedConfig.copy();
|
||||
this.model = this.dataService.model();
|
||||
this.playerChunk = this.playerChunk(client);
|
||||
@@ -204,6 +240,14 @@ implements SubtitleChatMonitor.Listener {
|
||||
return;
|
||||
}
|
||||
this.playerChunk = this.playerChunk(client);
|
||||
// 现实时间限制:运行中超出 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;
|
||||
}
|
||||
@@ -233,10 +277,6 @@ implements SubtitleChatMonitor.Listener {
|
||||
this.tickNext();
|
||||
break;
|
||||
}
|
||||
case 9: {
|
||||
this.tickReturning();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,50 +315,58 @@ implements SubtitleChatMonitor.Listener {
|
||||
|
||||
/** 让 Baritone 寻路前往当前目标区块,并进入 PATHING 状态 */
|
||||
private void startPathing() {
|
||||
if (!this.keepRetryCount) {
|
||||
this.retryCount = 0;
|
||||
}
|
||||
this.keepRetryCount = false;
|
||||
this.retryCount = 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.skipIfTargetInvalid()) {
|
||||
// 目标已被他人占领(地图刷新后发现):立即跳过,避免继续寻路到无效区块
|
||||
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
|
||||
return;
|
||||
}
|
||||
// Baritone 报告路径计算失败(目标不可达,如被悬崖/高山/水体完全隔断):立即跳过,不再傻等超时
|
||||
if (this.pathCalcFailed) {
|
||||
this.pathCalcFailed = false;
|
||||
this.log("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
|
||||
this.setState(State.FAIL, "\u5bfb\u8def\u5931\u8d25");
|
||||
return;
|
||||
}
|
||||
// 目标已不再与己方领土接壤(地图刷新后边界变化):立即跳过,避免走到无效区块
|
||||
if (this.skipIfTargetNotAdjacent()) {
|
||||
return;
|
||||
}
|
||||
if (this.playerChunk.equals(this.target)) {
|
||||
this.baritoneBridge.cancel();
|
||||
this.stageStartedMillis = System.currentTimeMillis();
|
||||
this.setState(State.WAITING, "\u5df2\u5230\u8fbe\uff0c\u7b49\u5f85\u9886\u5730\u5f00\u62d3");
|
||||
this.log("\u5df2\u5230\u8fbe\u76ee\u6807 " + this.target.x() + "," + this.target.z() + "\uff0c\u5f00\u59cb\u7b49\u5f85\u5f00\u57a6");
|
||||
return;
|
||||
}
|
||||
long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L;
|
||||
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());
|
||||
// 重新寻路(而非回城):取消旧路径并重新计算,往往“在边上走走”就能走出困境
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,8 +377,12 @@ implements SubtitleChatMonitor.Listener {
|
||||
this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a");
|
||||
return;
|
||||
}
|
||||
// 目标已失效(被占领或不再与己方领土接壤):立即跳过,避免被困在无效区块
|
||||
if (this.skipIfTargetInvalid()) {
|
||||
// 目标已被他人占领(地图刷新后发现):立即跳过,避免被困在已占领区块
|
||||
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
|
||||
return;
|
||||
}
|
||||
// 目标已不再与己方领土接壤:立即跳过,避免在无效区块上干等开垦
|
||||
if (this.skipIfTargetNotAdjacent()) {
|
||||
return;
|
||||
}
|
||||
// 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去
|
||||
@@ -355,9 +407,8 @@ implements SubtitleChatMonitor.Listener {
|
||||
|
||||
/** 重试当前目标:取消旧寻路,重新让 Baritone 前往同一目标区块 */
|
||||
private void retrySameTarget(String statusText) {
|
||||
// 目标已失效(被占领或不再与己方领土接壤):不再重试,直接跳过,
|
||||
// 避免玩家被反复拉回与领土不接壤的孤立区块
|
||||
if (this.skipIfTargetInvalid()) {
|
||||
// 目标已被他人占领(地图刷新后发现):不再重试,直接跳过,避免玩家被困在已占领区块
|
||||
if (this.skipIfTargetClaimed("\u91cd\u8bd5\u65f6\u53d1\u73b0\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
|
||||
return;
|
||||
}
|
||||
this.baritoneBridge.cancel();
|
||||
@@ -367,103 +418,57 @@ implements SubtitleChatMonitor.Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* 若当前目标已失效(被其他邦国占领,或不再与己方领土接壤),
|
||||
* 则取消寻路、记录日志、跳过该目标并进入 NEXT 状态,返回 true。
|
||||
* 用于修复"寻路走到与领土不接壤区块后死等"的 bug:
|
||||
* 地图数据刷新后,若目标周围被他人占领导致不再接壤,立即放弃该目标并重新规划。
|
||||
* 若当前目标区块已被任何邦国占领(含己方与本次会话),
|
||||
* 则记录日志、跳过该目标并进入 NEXT 状态,返回 true。
|
||||
* 用于在地图数据异步刷新后及时识别"已被占领"的区块,避免死循环。
|
||||
*/
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前目标是否仍然有效:
|
||||
* 未被任何邦国占领即可——规划器已保证目标与己方领土相邻,
|
||||
* 运行时无需重复验证邻居关系,避免地图刷新后 own 集合不一致导致误判跳过。
|
||||
* 若当前目标区块不再与己方领土(含本次会话已占领)的四个正方向接壤,
|
||||
* 则跳过并进入 NEXT 状态,返回 true。
|
||||
*
|
||||
* <p>注意:己方领土数据暂时不可用时保守视为有效(可能性极小,仅刷新竞态时发生),
|
||||
* 避免所有目标被误跳过导致玩家原地不动。
|
||||
* <p>这是解决“走到不接壤区块”的关键:规划时目标必然接壤,但寻路/等待过程中
|
||||
* 领土数据会异步刷新(边界变化、假占领被剔除等),导致目标可能变得不接壤。
|
||||
* 在寻路与等待阶段每帧校验,一旦发现不接壤立即放弃,避免在无效区块上白费时间。</p>
|
||||
*/
|
||||
private boolean isTargetStillValid() {
|
||||
if (this.target == null) {
|
||||
return false;
|
||||
}
|
||||
TerritoryModel m = this.dataService.model();
|
||||
return !m.allClaimedChunks().contains(this.target);
|
||||
}
|
||||
|
||||
/** 判断区块是否属于己方领土(含本次会话新占领) */
|
||||
private boolean isInOwnTerritory(ChunkPos chunk) {
|
||||
TerritoryModel m = this.dataService.model();
|
||||
if (m.sessionClaims().contains(chunk)) {
|
||||
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;
|
||||
}
|
||||
TerritoryState self = m.selfState();
|
||||
return self != null && self.chunks().contains(chunk);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断玩家是否被困在地下(头顶上方连续数格被实心方块遮挡)。
|
||||
* 用于寻路超时时识别"走进矿洞"的情况,从而触发回城自救。
|
||||
* 判断区块是否与己方领土(己方邦国地图区块 + 本次会话新占领区块)接壤。
|
||||
*
|
||||
* <p>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。</p>
|
||||
*/
|
||||
private boolean isUnderground() {
|
||||
MinecraftClient client = MinecraftClient.getInstance();
|
||||
if (client == null || client.player == null || client.world == null) {
|
||||
return false;
|
||||
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());
|
||||
}
|
||||
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()) {
|
||||
own.addAll(model.sessionClaims());
|
||||
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
|
||||
if (own.contains(neighbor)) {
|
||||
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"));
|
||||
@@ -524,6 +529,29 @@ implements SubtitleChatMonitor.Listener {
|
||||
return min == Integer.MAX_VALUE ? 0 : min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baritone 路径事件回调:记录关键事件,并在“计算失败”时置标志供寻路阶段消费。
|
||||
* 本回调可能来自 Baritone 事件线程,只做轻量操作(置标志 + 写日志),
|
||||
* 具体处理放在 tick 的寻路阶段(主线程)完成,避免跨线程状态竞争。
|
||||
*/
|
||||
@Override
|
||||
public void onPathEvent(PathEvent event) {
|
||||
if (event == PathEvent.CALC_STARTED) {
|
||||
this.pathCalcFailed = false;
|
||||
} else if (event == PathEvent.CALC_FAILED) {
|
||||
this.pathCalcFailed = true;
|
||||
}
|
||||
// 仅在任务运行中记录关键事件,避免空闲时 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) {
|
||||
@@ -698,13 +726,42 @@ implements SubtitleChatMonitor.Listener {
|
||||
private void log(String message) {
|
||||
Deque<String> deque = this.logs;
|
||||
synchronized (deque) {
|
||||
this.logs.addLast(System.currentTimeMillis() + " " + message);
|
||||
while (this.logs.size() > 80) {
|
||||
this.logs.addLast("[" + LocalTime.now().format(LOG_TIME_FORMAT) + "] " + message);
|
||||
while (this.logs.size() > MAX_LOGS) {
|
||||
this.logs.removeFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出运行日志到文件,返回文件路径;失败时返回 null。
|
||||
* 文件写入 Fabric 配置目录下的 {@code autostore-debug.log},
|
||||
* 内容包含当前状态摘要(状态、目标、玩家区块、队列、进度)与全部详细日志。
|
||||
*
|
||||
* @return 导出文件的绝对路径,或 null(写入失败时)
|
||||
*/
|
||||
public Path exportLogs() {
|
||||
Path path = FabricLoader.getInstance().getConfigDir().resolve("autostore-debug.log");
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder(8192);
|
||||
sb.append("==== AutoStore 调试日志 ====\n");
|
||||
sb.append("状态: ").append(this.state).append(" / ").append(this.status).append('\n');
|
||||
sb.append("目标区块: ").append(this.target == null ? "无" : this.target.x() + "," + this.target.z()).append('\n');
|
||||
sb.append("玩家区块: ").append(this.playerChunk.x()).append(',').append(this.playerChunk.z()).append('\n');
|
||||
sb.append("队列: ").append(this.queue.size()).append(" | 已占领: ").append(this.completedClaims)
|
||||
.append(" | 重试: ").append(this.retryCount).append(" | 最近进度: ").append(this.lastProgress).append("%\n\n");
|
||||
synchronized (this.logs) {
|
||||
for (String line : this.logs) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
}
|
||||
Files.writeString(path, sb.toString(), StandardCharsets.UTF_8);
|
||||
return path;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩地任务状态机枚举:
|
||||
* <ul>
|
||||
@@ -729,8 +786,7 @@ implements SubtitleChatMonitor.Listener {
|
||||
FAIL,
|
||||
NEXT,
|
||||
COMPLETE,
|
||||
STOPPED,
|
||||
RETURNING;
|
||||
STOPPED;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.client.gui.widget.CheckboxWidget;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.text.StringVisitable;
|
||||
import net.minecraft.text.OrderedText;
|
||||
|
||||
@Environment(value=EnvType.CLIENT)
|
||||
/**
|
||||
@@ -106,6 +107,8 @@ extends Screen {
|
||||
// 左上角外部链接按钮: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)"\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 stopButton = ButtonWidget.builder(Text.literal((String)"\u505c\u6b62"), button -> this.controller.stop()).dimensions(topX + 160, 34, 74, 30).build();
|
||||
this.addDrawableChild(startButton);
|
||||
@@ -174,6 +177,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 位) */
|
||||
private TextFieldWidget numberField(int value, int x, int y, int width) {
|
||||
TextFieldWidget field = new TextFieldWidget(this.textRenderer, x, y, width, 20, Text.literal((String)""));
|
||||
@@ -456,13 +471,26 @@ extends Screen {
|
||||
context.drawTextWithShadow(this.textRenderer, "\u72b6\u6001\u4fe1\u606f", statusX, infoTop, -5592406);
|
||||
context.fill(statusX - 12, infoTop - 1, statusX - 11, infoTop + 58, -12761518);
|
||||
|
||||
// 运行日志:最多 4 条,最新日志在最下方
|
||||
// 运行日志:使用较小字体并自动换行,最新日志在最上方,逐行向下显示直到填满可视高度
|
||||
List<String> logs = this.controller.logs();
|
||||
int shown = Math.min(4, logs.size());
|
||||
for (int i = 0; i < shown; ++i) {
|
||||
String trimmed = this.textRenderer.trimToWidth(logs.get(logs.size() - shown + i), logWidth - 4);
|
||||
context.drawTextWithShadow(this.textRenderer, trimmed, this.mapX + 2, infoTop + 12 + i * 12, -6577233);
|
||||
float logScale = 0.75f; // 日志字号缩放(比默认小一号)
|
||||
int logAreaTop = infoTop + 12;
|
||||
int logAreaBottom = infoTop + 56; // 与竖向分隔线底部对齐
|
||||
int wrapWidth = (int)((logWidth - 4) / logScale); // 缩放后的可用换行宽度(按未缩放坐标计算)
|
||||
context.getMatrices().push();
|
||||
context.getMatrices().scale(logScale, logScale, 1.0f);
|
||||
int lineY = logAreaTop;
|
||||
outer:
|
||||
for (int i = logs.size() - 1; i >= 0; --i) {
|
||||
for (OrderedText line : this.textRenderer.wrapLines(Text.literal(logs.get(i)), wrapWidth)) {
|
||||
if (lineY + 9 > logAreaBottom) {
|
||||
break outer;
|
||||
}
|
||||
context.drawTextWithShadow(this.textRenderer, line, (int)((this.mapX + 2) / logScale), (int)(lineY / logScale), -6577233);
|
||||
lineY += 9;
|
||||
}
|
||||
}
|
||||
context.getMatrices().pop();
|
||||
|
||||
// 状态信息:单行截断 + 固定 12px 行距,彻底消除换行重叠
|
||||
int target = this.config.targetCount;
|
||||
|
||||
@@ -227,14 +227,17 @@ public final class ExpansionPlanner {
|
||||
ChunkPos start = this.chooseStart(borderChunks, minX, maxX, minZ, maxZ);
|
||||
// 生成 S 形扫描路径(覆盖矩形内所有区块,含已占领的)
|
||||
List<ChunkPos> sPath = this.buildSPath(start, minX, maxX, minZ, maxZ);
|
||||
// 已占领集合:己方领土 + 所有已占领 + 本次规划已纳入的区块
|
||||
// 己方领土集合(含本次规划已纳入的区块)。
|
||||
// 注意:这里绝不能把其他邦国的领土(allClaimed)混入接壤判断集合,
|
||||
// 否则会选中一个“挨着其他邦国、但与己方领土不相邻”的区块,
|
||||
// 服务器会因不接壤而拒绝开拓,导致玩家反复走到无效目标(历史 bug)。
|
||||
HashSet<ChunkPos> claimed = new HashSet<ChunkPos>(actualOwn);
|
||||
claimed.addAll(allClaimed);
|
||||
ArrayList<ChunkPos> result = new ArrayList<ChunkPos>();
|
||||
for (ChunkPos chunk : sPath) {
|
||||
if (result.size() >= limit) break;
|
||||
if (claimed.contains(chunk)) continue;
|
||||
// 必须与已占领领土接壤,保证能够开拓
|
||||
// 已被任何邦国(含己方)占领,或本次规划已纳入:跳过,避免重复/跨邦国开拓
|
||||
if (allClaimed.contains(chunk) || claimed.contains(chunk)) continue;
|
||||
// 必须与“己方”领土接壤(含本次已纳入的区块),保证服务器能成功开拓
|
||||
if (this.ownNeighbors(chunk, claimed) == 0) continue;
|
||||
result.add(chunk);
|
||||
claimed.add(chunk);
|
||||
@@ -478,7 +481,13 @@ public final class ExpansionPlanner {
|
||||
return Math.max(dx, dz = Math.abs(chunk.z() - config.regionCenterZ)) <= Math.max(1, config.regionRadius);
|
||||
}
|
||||
|
||||
/** 统计某区块周围(上下左右)属于给定集合的邻居数量 */
|
||||
/**
|
||||
* 统计某区块周围(上下左右四个正方向)属于给定集合的邻居数量。
|
||||
*
|
||||
* <p>调用方必须传入“己方领土”集合(含本次规划已纳入的区块)作为 {@code own},
|
||||
* 用于判断区块是否与己方领土接壤。绝不能把其他邦国的领土混入该集合,
|
||||
* 否则会误判“与他国接壤”为“可开拓”,选中一个与己方领土不相邻的无效目标。</p>
|
||||
*/
|
||||
private int ownNeighbors(ChunkPos chunk, Set<ChunkPos> own) {
|
||||
int count = 0;
|
||||
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -65,6 +66,16 @@ public final class TerritoryDataService {
|
||||
private volatile long lastSuccessfulFetchMillis;
|
||||
/** 最近一次尝试拉取的时间戳(毫秒) */
|
||||
private volatile long lastAttemptMillis;
|
||||
/** 会话内新占领区块的本地记录时间(毫秒),用于刷新时判断该占领是否已被地图数据确认 */
|
||||
private final Map<ChunkPos, Long> sessionClaimTimes = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 会话占领"确认宽限"时长(毫秒)。
|
||||
*
|
||||
* <p>占领成功后,服务器动态地图(Dynmap/markers.json)可能尚未同步该区块,
|
||||
* 因此在宽限期内即使地图未确认也先保留"会话占领"标记,避免刚占领的区块被误判为假占领。
|
||||
* 超过宽限期仍未在地图中确认的,视为未真正占领并丢弃,使空洞区块能重新进入规划。
|
||||
*/
|
||||
private static final long CLAIM_CONFIRM_GRACE_MS = 120_000L;
|
||||
|
||||
public TerritoryDataService() {
|
||||
this.executor = TerritoryDataService.newExecutor();
|
||||
@@ -111,8 +122,8 @@ public final class TerritoryDataService {
|
||||
|
||||
/**
|
||||
* 异步刷新领土数据(在调度线程中执行)。
|
||||
* 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"合并进新模型,
|
||||
* 避免刷新覆盖掉本次会话的占领记录。
|
||||
* 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"与刚拉取的最新地图数据仔细核对,
|
||||
* 只保留"已被地图确认"或"刚占领未超宽限期"的占领记录,避免刷新时把假占领永久留在模型里。
|
||||
*/
|
||||
public void refreshAsync() {
|
||||
this.executor.execute(() -> {
|
||||
@@ -127,8 +138,25 @@ public final class TerritoryDataService {
|
||||
TerritoryModel parsed = TerritoryDataService.parse(response.body());
|
||||
TerritoryModel previous = this.model;
|
||||
if (previous != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
// 本次拉取到的己方领土区块(地图数据即最终依据,无需缓存旧数据)
|
||||
Set<ChunkPos> freshOwn = new HashSet<>();
|
||||
TerritoryState freshSelf = parsed.state(this.selectedStateName);
|
||||
if (freshSelf != null) {
|
||||
freshOwn.addAll(freshSelf.chunks());
|
||||
}
|
||||
for (ChunkPos sessionClaim : previous.sessionClaims()) {
|
||||
parsed.addSessionClaim(sessionClaim);
|
||||
Long claimTime = this.sessionClaimTimes.get(sessionClaim);
|
||||
boolean confirmed = freshOwn.contains(sessionClaim);
|
||||
boolean withinGrace = claimTime != null && now - claimTime < CLAIM_CONFIRM_GRACE_MS;
|
||||
if (confirmed || withinGrace) {
|
||||
// 地图已确认,或尚在宽限期(Dynmap 可能还没同步):保留会话占领标记
|
||||
parsed.addSessionClaim(sessionClaim);
|
||||
} else if (claimTime != null) {
|
||||
// 地图始终未确认且超时:判定为假占领并移除,使空洞区块重新进入规划
|
||||
this.sessionClaimTimes.remove(sessionClaim);
|
||||
}
|
||||
// claimTime 为 null(从未在本服务记录)也视为未确认:不保留
|
||||
}
|
||||
}
|
||||
parsed.setSelfStateName(this.selectedStateName);
|
||||
@@ -163,6 +191,7 @@ public final class TerritoryDataService {
|
||||
}
|
||||
|
||||
public void addSessionClaim(ChunkPos chunk) {
|
||||
this.sessionClaimTimes.put(chunk, System.currentTimeMillis());
|
||||
TerritoryModel current = this.model;
|
||||
if (current != null) {
|
||||
current.addSessionClaim(chunk);
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"autostore","version":"1.1.7","name":"AutoStore","description":"","authors":[],"contact":{},"license":"All-Rights-Reserved","environment":"client","entrypoints":{"fabric-datagen":["autostore.client.AutoStoreModDataGenerator"],"client":["autostore.client.AutoStoreModClient"],"main":["autostore.AutoStoreMod"]},"mixins":["autostore.mixins.json",{"config":"autostore.client.mixins.json","environment":"client"}],"depends":{"fabricloader":">=0.16.14","fabric-api":"*","minecraft":"1.21.4"},"jars":[{"file":"META-INF/jars/baritone-api-fabric-1.13.1.jar"}]}
|
||||
{"schemaVersion":1,"id":"autostore","version":"1.1.9","name":"AutoStore","description":"","authors":[],"contact":{},"license":"All-Rights-Reserved","environment":"client","entrypoints":{"fabric-datagen":["autostore.client.AutoStoreModDataGenerator"],"client":["autostore.client.AutoStoreModClient"],"main":["autostore.AutoStoreMod"]},"mixins":["autostore.mixins.json",{"config":"autostore.client.mixins.json","environment":"client"}],"depends":{"fabricloader":">=0.16.14","fabric-api":"*","minecraft":"1.21.4"},"jars":[{"file":"META-INF/jars/baritone-api-fabric-1.13.1.jar"}]}
|
||||
在新工单中引用
屏蔽一个用户