网络封包协议

基本信息

属性 值
通道 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 同样不受校验,但只能改变枯竭显示状态,不影响矿脉本身是否存在。

相关条目