网络封包协议
基本信息
| 属性 | 值 |
|---|---|
| 通道 | serverutils.lib.net.NetworkWrapper.newWrapper("sharedprospecting") |
| 注册入口 | SPNetwork.init(),由 CommonProxy.preInit 在 preInit 阶段调用 |
| 封包总数 | 6(3 条 C→S、3 条 S→C) |
| 编码 | serverutils.lib.io.DataIn / DataOut(多数用 VarInt/VarLong 变长编码) |
| 判别号 | 由 NetworkWrapper 按 register() 调用顺序分配(0~5) |
| 附带通道 | 矿脉/流体增量数据不走本通道,走 VP 自己的 VP.network |
注册顺序
// SPNetwork.init() — 顺序即判别号顺序
NET.register(new MessageUpdateRevision()); // 0 S→C
NET.register(new MessageSendExistingData()); // 1 C→S
NET.register(new MessageRequestUpdate()); // 2 C→S
NET.register(new MessageSendDepleted()); // 3 C→S
NET.register(new MessageUpdateDepletions()); // 4 S→C
NET.register(new MessageUpdateSingleDepleted()); // 5 S→C
这是本 mod 唯一注册的网络内容。 SharedProspectingMod 的 init / postInit / serverStarting / serverStopping 四个事件处理器全部是空实现(CommonProxy 中),只有 preInit 调了 SPNetwork.init()。没有 SimpleNetworkWrapper、没有自己的 channel handler、没有频道字符串常量。
封包总表
| # | 类 | 方向 | 基类 | 字段 | 处理入口 |
|---|---|---|---|---|---|
| 0 | MessageUpdateRevision |
S→C | MessageToClient |
String teamId、int dim、long revision |
ClientRevision.updateRevision |
| 1 | MessageSendExistingData |
C→S | MessageToServer |
int dimension、LongList oreVeins、LongList undergroundFluids |
SPTeamData.addOreVeins(dim, …) + addUndergroundFluids(dim, …) |
| 2 | MessageRequestUpdate |
C→S | MessageToServer |
long revision |
SPTeamData.sendUpdate(player, revision) |
| 3 | MessageSendDepleted |
C→S | MessageToServer |
int dimension、Long2BooleanMap depletedVeins |
SPTeamData.updateDepletedVeins(dim, …) |
| 4 | MessageUpdateDepletions |
S→C | MessageToClient |
int dimension、LongList depletedVeins |
ClientRevision.updateDepletions |
| 5 | MessageUpdateSingleDepleted |
S→C | MessageToClient |
int dimension、long depletedVein、boolean isDepleted |
ClientRevision.setDepleted |
每条封包都覆写 getWrapper() 返回 SPNetwork.NET,客户端侧 onMessage 都带 @SideOnly(Side.CLIENT)。
逐条字段
0. MessageUpdateRevision(S→C)
唯一使用定长写入的封包(writeString / writeInt / writeLong,不是 VarInt):
String teamId
int dim
long revision
teamId 来自 team.getUIDCode()。clearClientRevision 复用同一条封包发 ("", 0, 0) 作为"清空客户端"信号(见 版本号同步机制)。客户端侧 updateRevision 会因 teamId 不匹配而清空整张 dimRevision 表,再因 team.isEmpty() 提前返回。
1. MessageSendExistingData(C→S)
VarInt dimension
VarInt oreCount + oreCount × VarLong
VarInt fluidCount + fluidCount × VarLong
客户端上传本地已有的矿脉/流体键。每批最多 1500 个 long:
// we can fit roughly 4000 longs in a client packet, split data at 1500 per to be 1000% safe
if (oreVeins.size() < 1500 && undergroundFluids.size() < 1500) {
new MessageSendExistingData(cache.dimensionId, oreVeins, undergroundFluids).sendToServer();
} else {
List<LongList> oreChunks = partitionList(oreVeins, 1500);
List<LongList> fluidChunks = partitionList(undergroundFluids, 1500);
for (int i = 0; i < Math.max(oreChunks.size(), fluidChunks.size()); i++) {
LongList ores = i < oreChunks.size() ? oreChunks.get(i) : new LongArrayList();
LongList fluids = i < fluidChunks.size() ? fluidChunks.get(i) : new LongArrayList();
new MessageSendExistingData(cache.dimensionId, ores, fluids).sendToServer();
}
}
partitionList 用 list.subList(i, end) 切片,两个列表按索引对齐发送,较短的补空 LongArrayList——所以尾批可能是一条"只有矿脉没有流体"或反之的包。1500 是源码注释里"4000 大约能塞进一个客户端包,再切到 1500 保险"的经验值。
2. MessageRequestUpdate(C→S)
long revision
只有 1 个字段,是整条链里最小的包。客户端把本地已知版本号报给服务端,服务端据此算差量。revision 可能为 0(首次见到该维度)。
3. MessageSendDepleted(C→S)
VarInt dimension
VarInt count
count × ( VarLong key + boolean value )
维度字段有哨兵值语义。单参构造:
public MessageSendDepleted(Long2BooleanMap depletedVeins) {
this(Integer.MIN_VALUE, depletedVeins);
}
Integer.MIN_VALUE 表示"用收包玩家当前所在维度",服务端在 onMessage 里还原:
data.updateDepletedVeins(dimension == Integer.MIN_VALUE ? player.dimension : dimension, depletedVeins);
两个发送方:Mixin 清单 的 MixinOreVeinPosition 用单参构造(玩家随手挖空一条,走 Long2BooleanMaps.singleton),ClientRevision.sendFullDepletion() 用双参构造(逐维度全量上报,走 Long2BooleanOpenHashMap)。
注意是 Long2BooleanMap 而不是单纯的 key 列表——键值对结构支持表达"未枯竭",尽管当前两个调用方实际只放 true。
4. MessageUpdateDepletions(S→C)
VarInt dimension
VarInt count
count × VarLong key
只有 key,没有布尔值——语义是"这批 key 都是已枯竭",客户端 updateDepletions 用 depletedVeins.contains(key) 做全量覆盖判定。这与 C→S 方向的双向 map 不对称。
发送时机:玩家换维度时(PlayerEvent.PlayerChangedDimensionEvent)补发目标维度的全部枯竭记录。
5. MessageUpdateSingleDepleted(S→C)
VarInt dimension
VarLong depletedVein
boolean isDepleted
单条精确更新。isDepleted 双向——既能标枯竭也能取消枯竭。触发条件在服务端 updateDepletedVeins:
if (depletedVeins.size() == 1) {
for (EntityPlayerMP player : team.getOnlineMembers()) {
if (player.dimension == dim) new MessageUpdateSingleDepleted(dim, entry.getLongKey(), depleted).sendTo(player);
}
}
即只有当整批恰好 1 条时才走单条广播;批量全量上报(sendFullDepletion)不触发这条,因为服务端不会把全量数据再回传给同一个队伍。
矿脉/流体的增量不走本通道
同步给客户端的矿脉与流体由 VP 自己的封包承载:
// SPTeamData.sendUpdate()
VP.network.sendTo(new ProspectingNotification(oreVeins, fluids), player);
ProspectingNotification 是 Visual Prospecting 的类,走它的通道。本 mod 只负责算出差量(getOresForRevision / getFluidsForRevision)并构造这个对象,不自己定义数据封包。
因此分工是:
| 数据 | 通道 | 由谁发出 |
|---|---|---|
| 版本号 | sharedprospecting |
本 mod |
| 差量请求 | sharedprospecting |
本 mod |
| 本地数据上传 | sharedprospecting |
本 mod |
| 枯竭状态同步 | sharedprospecting |
本 mod |
| 矿脉/流体增量 | VP 通道 | VP |
| 矿脉枯竭的单条上报 | sharedprospecting |
本 mod(MixinOreVeinPosition) |
权限与校验
所有 6 条封包都没有任何权限检查、距离校验或限流。 服务端 onMessage 一律先 SPTeamData.get(player),为 null(无队伍)直接 return——这是唯一的准入判断。之后收到的坐标一律直接采信:
MessageSendExistingData的矿脉键会走OrderedDimCache.putOreVein,但该方法会用ServerCache.instance.getOreVein(...)反查 VP 服务端缓存,查不到(EMPTY_VEIN)或类型为VeinType.NO_VEIN就拒绝。所以凭空伪造的坐标无法入库。undergroundFluids路径没有这层校验——putUndergroundFluid只做contains判重,客户端可以上传任意坐标键。但这些键只用于"哪些区块要重新探一遍"的列表,实际流体数据在发送时由服务端重新扫描世界得出,所以伪造不出不存在的流体。MessageSendDepleted的 key 同样不受校验,但只能改变枯竭显示状态,不影响矿脉本身是否存在。
相关条目
- 版本号同步机制 - 封包 0 与封包 2 的完整时序
- 队伍数据模型 - 服务端处理入口
- OrderedDimCache - 差量区间与坐标键
- Mixin 清单 - 封包 3 的触发点
- 持久化与 NBT 结构 -
dimRevision落盘