浏览器地图
游戏里的世界地图是拿来操作的,浏览器地图是拿来看的: 它画的就是游戏里那张地图——手画的路与铁路,加上阡陌交通、机械动力与 MTR 的只读图层—— 用一条命令送到浏览器标签页里,可以平移、缩放、按层过滤,还能导出一张带标题的 PNG。 它读的是这个客户端自己持有的数据——多人游戏里就是你自己画在本地的那一份——所以既不需要服务器配合, 也不会把地图发给别人;页面本身只读,不会改你的路网。
一条命令,一个标签页
在游戏里执行 /howtogo webmap,问道会在本机开一个小型 HTTP 服务;
服务启动成功后直接用系统默认浏览器把页面打开,不再先弹一个「是否打开链接」的确认框
要你点一下。聊天栏里仍然留着一行纯文本地址,想复制、想再点一次都行。
已经在运行的话只报地址,不会开第二个。
| 命令 | 作用 |
|---|---|
/howtogo webmap(与 /howtogo webmap start 相同) |
启动服务,成功后直接打开浏览器;已经在跑就只报一次地址。 |
/howtogo webmap stop |
停止服务并放开端口;本来就没在跑时会直说。 |
/howtogo webmap status |
报地址、当前世界与维度,以及路网的节点数、路段数与总长。 |
status 是在客户端线程上直接读一次路网再报的,所以它给出的规模,
就是页面此刻会画出来的规模——「地图上没东西」和「路网是空的」用这一条命令就能分开。
聊天消息里的地址就是唯一正确的地址:它带着实际绑定的端口。 换世界、换维度都不用重启服务——服务一直在,只是每次请求都读当前这一刻的世界。
为什么只监听本机
服务绑定 127.0.0.1 与 ::1 两个回环地址,
配置里没有任何能把它放到局域网上的开关。绑两个而不是一个,是因为
localhost 在双栈机器上会先解析到 ::1:只绑 IPv4 的话,
手输 localhost 的每一次连接都要先等一段 IPv6 回退,页面才是「开了但半天不出来」。
两个都是回环,这不是放宽了谁能访问,机器没有 IPv6 时 ::1 绑不上,
服务照常在 127.0.0.1 上跑,只在日志里记一行。
原因很直接:这张图画的是自己世界的坐标和地名。把它放到局域网上, 等于把整张地图连同所有地名、坐标,公开给同一个 Wi-Fi 下的每一台机器—— 酒店、学校、联机聚会,而这张图背后没有密码。
想让别人看到地图,就发导出的 PNG,而不是发地址。 地址只在跑游戏的那台机器上有效:同一台电脑的浏览器没问题, 手机、平板、另一台电脑都打不开,这不是配置问题,是设计如此。
页面、脚本和依赖都在 jar 里
页面、样式、五个脚本与 html2canvas 1.4.1(MIT)全部打包在模组 jar 里, 运行期不请求任何外部资源,所以没网也能用,也不会因为某个 CDN 挂了而白屏。
| 请求路径 | 内容 |
|---|---|
/、/index.html |
页面本体。 |
/style.css |
页面样式。 |
/js/model.js |
载荷解析、等级与层的文案、长度与时间的格式化。 |
/js/geom.js |
坐标变换、缩放与平移、最近点。 |
/js/labels.js |
文字的避让排布。 |
/js/render.js |
绘制:层序、描边与填充、箭头、标记、文字。 |
/js/app.js |
页面交互:唯一直接碰 DOM 的那个脚本。 |
/vendor/html2canvas.min.js |
html2canvas 1.4.1(MIT),导出带标题栏的 PNG 用。 |
/api/roads |
当前维度的路网快照(JSON):含只读图层,每条路段带可选的 curve 与 source。 |
/api/status |
一句存活回答,用来分辨「服务没开」与「世界没加载」。 |
服务只认这张表:表以外的路径一律 404,非 GET 一律 405,
响应也不缓存(no-store),所以刷新拿到的永远是这一版 jar 里的页面和数据,
不会出现「旧的 app.js 配新的数据」这种要对着一个你没在跑的版本排查的问题。
路径表是一对一映射,没有目录可查,也就无所谓目录穿越——它不是「检查过了」,而是根本没有目录可穿。
端口与自动启动
| 配置项 | 默认 | 作用 |
|---|---|---|
webmap_port |
7573 |
服务端口(配置里限定在 1024 ~ 65535)。被占用时依次试后面 9 个,一共 10 次。 |
webmap_auto_start |
关 | 打开后,世界加载完成时自动启动服务,不用每次手动敲命令。 |
配置的端口被占用是很普通的事:上一次会话还没放开那个套接字,或者别的模组也开了个小服务。
所以端口是试着来的——但实际绑定了哪个端口写在聊天消息里,
书签也就不会悄悄失效。想固定它,把 webmap_port 改成一个没人用的号码即可。
自动启动等世界加载之后才动手,而不是在客户端启动时就开: 否则从标题界面开始的每一个请求都会撞上「没有世界」,而玩家打开一个还没加载世界的页面时, 得到的应该是「等下再看」,不是一张空地图。启动失败只会记一次日志,不会每一刻都重试—— 端口全被占满这种事,一刻之后并不会自己变好。
这两项都在 config/howtogo-client.toml 里,
完整清单见 配置与数据文件。
数据只在游戏线程读
HTTP 处理线程从不碰路网对象。那是一个编辑器会就地修改一整个会话的
HashMap,从别的线程遍历它就是等着撞上 ConcurrentModificationException——
而且没有一个「安全的一瞬间」可以复制。所以请求被排进客户端刻的队列,
由游戏自己的线程把路网摊平成一份不可变快照,再交回给 HTTP 线程;
快照是值的副本,与还在被编辑的路网不共享任何东西。
等待有上限:5 秒。超时返回 503,而不是把浏览器挂住, 所以「这个世界还没画路」和「游戏还在加载世界 / 卡住了」是两个能分辨的答案, 页面会把这句话显示出来。刷新失败时,上一次画好的地图留在屏幕上, 不会因为一次请求失手就白屏。
页面上能做什么
页面画的是游戏里那张地图,不只是你手画的路:阡陌交通、机械动力与 MTR 的只读图层
也在里面,按道路等级取色,并带上画出来的平滑曲线。载荷里每条路段还多一个可选的
source(ROADWEAVER / CREATE / MTR /
ADDON;玩家自己手画的没有这一项),页面用它把「只读」和来源写进鼠标读数与统计,
所以侧栏的筛选下面多一行「自动识别」。只读图层各自怎么读,见
阡陌交通、机械动力铁路与
MTR 集成。
左边一列是筛选、统计与导出,中间是画布,底部一条状态栏。 所有筛选都同时作用于屏幕与导出——图上看到的和导出的就是同一张。
| 操作 | 结果 |
|---|---|
| 按住拖动 | 平移地图。 |
| 滚轮 | 以光标为中心缩放:光标底下的那一点不动,放大的就是你想看的地方。 |
| 「适应窗口」 | 把整张路网放进窗口(在页面上按 F 同样是这个)。 |
| 「刷新」/「自动刷新」 | 重新取一次 /api/roads;自动刷新每 5 秒一次,正在导出或上一次还没回来时跳过这一拍,不排队。 |
| 悬停 | 状态栏显示光标坐标、当前缩放,以及鼠标下最近的一条路或一个地点; 那条路如果来自只读图层,还会标出它是自动识别来的、来自哪个来源。 |
侧栏的筛选按道路等级与道路层各列一行,带颜色方块与数量, 路名与地点名可以单独关掉;统计数据列出节点、路段、命名道路、命名地点、总长度、有哪些层, 以及这份快照的生成时间与数据版本。页顶那一行则写着存档、维度、节点 / 路段、总长与生成时间。
图例就是筛选器。取消勾选某个等级或某一层,它同时从屏幕上和导出的 PNG 里消失; 鼠标下读到的那条路也会报出等级、方向、层,以及这一段的长度四项—— 和游戏里选中一条路时看到的是同一套读数。
谁压在谁上面
路段按 layer 从小到大画:高架(层 1)压在自己跨过去的地面路(层 0)之上、
隧道(层 -1)在最下面,低层永远不会盖住高层。一张平面俯视图里,这是「谁在谁上面」
唯一能画出来的方式;同一层内的先后不变,所以这一条只影响那些本来就画反了的交叉。
同一层里,先画完所有描边、再画所有填充。于是十字路口看起来是一条路从另一条上跨过去, 而不是两团颜色糊在一起;单向箭头跟着本层的填充画,所以桥也盖得住桥下的箭头。 这之后才是路口点与地点标记,最后是所有文字——文字永远在最上面。
层是平面地图看不出来的那一半:桥还是隧道,靠的就是这个顺序。游戏里同一套规则已经对齐, 细节见 地图显示与缩放。
路段带平滑曲线(curve)时,页面就用这条折线描边,所以描边的方向与画出来的线一致;
没有 curve 的手画道路照旧按顶点画。网页地图与游戏里共用同一个描边过程,
弯道外侧与接缝这两处不会一边补上、一边缺口。
文字会挪位置,而不是压住别人
名字的排布与游戏里的世界地图是同一套规则:同一张「已占位置」表、同一套字号与避让判断, 所以网页里看到的文字位置,就是游戏里能找到的那个位置,导出时也不会重新排一次。
线上的名字字号不超过这条路画出来的宽度:宽路字大、窄路字小,名字永远落在路面里;
路太细时(缩小的远景、小路、铁路)先不写名字,放大到够宽才出现。一段路很长时,
沿路每隔约 190 像素写一个,每段最多 8 个,并且只写在屏幕上的可见段里——
可见段比名字本身还短,这一段就不写。
地点名、铁路名、道路名排在同一张「已占位置」表上,按这个顺序找位置,排不下就不画;
两个地点靠近时只显示一个,放大到两个名字框分开才都显示。
地点名默认写在标记下方(地图上的名字就该在钉子下面),被占了就依次试上方、右方、左方、
四个斜角,都不行才退一步。地点名的字号按类型分:资源点 0.4、普通地点 0.55、
站点 0.7、商店 1.0(像素 / 格)。
地点名避开的是真正画出来的线:有平滑曲线(curve)的路段按曲线避让,
没有 curve 的手画道路照旧用顶点。所有方向都放不下时,这个标签就不画——
一句压在别人身上的路名,比没有路名更难读。所有文字都带一圈底色描边,所以压在任何颜色的路上都读得清。
导出 PNG
导出用的是屏幕上那同一个绘制函数,只是按 2 倍分辨率重画一遍:
同样的视野、同样的层序、同样的描边与填充顺序、同样的文字排布。所以导出的图与屏幕上看到的遮挡一致,
名字的数量与位置也与屏幕一致——2 倍分辨率是把同一张图放大重画,
不是换一块更大的画布重新排一次字。图上方带一条标题栏:世界、维度、时间、统计与图例,
文件名形如 howtogo-minecraft_overworld-20261006-233000.png。
万一 html2canvas 不可用(被拦下、损坏、被别的扩展动过),导出会退回直接导出画布: 图还是有的,只是不带标题与图例,页面上也会写清楚为什么这张图更朴素,按钮不会静默失效。
导出的是当前视野,不是整张路网。想导出一张全景图, 先按「适应窗口」,再点导出。
自检里的浏览器地图
/howtogo selftest 会多跑一项浏览器地图的检查:在一个由操作系统分配的端口上
真的把服务开起来,取回页面、脚本、html2canvas 与 /api/roads,
核对份数,然后把服务关掉——不会留下一个在监听的套接字,也不会和你正开着的页面撞端口。
它回答的是回归测试答不了的两个问题:这个启动器的模块图里到底有没有 JDK 自带的
com.sun.net.httpserver(少了它,页面就是一个 NoClassDefFoundError),
以及页面和脚本能不能透过模组自己的类加载器找到(开发时是转换过的类加载器,发行版是一个 jar)。
这两件事只有在游戏里才能回答。
它和游戏里那张地图的分工
两张图画的是同一份数据,但用途不同:游戏里的世界地图是操作界面, 浏览器地图是一张只读的图——它不修改任何东西,也不需要修改。
游戏里的世界地图
画路、命名、标地点、选目的地、跟着 HUD 出发。它是编辑器,也是驾驶舱。
浏览器地图
一张大图:检查整片路网(含阡陌交通、机械动力与 MTR 的只读图层)、按层看清高架与隧道、 导出 PNG 发给别人,或者放在第二个屏幕上当参考图。
打不开或一片空白时,按顺序查这四处
- 服务在跑吗?先
/howtogo webmap status;没在跑就/howtogo webmap。 - 地址对得上吗?必须用聊天消息里那一条。端口被占用时会顺延,
手输记忆里的
7573可能不是真正在用的那个。 - 世界加载了吗?页面写着「读取道路数据失败」,多半就是 503: 游戏还在加载世界,或者卡住了。等它跑起来,再点一次刷新。
- 画过路吗?「服务器返回了一个空网络」不是出错,是在说这个世界还没有画路——
/howtogo webmap status报的节点与路段数会一起证明这一点。
四处都正常还是打不开,通常只剩一件事:浏览器和游戏不在同一台机器上。 服务只听回环地址,手机、平板、另一台电脑本来就访问不到;想让别人看,导出 PNG 发过去。 相关规则见 常见问题与限制, 端口与自动启动的配置项见 配置与数据文件, 游戏内那张地图的显示规则见 地图显示与缩放。