远端 HTTP 协议(本客户端消费的服务端接口)
基本信息
| 属性 | 值 |
|---|---|
| 基址 | remote.baseUrl / RemoteConfiguration.baseUrl,默认 http://127.0.0.1:8123 |
| 协议 | 纯 HTTP GET,无鉴权、无请求头、无分页 |
| JSON 库 | com.github.cliftonlabs:json-simple:4.0.1(shadowImplementation 打进 jar) |
| 解析类 | RemoteConfiguration / TileLoaderUrl |
[!IMPORTANT] 本条目描述的是客户端如何调用服务端,不是服务端的实现。 服务端代码在另一个仓库
GTNH-Web-Map(dependencies.gradle中 以runtimeOnlyNonPublishable("com.github.GTNewHorizons:GTNH-Web-Map:0.3.39:dev")仅供开发期运行)。⚠️ 二者不要混淆:本仓库无服务端、无瓦片生成逻辑。
端点 1:地图目录
GET {baseUrl}/up/configuration
响应 JSON 结构(按 RemoteConfiguration.load() 实际取用的键):
{
"worlds": [
{
"name": "<世界名,用作 URL 路径段>",
"title": "<世界显示名>",
"maps": [
{
"name": "<地图名>",
"title": "<地图显示名>",
"prefix": "<瓦片 URL 用的前缀>",
"maptoworld": [9 个数,按行主序 3x3 矩阵],
"worldtomap": [6 个数,按行主序 2x3 矩阵]
}
]
}
]
}
字段用途:
name(世界)→ 瓦片 URL 的第 1 段路径(RemoteMap构造时存入PerMapTileDataBase.worldName)prefix(地图)→ 瓦片 URL 的第 2 段路径title(世界 + 地图)→ 仅用于PerMapTileDataBase.title显示, 拼接格式为世界title + " - " + 地图title(RemoteMap.java:23)name(地图)⚠️ 读取后从未被使用(RemoteConfiguration.java:44存入RemoteMap.name,但该字段无任何读取方)
坐标换算(RemoteMap.java:43-59):
worldToMapCoord:先做 2x3 矩阵乘(x, y, z),再各除以 128 得地图坐标mapCoordToWorld:先还原lat = mapX * 128、lon = mapY * 128 + 128, 再用 3x3 矩阵的第 0/1/2 行算 X、第 2 行算 Z
⚠️ 语义陷阱:mapCoordToWorld 中 Y 坐标被硬编码为 64
(RemoteMap.java:44 double y = 64;,返回值也是 new Point3d(wx, y, wz))。
⇒ 地图只有 XZ 平面,Y 永远是 64,不是玩家的真实高度。
focusOnWorldPoint(MapDrawer.java:211-218)传入玩家完整
(posX, posY, posZ),但只有 XZ 参与计算。
端点 2:PNG 瓦片
GET {baseUrl}/tiles/{worldName}/{mapPrefix}/{x/32}_{y/32}/{tileName}.png
{x/32}_{y/32} 是瓦片分组(TileLoaderUrl.java:30)。
tileName 规则(TileLoaderUrl.java:25-28):
| zoom | tileName |
|---|---|
| 0 | x_y |
| >0 | "z" * zoom + "_" + x + "_" + y(如 zoom 2、x=64、y=96 → zz_64_96) |
⚠️ 源码用字面量字符串 "zzzzzzzzzz".substring(0, zoom)
硬编码了 10 个 z,与 PerMapTileDataBase 的 10 个层级对应。
若层级数增加会直接 StringIndexOutOfBoundsException。
重试策略
| 数值 | 值 | 来源 |
|---|---|---|
| 最大重试次数 | 10(if (retries > 10) retries = 10;) |
TileLoaderUrl.java:45 |
| 重试延迟 | 60 * (1 << retries) ms,上限约 61.4 s |
TileLoaderUrl.java:53 |
| 线程池 | Executors.newCachedThreadPool()(静态,无上限) |
TileLoaderUrl.java:18 |
加载流程:PerZoomLevel.getTileAt 首次访问某瓦片时创建 TileLoaderUrl
并提交异步任务;之后每帧轮询 isDone();成功则把 Tile 存入
tiles 缓存并移除 loader,失败则 retry()。
⇒ 失败的瓦片会被无限次重试(每次间隔指数增长并封顶在 ~61 s),
且 tileLoaders 中的条目永不清理,地图长时间开着会累积条目。
源码核对(2026-10-01 审计)
- ⚠️ 死字段:
RemoteConfiguration.allMaps(:20)在:55被写入, 但全仓grep显示没有任何读取方。实际 GUI 用的是ModularTest.MapPanel.maps,两者无关。 - ⚠️ 参数名误导:
TileLoaderBase.beginLoad(String worldName, String mapName, ...)第二个形参叫mapName,但唯一调用点PerMapTileDataBase.java:56传入的是字段mapPrefix。 实际拼进 URL 的是 prefix,不是RemoteMap.name。 - ⚠️ 异常处理缺失:
load()只catch (IOException)(:94),ImageIO.read返回null时(非 PNG 内容 / 流为空)tile.newlyLoadedImage被赋为null,load()却返回 true, 之后Tile.update()因newlyLoadedImage == null静默跳过, 该瓦片永远空白且不再重试。 - ⚠️
MapDrawer.getTile(:56-59)不判currentTileDB空, 依赖draw()开头的if (currentTileDB == null) return;(:62-64)兜底。 若RemoteConfiguration解析出 0 张地图,setMap不会被调用 (ModularTest.java:66-69有size() > 0判断),此时draw()的 空判断生效,不会 NPE。 - 瓦片像素上传为 RGBA8 / GL_RGBA / UNSIGNED_BYTE,逐像素手工拆通道
(
Tile.java:62-71),绕过了GL_BGRA_EXT路径 —— 与 1.7.10 常见写法不同但功能等价。