时装护甲数据文件

基本信息

属性 值
文件名 <UUID>.cosarmor
所在目录 <存档目录>/playerdata/
格式 Gzip 压缩的 NBT(CompressedStreamTools.writeCompressed / readCompressed)
顶层键 CosArmor.Inventory(NBTTagList)、CosArmor.Inventory.Size(int)
条目键 Slot(byte)、isSkinArmor(boolean)
不是 原版 playerdata/<UUID>.dat——数据不写入玩家存档 NBT,而是独立文件
写盘时机 玩家登出、服务端关闭、PlayerEvent.SaveToFile
读盘时机 玩家登录(PlayerEvent.LoadFromFile)、缓存首次加载

存档目录的解析在 InventoryManager.getSavesDirectory():客户端取 server.getFile("saves") 下的当前世界目录名,专用服务端取 server.getFile(server.getFolderName())。

NBT 结构

InventoryCosArmor.writeToNBT() 遍历全部槽位(含空槽),因此文件长度恒定:

根 compound
├── int    "CosArmor.Inventory.Size" = 4
└── list   "CosArmor.Inventory" (type 10)
     ├── compound { byte "Slot" = 0, bool "isSkinArmor" = ..., [ItemStack 字段] }
     ├── compound { byte "Slot" = 1, ... }
     ├── compound { byte "Slot" = 2, ... }
     └── compound { byte "Slot" = 3, ... }

空槽只写 Slot 与 isSkinArmor 两个键;非空槽额外调用 ItemStack.writeToNBT(invSlot) 把 id / Count / tag 写进同一个 compound(与原版容器 NBT 格式一致),因此时装物品的附魔、耐久、自定义 NBT 全部保留。

读取与一个真实缺陷

readFromNBT() 里有两处值得注意:

  1. stacks = new ItemStack[compound.getInteger("CosArmor.Inventory.Size")]——数组长度完全由文件里的 Size 字段决定,不是硬编码 4。读取后 getSizeInventory() 返回的也是这个长度。
  2. 循环内 int j = invSlot.getByte("Slot") & 255; 之后直接 stacks[j] = stack;,没有边界检查。如果文件被外部工具改坏(Size 小于某个 Slot 值,例如 Size=2 而 Slot=9),会抛 ArrayIndexOutOfBoundsException。正常由本 mod 写出的文件不会出现该情况。

另外 isSkinArmor = new boolean[stacks.length] 在读完后整体重建,所以旧文件若缺少 isSkinArmor 键,getBoolean 返回 false——等价于关闭皮肤模式,向后兼容。

缓存与生命周期

服务端用一个 Guava LoadingCache<UUID, InventoryCosArmor>(InventoryManager.cache)持有物品栏实例,getCosArmorInventory(uuid) 用 cache.getUnchecked(uuid) 取用。CacheLoader.load() 内部会先 forceLoad() 读盘,读失败则退化为空物品栏并打印堆栈,不会中断调用。

事件 处理
PlayerEvent.LoadFromFile readFromNBT 读盘;遇 IOException 打印错误并 cache.refresh(uuid) 重建;FileNotFoundException 被静默忽略(新玩家无文件是正常路径)
PlayerEvent.SaveToFile writeToNBT + 写盘;IOException 只打印
PlayerLoggedInEvent 广播同步(见 网络封包),随后 markClean()
PlayerLoggedOutEvent forceSave() 写盘 → cache.invalidate(uuid) 逐出缓存
PlayerTickEvent (START) isDirty() 时广播并 markClean()(不在此写盘)
PlayerDropsEvent 死亡掉落,见下
FMLServerStartingEvent onServerStarting() → cache.invalidateAll()
FMLServerStoppingEvent onServerStopping() → 遍历 cache.asMap().keySet() 全部 forceSave() 后 invalidateAll()

写盘不依赖定时器——只在登出、SaveToFile 事件和关服时发生。PlayerTickEvent 只做网络广播。因此关服异常导致的数据丢失窗口存在于最后一次改动到下次写盘之间。

客户端用独立的 Map<UUID, InventoryCosArmor> cacheClient(普通 HashMap),computeIfAbsent 惰性创建,ClientDisconnectionFromServerEvent 时 clear()。服务端的 getCosArmorInventoryClient() 直接 throw new UnsupportedOperationException(),防止服务端误调。

keepInventory 与死亡掉落

InventoryManager.handleEvent(PlayerDropsEvent) 实现了标准的死亡掉落补偿,触发条件为三者同时成立:

  1. event.entityPlayer instanceof EntityPlayerMP
  2. !event.entityPlayer.worldObj.isRemote(在服务端)
  3. !worldObj.getGameRules().getGameRuleBooleanValue("keepInventory")(keepInventory 为 false)

命中后逐槽生成 EntityItem:

属性 值
生成位置 (posX, posY + player.getEyeHeight(), posZ)
delayBeforeCanPickup 40 tick(2 秒)
motionX / motionZ ∓sin(θ) × f 与 cos(θ) × f,f = rand × 0.5F,θ = rand × 2π
motionY 0.2

随后 inv.setInventorySlotContents(i, null) 清空槽位并 markDirty()。

结论:

  • keepInventory = false(默认)→ 时装护甲掉在死亡地点附近,需在 2 秒后拾取;超时则被烧掉或被他人捡走。
  • keepInventory = true → 此处理被完全跳过,时装护甲原样保留。
  • 掉落走的是 event.drops,与原版护甲的掉落分离但并行(原版护甲由 EntityPlayer.dropInventory 负责)。

相关条目