v1.2.0: 内置地图检测并标记飞地(疏漏区块),自动扩地时优先填补这些缺口;新增 analyze_holes.py 分析工具

这个提交包含在:
lyizai
2026-08-19 12:36:15 +08:00
父节点 25d95a5c22
当前提交 2b8211b3d0
共修改 6 个文件,包含 255 行新增和 10 行删除
@@ -35,6 +35,14 @@ public final class TerritoryModel {
private final Set<ChunkPos> sessionClaims;
/** 当前选中的己方邦国名称 */
private String selfStateName = "";
/**
* 飞地(疏漏区块)缓存:邦国名 → 该国飞地区块集合。
*
* <p>飞地指"四周(四个正方向)都被同一邦国占领、但自身未被占领"的区块,
* 通常是自动扩地时漏掉的疏漏。该结果在模型构建后懒计算并缓存,
* 仅在新增会话占领(addSessionClaim)后失效重算。
*/
private transient Map<String, Set<ChunkPos>> cachedFlyingChunks;
public TerritoryModel(Map<String, TerritoryState> states) {
this.states = new HashMap<String, TerritoryState>(states);
@@ -94,6 +102,80 @@ public final class TerritoryModel {
return null;
}
/**
* 返回所有邦国的飞地(疏漏区块)并集(只读视图)。
*
* <p>飞地指“四周(四个正方向)都被同一邦国占领、但自身未被占领”的区块。
* 这些往往是自动扩地过程中漏掉的疏漏,需要在地图上标出并优先填补。</p>
*/
public Set<ChunkPos> flyingChunks() {
HashSet<ChunkPos> result = new HashSet<ChunkPos>();
for (Set<ChunkPos> chunks : this.flyingChunksByState().values()) {
result.addAll(chunks);
}
return result;
}
/** 返回己方邦国的飞地(疏漏区块)(只读视图);未选择邦国时为空集 */
public Set<ChunkPos> selfFlyingChunks() {
return Collections.unmodifiableSet(this.flyingChunksByState().getOrDefault(this.selfStateName, Set.of()));
}
/** 计算并缓存各邦国的飞地区块(懒加载,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 +183,7 @@ public final class TerritoryModel {
public void addSessionClaim(ChunkPos chunk) {
this.sessionClaims.add(chunk);
this.allClaimed.add(chunk);
this.cachedFlyingChunks = null;
TerritoryState self = this.states.get(this.selfStateName);
if (self != null) {
HashSet<ChunkPos> updated = new HashSet<ChunkPos>(self.chunks());