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