3 次代码提交
共修改 9 个文件,包含 266 行新增和 12 行删除
+11 -2
查看文件
@@ -15,12 +15,13 @@
- 实时地图 GUI(通过 Ctrl+R 打开) - 实时地图 GUI(通过 Ctrl+R 打开)
- 监听聊天/字幕消息判断开垦结果 - 监听聊天/字幕消息判断开垦结果
- 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞 - 刷新地图时与最新 `markers.json` 核对已占领区块,丢弃地图未确认的超期假占领,避免高地扩地残留空洞
- 飞地(疏漏区块)检测:自动扫描每个邦国内外接矩形内"四周被同邦国占领但自身未占领"的区块,内置地图用红色标出己方飞地,自动扩地时优先填补这些缺口
- 日志导出: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.0.jar` 放入 `.minecraft/mods/` 目录
3. 启动游戏,按 `Ctrl+R` 打开 GUI 3. 启动游戏,按 `Ctrl+R` 打开 GUI
## 构建 ## 构建
@@ -30,4 +31,12 @@
./build_mod.sh ./build_mod.sh
``` ```
输出文件:`autostore-1.1.9.jar` 输出文件:`autostore-1.2.0.jar`
## 赞助支持
如果你觉得这个模组对你有帮助,欢迎赞助支持一下创作者 ❤️
![赞助码](docs/sponsor.png)
你的支持是持续开发的动力!
+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.0
maven_group=autostore maven_group=autostore
archives_base_name=autostore archives_base_name=autostore
@@ -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,7 +348,7 @@ 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();
for (TerritoryState state : model.states()) { for (TerritoryState state : model.states()) {
@@ -363,6 +364,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); }
/**
* 将己方领土内的"飞地(疏漏区块)"提到队列最前,优先填补。
*
* <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 区域扩地:玩家圈选了矩形时,完全按矩形区域扩地 int limit = config.targetCount > 0 ? config.targetCount : Math.max(planned.size(), flying.size());
if (config.strategy == 2 && config.regionMaxX >= config.regionMinX) { Comparator<ChunkPos> byDistance = Comparator.comparingInt(playerChunk::manhattanDistance);
return this.generateRectangle(model, config, playerChunk); 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;
} }
/** /**
@@ -35,6 +35,14 @@ public final class TerritoryModel {
private final Set<ChunkPos> sessionClaims; private final Set<ChunkPos> sessionClaims;
/** 当前选中的己方邦国名称 */ /** 当前选中的己方邦国名称 */
private String selfStateName = ""; private String selfStateName = "";
/**
* 飞地(疏漏区块)缓存:邦国名 → 该国飞地区块集合。
*
* <p>飞地指"四周(四个正方向)都被同一邦国占领、但自身未被占领"的区块,
* 通常是自动扩地时漏掉的疏漏。该结果在模型构建后懒计算并缓存,
* 仅在新增会话占领(addSessionClaim)后失效重算。
*/
private transient Map<String, Set<ChunkPos>> cachedFlyingChunks;
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 +102,80 @@ public final class TerritoryModel {
return null; 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) { public void addSessionClaim(ChunkPos chunk) {
this.sessionClaims.add(chunk); this.sessionClaims.add(chunk);
this.allClaimed.add(chunk); this.allClaimed.add(chunk);
this.cachedFlyingChunks = 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
查看文件
@@ -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.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"}]}