From ba79d0e31a885665a2380daef5ee6a0803ff9d0a Mon Sep 17 00:00:00 2001 From: C-H716 <1536152356@qq.com> Date: Sat, 20 Sep 2025 02:32:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E5=9B=9E=E9=80=80=E6=97=A0=E9=99=90?= =?UTF-8?q?=E7=A3=81=E7=9B=98=E4=BF=AE=E6=94=B9=E8=87=B3=E4=B8=8A=E4=B8=80?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E5=86=8D=E6=AC=A1=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InfinityBigIntegerCellInventory.java | 92 +++++++++++++++---- .../util/storage/InfinityDataStorage.java | 6 +- .../util/storage/InfinityStorageManager.java | 18 +++- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/extendedae_plus/ae/api/storage/InfinityBigIntegerCellInventory.java b/src/main/java/com/extendedae_plus/ae/api/storage/InfinityBigIntegerCellInventory.java index 2cbb417..e678dfc 100644 --- a/src/main/java/com/extendedae_plus/ae/api/storage/InfinityBigIntegerCellInventory.java +++ b/src/main/java/com/extendedae_plus/ae/api/storage/InfinityBigIntegerCellInventory.java @@ -16,6 +16,8 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.ItemStack; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.server.ServerStoppingEvent; @@ -23,7 +25,6 @@ import net.minecraftforge.event.server.ServerStoppingEvent; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; -import java.text.DecimalFormat; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; @@ -48,8 +49,7 @@ public class InfinityBigIntegerCellInventory implements StorageCell { // 待持久化队列(用于 debounce:在服务器 tick 中合并持久化) private static final ConcurrentLinkedQueue PENDING_PERSIST = new ConcurrentLinkedQueue<>(); - // 数字格式化对象,保留两位小数(复用以减少对象分配) - private static final DecimalFormat DF = new DecimalFormat("#.##"); + // 数字格式化对象:改为方法局部以避免静态非线程安全问题 // 关联的 ItemStack(含可能的 uuid NBT) private final ItemStack stack; @@ -106,6 +106,17 @@ public class InfinityBigIntegerCellInventory implements StorageCell { // 在服务器停止时被调用,立即强制持久化队列中的所有实例 public static void onServerStopping(ServerStoppingEvent event) { + // 尝试在服务端停止流程开始时初始化 SavedData(确保 INSTANCE 可用以便后续持久化) + try { + MinecraftServer server = event.getServer(); + if (server != null) { + for (ServerLevel level : server.getAllLevels()) { + InfinityStorageManager.getForLevel(level); + } + } + } catch (Throwable ignored) { + } + InfinityBigIntegerCellInventory inv; while ((inv = PENDING_PERSIST.poll()) != null) { try { @@ -116,7 +127,7 @@ public class InfinityBigIntegerCellInventory implements StorageCell { LOGGER.info("InfinityBigIntegerCellInventory onServerStopping error1: {}", ignored.getMessage()); } } - // 额外尝试将全局存储管理器标记为脏以确保 SavedData 被写回(在单人模式下可能直接由系统触发) + // 将全局存储管理器标记为脏以确保 SavedData 被写回 try { var stor = getStorageInstance(); if (stor != null) stor.setDirty(); @@ -127,7 +138,8 @@ public class InfinityBigIntegerCellInventory implements StorageCell { // 将 BigInteger 格式化为带单位的字符串,保留两位小数 public static String formatBigInteger(BigInteger number) { - // 使用局部 DF(非线程安全),但 Minecraft 通常在主线程运行 + // 使用方法局部的 DecimalFormat,避免静态共享的非线程安全问题 + java.text.DecimalFormat df = new java.text.DecimalFormat("#.##"); BigDecimal bd = new BigDecimal(number); BigDecimal thousand = new BigDecimal(1000); String[] units = new String[]{"", "K", "M", "G", "T", "P", "E", "Z", "Y"}; @@ -139,17 +151,23 @@ public class InfinityBigIntegerCellInventory implements StorageCell { if (idx == 0) { return bd.setScale(0, RoundingMode.DOWN).toPlainString(); } - return DF.format(bd.doubleValue()) + units[idx]; + return df.format(bd.doubleValue()) + units[idx]; } // 获取当前存储单元的数据存储对象 private InfinityDataStorage getCellStorage() { if (this.getUUID() == null) { - // 如果没有UUID,返回空存储 - return InfinityDataStorage.EMPTY; + // 如果没有UUID,返回空存储(返回新实例以避免共享可变状态) + return InfinityDataStorage.empty(); } else { + // 在访问全局 SavedData 之前做防御性检查,避免在客户端或尚未初始化的情况下 NPE + InfinityStorageManager mgr = getStorageInstance(); + if (mgr == null) { + // SavedData 尚未就绪,视为空存储(返回新实例以避免共享可变状态) + return InfinityDataStorage.empty(); + } // 否则获取或创建对应UUID的存储 - return getStorageInstance().getOrCreateCell(getUUID()); + return mgr.getOrCreateCell(getUUID()); } } @@ -200,8 +218,9 @@ public class InfinityBigIntegerCellInventory implements StorageCell { private void loadCellStoredMap() { boolean corruptedTag = false; // 标记数据是否损坏 if (!stack.hasTag()) return; - ListTag keys = this.getCellStorage().keys; - ListTag amounts = this.getCellStorage().amounts; + InfinityDataStorage storage = this.getCellStorage(); + ListTag keys = storage.keys; + ListTag amounts = storage.amounts; int len = Math.min(keys.size(), amounts.size()); for (int i = 0; i < len; i++) { AEKey key = AEKey.fromTagGeneric(keys.getCompound(i)); @@ -233,6 +252,12 @@ public class InfinityBigIntegerCellInventory implements StorageCell { if (corruptedTag) { this.saveChanges(); } + // 打印加载后的摘要日志 + try { + var uuid = this.getUUID(); + LOGGER.info("Loaded cell {}: types={}, totalCached={}", uuid, this.getCellStoredMap().size(), this.getTotalStorage()); + } catch (Throwable ignored) { + } } // 标记数据需要保存,并通知容器或直接持久化 @@ -289,16 +314,32 @@ public class InfinityBigIntegerCellInventory implements StorageCell { public void persist() { if (this.isPersisted) return; + // 若全局存储管理器尚未就绪,重新入队并在未来 tick 重试 + InfinityStorageManager mgr = getStorageInstance(); + if (mgr == null) { + try { + var id = this.getUUID(); + LOGGER.info("Persist deferred for cell {}: storage manager not ready", id); + } catch (Throwable ignored) { + } + if (!PENDING_PERSIST.contains(this)) { + PENDING_PERSIST.offer(this); + } + return; + } + Object2ObjectMap map = this.getCellStoredMap(); if (map.isEmpty()) { // 如果存储为空,移除UUID和全局存储中的数据 if (this.hasUUID()) { - getStorageInstance().removeCell(getUUID()); + mgr.removeCell(getUUID()); if (stack.getTag() != null) { stack.getTag().remove("uuid"); // 移除缓存的 total 字段 stack.getTag().remove("total"); } + // 标记为已持久化,避免重复尝试 + isPersisted = true; } return; } @@ -320,10 +361,10 @@ public class InfinityBigIntegerCellInventory implements StorageCell { } // 如果没有Key,更新为空存储,否则保存数据 if (keys.isEmpty()) { - getStorageInstance().updateCell(this.getUUID(), new InfinityDataStorage()); + mgr.updateCell(this.getUUID(), new InfinityDataStorage()); } else { // amounts 现在为 CompoundTag 列表 - getStorageInstance().modifyCell(this.getUUID(), keys, amountTags); + mgr.modifyCell(this.getUUID(), keys, amountTags); } // 将缓存的 totalStored 同步到 ItemStack 的 NBT,优先使用 long if (stack.getOrCreateTag() != null) { @@ -337,6 +378,12 @@ public class InfinityBigIntegerCellInventory implements StorageCell { stack.getOrCreateTag().putInt("types", typesCount); } isPersisted = true; + // 打印持久化摘要日志 + try { + var uuid = this.getUUID(); + LOGGER.info("Persisted cell {}: types={}, totalCached={}", uuid, this.getCellStoredMap().size(), this.getTotalStorage()); + } catch (Throwable ignored) { + } } // 插入物品到存储单元 @@ -348,12 +395,19 @@ public class InfinityBigIntegerCellInventory implements StorageCell { // 不允许存储无限单元自身 if (what instanceof AEItemKey itemKey && itemKey.getItem() instanceof InfinityBigIntegerCellItem) return 0; - // 如果没有UUID,生成UUID并初始化存储 + // 如果没有UUID,尝试在服务器端且存储管理器已就绪时生成UUID并初始化存储 if (!this.hasUUID()) { - stack.getOrCreateTag().putUUID("uuid", UUID.randomUUID()); - getStorageInstance().getOrCreateCell(getUUID()); - // 确保 storedMap 初始化并从持久层加载数据 - this.getCellStoredMap(); + InfinityStorageManager mgr = getStorageInstance(); + // 仅在 SavedData 已就绪(通常在服务器端)时创建 UUID 并注册持久化存储 + if (mgr != null) { + stack.getOrCreateTag().putUUID("uuid", UUID.randomUUID()); + mgr.getOrCreateCell(getUUID()); + // 确保 storedMap 初始化并从持久层加载数据 + this.getCellStoredMap(); + } else { + // SavedData 未就绪:把插入作为本地内存操作(不生成 UUID,不持久化),确保 storedMap 初始化以容纳临时数据 + this.getCellStoredMap(); + } } Object2ObjectMap map = this.getCellStoredMap(); BigInteger currentAmount = map.getOrDefault(what, BigInteger.ZERO); diff --git a/src/main/java/com/extendedae_plus/util/storage/InfinityDataStorage.java b/src/main/java/com/extendedae_plus/util/storage/InfinityDataStorage.java index 4ef9036..c56e421 100644 --- a/src/main/java/com/extendedae_plus/util/storage/InfinityDataStorage.java +++ b/src/main/java/com/extendedae_plus/util/storage/InfinityDataStorage.java @@ -18,8 +18,10 @@ import net.minecraft.nbt.Tag; */ public class InfinityDataStorage { - /** 空实例(表示没有数据) */ - public static final InfinityDataStorage EMPTY = new InfinityDataStorage(); + /** 空实例的访问器(返回新实例以避免共享可变状态) */ + public static InfinityDataStorage empty() { + return new InfinityDataStorage(); + } /** 序列化的键列表(NBT ListTag,元素为 CompoundTag) */ public ListTag keys; diff --git a/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java b/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java index e24a5ab..f6a31a9 100644 --- a/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java +++ b/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java @@ -10,6 +10,8 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; +import static com.extendedae_plus.util.ExtendedAELogger.LOGGER; + /** * InfinityStorageManager *

@@ -21,6 +23,10 @@ import java.util.UUID; */ public class InfinityStorageManager extends SavedData { + // 当前磁盘格式版本号,增加字段用于向后/向前兼容 + private static final int FORMAT_VERSION = 1; + + /** * SavedData 文件名常量 */ @@ -42,10 +48,18 @@ public class InfinityStorageManager extends SavedData { * 从 NBT 构造:用于在世界加载时从存档恢复数据 */ public InfinityStorageManager(CompoundTag nbt) { + // 读取格式版本,缺省视为 1(兼容旧档) + int version = nbt.contains("format_version") ? nbt.getInt("format_version") : 1; + LOGGER.info("Loading InfinityStorageManager format_version={}", version); + ListTag cellList = nbt.getList("list", CompoundTag.TAG_COMPOUND); for (int i = 0; i < cellList.size(); i++) { CompoundTag cell = cellList.getCompound(i); - cells.put(cell.getUUID("uuid"), InfinityDataStorage.loadFromNBT(cell.getCompound("data"))); + java.util.UUID uuid = cell.getUUID("uuid"); + CompoundTag dataTag = cell.getCompound("data"); + InfinityDataStorage data = InfinityDataStorage.loadFromNBT(dataTag); + cells.put(uuid, data); + LOGGER.info("Loaded InfinityDataStorage for uuid {}: keys={}, amounts={}", uuid, data.keys.size(), data.amounts.size()); } setDirty(); } @@ -71,6 +85,8 @@ public class InfinityStorageManager extends SavedData { cellList.add(cell); } nbt.put("list", cellList); + // 写入当前格式版本号,便于未来迁移与兼容判断 + nbt.putInt("format_version", FORMAT_VERSION); return nbt; } From bc3b280fdb88eef2d2a528854344b57f4b0173c5 Mon Sep 17 00:00:00 2001 From: C-H716 <1536152356@qq.com> Date: Sat, 20 Sep 2025 10:11:36 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9=E5=89=8D?= =?UTF-8?q?fix=E7=89=88=E6=9C=ACdat=E7=9A=84=E8=AF=BB=E5=8F=96=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../util/storage/InfinityStorageManager.java | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java b/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java index f6a31a9..24bbe28 100644 --- a/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java +++ b/src/main/java/com/extendedae_plus/util/storage/InfinityStorageManager.java @@ -2,10 +2,18 @@ package com.extendedae_plus.util.storage; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.storage.LevelResource; import org.jetbrains.annotations.NotNull; +import java.io.File; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -50,7 +58,14 @@ public class InfinityStorageManager extends SavedData { public InfinityStorageManager(CompoundTag nbt) { // 读取格式版本,缺省视为 1(兼容旧档) int version = nbt.contains("format_version") ? nbt.getInt("format_version") : 1; - LOGGER.info("Loading InfinityStorageManager format_version={}", version); + if (!nbt.contains("format_version")) { + // 旧档未包含 format_version,标记为需要写回以便下次保存写入版本号 + LOGGER.info("Found legacy InfinityStorageManager dat without format_version; treating as version {} and marking dirty for upgrade", version); + // 立即标记为脏以触发尽快写盘(升级写入 format_version) + this.setDirty(); + } else { + LOGGER.info("Loading InfinityStorageManager format_version={}", version); + } ListTag cellList = nbt.getList("list", CompoundTag.TAG_COMPOUND); for (int i = 0; i < cellList.size(); i++) { @@ -69,6 +84,53 @@ public class InfinityStorageManager extends SavedData { */ public static InfinityStorageManager getForLevel(ServerLevel level) { if (INSTANCE == null && level != null) { + // 迁移逻辑:若旧的压缩 dat 文件存在且缺少 format_version,则读取并补上 version,备份原文件 + try { + Path dataDir = level.getServer().getWorldPath(new LevelResource("data")); + Path filePath = dataDir.resolve(FILE_NAME + ".dat"); + File oldFile = filePath.toFile(); + if (oldFile.exists()) { + // 检测是否为 gzip(压缩 NBT) + boolean isGzip = false; + try (var fis = Files.newInputStream(filePath)) { + int b1 = fis.read(); + int b2 = fis.read(); + isGzip = (b1 == 0x1F && b2 == 0x8B); + } catch (IOException ignored) { + } + if (isGzip) { + try { + var root = NbtIo.readCompressed(oldFile); + if (!root.contains("format_version")) { + // 备份旧文件 + Path bak = filePath.resolveSibling(FILE_NAME + ".dat.bak"); + try { + Files.copy(filePath, bak); + } catch (IOException ignored) { + } + // 写回并加入 format_version + root.putInt("format_version", FORMAT_VERSION); + Path tmp = filePath.resolveSibling(FILE_NAME + ".tmp"); + File tmpFile = tmp.toFile(); + if (tmpFile.getParentFile() != null && !tmpFile.getParentFile().exists()) { + tmpFile.getParentFile().mkdirs(); + } + NbtIo.writeCompressed(root, tmpFile); + try { + Files.move(tmp, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException ex) { + Files.move(tmp, filePath, StandardCopyOption.REPLACE_EXISTING); + } + LOGGER.info("Migrated old compressed {} to include format_version", filePath); + } + } catch (IOException ex) { + LOGGER.info("Failed to migrate old compressed file {}: {}", filePath, ex.getMessage()); + } + } + } + } catch (Throwable ignored) { + } + INSTANCE = level.getDataStorage().computeIfAbsent(InfinityStorageManager::new, InfinityStorageManager::new, FILE_NAME); } return INSTANCE;