地图数据模型

图册的全部像素数据。这是 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 取自 ShortVec2 key,即区块索引,而 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 时不要混用。

相关条目