diff --git a/README.md b/README.md index 568b260..e62b618 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - 自动进食(饥饿时自动吃背包食物) - 自动攻击敌对生物:扩地途中自动面向并攻击索敌范围内的敌对生物(满蓄力出手、不追击、不打断寻路);严格豁免不会主动攻击玩家的中立生物——铁傀儡、僵尸猪灵永不攻击,末影人(仅被激怒)、蜘蛛(仅黑暗中)、猪灵(仅玩家未穿金甲)按原版敌对条件反击,狼、蜜蜂、羊驼等其它中立生物一律不打 - 耐久保护回城:每 2 秒检查主手/副手/四件盔甲耐久,任意一件剩余耐久低于阈值(默认 15%,可在 GUI 调整)时自动发送 `/u return` 回城并停止扩地,防止装备被怪物磨没 +- 自动用船过水:扩地途中若路径前方出现开阔水域(默认宽度 ≥ 4 格、长度 ≥ 6 格),自动切到主手放船并登船;前方出现岸边时自动蹲伏下船并攻击船实体回收船(仅背包有任意 `*_boat`/`*_chest_boat` 时生效;不打断 Baritone 寻路) - 实时地图 GUI(通过 Ctrl+R 打开) - 监听聊天/字幕消息判断开垦结果 - 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞 @@ -28,7 +29,7 @@ ## 安装 1. 安装 Fabric Loader 0.16.14+ 和 Fabric API -2. 将 `autostore-1.2.6.jar` 放入 `.minecraft/mods/` 目录 +2. 将 `autostore-1.3.0.jar` 放入 `.minecraft/mods/` 目录 3. 启动游戏,按 `Ctrl+R` 打开 GUI ## 构建 @@ -38,7 +39,7 @@ ./build_mod.sh ``` -输出文件:`autostore-1.2.6.jar` +输出文件:`autostore-1.3.0.jar` ## 赞助支持 diff --git a/gradle.properties b/gradle.properties index 4c5429e..3170488 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8 loader_version=0.19.3 # Mod Properties -mod_version=1.2.6 +mod_version=1.3.0 maven_group=autostore archives_base_name=autostore diff --git a/src/main/java/autostore/client/config/ExpansionConfig.java b/src/main/java/autostore/client/config/ExpansionConfig.java index a8fcadd..cf75741 100644 --- a/src/main/java/autostore/client/config/ExpansionConfig.java +++ b/src/main/java/autostore/client/config/ExpansionConfig.java @@ -100,6 +100,21 @@ public class ExpansionConfig { /** 耐久保护阈值:主手/副手/盔甲任意一件剩余耐久百分比 ≤ 该值时触发回城(1-99,默认 15) */ public int durabilityThreshold = 15; + /** 是否启用自动用船过水:路径前方出现开阔水域时自动放船登船,上岸前自动下船回收(默认 true) */ + public boolean autoBoat = true; + + /** + * 触发自动用船的水面最小宽度(格):玩家前进方向上连续水方块数 ≥ 该值时放船登船。 + * 避免小水洼/小溪浪费船只。默认 4。 + */ + public int autoBoatMinWidth = 4; + + /** + * 自动用船最短水域长度(格):玩家前进方向上从当前位置到首个非水方块的距离 ≥ 该值才放船登船。 + * 比宽度更稳健(防止横跨窄长水道时还在岸上就提前登船)。默认 6。 + */ + public int autoBoatMinLength = 6; + /** 扩圈上限(0 表示不限制) */ public int maxRings; @@ -141,6 +156,9 @@ public class ExpansionConfig { c.autoAttack = this.autoAttack; c.durabilityGuard = this.durabilityGuard; c.durabilityThreshold = this.durabilityThreshold; + c.autoBoat = this.autoBoat; + c.autoBoatMinWidth = this.autoBoatMinWidth; + c.autoBoatMinLength = this.autoBoatMinLength; c.maxRings = this.maxRings; c.refreshIntervalSeconds = this.refreshIntervalSeconds; return c; diff --git a/src/main/java/autostore/client/controller/ExpansionController.java b/src/main/java/autostore/client/controller/ExpansionController.java index 879c287..11a193c 100644 --- a/src/main/java/autostore/client/controller/ExpansionController.java +++ b/src/main/java/autostore/client/controller/ExpansionController.java @@ -8,6 +8,7 @@ import autostore.client.bridge.BaritoneBridge; import autostore.client.combat.AutoCombat; import autostore.client.config.ExpansionConfig; import autostore.client.monitor.SubtitleChatMonitor; +import autostore.client.navigation.AutoBoatController; import autostore.client.planner.ExpansionPlanner; import autostore.client.territory.ChunkPos; import autostore.client.territory.TerritoryDataService; @@ -68,6 +69,8 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener { private final BaritoneBridge baritoneBridge = new BaritoneBridge(); /** 扩地规划器(决定下一个目标区块) */ private final ExpansionPlanner planner = new ExpansionPlanner(); + /** 自动用船过水辅助(开阔水域自动放船登船、上岸前自动下船回收) */ + private final AutoBoatController autoBoat = new AutoBoatController(); /** 聊天/字幕监听器(识别开垦进度与结果) */ private final SubtitleChatMonitor monitor = new SubtitleChatMonitor(this); /** 领土数据服务(提供实时领土模型) */ @@ -275,6 +278,8 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener { if (this.sideStepping) { this.stopSideStep(false); } + // 若正处于自动用船动作流程,先释放蹲伏键与暂存的主手槽位 + this.autoBoat.reset(MinecraftClient.getInstance(), true); this.eating = false; this.setState(State.STOPPED, "\u5df2\u624b\u52a8\u505c\u6b62"); this.log("\u5df2\u624b\u52a8\u505c\u6b62"); @@ -318,6 +323,8 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener { return; } this.tickAutoSwim(client); + // 自动用船过水:开阔水域自动放船登船、上岸前自动下船回收 + this.tickAutoBoat(client); // 处于"往边上走两步"阶段:每帧驱动手动横向行走,走开后自动重试原目标(该阶段不自动攻击,避免抢方向键) if (this.sideStepping) { this.tickSideStep(); @@ -927,6 +934,27 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener { AutoCombat.tickAttack(client, target); } + /** + * 自动用船过水(每帧调用): + * 在岸上且朝向方向前方出现开阔水域时,自动放船并登船; + * 在船中且前方出现岸边时,自动下船并攻击船实体回收船。 + * 仅在扩地任务运行中、配置 autoBoat 开启时生效;不打断 Baritone 寻路。 + */ + private void tickAutoBoat(MinecraftClient client) { + this.autoBoat.tick(client, this.config, this.isRunning()); + if (this.autoBoat.isActive()) { + String desc = this.autoBoat.describe(); + if (desc != null) { + this.status = desc; + } + } + } + + /** @return 当前是否处于自动用船动作流程(用于状态栏 / 日志显示) */ + public boolean isAutoBoatActive() { + return this.autoBoat.isActive(); + } + /** * 耐久保护逻辑(每 {@link #DURABILITY_CHECK_INTERVAL_TICKS} 帧调用): * 主手/副手/四件盔甲任意一件剩余耐久低于阈值时,发送 /u return 回城并停止扩地, diff --git a/src/main/java/autostore/client/gui/AutoStoreGui.java b/src/main/java/autostore/client/gui/AutoStoreGui.java index 98634ff..a491c31 100644 --- a/src/main/java/autostore/client/gui/AutoStoreGui.java +++ b/src/main/java/autostore/client/gui/AutoStoreGui.java @@ -58,6 +58,7 @@ extends Screen { private CheckboxWidget durabilityGuardBox; private CheckboxWidget breakBox; private CheckboxWidget placeBox; + private CheckboxWidget autoBoatBox; private ButtonWidget refreshButton; // ---- 区域圈选状态 ---- private boolean selectingRegion; @@ -145,10 +146,11 @@ extends Screen { this.addDrawableChild(this.strategyButton("\u7ed5\u56fd\u4e00\u5708", 4, left + 96, strategyY + 24, 90)); this.advancedY = strategyY + 24 + 20 + 8; - // ===== 权限/辅助选择框:疾跑、跳跃、避怪、攻击、破坏、放置、耐久回城 ===== + // ===== 权限/辅助选择框:疾跑、跳跃、避怪、攻击、破坏、放置、耐久回城、用船过水 ===== int checkY1 = this.advancedY + 14; int checkY2 = checkY1 + advCheckGap; int checkY3 = checkY2 + advCheckGap; + int checkY4 = checkY3 + advCheckGap; int checkX = left; this.sprintBox = this.checkbox("\u75be\u8dd1", checkX, checkY1, this.config.allowSprint, value -> { this.config.allowSprint = value; @@ -174,6 +176,10 @@ extends Screen { }); this.durabilityField = this.numberField(this.config.durabilityThreshold, checkX + 96, checkY3, 34); this.durabilityField.setMaxLength(2); + // 第四行:自动用船过水(开阔水域自动放船登船,上岸前自动下船回收) + this.autoBoatBox = this.checkbox("\u7528\u8239\u8fc7\u6c34", checkX, checkY4, this.config.autoBoat, value -> { + this.config.autoBoat = value; + }); this.addDrawableChild(this.sprintBox); this.addDrawableChild(this.jumpBox); this.addDrawableChild(this.avoidBox); @@ -182,6 +188,7 @@ extends Screen { this.addDrawableChild(this.placeBox); this.addDrawableChild(this.durabilityGuardBox); this.addDrawableChild(this.durabilityField); + this.addDrawableChild(this.autoBoatBox); this.initializeMapView(); } @@ -598,6 +605,7 @@ extends Screen { this.config.allowBreak = this.breakBox.isChecked(); this.config.allowPlace = this.placeBox.isChecked(); this.config.durabilityGuard = this.durabilityGuardBox.isChecked(); + this.config.autoBoat = this.autoBoatBox.isChecked(); int threshold = this.parseInt(this.durabilityField.getText(), 15); // 钳制阈值到 1-99,避免 0(永不触发)或 100(刚装备就回城) this.config.durabilityThreshold = Math.max(1, Math.min(99, threshold)); @@ -617,6 +625,7 @@ extends Screen { target.allowPlace = this.config.allowPlace; target.durabilityGuard = this.config.durabilityGuard; target.durabilityThreshold = this.config.durabilityThreshold; + target.autoBoat = this.config.autoBoat; // 同步矩形圈选坐标到全局配置(修复:之前忘记同步,导致区域扩地总是为空) target.regionMinX = this.config.regionMinX; target.regionMaxX = this.config.regionMaxX; diff --git a/src/main/java/autostore/client/navigation/AutoBoatController.java b/src/main/java/autostore/client/navigation/AutoBoatController.java new file mode 100644 index 0000000..3defd27 --- /dev/null +++ b/src/main/java/autostore/client/navigation/AutoBoatController.java @@ -0,0 +1,652 @@ +/* + * AutoStore 模组自动用船过水辅助:检测路径前方水域自动放船登船、上岸前自动下船回收。 + * + *

仅在扩地任务运行中({@link ExpansionController#isRunning()} == true), + * 且配置开启 {@link ExpansionConfig#autoBoat} 时生效。 + * 设计上以"最小侵入"为原则:不主动取消/重排 Baritone 寻路,不改变状态机, + * 只在合适时机调用 Minecraft 交互接口({@code interactBlock} / {@code interactEntity}), + * 以及按下/释放蹲伏键完成上下船。

+ * + *

关键时序: + *

+ *

+ */ +package autostore.client.navigation; + +import autostore.client.config.ExpansionConfig; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.vehicle.AbstractBoatEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.Registries; +import net.minecraft.util.Hand; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +@Environment(EnvType.CLIENT) +/** + * 自动用船过水辅助(与 Baritone 寻路配合)。 + * + *

本类只负责在合适时机插桩"放船/登船/下船/回收船"四个动作, + * 路径规划与开垦逻辑仍由 {@link ExpansionController} + Baritone 完成。 + * 进出船流程会临时按住蹲伏键,阻止 Baritone 把玩家推进水里或撞上船, + * 流程结束后释放蹲伏键,Baritone 自动恢复正常寻路。

+ */ +public final class AutoBoatController { + + /** 进入船阶段:等待玩家进入水面时调用一次交互(在玩家脚下的水面方块上放船) */ + private static final int PHASE_PLACE = 0; + /** 放船完成后等待若干 tick 再尝试登船(给服务器时间确认实体生成) */ + private static final int PHASE_PLACE_WAIT = 1; + /** 登船阶段:寻找最近的可登船实体并调用 interactEntity */ + private static final int PHASE_BOARD = 2; + /** 登船后等待若干 tick,让 Baritone 与服务器同步位置 */ + private static final int PHASE_BOARD_WAIT = 3; + + /** 上岸阶段:先按一次蹲伏键下船(持续按住一段时间,确保服务器收到下船) */ + private static final int PHASE_DISMOUNT_PRESS = 4; + /** 下船后等待若干 tick,让服务器同步状态 */ + private static final int PHASE_DISMOUNT_WAIT = 5; + /** 攻击船实体回收船(直到船从世界中消失) */ + private static final int PHASE_BREAK_BOAT = 6; + /** 完成后等待若干 tick 再恢复 Baritone 寻路 */ + private static final int PHASE_BREAK_WAIT = 7; + + /** 当前阶段(详见常量定义) */ + private int phase = PHASE_PLACE; + /** 当前阶段已等待的 tick 数 */ + private int phaseTicks; + /** 上次保存的玩家主手槽位(完成后恢复) */ + private int savedSelectedSlot = -1; + /** 进入船阶段:玩家脚下水面方块(用于重新计算登船点) */ + private BlockPos placeTarget; + /** 进入船阶段:登船目标实体 ID(用于在 BOARD 阶段精确定位) */ + private int boardTargetEntityId = -1; + /** 下船阶段:船实体 ID(用于 BREAK 阶段精确定位) */ + private int boatEntityId = -1; + /** 当前阶段最近一次日志(用于去重,避免每帧刷屏) */ + private String lastLoggedPhase; + /** 登船流程是否正在按住蹲伏键以阻止 Baritone 把玩家推进水里 */ + private boolean holdingSneak; + + /** 状态机:当前未做任何事 */ + private static final int IDLE = -1; + /** 当前是进入船流程 */ + private static final int FLOW_ENTER = 1; + /** 当前是上岸流程 */ + private static final int FLOW_EXIT = 2; + private int flow = IDLE; + + /** + * 每帧调用入口:根据当前玩家状态决定是否触发登船或上岸流程; + * 已经处于某个流程中时推进阶段。 + * + * @param client 客户端 + * @param config 扩地配置(用于读取 autoBoat/最小宽度/最短长度) + * @param isRunning 当前扩地任务是否在运行(PAUSE/COMPLETE/STOPPED 时跳过) + */ + public void tick(MinecraftClient client, ExpansionConfig config, boolean isRunning) { + if (client == null || client.player == null || client.world == null || client.interactionManager == null) { + return; + } + // 配置未启用或任务未运行:清理状态(如有进行中的流程,强行取消并释放蹲伏键) + if (!isRunning || !config.autoBoat) { + this.reset(client, false); + return; + } + // 自动攻击、自动进食、自动游泳、走两步期间:让位(避免按键冲突) + if (this.flow == IDLE) { + // 无进行中流程:根据玩家状态决定是否需要登船 / 上岸 + Entity vehicle = client.player.getVehicle(); + if (vehicle instanceof AbstractBoatEntity) { + // 已在船中:检测前方是否即将到达岸边 + if (this.shouldDisembark(client, config)) { + this.beginDisembark(client, (AbstractBoatEntity) vehicle); + } + } else if (this.shouldBoardBoat(client, config)) { + // 在岸上且前方有开阔水域:尝试登船 + this.beginBoard(client); + } + return; + } + // 有进行中流程:推进阶段 + switch (this.flow) { + case FLOW_ENTER -> this.tickEnter(client); + case FLOW_EXIT -> this.tickExit(client); + } + } + + /** 强制停止所有动作(如任务被手动停止时) */ + public void reset(MinecraftClient client, boolean silent) { + if (this.flow == IDLE) { + return; + } + // 释放蹲伏键(避免卡住"按住"状态) + if (this.holdingSneak) { + this.releaseSneak(client); + this.holdingSneak = false; + } + // 还原主手槽位(如有暂存) + this.restoreSelectedSlot(client); + this.flow = IDLE; + this.phase = PHASE_PLACE; + this.phaseTicks = 0; + this.placeTarget = null; + this.boardTargetEntityId = -1; + this.boatEntityId = -1; + this.lastLoggedPhase = null; + } + + /** @return 当前是否处于进行中流程(用于状态栏显示) */ + public boolean isActive() { + return this.flow != IDLE; + } + + /** @return 当前流程的可读描述(用于状态/日志) */ + public String describe() { + return switch (this.flow) { + case FLOW_ENTER -> "正在自动登船"; + case FLOW_EXIT -> "正在上岸回收船"; + default -> null; + }; + } + + // =========================== 登船判断 =========================== + + /** + * 判断是否应该登船: + * 玩家在岸上或浅水中(脚下为可站立方块、或脚下为水/睡莲方块且头部未浸水)、 + * 朝向方向前方出现连续开阔水域(宽度 ≥ 配置阈值、长度 ≥ 配置阈值)、 + * 背包里有任意船物品。 + */ + private boolean shouldBoardBoat(MinecraftClient client, ExpansionConfig config) { + PlayerEntity player = client.player; + // 玩家完全没入水中:暂不处理,等 Baritone 处理上岸(或玩家主动下船) + if (player.isSubmergedInWater()) { + return false; + } + // 玩家脚下为水方块(浅水行走)或睡莲:也允许尝试放船 + BlockPos playerPos = player.getBlockPos(); + if (!isStandable(client.world, playerPos) && !this.isWaterAt(client.world, playerPos) + && !client.world.getBlockState(playerPos).isOf(Blocks.LILY_PAD)) { + return false; + } + // 背包里没有船 + if (this.findBoatSlot(player) < 0) { + return false; + } + // 朝向方向前方水域扫描:从玩家脚部位置按 yaw 方向扫描 + Vec3d dir = this.forwardHorizontal(player); + if (dir == null) { + return false; + } + int minWidth = Math.max(2, config.autoBoatMinWidth); + int minLength = Math.max(minWidth, config.autoBoatMinLength); + // 在垂直方向(-1/0/1 格)共采样三条平行线,取宽度最窄者作为判定值——避免窄水渠被误判 + int width = this.measureWaterWidth(client, playerPos, dir, minWidth); + if (width < minWidth) { + return false; + } + // 沿朝向方向连续水方块长度 + int length = this.measureWaterLength(client, playerPos, dir, minLength); + return length >= minLength; + } + + /** 判断是否应该下船:朝向上前方出现可站立方块(岸边) */ + private boolean shouldDisembark(MinecraftClient client, ExpansionConfig config) { + PlayerEntity player = client.player; + Vec3d dir = this.forwardHorizontal(player); + if (dir == null) { + return false; + } + // 沿玩家朝向方向逐格扫描,遇到第一个可站立方块即视为即将上岸 + BlockPos start = player.getBlockPos(); + for (int step = 1; step <= 8; ++step) { + int bx = (int)Math.round(start.getX() + dir.x * step); + int bz = (int)Math.round(start.getZ() + dir.z * step); + // 玩家身高 1.8,需要 y/y+1 都不被实心方块占据 + BlockPos feet = new BlockPos(bx, start.getY(), bz); + BlockPos head = feet.up(); + if (isStandable(client.world, feet) && isAirOrPassable(client.world, head)) { + return true; + } + } + return false; + } + + /** 玩家在水平面上的朝向单位向量(忽略 Y) */ + private Vec3d forwardHorizontal(PlayerEntity player) { + float yaw = player.getYaw(); + // 与 ExpansionController#tickSideStep 一致:yaw 是水平角,转为方向向量 + double rad = Math.toRadians(yaw); + double fx = -Math.sin(rad); + double fz = Math.cos(rad); + double len = Math.sqrt(fx * fx + fz * fz); + if (len < 1.0e-3) { + return null; + } + return new Vec3d(fx / len, 0.0, fz / len); + } + + /** + * 测量沿指定方向在玩家脚下 y 平面上"连续水方块的最大宽度"。 + * 从玩家前方第一格开始,沿主方向前进,每步同时检查主方向左右两侧是否也是水, + * 直到左右均不再是水为止。返回"至少左右各有 (count-1) 格水"的最大 width。 + */ + private int measureWaterWidth(MinecraftClient client, BlockPos playerPos, Vec3d dir, int maxSample) { + // 取主方向的左右向量(顺时针 90°) + double lx = -dir.z; + double lz = dir.x; + int best = 0; + for (int step = 1; step <= maxSample; ++step) { + int cx = (int)Math.round(playerPos.getX() + dir.x * step); + int cz = (int)Math.round(playerPos.getZ() + dir.z * step); + // 沿左右两侧各扩展最多 8 格 + int leftWidth = 0; + for (int side = 1; side <= 8; ++side) { + int sx = (int)Math.round(cx + lx * side); + int sz = (int)Math.round(cz + lz * side); + BlockPos p = new BlockPos(sx, playerPos.getY(), sz); + if (this.isWaterAt(client.world, p)) { + leftWidth = side; + } else { + break; + } + } + int rightWidth = 0; + for (int side = 1; side <= 8; ++side) { + int sx = (int)Math.round(cx - lx * side); + int sz = (int)Math.round(cz - lz * side); + BlockPos p = new BlockPos(sx, playerPos.getY(), sz); + if (this.isWaterAt(client.world, p)) { + rightWidth = side; + } else { + break; + } + } + int width = 1 + leftWidth + rightWidth; + if (width > best) { + best = width; + } + } + return best; + } + + /** 沿主方向连续水方块的长度(玩家高度 ±1 的可站立格也算"岸边") */ + private int measureWaterLength(MinecraftClient client, BlockPos playerPos, Vec3d dir, int maxSample) { + int length = 0; + for (int step = 1; step <= 32; ++step) { + int cx = (int)Math.round(playerPos.getX() + dir.x * step); + int cz = (int)Math.round(playerPos.getZ() + dir.z * step); + BlockPos p = new BlockPos(cx, playerPos.getY(), cz); + if (this.isWaterAt(client.world, p)) { + length = step; + continue; + } + // 玩家脚下 y 不是水,但同 x/z 在 ±1 y 出现可站立方块 → 也视为水域末端即将上岸 + if (isStandable(client.world, p) || isStandable(client.world, p.up()) || isStandable(client.world, p.down())) { + break; + } + // 都不满足则中断 + break; + } + return length; + } + + /** 指定坐标方块是否为水(含含水方块/植物/含水流体的方块) */ + private boolean isWaterAt(World world, BlockPos pos) { + var state = world.getBlockState(pos); + return state.isOf(Blocks.WATER) + || state.isOf(Blocks.BUBBLE_COLUMN) + || state.isOf(Blocks.KELP) + || state.isOf(Blocks.KELP_PLANT) + || state.isOf(Blocks.SEAGRASS) + || state.isOf(Blocks.TALL_SEAGRASS); + } + + /** 玩家脚下"可站立"方块:实心方块且上方是空气/可穿过 */ + private boolean isStandable(World world, BlockPos pos) { + var state = world.getBlockState(pos); + if (state.isAir()) return false; + if (state.getCollisionShape(world, pos).isEmpty()) return false; + return isAirOrPassable(world, pos.up()); + } + + /** 玩家头部位置可穿过(空气/草丛等无碰撞) */ + private boolean isAirOrPassable(World world, BlockPos pos) { + var state = world.getBlockState(pos); + if (state.isAir()) return true; + return state.getCollisionShape(world, pos).isEmpty(); + } + + // =========================== 登船流程 =========================== + + private void beginBoard(MinecraftClient client) { + this.flow = FLOW_ENTER; + this.phase = PHASE_PLACE; + this.phaseTicks = 0; + this.placeTarget = this.findWaterInFront(client); + this.boardTargetEntityId = -1; + this.lastLoggedPhase = null; + // 暂存当前主手槽位,结束后还原 + this.savedSelectedSlot = client.player.getInventory().selectedSlot; + // 按住蹲伏键以阻止 Baritone 把玩家推进水里(也避免登船后立即被推动离开船) + this.pressSneak(client); + this.holdingSneak = true; + } + + private void tickEnter(MinecraftClient client) { + switch (this.phase) { + case PHASE_PLACE -> this.enterPlace(client); + case PHASE_PLACE_WAIT -> this.enterPlaceWait(client); + case PHASE_BOARD -> this.enterBoard(client); + case PHASE_BOARD_WAIT -> this.enterBoardWait(client); + default -> this.reset(client, true); + } + } + + /** PHASE_PLACE:玩家脚下方块必须是水(站在岸边浅水处),在脚下方块上放船 */ + private void enterPlace(MinecraftClient client) { + PlayerEntity player = client.player; + if (this.placeTarget == null) { + this.placeTarget = this.findWaterInFront(client); + } + // 若玩家当前站在陆地:仍允许(船会被放在脚前的水方块上) + BlockPos waterPos = this.placeTarget != null ? this.placeTarget : player.getBlockPos(); + if (!this.isWaterAt(client.world, waterPos)) { + // 没有合适的水方块 → 取消 + this.reset(client, true); + return; + } + int slot = this.findBoatSlot(player); + if (slot < 0) { + this.reset(client, true); + return; + } + // 切换主手到船物品并放置 + player.getInventory().selectedSlot = slot; + BlockHitResult hit = new BlockHitResult( + Vec3d.ofCenter(waterPos).add(0, 0.5, 0), + Direction.UP, + waterPos, + false + ); + client.interactionManager.interactBlock(client.player, Hand.MAIN_HAND, hit); + this.logPhaseOnce(client, "已放船于 " + waterPos.getX() + "," + waterPos.getY() + "," + waterPos.getZ()); + this.phase = PHASE_PLACE_WAIT; + this.phaseTicks = 0; + } + + /** PHASE_PLACE_WAIT:等待 3 tick 让服务器确认船实体生成 */ + private void enterPlaceWait(MinecraftClient client) { + this.phaseTicks++; + if (this.phaseTicks < 3) { + return; + } + // 在附近寻找新放置的船 + AbstractBoatEntity boat = this.findNearbyBoat(client, 6.0); + if (boat == null) { + // 没找到:可能服务器拒收 / 反作弊拦截 → 取消 + this.logPhaseOnce(client, "放船后未找到船实体,取消登船"); + this.reset(client, true); + return; + } + this.boardTargetEntityId = boat.getId(); + this.phase = PHASE_BOARD; + this.phaseTicks = 0; + } + + /** PHASE_BOARD:找到目标船实体并 right-click 上船 */ + private void enterBoard(MinecraftClient client) { + AbstractBoatEntity boat = this.findBoatById(client, this.boardTargetEntityId); + if (boat == null) { + // 实体可能已被销毁:再扫一次附近 + boat = this.findNearbyBoat(client, 4.0); + if (boat == null) { + this.reset(client, true); + return; + } + this.boardTargetEntityId = boat.getId(); + } + // 上船:调用 interactEntity(玩家 main hand) + client.interactionManager.interactEntity(client.player, boat, Hand.MAIN_HAND); + this.logPhaseOnce(client, "已登船"); + this.phase = PHASE_BOARD_WAIT; + this.phaseTicks = 0; + } + + /** PHASE_BOARD_WAIT:等待服务器确认玩家已在船中 */ + private void enterBoardWait(MinecraftClient client) { + this.phaseTicks++; + if (this.phaseTicks < 3) { + return; + } + if (!(client.player.getVehicle() instanceof AbstractBoatEntity)) { + // 登船失败 → 取消 + this.reset(client, true); + return; + } + // 释放蹲伏键,Baritone 恢复自动寻路(此时玩家在船中,按 W 即可让船前进) + this.releaseSneak(client); + this.holdingSneak = false; + this.logPhaseOnce(client, "自动登船完成,开始过水"); + this.flow = IDLE; + this.phase = PHASE_PLACE; + this.phaseTicks = 0; + this.placeTarget = null; + this.boardTargetEntityId = -1; + // 还原主手槽位(让 Baritone 自由切回所需工具) + this.restoreSelectedSlot(client); + } + + /** 玩家正前方 1 格内的水面方块(脚下优先);找不到返回 null */ + private BlockPos findWaterInFront(MinecraftClient client) { + Vec3d dir = this.forwardHorizontal(client.player); + if (dir == null) { + return null; + } + BlockPos playerPos = client.player.getBlockPos(); + for (int step = 0; step <= 2; ++step) { + int cx = (int)Math.round(playerPos.getX() + dir.x * step); + int cz = (int)Math.round(playerPos.getZ() + dir.z * step); + BlockPos p = new BlockPos(cx, playerPos.getY(), cz); + if (this.isWaterAt(client.world, p)) { + return p; + } + // 退一步:玩家脚下方块向下找水 + BlockPos below = p.down(); + if (this.isWaterAt(client.world, below)) { + return below; + } + } + return null; + } + + /** 查找背包里第一个"船类"物品(含 chest_boat)的槽位;找不到返回 -1 */ + private int findBoatSlot(PlayerEntity player) { + var inventory = player.getInventory(); + for (int slot = 0; slot < inventory.size(); ++slot) { + ItemStack stack = inventory.getStack(slot); + if (stack.isEmpty()) continue; + Identifier id = Registries.ITEM.getId(stack.getItem()); + if (id == null) continue; + String path = id.getPath(); + if (path.endsWith("_boat") || path.endsWith("_chest_boat") || path.equals("boat") || path.equals("chest_boat")) { + return slot; + } + } + return -1; + } + + /** 还原玩家主手槽位(进入船前保存的) */ + private void restoreSelectedSlot(MinecraftClient client) { + if (this.savedSelectedSlot >= 0 && client.player != null) { + client.player.getInventory().selectedSlot = this.savedSelectedSlot; + } + this.savedSelectedSlot = -1; + } + + // =========================== 上岸流程 =========================== + + private void beginDisembark(MinecraftClient client, AbstractBoatEntity boat) { + this.flow = FLOW_EXIT; + this.phase = PHASE_DISMOUNT_PRESS; + this.phaseTicks = 0; + this.boatEntityId = boat.getId(); + this.lastLoggedPhase = null; + // 暂存主手槽位(攻击船后还原) + this.savedSelectedSlot = client.player.getInventory().selectedSlot; + } + + private void tickExit(MinecraftClient client) { + switch (this.phase) { + case PHASE_DISMOUNT_PRESS -> this.exitPressSneak(client); + case PHASE_DISMOUNT_WAIT -> this.exitWaitDismount(client); + case PHASE_BREAK_BOAT -> this.exitBreakBoat(client); + case PHASE_BREAK_WAIT -> this.exitBreakWait(client); + default -> this.reset(client, true); + } + } + + /** 按住蹲伏键一段时间(≥3 tick),触发玩家从船中下船 */ + private void exitPressSneak(MinecraftClient client) { + this.pressSneak(client); + this.holdingSneak = true; + this.phaseTicks++; + if (this.phaseTicks < 3) { + return; + } + // 玩家下船后再释放蹲伏键也会被 BREAK_BOAT/BREAK_WAIT 阶段重新按住 + this.releaseSneak(client); + this.holdingSneak = false; + this.phase = PHASE_DISMOUNT_WAIT; + this.phaseTicks = 0; + } + + /** 等待玩家从船中分离出来 */ + private void exitWaitDismount(MinecraftClient client) { + this.phaseTicks++; + if (this.phaseTicks < 3) { + return; + } + // 玩家已下船:进入攻击船阶段 + this.phase = PHASE_BREAK_BOAT; + this.phaseTicks = 0; + } + + /** 攻击船实体回收船(最多 20 tick;持续到船被销毁或超时); + * 整个阶段按住蹲伏键以阻止 Baritone 在破坏船时把玩家带离船边。 */ + private void exitBreakBoat(MinecraftClient client) { + if (!this.holdingSneak) { + this.pressSneak(client); + this.holdingSneak = true; + } + AbstractBoatEntity boat = this.findBoatById(client, this.boatEntityId); + if (boat == null || boat.isRemoved()) { + // 船已销毁(可能玩家已捡起 / 服务器回收) + this.phase = PHASE_BREAK_WAIT; + this.phaseTicks = 0; + return; + } + // 必须面朝船才能命中(服务端通常校验朝向) + Vec3d playerEye = client.player.getEyePos(); + Vec3d toBoat = boat.getPos().add(0.0, boat.getHeight() * 0.5, 0.0).subtract(playerEye); + double horizontal = Math.sqrt(toBoat.x * toBoat.x + toBoat.z * toBoat.z); + float yaw = (float) (Math.toDegrees(Math.atan2(toBoat.z, toBoat.x)) - 90.0); + float pitch = (float) (-Math.toDegrees(Math.atan2(toBoat.y, horizontal))); + client.player.setYaw(yaw); + client.player.setPitch(MathHelper.clamp(pitch, -90.0f, 90.0f)); + client.interactionManager.attackEntity(client.player, boat); + this.phaseTicks++; + if (this.phaseTicks > 20) { + // 超时:放弃回收,直接进入下一步 + this.phase = PHASE_BREAK_WAIT; + this.phaseTicks = 0; + } + } + + /** 收尾:等待船销毁完成 + 释放蹲伏键让 Baritone 恢复正常寻路 */ + private void exitBreakWait(MinecraftClient client) { + if (this.holdingSneak) { + this.releaseSneak(client); + this.holdingSneak = false; + } + this.phaseTicks++; + if (this.phaseTicks < 3) { + return; + } + this.logPhaseOnce(client, "上岸并回收船完成"); + this.flow = IDLE; + this.phase = PHASE_PLACE; + this.phaseTicks = 0; + this.boatEntityId = -1; + this.restoreSelectedSlot(client); + } + + /** 按下蹲伏键(持续按住) */ + private void pressSneak(MinecraftClient client) { + if (client.player == null) return; + KeyBinding sneak = client.options.sneakKey; + sneak.setPressed(true); + } + + /** 释放蹲伏键 */ + private void releaseSneak(MinecraftClient client) { + if (client.player == null) return; + KeyBinding sneak = client.options.sneakKey; + sneak.setPressed(false); + } + + /** 在指定半径内查找最接近玩家的船实体 */ + private AbstractBoatEntity findNearbyBoat(MinecraftClient client, double radius) { + Vec3d center = client.player.getPos(); + AbstractBoatEntity best = null; + double bestDistance = Double.MAX_VALUE; + for (Entity entity : client.world.getOtherEntities(client.player, client.player.getBoundingBox().expand(radius))) { + if (!(entity instanceof AbstractBoatEntity boat)) continue; + if (boat.isRemoved()) continue; + double d = entity.squaredDistanceTo(center); + if (d < bestDistance) { + bestDistance = d; + best = boat; + } + } + return best; + } + + /** 按实体 ID 精确查找船实体(避免世界内多艘船的歧义) */ + private AbstractBoatEntity findBoatById(MinecraftClient client, int entityId) { + if (entityId < 0) return null; + Entity entity = client.world.getEntityById(entityId); + if (entity instanceof AbstractBoatEntity boat && !boat.isRemoved()) { + return boat; + } + return null; + } + + /** 仅在阶段切换时记录一次日志,避免每帧刷屏 */ + private void logPhaseOnce(MinecraftClient client, String message) { + if (message == null || message.equals(this.lastLoggedPhase)) { + return; + } + this.lastLoggedPhase = message; + if (client.player != null) { + client.player.sendMessage(net.minecraft.text.Text.literal("[AutoStore] " + message), false); + } + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 37f441b..95871f1 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1 +1 @@ -{"schemaVersion":1,"id":"autostore","version":"1.2.5","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"}]} \ No newline at end of file +{"schemaVersion":1,"id":"autostore","version":"1.3.0","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"}]} \ No newline at end of file