feat: 实现无限存储元件的临时文件写入 + 原子替换

This commit is contained in:
C-H716 2025-09-19 20:49:13 +08:00
parent 572b6e6f17
commit 1279031535
3 changed files with 83 additions and 91 deletions

View File

@ -3,7 +3,6 @@ package com.extendedae_plus;
import appeng.api.storage.StorageCells; import appeng.api.storage.StorageCells;
import appeng.menu.locator.MenuLocators; import appeng.menu.locator.MenuLocators;
import com.extendedae_plus.ae.api.storage.InfinityBigIntegerCellHandler; import com.extendedae_plus.ae.api.storage.InfinityBigIntegerCellHandler;
import com.extendedae_plus.ae.api.storage.InfinityBigIntegerCellInventory;
import com.extendedae_plus.client.ClientRegistrar; import com.extendedae_plus.client.ClientRegistrar;
import com.extendedae_plus.config.ModConfig; import com.extendedae_plus.config.ModConfig;
import com.extendedae_plus.init.*; import com.extendedae_plus.init.*;
@ -53,9 +52,6 @@ public class ExtendedAEPlus {
// 注册到Forge事件总线 // 注册到Forge事件总线
MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_BUS.register(this);
MinecraftForge.EVENT_BUS.addListener(ExtendedAEPlus::onLevelLoad); MinecraftForge.EVENT_BUS.addListener(ExtendedAEPlus::onLevelLoad);
// 注册每秒合并持久化队列的事件监听Server tick end + stopping
MinecraftForge.EVENT_BUS.addListener(InfinityBigIntegerCellInventory::onServerTick);
MinecraftForge.EVENT_BUS.addListener(InfinityBigIntegerCellInventory::onServerStopping);
// 注册通用配置 // 注册通用配置
ModConfig.init(); ModConfig.init();
} }

View File

@ -17,14 +17,11 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag; import net.minecraft.nbt.ListTag;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
@ -48,8 +45,6 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
// 待持久化队列用于 debounce在服务器 tick 中合并持久化 // 待持久化队列用于 debounce在服务器 tick 中合并持久化
private static final ConcurrentLinkedQueue<InfinityBigIntegerCellInventory> PENDING_PERSIST = new ConcurrentLinkedQueue<>(); private static final ConcurrentLinkedQueue<InfinityBigIntegerCellInventory> PENDING_PERSIST = new ConcurrentLinkedQueue<>();
// 用于按 tick 计数 20 tick 1 触发一次合并写入
private static int TICK_COUNTER = 0;
// 数字格式化对象保留两位小数复用以减少对象分配 // 数字格式化对象保留两位小数复用以减少对象分配
private static final DecimalFormat DF = new DecimalFormat("#.##"); private static final DecimalFormat DF = new DecimalFormat("#.##");
@ -137,13 +132,13 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
// 判断物品堆栈是否有UUID // 判断物品堆栈是否有UUID
public boolean hasUUID() { public boolean hasUUID() {
return this.stack.hasTag() && this.stack.getOrCreateTag().contains("uuid"); return stack.hasTag() && stack.getOrCreateTag().contains("uuid");
} }
// 获取物品堆栈的UUID // 获取物品堆栈的UUID
public UUID getUUID() { public UUID getUUID() {
if (this.hasUUID()) { if (this.hasUUID()) {
return this.stack.getOrCreateTag().getUUID("uuid"); return stack.getOrCreateTag().getUUID("uuid");
} else { } else {
return null; return null;
} }
@ -151,17 +146,17 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
// 获取或初始化存储映射 // 获取或初始化存储映射
private Object2ObjectMap<AEKey, BigInteger> getCellStoredMap() { private Object2ObjectMap<AEKey, BigInteger> getCellStoredMap() {
if (this.storedMap == null) { if (storedMap == null) {
this.storedMap = new Object2ObjectOpenHashMap<>(); storedMap = new Object2ObjectOpenHashMap<>();
this.loadCellStoredMap(); this.loadCellStoredMap();
} }
return this.storedMap; return storedMap;
} }
// 从存储中加载物品映射 // 从存储中加载物品映射
private void loadCellStoredMap() { private void loadCellStoredMap() {
boolean corruptedTag = false; // 标记数据是否损坏 boolean corruptedTag = false; // 标记数据是否损坏
if (!this.stack.hasTag()) if (!stack.hasTag())
return; return;
ListTag keys = this.getCellStorage().keys; ListTag keys = this.getCellStorage().keys;
ListTag amounts = this.getCellStorage().amounts; ListTag amounts = this.getCellStorage().amounts;
@ -200,38 +195,12 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
// 标记数据需要保存并通知容器或直接持久化 // 标记数据需要保存并通知容器或直接持久化
private void saveChanges() { private void saveChanges() {
// 标记为未持久化交由容器或延迟任务合并写入以减少 I/O // 标记为未持久化交由容器或延迟任务合并写入以减少 I/O
this.isPersisted = false; isPersisted = false;
// 将本实例加入待处理队列去重 if (container != null) {
if (!PENDING_PERSIST.contains(this)) { // 当存在容器时优先让容器统一处理持久化
PENDING_PERSIST.add(this); container.saveChanges();
} } else {
if (this.container != null) { persist();
// 当存在容器时仍然通知容器以便 AE2 在合并时回调 persist()
this.container.saveChanges();
}
}
/**
* 每个服务器 tick 调用一次用于按秒合并并执行待持久化项的 persist()
*/
public static void onServerTick(TickEvent.ServerTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
TICK_COUNTER++;
if (TICK_COUNTER % 20 != 0) return; // 20 tick 1 执行一次
onServerStopping(null);
}
/**
* 在服务器停止时强制刷新所有待持久化项
*/
public static void onServerStopping(ServerStoppingEvent event) {
InfinityBigIntegerCellInventory inv;
while ((inv = PENDING_PERSIST.poll()) != null) {
try {
inv.persist();
} catch (Exception e) {
e.printStackTrace();
}
} }
} }
@ -282,10 +251,10 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
// 如果存储为空移除UUID和全局存储中的数据并清理缓存的 types/total // 如果存储为空移除UUID和全局存储中的数据并清理缓存的 types/total
if (hasUUID()) { if (hasUUID()) {
InfinityStorageManager.INSTANCE.removeCell(getUUID()); InfinityStorageManager.INSTANCE.removeCell(getUUID());
if (this.stack.getTag() != null) { if (stack.getTag() != null) {
this.stack.getTag().remove("uuid"); stack.getTag().remove("uuid");
this.stack.getTag().remove("types"); stack.getTag().remove("types");
this.stack.getTag().remove("total"); stack.getTag().remove("total");
} }
initData(); initData();
} }
@ -311,36 +280,34 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
if (keys.isEmpty()) { if (keys.isEmpty()) {
InfinityStorageManager.INSTANCE.updateCell(this.getUUID(), new InfinityDataStorage()); InfinityStorageManager.INSTANCE.updateCell(this.getUUID(), new InfinityDataStorage());
// 清理缓存 // 清理缓存
if (this.stack.getTag() != null) { if (stack.getTag() != null) {
this.stack.getTag().remove("types"); stack.getTag().remove("types");
this.stack.getTag().remove("total"); stack.getTag().remove("total");
} }
} else { } else {
// amounts 现在为 CompoundTag 列表 // amounts 现在为 CompoundTag 列表
InfinityStorageManager.INSTANCE.modifyCell(this.getUUID(), keys, amountTags); InfinityStorageManager.INSTANCE.modifyCell(this.getUUID(), keys, amountTags);
// 缓存类型数量与总量到 ItemStack NBT避免每次 tooltip 或展示时重新统计 // 缓存类型数量与总量到 ItemStack NBT避免每次 tooltip 或展示时重新统计
try { try {
if (this.stack.getTag() == null) { if (stack.getTag() == null) stack.setTag(new CompoundTag());
this.stack.setTag(new CompoundTag());
}
int typesCount = keys.size(); int typesCount = keys.size();
this.stack.getOrCreateTag().putInt("types", typesCount); stack.getOrCreateTag().putInt("types", typesCount);
BigInteger total = BigInteger.ZERO; java.math.BigInteger total = java.math.BigInteger.ZERO;
for (Map.Entry<AEKey, BigInteger> e : map.object2ObjectEntrySet()) { for (java.util.Map.Entry<AEKey, java.math.BigInteger> e : map.object2ObjectEntrySet()) {
BigInteger v = e.getValue(); java.math.BigInteger v = e.getValue();
if (v.compareTo(BigInteger.ZERO) > 0) { if (v.compareTo(java.math.BigInteger.ZERO) > 0) {
total = total.add(v); total = total.add(v);
} }
} }
if (total.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0) { if (total.compareTo(java.math.BigInteger.valueOf(Long.MAX_VALUE)) <= 0) {
this.stack.getOrCreateTag().putLong("total", total.longValue()); stack.getOrCreateTag().putLong("total", total.longValue());
} else { } else {
this.stack.getOrCreateTag().putString("total", total.toString()); stack.getOrCreateTag().putString("total", total.toString());
} }
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
this.isPersisted = true; isPersisted = true;
} }
// 插入物品到存储单元 // 插入物品到存储单元
@ -356,7 +323,7 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
} }
// 如果没有UUID生成UUID并初始化存储延迟创建全局存储以避免在 manager 未就绪时 NPE // 如果没有UUID生成UUID并初始化存储延迟创建全局存储以避免在 manager 未就绪时 NPE
if (!this.hasUUID()) { if (!this.hasUUID()) {
this.stack.getOrCreateTag().putUUID("uuid", UUID.randomUUID()); stack.getOrCreateTag().putUUID("uuid", UUID.randomUUID());
InfinityStorageManager.INSTANCE.getOrCreateCell(getUUID()); InfinityStorageManager.INSTANCE.getOrCreateCell(getUUID());
// 确保 storedMap 初始化并从持久层加载数据 // 确保 storedMap 初始化并从持久层加载数据
loadCellStoredMap(); loadCellStoredMap();
@ -403,4 +370,16 @@ public class InfinityBigIntegerCellInventory implements StorageCell {
} }
return 0; return 0;
} }
// 获取存储单元内所有物品的总数量格式化字符串
public String getTotalStorage() {
// 使用缓存的 totalStored避免每次全表扫描
BigInteger total = BigInteger.ZERO;
for (BigInteger value : getCellStoredMap().values()) {
if (value.compareTo(BigInteger.ZERO) > 0) {
total = total.add(value);
}
}
return formatBigInteger(total);
}
} }

View File

@ -1,6 +1,5 @@
package com.extendedae_plus.util.storage; package com.extendedae_plus.util.storage;
import com.extendedae_plus.ExtendedAEPlus;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag; import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtIo; import net.minecraft.nbt.NbtIo;
@ -13,13 +12,15 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.AtomicMoveNotSupportedException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* InfinityStorageManager * InfinityStorageManager
* <p> *
* 替代之前基于 SavedData 的实现本类使用手动文件 I/O world 目录下保存 NBT 数据 * 替代之前基于 SavedData 的实现本类使用手动文件 I/O world 目录下保存 NBT 数据
* 以避免依赖 Minecraft SavedData 机制 * 以避免依赖 Minecraft SavedData 机制
* 数据保持与之前兼容的 NBT 结构 Compound 包含 "list" => ListTag of Compound { uuid, data } * 数据保持与之前兼容的 NBT 结构 Compound 包含 "list" => ListTag of Compound { uuid, data }
@ -28,16 +29,15 @@ public class InfinityStorageManager {
public static final String FILE_NAME = "eap_infinity_biginteger_cells.dat"; public static final String FILE_NAME = "eap_infinity_biginteger_cells.dat";
/** /** 全局单例,由 mod 在 world load 时初始化 */
* 全局单例 mod world load 时初始化
*/
public static volatile InfinityStorageManager INSTANCE = new InfinityStorageManager(); public static volatile InfinityStorageManager INSTANCE = new InfinityStorageManager();
private final Map<UUID, InfinityDataStorage> cells = new HashMap<>(); private final Map<UUID, InfinityDataStorage> cells = new HashMap<>();
private Path saveFilePath = null; private Path saveFilePath = null;
private InfinityStorageManager() {} public InfinityStorageManager() {
}
/** /**
* 初始化并从 world 保存目录加载数据若文件不存在则保持空状态 * 初始化并从 world 保存目录加载数据若文件不存在则保持空状态
@ -47,17 +47,15 @@ public class InfinityStorageManager {
try { try {
File worldFolder = serverLevel.getServer().getWorldPath(LevelResource.ROOT).toFile(); File worldFolder = serverLevel.getServer().getWorldPath(LevelResource.ROOT).toFile();
// 保存到 world/<modid>/ 文件夹下避免与其它 mod 冲突 // 保存到 world/<modid>/ 文件夹下避免与其它 mod 冲突
File modDir = new File(worldFolder, ExtendedAEPlus.MODID); File modDir = new File(worldFolder, "data");
if (!modDir.exists()) { if (!modDir.exists()) modDir.mkdirs();
modDir.mkdirs(); saveFilePath = new File(modDir, FILE_NAME).toPath();
} if (Files.exists(saveFilePath)) {
this.saveFilePath = new File(modDir, FILE_NAME).toPath(); CompoundTag root = NbtIo.readCompressed(saveFilePath.toFile());
if (Files.exists(this.saveFilePath)) {
CompoundTag root = NbtIo.readCompressed(this.saveFilePath.toFile());
ListTag cellList = root.getList("list", Tag.TAG_COMPOUND); ListTag cellList = root.getList("list", Tag.TAG_COMPOUND);
for (int i = 0; i < cellList.size(); i++) { for (int i = 0; i < cellList.size(); i++) {
CompoundTag cell = cellList.getCompound(i); CompoundTag cell = cellList.getCompound(i);
this.cells.put(cell.getUUID("uuid"), InfinityDataStorage.loadFromNBT(cell.getCompound("data"))); cells.put(cell.getUUID("uuid"), InfinityDataStorage.loadFromNBT(cell.getCompound("data")));
} }
} }
} catch (IOException e) { } catch (IOException e) {
@ -70,40 +68,58 @@ public class InfinityStorageManager {
* 保存当前内存数据到文件会覆盖已有文件 * 保存当前内存数据到文件会覆盖已有文件
*/ */
public synchronized void saveToFile() { public synchronized void saveToFile() {
if (this.saveFilePath == null) if (saveFilePath == null) return;
return;
try { try {
CompoundTag root = new CompoundTag(); CompoundTag root = new CompoundTag();
ListTag cellList = new ListTag(); ListTag cellList = new ListTag();
for (Map.Entry<UUID, InfinityDataStorage> entry : this.cells.entrySet()) { for (Map.Entry<UUID, InfinityDataStorage> entry : cells.entrySet()) {
// 跳过可能的 null key防止写入时 NPE
if (entry.getKey() == null || entry.getValue() == null) continue;
CompoundTag cell = new CompoundTag(); CompoundTag cell = new CompoundTag();
cell.putUUID("uuid", entry.getKey()); cell.putUUID("uuid", entry.getKey());
cell.put("data", entry.getValue().serializeNBT()); cell.put("data", entry.getValue().serializeNBT());
cellList.add(cell); cellList.add(cell);
} }
root.put("list", cellList); root.put("list", cellList);
// 使用压缩写入以节省空间 // 使用压缩写入到临时文件然后原子替换目标文件以避免半成品/0字节文件
NbtIo.writeCompressed(root, this.saveFilePath.toFile()); Path tmp = saveFilePath.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, saveFilePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException ex) {
// 若底层文件系统不支持原子移动退回到非原子替换
Files.move(tmp, saveFilePath, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
public void updateCell(UUID uuid, InfinityDataStorage infinityDataStorage) { public void updateCell(UUID uuid, InfinityDataStorage infinityDataStorage) {
this.cells.put(uuid, infinityDataStorage); if (uuid == null) return; // 忽略无效 UUID
cells.put(uuid, infinityDataStorage);
saveToFile(); saveToFile();
} }
public InfinityDataStorage getOrCreateCell(UUID uuid) { public InfinityDataStorage getOrCreateCell(UUID uuid) {
if (!this.cells.containsKey(uuid)) { if (uuid == null) {
return InfinityDataStorage.EMPTY;
}
if (!cells.containsKey(uuid)) {
InfinityDataStorage newCell = new InfinityDataStorage(); InfinityDataStorage newCell = new InfinityDataStorage();
this.cells.put(uuid, newCell); cells.put(uuid, newCell);
saveToFile(); saveToFile();
} }
return this.cells.get(uuid); return cells.get(uuid);
} }
public void modifyCell(UUID cellID, ListTag stackKeys, ListTag stackAmounts) { public void modifyCell(UUID cellID, ListTag stackKeys, ListTag stackAmounts) {
if (cellID == null) return;
InfinityDataStorage cellToModify = getOrCreateCell(cellID); InfinityDataStorage cellToModify = getOrCreateCell(cellID);
if (stackKeys != null && stackAmounts != null) { if (stackKeys != null && stackAmounts != null) {
cellToModify.keys = stackKeys; cellToModify.keys = stackKeys;
@ -113,7 +129,8 @@ public class InfinityStorageManager {
} }
public void removeCell(UUID uuid) { public void removeCell(UUID uuid) {
this.cells.remove(uuid); if (uuid == null) return;
cells.remove(uuid);
saveToFile(); saveToFile();
} }
} }