allClaimed, int x, int z, int limit) {
+ if (result.size() >= limit) {
+ return;
}
- ChunkPos best = null;
- int bestDist = Integer.MAX_VALUE;
- for (ChunkPos chunk : borderChunks) {
- for (ChunkPos corner : corners) {
- int dist = Math.abs(chunk.x() - corner.x()) + Math.abs(chunk.z() - corner.z());
- if (dist < bestDist) {
- bestDist = dist;
- best = chunk;
- }
- }
+ ChunkPos chunk = new ChunkPos(x, z);
+ if (allClaimed.contains(chunk) || claimed.contains(chunk)) {
+ return;
}
- return best;
- }
-
- /**
- * 生成从起点开始的 S 形扫描路径,覆盖矩形内所有区块。
- *
- * 扫描主轴根据起点所在边缘决定:起点在顶/底行时横向逐行扫描,
- * 起点在左/右列时纵向逐列扫描。每行/列内方向交替反向,形成 S 形。
- * 起点所在行/列从起点向两侧铺开,保证起点是路径第一个区块。
- */
- private List buildSPath(ChunkPos start, int minX, int maxX, int minZ, int maxZ) {
- ArrayList path = new ArrayList();
- boolean horizontal = start.z() == minZ || start.z() == maxZ;
- if (horizontal) {
- int startRow = start.z();
- // 起点行:从起点向一侧到边缘,再反向到另一侧,完整覆盖该行
- boolean leftToRight = start.x() == minX;
- if (leftToRight) {
- for (int x = start.x(); x <= maxX; ++x) path.add(new ChunkPos(x, startRow));
- for (int x = start.x() - 1; x >= minX; --x) path.add(new ChunkPos(x, startRow));
- } else {
- for (int x = start.x(); x >= minX; --x) path.add(new ChunkPos(x, startRow));
- for (int x = start.x() + 1; x <= maxX; ++x) path.add(new ChunkPos(x, startRow));
- }
- // 其余行:先向下(+z)再向上(-z),每行方向交替,形成 S 形
- boolean ltr = leftToRight;
- for (int z = startRow + 1; z <= maxZ; ++z) {
- this.appendRow(path, z, minX, maxX, ltr);
- ltr = !ltr;
- }
- for (int z = startRow - 1; z >= minZ; --z) {
- this.appendRow(path, z, minX, maxX, ltr);
- ltr = !ltr;
- }
- } else {
- int startCol = start.x();
- // 起点列:从起点向一侧到边缘,再反向到另一侧,完整覆盖该列
- boolean topToBottom = start.z() == minZ;
- if (topToBottom) {
- for (int z = start.z(); z <= maxZ; ++z) path.add(new ChunkPos(startCol, z));
- for (int z = start.z() - 1; z >= minZ; --z) path.add(new ChunkPos(startCol, z));
- } else {
- for (int z = start.z(); z >= minZ; --z) path.add(new ChunkPos(startCol, z));
- for (int z = start.z() + 1; z <= maxZ; ++z) path.add(new ChunkPos(startCol, z));
- }
- // 其余列:先向右(+x)再向左(-x),每列方向交替,形成 S 形
- boolean ttb = topToBottom;
- for (int x = startCol + 1; x <= maxX; ++x) {
- this.appendCol(path, x, minZ, maxZ, ttb);
- ttb = !ttb;
- }
- for (int x = startCol - 1; x >= minX; --x) {
- this.appendCol(path, x, minZ, maxZ, ttb);
- ttb = !ttb;
- }
- }
- return path;
- }
-
- /** 追加一行区块到路径(leftToRight=true 时从左到右,否则从右到左) */
- private void appendRow(List path, int row, int minX, int maxX, boolean leftToRight) {
- if (leftToRight) {
- for (int x = minX; x <= maxX; ++x) path.add(new ChunkPos(x, row));
- } else {
- for (int x = maxX; x >= minX; --x) path.add(new ChunkPos(x, row));
- }
- }
-
- /** 追加一列区块到路径(topToBottom=true 时从上到下,否则从下到上) */
- private void appendCol(List path, int col, int minZ, int maxZ, boolean topToBottom) {
- if (topToBottom) {
- for (int z = minZ; z <= maxZ; ++z) path.add(new ChunkPos(col, z));
- } else {
- for (int z = maxZ; z >= minZ; --z) path.add(new ChunkPos(col, z));
+ if (this.ownNeighbors(chunk, claimed) == 0) {
+ return;
}
+ result.add(chunk);
+ claimed.add(chunk);
}
/** 在候选集中找到离当前游标最近的一个区块(曼哈顿距离),作为推进的前沿 */
diff --git a/src/main/java/autostore/client/territory/TerritoryDataService.java b/src/main/java/autostore/client/territory/TerritoryDataService.java
index 2d3cd24..e1dac21 100644
--- a/src/main/java/autostore/client/territory/TerritoryDataService.java
+++ b/src/main/java/autostore/client/territory/TerritoryDataService.java
@@ -74,8 +74,20 @@ public final class TerritoryDataService {
* 占领成功后,服务器动态地图(Dynmap/markers.json)可能尚未同步该区块,
* 因此在宽限期内即使地图未确认也先保留"会话占领"标记,避免刚占领的区块被误判为假占领。
* 超过宽限期仍未在地图中确认的,视为未真正占领并丢弃,使空洞区块能重新进入规划。
+ * (v1.2.1 由 120 秒缩短为 60 秒:配合请求侧缓存击穿,Dynmap 秒级同步,缩短可更快修正假占领)
*/
- private static final long CLAIM_CONFIRM_GRACE_MS = 120_000L;
+ private static final long CLAIM_CONFIRM_GRACE_MS = 60_000L;
+ /**
+ * ChunkPos 驻留池:坐标 → 复用实例。
+ *
+ * 每次解析 markers.json 会创建约 8 万个 ChunkPos,且各次刷新的坐标高度重合。
+ * 驻留后新旧模型共享同一批实例,刷新时垃圾量从"整份模型"降为"集合外壳",
+ * 显著降低 GC 压力(缓解刷新时游戏卡顿与内存占用)。
+ */
+ private static final ConcurrentHashMap CHUNK_POOL = new ConcurrentHashMap();
+ /** 上次成功响应的正文哈希与长度,用于识别"网络缓存返回了相同旧数据"并跳过重复解析 */
+ private volatile int lastBodyHash;
+ private volatile int lastBodyLength = -1;
public TerritoryDataService() {
this.executor = TerritoryDataService.newExecutor();
@@ -129,12 +141,28 @@ public final class TerritoryDataService {
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 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) {
@@ -160,6 +188,9 @@ public final class TerritoryDataService {
}
}
parsed.setSelfStateName(this.selectedStateName);
+ // 后台线程预计算飞地缓存:把"各国外接矩形全量扫描"从渲染帧挪到这里,
+ // 修复"刷新地图/打开 GUI 时游戏卡一下"的问题
+ parsed.precomputeFlyingChunks();
this.model = parsed;
this.lastSuccessfulFetchMillis = System.currentTimeMillis();
this.lastError = "";
@@ -375,12 +406,21 @@ public final class TerritoryDataService {
}
}
if (inside) {
- output.add(new ChunkPos(cx, cz));
+ 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 polygon) {
boolean inside = false;
diff --git a/src/main/java/autostore/client/territory/TerritoryModel.java b/src/main/java/autostore/client/territory/TerritoryModel.java
index d725892..523caf4 100644
--- a/src/main/java/autostore/client/territory/TerritoryModel.java
+++ b/src/main/java/autostore/client/territory/TerritoryModel.java
@@ -41,8 +41,12 @@ public final class TerritoryModel {
* 飞地指"四周(四个正方向)都被同一邦国占领、但自身未被占领"的区块,
* 通常是自动扩地时漏掉的疏漏。该结果在模型构建后懒计算并缓存,
* 仅在新增会话占领(addSessionClaim)后失效重算。
+ * 字段为 volatile:后台线程可调用 {@link #precomputeFlyingChunks()} 预热,
+ * 渲染线程读取时保证可见性,避免把全量扫描留在渲染帧里(卡顿来源)。
*/
- private transient Map> cachedFlyingChunks;
+ private transient volatile Map> cachedFlyingChunks;
+ /** 所有邦国飞地的合并并集缓存(volatile,同上);仅少量区块,供 GUI 直接遍历 */
+ private transient volatile Set cachedAllFlyingChunks;
public TerritoryModel(Map states) {
this.states = new HashMap(states);
@@ -103,17 +107,23 @@ public final class TerritoryModel {
}
/**
- * 返回所有邦国的飞地(疏漏区块)并集(只读视图)。
+ * 返回所有邦国的飞地(疏漏区块)并集(只读视图,结果缓存,帧间零分配)。
*
* 飞地指“四周(四个正方向)都被同一邦国占领、但自身未被占领”的区块。
* 这些往往是自动扩地过程中漏掉的疏漏,需要在地图上标出并优先填补。
*/
public Set flyingChunks() {
+ Set cached = this.cachedAllFlyingChunks;
+ if (cached != null) {
+ return cached;
+ }
HashSet result = new HashSet();
for (Set chunks : this.flyingChunksByState().values()) {
result.addAll(chunks);
}
- return result;
+ Set frozen = Collections.unmodifiableSet(result);
+ this.cachedAllFlyingChunks = frozen;
+ return frozen;
}
/** 返回己方邦国的飞地(疏漏区块)(只读视图);未选择邦国时为空集 */
@@ -121,6 +131,15 @@ public final class TerritoryModel {
return Collections.unmodifiableSet(this.flyingChunksByState().getOrDefault(this.selfStateName, Set.of()));
}
+ /**
+ * 后台预计算飞地缓存(在数据刷新线程调用):
+ * 把"各国外接矩形全量扫描"提前到后台线程完成,
+ * 渲染线程读到的永远是现成缓存,避免模型刷新后第一帧卡顿。
+ */
+ public void precomputeFlyingChunks() {
+ this.flyingChunks();
+ }
+
/** 计算并缓存各邦国的飞地区块(懒加载,addSessionClaim 后失效重算) */
private Map> flyingChunksByState() {
Map> cached = this.cachedFlyingChunks;
@@ -184,6 +203,7 @@ public final class TerritoryModel {
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 updated = new HashSet(self.chunks());