多人协作增量地图上报系统:每个玩家的 Minecraft 客户端把自己看到的地图增量上报给中央服务器,服务器为每个玩家维护独立地图会话、裁决合成一张完整地图,并通过 squaremap 兼容的 Web 界面展示。
TeamMap Mod (Fabric 26.1.2) Rust 后端 (Axum) Web 前端 (squaremap)
┌──────────────────────┐ WS+protobuf ┌──────────────────────────┐ HTTP ┌──────────────────┐
│ 区块着色 -> canonical │ ───────────▶ │ 会话隔离 -> Merkle 对账 │ ◀───── │ Leaflet 瓦片地图 │
│ XXH3 哈希 -> 脏队列 │ zstd 压缩 │ 内容寻址 blob -> 裁决合成 │ SSE │ 玩家标记/名牌 │
│ 令牌桶 -> 增量上传 │ ◀─────────── │ PNG 金字塔 -> SQLite │ ─────▶ │ 多视图切换 │
└──────────────────────┘ ACK/差量 └──────────────────────────┘ └──────────────────┘
| 目录 | 内容 |
|---|---|
protocol/ |
teammap/v1/map.proto —— 客户端/服务端共用协议(XXH3 双种子 128 位内容哈希、region Merkle 两级树、组级差量) |
mod/ |
Fabric 26.1.2 客户端 mod(Java 25,官方命名,loom 1.17.17 no-mappings 模式) |
backend/ |
Rust 中央服务器:axum + prost + sqlx(sqlite) + zstd + xxhash-rust |
web/ |
从 squaremap(MIT)vendor 的前端 + TeamMapBridge(SSE 即时刷新) |
squaremap/ |
上游 squaremap 源码,仅作实现参考(渲染管线/瓦片布局/前端基座) |
- 内容寻址去重:每个区块的 canonical 记录(256×ARGB + 256×高度 = 1536B)以
XXH3-64(seed0) ++ XXH3-64(seedGOLDEN)做 128 位内容哈希。相同内容全局只存一份; 重复上报在服务端按哈希直接去重(DEDUP),零存储、零重绘、零下发。 - Tile Merkle tree 对账:region(32×32 区块 = 512×512 像素,squaremap 同款瓦片) 内两级摘要(32 组 × 32 叶)。客户端每 30s 推一次世界摘要(每 region 仅几百字节), 服务端逐组比对,只请求真正不同的组;重连恢复时未变化区域零重传。
- 三层带宽护栏(客户端+服务端各一把令牌桶):内容去重 → flush 周期 + 令牌桶 (默认 64 KiB/s,突发 256 KiB)→ 未确认积压上限(默认 4 MB),超限暂停采集。
- 主线程保护:mod 端每 tick 限预算重着色(默认 2 个区块/tick);服务端合成重绘 按 1s 批量 flush,仅脏瓦片重画。
- 会话 =
(player_uuid, world),各自独立存储,互不交叉;交叉只发生在合成图。 - 裁决按最新 capture_ts 优先(客户端时钟偏差在服务端钳制到 ±24h/+5min)。
- 防抖:
- 内容相同(同哈希)不触发任何裁决/重绘;
- 滞回:挑战者须比现任胜者新 5s(翻转越多滞回越长,上限 30s);
- 冷却:60s 内翻转 3 次 → 锁死现任胜者并标记
disputed(Web 上黄框提示),冷却期 60s; - 离线来源的地图保留,在线来源内容更新即可接管(无需滞回)。
- 探索流式 e2e(
tests/exploration_e2e.rs)专门覆盖"一个人朝一个方向 持续跑图、那片视野只有他有"的场景:合成图随探索生长、独有视野成为胜者、 未探索者个人视图为空、探索者离线后成果保留。
- 内存:blob 以 zstd 压缩形态缓存的 FIFO 池(默认 256 MiB 上限),解码只发生在合成瞬间。
- SQLite(WAL):
blobs(内容寻址+GC)/chunk_state(会话区块)/region_state(Merkle 根)/composite(裁决结果)/sessions。重启后全量恢复。 - 派生 PNG 瓦片写磁盘缓存(可再生),HTTP ETag/304 协商缓存。
cd backend
cargo run --release
# 首次运行会生成 config/teammap.toml(含全部参数注释)cd web
pnpm install && pnpm build # 产物在 web/dist,由后端托管cd mod
./gradlew build # 产物 mod/build/libs/teammap-0.1.0.jar
# 放入 26.1.2-Fabric 实例的 mods/ 目录游戏内入口:Mods 列表(Mod Menu)-> TeamMap -> 设置。可配置服务器地址、
房间号、上传间隔/速率、着色预算,保存后立即生效(无需重启游戏)。
也可直接编辑实例目录 config/teammap.json。
连不上服务器? 设置页勾选"禁用系统代理"。游戏启动器常带
-Djava.net.useSystemProxies=true,系统代理会把 127.0.0.1/内网直连 也拦下来;客户端日志(logs/latest.log搜teammap)会显示每次 连接与上传的详细状态。
默认原生 4 级(z0~z3 全分辨率)+ 额外 2 级放大([map].extra_zoom,最大可视
64px/区块,像素锐化渲染)。调节 config/teammap.toml 的 extra_zoom 后重启生效。
- 无口令:房间号即身份——把服务器地址 + 房间号发给朋友,即可共同查看/上报同一张地图。
- 所有数据(会话、合成图、瓦片、玩家标记)按房间完全隔离;房间随首次加入自动创建。
- Web 端视图命名:合成图
{房间}~{世界}、个人图{房间}~{世界}~{玩家名}, 侧栏自动列出全部房间。
cd backend
cargo run --example sim_client -- --addr 127.0.0.1:8790 --players Alice,Bob --room game1 --stay-secs 600
# 打开 http://127.0.0.1:8790/?world=game1~overworld&zoom=3&x=0&z=0overworld—— 全体玩家裁决后的合成视图(disputed 区块带黄框描边)overworld~Alice—— Alice 的个人视图(她所见地图的原始状态)- 视图列表 = 顶层
tiles/settings.json的 worlds;squaremap 前端零改动兼容。
cd backend
cargo test # 21 单测 + 4 端到端(对账/差量/防抖/持久化/房间隔离/探索流式)
cargo clippy --all-targets # 0 警告backend/、protocol/、mod/:MITweb/基于 squaremap web(MIT,© jpenilla 及贡献者);squaremap/为上游参考副本。