比较提交
| 作者 | SHA1 | 提交日期 | |
|---|---|---|---|
|
|
00aa45a551 | ||
|
|
c55e4eaf8b | ||
|
|
448dad6cc7 | ||
|
|
2b8211b3d0 | ||
|
|
25d95a5c22 |
@@ -15,12 +15,16 @@
|
|||||||
- 实时地图 GUI(通过 Ctrl+R 打开)
|
- 实时地图 GUI(通过 Ctrl+R 打开)
|
||||||
- 监听聊天/字幕消息判断开垦结果
|
- 监听聊天/字幕消息判断开垦结果
|
||||||
- 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞
|
- 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞
|
||||||
|
- 刷新请求带缓存击穿(时间戳参数 + no-cache 头),修复约半数刷新命中网络缓存拿到旧数据、导致目标被误标"已占领"且长时间不修正的问题;占领成功后立即触发刷新确认,假占领确认宽限期缩短至 60 秒
|
||||||
|
- 飞地(疏漏区块)检测:按 dynmap 奇偶填充规则解析领土多边形,正确识别国中由边界围出的内圈空腔;内置地图用红色标出己方飞地,自动扩地时优先填补这些缺口
|
||||||
|
- 区域扩地严格 S 形(蛇形)扫描:起始边为矩形四条边中离王城最近的一条,由内而外推进;即使待开垦区块构成不规则多边形也按 S 形一路扫过去
|
||||||
|
- 性能优化:地图渲染按视口快速过滤(不再每帧全量遍历 8 万区块)、飞地检测移至后台线程预计算、接壤校验零集合复制、响应内容未变化时跳过重复解析、ChunkPos 跨模型驻留复用——修复打开 GUI 与刷新地图时游戏卡顿、内存占用偏高的问题
|
||||||
- 日志导出:GUI 右上角“导出日志”按钮,或 `/autostore log` 命令,记录寻路、坐标、状态、路径事件等
|
- 日志导出:GUI 右上角“导出日志”按钮,或 `/autostore log` 命令,记录寻路、坐标、状态、路径事件等
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
1. 安装 Fabric Loader 0.16.14+ 和 Fabric API
|
1. 安装 Fabric Loader 0.16.14+ 和 Fabric API
|
||||||
2. 将 `autostore-1.1.9.jar` 放入 `.minecraft/mods/` 目录
|
2. 将 `autostore-1.2.1.jar` 放入 `.minecraft/mods/` 目录
|
||||||
3. 启动游戏,按 `Ctrl+R` 打开 GUI
|
3. 启动游戏,按 `Ctrl+R` 打开 GUI
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
@@ -30,4 +34,12 @@
|
|||||||
./build_mod.sh
|
./build_mod.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
输出文件:`autostore-1.1.9.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} ===")
|
||||||
二进制
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 88 KiB |
二进制文件未显示。
|
之后 宽度: | 高度: | 大小: 11 KiB |
+1
-1
@@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8
|
|||||||
loader_version=0.19.3
|
loader_version=0.19.3
|
||||||
|
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=1.1.9
|
mod_version=1.2.1
|
||||||
maven_group=autostore
|
maven_group=autostore
|
||||||
archives_base_name=autostore
|
archives_base_name=autostore
|
||||||
|
|
||||||
|
|||||||
@@ -451,22 +451,27 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
|
|||||||
/**
|
/**
|
||||||
* 判断区块是否与己方领土(己方邦国地图区块 + 本次会话新占领区块)接壤。
|
* 判断区块是否与己方领土(己方邦国地图区块 + 本次会话新占领区块)接壤。
|
||||||
*
|
*
|
||||||
* <p>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。</p>
|
* <p>接壤采用四个正方向(上下左右);只对“己方”领土判断,绝不把其他邦国领土计入。
|
||||||
|
* 实现上直接查询四个邻居是否命中集合,零集合复制——
|
||||||
|
* 旧实现每帧把数千个己方区块复制进临时 HashSet,是持续卡顿与内存压力的来源之一。</p>
|
||||||
*/
|
*/
|
||||||
private boolean isAdjacentToOwn(ChunkPos chunk) {
|
private boolean isAdjacentToOwn(ChunkPos chunk) {
|
||||||
TerritoryModel model = this.dataService.model();
|
TerritoryModel model = this.dataService.model();
|
||||||
Set<ChunkPos> own = new HashSet<ChunkPos>();
|
|
||||||
TerritoryState self = model.selfState();
|
TerritoryState self = model.selfState();
|
||||||
if (self != null) {
|
Set<ChunkPos> ownChunks = self == null ? null : self.chunks();
|
||||||
own.addAll(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);
|
||||||
}
|
}
|
||||||
own.addAll(model.sessionClaims());
|
|
||||||
for (ChunkPos neighbor : this.cardinalNeighbors(chunk)) {
|
/** 判断指定坐标的区块是否属于己方领土或本次会话新占领 */
|
||||||
if (own.contains(neighbor)) {
|
private boolean isOwnAt(Set<ChunkPos> ownChunks, Set<ChunkPos> sessionClaims, int x, int z) {
|
||||||
return true;
|
ChunkPos probe = new ChunkPos(x, z);
|
||||||
}
|
return ownChunks != null && ownChunks.contains(probe) || sessionClaims.contains(probe);
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */
|
/** 成功后的过渡:进入 NEXT 状态,准备处理下一个目标 */
|
||||||
@@ -583,6 +588,9 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
|
|||||||
}
|
}
|
||||||
this.model.addSessionClaim(chunk);
|
this.model.addSessionClaim(chunk);
|
||||||
this.dataService.addSessionClaim(chunk);
|
this.dataService.addSessionClaim(chunk);
|
||||||
|
// 占领成功后立即触发一次地图刷新(请求侧已带缓存击穿),尽快让地图数据确认该区块,
|
||||||
|
// 缩短"会话占领等待确认"窗口,避免假占领长时间残留
|
||||||
|
this.dataService.refreshAsync();
|
||||||
++this.completedClaims;
|
++this.completedClaims;
|
||||||
this.target = chunk;
|
this.target = chunk;
|
||||||
this.lastProgress = 100;
|
this.lastProgress = 100;
|
||||||
@@ -599,12 +607,20 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
|
|||||||
if (this.target == null) {
|
if (this.target == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 开垦只能发生在玩家所在区块:校验玩家确实位于目标区块上。
|
||||||
|
// 通用成功文案采用模糊匹配,可能被服务器广播的其他玩家开垦消息误触发;
|
||||||
|
// 不校验会把未占领的目标错误标记为"已占领"并跳过,直到很久之后才被地图数据纠正(v1.2.1 修复)。
|
||||||
|
if (!this.playerChunk.equals(this.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 去重:同一区块只计一次
|
// 去重:同一区块只计一次
|
||||||
if (!this.claimedSet.add(this.target)) {
|
if (!this.claimedSet.add(this.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.model.addSessionClaim(this.target);
|
this.model.addSessionClaim(this.target);
|
||||||
this.dataService.addSessionClaim(this.target);
|
this.dataService.addSessionClaim(this.target);
|
||||||
|
// 占领成功后立即触发一次地图刷新,尽快确认该区块
|
||||||
|
this.dataService.refreshAsync();
|
||||||
++this.completedClaims;
|
++this.completedClaims;
|
||||||
this.lastProgress = 100;
|
this.lastProgress = 100;
|
||||||
this.log("\u5360\u9886\u6210\u529f " + this.target.x() + "," + this.target.z());
|
this.log("\u5360\u9886\u6210\u529f " + this.target.x() + "," + this.target.z());
|
||||||
@@ -622,11 +638,6 @@ implements SubtitleChatMonitor.Listener, BaritoneBridge.PathListener {
|
|||||||
return this.dataService.model().allClaimedChunks().contains(chunk);
|
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动游泳逻辑(每帧调用):
|
* 自动游泳逻辑(每帧调用):
|
||||||
* 仅在玩家完全没入水中(有溺水风险)时自动跳跃,保持浮在水面。
|
* 仅在玩家完全没入水中(有溺水风险)时自动跳跃,保持浮在水面。
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import autostore.client.territory.ChunkPos;
|
|||||||
import autostore.client.territory.TerritoryModel;
|
import autostore.client.territory.TerritoryModel;
|
||||||
import autostore.client.territory.TerritoryState;
|
import autostore.client.territory.TerritoryState;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import net.fabricmc.api.EnvType;
|
import net.fabricmc.api.EnvType;
|
||||||
import net.fabricmc.api.Environment;
|
import net.fabricmc.api.Environment;
|
||||||
@@ -347,14 +348,27 @@ extends Screen {
|
|||||||
context.drawTextWithShadow(this.textRenderer, "\u9886\u571f\u5730\u56fe", this.mapX + 2, this.mapY - 12, -1513240);
|
context.drawTextWithShadow(this.textRenderer, "\u9886\u571f\u5730\u56fe", this.mapX + 2, this.mapY - 12, -1513240);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绘制各邦国领土区块(己方高亮、其他半透明),并绘制会话新占领区块 */
|
/** 绘制各邦国领土区块(己方高亮、其他半透明),并绘制会话新占领区块与飞地(疏漏区块) */
|
||||||
private void drawTerritories(DrawContext context) {
|
private void drawTerritories(DrawContext context) {
|
||||||
TerritoryModel model = this.mod.dataService().model();
|
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()) {
|
for (TerritoryState state : model.states()) {
|
||||||
boolean self = state.equals(model.selfState());
|
boolean self = state.equals(model.selfState());
|
||||||
int color = state.color();
|
int color = state.color();
|
||||||
int fill = this.withAlpha(color, self ? 155 : 92);
|
int fill = this.withAlpha(color, self ? 155 : 92);
|
||||||
for (ChunkPos chunk : state.chunks()) {
|
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);
|
this.drawChunkFill(context, chunk, fill);
|
||||||
}
|
}
|
||||||
if (!this.isVisible(state.capital())) continue;
|
if (!this.isVisible(state.capital())) continue;
|
||||||
@@ -363,6 +377,11 @@ extends Screen {
|
|||||||
for (ChunkPos chunk : model.sessionClaims()) {
|
for (ChunkPos chunk : model.sessionClaims()) {
|
||||||
this.drawChunkFill(context, chunk, -1938440928);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 绘制玩家位置(白色方块)与当前目标区块(橙色描边) */
|
/** 绘制玩家位置(白色方块)与当前目标区块(橙色描边) */
|
||||||
|
|||||||
@@ -40,17 +40,47 @@ public final class ExpansionPlanner {
|
|||||||
* @return 建议占领的区块列表(按优先级排序)
|
* @return 建议占领的区块列表(按优先级排序)
|
||||||
*/
|
*/
|
||||||
public List<ChunkPos> generate(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) {
|
public List<ChunkPos> generate(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) {
|
||||||
|
List<ChunkPos> result;
|
||||||
if (config.strategy == 1) {
|
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.prefixFlyingChunks(model, config, playerChunk, result);
|
||||||
return this.generatePerimeter(model, config, playerChunk);
|
|
||||||
}
|
}
|
||||||
// 策略 2 区域扩地:玩家圈选了矩形时,完全按矩形区域扩地
|
|
||||||
if (config.strategy == 2 && config.regionMaxX >= config.regionMinX) {
|
/**
|
||||||
return this.generateRectangle(model, config, playerChunk);
|
* 将己方领土内的"飞地(疏漏区块)"提到队列最前,优先填补。
|
||||||
|
*
|
||||||
|
* <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;
|
||||||
}
|
}
|
||||||
return this.generateLine(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);
|
||||||
|
}
|
||||||
|
result.sort(byDistance);
|
||||||
|
// 其余规划区块依次补充,去重且不超过 limit
|
||||||
|
for (ChunkPos chunk : planned) {
|
||||||
|
if (result.size() >= limit) break;
|
||||||
|
if (flying.contains(chunk)) continue;
|
||||||
|
result.add(chunk);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,13 +220,18 @@ public final class ExpansionPlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 策略 2 矩形区域扩地:在玩家圈选的矩形内,从与现有领土接壤的区块开始,
|
* 策略 2 矩形区域扩地:严格 S 形(蛇形)扫描,由内而外推进。
|
||||||
* 横向或纵向 S 形扫过整个矩形。
|
|
||||||
*
|
*
|
||||||
* <p>与旧版"沿邦国边界逐层向内 BFS"不同,本算法不再贴着邦国现有边界,
|
* <p>扫描规则(v1.2.1 重写):
|
||||||
* 而是生成一条贯穿矩形的 S 形路径:从接壤起点开始,逐行/逐列交替反向扫过,
|
* <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) {
|
private List<ChunkPos> generateRectangle(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) {
|
||||||
Set<ChunkPos> actualOwn = this.ownChunks(model);
|
Set<ChunkPos> actualOwn = this.ownChunks(model);
|
||||||
@@ -209,139 +244,84 @@ public final class ExpansionPlanner {
|
|||||||
int minZ = config.regionMinZ;
|
int minZ = config.regionMinZ;
|
||||||
int maxZ = config.regionMaxZ;
|
int maxZ = config.regionMaxZ;
|
||||||
int limit = config.targetCount > 0 ? config.targetCount : Integer.MAX_VALUE;
|
int limit = config.targetCount > 0 ? config.targetCount : Integer.MAX_VALUE;
|
||||||
// 找出矩形内所有与己方领土接壤的未占领区块(作为 S 形起点候选)
|
ChunkPos capital = this.capital(model, playerChunk);
|
||||||
ArrayList<ChunkPos> borderChunks = new ArrayList<ChunkPos>();
|
// ===== 1. 四条边到王城的距离(点到线段距离的平方),选最近者为起始边 =====
|
||||||
for (int x = minX; x <= maxX; ++x) {
|
// 王城坐标投影到边所在线段的横向偏移(王城在区域横向范围外时才有偏移)
|
||||||
for (int z = minZ; z <= maxZ; ++z) {
|
int dxEdge = Math.max(0, Math.max(minX - capital.x(), capital.x() - maxX));
|
||||||
ChunkPos chunk = new ChunkPos(x, z);
|
int dzEdge = Math.max(0, Math.max(minZ - capital.z(), capital.z() - maxZ));
|
||||||
if (allClaimed.contains(chunk)) continue;
|
long distNorth = (long)dxEdge * dxEdge + (long)(minZ - capital.z()) * (minZ - capital.z());
|
||||||
if (this.ownNeighbors(chunk, actualOwn) > 0) {
|
long distSouth = (long)dxEdge * dxEdge + (long)(maxZ - capital.z()) * (maxZ - capital.z());
|
||||||
borderChunks.add(chunk);
|
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);
|
||||||
if (borderChunks.isEmpty()) {
|
// ===== 2. 生成 S 形扫描序列并按接壤过滤 =====
|
||||||
return List.of();
|
// 接壤判断集合:己方领土 + 本次已选区块。
|
||||||
}
|
// 注意:绝不能把其他邦国领土(allClaimed)混入接壤判断集合,
|
||||||
// 选择起点:优先矩形角落附近与领土接壤的区块,使 S 形能自然覆盖整个矩形
|
// 否则会选中"挨着其他邦国、但与己方领土不相邻"的区块,服务器会拒绝开拓(历史 bug)。
|
||||||
ChunkPos start = this.chooseStart(borderChunks, minX, maxX, minZ, maxZ);
|
|
||||||
// 生成 S 形扫描路径(覆盖矩形内所有区块,含已占领的)
|
|
||||||
List<ChunkPos> sPath = this.buildSPath(start, minX, maxX, minZ, maxZ);
|
|
||||||
// 己方领土集合(含本次规划已纳入的区块)。
|
|
||||||
// 注意:这里绝不能把其他邦国的领土(allClaimed)混入接壤判断集合,
|
|
||||||
// 否则会选中一个“挨着其他邦国、但与己方领土不相邻”的区块,
|
|
||||||
// 服务器会因不接壤而拒绝开拓,导致玩家反复走到无效目标(历史 bug)。
|
|
||||||
HashSet<ChunkPos> claimed = new HashSet<ChunkPos>(actualOwn);
|
HashSet<ChunkPos> claimed = new HashSet<ChunkPos>(actualOwn);
|
||||||
ArrayList<ChunkPos> result = new ArrayList<ChunkPos>();
|
ArrayList<ChunkPos> result = new ArrayList<ChunkPos>();
|
||||||
for (ChunkPos chunk : sPath) {
|
if (rowScan) {
|
||||||
if (result.size() >= limit) break;
|
// 行序:从离王城近的边(z=minZ 或 maxZ)向对边推进;首行行内起点选离王城 x 近的一端
|
||||||
// 已被任何邦国(含己方)占领,或本次规划已纳入:跳过,避免重复/跨邦国开拓
|
int startRow = distNorth <= distSouth ? minZ : maxZ;
|
||||||
if (allClaimed.contains(chunk) || claimed.contains(chunk)) continue;
|
int rowStep = startRow == minZ ? 1 : -1;
|
||||||
// 必须与“己方”领土接壤(含本次已纳入的区块),保证服务器能成功开拓
|
boolean ltr = Math.abs(capital.x() - minX) <= Math.abs(capital.x() - maxX);
|
||||||
if (this.ownNeighbors(chunk, claimed) == 0) continue;
|
for (int row = startRow; row >= minZ && row <= maxZ; row += rowStep) {
|
||||||
result.add(chunk);
|
if (ltr) {
|
||||||
claimed.add(chunk);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从接壤候选区块中选择 S 形扫描的起点:
|
* S 形扫描中的一个格子:若该区块未被任何邦国占领、且与"己方领土(含本次已选)"接壤,
|
||||||
* 优先选择矩形角落上的接壤区块;没有角落接壤时,选择离任一角落最近的接壤区块。
|
* 则纳入本次扩张计划;否则跳过(保持 S 形顺序一路扫过去,跳过的不打乱后续顺序)。
|
||||||
*/
|
*/
|
||||||
private ChunkPos chooseStart(List<ChunkPos> borderChunks, int minX, int maxX, int minZ, int maxZ) {
|
private void tryClaimRect(List<ChunkPos> result, Set<ChunkPos> claimed, Set<ChunkPos> allClaimed, int x, int z, int limit) {
|
||||||
ChunkPos[] corners = new ChunkPos[]{new ChunkPos(minX, minZ), new ChunkPos(maxX, minZ), new ChunkPos(minX, maxZ), new ChunkPos(maxX, maxZ)};
|
if (result.size() >= limit) {
|
||||||
for (ChunkPos corner : corners) {
|
return;
|
||||||
if (borderChunks.contains(corner)) {
|
|
||||||
return corner;
|
|
||||||
}
|
}
|
||||||
|
ChunkPos chunk = new ChunkPos(x, z);
|
||||||
|
if (allClaimed.contains(chunk) || claimed.contains(chunk)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
ChunkPos best = null;
|
if (this.ownNeighbors(chunk, claimed) == 0) {
|
||||||
int bestDist = Integer.MAX_VALUE;
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成从起点开始的 S 形扫描路径,覆盖矩形内所有区块。
|
|
||||||
*
|
|
||||||
* <p>扫描主轴根据起点所在边缘决定:起点在顶/底行时横向逐行扫描,
|
|
||||||
* 起点在左/右列时纵向逐列扫描。每行/列内方向交替反向,形成 S 形。
|
|
||||||
* 起点所在行/列从起点向两侧铺开,保证起点是路径第一个区块。
|
|
||||||
*/
|
|
||||||
private List<ChunkPos> buildSPath(ChunkPos start, int minX, int maxX, int minZ, int maxZ) {
|
|
||||||
ArrayList<ChunkPos> path = new ArrayList<ChunkPos>();
|
|
||||||
boolean horizontal = start.z() == minZ || start.z() == maxZ;
|
|
||||||
if (horizontal) {
|
|
||||||
int startRow = start.z();
|
|
||||||
// 起点行:从起点向一侧到边缘,再反向到另一侧,完整覆盖该行
|
|
||||||
boolean leftToRight = start.x() == minX;
|
|
||||||
if (leftToRight) {
|
|
||||||
for (int x = start.x(); x <= maxX; ++x) path.add(new ChunkPos(x, startRow));
|
|
||||||
for (int x = start.x() - 1; x >= minX; --x) path.add(new ChunkPos(x, startRow));
|
|
||||||
} else {
|
|
||||||
for (int x = start.x(); x >= minX; --x) path.add(new ChunkPos(x, startRow));
|
|
||||||
for (int x = start.x() + 1; x <= maxX; ++x) path.add(new ChunkPos(x, startRow));
|
|
||||||
}
|
|
||||||
// 其余行:先向下(+z)再向上(-z),每行方向交替,形成 S 形
|
|
||||||
boolean ltr = leftToRight;
|
|
||||||
for (int z = startRow + 1; z <= maxZ; ++z) {
|
|
||||||
this.appendRow(path, z, minX, maxX, ltr);
|
|
||||||
ltr = !ltr;
|
|
||||||
}
|
|
||||||
for (int z = startRow - 1; z >= minZ; --z) {
|
|
||||||
this.appendRow(path, z, minX, maxX, ltr);
|
|
||||||
ltr = !ltr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
int startCol = start.x();
|
|
||||||
// 起点列:从起点向一侧到边缘,再反向到另一侧,完整覆盖该列
|
|
||||||
boolean topToBottom = start.z() == minZ;
|
|
||||||
if (topToBottom) {
|
|
||||||
for (int z = start.z(); z <= maxZ; ++z) path.add(new ChunkPos(startCol, z));
|
|
||||||
for (int z = start.z() - 1; z >= minZ; --z) path.add(new ChunkPos(startCol, z));
|
|
||||||
} else {
|
|
||||||
for (int z = start.z(); z >= minZ; --z) path.add(new ChunkPos(startCol, z));
|
|
||||||
for (int z = start.z() + 1; z <= maxZ; ++z) path.add(new ChunkPos(startCol, z));
|
|
||||||
}
|
|
||||||
// 其余列:先向右(+x)再向左(-x),每列方向交替,形成 S 形
|
|
||||||
boolean ttb = topToBottom;
|
|
||||||
for (int x = startCol + 1; x <= maxX; ++x) {
|
|
||||||
this.appendCol(path, x, minZ, maxZ, ttb);
|
|
||||||
ttb = !ttb;
|
|
||||||
}
|
|
||||||
for (int x = startCol - 1; x >= minX; --x) {
|
|
||||||
this.appendCol(path, x, minZ, maxZ, ttb);
|
|
||||||
ttb = !ttb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 追加一行区块到路径(leftToRight=true 时从左到右,否则从右到左) */
|
|
||||||
private void appendRow(List<ChunkPos> path, int row, int minX, int maxX, boolean leftToRight) {
|
|
||||||
if (leftToRight) {
|
|
||||||
for (int x = minX; x <= maxX; ++x) path.add(new ChunkPos(x, row));
|
|
||||||
} else {
|
|
||||||
for (int x = maxX; x >= minX; --x) path.add(new ChunkPos(x, row));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 追加一列区块到路径(topToBottom=true 时从上到下,否则从下到上) */
|
|
||||||
private void appendCol(List<ChunkPos> path, int col, int minZ, int maxZ, boolean topToBottom) {
|
|
||||||
if (topToBottom) {
|
|
||||||
for (int z = minZ; z <= maxZ; ++z) path.add(new ChunkPos(col, z));
|
|
||||||
} else {
|
|
||||||
for (int z = maxZ; z >= minZ; --z) path.add(new ChunkPos(col, z));
|
|
||||||
}
|
}
|
||||||
|
result.add(chunk);
|
||||||
|
claimed.add(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 在候选集中找到离当前游标最近的一个区块(曼哈顿距离),作为推进的前沿 */
|
/** 在候选集中找到离当前游标最近的一个区块(曼哈顿距离),作为推进的前沿 */
|
||||||
|
|||||||
@@ -74,8 +74,20 @@ public final class TerritoryDataService {
|
|||||||
* <p>占领成功后,服务器动态地图(Dynmap/markers.json)可能尚未同步该区块,
|
* <p>占领成功后,服务器动态地图(Dynmap/markers.json)可能尚未同步该区块,
|
||||||
* 因此在宽限期内即使地图未确认也先保留"会话占领"标记,避免刚占领的区块被误判为假占领。
|
* 因此在宽限期内即使地图未确认也先保留"会话占领"标记,避免刚占领的区块被误判为假占领。
|
||||||
* 超过宽限期仍未在地图中确认的,视为未真正占领并丢弃,使空洞区块能重新进入规划。
|
* 超过宽限期仍未在地图中确认的,视为未真正占领并丢弃,使空洞区块能重新进入规划。
|
||||||
|
* (v1.2.1 由 120 秒缩短为 60 秒:配合请求侧缓存击穿,Dynmap 秒级同步,缩短可更快修正假占领)</p>
|
||||||
*/
|
*/
|
||||||
private static final long CLAIM_CONFIRM_GRACE_MS = 120_000L;
|
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() {
|
public TerritoryDataService() {
|
||||||
this.executor = TerritoryDataService.newExecutor();
|
this.executor = TerritoryDataService.newExecutor();
|
||||||
@@ -129,12 +141,28 @@ public final class TerritoryDataService {
|
|||||||
this.executor.execute(() -> {
|
this.executor.execute(() -> {
|
||||||
this.lastAttemptMillis = System.currentTimeMillis();
|
this.lastAttemptMillis = System.currentTimeMillis();
|
||||||
try {
|
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));
|
HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||||
this.lastError = "HTTP " + response.statusCode();
|
this.lastError = "HTTP " + response.statusCode();
|
||||||
return;
|
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 parsed = TerritoryDataService.parse(response.body());
|
||||||
TerritoryModel previous = this.model;
|
TerritoryModel previous = this.model;
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
@@ -160,6 +188,9 @@ public final class TerritoryDataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parsed.setSelfStateName(this.selectedStateName);
|
parsed.setSelfStateName(this.selectedStateName);
|
||||||
|
// 后台线程预计算飞地缓存:把"各国外接矩形全量扫描"从渲染帧挪到这里,
|
||||||
|
// 修复"刷新地图/打开 GUI 时游戏卡一下"的问题
|
||||||
|
parsed.precomputeFlyingChunks();
|
||||||
this.model = parsed;
|
this.model = parsed;
|
||||||
this.lastSuccessfulFetchMillis = System.currentTimeMillis();
|
this.lastSuccessfulFetchMillis = System.currentTimeMillis();
|
||||||
this.lastError = "";
|
this.lastError = "";
|
||||||
@@ -269,9 +300,9 @@ public final class TerritoryDataService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Set chunks = claims.computeIfAbsent(name, ignored -> new HashSet());
|
Set chunks = claims.computeIfAbsent(name, ignored -> new HashSet());
|
||||||
for (List<int[]> polygon : TerritoryDataService.extractPolygons(marker.get("points"))) {
|
// 一个 marker 的多个多边形遵循 dynmap 的 even-odd 填充规则:
|
||||||
TerritoryDataService.fillPolygon(chunks, polygon);
|
// 外圈为正,内圈多边形(国中飞地的边界)作为孔洞被减去,而非作为独立领土并集填充。
|
||||||
}
|
TerritoryDataService.fillPolygonEvenOdd(chunks, TerritoryDataService.extractPolygons(marker.get("points")));
|
||||||
colors.putIfAbsent(name, TerritoryDataService.parseColor(TerritoryDataService.stringOr(marker, "color", "#75bd20")));
|
colors.putIfAbsent(name, TerritoryDataService.parseColor(TerritoryDataService.stringOr(marker, "color", "#75bd20")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,33 +368,57 @@ public final class TerritoryDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将多边形覆盖的区块填充进集合:
|
* 将多边形组覆盖的区块按 even-odd 规则填充进集合。
|
||||||
* 先算出多边形外接矩形(换算为区块坐标),再逐区块用"点在多边形内"算法判断,
|
*
|
||||||
* 以区块中心点(cx*16+8, cz*16+8)是否落在多边形内为准。
|
* <p>一个 marker 里的多个多边形遵循 dynmap 的奇偶填充:区块落入<b>奇数个</b>多边形内时才视为领土;
|
||||||
* 面积超过 600 万区块时直接放弃,防止异常数据导致卡死。
|
* 这样内圈多边形(国中飞地/孔洞的边界)会被正确减去,而不是像并集那样把孔洞也填成领土。</p>
|
||||||
|
*
|
||||||
|
* @param output 输出集合(该邦国的区块集合)
|
||||||
|
* @param polygons 该 marker 的全部多边形,第一个为外圈、可为多个内圈孔洞
|
||||||
*/
|
*/
|
||||||
private static void fillPolygon(Set<ChunkPos> output, List<int[]> polygon) {
|
private static void fillPolygonEvenOdd(Set<ChunkPos> output, List<List<int[]>> polygons) {
|
||||||
int area;
|
if (polygons.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
int minX = Integer.MAX_VALUE;
|
int minX = Integer.MAX_VALUE;
|
||||||
int maxX = Integer.MIN_VALUE;
|
int maxX = Integer.MIN_VALUE;
|
||||||
int minZ = Integer.MAX_VALUE;
|
int minZ = Integer.MAX_VALUE;
|
||||||
int maxZ = Integer.MIN_VALUE;
|
int maxZ = Integer.MIN_VALUE;
|
||||||
|
for (List<int[]> polygon : polygons) {
|
||||||
for (int[] point : polygon) {
|
for (int[] point : polygon) {
|
||||||
minX = Math.min(minX, Math.floorDiv(point[0], 16));
|
minX = Math.min(minX, Math.floorDiv(point[0], 16));
|
||||||
maxX = Math.max(maxX, Math.floorDiv(point[0], 16));
|
maxX = Math.max(maxX, Math.floorDiv(point[0], 16));
|
||||||
minZ = Math.min(minZ, Math.floorDiv(point[1], 16));
|
minZ = Math.min(minZ, Math.floorDiv(point[1], 16));
|
||||||
maxZ = Math.max(maxZ, 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;
|
return;
|
||||||
}
|
}
|
||||||
for (int cx = minX; cx <= maxX; ++cx) {
|
for (int cx = minX; cx <= maxX; ++cx) {
|
||||||
for (int cz = minZ; cz <= maxZ; ++cz) {
|
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) 是否在多边形内(经典奇偶规则) */
|
/** 射线法判断点 (x, z) 是否在多边形内(经典奇偶规则) */
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ public final class TerritoryModel {
|
|||||||
private final Set<ChunkPos> sessionClaims;
|
private final Set<ChunkPos> sessionClaims;
|
||||||
/** 当前选中的己方邦国名称 */
|
/** 当前选中的己方邦国名称 */
|
||||||
private String selfStateName = "";
|
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) {
|
public TerritoryModel(Map<String, TerritoryState> states) {
|
||||||
this.states = new HashMap<String, TerritoryState>(states);
|
this.states = new HashMap<String, TerritoryState>(states);
|
||||||
@@ -94,6 +106,95 @@ public final class TerritoryModel {
|
|||||||
return null;
|
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) {
|
public void addSessionClaim(ChunkPos chunk) {
|
||||||
this.sessionClaims.add(chunk);
|
this.sessionClaims.add(chunk);
|
||||||
this.allClaimed.add(chunk);
|
this.allClaimed.add(chunk);
|
||||||
|
this.cachedFlyingChunks = null;
|
||||||
|
this.cachedAllFlyingChunks = null;
|
||||||
TerritoryState self = this.states.get(this.selfStateName);
|
TerritoryState self = this.states.get(this.selfStateName);
|
||||||
if (self != null) {
|
if (self != null) {
|
||||||
HashSet<ChunkPos> updated = new HashSet<ChunkPos>(self.chunks());
|
HashSet<ChunkPos> updated = new HashSet<ChunkPos>(self.chunks());
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"schemaVersion":1,"id":"autostore","version":"1.1.9","name":"AutoStore","description":"","authors":[],"contact":{},"license":"All-Rights-Reserved","environment":"client","entrypoints":{"fabric-datagen":["autostore.client.AutoStoreModDataGenerator"],"client":["autostore.client.AutoStoreModClient"],"main":["autostore.AutoStoreMod"]},"mixins":["autostore.mixins.json",{"config":"autostore.client.mixins.json","environment":"client"}],"depends":{"fabricloader":">=0.16.14","fabric-api":"*","minecraft":"1.21.4"},"jars":[{"file":"META-INF/jars/baritone-api-fabric-1.13.1.jar"}]}
|
{"schemaVersion":1,"id":"autostore","version":"1.2.1","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"}]}
|
||||||
在新工单中引用
屏蔽一个用户