网络消息

全部通信走 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 - 触发首次同步的前提