标记系统

图上的图标。每个标记只有类型、标签、坐标、是否透视可见四个数据,图标是客户端按类型名查贴图决定的。

两层数据

层 类 存档键 范围 客户端可写
本地 MarkersData aaMarkers_<atlasID> 只在这一本图册里显示 是(只能写自己这本)
全局 GlobalMarkersData extends MarkersData aAtlasGlobalMarkers 所有图册都显示 否,只能服务端 / API 写

GlobalMarkersData 只覆写了两处:createAndSaveMarker 和 loadMarker 都调 .setGlobal(true),以及 newMarkersPacket(atlasID, dimension) 忽略 atlasID 固定传 -1。

GlobalMarkersDataHandler.onWorldLoad 限定 dimensionId == 0——全局标记只从主世界加载,所以下界等地基世界里的标记不参与。

GlobalMarkersDataHandler.onClientConnectedToServer 在连远程服务器(非单机)时把 data 置 null,防止上一个世界的全局标记残留。源码注释解释了为什么用 connect 而非 disconnect 事件:后者在客户端其实不触发。

Marker

hunternif.mc.atlas.marker.Marker,字段全部 final(isGlobal 除外)。

字段 类型 说明
id int 只在单个 MarkersData 实例内唯一(一本图册内,或全世界的全局标记内)
type String 决定贴图的类型名,见下表
label String 可能是 gui.antiqueatlas.marker.* 占位键,构造时 null 转 ""
dim int 维度 ID
x / z int 方块坐标
visibleAhead boolean 为 true 时即使玩家没探索过该位置也显示
isGlobal boolean 运行时标志,不写入 NBT,由所属的 MarkersData 决定

getChunkX() / getChunkZ() 是 x >> 4 / z >> 4;getChunkCoords() 合并成 new ShortVec2(x >> 4, z >> 4)。equals / hashCode 只比较 id。

标签本地化

getLocalizedLabel() 的解析规则:把 label 从第一个空格切开,前面当本地化键、后面当唯一参数(玩家名)。

  • 无空格 → StatCollector.translateToLocal(label) 整串翻译。
  • 有空格 → 先只翻 key;若结果与 key 不同(说明翻译成功),返回 String.format(翻译结果, param);否则原样返回 label。

这正是墓碑标记存的 "gui.antiqueatlas.marker.tomb " + player.getCommandSenderName() 能显示成 Here lies <名字> 的原理。

内置标记类型

ClientProxy 用 setMarkerTextureIfNone(type, texture) 在 preInit 注册 13 种,配置里已存在的不覆盖。MarkerTextureMap.getTexture 查不到时回退 defaultTexture = MARKER_RED_X_SMALL(antiqueatlas:textures/marker/red_x_small.png)。

类型名 贴图常量 文件 由谁产生
tomb MARKER_TOMB marker/tomb.png DeathWatcher
village MARKER_VILLAGE marker/village.png VillageWatcher(全局)
nether_portal MARKER_NETHER_PORTAL marker/nether_portal.png NetherPortalWatcher
google MARKER_GOOGLE_MARKER marker/google_marker.png 手动
red_x_large MARKER_RED_X_LARGE marker/red_x_large.png 手动
red_x_small MARKER_RED_X_SMALL marker/red_x_small.png 手动(也是未注册类型的默认贴图)
diamond MARKER_DIAMOND marker/diamond.png 手动
bed MARKER_BED marker/bed.png 手动
pickaxe MARKER_PICKAXE marker/pickaxe.png 手动
sword MARKER_SWORD marker/sword.png 手动
skull MARKER_SKULL marker/skull.png 手动
tower MARKER_TOWER marker/tower.png 手动
scroll MARKER_SCROLL marker/scroll.png 手动

本地化键(assets/antiqueatlas/lang/en_US.lang):

键 英文
gui.antiqueatlas.marker.tomb Here lies %s
gui.antiqueatlas.marker.village Village
gui.antiqueatlas.marker.netherPortal Nether Portal

Marker 源码留了一条 TODO:make an option for the marker to disappear at a certain scale.——目前没有按缩放级别隐藏标记的选项。

存储结构

MarkersData 的内存字段:

字段 类型 说明
idMap Map<Integer, Marker> 全维度按 id 查
dimensionMap Map<Integer, DimensionMarkersData> 维度 → 该维度的标记集合
largestID AtomicInteger 发号器,getNewID() = incrementAndGet()
playersSentTo Set<EntityPlayer> 同步过一次就不再同步
CHUNK_STEP int = 8 标记按 8×8 MC 区块的方形分桶存,比 TileGroup.CHUNK_STEP(16)粗

DimensionMarkersData 的每个桶内标记按 Z 坐标排序,注释说明意图:放在靠南的标记要画在靠北的标记前面。源码 TODO 建议改用四叉树,因为小缩放级别下逐桶遍历渲染太慢。

NBT 字段名(v3)

MarkersData 的 VERSION = 3,字段名与 AtlasData 完全不同(没有 q 前缀):

字段名 常量 类型 内容
aaVersion TAG_VERSION int 3
dimMap TAG_DIMENSION_MAP_LIST NBTTagList 每项一个维度 compound
dimID TAG_DIMENSION_ID int 维度 ID
markers TAG_MARKERS NBTTagList 该维度的全部标记

每个标记 compound:

字段名 常量 类型 内容
id TAG_MARKER_ID int 标记 ID
markerType TAG_MARKER_TYPE String 类型名
label TAG_MARKER_LABEL String 标签(可能是占位键)
x TAG_MARKER_X int X 坐标
y TAG_MARKER_Y int 实际存的是 Z 坐标(markerTag.setInteger(TAG_MARKER_Y, marker.getZ()))
visAh TAG_MARKER_VISIBLE_AHEAD boolean 是否透视可见

y 存的是 Z,这是本 mod 最容易踩的 NBT 陷阱。

isGlobal 完全不落盘——读回来时 GlobalMarkersData.loadMarker 重新 setGlobal(true)。

版本兼容

readFromNBT 按 aaVersion 分支:

版本 行为
< 2 visAh 强制为 true,打日志 Marker is visible ahead by default
< 3 标记 id 重新发号(getNewID()),旧存档里没有持久化 id
= 3 读 id。若 getMarkerByID(id) != null(id 冲突),打日志 Loading marker with duplicate id %d. Getting new id 并重新发号
全部 加载后若 largestID < id 则把 largestID 推到 id;v3 分支无条件 markDirty()

增删

路径 端 入口
新增本地 客户端发起 地图界面 → AddMarkerPacket(C→S)→ 服务端校验归属 → createAndSaveMarker → sendToAll(new MarkersPacket(...))
新增本地 服务端 / API AtlasAPI.markers.putMarker(world, isGlobal, atlasID, type, label, x, z)
新增全局 服务端 / API AtlasAPI.markers.putGlobalMarker(world, visibleAhead, type, label, x, z)
删除 客户端发起 DeleteMarkerPacket(双向)→ 服务端校验归属 → 删除 → sendToAll
删除 服务端 / API AtlasAPI.markers.deleteMarker / deleteGlobalMarker

MarkersData.loadMarker 的语义是幂等但会吞掉重复:id 已在 idMap 里就整个跳过(注释说 the marker is not loaded!),但无论是否真的插入都 return marker。

AddMarkerPacket 收到后会 sendToAll 包括原发送者——注释解释:原发送者在等服务器确认,所以必须自己也收到。

渲染

客户端 GuiAtlas 逐桶遍历渲染,用 MathUtil.roundToBase(x, MarkersData.CHUNK_STEP) / CHUNK_STEP 计算可见桶范围,桶索引再 ± 1 外扩一圈。缩放时的标记尺寸由 settings.do_scale_markers 控制(见 SettingsConfig)。

相关条目

  • Antique Atlas - 本地标记随本物品的 atlasID 分开存
  • 自动标记 - tomb / village / nether_portal 三个 watcher
  • 地图数据模型 - 同一存档里的群系地图数据(字段名不同,注意区分)
  • 网络消息 - MarkersPacket / AddMarkerPacket / DeleteMarkerPacket
  • SettingsConfig - marker_textures.json 与 do_scale_markers