From 2b8211b3d0c19125f54e737c2d90adf1cf27da3b Mon Sep 17 00:00:00 2001 From: lyizai <3945107889@qq.com> Date: Wed, 19 Aug 2026 12:36:15 +0800 Subject: [PATCH] =?UTF-8?q?v1.2.0:=20=E5=86=85=E7=BD=AE=E5=9C=B0=E5=9B=BE?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E5=B9=B6=E6=A0=87=E8=AE=B0=E9=A3=9E=E5=9C=B0?= =?UTF-8?q?=EF=BC=88=E7=96=8F=E6=BC=8F=E5=8C=BA=E5=9D=97=EF=BC=89=EF=BC=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=89=A9=E5=9C=B0=E6=97=B6=E4=BC=98=E5=85=88?= =?UTF-8?q?=E5=A1=AB=E8=A1=A5=E8=BF=99=E4=BA=9B=E7=BC=BA=E5=8F=A3=EF=BC=9B?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20analyze=5Fholes.py=20=E5=88=86=E6=9E=90?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- analyze_holes.py | 126 ++++++++++++++++++ gradle.properties | 2 +- .../autostore/client/gui/AutoStoreGui.java | 8 +- .../client/planner/ExpansionPlanner.java | 44 +++++- .../client/territory/TerritoryModel.java | 83 ++++++++++++ src/main/resources/fabric.mod.json | 2 +- 6 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 analyze_holes.py diff --git a/analyze_holes.py b/analyze_holes.py new file mode 100644 index 0000000..deb5eb8 --- /dev/null +++ b/analyze_holes.py @@ -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} ===") \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 1ea826c..9692199 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ yarn_mappings=1.21.4+build.8 loader_version=0.19.3 # Mod Properties -mod_version=1.1.9 +mod_version=1.2.0 maven_group=autostore archives_base_name=autostore diff --git a/src/main/java/autostore/client/gui/AutoStoreGui.java b/src/main/java/autostore/client/gui/AutoStoreGui.java index f1ae133..8010267 100644 --- a/src/main/java/autostore/client/gui/AutoStoreGui.java +++ b/src/main/java/autostore/client/gui/AutoStoreGui.java @@ -14,6 +14,7 @@ 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; @@ -347,7 +348,7 @@ 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(); for (TerritoryState state : model.states()) { @@ -363,6 +364,11 @@ extends Screen { for (ChunkPos chunk : model.sessionClaims()) { this.drawChunkFill(context, chunk, -1938440928); } + // 标记飞地(疏漏区块):己方用醒目的红色描边提示填补,其他邦国用暗灰色标示 + Set selfFlying = model.selfFlyingChunks(); + for (ChunkPos chunk : model.flyingChunks()) { + this.drawChunkOutline(context, chunk, selfFlying.contains(chunk) ? -65536 : this.withAlpha(-8333060, 160)); + } } /** 绘制玩家位置(白色方块)与当前目标区块(橙色描边) */ diff --git a/src/main/java/autostore/client/planner/ExpansionPlanner.java b/src/main/java/autostore/client/planner/ExpansionPlanner.java index 6bba45a..5e203e2 100644 --- a/src/main/java/autostore/client/planner/ExpansionPlanner.java +++ b/src/main/java/autostore/client/planner/ExpansionPlanner.java @@ -40,17 +40,47 @@ public final class ExpansionPlanner { * @return 建议占领的区块列表(按优先级排序) */ public List generate(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk) { + List 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); + } + + /** + * 将己方领土内的"飞地(疏漏区块)"提到队列最前,优先填补。 + * + *

飞地指未被占领、但四周(四个正方向)都被己方领土包围的区块, + * 是自动扩地过程中最容易漏掉的疏漏。先把它们填上可保证领土闭合、不留缺口。

+ */ + private List prefixFlyingChunks(TerritoryModel model, ExpansionConfig config, ChunkPos playerChunk, List planned) { + Set 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 byDistance = Comparator.comparingInt(playerChunk::manhattanDistance); + ArrayList result = new ArrayList(); + // 飞地优先,且必须仍在配置区域内 + 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; } /** diff --git a/src/main/java/autostore/client/territory/TerritoryModel.java b/src/main/java/autostore/client/territory/TerritoryModel.java index 081ca77..d725892 100644 --- a/src/main/java/autostore/client/territory/TerritoryModel.java +++ b/src/main/java/autostore/client/territory/TerritoryModel.java @@ -35,6 +35,14 @@ public final class TerritoryModel { private final Set sessionClaims; /** 当前选中的己方邦国名称 */ private String selfStateName = ""; + /** + * 飞地(疏漏区块)缓存:邦国名 → 该国飞地区块集合。 + * + *

飞地指"四周(四个正方向)都被同一邦国占领、但自身未被占领"的区块, + * 通常是自动扩地时漏掉的疏漏。该结果在模型构建后懒计算并缓存, + * 仅在新增会话占领(addSessionClaim)后失效重算。 + */ + private transient Map> cachedFlyingChunks; public TerritoryModel(Map states) { this.states = new HashMap(states); @@ -94,6 +102,80 @@ public final class TerritoryModel { return null; } + /** + * 返回所有邦国的飞地(疏漏区块)并集(只读视图)。 + * + *

飞地指“四周(四个正方向)都被同一邦国占领、但自身未被占领”的区块。 + * 这些往往是自动扩地过程中漏掉的疏漏,需要在地图上标出并优先填补。

+ */ + public Set flyingChunks() { + HashSet result = new HashSet(); + for (Set chunks : this.flyingChunksByState().values()) { + result.addAll(chunks); + } + return result; + } + + /** 返回己方邦国的飞地(疏漏区块)(只读视图);未选择邦国时为空集 */ + public Set selfFlyingChunks() { + return Collections.unmodifiableSet(this.flyingChunksByState().getOrDefault(this.selfStateName, Set.of())); + } + + /** 计算并缓存各邦国的飞地区块(懒加载,addSessionClaim 后失效重算) */ + private Map> flyingChunksByState() { + Map> cached = this.cachedFlyingChunks; + if (cached != null) { + return cached; + } + HashMap> map = new HashMap>(); + for (TerritoryState state : this.states.values()) { + map.put(state.name(), this.computeFlyingChunks(state.chunks())); + } + this.cachedFlyingChunks = map; + return map; + } + + /** + * 计算某邦国的全部飞地区块: + * 在该邦国的外接矩形内,找出“未被任何邦国占领、但四个正方向均属于该邦国”的区块。 + */ + private Set computeFlyingChunks(Set own) { + HashSet result = new HashSet(); + 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 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 updated = new HashSet(self.chunks()); diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 133c0c5..669172c 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -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"}]} \ No newline at end of file +{"schemaVersion":1,"id":"autostore","version":"1.2.0","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"}]} \ No newline at end of file