比较提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
2309f01291 | ||
|
|
3cddee2abb | ||
|
|
28fe848fb6 | ||
|
|
340fb2ddf1 | ||
|
|
fc14e51894 | ||
|
|
00aa45a551 | ||
|
|
c55e4eaf8b | ||
|
|
448dad6cc7 | ||
|
|
2b8211b3d0 | ||
|
|
25d95a5c22 | ||
|
|
385e0d13f1 | ||
|
|
1e99ff39e0 | ||
|
|
c569065054 | ||
|
|
3601bcd40e | ||
|
|
e08dbb8104 | ||
|
|
7a45e6f676 | ||
|
|
d2a52199ae | ||
|
|
1f817199f7 | ||
|
|
9ee4b41630 | ||
|
|
15b4a40b6d | ||
|
|
b8e56dafcb | ||
|
|
d0ed839d9f | ||
|
|
20c9cb10a3 | ||
|
|
e8f6009a66 | ||
|
|
8a2c4a21d8 | ||
|
|
134a6e6f20 | ||
|
|
84abb75573 | ||
|
|
e0aadf6564 | ||
|
|
6cbd2a324a | ||
|
|
64e47e7781 | ||
|
|
e69a24eb3e | ||
|
|
77c80e5141 |
+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/
|
||||
@@ -6,15 +6,25 @@
|
||||
|
||||
- 从远程 `markers.json` 拉取各国领土边界
|
||||
- 四种扩地策略:方形外扩、区域扩地、玩家周围、绕国一圈
|
||||
- 自动寻路(Baritone)前往目标区块
|
||||
- 区域扩地采用 S 形扫描:从与己方领土接壤的区块开始,横向/纵向 S 形扫过整个矩形,不沿邦国边界,但保证每个新占领区块都与**己方领土**接壤(仅四个正方向,不把其他邦国领土计入接壤)
|
||||
- 自动寻路(Baritone)前往目标区块;监听 Baritone 路径事件,目标不可达(CALC_FAILED,如被悬崖/高山/水体隔断)时立即跳过;寻路超时则重新寻路(不立即回城),重试耗尽才跳过当前目标
|
||||
- 运行时接壤校验:寻路/等待阶段每帧校验目标是否仍与己方领土接壤,地图刷新导致边界变化时立即跳过
|
||||
- 目标有效性校验:目标若已被其他邦国占领则立即跳过
|
||||
- 现实时间限制:仅 11:00-22:00(本地现实时间)允许开拓,其余时段启动会被拒绝、运行中到点自动停止
|
||||
- 自动进食(饥饿时自动吃背包食物)
|
||||
- 实时地图 GUI(通过 Ctrl+R 打开)
|
||||
- 监听聊天/字幕消息判断开垦结果
|
||||
- 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞
|
||||
- 刷新请求带缓存击穿(时间戳参数 + no-cache 头),修复约半数刷新命中网络缓存拿到旧数据、导致目标被误标"已占领"且长时间不修正的问题;占领成功后立即触发刷新确认,假占领确认宽限期缩短至 60 秒
|
||||
- 飞地(疏漏区块)检测:按 dynmap 奇偶填充规则解析领土多边形,正确识别国中由边界围出的内圈空腔;内置地图用红色标出己方飞地,自动扩地时优先填补这些缺口
|
||||
- 区域扩地严格 S 形(蛇形)扫描:起始边为矩形四条边中离王城最近的一条,由内而外推进;即使待开垦区块构成不规则多边形也按 S 形一路扫过去
|
||||
- 性能优化:地图渲染按视口快速过滤(不再每帧全量遍历 8 万区块)、飞地检测移至后台线程预计算、接壤校验零集合复制、响应内容未变化时跳过重复解析、ChunkPos 跨模型驻留复用——修复打开 GUI 与刷新地图时游戏卡顿、内存占用偏高的问题
|
||||
- 日志导出:GUI 右上角“导出日志”按钮,或 `/autostore log` 命令,记录寻路、坐标、状态、路径事件等
|
||||
|
||||
## 安装
|
||||
|
||||
1. 安装 Fabric Loader 0.16.14+ 和 Fabric API
|
||||
2. 将 `autostore-1.0-SNAPSHOT.jar` 放入 `.minecraft/mods/` 目录
|
||||
2. 将 `autostore-1.2.1.jar` 放入 `.minecraft/mods/` 目录
|
||||
3. 启动游戏,按 `Ctrl+R` 打开 GUI
|
||||
|
||||
## 构建
|
||||
@@ -24,4 +34,12 @@
|
||||
./build_mod.sh
|
||||
```
|
||||
|
||||
输出文件:`autostore-1.0-SNAPSHOT.jar`
|
||||
输出文件:`autostore-1.2.1.jar`
|
||||
|
||||
## 赞助支持
|
||||
|
||||
如果你觉得这个模组对你有帮助,欢迎赞助支持一下创作者 ❤️
|
||||
|
||||

|
||||
|
||||
你的支持是持续开发的动力!
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import json, math, urllib.request, sys
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
URL = "http://bgjq.simpfun.cn/tiles/minecraft_overworld/markers.json"
|
||||
data = urllib.request.urlopen(
|
||||
urllib.request.Request(URL, headers={"User-Agent": "Mozilla/5.0 autostore/1.0"}),
|
||||
timeout=20).read()
|
||||
root = json.loads(data.decode("utf-8"))
|
||||
|
||||
CLAIM_PATTERN_prefix = "疆土归属:"
|
||||
|
||||
def extract_polygons(points):
|
||||
result = []
|
||||
if points is None or not isinstance(points, list):
|
||||
return result
|
||||
if len(points) == 0:
|
||||
return result
|
||||
if isinstance(points[0], dict) and "x" in points[0] and "z" in points[0]:
|
||||
poly = [(p["x"], p["z"]) for p in points]
|
||||
if len(poly) >= 3:
|
||||
result.append(poly)
|
||||
else:
|
||||
for el in points:
|
||||
result.extend(extract_polygons(el))
|
||||
return result
|
||||
|
||||
def point_in_polygon(x, z, poly):
|
||||
inside = False
|
||||
j = len(poly) - 1
|
||||
for i in range(len(poly)):
|
||||
xi, zi = poly[i]
|
||||
xj, zj = poly[j]
|
||||
if (zi > z) != (zj > z) and x < (xj - xi) * (z - zi) / (zj - zi) + xi:
|
||||
inside = not inside
|
||||
j = i
|
||||
return inside
|
||||
|
||||
def fill_polygon(out, poly):
|
||||
minx = min(math.floor(p[0]/16) for p in poly)
|
||||
maxx = max(math.floor(p[0]/16) for p in poly)
|
||||
minz = min(math.floor(p[1]/16) for p in poly)
|
||||
maxz = max(math.floor(p[1]/16) for p in poly)
|
||||
area = (maxx - minx + 1) * (maxz - minz + 1)
|
||||
if area > 6_000_000:
|
||||
return
|
||||
for cx in range(minx, maxx+1):
|
||||
for cz in range(minz, maxz+1):
|
||||
if point_in_polygon(cx*16+8, cz*16+8, poly):
|
||||
out.add((cx, cz))
|
||||
|
||||
# Polygon count per marker
|
||||
poly_counts = {}
|
||||
for layer in root:
|
||||
if layer.get("id") != "union_claims":
|
||||
continue
|
||||
for marker in layer.get("markers", []):
|
||||
popup = marker.get("popup", "")
|
||||
idx = popup.find(CLAIM_PATTERN_prefix)
|
||||
if idx < 0:
|
||||
continue
|
||||
rest = popup[idx+len(CLAIM_PATTERN_prefix):].lstrip()
|
||||
name = ""
|
||||
for ch in rest:
|
||||
if ch in "<\r\n":
|
||||
break
|
||||
name += ch
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
polys = extract_polygons(marker.get("points"))
|
||||
poly_counts.setdefault(name, 0)
|
||||
poly_counts[name] += len(polys)
|
||||
|
||||
print("=== markers with multiple polygons (potential holes as inner rings) ===")
|
||||
for name, cnt in poly_counts.items():
|
||||
if cnt > 1:
|
||||
print(f" {name}: {cnt} polygons")
|
||||
|
||||
# Also report how many total markers
|
||||
print(f"\nTotal markers: {sum(poly_counts.values())}, distinct countries: {len(poly_counts)}")
|
||||
|
||||
claims = {}
|
||||
for layer in root:
|
||||
if layer.get("id") != "union_claims":
|
||||
continue
|
||||
for marker in layer.get("markers", []):
|
||||
popup = marker.get("popup", "")
|
||||
idx = popup.find(CLAIM_PATTERN_prefix)
|
||||
if idx < 0:
|
||||
continue
|
||||
rest = popup[idx+len(CLAIM_PATTERN_prefix):].lstrip()
|
||||
name = ""
|
||||
for ch in rest:
|
||||
if ch in "<\r\n":
|
||||
break
|
||||
name += ch
|
||||
name = name.strip()
|
||||
if not name:
|
||||
continue
|
||||
s = claims.setdefault(name, set())
|
||||
for poly in extract_polygons(marker.get("points")):
|
||||
fill_polygon(s, poly)
|
||||
|
||||
print(f"Total countries parsed: {len(claims)}")
|
||||
print(f"Total claimed chunks: {sum(len(v) for v in claims.values())}")
|
||||
|
||||
print("\n=== Flying chunks (holes) per country ===")
|
||||
total_holes = 0
|
||||
for name, chunks in claims.items():
|
||||
if not chunks:
|
||||
continue
|
||||
xs = [c[0] for c in chunks]
|
||||
zs = [c[1] for c in chunks]
|
||||
minx, maxx = min(xs), max(xs)
|
||||
minz, maxz = min(zs), max(zs)
|
||||
holes = []
|
||||
for cx in range(minx, maxx+1):
|
||||
for cz in range(minz, maxz+1):
|
||||
if (cx, cz) in chunks:
|
||||
continue
|
||||
if all(((cx+dx, cz+dz) in chunks) for dx, dz in [(1,0),(-1,0),(0,1),(0,-1)]):
|
||||
holes.append((cx, cz))
|
||||
if holes:
|
||||
total_holes += len(holes)
|
||||
print(f" {name}: {len(holes)} holes, e.g. {holes[:8]}")
|
||||
print(f"\n=== TOTAL flying chunks: {total_holes} ===")
|
||||
+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)
|
||||
}
|
||||
}
|
||||
二进制
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 88 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 11 KiB |
@@ -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.2.2
|
||||
maven_group=autostore
|
||||
archives_base_name=autostore
|
||||
|
||||
# Fabric API
|
||||
fabric_version=0.119.4+1.21.4
|
||||
二进制
二进制文件未显示。
@@ -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
|
||||
@@ -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,13 @@ package autostore.client.bridge;
|
||||
import baritone.api.BaritoneAPI;
|
||||
import baritone.api.IBaritone;
|
||||
import baritone.api.Settings;
|
||||
import baritone.api.pathing.goals.Goal;
|
||||
import baritone.api.pathing.goals.GoalXZ;
|
||||
import baritone.api.event.events.PathEvent;
|
||||
import baritone.api.event.listener.AbstractGameEventListener;
|
||||
import autostore.client.config.ExpansionConfig;
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import baritone.api.pathing.goals.GoalXZ;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
|
||||
@@ -24,6 +27,25 @@ import net.fabricmc.api.Environment;
|
||||
@Environment(EnvType.CLIENT)
|
||||
public final class BaritoneBridge {
|
||||
|
||||
/**
|
||||
* 寻路事件回调:用于把 Baritone 的路径事件(如计算失败、到达目标)转发给控制器,
|
||||
* 使控制器能及时识别“目标不可达”并跳过,而不是傻等到超时。
|
||||
*/
|
||||
public interface PathListener {
|
||||
void onPathEvent(PathEvent event);
|
||||
}
|
||||
|
||||
/** 已注册的路径事件监听器集合(线程安全) */
|
||||
private final Set<PathListener> pathListeners = ConcurrentHashMap.newKeySet();
|
||||
/** 是否已向 Baritone 注册事件监听器(只注册一次) */
|
||||
private volatile boolean eventListenerRegistered;
|
||||
|
||||
/** 注册一个路径事件监听器;同时确保已向 Baritone 事件总线挂载转发器 */
|
||||
public void addPathListener(PathListener listener) {
|
||||
this.pathListeners.add(listener);
|
||||
this.primary();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将模组配置同步到 Baritone 的全局设置。
|
||||
*
|
||||
@@ -34,12 +56,27 @@ public final class BaritoneBridge {
|
||||
settings.allowSprint.value = config.allowSprint;
|
||||
settings.allowBreak.value = config.allowBreak;
|
||||
settings.allowPlace.value = config.allowPlace;
|
||||
settings.allowParkour.value = config.allowJump;
|
||||
// 跳跃(跑酷)前进:开启放置时也允许跳跃搭路,避免过河/过缝时必须纯步行
|
||||
settings.allowParkour.value = config.allowJump || config.allowPlace;
|
||||
// 关键修复:开启放置时,配套允许“跑酷搭路放方块”。
|
||||
// 若仅开 allowPlace 而不给 allowParkourPlace,Baritone 在边缘会把
|
||||
// “下蹲放方块”与“跳跃过去”两条腿计划打架:想放又判定不该放,
|
||||
// 于是原地反复下蹲+跳跃却始终放不出方块(用户观察到的“原地抽搐”)。
|
||||
settings.allowParkourPlace.value = config.allowPlace;
|
||||
// 降低放方块的路径代价:Baritone 默认对搭路惩罚较高,会倾向犹豫/绕行;
|
||||
// 归零后它更愿意稳定搭路,而不是在缺口边反复空跳。
|
||||
settings.blockPlacementPenalty.value = 0.0;
|
||||
settings.avoidance.value = config.avoidMobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 让 Baritone 寻路前往指定区块的中心。
|
||||
* 让 Baritone 寻路前往指定区块中心。
|
||||
*
|
||||
* <p>使用 Baritone <b>原生</b>{@link GoalXZ}(目标区块中心单格):原生 GoalXZ 对
|
||||
* 可达地形几乎不会误判“目标不可达”,是最好的默认选择。早期用自定义
|
||||
* {@code ChunkGoal}(曾加入高度硬约束)疑似是“平原上一按 W 就能走、Baritone 却秒
|
||||
* CALC_FAILED”的根因——自定义 Goal 在某些站位/边界下会把可达目标判成不可达。
|
||||
* 到达判定仍由控制器 {@code playerChunk.equals(target)} 保证,与 Goal 自身容差无关。</p>
|
||||
*
|
||||
* @param chunk 目标区块
|
||||
*/
|
||||
@@ -48,8 +85,6 @@ public final class BaritoneBridge {
|
||||
if (baritone == null) {
|
||||
return;
|
||||
}
|
||||
// 使用 GoalXZ 前往区块中心:Baritone 的 GoalXZ 自带约 10 格容差,
|
||||
// 只需到达区块中心附近即可,避免中心点不可达导致寻路卡死
|
||||
baritone.getCustomGoalProcess().setGoalAndPath(new GoalXZ(chunk.centerBlockX(), chunk.centerBlockZ()));
|
||||
}
|
||||
|
||||
@@ -75,12 +110,30 @@ public final class BaritoneBridge {
|
||||
|
||||
/**
|
||||
* 获取 Baritone 主实例;加载失败或异常时返回 null。
|
||||
* 首次成功获取时,向 Baritone 事件总线注册一个转发器,
|
||||
* 把路径事件分发给所有已注册的 {@link PathListener}。
|
||||
*
|
||||
* @return 主 Baritone 实例,或 null
|
||||
*/
|
||||
private IBaritone primary() {
|
||||
try {
|
||||
return BaritoneAPI.getProvider().getPrimaryBaritone();
|
||||
IBaritone baritone = BaritoneAPI.getProvider().getPrimaryBaritone();
|
||||
if (baritone != null && !this.eventListenerRegistered) {
|
||||
this.eventListenerRegistered = true;
|
||||
baritone.getGameEventHandler().registerEventListener(new AbstractGameEventListener() {
|
||||
@Override
|
||||
public void onPathEvent(PathEvent event) {
|
||||
for (PathListener listener : BaritoneBridge.this.pathListeners) {
|
||||
try {
|
||||
listener.onPathEvent(event);
|
||||
} catch (Throwable ignored) {
|
||||
// 单个监听器异常不影响其他监听器
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return baritone;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
|
||||
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
|
||||
* AutoStore 客户端命令:注册 /autostore 系列命令。
|
||||
*/
|
||||
package autostore.client.command;
|
||||
|
||||
@@ -10,11 +9,9 @@ import autostore.client.controller.ExpansionController;
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import autostore.client.territory.TerritoryState;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
|
||||
@@ -37,6 +34,7 @@ import net.minecraft.text.Text;
|
||||
* <li>/autostore state <邦国名> —— 设置目标邦国</li>
|
||||
* <li>/autostore strategy <1-4> —— 设置扩地策略</li>
|
||||
* <li>/autostore rings <层数> —— 设置扩圈上限(0 为持续)</li>
|
||||
* <li>/autostore log —— 导出运行日志到文件</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class AutoStoreCommands {
|
||||
@@ -45,50 +43,108 @@ public final class AutoStoreCommands {
|
||||
|
||||
/** 注册全部 /autostore 子命令到命令分发器 */
|
||||
public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, AutoStoreModClient mod) {
|
||||
dispatcher.register((((((((((ClientCommandManager.literal((String)"autostore").then(ClientCommandManager.literal((String)"gui").executes(ctx -> {
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u6253\u5f00 AutoStore GUI"));
|
||||
LiteralArgumentBuilder<FabricClientCommandSource> root = ClientCommandManager.literal("autostore");
|
||||
|
||||
// 打开 GUI
|
||||
root.then(ClientCommandManager.literal("gui").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(Text.literal("已打开 AutoStore GUI"));
|
||||
mod.openGui();
|
||||
return 1;
|
||||
}))).then((ClientCommandManager.literal((String)"start").executes(ctx -> AutoStoreCommands.start(ctx.getSource(), mod, mod.config().targetCount, mod.config().strategy))).then(ClientCommandManager.argument((String)"count", IntegerArgumentType.integer()).executes(ctx -> AutoStoreCommands.start(ctx.getSource(), mod, IntegerArgumentType.getInteger(ctx, (String)"count"), mod.config().strategy))))).then(ClientCommandManager.literal((String)"stop").executes(ctx -> {
|
||||
}));
|
||||
|
||||
// 开始扩地(可选数量参数)
|
||||
root.then(ClientCommandManager.literal("start")
|
||||
.executes(ctx -> start(ctx.getSource(), mod, mod.config().targetCount, mod.config().strategy))
|
||||
.then(ClientCommandManager.argument("count", IntegerArgumentType.integer())
|
||||
.executes(ctx -> start(ctx.getSource(), mod, IntegerArgumentType.getInteger(ctx, "count"), mod.config().strategy))));
|
||||
|
||||
// 停止扩地
|
||||
root.then(ClientCommandManager.literal("stop").executes(ctx -> {
|
||||
mod.controller().stop();
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"\u5df2\u505c\u6b62\u81ea\u52a8\u6269\u5730"));
|
||||
ctx.getSource().sendFeedback(Text.literal("已停止自动扩地"));
|
||||
return 1;
|
||||
}))).then(ClientCommandManager.literal((String)"status").executes(ctx -> AutoStoreCommands.status(ctx.getSource(), mod)))).then(ClientCommandManager.literal((String)"refresh").executes(ctx -> {
|
||||
}));
|
||||
|
||||
// 查看状态
|
||||
root.then(ClientCommandManager.literal("status").executes(ctx -> status(ctx.getSource(), mod)));
|
||||
|
||||
// 刷新地图
|
||||
root.then(ClientCommandManager.literal("refresh").executes(ctx -> {
|
||||
mod.dataService().refreshAsync();
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"\u6b63\u5728\u5237\u65b0\u90a6\u56fd\u5730\u56fe"));
|
||||
ctx.getSource().sendFeedback(Text.literal("正在刷新邦国地图"));
|
||||
return 1;
|
||||
}))).then(ClientCommandManager.literal((String)"auto").executes(ctx -> {
|
||||
TerritoryState state = mod.dataService().model().findContainingState(AutoStoreCommands.playerChunk(ctx.getSource()));
|
||||
}));
|
||||
|
||||
// 自动识别邦国
|
||||
root.then(ClientCommandManager.literal("auto").executes(ctx -> {
|
||||
TerritoryState state = mod.dataService().model().findContainingState(playerChunk(ctx.getSource()));
|
||||
if (state == null) {
|
||||
(ctx.getSource()).sendError(Text.literal((String)"\u65e0\u6cd5\u8bc6\u522b\u5f53\u524d\u90a6\u56fd"));
|
||||
ctx.getSource().sendError(Text.literal("无法识别当前邦国"));
|
||||
} else {
|
||||
mod.config().stateName = state.name();
|
||||
mod.saveConfig();
|
||||
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u81ea\u52a8\u8bc6\u522b\u90a6\u56fd\uff1a" + state.name())));
|
||||
ctx.getSource().sendFeedback(Text.literal("已自动识别邦国:" + state.name()));
|
||||
}
|
||||
return 1;
|
||||
}))).then(ClientCommandManager.literal((String)"state").then(ClientCommandManager.argument((String)"name", StringArgumentType.greedyString()).executes(ctx -> {
|
||||
String name;
|
||||
mod.config().stateName = name = StringArgumentType.getString(ctx, (String)"name");
|
||||
}));
|
||||
|
||||
// 设置邦国
|
||||
root.then(ClientCommandManager.literal("state").then(ClientCommandManager.argument("name", StringArgumentType.greedyString()).executes(ctx -> {
|
||||
String name = StringArgumentType.getString(ctx, "name");
|
||||
mod.config().stateName = name;
|
||||
mod.saveConfig();
|
||||
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u8bbe\u7f6e\u90a6\u56fd\uff1a" + name)));
|
||||
return 1;
|
||||
})))).then(ClientCommandManager.literal((String)"strategy").then(ClientCommandManager.argument((String)"strategy", IntegerArgumentType.integer((int)1, (int)4)).executes(ctx -> {
|
||||
int strategy;
|
||||
mod.config().strategy = strategy = IntegerArgumentType.getInteger(ctx, (String)"strategy");
|
||||
mod.saveConfig();
|
||||
(ctx.getSource()).sendFeedback(Text.literal(("\u5df2\u8bbe\u7f6e\u6269\u5f20\u7b56\u7565\uff1a" + strategy)));
|
||||
return 1;
|
||||
})))).then(ClientCommandManager.literal((String)"rings").then(ClientCommandManager.argument((String)"rings", IntegerArgumentType.integer((int)0)).executes(ctx -> {
|
||||
int rings;
|
||||
mod.config().maxRings = rings = IntegerArgumentType.getInteger(ctx, (String)"rings");
|
||||
mod.saveConfig();
|
||||
(ctx.getSource()).sendFeedback(Text.literal((rings <= 0 ? "\u5df2\u8bbe\u7f6e\u6301\u7eed\u6269\u5730" : "\u5df2\u8bbe\u7f6e\u6269\u5708\u4e0a\u9650\uff1a" + rings)));
|
||||
return 1;
|
||||
})))).then(ClientCommandManager.literal((String)"help").executes(ctx -> {
|
||||
(ctx.getSource()).sendFeedback(Text.literal((String)"/autostore help \u663e\u793a\u5e2e\u52a9\n/autostore gui \u6253\u5f00 GUI\uff08\u6216\u6309 F6 \u5feb\u6377\u952e\uff09\n/autostore start \u5f00\u59cb\u6269\u5730\n/autostore start <\u6570\u91cf> \u6307\u5b9a\u76ee\u6807\u6570\u91cf\uff0c-1 \u4e3a\u6301\u7eed\n/autostore stop \u505c\u6b62\n/autostore status \u67e5\u770b\u72b6\u6001\n/autostore refresh \u5237\u65b0\u5730\u56fe\n/autostore auto \u81ea\u52a8\u8bc6\u522b\u90a6\u56fd\n/autostore state <\u90a6\u56fd\u540d> \u8bbe\u7f6e\u90a6\u56fd\n/autostore strategy <1|2|3|4> \u8bbe\u7f6e\u7b56\u7565\n/autostore rings <\u5c42\u6570> \u8bbe\u7f6e\u5708\u6570\u4e0a\u9650"));
|
||||
ctx.getSource().sendFeedback(Text.literal("已设置邦国:" + name));
|
||||
return 1;
|
||||
})));
|
||||
|
||||
// 设置策略
|
||||
root.then(ClientCommandManager.literal("strategy").then(ClientCommandManager.argument("strategy", IntegerArgumentType.integer(1, 4)).executes(ctx -> {
|
||||
int strategy = IntegerArgumentType.getInteger(ctx, "strategy");
|
||||
mod.config().strategy = strategy;
|
||||
mod.saveConfig();
|
||||
ctx.getSource().sendFeedback(Text.literal("已设置扩张策略:" + strategy));
|
||||
return 1;
|
||||
})));
|
||||
|
||||
// 设置圈数上限
|
||||
root.then(ClientCommandManager.literal("rings").then(ClientCommandManager.argument("rings", IntegerArgumentType.integer(0)).executes(ctx -> {
|
||||
int rings = IntegerArgumentType.getInteger(ctx, "rings");
|
||||
mod.config().maxRings = rings;
|
||||
mod.saveConfig();
|
||||
ctx.getSource().sendFeedback(Text.literal(rings <= 0 ? "已设置持续扩地" : "已设置扩圈上限:" + rings));
|
||||
return 1;
|
||||
})));
|
||||
|
||||
// 帮助
|
||||
root.then(ClientCommandManager.literal("help").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(Text.literal(
|
||||
"/autostore help 显示帮助\n" +
|
||||
"/autostore gui 打开 GUI(或按 Ctrl+R 快捷键)\n" +
|
||||
"/autostore start 开始扩地\n" +
|
||||
"/autostore start <数量> 指定目标数量,-1 为持续\n" +
|
||||
"/autostore stop 停止\n" +
|
||||
"/autostore status 查看状态\n" +
|
||||
"/autostore refresh 刷新地图\n" +
|
||||
"/autostore auto 自动识别邦国\n" +
|
||||
"/autostore state <邦国名> 设置邦国\n" +
|
||||
"/autostore strategy <1|2|3|4> 设置策略\n" +
|
||||
"/autostore rings <层数> 设置圈数上限\n" +
|
||||
"/autostore log 导出日志"));
|
||||
return 1;
|
||||
}));
|
||||
|
||||
// 导出日志
|
||||
root.then(ClientCommandManager.literal("log").executes(ctx -> {
|
||||
java.nio.file.Path path = mod.controller().exportLogs();
|
||||
if (path != null) {
|
||||
ctx.getSource().sendFeedback(Text.literal("日志已导出:" + path));
|
||||
} else {
|
||||
ctx.getSource().sendError(Text.literal("日志导出失败"));
|
||||
}
|
||||
return 1;
|
||||
}));
|
||||
|
||||
dispatcher.register(root);
|
||||
}
|
||||
|
||||
/** /autostore start 命令处理:复制当前配置并覆盖目标数量与策略,然后启动扩地 */
|
||||
@@ -100,15 +156,20 @@ public final class AutoStoreCommands {
|
||||
mod.config().strategy = strategy;
|
||||
mod.saveConfig();
|
||||
boolean started = mod.controller().start(config);
|
||||
source.sendFeedback(Text.literal((started ? "\u5f00\u59cb\u81ea\u52a8\u6269\u5730" : "\u542f\u52a8\u5931\u8d25\uff1a" + mod.controller().status())));
|
||||
source.sendFeedback(Text.literal(started ? "开始自动扩地" : "启动失败:" + mod.controller().status()));
|
||||
return started ? 1 : 0;
|
||||
}
|
||||
|
||||
/** /autostore status 命令处理:汇总状态、进度、队列、策略与邦国信息并反馈 */
|
||||
private static int status(FabricClientCommandSource source, AutoStoreModClient mod) {
|
||||
ExpansionController controller = mod.controller();
|
||||
String text = "\u72b6\u6001\uff1a" + controller.status() + " | \u8fdb\u5ea6\uff1a" + controller.completedClaims() + (mod.config().targetCount > 0 ? "/" + mod.config().targetCount : "/\u6301\u7eed") + " | \u961f\u5217\uff1a" + controller.queuedTargets() + " | \u7b56\u7565\uff1a" + mod.config().strategy + " | \u90a6\u56fd\uff1a" + (mod.config().stateName.isEmpty() ? "\u81ea\u52a8" : mod.config().stateName);
|
||||
source.sendFeedback(Text.literal((String)text));
|
||||
String text = "状态:" + controller.status()
|
||||
+ " | 进度:" + controller.completedClaims()
|
||||
+ (mod.config().targetCount > 0 ? "/" + mod.config().targetCount : "/持续")
|
||||
+ " | 队列:" + controller.queuedTargets()
|
||||
+ " | 策略:" + mod.config().strategy
|
||||
+ " | 邦国:" + (mod.config().stateName.isEmpty() ? "自动" : mod.config().stateName);
|
||||
source.sendFeedback(Text.literal(text));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -118,4 +179,3 @@ public final class AutoStoreCommands {
|
||||
return ChunkPos.fromBlock(pos.getX(), pos.getZ());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,13 @@ import autostore.client.territory.ChunkPos;
|
||||
import autostore.client.territory.TerritoryDataService;
|
||||
import autostore.client.territory.TerritoryModel;
|
||||
import autostore.client.territory.TerritoryState;
|
||||
import baritone.api.event.events.PathEvent;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
@@ -20,6 +27,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.util.Hand;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.entity.player.PlayerInventory;
|
||||
@@ -27,6 +35,7 @@ import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.component.DataComponentTypes;
|
||||
import net.minecraft.text.Text;
|
||||
|
||||
@Environment(value=EnvType.CLIENT)
|
||||
/**
|
||||
@@ -37,9 +46,15 @@ import net.minecraft.component.DataComponentTypes;
|
||||
* 并监听聊天/字幕消息判断开垦是否成功。支持自动进食、超时重试、手动停止等功能。
|
||||
*/
|
||||
public final class ExpansionController
|
||||
implements SubtitleChatMonitor.Listener {
|
||||
/** 日志环形缓冲区的最大条数 */
|
||||
private static final int MAX_LOGS = 80;
|
||||
implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
|
||||
/** 日志环形缓冲区的最大条数(含导出用详细日志,故调大) */
|
||||
private static final int MAX_LOGS = 2000;
|
||||
/** 日志时间戳格式(时:分:秒) */
|
||||
private static final DateTimeFormatter LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
/** 允许开拓的现实时间起始小时(含,本地时区) */
|
||||
private static final int CLAIM_START_HOUR = 11;
|
||||
/** 允许开拓的现实时间结束小时(不含,本地时区),即 22:00 起停止开拓 */
|
||||
private static final int CLAIM_END_HOUR = 22;
|
||||
/** Baritone 寻路桥接(负责自动移动) */
|
||||
private final BaritoneBridge baritoneBridge = new BaritoneBridge();
|
||||
/** 扩地规划器(决定下一个目标区块) */
|
||||
@@ -76,9 +91,13 @@ implements SubtitleChatMonitor.Listener {
|
||||
private final Set<ChunkPos> claimedSet = new HashSet<>();
|
||||
/** 是否正在自动进食 */
|
||||
private boolean eating;
|
||||
/** Baritone 最近一次路径计算是否失败(目标不可达);由事件监听设置,寻路阶段消费 */
|
||||
private volatile boolean pathCalcFailed;
|
||||
|
||||
public ExpansionController(TerritoryDataService dataService) {
|
||||
this.dataService = dataService;
|
||||
// 注册路径事件监听,及时识别“目标不可达”并跳过(避免大山/悬崖地形傻等超时)
|
||||
this.baritoneBridge.addPathListener(this);
|
||||
}
|
||||
|
||||
public SubtitleChatMonitor monitor() {
|
||||
@@ -121,6 +140,17 @@ implements SubtitleChatMonitor.Listener {
|
||||
return this.state != State.IDLE && this.state != State.COMPLETE && this.state != State.STOPPED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前现实时间(本地时区)是否在允许开拓的时间段内。
|
||||
* 仅 11:00(含)到 22:00(不含)允许开拓,其余时段服务器会拒绝开拓。
|
||||
*
|
||||
* @return true 表示当前在允许开拓时段内
|
||||
*/
|
||||
public static boolean isWithinClaimTime() {
|
||||
int hour = LocalTime.now().getHour();
|
||||
return hour >= CLAIM_START_HOUR && hour < CLAIM_END_HOUR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动扩地任务。
|
||||
*
|
||||
@@ -141,6 +171,15 @@ implements SubtitleChatMonitor.Listener {
|
||||
this.status = "Baritone \u5c1a\u672a\u521d\u59cb\u5316\uff0c\u8bf7\u7a0d\u540e\u518d\u8bd5";
|
||||
return false;
|
||||
}
|
||||
// 现实时间限制:仅 11:00-22:00 允许开拓,其余时段服务器会拒绝开拓(等待超时)
|
||||
if (!ExpansionController.isWithinClaimTime()) {
|
||||
this.status = "\u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\uff0811:00-22:00\uff09\uff0c\u65e0\u6cd5\u5f00\u62d3";
|
||||
this.setState(State.IDLE, this.status);
|
||||
this.log("\u542f\u52a8\u5931\u8d25\uff1a\u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\uff0c\u5f53\u524d\u73b0\u5b9e\u65f6\u95f4 " + LocalTime.now().format(LOG_TIME_FORMAT));
|
||||
// 给玩家发送聊天栏提示,避免玩家未打开 GUI 时看不到原因
|
||||
client.player.sendMessage(Text.literal("[AutoStore] \u4e0d\u5728\u5f00\u62d3\u65f6\u9650\u5185\uff0811:00-22:00\uff09\uff0c\u65e0\u6cd5\u5f00\u62d3"), false);
|
||||
return false;
|
||||
}
|
||||
this.config = requestedConfig.copy();
|
||||
this.model = this.dataService.model();
|
||||
this.playerChunk = this.playerChunk(client);
|
||||
@@ -201,9 +240,18 @@ implements SubtitleChatMonitor.Listener {
|
||||
return;
|
||||
}
|
||||
this.playerChunk = this.playerChunk(client);
|
||||
// 现实时间限制:运行中超出 11:00-22:00 则自动停止,避免服务器拒绝开拓导致超时
|
||||
if (this.isRunning() && !ExpansionController.isWithinClaimTime()) {
|
||||
this.stop();
|
||||
this.status = "\u5df2\u5230\u5f00\u62d3\u65f6\u9650\u5916\uff0811:00-22:00\uff09\uff0c\u4efb\u52a1\u505c\u6b62";
|
||||
this.log("\u5df2\u5230\u5f00\u62d3\u65f6\u9650\u5916\uff0c\u505c\u6b62\u4efb\u52a1\uff08\u5f53\u524d\u73b0\u5b9e\u65f6\u95f4 " + LocalTime.now().format(LOG_TIME_FORMAT) + "\uff09");
|
||||
client.player.sendMessage(Text.literal("[AutoStore] \u5df2\u5230\u5f00\u62d3\u65f6\u9650\u5916\uff0811:00-22:00\uff09\uff0c\u4efb\u52a1\u505c\u6b62"), false);
|
||||
return;
|
||||
}
|
||||
if (this.tickAutoFood(client)) {
|
||||
return;
|
||||
}
|
||||
this.tickAutoSwim(client);
|
||||
switch (this.state.ordinal()) {
|
||||
case 1: {
|
||||
this.plan();
|
||||
@@ -272,32 +320,62 @@ implements SubtitleChatMonitor.Listener {
|
||||
this.lastProgress = 0;
|
||||
this.baritoneBridge.pathTo(this.target);
|
||||
this.setState(State.PATHING, "\u6b63\u5728\u524d\u5f80\u76ee\u6807\u533a\u5757 " + this.target.x() + "," + this.target.z());
|
||||
this.log("\u5bfb\u8def\u524d\u5f80 " + this.target.x() + "," + this.target.z() + "\uff08\u73a9\u5bb6\u533a\u5757 " + this.playerChunk.x() + "," + this.playerChunk.z() + "\uff09");
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻路阶段每帧逻辑:到达目标区块后进入等待开垦状态;
|
||||
* 若寻路超时则重试,超过重试次数则跳过该目标。
|
||||
* 若寻路超时则重新寻路(而非回城),超过重试次数则跳过该目标。
|
||||
*/
|
||||
private void tickPathing() {
|
||||
if (this.target == null) {
|
||||
this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a");
|
||||
return;
|
||||
}
|
||||
// 目标已被他人占领(地图刷新后发现):立即跳过,避免继续寻路到无效区块
|
||||
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
|
||||
return;
|
||||
}
|
||||
// Baritone 报告路径计算失败(目标不可达,如被悬崖/高山/水体完全隔断)。
|
||||
// 不立即跳过:CALC_FAILED 常因瞬时能力限制或起点卡位误报,先重新寻路一次
|
||||
//(相当于“往边上走两步再算一遍”),重试耗尽才跳过当前目标。
|
||||
if (this.pathCalcFailed) {
|
||||
this.pathCalcFailed = false;
|
||||
++this.retryCount;
|
||||
if (this.retryCount >= this.config.retries) {
|
||||
this.log("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09\uff0c\u91cd\u8bd5\u8017\u5c3d\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
|
||||
this.setState(State.FAIL, "\u5bfb\u8def\u5931\u8d25");
|
||||
} else {
|
||||
this.log("\u5bfb\u8def\u5931\u8d25\uff08\u76ee\u6807\u4e0d\u53ef\u8fbe\uff09\uff0c\u91cd\u65b0\u5bfb\u8def " + this.retryCount + "/" + this.config.retries);
|
||||
this.dataService.refreshAsync();
|
||||
this.retrySameTarget("\u5bfb\u8def\u5931\u8d25\uff0c\u91cd\u65b0\u5bfb\u8def");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 目标已不再与己方领土接壤(地图刷新后边界变化):立即跳过,避免走到无效区块
|
||||
if (this.skipIfTargetNotAdjacent()) {
|
||||
return;
|
||||
}
|
||||
if (this.playerChunk.equals(this.target)) {
|
||||
this.baritoneBridge.cancel();
|
||||
this.stageStartedMillis = System.currentTimeMillis();
|
||||
this.setState(State.WAITING, "\u5df2\u5230\u8fbe\uff0c\u7b49\u5f85\u9886\u5730\u5f00\u62d3");
|
||||
this.log("\u5df2\u5230\u8fbe\u76ee\u6807 " + this.target.x() + "," + this.target.z() + "\uff0c\u5f00\u59cb\u7b49\u5f85\u5f00\u57a6");
|
||||
return;
|
||||
}
|
||||
long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L;
|
||||
if (elapsedSeconds >= this.config.waitTimeoutSeconds) {
|
||||
++this.retryCount;
|
||||
// 主动刷新地图,尽快同步最新领土状态
|
||||
this.dataService.refreshAsync();
|
||||
if (this.retryCount >= this.config.retries) {
|
||||
// 重试耗尽:跳过当前目标(不再回城),交给下一个目标
|
||||
this.log("\u5bfb\u8def\u8d85\u65f6\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
|
||||
this.setState(State.FAIL, "\u5bfb\u8def\u8d85\u65f6");
|
||||
} else {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +386,14 @@ implements SubtitleChatMonitor.Listener {
|
||||
this.setState(State.NEXT, "\u76ee\u6807\u4e3a\u7a7a");
|
||||
return;
|
||||
}
|
||||
// 目标已被他人占领(地图刷新后发现):立即跳过,避免被困在已占领区块
|
||||
if (this.skipIfTargetClaimed("\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
|
||||
return;
|
||||
}
|
||||
// 目标已不再与己方领土接壤:立即跳过,避免在无效区块上干等开垦
|
||||
if (this.skipIfTargetNotAdjacent()) {
|
||||
return;
|
||||
}
|
||||
// 玩家离开了目标区块(被击退/寻路偏差),重新寻路回去
|
||||
if (!this.playerChunk.equals(this.target)) {
|
||||
this.retrySameTarget("\u79bb\u5f00\u76ee\u6807\u533a\u5757\uff0c\u91cd\u65b0\u524d\u5f80 " + this.target.x() + "," + this.target.z());
|
||||
@@ -316,6 +402,8 @@ implements SubtitleChatMonitor.Listener {
|
||||
long elapsedSeconds = (System.currentTimeMillis() - this.stageStartedMillis) / 1000L;
|
||||
if (elapsedSeconds >= this.config.waitTimeoutSeconds) {
|
||||
++this.retryCount;
|
||||
// 主动刷新地图,尽快同步"区块已被占领"的最新状态,避免玩家被困
|
||||
this.dataService.refreshAsync();
|
||||
if (this.retryCount >= this.config.retries) {
|
||||
this.log("\u7b49\u5f85\u8d85\u65f6\uff0c\u8df3\u8fc7 " + this.target.x() + "," + this.target.z());
|
||||
this.setState(State.FAIL, "\u7b49\u5f85\u9886\u5730\u5f00\u62d3\u8d85\u65f6");
|
||||
@@ -328,12 +416,73 @@ implements SubtitleChatMonitor.Listener {
|
||||
|
||||
/** 重试当前目标:取消旧寻路,重新让 Baritone 前往同一目标区块 */
|
||||
private void retrySameTarget(String statusText) {
|
||||
// 目标已被他人占领(地图刷新后发现):不再重试,直接跳过,避免玩家被困在已占领区块
|
||||
if (this.skipIfTargetClaimed("\u91cd\u8bd5\u65f6\u53d1\u73b0\u76ee\u6807\u5df2\u88ab\u5360\u9886")) {
|
||||
return;
|
||||
}
|
||||
this.baritoneBridge.cancel();
|
||||
this.stageStartedMillis = System.currentTimeMillis();
|
||||
this.baritoneBridge.pathTo(this.target);
|
||||
this.setState(State.PATHING, statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 若当前目标区块已被任何邦国占领(含己方与本次会话),
|
||||
* 则记录日志、跳过该目标并进入 NEXT 状态,返回 true。
|
||||
* 用于在地图数据异步刷新后及时识别"已被占领"的区块,避免死循环。
|
||||
*/
|
||||
private boolean skipIfTargetClaimed(String reason) {
|
||||
if (this.target != null && this.isClaimedByOthers(this.target)) {
|
||||
this.log(reason + " " + this.target.x() + "," + this.target.z() + "\uff0c\u8df3\u8fc7");
|
||||
this.setState(State.NEXT, "\u8df3\u8fc7\u5df2\u5360\u9886\u533a\u5757");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 若当前目标区块不再与己方领土(含本次会话已占领)的四个正方向接壤,
|
||||
* 则跳过并进入 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>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。
|
||||
* 实现上直接查询四个邻居是否命中集合,零集合复制——
|
||||
* 旧实现每帧把数千个己方区块复制进临时 HashSet,是持续卡顿与内存压力的来源之一。</p>
|
||||
*/
|
||||
private boolean isAdjacentToOwn(ChunkPos chunk) {
|
||||
TerritoryModel model = this.dataService.model();
|
||||
TerritoryState self = model.selfState();
|
||||
Set<ChunkPos> ownChunks = self == null ? null : self.chunks();
|
||||
Set<ChunkPos> sessionClaims = model.sessionClaims();
|
||||
int x = chunk.x();
|
||||
int z = chunk.z();
|
||||
return this.isOwnAt(ownChunks, sessionClaims, x + 1, z)
|
||||
|| this.isOwnAt(ownChunks, sessionClaims, x - 1, z)
|
||||
|| this.isOwnAt(ownChunks, sessionClaims, x, z + 1)
|
||||
|| this.isOwnAt(ownChunks, sessionClaims, x, z - 1);
|
||||
}
|
||||
|
||||
/** 判断指定坐标的区块是否属于己方领土或本次会话新占领 */
|
||||
private boolean isOwnAt(Set<ChunkPos> ownChunks, Set<ChunkPos> sessionClaims, int x, int z) {
|
||||
ChunkPos probe = new ChunkPos(x, z);
|
||||
return ownChunks != null && ownChunks.contains(probe) || sessionClaims.contains(probe);
|
||||
}
|
||||
|
||||
/** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */
|
||||
private void tickSuccess() {
|
||||
this.setState(State.NEXT, "\u5df2\u5b8c\u6210 " + this.completedClaims + (this.config.targetCount > 0 ? "/" + this.config.targetCount : "/\u6301\u7eed"));
|
||||
@@ -394,6 +543,29 @@ implements SubtitleChatMonitor.Listener {
|
||||
return min == Integer.MAX_VALUE ? 0 : min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baritone 路径事件回调:记录关键事件,并在“计算失败”时置标志供寻路阶段消费。
|
||||
* 本回调可能来自 Baritone 事件线程,只做轻量操作(置标志 + 写日志),
|
||||
* 具体处理放在 tick 的寻路阶段(主线程)完成,避免跨线程状态竞争。
|
||||
*/
|
||||
@Override
|
||||
public void onPathEvent(PathEvent event) {
|
||||
if (event == PathEvent.CALC_STARTED) {
|
||||
this.pathCalcFailed = false;
|
||||
} else if (event == PathEvent.CALC_FAILED) {
|
||||
this.pathCalcFailed = true;
|
||||
}
|
||||
// 仅在任务运行中记录关键事件,避免空闲时 Baritone 高频 CANCELED/中间态刷屏
|
||||
if (!this.isRunning()) {
|
||||
return;
|
||||
}
|
||||
// 只记录关键事件,且跳过高频的 CANCELED(空闲/重寻路时每秒触发多次,无排查价值)
|
||||
if (event == PathEvent.CALC_STARTED || event == PathEvent.CALC_FINISHED_NOW_EXECUTING
|
||||
|| event == PathEvent.CALC_FAILED || event == PathEvent.AT_GOAL) {
|
||||
this.log("\u5bfb\u8def\u4e8b\u4ef6\uff1a" + event);
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听器回调:开垦进度更新(0-100%) */
|
||||
@Override
|
||||
public void onProgress(int percent) {
|
||||
@@ -425,6 +597,9 @@ implements SubtitleChatMonitor.Listener {
|
||||
}
|
||||
this.model.addSessionClaim(chunk);
|
||||
this.dataService.addSessionClaim(chunk);
|
||||
// 占领成功后立即触发一次地图刷新(请求侧已带缓存击穿),尽快让地图数据确认该区块,
|
||||
// 缩短"会话占领等待确认"窗口,避免假占领长时间残留
|
||||
this.dataService.refreshAsync();
|
||||
++this.completedClaims;
|
||||
this.target = chunk;
|
||||
this.lastProgress = 100;
|
||||
@@ -441,12 +616,20 @@ implements SubtitleChatMonitor.Listener {
|
||||
if (this.target == null) {
|
||||
return;
|
||||
}
|
||||
// 开垦只能发生在玩家所在区块:校验玩家确实位于目标区块上。
|
||||
// 通用成功文案采用模糊匹配,可能被服务器广播的其他玩家开垦消息误触发;
|
||||
// 不校验会把未占领的目标错误标记为"已占领"并跳过,直到很久之后才被地图数据纠正(v1.2.1 修复)。
|
||||
if (!this.playerChunk.equals(this.target)) {
|
||||
return;
|
||||
}
|
||||
// 去重:同一区块只计一次
|
||||
if (!this.claimedSet.add(this.target)) {
|
||||
return;
|
||||
}
|
||||
this.model.addSessionClaim(this.target);
|
||||
this.dataService.addSessionClaim(this.target);
|
||||
// 占领成功后立即触发一次地图刷新,尽快确认该区块
|
||||
this.dataService.refreshAsync();
|
||||
++this.completedClaims;
|
||||
this.lastProgress = 100;
|
||||
this.log("\u5360\u9886\u6210\u529f " + this.target.x() + "," + this.target.z());
|
||||
@@ -464,9 +647,23 @@ implements SubtitleChatMonitor.Listener {
|
||||
return this.dataService.model().allClaimedChunks().contains(chunk);
|
||||
}
|
||||
|
||||
/** 返回区块的四个正方向邻居 */
|
||||
private List<ChunkPos> cardinalNeighbors(ChunkPos chunk) {
|
||||
return List.of(new ChunkPos(chunk.x() + 1, chunk.z()), new ChunkPos(chunk.x() - 1, chunk.z()), new ChunkPos(chunk.x(), chunk.z() + 1), new ChunkPos(chunk.x(), chunk.z() - 1));
|
||||
/**
|
||||
* 自动游泳逻辑(每帧调用):
|
||||
* 仅在玩家完全没入水中(有溺水风险)时自动跳跃,保持浮在水面。
|
||||
* 玩家在水面上正常游泳时不做任何干预,避免干扰 Baritone 的水平寻路。
|
||||
*/
|
||||
private void tickAutoSwim(MinecraftClient client) {
|
||||
if (!this.isRunning()) {
|
||||
return;
|
||||
}
|
||||
PlayerEntity player = client.player;
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
// 只有完全没入水中(头部在水面以下)才有溺水风险,此时才需要跳跃上浮
|
||||
if (player.isSubmergedInWater()) {
|
||||
player.jump();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,7 +710,7 @@ implements SubtitleChatMonitor.Listener {
|
||||
}
|
||||
this.baritoneBridge.cancel();
|
||||
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.status = "\u81ea\u52a8\u8fdb\u98df\u4e2d";
|
||||
this.log("\u81ea\u52a8\u8fdb\u98df\uff0c\u9965\u997f\u503c " + foodLevel);
|
||||
@@ -524,7 +721,7 @@ implements SubtitleChatMonitor.Listener {
|
||||
private int findFoodSlot(PlayerInventory inventory) {
|
||||
for (int slot = 0; slot < 36; ++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 -1;
|
||||
@@ -549,13 +746,42 @@ implements SubtitleChatMonitor.Listener {
|
||||
private void log(String message) {
|
||||
Deque<String> deque = this.logs;
|
||||
synchronized (deque) {
|
||||
this.logs.addLast(System.currentTimeMillis() + " " + message);
|
||||
while (this.logs.size() > 80) {
|
||||
this.logs.addLast("[" + LocalTime.now().format(LOG_TIME_FORMAT) + "] " + message);
|
||||
while (this.logs.size() > MAX_LOGS) {
|
||||
this.logs.removeFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出运行日志到文件,返回文件路径;失败时返回 null。
|
||||
* 文件写入 Fabric 配置目录下的 {@code autostore-debug.log},
|
||||
* 内容包含当前状态摘要(状态、目标、玩家区块、队列、进度)与全部详细日志。
|
||||
*
|
||||
* @return 导出文件的绝对路径,或 null(写入失败时)
|
||||
*/
|
||||
public Path exportLogs() {
|
||||
Path path = FabricLoader.getInstance().getConfigDir().resolve("autostore-debug.log");
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder(8192);
|
||||
sb.append("==== AutoStore 调试日志 ====\n");
|
||||
sb.append("状态: ").append(this.state).append(" / ").append(this.status).append('\n');
|
||||
sb.append("目标区块: ").append(this.target == null ? "无" : this.target.x() + "," + this.target.z()).append('\n');
|
||||
sb.append("玩家区块: ").append(this.playerChunk.x()).append(',').append(this.playerChunk.z()).append('\n');
|
||||
sb.append("队列: ").append(this.queue.size()).append(" | 已占领: ").append(this.completedClaims)
|
||||
.append(" | 重试: ").append(this.retryCount).append(" | 最近进度: ").append(this.lastProgress).append("%\n\n");
|
||||
synchronized (this.logs) {
|
||||
for (String line : this.logs) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
}
|
||||
Files.writeString(path, sb.toString(), StandardCharsets.UTF_8);
|
||||
return path;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩地任务状态机枚举:
|
||||
* <ul>
|
||||
|
||||
@@ -14,9 +14,11 @@ import autostore.client.territory.ChunkPos;
|
||||
import autostore.client.territory.TerritoryModel;
|
||||
import autostore.client.territory.TerritoryState;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.minecraft.util.Util;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.client.font.TextRenderer;
|
||||
@@ -27,6 +29,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
|
||||
import net.minecraft.client.gui.widget.CheckboxWidget;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.text.StringVisitable;
|
||||
import net.minecraft.text.OrderedText;
|
||||
|
||||
@Environment(value=EnvType.CLIENT)
|
||||
/**
|
||||
@@ -38,19 +41,15 @@ import net.minecraft.text.StringVisitable;
|
||||
*/
|
||||
public final class AutoStoreGui
|
||||
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 ExpansionConfig config;
|
||||
private final ExpansionController controller;
|
||||
// ---- 输入控件 ----
|
||||
private TextFieldWidget stateField;
|
||||
private TextFieldWidget targetCountField;
|
||||
private TextFieldWidget waitTimeoutField;
|
||||
private TextFieldWidget retriesField;
|
||||
private TextFieldWidget regionXField;
|
||||
private TextFieldWidget regionZField;
|
||||
private TextFieldWidget regionRadiusField;
|
||||
private TextFieldWidget ringsField;
|
||||
private TextFieldWidget refreshIntervalField;
|
||||
private CheckboxWidget sprintBox;
|
||||
private CheckboxWidget jumpBox;
|
||||
private CheckboxWidget avoidBox;
|
||||
@@ -64,7 +63,6 @@ extends Screen {
|
||||
private int rectStartZ;
|
||||
private int rectEndX;
|
||||
private int rectEndZ;
|
||||
private boolean updatingFields;
|
||||
// ---- 布局尺寸 ----
|
||||
private int mapX = 12;
|
||||
private int mapY = 40;
|
||||
@@ -74,7 +72,6 @@ extends Screen {
|
||||
private int advancedY = 208;
|
||||
private int stateFieldY = 84;
|
||||
private int targetFieldY = 116;
|
||||
private int advRowGap = 24;
|
||||
// ---- 地图视图 ----
|
||||
private double scale = 5.0;
|
||||
private int viewCenterX;
|
||||
@@ -98,17 +95,21 @@ extends Screen {
|
||||
this.mapY = 40;
|
||||
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);
|
||||
|
||||
// 屏幕较矮时压缩垂直间距,保证右侧面板内容不被截断
|
||||
boolean compact = this.height < 480;
|
||||
int rowGap = compact ? 24 : 28; // 主字段行间距
|
||||
this.advRowGap = compact ? 20 : 24; // 高级面板字段行间距
|
||||
int advCheckGap = compact ? 16 : 18; // 高级面板复选框行间距
|
||||
|
||||
// ===== 顶部控制按钮 =====
|
||||
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 stopButton = ButtonWidget.builder(Text.literal((String)"\u505c\u6b62"), button -> this.controller.stop()).dimensions(topX + 160, 34, 74, 30).build();
|
||||
this.addDrawableChild(startButton);
|
||||
@@ -140,41 +141,9 @@ extends Screen {
|
||||
this.addDrawableChild(this.strategyButton("\u73a9\u5bb6\u5468\u56f4", 3, left, strategyY + 24, 90));
|
||||
this.addDrawableChild(this.strategyButton("\u7ed5\u56fd\u4e00\u5708", 4, left + 96, strategyY + 24, 90));
|
||||
|
||||
// ===== 高级设置面板:直接显示(无展开/收起按钮),紧贴策略按钮下方 =====
|
||||
this.updateRegionButtons();
|
||||
// 策略按钮第二行底部 + 8px 间距,作为高级面板起始位置
|
||||
this.advancedY = strategyY + 24 + 8;
|
||||
int advFieldX1 = this.panelX + 62;
|
||||
int advFieldX2 = this.panelX + 178;
|
||||
int advFieldWidth = Math.max(46, panelWidth - 184);
|
||||
this.waitTimeoutField = this.numberField(this.config.waitTimeoutSeconds, advFieldX1, this.advancedY, advFieldWidth);
|
||||
this.retriesField = this.numberField(this.config.retries, advFieldX2, this.advancedY, advFieldWidth);
|
||||
this.regionXField = this.numberField(this.config.regionCenterX, advFieldX1, this.advancedY + this.advRowGap, advFieldWidth);
|
||||
this.regionZField = this.numberField(this.config.regionCenterZ, advFieldX2, this.advancedY + this.advRowGap, advFieldWidth);
|
||||
this.regionRadiusField = this.numberField(this.config.regionRadius, advFieldX1, this.advancedY + this.advRowGap * 2, advFieldWidth);
|
||||
this.ringsField = this.numberField(this.config.maxRings, advFieldX2, this.advancedY + this.advRowGap * 2, advFieldWidth);
|
||||
this.refreshIntervalField = this.numberField(this.config.refreshIntervalSeconds, advFieldX1, this.advancedY + this.advRowGap * 3, advFieldWidth);
|
||||
this.addDrawableChild(this.waitTimeoutField);
|
||||
this.addDrawableChild(this.retriesField);
|
||||
this.addDrawableChild(this.regionXField);
|
||||
this.addDrawableChild(this.regionZField);
|
||||
this.addDrawableChild(this.regionRadiusField);
|
||||
this.addDrawableChild(this.ringsField);
|
||||
this.addDrawableChild(this.refreshIntervalField);
|
||||
// 用户手动编辑中心/半径时,清除圈选矩形(避免边界与输入框不一致)
|
||||
Consumer<String> clearRectOnEdit = s -> {
|
||||
if (!this.updatingFields) {
|
||||
this.config.regionMinX = 0;
|
||||
this.config.regionMaxX = -1;
|
||||
this.config.regionMinZ = 0;
|
||||
this.config.regionMaxZ = -1;
|
||||
this.updateRegionButtons();
|
||||
}
|
||||
};
|
||||
this.regionXField.setChangedListener(clearRectOnEdit);
|
||||
this.regionZField.setChangedListener(clearRectOnEdit);
|
||||
this.regionRadiusField.setChangedListener(clearRectOnEdit);
|
||||
int checkY1 = this.advancedY + this.advRowGap * 3 + 20 + 8;
|
||||
this.advancedY = strategyY + 24 + 20 + 8;
|
||||
// ===== 只保留五个权限选择框:疾跑、跳跃、避怪、破坏、放置 =====
|
||||
int checkY1 = this.advancedY + 14;
|
||||
int checkY2 = checkY1 + advCheckGap;
|
||||
int checkX = left;
|
||||
this.sprintBox = this.checkbox("\u75be\u8dd1", checkX, checkY1, this.config.allowSprint, value -> {
|
||||
@@ -200,6 +169,27 @@ extends Screen {
|
||||
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 位) */
|
||||
private TextFieldWidget numberField(int value, int x, int y, int width) {
|
||||
TextFieldWidget field = new TextFieldWidget(this.textRenderer, x, y, width, 20, Text.literal((String)""));
|
||||
@@ -260,7 +250,7 @@ extends Screen {
|
||||
}
|
||||
}
|
||||
|
||||
/** 将当前拖拽的矩形提交到配置,并更新高级面板的输入框 */
|
||||
/** 将当前拖拽的矩形提交到配置 */
|
||||
private void finalizeRegion() {
|
||||
if (this.rectStartX == this.rectEndX && this.rectStartZ == this.rectEndZ) {
|
||||
// 单点无效,清空
|
||||
@@ -274,18 +264,6 @@ extends Screen {
|
||||
this.selectingRegion = false;
|
||||
this.draggingRect = false;
|
||||
this.updateRegionButtons();
|
||||
// 同步更新中心+半径输入框,使高级面板保持一致
|
||||
int centerX = (this.config.regionMinX + this.config.regionMaxX) / 2;
|
||||
int centerZ = (this.config.regionMinZ + this.config.regionMaxZ) / 2;
|
||||
int radius = Math.max(
|
||||
this.config.regionMaxX - this.config.regionMinX,
|
||||
this.config.regionMaxZ - this.config.regionMinZ
|
||||
) / 2 + 1;
|
||||
this.updatingFields = true;
|
||||
this.regionXField.setText(Integer.toString(centerX));
|
||||
this.regionZField.setText(Integer.toString(centerZ));
|
||||
this.regionRadiusField.setText(Integer.toString(radius));
|
||||
this.updatingFields = false;
|
||||
}
|
||||
|
||||
/** 绘制区域圈选矩形(覆盖层 + 边框);仅区域扩地模式下绘制 */
|
||||
@@ -370,14 +348,27 @@ extends Screen {
|
||||
context.drawTextWithShadow(this.textRenderer, "\u9886\u571f\u5730\u56fe", this.mapX + 2, this.mapY - 12, -1513240);
|
||||
}
|
||||
|
||||
/** 绘制各邦国领土区块(己方高亮、其他半透明),并绘制会话新占领区块 */
|
||||
/** 绘制各邦国领土区块(己方高亮、其他半透明),并绘制会话新占领区块与飞地(疏漏区块) */
|
||||
private void drawTerritories(DrawContext context) {
|
||||
TerritoryModel model = this.mod.dataService().model();
|
||||
// 先反推当前视口覆盖的区块范围(含一个区块边距),遍历时用整数比较快速剔除视口外区块。
|
||||
// 全图约 8 万区块,旧实现每帧对每个区块都调用屏幕坐标换算,是打开 GUI / 刷新后掉帧的主因。
|
||||
double invScale = 1.0 / this.scale;
|
||||
int edge = (int)Math.ceil(this.scale) + 2;
|
||||
int halfW = this.mapW / 2;
|
||||
int halfH = this.mapH / 2;
|
||||
int visMinX = (int)Math.floor((this.mapX - edge - this.mapX - halfW) * invScale) + this.viewCenterX;
|
||||
int visMaxX = (int)Math.ceil((this.mapX + this.mapW + edge - this.mapX - halfW) * invScale) + this.viewCenterX;
|
||||
int visMinZ = (int)Math.floor((this.mapY - edge - this.mapY - halfH) * invScale) + this.viewCenterZ;
|
||||
int visMaxZ = (int)Math.ceil((this.mapY + this.mapH + edge - this.mapY - halfH) * invScale) + this.viewCenterZ;
|
||||
for (TerritoryState state : model.states()) {
|
||||
boolean self = state.equals(model.selfState());
|
||||
int color = state.color();
|
||||
int fill = this.withAlpha(color, self ? 155 : 92);
|
||||
for (ChunkPos chunk : state.chunks()) {
|
||||
int cx = chunk.x();
|
||||
int cz = chunk.z();
|
||||
if (cx < visMinX || cx > visMaxX || cz < visMinZ || cz > visMaxZ) continue;
|
||||
this.drawChunkFill(context, chunk, fill);
|
||||
}
|
||||
if (!this.isVisible(state.capital())) continue;
|
||||
@@ -386,6 +377,11 @@ extends Screen {
|
||||
for (ChunkPos chunk : model.sessionClaims()) {
|
||||
this.drawChunkFill(context, chunk, -1938440928);
|
||||
}
|
||||
// 标记飞地(疏漏区块):己方用醒目的红色描边提示填补,其他邦国用暗灰色标示
|
||||
Set<ChunkPos> selfFlying = model.selfFlyingChunks();
|
||||
for (ChunkPos chunk : model.flyingChunks()) {
|
||||
this.drawChunkOutline(context, chunk, selfFlying.contains(chunk) ? -65536 : this.withAlpha(-8333060, 160));
|
||||
}
|
||||
}
|
||||
|
||||
/** 绘制玩家位置(白色方块)与当前目标区块(橙色描边) */
|
||||
@@ -476,32 +472,63 @@ extends Screen {
|
||||
|
||||
/** 绘制右侧信息面板:字段标签、运行日志与状态/进度信息 */
|
||||
private void drawInfo(DrawContext context) {
|
||||
int shown;
|
||||
// ===== 右侧配置面板字段标签 =====
|
||||
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, "\u7b49\u5f85\u8d85\u65f6", this.panelX + 4, this.advancedY + 4, -5592406);
|
||||
context.drawTextWithShadow(this.textRenderer, "\u91cd\u8bd5\u6b21\u6570", this.panelX + 116, this.advancedY + 4, -5592406);
|
||||
context.drawTextWithShadow(this.textRenderer, "\u533a\u57df\u4e2d\u5fc3X", this.panelX + 4, this.advancedY + this.advRowGap + 4, -5592406);
|
||||
context.drawTextWithShadow(this.textRenderer, "\u533a\u57df\u4e2d\u5fc3Z", this.panelX + 116, this.advancedY + this.advRowGap + 4, -5592406);
|
||||
context.drawTextWithShadow(this.textRenderer, "\u533a\u57df\u534a\u5f84", this.panelX + 4, this.advancedY + this.advRowGap * 2 + 4, -5592406);
|
||||
context.drawTextWithShadow(this.textRenderer, "\u6269\u5708\u4e0a\u9650", this.panelX + 116, this.advancedY + this.advRowGap * 2 + 4, -5592406);
|
||||
context.drawTextWithShadow(this.textRenderer, "\u5237\u65b0\u95f4\u9694", this.panelX + 4, this.advancedY + this.advRowGap * 3 + 4, -5592406);
|
||||
int belowY = this.mapY + this.mapH + 12;
|
||||
context.drawTextWithShadow(this.textRenderer, "\u6743\u9650\u8bbe\u7f6e", this.panelX + 4, this.advancedY, -5592406);
|
||||
|
||||
// ===== 底部信息面板:日志(左)+ 状态(右),均单行截断,避免换行导致的重叠 =====
|
||||
int infoTop = this.mapY + this.mapH + 10;
|
||||
int logWidth = Math.max(120, (int)(this.mapW * 0.52));
|
||||
int statusX = this.mapX + logWidth + 20;
|
||||
int statusWidth = this.mapW - logWidth - 20;
|
||||
int statusX = this.mapX + logWidth + 24;
|
||||
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();
|
||||
for (int i = shown = Math.min(4, logs.size()); i > 0; --i) {
|
||||
String trimmed = this.textRenderer.trimToWidth(logs.get(logs.size() - i), logWidth);
|
||||
context.drawTextWithShadow(this.textRenderer, trimmed, this.mapX, belowY + (shown - i) * 12, -6577233);
|
||||
float logScale = 0.75f; // 日志字号缩放(比默认小一号)
|
||||
int logAreaTop = infoTop + 12;
|
||||
int logAreaBottom = infoTop + 56; // 与竖向分隔线底部对齐
|
||||
int wrapWidth = (int)((logWidth - 4) / logScale); // 缩放后的可用换行宽度(按未缩放坐标计算)
|
||||
context.getMatrices().push();
|
||||
context.getMatrices().scale(logScale, logScale, 1.0f);
|
||||
int lineY = logAreaTop;
|
||||
outer:
|
||||
for (int i = logs.size() - 1; i >= 0; --i) {
|
||||
for (OrderedText line : this.textRenderer.wrapLines(Text.literal(logs.get(i)), wrapWidth)) {
|
||||
if (lineY + 9 > logAreaBottom) {
|
||||
break outer;
|
||||
}
|
||||
context.drawTextWithShadow(this.textRenderer, line, (int)((this.mapX + 2) / logScale), (int)(lineY / logScale), -6577233);
|
||||
lineY += 9;
|
||||
}
|
||||
}
|
||||
context.getMatrices().pop();
|
||||
|
||||
// 状态信息:单行截断 + 固定 12px 行距,彻底消除换行重叠
|
||||
int target = this.config.targetCount;
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
context.drawWrappedTextWithShadow(this.textRenderer, Text.literal(this.refreshStatusText()), statusX, belowY + 46, statusWidth, -5592406);
|
||||
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);
|
||||
String error = this.mod.dataService().lastError();
|
||||
String[] statusLines = new String[]{
|
||||
"\u72b6\u6001\uff1a" + this.controller.status(),
|
||||
"\u8fdb\u5ea6\uff1a" + this.controller.completedClaims() + " / " + targetText + "\uff0c\u7b49\u5f85\uff1a" + this.controller.lastProgress() + "%",
|
||||
"\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);
|
||||
}
|
||||
|
||||
/** 生成地图刷新状态文本(显示上次成功刷新距今的时间) */
|
||||
@@ -542,13 +569,6 @@ extends Screen {
|
||||
private void parseToConfig() {
|
||||
this.config.stateName = this.stateField.getText().trim();
|
||||
this.config.targetCount = this.parseInt(this.targetCountField.getText(), 20);
|
||||
this.config.waitTimeoutSeconds = this.parseInt(this.waitTimeoutField.getText(), 60);
|
||||
this.config.retries = this.parseInt(this.retriesField.getText(), 2);
|
||||
this.config.regionCenterX = this.parseInt(this.regionXField.getText(), this.playerChunk().x());
|
||||
this.config.regionCenterZ = this.parseInt(this.regionZField.getText(), this.playerChunk().z());
|
||||
this.config.regionRadius = this.parseInt(this.regionRadiusField.getText(), 5);
|
||||
this.config.maxRings = this.parseInt(this.ringsField.getText(), 0);
|
||||
this.config.refreshIntervalSeconds = this.parseInt(this.refreshIntervalField.getText(), 30);
|
||||
this.config.allowSprint = this.sprintBox.isChecked();
|
||||
this.config.allowJump = this.jumpBox.isChecked();
|
||||
this.config.avoidMobs = this.avoidBox.isChecked();
|
||||
@@ -562,22 +582,16 @@ extends Screen {
|
||||
target.strategy = this.config.strategy;
|
||||
target.stateName = this.config.stateName;
|
||||
target.targetCount = this.config.targetCount;
|
||||
target.waitTimeoutSeconds = this.config.waitTimeoutSeconds;
|
||||
target.retries = this.config.retries;
|
||||
target.regionCenterX = this.config.regionCenterX;
|
||||
target.regionCenterZ = this.config.regionCenterZ;
|
||||
target.regionRadius = this.config.regionRadius;
|
||||
target.regionMinX = this.config.regionMinX;
|
||||
target.regionMaxX = this.config.regionMaxX;
|
||||
target.regionMinZ = this.config.regionMinZ;
|
||||
target.regionMaxZ = this.config.regionMaxZ;
|
||||
target.maxRings = this.config.maxRings;
|
||||
target.refreshIntervalSeconds = this.config.refreshIntervalSeconds;
|
||||
target.allowSprint = this.config.allowSprint;
|
||||
target.allowJump = this.config.allowJump;
|
||||
target.avoidMobs = this.config.avoidMobs;
|
||||
target.allowBreak = this.config.allowBreak;
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import autostore.client.config.ExpansionConfig;
|
||||
import autostore.client.territory.ChunkPos;
|
||||
import autostore.client.territory.TerritoryModel;
|
||||
import autostore.client.territory.TerritoryState;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
@@ -41,17 +40,47 @@ public final class ExpansionPlanner {
|
||||
* @return 建议占领的区块列表(按优先级排序)
|
||||
*/
|
||||
public List<ChunkPos> generate(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) {
|
||||
List<ChunkPos> result;
|
||||
if (config.strategy == 1) {
|
||||
return this.generateHolePriority(model, config, playerChunk);
|
||||
result = this.generateHolePriority(model, config, playerChunk);
|
||||
} else if (config.strategy == 4) {
|
||||
result = this.generatePerimeter(model, config, playerChunk);
|
||||
} else if (config.strategy == 2 && config.regionMaxX >= config.regionMinX) {
|
||||
result = this.generateRectangle(model, config, playerChunk);
|
||||
} else {
|
||||
result = this.generateLine(model, config, playerChunk);
|
||||
}
|
||||
if (config.strategy == 4) {
|
||||
return this.generatePerimeter(model, config, playerChunk);
|
||||
return this.prefixFlyingChunks(model, config, playerChunk, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将己方领土内的"飞地(疏漏区块)"提到队列最前,优先填补。
|
||||
*
|
||||
* <p>飞地指未被占领、但四周(四个正方向)都被己方领土包围的区块,
|
||||
* 是自动扩地过程中最容易漏掉的疏漏。先把它们填上可保证领土闭合、不留缺口。</p>
|
||||
*/
|
||||
private List<ChunkPos> prefixFlyingChunks(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk, List<ChunkPos> planned) {
|
||||
Set<ChunkPos> flying = model.selfFlyingChunks();
|
||||
if (flying == null || flying.isEmpty()) {
|
||||
return planned;
|
||||
}
|
||||
// 策略 2 区域扩地:玩家圈选了矩形时,完全按矩形区域扩地
|
||||
if (config.strategy == 2 && config.regionMaxX >= config.regionMinX) {
|
||||
return this.generateRectangle(model, config, playerChunk);
|
||||
int limit = config.targetCount > 0 ? config.targetCount : Math.max(planned.size(), flying.size());
|
||||
Comparator<ChunkPos> byDistance = Comparator.comparingInt(playerChunk::manhattanDistance);
|
||||
ArrayList<ChunkPos> result = new ArrayList<ChunkPos>();
|
||||
// 飞地优先,且必须仍在配置区域内
|
||||
for (ChunkPos chunk : flying) {
|
||||
if (result.size() >= limit) break;
|
||||
if (!this.isInRegion(chunk, config)) continue;
|
||||
result.add(chunk);
|
||||
}
|
||||
return this.generateLine(model, config, playerChunk);
|
||||
result.sort(byDistance);
|
||||
// 其余规划区块依次补充,去重且不超过 limit
|
||||
for (ChunkPos chunk : planned) {
|
||||
if (result.size() >= limit) break;
|
||||
if (flying.contains(chunk)) continue;
|
||||
result.add(chunk);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,11 +220,18 @@ public final class ExpansionPlanner {
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略 2 矩形区域扩地:完全按玩家圈选的矩形区域扩地。
|
||||
* 策略 2 矩形区域扩地:严格 S 形(蛇形)扫描,由内而外推进。
|
||||
*
|
||||
* <p>先检查矩形区域是否与己方领土接壤(矩形内至少有一个未占领区块与领土相邻),
|
||||
* 若未接壤则返回空列表(不扩地)。接壤后,从接壤边界开始 BFS 逐层向内,
|
||||
* 将矩形内所有未被占领的区块全部纳入计划,保证每个区块都与已占领领土连通。
|
||||
* <p>扫描规则(v1.2.1 重写):
|
||||
* <ul>
|
||||
* <li>起始扫描线 = 矩形四条边中<b>离王城直线距离最近</b>的那条边(点到线段距离),
|
||||
* 从该边向对边单调推进——即"由内而外",避免先跑到区域外侧绕圈;</li>
|
||||
* <li>扫描行/列内做标准蛇形往返(相邻行方向交替),即使待开垦区块构成不规则多边形,
|
||||
* 也始终按 S 形顺序一路扫过去,跳过暂时不可开垦的区块而不打乱顺序;</li>
|
||||
* <li>首条扫描线的行内起始端同样选取离王城更近的一端;</li>
|
||||
* <li>每个入选区块必须与"己方领土(含本次已选)"四正方向接壤,保证服务器接受开拓。</li>
|
||||
* </ul>
|
||||
* 复杂度:O(区域面积) 单遍扫描 + O(4) 接壤检查。</p>
|
||||
*/
|
||||
private List<ChunkPos> generateRectangle(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) {
|
||||
Set<ChunkPos> actualOwn = this.ownChunks(model);
|
||||
@@ -208,50 +244,86 @@ public final class ExpansionPlanner {
|
||||
int minZ = config.regionMinZ;
|
||||
int maxZ = config.regionMaxZ;
|
||||
int limit = config.targetCount > 0 ? config.targetCount : Integer.MAX_VALUE;
|
||||
// 检查矩形是否与己方领土接壤:矩形内至少有一个未占领区块与领土相邻
|
||||
boolean bordersOwn = false;
|
||||
for (int x = minX; x <= maxX; ++x) {
|
||||
for (int z = minZ; z <= maxZ; ++z) {
|
||||
ChunkPos chunk = new ChunkPos(x, z);
|
||||
if (allClaimed.contains(chunk)) continue;
|
||||
if (this.ownNeighbors(chunk, actualOwn) > 0) {
|
||||
bordersOwn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bordersOwn) break;
|
||||
}
|
||||
if (!bordersOwn) {
|
||||
return List.of();
|
||||
}
|
||||
// 初始边界:矩形内与己方领土(原始领土)直接相邻的未占领区块
|
||||
HashSet<ChunkPos> plannedOwn = new HashSet<ChunkPos>(actualOwn);
|
||||
ArrayDeque<ChunkPos> frontier = new ArrayDeque<ChunkPos>();
|
||||
ChunkPos capital = this.capital(model, playerChunk);
|
||||
// ===== 1. 四条边到王城的距离(点到线段距离的平方),选最近者为起始边 =====
|
||||
// 王城坐标投影到边所在线段的横向偏移(王城在区域横向范围外时才有偏移)
|
||||
int dxEdge = Math.max(0, Math.max(minX - capital.x(), capital.x() - maxX));
|
||||
int dzEdge = Math.max(0, Math.max(minZ - capital.z(), capital.z() - maxZ));
|
||||
long distNorth = (long)dxEdge * dxEdge + (long)(minZ - capital.z()) * (minZ - capital.z());
|
||||
long distSouth = (long)dxEdge * dxEdge + (long)(maxZ - capital.z()) * (maxZ - capital.z());
|
||||
long distWest = (long)(minX - capital.x()) * (minX - capital.x()) + (long)dzEdge * dzEdge;
|
||||
long distEast = (long)(maxX - capital.x()) * (maxX - capital.x()) + (long)dzEdge * dzEdge;
|
||||
// 最近边决定扫描方向:北/南边最近 → 横向逐行扫(行沿 Z 推进);西/东边最近 → 纵向逐列扫
|
||||
boolean rowScan = Math.min(distNorth, distSouth) <= Math.min(distWest, distEast);
|
||||
// ===== 2. 生成 S 形扫描序列并按接壤过滤 =====
|
||||
// 接壤判断集合:己方领土 + 本次已选区块。
|
||||
// 注意:绝不能把其他邦国领土(allClaimed)混入接壤判断集合,
|
||||
// 否则会选中"挨着其他邦国、但与己方领土不相邻"的区块,服务器会拒绝开拓(历史 bug)。
|
||||
HashSet<ChunkPos> claimed = new HashSet<ChunkPos>(actualOwn);
|
||||
ArrayList<ChunkPos> result = new ArrayList<ChunkPos>();
|
||||
for (int x = minX; x <= maxX; ++x) {
|
||||
for (int z = minZ; z <= maxZ; ++z) {
|
||||
ChunkPos chunk = new ChunkPos(x, z);
|
||||
if (allClaimed.contains(chunk) || plannedOwn.contains(chunk)) continue;
|
||||
if (this.ownNeighbors(chunk, actualOwn) > 0) {
|
||||
plannedOwn.add(chunk);
|
||||
frontier.add(chunk);
|
||||
if (rowScan) {
|
||||
// 行序:从离王城近的边(z=minZ 或 maxZ)向对边推进;首行行内起点选离王城 x 近的一端
|
||||
int startRow = distNorth <= distSouth ? minZ : maxZ;
|
||||
int rowStep = startRow == minZ ? 1 : -1;
|
||||
boolean ltr = Math.abs(capital.x() - minX) <= Math.abs(capital.x() - maxX);
|
||||
for (int row = startRow; row >= minZ && row <= maxZ; row += rowStep) {
|
||||
if (ltr) {
|
||||
for (int x = minX; x <= maxX; ++x) {
|
||||
this.tryClaimRect(result, claimed, allClaimed, x, row, limit);
|
||||
}
|
||||
} else {
|
||||
for (int x = maxX; x >= minX; --x) {
|
||||
this.tryClaimRect(result, claimed, allClaimed, x, row, limit);
|
||||
}
|
||||
}
|
||||
ltr = !ltr;
|
||||
if (result.size() >= limit) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
// BFS 逐层向内填满整个矩形
|
||||
while (!frontier.isEmpty() && result.size() < limit) {
|
||||
ChunkPos chunk = frontier.removeFirst();
|
||||
result.add(chunk);
|
||||
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
|
||||
if (neighbor.x() < minX || neighbor.x() > maxX || neighbor.z() < minZ || neighbor.z() > maxZ) continue;
|
||||
if (allClaimed.contains(neighbor) || plannedOwn.contains(neighbor)) continue;
|
||||
plannedOwn.add(neighbor);
|
||||
frontier.add(neighbor);
|
||||
} else {
|
||||
// 列序:从离王城近的边(x=minX 或 maxX)向对边推进;首列列内起点选离王城 z 近的一端
|
||||
int startCol = distWest <= distEast ? minX : maxX;
|
||||
int colStep = startCol == minX ? 1 : -1;
|
||||
boolean ttb = Math.abs(capital.z() - minZ) <= Math.abs(capital.z() - maxZ);
|
||||
for (int col = startCol; col >= minX && col <= maxX; col += colStep) {
|
||||
if (ttb) {
|
||||
for (int z = minZ; z <= maxZ; ++z) {
|
||||
this.tryClaimRect(result, claimed, allClaimed, col, z, limit);
|
||||
}
|
||||
} else {
|
||||
for (int z = maxZ; z >= minZ; --z) {
|
||||
this.tryClaimRect(result, claimed, allClaimed, col, z, limit);
|
||||
}
|
||||
}
|
||||
ttb = !ttb;
|
||||
if (result.size() >= limit) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* S 形扫描中的一个格子:若该区块未被任何邦国占领、且与"己方领土(含本次已选)"接壤,
|
||||
* 则纳入本次扩张计划;否则跳过(保持 S 形顺序一路扫过去,跳过的不打乱后续顺序)。
|
||||
*/
|
||||
private void tryClaimRect(List<ChunkPos> result, Set<ChunkPos> claimed, Set<ChunkPos> allClaimed, int x, int z, int limit) {
|
||||
if (result.size() >= limit) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunk = new ChunkPos(x, z);
|
||||
if (allClaimed.contains(chunk) || claimed.contains(chunk)) {
|
||||
return;
|
||||
}
|
||||
if (this.ownNeighbors(chunk, claimed) == 0) {
|
||||
return;
|
||||
}
|
||||
result.add(chunk);
|
||||
claimed.add(chunk);
|
||||
}
|
||||
|
||||
/** 在候选集中找到离当前游标最近的一个区块(曼哈顿距离),作为推进的前沿 */
|
||||
private ChunkPos nearestFrontier(Set<ChunkPos> candidates, ChunkPos cursor) {
|
||||
return candidates.stream().min(Comparator.comparingInt(cursor::manhattanDistance)).orElse(null);
|
||||
@@ -389,7 +461,13 @@ public final class ExpansionPlanner {
|
||||
return Math.max(dx, dz = Math.abs(chunk.z() - config.regionCenterZ)) <= Math.max(1, config.regionRadius);
|
||||
}
|
||||
|
||||
/** 统计某区块周围(上下左右)属于给定集合的邻居数量 */
|
||||
/**
|
||||
* 统计某区块周围(上下左右四个正方向)属于给定集合的邻居数量。
|
||||
*
|
||||
* <p>调用方必须传入“己方领土”集合(含本次规划已纳入的区块)作为 {@code own},
|
||||
* 用于判断区块是否与己方领土接壤。绝不能把其他邦国的领土混入该集合,
|
||||
* 否则会误判“与他国接壤”为“可开拓”,选中一个与己方领土不相邻的无效目标。</p>
|
||||
*/
|
||||
private int ownNeighbors(ChunkPos chunk, Set<ChunkPos> own) {
|
||||
int count = 0;
|
||||
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -65,6 +66,28 @@ public final class TerritoryDataService {
|
||||
private volatile long lastSuccessfulFetchMillis;
|
||||
/** 最近一次尝试拉取的时间戳(毫秒) */
|
||||
private volatile long lastAttemptMillis;
|
||||
/** 会话内新占领区块的本地记录时间(毫秒),用于刷新时判断该占领是否已被地图数据确认 */
|
||||
private final Map<ChunkPos, Long> sessionClaimTimes = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 会话占领"确认宽限"时长(毫秒)。
|
||||
*
|
||||
* <p>占领成功后,服务器动态地图(Dynmap/markers.json)可能尚未同步该区块,
|
||||
* 因此在宽限期内即使地图未确认也先保留"会话占领"标记,避免刚占领的区块被误判为假占领。
|
||||
* 超过宽限期仍未在地图中确认的,视为未真正占领并丢弃,使空洞区块能重新进入规划。
|
||||
* (v1.2.1 由 120 秒缩短为 60 秒:配合请求侧缓存击穿,Dynmap 秒级同步,缩短可更快修正假占领)</p>
|
||||
*/
|
||||
private static final long CLAIM_CONFIRM_GRACE_MS = 60_000L;
|
||||
/**
|
||||
* ChunkPos 驻留池:坐标 → 复用实例。
|
||||
*
|
||||
* <p>每次解析 markers.json 会创建约 8 万个 ChunkPos,且各次刷新的坐标高度重合。
|
||||
* 驻留后新旧模型共享同一批实例,刷新时垃圾量从"整份模型"降为"集合外壳",
|
||||
* 显著降低 GC 压力(缓解刷新时游戏卡顿与内存占用)。</p>
|
||||
*/
|
||||
private static final ConcurrentHashMap<Long, ChunkPos> CHUNK_POOL = new ConcurrentHashMap<Long, ChunkPos>();
|
||||
/** 上次成功响应的正文哈希与长度,用于识别"网络缓存返回了相同旧数据"并跳过重复解析 */
|
||||
private volatile int lastBodyHash;
|
||||
private volatile int lastBodyLength = -1;
|
||||
|
||||
public TerritoryDataService() {
|
||||
this.executor = TerritoryDataService.newExecutor();
|
||||
@@ -111,27 +134,63 @@ public final class TerritoryDataService {
|
||||
|
||||
/**
|
||||
* 异步刷新领土数据(在调度线程中执行)。
|
||||
* 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"合并进新模型,
|
||||
* 避免刷新覆盖掉本次会话的占领记录。
|
||||
* 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"与刚拉取的最新地图数据仔细核对,
|
||||
* 只保留"已被地图确认"或"刚占领未超宽限期"的占领记录,避免刷新时把假占领永久留在模型里。
|
||||
*/
|
||||
public void refreshAsync() {
|
||||
this.executor.execute(() -> {
|
||||
this.lastAttemptMillis = System.currentTimeMillis();
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(MARKERS_URL)).timeout(Duration.ofSeconds(15L)).header("User-Agent", "Mozilla/5.0 autostore/1.0").GET().build();
|
||||
// 请求侧缓存击穿:URL 追加时间戳参数 + no-cache 头,确保每次都拿到服务器最新数据。
|
||||
// 此前约半数刷新命中网络缓存拿到旧数据,导致会话占领迟迟无法被地图确认/纠正。
|
||||
String url = MARKERS_URL + "?_=" + System.currentTimeMillis();
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(15L))
|
||||
.header("User-Agent", "Mozilla/5.0 autostore/1.2.1")
|
||||
.header("Cache-Control", "no-cache, no-store")
|
||||
.header("Pragma", "no-cache").GET().build();
|
||||
HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
this.lastError = "HTTP " + response.statusCode();
|
||||
return;
|
||||
}
|
||||
String body = response.body();
|
||||
// 内容未变化(服务器确实无更新,或缓存仍命中相同旧文):跳过重复解析,
|
||||
// 避免"每次刷新都重建 8 万区块模型"的 CPU/GC 开销;模型与飞地缓存保持原样。
|
||||
if (body.length() == this.lastBodyLength && body.hashCode() == this.lastBodyHash) {
|
||||
this.lastSuccessfulFetchMillis = System.currentTimeMillis();
|
||||
this.lastError = "";
|
||||
return;
|
||||
}
|
||||
this.lastBodyHash = body.hashCode();
|
||||
this.lastBodyLength = body.length();
|
||||
TerritoryModel parsed = TerritoryDataService.parse(response.body());
|
||||
TerritoryModel previous = this.model;
|
||||
if (previous != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
// 本次拉取到的己方领土区块(地图数据即最终依据,无需缓存旧数据)
|
||||
Set<ChunkPos> freshOwn = new HashSet<>();
|
||||
TerritoryState freshSelf = parsed.state(this.selectedStateName);
|
||||
if (freshSelf != null) {
|
||||
freshOwn.addAll(freshSelf.chunks());
|
||||
}
|
||||
for (ChunkPos sessionClaim : previous.sessionClaims()) {
|
||||
parsed.addSessionClaim(sessionClaim);
|
||||
Long claimTime = this.sessionClaimTimes.get(sessionClaim);
|
||||
boolean confirmed = freshOwn.contains(sessionClaim);
|
||||
boolean withinGrace = claimTime != null && now - claimTime < CLAIM_CONFIRM_GRACE_MS;
|
||||
if (confirmed || withinGrace) {
|
||||
// 地图已确认,或尚在宽限期(Dynmap 可能还没同步):保留会话占领标记
|
||||
parsed.addSessionClaim(sessionClaim);
|
||||
} else if (claimTime != null) {
|
||||
// 地图始终未确认且超时:判定为假占领并移除,使空洞区块重新进入规划
|
||||
this.sessionClaimTimes.remove(sessionClaim);
|
||||
}
|
||||
// claimTime 为 null(从未在本服务记录)也视为未确认:不保留
|
||||
}
|
||||
}
|
||||
parsed.setSelfStateName(this.selectedStateName);
|
||||
// 后台线程预计算飞地缓存:把"各国外接矩形全量扫描"从渲染帧挪到这里,
|
||||
// 修复"刷新地图/打开 GUI 时游戏卡一下"的问题
|
||||
parsed.precomputeFlyingChunks();
|
||||
this.model = parsed;
|
||||
this.lastSuccessfulFetchMillis = System.currentTimeMillis();
|
||||
this.lastError = "";
|
||||
@@ -163,6 +222,7 @@ public final class TerritoryDataService {
|
||||
}
|
||||
|
||||
public void addSessionClaim(ChunkPos chunk) {
|
||||
this.sessionClaimTimes.put(chunk, System.currentTimeMillis());
|
||||
TerritoryModel current = this.model;
|
||||
if (current != null) {
|
||||
current.addSessionClaim(chunk);
|
||||
@@ -240,9 +300,9 @@ public final class TerritoryDataService {
|
||||
return;
|
||||
}
|
||||
Set chunks = claims.computeIfAbsent(name, ignored -> new HashSet());
|
||||
for (List<int[]> polygon : TerritoryDataService.extractPolygons(marker.get("points"))) {
|
||||
TerritoryDataService.fillPolygon(chunks, polygon);
|
||||
}
|
||||
// 一个 marker 的多个多边形遵循 dynmap 的 even-odd 填充规则:
|
||||
// 外圈为正,内圈多边形(国中飞地的边界)作为孔洞被减去,而非作为独立领土并集填充。
|
||||
TerritoryDataService.fillPolygonEvenOdd(chunks, TerritoryDataService.extractPolygons(marker.get("points")));
|
||||
colors.putIfAbsent(name, TerritoryDataService.parseColor(TerritoryDataService.stringOr(marker, "color", "#75bd20")));
|
||||
}
|
||||
|
||||
@@ -308,35 +368,59 @@ public final class TerritoryDataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多边形覆盖的区块填充进集合:
|
||||
* 先算出多边形外接矩形(换算为区块坐标),再逐区块用"点在多边形内"算法判断,
|
||||
* 以区块中心点(cx*16+8, cz*16+8)是否落在多边形内为准。
|
||||
* 面积超过 600 万区块时直接放弃,防止异常数据导致卡死。
|
||||
* 将多边形组覆盖的区块按 even-odd 规则填充进集合。
|
||||
*
|
||||
* <p>一个 marker 里的多个多边形遵循 dynmap 的奇偶填充:区块落入<b>奇数个</b>多边形内时才视为领土;
|
||||
* 这样内圈多边形(国中飞地/孔洞的边界)会被正确减去,而不是像并集那样把孔洞也填成领土。</p>
|
||||
*
|
||||
* @param output 输出集合(该邦国的区块集合)
|
||||
* @param polygons 该 marker 的全部多边形,第一个为外圈、可为多个内圈孔洞
|
||||
*/
|
||||
private static void fillPolygon(Set<ChunkPos> output, List<int[]> polygon) {
|
||||
int area;
|
||||
private static void fillPolygonEvenOdd(Set<ChunkPos> output, List<List<int[]>> polygons) {
|
||||
if (polygons.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (int[] point : polygon) {
|
||||
minX = Math.min(minX, Math.floorDiv(point[0], 16));
|
||||
maxX = Math.max(maxX, Math.floorDiv(point[0], 16));
|
||||
minZ = Math.min(minZ, Math.floorDiv(point[1], 16));
|
||||
maxZ = Math.max(maxZ, Math.floorDiv(point[1], 16));
|
||||
for (List<int[]> polygon : polygons) {
|
||||
for (int[] point : polygon) {
|
||||
minX = Math.min(minX, Math.floorDiv(point[0], 16));
|
||||
maxX = Math.max(maxX, Math.floorDiv(point[0], 16));
|
||||
minZ = Math.min(minZ, Math.floorDiv(point[1], 16));
|
||||
maxZ = Math.max(maxZ, Math.floorDiv(point[1], 16));
|
||||
}
|
||||
}
|
||||
int n = area = ((maxX - minX) + 1L) * ((maxZ - minZ) + 1L) > Integer.MAX_VALUE ? Integer.MAX_VALUE : (maxX - minX + 1) * (maxZ - minZ + 1);
|
||||
if (area > 6000000) {
|
||||
// 面积超过 600 万区块时直接放弃,防止异常数据导致卡死
|
||||
if ((long)(maxX - minX + 1) * (maxZ - minZ + 1) > 6000000L) {
|
||||
return;
|
||||
}
|
||||
for (int cx = minX; cx <= maxX; ++cx) {
|
||||
for (int cz = minZ; cz <= maxZ; ++cz) {
|
||||
if (!TerritoryDataService.pointInPolygon(cx * 16 + 8, cz * 16 + 8, polygon)) continue;
|
||||
output.add(new ChunkPos(cx, cz));
|
||||
// 奇偶填充:落在奇数个多边形内则为领土;否则(含恰好落在偶数个内圈孔洞中)忽略
|
||||
boolean inside = false;
|
||||
for (List<int[]> polygon : polygons) {
|
||||
if (TerritoryDataService.pointInPolygon(cx * 16 + 8, cz * 16 + 8, polygon)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
if (inside) {
|
||||
output.add(TerritoryDataService.internChunk(cx, cz));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驻留池获取区块坐标实例:同一坐标跨模型复用同一对象。
|
||||
* 刷新频率下大幅减少 ChunkPos 临时分配与 GC 压力(见 {@link #CHUNK_POOL})。
|
||||
*/
|
||||
private static ChunkPos internChunk(int x, int z) {
|
||||
long key = (long)x << 32 | (long)z & 0xFFFFFFFFL;
|
||||
return TerritoryDataService.CHUNK_POOL.computeIfAbsent(key, k -> new ChunkPos(x, z));
|
||||
}
|
||||
|
||||
/** 射线法判断点 (x, z) 是否在多边形内(经典奇偶规则) */
|
||||
private static boolean pointInPolygon(int x, int z, List<int[]> polygon) {
|
||||
boolean inside = false;
|
||||
|
||||
@@ -35,6 +35,18 @@ public final class TerritoryModel {
|
||||
private final Set<ChunkPos> sessionClaims;
|
||||
/** 当前选中的己方邦国名称 */
|
||||
private String selfStateName = "";
|
||||
/**
|
||||
* 飞地(疏漏区块)缓存:邦国名 → 该国飞地区块集合。
|
||||
*
|
||||
* <p>飞地指"四周(四个正方向)都被同一邦国占领、但自身未被占领"的区块,
|
||||
* 通常是自动扩地时漏掉的疏漏。该结果在模型构建后懒计算并缓存,
|
||||
* 仅在新增会话占领(addSessionClaim)后失效重算。
|
||||
* 字段为 volatile:后台线程可调用 {@link #precomputeFlyingChunks()} 预热,
|
||||
* 渲染线程读取时保证可见性,避免把全量扫描留在渲染帧里(卡顿来源)。</p>
|
||||
*/
|
||||
private transient volatile Map<String, Set<ChunkPos>> cachedFlyingChunks;
|
||||
/** 所有邦国飞地的合并并集缓存(volatile,同上);仅少量区块,供 GUI 直接遍历 */
|
||||
private transient volatile Set<ChunkPos> cachedAllFlyingChunks;
|
||||
|
||||
public TerritoryModel(Map<String, TerritoryState> states) {
|
||||
this.states = new HashMap<String, TerritoryState>(states);
|
||||
@@ -94,6 +106,95 @@ public final class TerritoryModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有邦国的飞地(疏漏区块)并集(只读视图,结果缓存,帧间零分配)。
|
||||
*
|
||||
* <p>飞地指“四周(四个正方向)都被同一邦国占领、但自身未被占领”的区块。
|
||||
* 这些往往是自动扩地过程中漏掉的疏漏,需要在地图上标出并优先填补。</p>
|
||||
*/
|
||||
public Set<ChunkPos> flyingChunks() {
|
||||
Set<ChunkPos> cached = this.cachedAllFlyingChunks;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
HashSet<ChunkPos> result = new HashSet<ChunkPos>();
|
||||
for (Set<ChunkPos> chunks : this.flyingChunksByState().values()) {
|
||||
result.addAll(chunks);
|
||||
}
|
||||
Set<ChunkPos> frozen = Collections.unmodifiableSet(result);
|
||||
this.cachedAllFlyingChunks = frozen;
|
||||
return frozen;
|
||||
}
|
||||
|
||||
/** 返回己方邦国的飞地(疏漏区块)(只读视图);未选择邦国时为空集 */
|
||||
public Set<ChunkPos> selfFlyingChunks() {
|
||||
return Collections.unmodifiableSet(this.flyingChunksByState().getOrDefault(this.selfStateName, Set.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台预计算飞地缓存(在数据刷新线程调用):
|
||||
* 把"各国外接矩形全量扫描"提前到后台线程完成,
|
||||
* 渲染线程读到的永远是现成缓存,避免模型刷新后第一帧卡顿。
|
||||
*/
|
||||
public void precomputeFlyingChunks() {
|
||||
this.flyingChunks();
|
||||
}
|
||||
|
||||
/** 计算并缓存各邦国的飞地区块(懒加载,addSessionClaim 后失效重算) */
|
||||
private Map<String, Set<ChunkPos>> flyingChunksByState() {
|
||||
Map<String, Set<ChunkPos>> cached = this.cachedFlyingChunks;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
HashMap<String, Set<ChunkPos>> map = new HashMap<String, Set<ChunkPos>>();
|
||||
for (TerritoryState state : this.states.values()) {
|
||||
map.put(state.name(), this.computeFlyingChunks(state.chunks()));
|
||||
}
|
||||
this.cachedFlyingChunks = map;
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算某邦国的全部飞地区块:
|
||||
* 在该邦国的外接矩形内,找出“未被任何邦国占领、但四个正方向均属于该邦国”的区块。
|
||||
*/
|
||||
private Set<ChunkPos> computeFlyingChunks(Set<ChunkPos> own) {
|
||||
HashSet<ChunkPos> result = new HashSet<ChunkPos>();
|
||||
if (own.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (ChunkPos chunk : own) {
|
||||
minX = Math.min(minX, chunk.x());
|
||||
maxX = Math.max(maxX, chunk.x());
|
||||
minZ = Math.min(minZ, chunk.z());
|
||||
maxZ = Math.max(maxZ, chunk.z());
|
||||
}
|
||||
for (int cx = minX; cx <= maxX; ++cx) {
|
||||
for (int cz = minZ; cz <= maxZ; ++cz) {
|
||||
ChunkPos chunk = new ChunkPos(cx, cz);
|
||||
if (own.contains(chunk) || this.allClaimed.contains(chunk)) {
|
||||
continue;
|
||||
}
|
||||
if (this.surroundedBySameState(chunk, own)) {
|
||||
result.add(chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 判断区块的四个正方向邻居是否全部属于给定邦国的区块集合 */
|
||||
private boolean surroundedBySameState(ChunkPos chunk, Set<ChunkPos> own) {
|
||||
return own.contains(new ChunkPos(chunk.x() + 1, chunk.z()))
|
||||
&& own.contains(new ChunkPos(chunk.x() - 1, chunk.z()))
|
||||
&& own.contains(new ChunkPos(chunk.x(), chunk.z() + 1))
|
||||
&& own.contains(new ChunkPos(chunk.x(), chunk.z() - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录一次会话内新占领的区块:
|
||||
* 加入会话集合与全局已占领集合,并同步更新己方邦国的区块集合。
|
||||
@@ -101,6 +202,8 @@ public final class TerritoryModel {
|
||||
public void addSessionClaim(ChunkPos chunk) {
|
||||
this.sessionClaims.add(chunk);
|
||||
this.allClaimed.add(chunk);
|
||||
this.cachedFlyingChunks = null;
|
||||
this.cachedAllFlyingChunks = null;
|
||||
TerritoryState self = this.states.get(this.selfStateName);
|
||||
if (self != null) {
|
||||
HashSet<ChunkPos> updated = new HashSet<ChunkPos>(self.chunks());
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"autostore","version":"1.0.2","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.2.2","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"}]}
|
||||
在新工单中引用
屏蔽一个用户