远端 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 常见写法不同但功能等价。