19 次代码提交
作者 SHA1 备注 提交日期
lyizai 385e0d13f1 v1.1.9: UI 运行日志区域改用小字号并自动换行,最新日志置顶显示 2026-08-19 10:45:29 +08:00
lyizai 1e99ff39e0 v1.1.9: 不在开拓时限内时向玩家发送聊天提示,并修复日志被 CANCELED 事件刷屏 2026-08-19 09:25:30 +08:00
lyizai c569065054 v1.1.9: 增加现实时间限制,仅 11:00-22:00 允许开拓 2026-08-19 08:19:03 +08:00
lyizai 3601bcd40e v1.1.9: 寻路超时改为重新寻路/跳过目标而非回城 2026-08-18 20:59:39 +08:00
lyizai e08dbb8104 v1.1.9: 增加运行时接壤校验与寻路失败监听,修复不接壤目标与大山寻路超时;新增日志导出 2026-08-18 20:40:04 +08:00
lyizai 7a45e6f676 v1.1.9: 修复区域扩地接壤判断误把其他邦国领土计入,导致选中与己方不相邻的目标 2026-08-18 20:04:26 +08:00
lyizai d2a52199ae v1.1.8: 刷新地图时核对已占领区块,丢弃地图未确认的超期假占领,修复高地空洞 2026-08-18 18:59:59 +08:00
lyizai 1f817199f7 v1.1.8: 寻路超时自动 /u return 回城自救
寻路超时后发送 /u return 回城,等待 10 秒传送回王城后重新寻路;重试次数耗尽则回城后跳过,避免整个系统因寻路卡死而停止。
2026-08-18 16:11:32 +08:00
lyizai 9ee4b41630 v1.1.8: 回退寻路逻辑至 1.1.3 并修复目标区块错位
回退:移除 1.1.4+ 引入的回城自救/接壤校验等寻路改动(导致玩家原地不动),恢复 1.1.3 的寻路行为。

修复错位:Baritone 自带 GoalXZ 容差为 0,精确到区块中心单格,中心不可达时玩家停在相邻区块造成目标错位与反复寻路。新增 ChunkGoal,完成条件改为「玩家脚下方块落在目标区块内」,与 playerChunk.equals(target) 判定一致。
2026-08-18 14:20:59 +08:00
lyizai 15b4a40b6d v1.1.7: 修复玩家不移动问题(移除寻路前相邻领土校验)
1.1.5 新增的「目标与己方领土接壤校验」在第三方领土地图刷新后,own 集合与规划器判定不一致,导致有效目标被误判跳过,玩家原地不动。已移除运行时的重复邻居验证(规划器已保证目标与己方领土相邻),仅保留「未被其它邦国占领」判断,恢复 1.1.2 及更早版本的行为。

构建:修正 build.gradle 使 baritone 嵌套打包进 META-INF/jars(1.1.6 缺失)。
2026-08-18 13:47:20 +08:00
lyizai b8e56dafcb v1.1.6: 新增 Fabric Loom 构建环境
- 添加 build.gradle / settings.gradle / gradle.properties / wrapper
- 嵌入 baritone-api-fabric-1.13.1.jar 依赖
- 添加 .gitignore
- 更新版本号至 1.1.6
2026-08-18 13:07:04 +08:00
Lyizai d0ed839d9f v1.1.5: 修复规划完成后不寻路问题
- isTargetStillValid 增加己方领土数据 fallback 与保守放行
- 修复地图刷新竞态导致 selfState 暂时为 null 时所有目标被跳过、玩家原地不动的问题
2026-08-17 13:07:51 +00:00
Lyizai 20c9cb10a3 docs: README 更新 v1.1.4 寻路优化说明 2026-08-17 12:29:29 +00:00
Lyizai e8f6009a66 v1.1.4: 寻路优化(避矿洞+超时回城自救+不接壤目标校验)
- Baritone设置:限制下落高度(10格)与轻微挖方块惩罚,减少走入矿洞,同时保留峡谷/高山通行
- 寻路超时且玩家在地下时自动执行 /u return 回城自救,等待10秒传送后重新规划
- 新增目标有效性校验:寻路/等待/重试前实时校验目标是否仍与己方领土接壤,不接壤立即跳过并重新规划,修复走到不接壤区块死等bug
2026-08-17 12:27:38 +00:00
Lyizai 8a2c4a21d8 v1.1.3: GUI左上角添加Git仓库与官网链接按钮(点击在系统浏览器打开) 2026-08-17 11:12:20 +00:00
Lyizai 134a6e6f20 v1.1.2: 美化UI并修复状态/进度文字重叠(底部信息区改单行截断+双栏面板) 2026-08-17 10:56:29 +00:00
Lyizai 84abb75573 fix: v1.1.1 修复区域扩地总是无可用区块(同步矩形圈选坐标到配置) 2026-08-17 10:20:22 +00:00
Lyizai e0aadf6564 docs: README 更新 v1.1.0 S形扩地说明 2026-08-17 09:43:04 +00:00
Lyizai 6cbd2a324a feat: v1.1.0 区域扩地改为S形扫过矩形(不沿邦国边界,仍保证接壤) 2026-08-17 09:26:58 +00:00
共修改 18 个文件,包含 1067 行新增和 107 行删除
+22
查看文件
@@ -0,0 +1,22 @@
# Gradle
.gradle/
build/
# Secrets
.env
# IDE
.idea/
*.iml
.vscode/
.settings/
.classpath
.project
bin/
# OS
Thumbs.db
.DS_Store
# Build temp
.buildtmp/
+9 -3
查看文件
@@ -6,15 +6,21 @@
- 从远程 `markers.json` 拉取各国领土边界 - 从远程 `markers.json` 拉取各国领土边界
- 四种扩地策略:方形外扩、区域扩地、玩家周围、绕国一圈 - 四种扩地策略:方形外扩、区域扩地、玩家周围、绕国一圈
- 自动寻路(Baritone)前往目标区块 - 区域扩地采用 S 形扫描:从与己方领土接壤的区块开始,横向/纵向 S 形扫过整个矩形,不沿邦国边界,但保证每个新占领区块都与**己方领土**接壤(仅四个正方向,不把其他邦国领土计入接壤)
- 自动寻路(Baritone)前往目标区块;监听 Baritone 路径事件,目标不可达(CALC_FAILED,如被悬崖/高山/水体隔断)时立即跳过;寻路超时则重新寻路(不立即回城),重试耗尽才跳过当前目标
- 运行时接壤校验:寻路/等待阶段每帧校验目标是否仍与己方领土接壤,地图刷新导致边界变化时立即跳过
- 目标有效性校验:目标若已被其他邦国占领则立即跳过
- 现实时间限制:仅 11:00-22:00(本地现实时间)允许开拓,其余时段启动会被拒绝、运行中到点自动停止
- 自动进食(饥饿时自动吃背包食物) - 自动进食(饥饿时自动吃背包食物)
- 实时地图 GUI(通过 Ctrl+R 打开) - 实时地图 GUI(通过 Ctrl+R 打开)
- 监听聊天/字幕消息判断开垦结果 - 监听聊天/字幕消息判断开垦结果
- 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞
- 日志导出:GUI 右上角“导出日志”按钮,或 `/autostore log` 命令,记录寻路、坐标、状态、路径事件等
## 安装 ## 安装
1. 安装 Fabric Loader 0.16.14+ 和 Fabric API 1. 安装 Fabric Loader 0.16.14+ 和 Fabric API
2. 将 `autostore-1.0.5.jar` 放入 `.minecraft/mods/` 目录 2. 将 `autostore-1.1.9.jar` 放入 `.minecraft/mods/` 目录
3. 启动游戏,按 `Ctrl+R` 打开 GUI 3. 启动游戏,按 `Ctrl+R` 打开 GUI
## 构建 ## 构建
@@ -24,4 +30,4 @@
./build_mod.sh ./build_mod.sh
``` ```
输出文件:`autostore-1.0.5.jar` 输出文件:`autostore-1.1.9.jar`
+57
查看文件
@@ -0,0 +1,57 @@
plugins {
id 'fabric-loom' version '1.17.19'
id 'maven-publish'
}
version = project.mod_version
group = project.maven_group
base {
archivesName = project.archives_base_name
}
// 用于把 Baritone 原样打包进模组 jar 的资源(不放默认依赖里,避免 Gradle 误解析)
configurations {
nestedJar
}
dependencies {
// Minecraft
minecraft "com.mojang:minecraft:${project.minecraft_version}"
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// Fabric API
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
// Baritone API (local jar, compile only)
modImplementation files("libs/baritone-api-fabric-1.13.1.jar")
// 标记需要嵌套打包的 jar
nestedJar files("libs/baritone-api-fabric-1.13.1.jar")
}
processResources {
inputs.property "version", project.version
filteringCharset "UTF-8"
filesMatching("fabric.mod.json") {
expand "version": project.version
}
// 将 Baritone 嵌套打包进 META-INF/jars,匹配 fabric.mod.json 的 jars 声明,
// 使玩家无需单独安装 baritone 依赖即可运行
from(configurations.nestedJar) {
into "META-INF/jars"
}
}
tasks.withType(JavaCompile).configureEach {
it.options.release = 21
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
+15
查看文件
@@ -0,0 +1,15 @@
org.gradle.jvmargs=-Xmx2G
org.gradle.parallel=true
# Fabric Properties
minecraft_version=1.21.4
yarn_mappings=1.21.4+build.8
loader_version=0.19.3
# Mod Properties
mod_version=1.1.9
maven_group=autostore
archives_base_name=autostore
# Fabric API
fabric_version=0.119.4+1.21.4
二进制
查看文件
二进制文件未显示。
+9
查看文件
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.7.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
第三方依赖
+248
查看文件
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
第三方依赖
+82
查看文件
@@ -0,0 +1,82 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
二进制文件未显示。
+12
查看文件
@@ -0,0 +1,12 @@
pluginManagement {
repositories {
maven {
name = 'Fabric'
url = 'https://maven.fabricmc.net/'
}
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = 'autostore'
@@ -7,10 +7,12 @@ package autostore.client.bridge;
import baritone.api.BaritoneAPI; import baritone.api.BaritoneAPI;
import baritone.api.IBaritone; import baritone.api.IBaritone;
import baritone.api.Settings; import baritone.api.Settings;
import baritone.api.pathing.goals.Goal; import baritone.api.event.events.PathEvent;
import baritone.api.pathing.goals.GoalXZ; import baritone.api.event.listener.AbstractGameEventListener;
import autostore.client.config.ExpansionConfig; import autostore.client.config.ExpansionConfig;
import autostore.client.territory.ChunkPos; import autostore.client.territory.ChunkPos;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
@@ -24,6 +26,25 @@ import net.fabricmc.api.Environment;
@Environment(EnvType.CLIENT) @Environment(EnvType.CLIENT)
public final class BaritoneBridge { 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 的全局设置。 * 将模组配置同步到 Baritone 的全局设置。
* *
@@ -39,7 +60,7 @@ public final class BaritoneBridge {
} }
/** /**
* 让 Baritone 寻路前往指定区块的中心。 * 让 Baritone 寻路前往指定区块(以“到达区块内”为完成条件)。
* *
* @param chunk 目标区块 * @param chunk 目标区块
*/ */
@@ -48,9 +69,11 @@ public final class BaritoneBridge {
if (baritone == null) { if (baritone == null) {
return; return;
} }
// 使用 GoalXZ 前往区块中心:Baritone 的 GoalXZ 自带约 10 格容差, // 使用自定义 ChunkGoal:完成条件为“玩家脚下方块落在目标区块内”,
// 只需到达区块中心附近即可,避免中心点不可达导致寻路卡死 // 与控制器 playerChunk.equals(target) 的判定一致。避免 Baritone 自带
baritone.getCustomGoalProcess().setGoalAndPath(new GoalXZ(chunk.centerBlockX(), chunk.centerBlockZ())); // GoalXZ(容差 0,精确到区块中心单格)在中心不可达时停在相邻区块,
// 造成目标区块错位与反复寻路。
baritone.getCustomGoalProcess().setGoalAndPath(new ChunkGoal(chunk));
} }
/** 强制取消当前寻路任务。 */ /** 强制取消当前寻路任务。 */
@@ -75,12 +98,30 @@ public final class BaritoneBridge {
/** /**
* 获取 Baritone 主实例;加载失败或异常时返回 null。 * 获取 Baritone 主实例;加载失败或异常时返回 null。
* 首次成功获取时,向 Baritone 事件总线注册一个转发器,
* 把路径事件分发给所有已注册的 {@link PathListener}。
* *
* @return 主 Baritone 实例,或 null * @return 主 Baritone 实例,或 null
*/ */
private IBaritone primary() { private IBaritone primary() {
try { 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) { } catch (Throwable ignored) {
return null; 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), * AutoStore 客户端命令:注册 /autostore 系列命令。
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
*/ */
package autostore.client.command; package autostore.client.command;
@@ -10,11 +9,9 @@ import autostore.client.controller.ExpansionController;
import autostore.client.territory.ChunkPos; import autostore.client.territory.ChunkPos;
import autostore.client.territory.TerritoryState; import autostore.client.territory.TerritoryState;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
@@ -37,6 +34,7 @@ import net.minecraft.text.Text;
* <li>/autostore state &lt;邦国名&gt; —— 设置目标邦国</li> * <li>/autostore state &lt;邦国名&gt; —— 设置目标邦国</li>
* <li>/autostore strategy &lt;1-4&gt; —— 设置扩地策略</li> * <li>/autostore strategy &lt;1-4&gt; —— 设置扩地策略</li>
* <li>/autostore rings &lt;层数&gt; —— 设置扩圈上限(0 为持续)</li> * <li>/autostore rings &lt;层数&gt; —— 设置扩圈上限(0 为持续)</li>
* <li>/autostore log —— 导出运行日志到文件</li>
* </ul> * </ul>
*/ */
public final class AutoStoreCommands { public final class AutoStoreCommands {
@@ -45,50 +43,108 @@ public final class AutoStoreCommands {
/** 注册全部 /autostore 子命令到命令分发器 */ /** 注册全部 /autostore 子命令到命令分发器 */
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, AutoStoreModClient mod) { public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, AutoStoreModClient mod) {
dispatcher.register((((((((((ClientCommandManager.literal((String)"autostore").then(ClientCommandManager.literal((String)"gui").executes(ctx -> { LiteralArgumentBuilder<FabricClientCommandSource> root = ClientCommandManager.literal("autostore");
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u6253\u5f00 AutoStore GUI"));
// 打开 GUI
root.then(ClientCommandManager.literal("gui").executes(ctx -> {
ctx.getSource().sendFeedback(Text.literal("已打开 AutoStore GUI"));
mod.openGui(); mod.openGui();
return 1; 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(); mod.controller().stop();
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u505c\u6b62\u81ea\u52a8\u6269\u5730")); ctx.getSource().sendFeedback(Text.literal("已停止自动扩地"));
return 1; 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(); mod.dataService().refreshAsync();
(ctx.getSource()).sendFeedback(Text.literal((String)"\u6b63\u5728\u5237\u65b0\u90a6\u56fd\u5730\u56fe")); ctx.getSource().sendFeedback(Text.literal("正在刷新邦国地图"));
return 1; 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) { if (state == null) {
(ctx.getSource()).sendError(Text.literal((String)"\u65e0\u6cd5\u8bc6\u522b\u5f53\u524d\u90a6\u56fd")); ctx.getSource().sendError(Text.literal("无法识别当前邦国"));
} else { } else {
mod.config().stateName = state.name(); mod.config().stateName = state.name();
mod.saveConfig(); 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; 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(); mod.saveConfig();
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u8bbe\u7f6e\u90a6\u56fd\uff1a" + name))); ctx.getSource().sendFeedback(Text.literal("已设置邦国:" + 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"));
return 1; 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 命令处理:复制当前配置并覆盖目标数量与策略,然后启动扩地 */ /** /autostore start 命令处理:复制当前配置并覆盖目标数量与策略,然后启动扩地 */
@@ -100,15 +156,20 @@ public final class AutoStoreCommands {
mod.config().strategy = strategy; mod.config().strategy = strategy;
mod.saveConfig(); mod.saveConfig();
boolean started = mod.controller().start(config); 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; return started ? 1 : 0;
} }
/** /autostore status 命令处理:汇总状态、进度、队列、策略与邦国信息并反馈 */ /** /autostore status 命令处理:汇总状态、进度、队列、策略与邦国信息并反馈 */
private static int status(FabricClientCommandSource source, AutoStoreModClient mod) { private static int status(FabricClientCommandSource source, AutoStoreModClient mod) {
ExpansionController controller = mod.controller(); 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); String text = "状态:" + controller.status()
source.sendFeedback(Text.literal((String)text)); + " | 进度:" + 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; return 1;
} }
@@ -118,4 +179,3 @@ public final class AutoStoreCommands {
return ChunkPos.fromBlock(pos.getX(), pos.getZ()); 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.TerritoryDataService;
import autostore.client.territory.TerritoryModel; import autostore.client.territory.TerritoryModel;
import autostore.client.territory.TerritoryState; 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.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Deque; import java.util.Deque;
@@ -20,6 +27,7 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.util.Hand; import net.minecraft.util.Hand;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerInventory;
@@ -27,6 +35,7 @@ import net.minecraft.item.ItemStack;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.component.DataComponentTypes; import net.minecraft.component.DataComponentTypes;
import net.minecraft.text.Text;
@Environment(value=EnvType.CLIENT) @Environment(value=EnvType.CLIENT)
/** /**
@@ -37,9 +46,15 @@ import net.minecraft.component.DataComponentTypes;
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。 * 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
*/ */
public final class ExpansionController public final class ExpansionController
implements SubtitleChatMonitor.Listener { implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
/** 日志环形缓冲区的最大条数 */ /** 日志环形缓冲区的最大条数(含导出用详细日志,故调大) */
private static final int MAX_LOGS = 80; 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 寻路桥接(负责自动移动) */ /** Baritone 寻路桥接(负责自动移动) */
private final BaritoneBridge baritoneBridge = new BaritoneBridge(); private final BaritoneBridge baritoneBridge = new BaritoneBridge();
/** 扩地规划器(决定下一个目标区块) */ /** 扩地规划器(决定下一个目标区块) */
@@ -76,9 +91,13 @@ implements SubtitleChatMonitor.Listener {
private final Set<ChunkPos> claimedSet = new HashSet<>(); private final Set<ChunkPos> claimedSet = new HashSet<>();
/** 是否正在自动进食 */ /** 是否正在自动进食 */
private boolean eating; private boolean eating;
/** Baritone 最近一次路径计算是否失败(目标不可达);由事件监听设置,寻路阶段消费 */
private volatile boolean pathCalcFailed;
public ExpansionController(TerritoryDataService dataService) { public ExpansionController(TerritoryDataService dataService) {
this.dataService = dataService; this.dataService = dataService;
// 注册路径事件监听,及时识别“目标不可达”并跳过(避免大山/悬崖地形傻等超时)
this.baritoneBridge.addPathListener(this);
} }
public SubtitleChatMonitor monitor() { public SubtitleChatMonitor monitor() {
@@ -121,6 +140,17 @@ implements SubtitleChatMonitor.Listener {
return this.state != State.IDLE && this.state != State.COMPLETE && this.state != State.STOPPED; 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;
}
/** /**
* 启动自动扩地任务。 * 启动自动扩地任务。
* *
@@ -141,6 +171,15 @@ implements SubtitleChatMonitor.Listener {
this.status = "Baritone \u5c1a\u672a\u521d\u59cb\u5316\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5"; this.status = "Baritone \u5c1a\u672a\u521d\u59cb\u5316\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5";
return false; 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.config = requestedConfig.copy();
this.model = this.dataService.model(); this.model = this.dataService.model();
this.playerChunk = this.playerChunk(client); this.playerChunk = this.playerChunk(client);
@@ -201,6 +240,14 @@ implements SubtitleChatMonitor.Listener {
return; return;
} }
this.playerChunk = this.playerChunk(client); 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)) { if (this.tickAutoFood(client)) {
return; return;
} }
@@ -273,11 +320,12 @@ implements SubtitleChatMonitor.Listener {
this.lastProgress = 0; this.lastProgress = 0;
this.baritoneBridge.pathTo(this.target); this.baritoneBridge.pathTo(this.target);
this.setState(State.PATHING, "\u6b63\u5728\u524d\u5f80\u76ee\u6807\u533a\u5757 " + this.target.x() + "," + this.target.z()); 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() { private void tickPathing() {
if (this.target == null) { if (this.target == null) {
@@ -288,23 +336,37 @@ implements SubtitleChatMonitor.Listener {
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) { if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
return; 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)) { if (this.playerChunk.equals(this.target)) {
this.baritoneBridge.cancel(); this.baritoneBridge.cancel();
this.stageStartedMillis = System.currentTimeMillis(); this.stageStartedMillis = System.currentTimeMillis();
this.setState(State.WAITING, "\u5df2\u5230\u8fbe\uff0c\u7b49\u5f85\u9886\u5730\u5f00\u62d3"); 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; return;
} }
long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L; long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L;
if (elapsedSeconds >= this.config.waitTimeoutSeconds) { if (elapsedSeconds >= this.config.waitTimeoutSeconds) {
++this.retryCount; ++this.retryCount;
// 主动刷新地图,尽快同步"区块已被占领"的最新状态,避免玩家被困 // 主动刷新地图,尽快同步最新领土状态
this.dataService.refreshAsync(); this.dataService.refreshAsync();
if (this.retryCount >= this.config.retries) { if (this.retryCount >= this.config.retries) {
// 重试耗尽:跳过当前目标(不再回城),交给下一个目标
this.log("\u5bfb\u8def\u8d85\u65f6\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z()); this.log("\u5bfb\u8def\u8d85\u65f6\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
this.setState(State.FAIL, "\u5bfb\u8def\u8d85\u65f6"); this.setState(State.FAIL, "\u5bfb\u8def\u8d85\u65f6");
} else { } 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");
} }
} }
} }
@@ -319,6 +381,10 @@ implements SubtitleChatMonitor.Listener {
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) { if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
return; return;
} }
// 目标已不再与己方领土接壤:立即跳过,避免在无效区块上干等开垦
if (this.skipIfTargetNotAdjacent()) {
return;
}
// 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去 // 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去
if (!this.playerChunk.equals(this.target)) { if (!this.playerChunk.equals(this.target)) {
this.retrySameTarget("\u79bb\u5f00\u76ee\u6807\u533a\u5757\uff0c\u91cd\u65b0\u524d\u5f80 " + this.target.x() + "," + this.target.z()); this.retrySameTarget("\u79bb\u5f00\u76ee\u6807\u533a\u5757\uff0c\u91cd\u65b0\u524d\u5f80 " + this.target.x() + "," + this.target.z());
@@ -365,6 +431,44 @@ implements SubtitleChatMonitor.Listener {
return false; return false;
} }
/**
* 若当前目标区块不再与己方领土(含本次会话已占领)的四个正方向接壤,
* 则跳过并进入 NEXT 状态,返回 true。
*
* <p>这是解决“走到不接壤区块”的关键:规划时目标必然接壤,但寻路/等待过程中
* 领土数据会异步刷新(边界变化、假占领被剔除等),导致目标可能变得不接壤。
* 在寻路与等待阶段每帧校验,一旦发现不接壤立即放弃,避免在无效区块上白费时间。</p>
*/
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;
}
return false;
}
/**
* 判断区块是否与己方领土(己方邦国地图区块 + 本次会话新占领区块)接壤。
*
* <p>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。</p>
*/
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());
}
own.addAll(model.sessionClaims());
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
if (own.contains(neighbor)) {
return true;
}
}
return false;
}
/** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */ /** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */
private void tickSuccess() { private void tickSuccess() {
this.setState(State.NEXT, "\u5df2\u5b8c\u6210 " + this.completedClaims + (this.config.targetCount > 0 ? "/" + this.config.targetCount : "/\u6301\u7eed")); this.setState(State.NEXT, "\u5df2\u5b8c\u6210 " + this.completedClaims + (this.config.targetCount > 0 ? "/" + this.config.targetCount : "/\u6301\u7eed"));
@@ -425,6 +529,29 @@ implements SubtitleChatMonitor.Listener {
return min == Integer.MAX_VALUE ? 0 : min; 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%) */ /** 监听器回调:开垦进度更新(0-100%) */
@Override @Override
public void onProgress(int percent) { public void onProgress(int percent) {
@@ -563,7 +690,7 @@ implements SubtitleChatMonitor.Listener {
} }
this.baritoneBridge.cancel(); this.baritoneBridge.cancel();
client.player.getInventory().selectedSlot = slot; client.player.getInventory().selectedSlot = slot;
client.interactionManager.interactItem(client.player, Hand.field_5808); client.interactionManager.interactItem(client.player, Hand.MAIN_HAND);
this.eating = true; this.eating = true;
this.status = "\u81ea\u52a8\u8fdb\u98df\u4e2d"; this.status = "\u81ea\u52a8\u8fdb\u98df\u4e2d";
this.log("\u81ea\u52a8\u8fdb\u98df\uff0c\u9965\u997f\u503c " + foodLevel); this.log("\u81ea\u52a8\u8fdb\u98df\uff0c\u9965\u997f\u503c " + foodLevel);
@@ -574,7 +701,7 @@ implements SubtitleChatMonitor.Listener {
private int findFoodSlot(PlayerInventory inventory) { private int findFoodSlot(PlayerInventory inventory) {
for (int slot = 0; slot < 36; ++slot) { for (int slot = 0; slot < 36; ++slot) {
ItemStack stack = inventory.getStack(slot); ItemStack stack = inventory.getStack(slot);
if (stack.isEmpty() || stack.getComponents().get(DataComponentTypes.field_50075) == null) continue; if (stack.isEmpty() || stack.getComponents().get(DataComponentTypes.FOOD) == null) continue;
return slot; return slot;
} }
return -1; return -1;
@@ -599,13 +726,42 @@ implements SubtitleChatMonitor.Listener {
private void log(String message) { private void log(String message) {
Deque<String> deque = this.logs; Deque<String> deque = this.logs;
synchronized (deque) { synchronized (deque) {
this.logs.addLast(System.currentTimeMillis() + " " + message); this.logs.addLast("[" + LocalTime.now().format(LOG_TIME_FORMAT) + "] " + message);
while (this.logs.size() > 80) { while (this.logs.size() > MAX_LOGS) {
this.logs.removeFirst(); 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> * <ul>
@@ -17,6 +17,7 @@ import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import net.fabricmc.api.EnvType; import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment; import net.fabricmc.api.Environment;
import net.minecraft.util.Util;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.client.font.TextRenderer; import net.minecraft.client.font.TextRenderer;
@@ -27,6 +28,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.CheckboxWidget; import net.minecraft.client.gui.widget.CheckboxWidget;
import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.Screen;
import net.minecraft.text.StringVisitable; import net.minecraft.text.StringVisitable;
import net.minecraft.text.OrderedText;
@Environment(value=EnvType.CLIENT) @Environment(value=EnvType.CLIENT)
/** /**
@@ -38,6 +40,9 @@ import net.minecraft.text.StringVisitable;
*/ */
public final class AutoStoreGui public final class AutoStoreGui
extends Screen { extends Screen {
// ---- 外部链接 ----
private static final String GIT_REPO_URL = "https://git.bgjq.top/Lyizai/AutoStore/";
private static final String HOMEPAGE_URL = "https://8w.bgjq.top/";
private final AutoStoreModClient mod; private final AutoStoreModClient mod;
private final ExpansionConfig config; private final ExpansionConfig config;
private final ExpansionController controller; private final ExpansionController controller;
@@ -89,7 +94,7 @@ extends Screen {
this.mapY = 40; this.mapY = 40;
this.mapW = Math.max(120, this.panelX - this.mapX - 8); this.mapW = Math.max(120, this.panelX - this.mapX - 8);
// 底部信息区高度(日志 + 状态文本),地图填满其上方空间 // 底部信息区高度(日志 + 状态文本),地图填满其上方空间
int infoHeight = 78; int infoHeight = 88;
this.mapH = Math.max(140, this.height - this.mapY - infoHeight - 8); this.mapH = Math.max(140, this.height - this.mapY - infoHeight - 8);
// 屏幕较矮时压缩垂直间距,保证右侧面板内容不被截断 // 屏幕较矮时压缩垂直间距,保证右侧面板内容不被截断
@@ -99,6 +104,11 @@ extends Screen {
// ===== 顶部控制按钮 ===== // ===== 顶部控制按钮 =====
int topX = this.panelX; int topX = this.panelX;
// 左上角外部链接按钮: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 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(); ButtonWidget stopButton = ButtonWidget.builder(Text.literal((String)"\u505c\u6b62"), button -> this.controller.stop()).dimensions(topX + 160, 34, 74, 30).build();
this.addDrawableChild(startButton); this.addDrawableChild(startButton);
@@ -158,6 +168,27 @@ extends Screen {
this.initializeMapView(); this.initializeMapView();
} }
/** 在系统默认浏览器中打开外部链接(失败时静默忽略) */
private void openUrl(String url) {
try {
Util.getOperatingSystem().open(new java.net.URI(url));
} catch (Exception ignored) {
// 链接打开失败不影响界面正常使用
}
}
/** 导出运行日志到文件,并在聊天栏提示导出结果与路径 */
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 位) */ /** 创建整数输入框(初始值为 value,最多 12 位) */
private TextFieldWidget numberField(int value, int x, int y, int width) { private TextFieldWidget numberField(int value, int x, int y, int width) {
TextFieldWidget field = new TextFieldWidget(this.textRenderer, x, y, width, 20, Text.literal((String)"")); TextFieldWidget field = new TextFieldWidget(this.textRenderer, x, y, width, 20, Text.literal((String)""));
@@ -422,26 +453,63 @@ extends Screen {
/** 绘制右侧信息面板:字段标签、运行日志与状态/进度信息 */ /** 绘制右侧信息面板:字段标签、运行日志与状态/进度信息 */
private void drawInfo(DrawContext context) { private void drawInfo(DrawContext context) {
int shown; // ===== 右侧配置面板字段标签 =====
context.drawTextWithShadow(this.textRenderer, "\u90a6\u56fd", this.panelX + 4, this.stateFieldY + 4, -5592406); context.drawTextWithShadow(this.textRenderer, "\u90a6\u56fd", this.panelX + 4, this.stateFieldY + 4, -5592406);
context.drawTextWithShadow(this.textRenderer, "\u76ee\u6807\u6570\u91cf", this.panelX + 4, this.targetFieldY + 4, -5592406); context.drawTextWithShadow(this.textRenderer, "\u76ee\u6807\u6570\u91cf", this.panelX + 4, this.targetFieldY + 4, -5592406);
context.drawTextWithShadow(this.textRenderer, "\u6743\u9650\u8bbe\u7f6e", this.panelX + 4, this.advancedY, -5592406); context.drawTextWithShadow(this.textRenderer, "\u6743\u9650\u8bbe\u7f6e", this.panelX + 4, this.advancedY, -5592406);
int belowY = this.mapY + this.mapH + 12;
// ===== 底部信息面板:日志(左)+ 状态(右),均单行截断,避免换行导致的重叠 =====
int infoTop = this.mapY + this.mapH + 10;
int logWidth = Math.max(120, (int)(this.mapW * 0.52)); int logWidth = Math.max(120, (int)(this.mapW * 0.52));
int statusX = this.mapX + logWidth + 20; int statusX = this.mapX + logWidth + 24;
int statusWidth = this.mapW - logWidth - 20; int statusWidth = this.mapW - logWidth - 24;
// 半透明面板背景,使信息区与地图明显分隔
context.fill(this.mapX, infoTop - 2, this.mapX + this.mapW, infoTop + 74, 0x30000000);
// 两栏标题与竖向分隔线
context.drawTextWithShadow(this.textRenderer, "\u8fd0\u884c\u65e5\u5fd7", this.mapX + 2, infoTop, -5592406);
context.drawTextWithShadow(this.textRenderer, "\u72b6\u6001\u4fe1\u606f", statusX, infoTop, -5592406);
context.fill(statusX - 12, infoTop - 1, statusX - 11, infoTop + 58, -12761518);
// 运行日志:使用较小字体并自动换行,最新日志在最上方,逐行向下显示直到填满可视高度
List<String> logs = this.controller.logs(); List<String> logs = this.controller.logs();
for (int i = shown = Math.min(4, logs.size()); i > 0; --i) { float logScale = 0.75f; // 日志字号缩放(比默认小一号)
String trimmed = this.textRenderer.trimToWidth(logs.get(logs.size() - i), logWidth); int logAreaTop = infoTop + 12;
context.drawTextWithShadow(this.textRenderer, trimmed, this.mapX, belowY + (shown - i) * 12, -6577233); 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; int target = this.config.targetCount;
String targetText = target > 0 ? Integer.toString(target) : "\u6301\u7eed"; String targetText = target > 0 ? Integer.toString(target) : "\u6301\u7eed";
context.drawWrappedTextWithShadow(this.textRenderer, Text.literal(("\u72b6\u6001\uff1a" + this.controller.status())), statusX, belowY, statusWidth, -1); String error = this.mod.dataService().lastError();
context.drawWrappedTextWithShadow(this.textRenderer, Text.literal(("\u8fdb\u5ea6\uff1a" + this.controller.completedClaims() + " / " + targetText + "\uff0c\u7b49\u5f85\uff1a" + this.controller.lastProgress() + "%")), statusX, belowY + 14, statusWidth, -1); String[] statusLines = new String[]{
context.drawWrappedTextWithShadow(this.textRenderer, Text.literal(("\u961f\u5217\uff1a" + this.controller.queuedTargets() + "\uff0c\u6570\u636e\u9519\u8bef\uff1a" + (this.mod.dataService().lastError().isEmpty() ? "\u65e0" : this.mod.dataService().lastError()))), statusX, belowY + 30, statusWidth, -1); "\u72b6\u6001\uff1a" + this.controller.status(),
context.drawWrappedTextWithShadow(this.textRenderer, Text.literal(this.refreshStatusText()), statusX, belowY + 46, statusWidth, -5592406); "\u8fdb\u5ea6\uff1a" + this.controller.completedClaims() + " / " + targetText + "\uff0c\u7b49\u5f85\uff1a" + this.controller.lastProgress() + "%",
context.drawWrappedTextWithShadow(this.textRenderer, Text.literal((String)"\u5f00\u59cb\u524d\u8bf7\u786e\u8ba4\u5df2\u624b\u52a8\u6267\u884c /u claimmode"), statusX, belowY + 62, statusWidth, -13210); "\u961f\u5217\uff1a" + this.controller.queuedTargets() + "\uff0c\u6570\u636e\u9519\u8bef\uff1a" + (error.isEmpty() ? "\u65e0" : error),
this.refreshStatusText()
};
for (int i = 0; i < statusLines.length; ++i) {
String trimmed = this.textRenderer.trimToWidth(statusLines[i], statusWidth);
context.drawTextWithShadow(this.textRenderer, trimmed, statusX, infoTop + 12 + i * 12, -1);
}
// 底部整行提示
String hint = this.textRenderer.trimToWidth("\u5f00\u59cb\u524d\u8bf7\u786e\u8ba4\u5df2\u624b\u52a8\u6267\u884c /u claimmode", this.mapW);
context.drawTextWithShadow(this.textRenderer, hint, this.mapX + 2, infoTop + 62, -13210);
} }
/** 生成地图刷新状态文本(显示上次成功刷新距今的时间) */ /** 生成地图刷新状态文本(显示上次成功刷新距今的时间) */
@@ -500,6 +568,11 @@ extends Screen {
target.avoidMobs = this.config.avoidMobs; target.avoidMobs = this.config.avoidMobs;
target.allowBreak = this.config.allowBreak; target.allowBreak = this.config.allowBreak;
target.allowPlace = this.config.allowPlace; target.allowPlace = this.config.allowPlace;
// 同步矩形圈选坐标到全局配置(修复:之前忘记同步,导致区域扩地总是为空)
target.regionMinX = this.config.regionMinX;
target.regionMaxX = this.config.regionMaxX;
target.regionMinZ = this.config.regionMinZ;
target.regionMaxZ = this.config.regionMaxZ;
this.mod.saveConfig(); this.mod.saveConfig();
} }
@@ -8,7 +8,6 @@ import autostore.client.config.ExpansionConfig;
import autostore.client.territory.ChunkPos; import autostore.client.territory.ChunkPos;
import autostore.client.territory.TerritoryModel; import autostore.client.territory.TerritoryModel;
import autostore.client.territory.TerritoryState; import autostore.client.territory.TerritoryState;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
@@ -191,11 +190,13 @@ public final class ExpansionPlanner {
} }
/** /**
* 策略 2 矩形区域扩地:完全按玩家圈选的矩形区域扩地。 * 策略 2 矩形区域扩地:在玩家圈选的矩形内,从与现有领土接壤的区块开始,
* 横向或纵向 S 形扫过整个矩形。
* *
* <p>先检查矩形区域是否与己方领土接壤(矩形内至少有一个未占领区块与领土相邻), * <p>与旧版"沿邦国边界逐层向内 BFS"不同,本算法不再贴着邦国现有边界,
* 若未接壤则返回空列表(不扩地)。接壤后,从接壤边界开始 BFS 逐层向内, * 而是生成一条贯穿矩形的 S 形路径:从接壤起点开始,逐行/逐列交替反向扫过,
* 将矩形内所有未被占领的区块全部纳入计划,保证每个区块都与已占领领土连通。 * 保证路径上每个区块都与已占领领土(含己方领土、本次会话新占领)接壤,
* 矩形内已占领的区块自动跳过、不重复占领。
*/ */
private List<ChunkPos> generateRectangle(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) { private List<ChunkPos> generateRectangle(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) {
Set<ChunkPos> actualOwn = this.ownChunks(model); Set<ChunkPos> actualOwn = this.ownChunks(model);
@@ -208,48 +209,139 @@ public final class ExpansionPlanner {
int minZ = config.regionMinZ; int minZ = config.regionMinZ;
int maxZ = config.regionMaxZ; int maxZ = config.regionMaxZ;
int limit = config.targetCount > 0 ? config.targetCount : Integer.MAX_VALUE; int limit = config.targetCount > 0 ? config.targetCount : Integer.MAX_VALUE;
// 检查矩形是否与己方领土接壤:矩形内至少有一个未占领区块与领土相邻 // 找出矩形内所有与己方领土接壤的未占领区块(作为 S 形起点候选)
boolean bordersOwn = false; ArrayList<ChunkPos> borderChunks = new ArrayList<ChunkPos>();
for (int x = minX; x <= maxX; ++x) { for (int x = minX; x <= maxX; ++x) {
for (int z = minZ; z <= maxZ; ++z) { for (int z = minZ; z <= maxZ; ++z) {
ChunkPos chunk = new ChunkPos(x, z); ChunkPos chunk = new ChunkPos(x, z);
if (allClaimed.contains(chunk)) continue; if (allClaimed.contains(chunk)) continue;
if (this.ownNeighbors(chunk, actualOwn) > 0) { if (this.ownNeighbors(chunk, actualOwn) > 0) {
bordersOwn = true; borderChunks.add(chunk);
break;
} }
} }
if (bordersOwn) break;
} }
if (!bordersOwn) { if (borderChunks.isEmpty()) {
return List.of(); return List.of();
} }
// 初始边界:矩形内与己方领土(原始领土)直接相邻的未占领区块 // 选择起点:优先矩形角落附近与领土接壤的区块,使 S 形能自然覆盖整个矩形
HashSet<ChunkPos> plannedOwn = new HashSet<ChunkPos>(actualOwn); ChunkPos start = this.chooseStart(borderChunks, minX, maxX, minZ, maxZ);
ArrayDeque<ChunkPos> frontier = new ArrayDeque<ChunkPos>(); // 生成 S 形扫描路径(覆盖矩形内所有区块,含已占领的)
List<ChunkPos> sPath = this.buildSPath(start, minX, maxX, minZ, maxZ);
// 己方领土集合(含本次规划已纳入的区块)。
// 注意:这里绝不能把其他邦国的领土(allClaimed)混入接壤判断集合,
// 否则会选中一个“挨着其他邦国、但与己方领土不相邻”的区块,
// 服务器会因不接壤而拒绝开拓,导致玩家反复走到无效目标(历史 bug)。
HashSet<ChunkPos> claimed = new HashSet<ChunkPos>(actualOwn);
ArrayList<ChunkPos> result = new ArrayList<ChunkPos>(); ArrayList<ChunkPos> result = new ArrayList<ChunkPos>();
for (int x = minX; x <= maxX; ++x) { for (ChunkPos chunk : sPath) {
for (int z = minZ; z <= maxZ; ++z) { if (result.size() >= limit) break;
ChunkPos chunk = new ChunkPos(x, z); // 已被任何邦国(含己方)占领,或本次规划已纳入:跳过,避免重复/跨邦国开拓
if (allClaimed.contains(chunk) || plannedOwn.contains(chunk)) continue; if (allClaimed.contains(chunk) || claimed.contains(chunk)) continue;
if (this.ownNeighbors(chunk, actualOwn) > 0) { // 必须与“己方”领土接壤(含本次已纳入的区块),保证服务器能成功开拓
plannedOwn.add(chunk); if (this.ownNeighbors(chunk, claimed) == 0) continue;
frontier.add(chunk); result.add(chunk);
claimed.add(chunk);
}
return result;
}
/**
* 从接壤候选区块中选择 S 形扫描的起点:
* 优先选择矩形角落上的接壤区块;没有角落接壤时,选择离任一角落最近的接壤区块。
*/
private ChunkPos chooseStart(List<ChunkPos> borderChunks, int minX, int maxX, int minZ, int maxZ) {
ChunkPos[] corners = new ChunkPos[]{new ChunkPos(minX, minZ), new ChunkPos(maxX, minZ), new ChunkPos(minX, maxZ), new ChunkPos(maxX, maxZ)};
for (ChunkPos corner : corners) {
if (borderChunks.contains(corner)) {
return corner;
}
}
ChunkPos best = null;
int bestDist = Integer.MAX_VALUE;
for (ChunkPos chunk : borderChunks) {
for (ChunkPos corner : corners) {
int dist = Math.abs(chunk.x() - corner.x()) + Math.abs(chunk.z() - corner.z());
if (dist < bestDist) {
bestDist = dist;
best = chunk;
} }
} }
} }
// BFS 逐层向内填满整个矩形 return best;
while (!frontier.isEmpty() && result.size() < limit) { }
ChunkPos chunk = frontier.removeFirst();
result.add(chunk); /**
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) { * 生成从起点开始的 S 形扫描路径,覆盖矩形内所有区块。
if (neighbor.x() < minX || neighbor.x() > maxX || neighbor.z() < minZ || neighbor.z() > maxZ) continue; *
if (allClaimed.contains(neighbor) || plannedOwn.contains(neighbor)) continue; * <p>扫描主轴根据起点所在边缘决定:起点在顶/底行时横向逐行扫描,
plannedOwn.add(neighbor); * 起点在左/右列时纵向逐列扫描。每行/列内方向交替反向,形成 S 形。
frontier.add(neighbor); * 起点所在行/列从起点向两侧铺开,保证起点是路径第一个区块。
*/
private List<ChunkPos> buildSPath(ChunkPos start, int minX, int maxX, int minZ, int maxZ) {
ArrayList<ChunkPos> path = new ArrayList<ChunkPos>();
boolean horizontal = start.z() == minZ || start.z() == maxZ;
if (horizontal) {
int startRow = start.z();
// 起点行:从起点向一侧到边缘,再反向到另一侧,完整覆盖该行
boolean leftToRight = start.x() == minX;
if (leftToRight) {
for (int x = start.x(); x <= maxX; ++x) path.add(new ChunkPos(x, startRow));
for (int x = start.x() - 1; x >= minX; --x) path.add(new ChunkPos(x, startRow));
} else {
for (int x = start.x(); x >= minX; --x) path.add(new ChunkPos(x, startRow));
for (int x = start.x() + 1; x <= maxX; ++x) path.add(new ChunkPos(x, startRow));
}
// 其余行:先向下(+z)再向上(-z),每行方向交替,形成 S 形
boolean ltr = leftToRight;
for (int z = startRow + 1; z <= maxZ; ++z) {
this.appendRow(path, z, minX, maxX, ltr);
ltr = !ltr;
}
for (int z = startRow - 1; z >= minZ; --z) {
this.appendRow(path, z, minX, maxX, ltr);
ltr = !ltr;
}
} else {
int startCol = start.x();
// 起点列:从起点向一侧到边缘,再反向到另一侧,完整覆盖该列
boolean topToBottom = start.z() == minZ;
if (topToBottom) {
for (int z = start.z(); z <= maxZ; ++z) path.add(new ChunkPos(startCol, z));
for (int z = start.z() - 1; z >= minZ; --z) path.add(new ChunkPos(startCol, z));
} else {
for (int z = start.z(); z >= minZ; --z) path.add(new ChunkPos(startCol, z));
for (int z = start.z() + 1; z <= maxZ; ++z) path.add(new ChunkPos(startCol, z));
}
// 其余列:先向右(+x)再向左(-x),每列方向交替,形成 S 形
boolean ttb = topToBottom;
for (int x = startCol + 1; x <= maxX; ++x) {
this.appendCol(path, x, minZ, maxZ, ttb);
ttb = !ttb;
}
for (int x = startCol - 1; x >= minX; --x) {
this.appendCol(path, x, minZ, maxZ, ttb);
ttb = !ttb;
} }
} }
return result; return path;
}
/** 追加一行区块到路径(leftToRight=true 时从左到右,否则从右到左) */
private void appendRow(List<ChunkPos> path, int row, int minX, int maxX, boolean leftToRight) {
if (leftToRight) {
for (int x = minX; x <= maxX; ++x) path.add(new ChunkPos(x, row));
} else {
for (int x = maxX; x >= minX; --x) path.add(new ChunkPos(x, row));
}
}
/** 追加一列区块到路径(topToBottom=true 时从上到下,否则从下到上) */
private void appendCol(List<ChunkPos> path, int col, int minZ, int maxZ, boolean topToBottom) {
if (topToBottom) {
for (int z = minZ; z <= maxZ; ++z) path.add(new ChunkPos(col, z));
} else {
for (int z = maxZ; z >= minZ; --z) path.add(new ChunkPos(col, z));
}
} }
/** 在候选集中找到离当前游标最近的一个区块(曼哈顿距离),作为推进的前沿 */ /** 在候选集中找到离当前游标最近的一个区块(曼哈顿距离),作为推进的前沿 */
@@ -389,7 +481,13 @@ public final class ExpansionPlanner {
return Math.max(dx, dz = Math.abs(chunk.z() - config.regionCenterZ)) <= Math.max(1, config.regionRadius); 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) { private int ownNeighbors(ChunkPos chunk, Set<ChunkPos> own) {
int count = 0; int count = 0;
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) { for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
@@ -24,6 +24,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -65,6 +66,16 @@ public final class TerritoryDataService {
private volatile long lastSuccessfulFetchMillis; private volatile long lastSuccessfulFetchMillis;
/** 最近一次尝试拉取的时间戳(毫秒) */ /** 最近一次尝试拉取的时间戳(毫秒) */
private volatile long lastAttemptMillis; 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() { public TerritoryDataService() {
this.executor = TerritoryDataService.newExecutor(); this.executor = TerritoryDataService.newExecutor();
@@ -111,8 +122,8 @@ public final class TerritoryDataService {
/** /**
* 异步刷新领土数据(在调度线程中执行)。 * 异步刷新领土数据(在调度线程中执行)。
* 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"合并进新模型, * 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"与刚拉取的最新地图数据仔细核对,
* 避免刷新覆盖掉本次会话的占领记录。 * 只保留"已被地图确认"或"刚占领未超宽限期"的占领记录,避免刷新时把假占领永久留在模型里。
*/ */
public void refreshAsync() { public void refreshAsync() {
this.executor.execute(() -> { this.executor.execute(() -> {
@@ -127,8 +138,25 @@ public final class TerritoryDataService {
TerritoryModel parsed = TerritoryDataService.parse(response.body()); TerritoryModel parsed = TerritoryDataService.parse(response.body());
TerritoryModel previous = this.model; TerritoryModel previous = this.model;
if (previous != null) { 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()) { 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); parsed.setSelfStateName(this.selectedStateName);
@@ -163,6 +191,7 @@ public final class TerritoryDataService {
} }
public void addSessionClaim(ChunkPos chunk) { public void addSessionClaim(ChunkPos chunk) {
this.sessionClaimTimes.put(chunk, System.currentTimeMillis());
TerritoryModel current = this.model; TerritoryModel current = this.model;
if (current != null) { if (current != null) {
current.addSessionClaim(chunk); current.addSessionClaim(chunk);
+1 -1
查看文件
@@ -1 +1 @@
{"schemaVersion":1,"id":"autostore","version":"1.0.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.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"}]}