地图数据模型
图册的全部像素数据。这是 Antique Atlas 唯一真正的「世界」:没有方块实体、没有 tile 实体,地图就是挂在 WorldSavedData 上的 NBT。
层次结构
ItemStack(metadata = atlasID) ← Antique Atlas 的物品 damage 值
└─ AtlasData WorldSavedData, key = "aAtlas_" + atlasID
└─ Map<Integer, DimensionData> key = dimensionID
├─ Map<ShortVec2, TileGroup> tileGroups key = 区块索引 (x>>4, z>>4)
├─ Rect scope 已探索范围
└─ browsingX / browsingY / browsingZoom 浏览位置
三份存档数据并存,键名互不冲突:
| 数据 | 键 | 版本 | 归属 |
|---|---|---|---|
| 群系地图 | aAtlas_<atlasID> |
3 | 每本图册一份 |
| 标记 | aaMarkers_<atlasID> |
3 | 每本图册一份,见 标记系统 |
| 伪群系坐标 | aAtlasExtTiles |
1 | 全世界共用 |
| 全局标记 | aAtlasGlobalMarkers |
3 | 全世界共用,见 标记系统 |
Tile
hunternif.mc.atlas.core.Tile —— 地图上的一格,值 16×16 格方块。
| 字段 | 类型 | 说明 |
|---|---|---|
biomeID |
public final int |
群系 ID。自然群系取原版 ID;伪群系取 ExtTileIdMap 分配的自定义 ID(必为负数) |
variationNumber |
private transient short |
贴图变体号,0 ~ Short.MAX_VALUE - 1。transient,不写入 NBT,每次加载都重新随机 |
equals / hashCode 只比较 biomeID:obj instanceof Tile && ((Tile)obj).biomeID == biomeID。变体号不参与相等判定,所以「该区块已扫描」的判断不受贴图变体影响。
ITileStorage
hunternif.mc.atlas.core.ITileStorage,5 个方法。两个实现:TileGroup 和 DimensionData。
| 方法 | 语义 |
|---|---|
void setTile(int x, int y, Tile tile) |
写入一格 |
Tile removeTile(int x, int y) |
清空一格,返回被清掉的旧 Tile |
Tile getTile(int x, int y) |
读取一格;越界返回 null |
boolean hasTileAt(int x, int y) |
判存在 |
Rect getScope() |
覆盖范围 |
两个实现的 hasTileAt 语义相反:正常应为 getTile(x,y) != null,DimensionData 写对了(return getTile(x,y)!=null;),而 TileGroup 写成了 return getTile(x, y) == null;(取反)。实际调用方(ItemAtlas.onUpdate)用的是 getTile 判空,所以不影响扫描行为。
TileGroup
hunternif.mc.atlas.core.TileGroup 实现 ITileStorage,一个 TileGroup 就是一个 16×16 区块。内存结构是二维数组,NBT 里才压成一维 int 数组。
| 字段 | 类型 | 说明 |
|---|---|---|
scope |
Rect |
new Rect(x, y, x+15, y+15),CHUNK_STEP = 16 |
tiles |
Tile[16][16] |
二维数组,tiles[rx][ry],rx = x - scope.minX |
setTile / getTile 先做 scope 越界检查,越界写会 Log.warn 输出 TileGroup tried to set tile out of bounds,越界读返回 null。
NBT
writeToNBT / readFromNBT 只写两个字段:
| 字段名 | 常量 | 类型 | 内容 |
|---|---|---|---|
p |
TAG_POSITION |
int[2] | {scope.minX, scope.minY}。maxX/maxY 不存,读回时由 minX + CHUNK_STEP - 1 重算 |
t |
TAG_TILES |
int[256] | 256 个群系 ID,索引 x + y * 16(x 为快维,顺序 0 1 2 / 3 4 5 / 6 7 8),-1 = 空 |
readFromNBT 时 -1 还原为 null,非 -1 直接 new Tile(biomeID)——变体号因此在加载时重新随机。
DimensionData
hunternif.mc.atlas.core.DimensionData 实现 ITileStorage,注释自述「Thread-safe (probably)」。所有集合都是 ConcurrentHashMap。
| 字段 | 类型 | 说明 |
|---|---|---|
parent |
AtlasData |
所属图册,写入时调 parent.markDirty() |
dimension |
int |
维度 ID |
browsingX / browsingY |
int |
浏览中心,方块单位 |
browsingZoom |
double |
缩放,字段初值 0.5 |
tileGroups |
Map<ShortVec2, TileGroup> |
key 是区块索引(floor(x/16), floor(y/16)),不是方块坐标 |
thread2KeyMap |
Map<Thread, ShortVec2> |
复用临时 key,避免每次 new ShortVec2 |
scope |
Rect |
已探索范围,setTile 时 extendTo(x, y) |
setTileGroup / putTileGroup 覆盖写入(TileGroupsPacket 收包走这条)。setBrowsingPosition 写入时若 browsingZoom <= 0 会 Log.warn 并回退到 settings.minScale。
两个已知的未实现点:
removeTile只return getTile(x,y),没有真的删除。源码注释:since scope is not modified, I assume this was never really used。ItemAtlas.onUpdate重扫时依赖它删掉已消失的群系。getSeenChunks()每次调用都新建一个Map<ShortVec2, Tile>(注释:packet rework 之后每次都要新 map),且内层循环for (x = basex; x < basex+16; x++)里basex取自ShortVec2key,即区块索引,而TileGroup.getTile期望方块坐标。
writeToNBT / readFromNBT 直接操作 NBTTagList,逐个调 TileGroup.writeToNBT。
AtlasData
hunternif.mc.atlas.core.AtlasData 继承 WorldSavedData,VERSION = 3。dimensionMap 是 Map<Integer, DimensionData>,getDimensionData(dimension) 懒创建(不存在就 new 并放入)。
playersSentTo 记录本图册数据已同步给哪些玩家——只同步一次,之后 ItemAtlas.onUpdate 不再重复推送。syncOnPlayer 流程:先 writeToNBT(nbt, false)(includeTileData=false,不写 tile 数据)发 MapDataPacket,再对每个维度调 DimensionData.syncOnPlayer 发 TileGroupsPacket(每包最多 TILE_GROUPS_PER_PACKET = 100 个 TileGroup)。
NBT 字段名(v3)
| 字段名 | 常量 | 类型 | 内容 |
|---|---|---|---|
aaVersion |
TAG_VERSION |
int | 常量 3 |
qDimensionMap |
TAG_DIMENSION_MAP_LIST |
NBTTagList | 每项一个维度 compound |
每个维度 compound:
| 字段名 | 常量 | 类型 | 内容 |
|---|---|---|---|
qDimensionID |
TAG_DIMENSION_ID |
int | 维度 ID |
qVisitedChunks |
TAG_VISITED_CHUNKS |
NBTTagList | 每项一个 TileGroup 的 NBT(p + t) |
qBrowseX |
TAG_BROWSING_X |
int | 浏览中心 X |
qBrowseY |
TAG_BROWSING_Y |
int | 浏览中心 Y |
qBrowseZoom |
TAG_BROWSING_ZOOM |
int | round(zoom × 1024),1024 来自 BrowsingPositionPacket.ZOOM_SCALE_FACTOR |
版本兼容
readFromNBT:aaVersion < 3时打日志Outdated atlas data format! Was %d but current is %d. Updating.并转readFromNBT2。readFromNBT2:aaVersion < 2时打日志Loading map with version 2 failed并直接放弃(markDirty()后 return)。否则按旧格式读——此时qVisitedChunks是扁平 int 数组而非 TileGroup 列表,每 3 个一组[x, y, biomeID],逐组setTile重建。检测到重复 tile 会打Duplicate tile at ...。- 两处读缩放时若结果为 0 都会强制回退 0.5。
readFromNBT2末尾打Updated N chunks(N = 数组长度 / 3)。
getVisitedDimensions() 返回 dimensionMap.keySet(),getSeenChunksInDimension(dim) 转发给 DimensionData.getSeenChunks()。
伪群系坐标表
hunternif.mc.atlas.ext.ExtBiomeData 继承 WorldSavedData,键 aAtlasExtTiles,VERSION = 1,内存结构 Map<Integer, Map<ShortVec2, Integer>>(维度 → 坐标 → 伪群系 ID)。
| 字段名 | 常量 | 类型 | 内容 |
|---|---|---|---|
aaVersion |
TAG_VERSION |
int | 1 |
dimMap |
TAG_DIMENSION_MAP_LIST |
NBTTagList | 每项一个维度 compound |
dimID |
TAG_DIMENSION_ID |
int | 维度 ID |
biomeIDs |
TAG_BIOME_IDS |
int[] | 扁平三元组 [x, y, biomeID],每 3 个一组 |
getBiomeIdAt 无记录时返回 -1——这个 -1 就是 ItemAtlas.onUpdate 判定「此处没有伪群系」的唯一依据。setBiomeIdAt / removeBiomeAt 都调 markDirty()。
注意 ExtBiomeData 的字段名(dimMap / dimID / biomeIDs)与 AtlasData(qDimensionMap / qDimensionID / qVisitedChunks)完全不同,写 NBT 时不要混用。
相关条目
- Antique Atlas - atlasID 的来源(物品 metadata)
- 空白 Antique Atlas - 首次创建
aAtlas_<id>与aaMarkers_<id> - 网络消息 - MapDataPacket / TileGroupsPacket / TilesPacket / TileNameIDPacket 搬运本文件的 NBT
- 群系检测 - 谁决定
biomeID - 标记系统 - 同一存档里另一套
WorldSavedData - 自动标记 - 谁往伪群系表和标记表里写