AutoStore 模组完整源码

这个提交包含在:
Lyizai
2026-08-17 04:12:56 +00:00
父节点 2593a219a2
当前提交 8ddec083bd
共修改 21 个文件,包含 3145 行新增和 0 行删除
@@ -0,0 +1,52 @@
/*
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
*/
package autostore.client.territory;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
/**
* 区块坐标(record),以“区块”为单位表示二维坐标。
*
* <p>Minecraft 中 1 个区块为 16×16 方块,本类提供方块坐标与区块坐标的换算,
* 以及曼哈顿距离等常用工具方法。</p>
*
* @param x 区块 X 坐标(= 方块 X / 16)
* @param z 区块 Z 坐标(= 方块 Z / 16)
*/
@Environment(EnvType.CLIENT)
public record ChunkPos(int x, int z) {
/**
* 由方块坐标换算为区块坐标(向下取整,兼容负数坐标)。
*
* @param blockX 方块 X 坐标
* @param blockZ 方块 Z 坐标
* @return 对应的区块坐标
*/
public static ChunkPos fromBlock(int blockX, int blockZ) {
return new ChunkPos(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16));
}
/** @return 本区块中心的方块 X 坐标 */
public int centerBlockX() {
return this.x * 16 + 8;
}
/** @return 本区块中心的方块 Z 坐标 */
public int centerBlockZ() {
return this.z * 16 + 8;
}
/**
* 计算与另一区块的曼哈顿距离(|dx| + |dz|)。
*
* @param other 另一个区块坐标
* @return 曼哈顿距离
*/
public int manhattanDistance(ChunkPos other) {
return Math.abs(this.x - other.x) + Math.abs(this.z - other.z);
}
}
@@ -0,0 +1,399 @@
/*
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
*/
package autostore.client.territory;
import autostore.client.territory.ChunkPos;
import autostore.client.territory.TerritoryModel;
import autostore.client.territory.TerritoryState;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
@Environment(value=EnvType.CLIENT)
/**
* 领土数据服务:定时从远程地图服务拉取邦国领土数据(markers.json),
* 解析为 {@link TerritoryModel} 供规划器与 GUI 使用。
*
* <p>数据来源是 AutoStore 服务器自带的动态地图(Dynmap)接口,包含:
* <ul>
* <li>union_claims:各邦国的领土多边形(区块归属)</li>
* <li>union_capitals:各邦国的王城坐标</li>
* </ul>
* 解析出的领土数据会与本次会话新占领的区块合并,保证扩地过程中地图实时更新。
*/
public final class TerritoryDataService {
/** 动态地图的 markers.json 接口地址(AutoStore 服务器) */
private static final String MARKERS_URL = "http://autostore.simpfun.cn/tiles/minecraft_overworld/markers.json";
/** 从 marker 弹窗文本中提取"疆土归属: xxx"的邦国名 */
private static final Pattern CLAIM_PATTERN = Pattern.compile("\u7586\u571f\u5f52\u5c5e:\\s*([^<\\r\\n]+)");
/** 从 marker 弹窗文本中提取"「xxx」的王城"的王城名 */
private static final Pattern CAPITAL_PATTERN = Pattern.compile("\u300c([^\u300d]+)\u300d\u7684\u738b\u57ce");
/** HTTP 客户端:10 秒连接超时,自动跟随重定向 */
private final HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10L)).followRedirects(HttpClient.Redirect.NORMAL).build();
/** 单线程调度器,用于周期刷新领土数据(守护线程,不阻塞游戏退出) */
private volatile ScheduledExecutorService executor;
/** 当前解析出的领土模型(volatile 保证多线程可见性) */
private volatile TerritoryModel model = TerritoryModel.empty();
/** 当前选中的己方邦国名称 */
private volatile String selectedStateName = "";
/** 最近一次拉取失败的错误信息(空串表示无错误) */
private volatile String lastError = "";
/** 最近一次成功拉取的时间戳(毫秒) */
private volatile long lastSuccessfulFetchMillis;
/** 最近一次尝试拉取的时间戳(毫秒) */
private volatile long lastAttemptMillis;
public TerritoryDataService() {
this.executor = TerritoryDataService.newExecutor();
}
/** 创建单线程调度器(守护线程,不阻塞游戏退出) */
private static ScheduledExecutorService newExecutor() {
return Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "AutoStore-TerritoryData");
thread.setDaemon(true);
return thread;
});
}
/**
* 启动定时刷新:立即刷新一次,之后按固定间隔(至少 10 秒)循环拉取。
*
* @param refreshIntervalSeconds 刷新间隔(秒)
*/
public void start(long refreshIntervalSeconds) {
this.refreshAsync();
this.reschedule(refreshIntervalSeconds);
}
/**
* 按新间隔重新调度定时刷新(立即生效)。
* 会关闭旧的调度线程并创建一个新线程,用于 GUI 中修改刷新间隔后即时应用。
*
* @param refreshIntervalSeconds 新的刷新间隔(秒),最小 10 秒
*/
public void reschedule(long refreshIntervalSeconds) {
ScheduledExecutorService fresh = TerritoryDataService.newExecutor();
ScheduledExecutorService old = this.executor;
this.executor = fresh;
long interval = Math.max(10L, refreshIntervalSeconds);
fresh.scheduleWithFixedDelay(this::refreshAsync, interval, interval, TimeUnit.SECONDS);
old.shutdownNow();
}
/** 停止定时刷新,关闭调度线程 */
public void stop() {
this.executor.shutdownNow();
}
/**
* 异步刷新领土数据(在调度线程中执行)。
* 拉取成功后解析 JSON,并把上一份模型中的"会话新占领区块"合并进新模型,
* 避免刷新覆盖掉本次会话的占领记录。
*/
public void refreshAsync() {
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();
HttpResponse<String> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (response.statusCode() < 200 || response.statusCode() >= 300) {
this.lastError = "HTTP " + response.statusCode();
return;
}
TerritoryModel parsed = TerritoryDataService.parse(response.body());
TerritoryModel previous = this.model;
if (previous != null) {
for (ChunkPos sessionClaim : previous.sessionClaims()) {
parsed.addSessionClaim(sessionClaim);
}
}
parsed.setSelfStateName(this.selectedStateName);
this.model = parsed;
this.lastSuccessfulFetchMillis = System.currentTimeMillis();
this.lastError = "";
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
this.lastError = "\u62c9\u53d6\u88ab\u4e2d\u65ad";
}
catch (Exception e) {
this.lastError = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
}
});
}
public TerritoryModel model() {
return this.model;
}
public void setSelectedStateName(String name) {
this.selectedStateName = name == null ? "" : name;
TerritoryModel current = this.model;
if (current != null) {
current.setSelfStateName(this.selectedStateName);
}
}
public String selectedStateName() {
return this.selectedStateName;
}
public void addSessionClaim(ChunkPos chunk) {
TerritoryModel current = this.model;
if (current != null) {
current.addSessionClaim(chunk);
}
}
public String lastError() {
return this.lastError;
}
public long lastSuccessfulFetchMillis() {
return this.lastSuccessfulFetchMillis;
}
public long lastAttemptMillis() {
return this.lastAttemptMillis;
}
/**
* 解析 markers.json 文本为领土模型。
*
* <p>JSON 结构为数组,每层包含 id 与 markers 列表:
* <ul>
* <li>union_claims 层:每个 marker 是一个领土多边形,弹窗文本含"疆土归属: 邦国名"</li>
* <li>union_capitals 层:每个 marker 是一个王城坐标点,弹窗文本含"「邦国名」的王城"</li>
* </ul>
* 最终按邦国名聚合为 {@link TerritoryState} 列表。
*/
public static TerritoryModel parse(String json) {
JsonElement root = JsonParser.parseString((String)json);
if (!root.isJsonArray()) {
throw new IllegalArgumentException("markers.json must be an array");
}
HashMap<String, Set<ChunkPos>> claims = new HashMap<String, Set<ChunkPos>>();
HashMap<String, Integer> claimColors = new HashMap<String, Integer>();
HashMap<String, ChunkPos> capitals = new HashMap<String, ChunkPos>();
for (JsonElement layerElement : root.getAsJsonArray()) {
JsonObject layer = layerElement.getAsJsonObject();
String id = TerritoryDataService.stringOr(layer, "id", "");
JsonArray markers = layer.getAsJsonArray("markers");
if (markers == null) continue;
if ("union_claims".equals(id)) {
for (JsonElement markerElement : markers) {
TerritoryDataService.parseClaimMarker(markerElement.getAsJsonObject(), claims, claimColors);
}
continue;
}
if (!"union_capitals".equals(id)) continue;
for (JsonElement markerElement : markers) {
TerritoryDataService.parseCapitalMarker(markerElement.getAsJsonObject(), capitals);
}
}
HashMap<String, TerritoryState> states = new HashMap<String, TerritoryState>();
for (Map.Entry entry : claims.entrySet()) {
String name = (String)entry.getKey();
ChunkPos capital = capitals.getOrDefault(name, TerritoryDataService.firstChunk((Set)entry.getValue()));
int color = claimColors.getOrDefault(name, -9061088);
states.put(name, new TerritoryState(name, capital, Set.copyOf((Collection)entry.getValue()), color));
}
return new TerritoryModel(states);
}
/**
* 解析一个领土多边形 marker:从弹窗文本提取邦国名,
* 将多边形覆盖的区块加入该邦国的区块集合,并记录颜色。
*/
private static void parseClaimMarker(JsonObject marker, Map<String, Set<ChunkPos>> claims, Map<String, Integer> colors) {
String popup = TerritoryDataService.stringOr(marker, "popup", "");
Matcher matcher = CLAIM_PATTERN.matcher(popup);
if (!matcher.find()) {
return;
}
String name = matcher.group(1).trim();
if (name.isEmpty()) {
return;
}
Set chunks = claims.computeIfAbsent(name, ignored -> new HashSet());
for (List<int[]> polygon : TerritoryDataService.extractPolygons(marker.get("points"))) {
TerritoryDataService.fillPolygon(chunks, polygon);
}
colors.putIfAbsent(name, TerritoryDataService.parseColor(TerritoryDataService.stringOr(marker, "color", "#75bd20")));
}
/** 解析一个王城 marker:从弹窗文本提取邦国名,记录王城的方块坐标(换算为区块坐标) */
private static void parseCapitalMarker(JsonObject marker, Map<String, ChunkPos> capitals) {
String popup = TerritoryDataService.stringOr(marker, "popup", "");
Matcher matcher = CAPITAL_PATTERN.matcher(popup);
if (!matcher.find()) {
return;
}
JsonObject point = marker.getAsJsonObject("point");
if (point == null) {
return;
}
int x = TerritoryDataService.intOr(point, "x", 0);
int z = TerritoryDataService.intOr(point, "z", 0);
capitals.put(matcher.group(1).trim(), ChunkPos.fromBlock(x, z));
}
/**
* 递归提取多边形列表:points 可能是单个多边形(点数组),
* 也可能是多边形数组(多层嵌套),统一展平为 List<List<int[]>>。
*/
private static List<List<int[]>> extractPolygons(JsonElement points) {
ArrayList<List<int[]>> result = new ArrayList<List<int[]>>();
if (points == null || !points.isJsonArray()) {
return result;
}
JsonArray array = points.getAsJsonArray();
if (array.size() == 0) {
return result;
}
if (TerritoryDataService.isPointObject(array.get(0))) {
List<int[]> polygon = TerritoryDataService.parsePointList((JsonElement)array);
if (polygon.size() >= 3) {
result.add(polygon);
}
} else {
for (JsonElement element : array) {
result.addAll(TerritoryDataService.extractPolygons(element));
}
}
return result;
}
/** 判断 JSON 元素是否是一个"点对象"(同时含 x 与 z 字段) */
private static boolean isPointObject(JsonElement element) {
return element != null && element.isJsonObject() && element.getAsJsonObject().has("x") && element.getAsJsonObject().has("z");
}
/** 解析一个点数组为 int[] 列表(每个点取 x、z 两个坐标) */
private static List<int[]> parsePointList(JsonElement element) {
ArrayList<int[]> points = new ArrayList<int[]>();
if (!element.isJsonArray()) {
return points;
}
for (JsonElement pointElement : element.getAsJsonArray()) {
JsonObject point = pointElement.getAsJsonObject();
if (point == null) continue;
points.add(new int[]{TerritoryDataService.intOr(point, "x", 0), TerritoryDataService.intOr(point, "z", 0)});
}
return points;
}
/**
* 将多边形覆盖的区块填充进集合:
* 先算出多边形外接矩形(换算为区块坐标),再逐区块用"点在多边形内"算法判断,
* 以区块中心点(cx*16+8, cz*16+8)是否落在多边形内为准。
* 面积超过 600 万区块时直接放弃,防止异常数据导致卡死。
*/
private static void fillPolygon(Set<ChunkPos> output, List<int[]> polygon) {
int area;
int minX = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int minZ = Integer.MAX_VALUE;
int maxZ = Integer.MIN_VALUE;
for (int[] point : polygon) {
minX = Math.min(minX, Math.floorDiv(point[0], 16));
maxX = Math.max(maxX, Math.floorDiv(point[0], 16));
minZ = Math.min(minZ, 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) {
return;
}
for (int cx = minX; cx <= maxX; ++cx) {
for (int cz = minZ; cz <= maxZ; ++cz) {
if (!TerritoryDataService.pointInPolygon(cx * 16 + 8, cz * 16 + 8, polygon)) continue;
output.add(new ChunkPos(cx, cz));
}
}
}
/** 射线法判断点 (x, z) 是否在多边形内(经典奇偶规则) */
private static boolean pointInPolygon(int x, int z, List<int[]> polygon) {
boolean inside = false;
int i = 0;
int j = polygon.size() - 1;
while (i < polygon.size()) {
boolean intersects;
int xi = polygon.get(i)[0];
int zi = polygon.get(i)[1];
int xj = polygon.get(j)[0];
int zj = polygon.get(j)[1];
boolean bl = intersects = zi > z != zj > z && x < (xj - xi) * (z - zi) / (zj - zi) + xi;
if (intersects) {
inside = !inside;
}
j = i++;
}
return inside;
}
/** 取集合中的第一个区块作为默认王城(当该邦国没有标记王城时) */
private static ChunkPos firstChunk(Set<ChunkPos> chunks) {
return chunks.stream().findFirst().orElse(new ChunkPos(0, 0));
}
/** 安全读取 JSON 字符串字段,缺失或为 null 时返回默认值 */
private static String stringOr(JsonObject object, String key, String fallback) {
JsonElement element = object.get(key);
return element == null || element.isJsonNull() ? fallback : element.getAsString();
}
/** 安全读取 JSON 整数字段,缺失或为 null 时返回默认值 */
private static int intOr(JsonObject object, String key, int fallback) {
JsonElement element = object.get(key);
return element == null || element.isJsonNull() ? fallback : element.getAsInt();
}
/**
* 解析十六进制颜色字符串为 ARGB 整数:
* 支持 #RRGGBB(自动补全不透明 alpha)与 #AARRGGBB 两种格式;
* 解析失败时返回默认的邦国绿(#75bd20)。
*/
private static int parseColor(String hex) {
try {
String value;
String string = value = hex.startsWith("#") ? hex.substring(1) : hex;
if (value.length() == 8) {
return (int)Long.parseLong(value, 16);
}
if (value.length() == 6) {
return 0xFF000000 | Integer.parseInt(value, 16);
}
}
catch (NumberFormatException numberFormatException) {
// 颜色格式非法时忽略,使用默认颜色
}
return -9061088;
}
}
@@ -0,0 +1,112 @@
/*
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
*/
package autostore.client.territory;
import autostore.client.territory.ChunkPos;
import autostore.client.territory.TerritoryState;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
@Environment(value=EnvType.CLIENT)
/**
* 领土模型:聚合所有邦国的领土状态,并提供全局查询能力。
*
* <p>维护三份数据:
* <ul>
* <li>states:邦国名 → 邦国领土状态(含区块集合、王城、颜色)</li>
* <li>allClaimed:所有邦国已占领区块的并集(用于判断区块是否已被占用)</li>
* <li>sessionClaims:本次会话新占领的区块(刷新地图时不会被覆盖)</li>
* </ul>
*/
public final class TerritoryModel {
/** 邦国名 → 邦国领土状态 */
private final Map<String, TerritoryState> states;
/** 所有邦国已占领区块的并集 */
private final Set<ChunkPos> allClaimed;
/** 本次会话新占领的区块 */
private final Set<ChunkPos> sessionClaims;
/** 当前选中的己方邦国名称 */
private String selfStateName = "";
public TerritoryModel(Map<String, TerritoryState> states) {
this.states = new HashMap<String, TerritoryState>(states);
this.allClaimed = new HashSet<ChunkPos>();
for (TerritoryState state : states.values()) {
this.allClaimed.addAll(state.chunks());
}
this.sessionClaims = new HashSet<ChunkPos>();
}
/** 返回一个空的领土模型(无任何邦国数据) */
public static TerritoryModel empty() {
return new TerritoryModel(Map.of());
}
/** 返回所有邦国领土状态(只读视图) */
public Collection<TerritoryState> states() {
return Collections.unmodifiableCollection(this.states.values());
}
/** 按名称查询邦国领土状态,不存在返回 null */
public TerritoryState state(String name) {
return this.states.get(name);
}
/** 返回所有邦国已占领区块的并集(只读视图) */
public Set<ChunkPos> allClaimedChunks() {
return Collections.unmodifiableSet(this.allClaimed);
}
/** 返回本次会话新占领的区块(只读视图) */
public Set<ChunkPos> sessionClaims() {
return Collections.unmodifiableSet(this.sessionClaims);
}
/** 返回当前选中的己方邦国名称 */
public String selfStateName() {
return this.selfStateName;
}
/** 设置当前选中的己方邦国名称(null 视为空串) */
public void setSelfStateName(String name) {
this.selfStateName = name == null ? "" : name;
}
/** 返回己方邦国的领土状态;未设置或不存在时返回 null */
public TerritoryState selfState() {
return this.states.get(this.selfStateName);
}
/** 查找包含指定区块的邦国;没有邦国覆盖该区块时返回 null */
public TerritoryState findContainingState(ChunkPos chunk) {
for (TerritoryState state : this.states.values()) {
if (!state.chunks().contains(chunk)) continue;
return state;
}
return null;
}
/**
* 记录一次会话内新占领的区块:
* 加入会话集合与全局已占领集合,并同步更新己方邦国的区块集合。
*/
public void addSessionClaim(ChunkPos chunk) {
this.sessionClaims.add(chunk);
this.allClaimed.add(chunk);
TerritoryState self = this.states.get(this.selfStateName);
if (self != null) {
HashSet<ChunkPos> updated = new HashSet<ChunkPos>(self.chunks());
updated.add(chunk);
this.states.put(this.selfStateName, new TerritoryState(self.name(), self.capital(), updated, self.color()));
}
}
}
@@ -0,0 +1,21 @@
/*
* 本文件由 autostore-1.0-SNAPSHOT.jar 反编译并整理而来(CFR 0.152),
* 混淆名已替换为 Minecraft 1.21.4 Yarn 可读映射,并补充中文注释。
*/
package autostore.client.territory;
import java.util.Set;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
/**
* 一个国家(邦国)的领土状态(record)。
*
* @param name 国家名称
* @param capital 首都所在的区块坐标
* @param chunks 该国占有的全部区块坐标集合
* @param color 地图上用于绘制该国的 ARGB 颜色值
*/
@Environment(EnvType.CLIENT)
public record TerritoryState(String name, ChunkPos capital, Set<ChunkPos> chunks, int color) {
}