网络消息
全部通信走 SimpleNetworkWrapper,通道名 AntiqueAtlasMod.CHANNEL,注册入口 PacketDispatcher.registerPackets()(preInit 阶段调用)。共 11 条消息,packetId 从 0 递增。
方向如何决定
AbstractMessage 的三个子类 / 抽象内部类决定注册端:
| 基类 | isValidOnSide |
注册端 |
|---|---|---|
AbstractClientMessage |
side.isClient() |
Side.CLIENT |
AbstractServerMessage |
side.isServer() |
Side.SERVER |
AbstractMessage 直接继承 |
恒为 true |
同一 packetId 双向注册 |
onMessage 收到后先 isValidOnSide(ctx.side) 校验,非法直接抛 RuntimeException("Invalid side ..."),再调 process(player, side)——所以双向包的 process 必须自己分支 side。
消息一览
| ID | 类 | 方向 | 载荷(读/写顺序) | 何时发送 |
|---|---|---|---|---|
| 0 | DeleteMarkerPacket |
双向 | varint atlasID,varint markerID |
客户端请求删除标记;服务端处理后 sendToAll 广播响应 |
| 1 | PutBiomeTilePacket |
双向 | varint atlasID,varint dimension,varint x,varint z,varint biomeID |
客户端请求写入一格群系;服务端转发给所有已同步该图册的客户端 |
| 2 | MapDataPacket |
S→C | varint atlasID,NBT(int 长度 + gzip 字节;-1 表示 null) |
AtlasData.syncOnPlayer 的第一步 |
| 3 | TileGroupsPacket |
S→C | varint atlasID,varint dimension,varint 数量,然后每个 TileGroup 一段 NBT |
DimensionData.syncOnPlayer,每包最多 100 个 TileGroup |
| 4 | TileNameIDPacket |
S→C | varint 数量,然后每个:UTF-8 name + varint -biomeID |
ExtTileIdMap.syncOnPlayer;RegisterTileIdPacket 收到后单条广播 |
| 5 | TilesPacket |
S→C | varint dimension,varint tileCount,然后 tileCount × 6 原始字节 |
ExtBiomeData.syncOnPlayer,每维度一条 |
| 6 | MarkersPacket |
S→C | varint atlasID,varint dimension,varint 类型数,然后每类型:UTF-8 type + varint 数量 + 每标记(varint id、UTF-8 label、int x、int z、boolean visibleAhead) |
MarkersData.syncOnPlayer / GlobalMarkersData.syncOnPlayer;AddMarkerPacket 收到后广播 |
| 7 | DeleteCustomGlobalTilePacket |
S→C | int dimension,int chunkX,int chunkZ(非 varint) |
服务端删除一个全局伪群系 tile 时 |
| 8 | AddMarkerPacket |
C→S | varint atlasID,varint dimension,UTF-8 type,UTF-8 label,int x,int y,boolean visibleAhead |
地图界面手动新增标记 |
| 9 | RegisterTileIdPacket |
C→S | UTF-8 name |
客户端注册一个新的 (唯一 tile 名, 伪群系 ID) 对 |
| 10 | BrowsingPositionPacket |
C→S | varint atlasID,varint dimension,varint x,varint y,varint round(zoom × 1024) |
地图界面缩放/平移后保存浏览位置 |
包分布:
network/
├── AbstractMessage.java 基类 + writeNBT/readNBT 静态工具
├── PacketDispatcher.java 通道 + 11 条注册 + 5 种发送方法
├── bidirectional/ DeleteMarkerPacket, PutBiomeTilePacket
├── client/ MapDataPacket, TileGroupsPacket, TileNameIDPacket,
│ TilesPacket, MarkersPacket, DeleteCustomGlobalTilePacket
└── server/ AddMarkerPacket, RegisterTileIdPacket, BrowsingPositionPacket
GLOBAL 哨兵值
DeleteMarkerPacket 和 MarkersPacket 都定义 private static final int GLOBAL = -1,放在 atlasID 位置表示「全局标记」。两者都有 isGlobal() 判 atlasID == GLOBAL。
服务端归属校验
AddMarkerPacket、BrowsingPositionPacket、DeleteMarkerPacket、PutBiomeTilePacket 四条的 process 在服务端分支都先做同一条检查:
if (!player.inventory.hasItemStack(new ItemStack(AntiqueAtlasMod.itemAtlas, 1, atlasID))) { ... return; }
ItemStack 的 damage 即 atlasID,所以 hasItemStack 等价于「玩家的主物品栏里有一本 ID 匹配的地图册」。不匹配时打日志后静默丢弃,不报错:
| 包 | 日志文案 |
|---|---|
AddMarkerPacket |
Player %s attempted to put marker into someone else's Atlas #%d |
BrowsingPositionPacket |
同上(文案与实际行为不符,抄自 AddMarker) |
DeleteMarkerPacket |
Player %s attempted to delete marker from someone else's Atlas #%d |
PutBiomeTilePacket |
Player %s attempted to modify someone else's Atlas #%d |
这防的是客户端伪造 atlasID 改别人的图册。AddMarkerPacket 的注释还点明:手动加的标记必须是本地的(isGlobal 不可由客户端指定),以防 griefing。
NBT 传输编码
AbstractMessage 的两个静态工具方法,所有含 NBT 的包都用它:
| 方法 | 编码 |
|---|---|
writeNBT(ByteBuf, NBTTagCompound) |
tag 为 null → writeInt(-1);否则 CompressedStreamTools.compress 后 writeInt(length) + writeBytes |
readNBT(ByteBuf) |
readInt(),负数 → null;否则读 length 字节,用 CompressedStreamTools.func_152457_a 配 new NBTSizeTracker(Integer.MAX_VALUE) 解压 |
源码在此处留了 TODO:make sure that very large atlases can be loaded, synced and rendered.
流量优化:负 varint
TileNameIDPacket 对每个 ID 取负后再写 varint(writeVarIntToBuffer(-entry.getValue()),读时再取负),注释说明原因是 custom biome IDs are always negative——这样能省流量,因为原版群系 ID(0~255)用 varint 只要 1 字节,负数要多 1~2 字节。
同步时机
只在每本图册首次遇到该玩家时同步一次,由两份数据的 playersSentTo 集合各自记录:
| 数据 | 触发点 | 发的包 |
|---|---|---|
| 群系地图 | ItemAtlas.onUpdate → data.syncOnPlayer(stack.getItemDamage(), player) |
MapDataPacket + 每维度 1+ 条 TileGroupsPacket |
| 本地标记 | ItemAtlas.onUpdate → markers.syncOnPlayer(stack.getItemDamage(), player) |
每维度 1 条 MarkersPacket |
| 全局标记 | GlobalMarkersDataHandler.onPlayerLogin(PlayerLoggedInEvent) |
每维度 1 条 MarkersPacket(atlasID = -1) |
| 伪群系 ID 映射 | ExtBiomeDataHandler.onPlayerLogin → ExtTileIdMap.instance().syncOnPlayer |
1 条 TileNameIDPacket |
| 伪群系坐标 | ExtBiomeDataHandler.onPlayerLogin → data.syncOnPlayer |
每维度 1 条 TilesPacket |
登录时同步的两条走同一个事件,但注册在不同 handler:GlobalMarkersDataHandler 挂在 AntiqueAtlasMod 的 handler 列表里,ExtBiomeDataHandler 单独注册。GlobalMarkersData 的 onWorldLoad 额外限定 dimensionId == 0(只从主世界加载),AtlasDataHandler / MarkersDataHandler 无此限定。
单机模式下 GlobalMarkersDataHandler 的注释指出客户端与服务器共享同一个实例,所以封包同步是冗余的;onClientConnectedToServer 在连远程服务器时把 data 置 null,避免上一个世界的全局标记被带过来。
ItemAtlas.onUpdate 里都有 && !data.isEmpty() / && !markers.isEmpty() 前置条件——空图册不同步,所以拿到一本全新的空白图册时客户端不会收到任何包,地图自然是空的。
广播方法
PacketDispatcher 提供 5 个发送封装,实际用到的有:
| 方法 | 用途 |
|---|---|
sendToAll(IMessage) |
DeleteMarkerPacket / AddMarkerPacket / PutBiomeTilePacket / RegisterTileIdPacket 的响应广播 |
sendTo(IMessage, EntityPlayerMP) |
所有定向同步 |
sendToDimension(IMessage, int) |
已定义,本 mod 未调用 |
sendToAllAround(IMessage, EntityPlayer, double) |
已定义,本 mod 未调用 |
sendToServer(IMessage) |
已定义,本 mod 未调用——客户端请求统一走 AbstractMessage.onMessage 自动路由 |
相关条目
- 地图数据模型 - 封包里搬的 NBT 结构与
WorldSavedData键名 - 标记系统 -
MarkersPacket/AddMarkerPacket/DeleteMarkerPacket的两端 - 群系检测 -
TilesPacket/TileNameIDPacket/RegisterTileIdPacket/DeleteCustomGlobalTilePacket的两端 - Antique Atlas -
BrowsingPositionPacket与同步触发的起点 - 空白 Antique Atlas - 触发首次同步的前提