标记系统
图上的图标。每个标记只有类型、标签、坐标、是否透视可见四个数据,图标是客户端按类型名查贴图决定的。
两层数据
| 层 | 类 | 存档键 | 范围 | 客户端可写 |
|---|---|---|---|---|
| 本地 | 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