队伍数据模型 SPTeamData

基本信息

属性 值
源码类 com.rune580.sharedprospecting.database.SPTeamData
基类 serverutils.lib.data.TeamData
事件订阅 @EventBusSubscriber(无 side 参数 → 双端)
唯一数据字段 Int2ObjectMap<OrderedDimCache> dimensions(fastutil Int2ObjectOpenHashMap)
注册键 getId() 返回 "sharedprospecting",即 team.getData().get(MOD_ID) 的键
条目数 0(无方块 / 物品 / 实体 / 指令)

什么是"队伍"

本 mod 不自己实现队伍系统,完全寄生在 ServerUtilities 的队伍抽象上:

  • 队伍类型是 serverutils.lib.data.ForgeTeam,通过 team.getUIDCode() 拿到字符串形式的队伍标识。
  • 取玩家所属队伍:Universe.get().getPlayer(player).team。
  • 关键判定:SPTeamData.get(EntityPlayerMP) 里如果 team.type.equals(TeamType.NONE) 就返回 null。也就是说不在任何 ServerUtilities 队伍里的玩家视为"无队伍",本 mod 对其不做任何处理(不会报错,也不会建缓存)。
  • 队伍成员 = team.getOnlineMembers(),返回在线的 EntityPlayerMP 集合。

由于没有自定义字段,本 mod 的"队伍数据"实际上就是一个按维度 ID 索引的矿脉坐标表——队伍名、颜色、成员名单、权限等全部由 ServerUtilities 自己保管,本 mod 既不读也不写。

数据字段

@Getter
private final Int2ObjectMap<OrderedDimCache> dimensions = new Int2ObjectOpenHashMap<>();

除这一个字段外,SPTeamData 不持有任何其它实例状态。dimensions 是 final,只能通过 computeIfAbsent(dim, OrderedDimCache::new) 惰性扩容。

每个 OrderedDimCache 的内容见 OrderedDimCache。

事件挂钩

SPTeamData 内以 @SubscribeEvent 挂了 8 个事件,构成队伍数据的完整生命周期:

事件 优先级 处理
ForgeTeamDataEvent 默认 event.register(new SPTeamData(event.getTeam())) — 把自己注册为该 mod 在队伍上的数据槽
ForgeTeamLoadedEvent 默认 NBTUtils.readNBT(team.getDataFile("sharedprospecting")) → load(nbt)
ForgeTeamSavedEvent 默认 NBTUtils.writeNBTSafe(team.getDataFile("sharedprospecting"), save())
ForgeTeamDeletedEvent 默认 FileUtils.delete(team.getDataFile("sharedprospecting"))
ForgeTeamPlayerJoinedEvent 默认 updateMemberRevision(player) — 立刻把当前维度版本号推给新成员
ForgeTeamPlayerLeftEvent 默认 clearClientRevision(player) — 发一条 teamId = "" 的空版本包,让客户端清表
PlayerEvent.PlayerLoggedInEvent EventPriority.LOWEST updatePlayer(playerMP);不在队伍则 clearClientRevision
PlayerEvent.PlayerChangedDimensionEvent 默认 updatePlayer(playerMP) + 向该维度发一次 MessageUpdateDepletions

两个事件都带 event.player.worldObj.isRemote 早退守卫,只在服务端执行。登录事件用 LOWEST 优先级,是为了等其它 mod 改完队伍归属后再判定本玩家是否有队伍数据。

版本推送的两个入口

updateMemberRevision(EntityPlayerMP player) — 组队的主动推送:

int dim = player.dimension;
new MessageUpdateRevision(team.getUIDCode(), dim, getDimRevision(dim)).sendTo(player);

注意它推的是该玩家自己当前所在维度的版本号,不是玩家加入时的维度。

updateMembers(IntSet modifiedDims) — 数据变更后的广播:

if (modifiedDims.isEmpty()) return;
team.markDirty();
for (EntityPlayerMP player : team.getOnlineMembers()) {
    if (modifiedDims.contains(player.dimension)) {
        updateMemberRevision(player);
    }
}

只在"变更维度集合"里且"玩家当前就在该维度"的成员才会收到推送。 队员待在主世界时同事在下界挖到矿,下界队员会收到版本号更新而在下界的队员不会——这是有意的按维度隔离,不是 bug。team.markDirty() 保证下次存档把变更刷盘。

成员变更的入口

  • addOreVeins(Collection<OreVeinPosition>) / addUndergroundFluids(Collection<UndergroundFluidPosition>):从 VP 回调拿到的绝对位置对象(走 Mixin 清单),按 oreVein.dimensionId 分组后调 updateMembers。
  • addOreVeins(int dim, LongList) / addUndergroundFluids(int dim, LongList):客户端上传的已压位键(走 网络封包协议),用 IntSets.singleton(dim)。
  • putOreVein / putUndergroundFluids:单条写入,转发给 OrderedDimCache 做去重。

旧数据迁移

load() 的第一件事就是 importOldData(),在读新 NBT 之前执行。迁移源是旧版按队伍分目录的缓存,路径在本仓库里硬编码为相对路径:

sharedprospecting/teams/<team.getUIDCode()>/

流程是匿名子类 + 反射,不是直接读 NBT:

  1. new File(SharedProspectingMod.MOD_ID + "/teams/", team.getUIDCode()),不存在则直接返回。
  2. 造一个匿名 WorldCache,覆写 protected File getStorageDirectory() 返回该目录(这个覆写能生效正是因为 WorldCacheAccessor 提供的 @Invoker 语义在本类里是直接的方法重写)。
  3. 强转为 WorldCacheAccessor 后 callGetStorageDirectory().exists() 再确认一次。
  4. tempCache.loadVeinCache(WorldIdHandler.getWorldId()) 载入 VP 旧格式。
  5. ReflectionHelper.findField(WorldCache.class, "dimensions") 反射拿 Map<Integer, DimensionCache>(源码注释:仅迁移时执行一次,不必缓存字段)。
  6. 遍历每个 DimensionCache 的 getAllOreVeins() / getAllUndergroundFluids() 灌进新结构。
  7. FileUtils.delete(oldFile) 删掉旧目录 + team.markDirty()。

迁移只做一次:旧目录删掉后第 1 步的 exists() 检查就短路了。反射失败时只 LOG.error("Failed to get dimension cache map", e) 并返回,不会崩服。

相关条目