队伍数据模型 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:
new File(SharedProspectingMod.MOD_ID + "/teams/", team.getUIDCode()),不存在则直接返回。- 造一个匿名
WorldCache,覆写protected File getStorageDirectory()返回该目录(这个覆写能生效正是因为 WorldCacheAccessor 提供的@Invoker语义在本类里是直接的方法重写)。 - 强转为
WorldCacheAccessor后callGetStorageDirectory().exists()再确认一次。 tempCache.loadVeinCache(WorldIdHandler.getWorldId())载入 VP 旧格式。ReflectionHelper.findField(WorldCache.class, "dimensions")反射拿Map<Integer, DimensionCache>(源码注释:仅迁移时执行一次,不必缓存字段)。- 遍历每个
DimensionCache的getAllOreVeins()/getAllUndergroundFluids()灌进新结构。 FileUtils.delete(oldFile)删掉旧目录 +team.markDirty()。
迁移只做一次:旧目录删掉后第 1 步的 exists() 检查就短路了。反射失败时只 LOG.error("Failed to get dimension cache map", e) 并返回,不会崩服。
相关条目
- OrderedDimCache - 本类唯一字段的值类型
- 版本号同步机制 -
updateMemberRevision/updateMembers推的是什么 - 持久化与 NBT 结构 -
load/save的字段结构与旧格式兼容 - 网络封包协议 - 本类发起的 3 条服务端封包
- Mixin 清单 - 矿脉位置从哪注入进本类