时装护甲数据文件
基本信息
| 属性 | 值 |
|---|---|
| 文件名 | <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() 里有两处值得注意:
stacks = new ItemStack[compound.getInteger("CosArmor.Inventory.Size")]——数组长度完全由文件里的Size字段决定,不是硬编码 4。读取后getSizeInventory()返回的也是这个长度。- 循环内
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) 实现了标准的死亡掉落补偿,触发条件为三者同时成立:
event.entityPlayer instanceof EntityPlayerMP!event.entityPlayer.worldObj.isRemote(在服务端)!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负责)。