diff --git a/.gitignore b/.gitignore index c6db4838..cf724bbc 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,8 @@ replay_pid* # Maven build directory target/ -.git-versioned-pom.xml \ No newline at end of file +.git-versioned-pom.xml +dependency-reduced-pom.xml + +# Addon Files +/NetworksExpansion/ \ No newline at end of file diff --git a/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java b/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java index 67a031d8..67541c42 100644 --- a/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java +++ b/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java @@ -57,6 +57,7 @@ public final class SlimeAEPlugin extends JavaPlugin implements SlimefunAddon { private SlimeAECommand slimeAECommand = new SlimeAECommand(); private PinnedManager pinnedManager; private Metrics metrics; + private static boolean debug = false; @Override public void onEnable() { @@ -317,12 +318,23 @@ public static PinnedManager getPinnedManager() { return getInstance().pinnedManager; } + /** + * 判断是否开启了调试模式 + * @return 是否开启调试模式 + */ + public static boolean isDebug() { + return debug; + } + public void reloadConfig0() { // 保存默认配置 saveDefaultConfig(); reloadConfig(); + // 重载调试模式配置 + debug = getConfig().getBoolean("debug", false); + // 重载网络配置 NetworkInfo.reloadConfig(); diff --git a/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java b/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java index 2b82e1d6..fbdcfdbf 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java @@ -26,6 +26,7 @@ import me.ddggdd135.slimeae.api.interfaces.*; import me.ddggdd135.slimeae.api.items.ItemRequest; import me.ddggdd135.slimeae.api.items.ItemStorage; +import me.ddggdd135.slimeae.api.items.StorageCollection; import me.ddggdd135.slimeae.core.NetworkInfo; import me.ddggdd135.slimeae.core.items.MenuItems; import me.ddggdd135.slimeae.utils.ItemUtils; @@ -66,13 +67,57 @@ public AutoCraftingTask(@Nonnull NetworkInfo info, @Nonnull CraftingRecipe recip List newSteps = null; List usingSteps; + // 强制使存储缓存失效,确保获取最新数据快照 + // (防止 dispose 归还物品后 F3 缓存仍保存旧数据) + info.getStorage().invalidateStorageCache(); + info.getStorage().clearNotIncluded(); + + // === 调试日志 === + java.util.logging.Logger debugLog = SlimeAEPlugin.getInstance().getLogger(); + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] === 开始创建合成任务 ==="); + debugLog.info("[AutoCraft-Debug] 配方输出: " + ItemUtils.getItemName(recipe.getOutput()[0]) + " x" + count); + debugLog.info("[AutoCraft-Debug] recipe在getRecipes中: " + info.getRecipes().contains(recipe)); + debugLog.info("[AutoCraft-Debug] getRecipes大小: " + info.getRecipes().size()); + ItemHashMap debugSnapshot = info.getStorage().getStorageUnsafe(); + for (Map.Entry de : debugSnapshot.keyEntrySet()) { + if (de.getValue() > 0) { + debugLog.info("[AutoCraft-Debug] 存储内容: " + ItemUtils.getItemName(de.getKey().getItemStack()) + " = " + de.getValue()); + } + } + } + // === 调试日志结束 === + try { newSteps = calcCraftSteps(recipe, count, new ItemStorage(info.getStorage())); if (checkCraftStepsValid(newSteps, new ItemStorage(info.getStorage()))) usingSteps = newSteps; else throw new IllegalStateException("新版算法出错,退回旧版算法"); } catch (Exception ignored) { // 新版算法出错,退回旧版算法 - usingSteps = match(recipe, count, new ItemStorage(info.getStorage())); + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] 新版算法异常: " + ignored.getClass().getSimpleName() + " - " + ignored.getMessage()); + if (ignored instanceof NoEnoughMaterialsException nee) { + debugLog.info("[AutoCraft-Debug] 新版算法缺失材料:"); + for (Map.Entry me : nee.getMissingMaterials().entrySet()) { + debugLog.info("[AutoCraft-Debug] " + ItemUtils.getItemName(me.getKey()) + " x " + me.getValue()); + } + } + debugLog.info("[AutoCraft-Debug] 回退到旧版算法 match()"); + } + try { + usingSteps = match(recipe, count, new ItemStorage(info.getStorage())); + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] 旧版算法成功,步骤数: " + usingSteps.size()); + } + } catch (NoEnoughMaterialsException matchEx) { + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] 旧版算法也失败了! 缺失材料:"); + for (Map.Entry me : matchEx.getMissingMaterials().entrySet()) { + debugLog.info("[AutoCraft-Debug] " + ItemUtils.getItemName(me.getKey()) + " x " + me.getValue()); + } + } + throw matchEx; + } } craftingSteps = usingSteps; @@ -137,14 +182,20 @@ private List match(CraftingRecipe recipe, long count, ItemStorage sto try { if (!info.getRecipes().contains(recipe)) { - // 记录直接缺少的材料 + // 配方不可用,记录所有所需材料作为缺失 ItemStorage missing = new ItemStorage(); ItemHashMap in = recipe.getInputAmounts(); - for (ItemStack template : in.keySet()) { - long amount = storage.getStorageUnsafe().getOrDefault(template, 0L); - long need = in.get(template) * count; + for (ItemKey key : in.sourceKeySet()) { + long amount = storage.getStorageUnsafe().getOrDefault(key, 0L); + long need = in.getKey(key) * count; if (amount < need) { - missing.addItem(new ItemKey(template), need - amount); + missing.addItem(key, need - amount); + } + } + // 如果所有材料够用但配方不可用,仍然报告所有输入为缺失 + if (missing.getStorageUnsafe().isEmpty()) { + for (ItemKey key : in.sourceKeySet()) { + missing.addItem(key, in.getKey(key) * count); } } throw new NoEnoughMaterialsException(missing.getStorageUnsafe()); @@ -268,14 +319,20 @@ private void calcCraftStep(CraftingRecipe recipe, long count, ItemStorage storag try { if (!info.getRecipes().contains(recipe)) { - // 记录直接缺少的材料 + // 配方不可用,记录所有所需材料作为缺失 ItemStorage missing = new ItemStorage(); ItemHashMap in = recipe.getInputAmounts(); - for (ItemStack template : in.keySet()) { - long amount = storage.getStorageUnsafe().getOrDefault(template, 0L); - long need = in.get(template) * count; + for (ItemKey key : in.sourceKeySet()) { + long amount = storage.getStorageUnsafe().getOrDefault(key, 0L); + long need = in.getKey(key) * count; if (amount < need) { - missing.addItem(new ItemKey(template), need - amount); + missing.addItem(key, need - amount); + } + } + // 如果所有材料够用但配方不可用,仍然报告所有输入为缺失 + if (missing.getStorageUnsafe().isEmpty()) { + for (ItemKey key : in.sourceKeySet()) { + missing.addItem(key, in.getKey(key) * count); } } throw new NoEnoughMaterialsException(missing.getStorageUnsafe()); @@ -284,11 +341,32 @@ private void calcCraftStep(CraftingRecipe recipe, long count, ItemStorage storag ItemStorage missing = new ItemStorage(); ItemHashMap in = recipe.getInputAmounts(); + java.util.logging.Logger cLog = SlimeAEPlugin.getInstance().getLogger(); + if (SlimeAEPlugin.isDebug()) { + cLog.info("[AutoCraft-Debug] calcCraftStep: 配方=" + ItemUtils.getItemName(recipe.getOutput()[0]) + " count=" + count); + cLog.info("[AutoCraft-Debug] calcCraftStep: storage快照大小=" + storage.getStorageUnsafe().size() + ", input种类=" + in.size()); + } + // 遍历所需材料 for (ItemKey key : in.sourceKeySet()) { long amount = storage.getStorageUnsafe().getOrDefault(key, 0L); long need = in.getKey(key) * count; + if (SlimeAEPlugin.isDebug()) { + cLog.info("[AutoCraft-Debug] calcCraftStep: 材料=" + ItemUtils.getItemName(key.getItemStack()) + + " amount(存储中)=" + amount + " need=" + need + + " keyHash=" + key.hashCode()); + // 额外检查:遍历 storage 的 key 看看是否有"同物品但不同hash"的情况 + for (Map.Entry se : storage.getStorageUnsafe().keyEntrySet()) { + if (se.getValue() > 0) { + boolean eq = key.equals(se.getKey()); + cLog.info("[AutoCraft-Debug] storage key: " + ItemUtils.getItemName(se.getKey().getItemStack()) + + "=" + se.getValue() + " hash=" + se.getKey().hashCode() + + " equals=" + eq); + } + } + } + if (amount >= need) { storage.takeItem(new ItemRequest(key, need)); } else { @@ -299,6 +377,10 @@ private void calcCraftStep(CraftingRecipe recipe, long count, ItemStorage storag // 尝试合成缺少的材料 CraftingRecipe craftingRecipe = getRecipe(key.getItemStack()); + if (SlimeAEPlugin.isDebug()) { + cLog.info("[AutoCraft-Debug] calcCraftStep: 尝试子合成 " + ItemUtils.getItemName(key.getItemStack()) + + " craftingRecipe=" + (craftingRecipe != null ? "found" : "null")); + } if (craftingRecipe == null) { missing.addItem(new ItemKey(key.getItemStack()), remainingNeed); continue; @@ -330,6 +412,9 @@ private void calcCraftStep(CraftingRecipe recipe, long count, ItemStorage storag // 如果有缺少的材料就抛出异常 if (!missing.getStorageUnsafe().isEmpty()) { + if (SlimeAEPlugin.isDebug()) { + cLog.info("[AutoCraft-Debug] calcCraftStep: 缺失材料! 将抛出异常"); + } throw new NoEnoughMaterialsException(missing.getStorageUnsafe()); } } finally { @@ -608,7 +693,64 @@ public void dispose() { Bukkit.getPluginManager().callEvent(e); info.getAutoCraftingSessions().remove(this); - info.getTempStorage().addItem(storage.getStorageUnsafe(), true); + + // 把材料直接推回 StorageCollection(而非通过 tempStorage 间接转移), + // 确保物品立即回到真实存储中,避免以下问题: + // 1. tempStorage 的 addItem 与 updateTempStorage 的 takeItem 并发竞态导致物品丢失或不可见 + // 2. getStorageUnsafe() 缓存 (F3) 过期导致快照中不含已归还物品 + // 3. notIncluded 负面缓存导致 takeItem/contains 错误跳过已归还物品 + ItemHashMap toReturn = new ItemHashMap<>(storage.getStorageUnsafe()); + + // === 调试日志 === + java.util.logging.Logger debugLog = SlimeAEPlugin.getInstance().getLogger(); + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] === dispose() 开始归还物品 ==="); + for (Map.Entry de : toReturn.keyEntrySet()) { + if (de.getValue() > 0) { + debugLog.info("[AutoCraft-Debug] 待归还: " + ItemUtils.getItemName(de.getKey().getItemStack()) + " = " + de.getValue()); + } + } + } + // === 调试日志结束 === + + StorageCollection currentStorage = info.getStorage(); + currentStorage.clearNotIncluded(); + currentStorage.clearTakeAndPushCache(); + currentStorage.pushItem(toReturn); + // pushItem 会将剩余量写回 toReturn 的 entry 中, + // 推不进去的物品放入 tempStorage 作为后备 + ItemUtils.trim(toReturn); + + // === 调试日志 === + if (!toReturn.isEmpty()) { + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] pushItem后仍有剩余(将放入tempStorage):"); + for (Map.Entry de : toReturn.keyEntrySet()) { + debugLog.info("[AutoCraft-Debug] 剩余: " + ItemUtils.getItemName(de.getKey().getItemStack()) + " = " + de.getValue()); + } + } + info.getTempStorage().addItem(toReturn, true); + } else { + if (SlimeAEPlugin.isDebug()) { + debugLog.info("[AutoCraft-Debug] 所有物品已成功推回存储"); + } + } + // === 调试日志结束 === + + currentStorage.invalidateStorageCache(); + + // === 调试日志: 验证归还后存储 === + if (SlimeAEPlugin.isDebug()) { + currentStorage.invalidateStorageCache(); + ItemHashMap afterReturn = currentStorage.getStorageUnsafe(); + debugLog.info("[AutoCraft-Debug] 归还后存储快照:"); + for (Map.Entry de : afterReturn.keyEntrySet()) { + if (de.getValue() > 0) { + debugLog.info("[AutoCraft-Debug] " + ItemUtils.getItemName(de.getKey().getItemStack()) + " = " + de.getValue()); + } + } + debugLog.info("[AutoCraft-Debug] === dispose() 完成 ==="); + } Bukkit.getScheduler() .runTask(SlimeAEPlugin.getInstance(), () -> menu.getInventory().close()); diff --git a/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftingRecipe.java b/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftingRecipe.java index 4b58fa8d..22df36b8 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftingRecipe.java +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftingRecipe.java @@ -6,14 +6,13 @@ import me.ddggdd135.guguslimefunlib.api.ItemHashMap; import me.ddggdd135.slimeae.utils.CraftItemStackUtils; import me.ddggdd135.slimeae.utils.ItemUtils; +import org.bukkit.Material; import org.bukkit.inventory.ItemStack; public class CraftingRecipe { private final CraftType craftType; private final ItemStack[] input; private final ItemStack[] output; - private ItemHashMap inputAmounts; - private ItemHashMap outputAmounts; public CraftingRecipe(@Nonnull CraftType craftType, @Nonnull ItemStack[] input, @Nonnull ItemStack[] output) { this.craftType = craftType; @@ -42,16 +41,32 @@ public ItemStack[] getOutput() { @Nonnull public ItemHashMap getInputAmounts() { - if (inputAmounts == null) inputAmounts = ItemUtils.getAmounts(input); - - return inputAmounts; + return ItemUtils.getAmounts(toBukkitCopy(input)); } @Nonnull public ItemHashMap getOutputAmounts() { - if (outputAmounts == null) outputAmounts = ItemUtils.getAmounts(output); + return ItemUtils.getAmounts(toBukkitCopy(output)); + } - return outputAmounts; + /** + * 将 CraftItemStack 数组转换为纯 Bukkit ItemStack 数组。 + * 避免 ItemKey 内部持有 CraftItemStack 的 NMS 引用, + * 防止 NMS 对象被外部操作修改导致 ItemKey.equals() 失效。 + */ + private static ItemStack[] toBukkitCopy(ItemStack[] craftStacks) { + ItemStack[] result = new ItemStack[craftStacks.length]; + for (int i = 0; i < craftStacks.length; i++) { + ItemStack cs = craftStacks[i]; + if (cs == null || cs.getType().isAir()) { + result[i] = new ItemStack(Material.AIR); + } else { + // 使用 new ItemStack(cs) 创建纯 Bukkit ItemStack 副本, + // 与 CraftItemStack 完全解耦 + result[i] = new ItemStack(cs); + } + } + return result; } @Override @@ -67,8 +82,26 @@ public boolean equals(Object o) { @Override public int hashCode() { int result = Objects.hash(craftType); - result = 31 * result + Arrays.hashCode(input); - result = 31 * result + Arrays.hashCode(output); + result = 31 * result + stableArrayHashCode(input); + result = 31 * result + stableArrayHashCode(output); return result; } + + /** + * 计算 ItemStack 数组的稳定 hashCode。 + * 对 AIR 类型统一使用固定 hashCode(与 equals 中 AIR amount=0 的处理保持一致), + * 避免因 CraftItemStack 的可变 amount 导致 hashCode 不稳定。 + */ + private static int stableArrayHashCode(ItemStack[] array) { + if (array == null) return 0; + int h = 1; + for (ItemStack item : array) { + if (item == null || item.getType().isAir()) { + h = 31 * h; // AIR/null 统一为 0 + } else { + h = 31 * h + item.hashCode(); + } + } + return h; + } } diff --git a/src/main/java/me/ddggdd135/slimeae/api/items/MEStorageCellCache.java b/src/main/java/me/ddggdd135/slimeae/api/items/MEStorageCellCache.java index f7e76c7d..bf30b2d7 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/items/MEStorageCellCache.java +++ b/src/main/java/me/ddggdd135/slimeae/api/items/MEStorageCellCache.java @@ -143,7 +143,7 @@ public void pushItem(@Nonnull ItemInfo itemInfo) { stored += toAdd; storageData.setStored(stored); storages.putKey(key, amount + toAdd); - itemInfo.setAmount((int) (itemInfo.getAmount() - toAdd)); + itemInfo.setAmount(itemInfo.getAmount() - toAdd); SlimeAEPlugin.getStorageCellStorageDataController().markDirty(storageData); trim(key); } diff --git a/src/main/java/me/ddggdd135/slimeae/api/items/StorageCollection.java b/src/main/java/me/ddggdd135/slimeae/api/items/StorageCollection.java index 1b37f697..6c1b791c 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/items/StorageCollection.java +++ b/src/main/java/me/ddggdd135/slimeae/api/items/StorageCollection.java @@ -20,6 +20,11 @@ public class StorageCollection implements IStorage { private final Map pushCache; private final ItemHashSet notIncluded; + // === F3: getStorageUnsafe() 结果缓存 === + private volatile ItemHashMap cachedStorage = null; + private volatile long lastCacheTime = 0; + private static final long STORAGE_CACHE_INTERVAL = 500; // 500ms 刷新一次 + public StorageCollection(@Nonnull IStorage... storages) { this.storages = new ConcurrentHashSet<>(); this.takeCache = new HashMap<>(); @@ -38,10 +43,12 @@ public void addStorage(@Nullable IStorage storage) { if (storage == null) return; if (storage instanceof StorageCollection storageCollection) { storages.addAll(storageCollection.getStorages()); + invalidateStorageCache(); return; } storages.add(storage); notIncluded.clear(); + invalidateStorageCache(); } public boolean removeStorage(@Nonnull IStorage storage) { @@ -75,15 +82,23 @@ public boolean removeStorage(@Nonnull IStorage storage) { pushCache.remove(toRemove.getKey()); } - return storages.remove(storage); + boolean removed = storages.remove(storage); + if (removed) invalidateStorageCache(); + return removed; } public void pushItem(@Nonnull ItemStackCache itemStackCache) { ItemStack itemStack = itemStackCache.getItemStack(); ItemKey key = itemStackCache.getItemKey(); + // 物品被推入时,从负面缓存中移除,因为该物品在存储中已有可用量 + if (notIncluded.contains(key)) notIncluded.remove(key); + IStorage pushStorage = pushCache.get(key.getType()); - if (pushStorage != null) pushStorage.pushItem(itemStackCache); + if (pushStorage != null) { + pushStorage.pushItem(itemStackCache); + invalidateStorageCache(); + } if (itemStack.getType().isAir() || itemStack.getAmount() == 0) return; @@ -104,6 +119,7 @@ public void pushItem(@Nonnull ItemStackCache itemStackCache) { for (ObjectIntImmutablePair storage : sorted) { storage.left().pushItem(itemStackCache); pushCache.put(key.getType(), storage.left()); + invalidateStorageCache(); if (itemStack.getType().isAir() || itemStack.getAmount() == 0) return; } } @@ -111,8 +127,14 @@ public void pushItem(@Nonnull ItemStackCache itemStackCache) { public void pushItem(@Nonnull ItemInfo itemInfo) { ItemKey key = itemInfo.getItemKey(); + // 物品被推入时,从负面缓存中移除,因为该物品在存储中已有可用量 + if (notIncluded.contains(key)) notIncluded.remove(key); + IStorage pushStorage = pushCache.get(key.getType()); - if (pushStorage != null) pushStorage.pushItem(itemInfo); + if (pushStorage != null) { + pushStorage.pushItem(itemInfo); + invalidateStorageCache(); + } if (itemInfo.isEmpty()) return; @@ -132,6 +154,7 @@ public void pushItem(@Nonnull ItemInfo itemInfo) { for (ObjectIntImmutablePair storage : sorted) { storage.left().pushItem(itemInfo); + invalidateStorageCache(); if (itemInfo.isEmpty()) return; } } @@ -189,12 +212,46 @@ public ItemStorage takeItem(@Nonnull ItemRequest[] requests) { if (rest.keySet().isEmpty()) break; } notIncluded.addAll(rest.sourceKeySet()); + invalidateStorageCache(); return found; } + /** + * F3: 使存储缓存失效 + */ + public void invalidateStorageCache() { + cachedStorage = null; + } + + /** + * 清除负面缓存(notIncluded 集合)。 + * 当物品通过非 pushItem 路径(如 tempStorage.addItem)归还到子存储后, + * 需要调用此方法以确保后续 takeItem/contains 不会错误地跳过这些物品。 + */ + public void clearNotIncluded() { + notIncluded.clear(); + } + + /** + * 清除 takeCache 和 pushCache 路由缓存。 + * 当存储内容发生重大变化(如 dispose 归还大量物品)时, + * 需要调用此方法以确保后续 takeItem/pushItem 不会使用过时的路由缓存。 + */ + public void clearTakeAndPushCache() { + takeCache.clear(); + pushCache.clear(); + } + @Override public @Nonnull ItemHashMap getStorageUnsafe() { + // F3: 时间窗口内复用缓存结果 + long now = System.currentTimeMillis(); + ItemHashMap cached = cachedStorage; + if (cached != null && (now - lastCacheTime) < STORAGE_CACHE_INTERVAL) { + return cached; + } + ItemHashMap result = new ItemHashMap<>(); for (IStorage storage : storages) { @@ -214,6 +271,9 @@ public ItemStorage takeItem(@Nonnull ItemRequest[] requests) { } } } + + cachedStorage = result; + lastCacheTime = now; return result; } diff --git a/src/main/java/me/ddggdd135/slimeae/core/NetworkData.java b/src/main/java/me/ddggdd135/slimeae/core/NetworkData.java index 7f96bb40..c0f5374a 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/NetworkData.java +++ b/src/main/java/me/ddggdd135/slimeae/core/NetworkData.java @@ -121,6 +121,16 @@ public boolean updateAutoCraft(@Nonnull NetworkInfo info) { } } + // 先计算新的配方缓存快照 + Set newCachedRecipes = new HashSet<>(); + for (Set recipes : newRecipeMap.values()) { + newCachedRecipes.addAll(recipes); + } + + // 原子地替换底层数据:先设置 F9 缓存快照,使 getRecipes() 返回新结果, + // 然后再更新底层数据结构。这避免了 clear()+putAll() 之间的竞态条件。 + info.setRecipeCache(Collections.unmodifiableSet(newCachedRecipes)); + info.getCraftingHolders().clear(); info.getCraftingHolders().addAll(newCraftingHolders); info.getRecipeMap().clear(); diff --git a/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java b/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java index 539cfd79..2cfcac4d 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java +++ b/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java @@ -2,6 +2,7 @@ import io.github.thebusybiscuit.slimefun4.utils.ChestMenuUtils; import java.util.*; +import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -34,12 +35,15 @@ public class NetworkInfo implements IDisposable { private final Map> recipeMap = new ConcurrentHashMap<>(); private final Map virtualCraftingDeviceSpeeds = new ConcurrentHashMap<>(); private final Map virtualCraftingDeviceUsed = new ConcurrentHashMap<>(); - private StorageCollection storage = new StorageCollection(); - private IStorage storageNoNetworks = new StorageCollection(); + private volatile StorageCollection storage = new StorageCollection(); + private volatile IStorage storageNoNetworks = new StorageCollection(); private final ConcurrentHashSet autoCraftingTasks = new ConcurrentHashSet<>(); private final AEMenu autoCraftingMenu = new AEMenu("&e自动合成任务"); private final ItemStorage tempStorage = new ItemStorage(); + // === F9: 配方缓存 === + private volatile Set cachedRecipes = null; + private static int maxCraftingSessions; private static int maxCraftingAmount; @@ -155,6 +159,10 @@ public Map getVirtualCraftingDeviceUsed() { @Nonnull public Set getRecipes() { + // F9: 使用缓存的配方集合 + Set cached = cachedRecipes; + if (cached != null) return cached; + Set recipes = new HashSet<>(); for (Location location : craftingHolders) { // fix NullPointException in here @@ -163,7 +171,25 @@ public Set getRecipes() { recipes.addAll(recipes1); } } - return recipes; + Set result = Collections.unmodifiableSet(recipes); + cachedRecipes = result; + return result; + } + + /** + * 使配方缓存失效,当 recipeMap 或 craftingHolders 发生变化时调用 + */ + public void invalidateRecipeCache() { + cachedRecipes = null; + } + + /** + * 直接设置配方缓存快照。 + * 用于在 updateAutoCraft() 中原子替换底层数据时,先设置新缓存, + * 确保 getRecipes() 在 clear()+putAll() 之间不会返回空集合。 + */ + public void setRecipeCache(@Nonnull Set cache) { + cachedRecipes = cache; } @Nullable public CraftingRecipe getRecipeFor(@Nonnull ItemStack output) { diff --git a/src/main/java/me/ddggdd135/slimeae/core/managers/PinnedManager.java b/src/main/java/me/ddggdd135/slimeae/core/managers/PinnedManager.java index 2bdadace..eb0d8c1c 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/managers/PinnedManager.java +++ b/src/main/java/me/ddggdd135/slimeae/core/managers/PinnedManager.java @@ -7,6 +7,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Nonnull; @@ -30,11 +32,18 @@ public class PinnedManager implements IManager { Slimefun.getDatabaseManager().getProfileDataController(); private final NamespacedKey PINNED_KEY; + // === F6: 置顶物品查询缓存 === + private final ConcurrentHashMap> pinnedCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap pinnedCacheTime = new ConcurrentHashMap<>(); + private static final long PINNED_CACHE_TTL = 3000; // 3秒 TTL + public PinnedManager() { this.PINNED_KEY = new NamespacedKey(SlimeAEPlugin.getInstance(), "pinned"); } public void addPinned(@Nonnull Player player, @Nonnull ItemStack itemStack) { + // F6: 主动使缓存失效 + invalidatePinnedCache(player); PlayerBackpack backpack = getOrCreateBookmarkBackpack(player); if (backpack == null) { return; @@ -44,6 +53,8 @@ public void addPinned(@Nonnull Player player, @Nonnull ItemStack itemStack) { } public void removePinned(@Nonnull Player player, @Nonnull ItemStack itemStack) { + // F6: 主动使缓存失效 + invalidatePinnedCache(player); PlayerBackpack backpack = getOrCreateBookmarkBackpack(player); if (backpack == null) { return; @@ -104,6 +115,40 @@ private void removePinned0(@Nonnull Player player, @Nonnull PlayerBackpack backp } @Nullable public List getPinnedItems(@Nonnull Player player) { + // F6: 检查缓存 + UUID uuid = player.getUniqueId(); + Long cachedTime = pinnedCacheTime.get(uuid); + if (cachedTime != null && (System.currentTimeMillis() - cachedTime) < PINNED_CACHE_TTL) { + List cached = pinnedCache.get(uuid); + if (cached != null) return cached; + } + + List result = getPinnedItemsInternal(player); + + // 缓存结果(包括null,用空列表代替) + pinnedCache.put(uuid, result != null ? result : new ArrayList<>()); + pinnedCacheTime.put(uuid, System.currentTimeMillis()); + return result; + } + + /** + * 使指定玩家的置顶缓存失效 + */ + public void invalidatePinnedCache(@Nonnull Player player) { + UUID uuid = player.getUniqueId(); + pinnedCache.remove(uuid); + pinnedCacheTime.remove(uuid); + } + + /** + * 清空所有置顶缓存 + */ + public void clearPinnedCache() { + pinnedCache.clear(); + pinnedCacheTime.clear(); + } + + @Nullable private List getPinnedItemsInternal(@Nonnull Player player) { PlayerBackpack backpack = getPinnedBackpack(player); if (backpack == null) { return null; @@ -241,7 +286,8 @@ private void operateController(@Nonnull Consumer consumer } } - private @Nullable R operateController(@Nonnull Function function) { + @Nullable + private R operateController(@Nonnull Function function) { if (controller != null) { return function.apply(controller); } diff --git a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MECraftPlanningTerminal.java b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MECraftPlanningTerminal.java index 6aea6962..e8308037 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MECraftPlanningTerminal.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MECraftPlanningTerminal.java @@ -1,6 +1,5 @@ package me.ddggdd135.slimeae.core.slimefun.terminals; -import com.balugaq.jeg.api.groups.SearchGroup; import com.balugaq.jeg.implementation.JustEnoughGuide; import com.xzavier0722.mc.plugin.slimefun4.storage.util.StorageCacheUtils; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; @@ -11,6 +10,7 @@ import io.github.thebusybiscuit.slimefun4.utils.ChestMenuUtils; import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils; import java.util.*; +import java.util.HashSet; import javax.annotation.Nonnull; import me.ddggdd135.guguslimefunlib.api.AEMenu; import me.ddggdd135.guguslimefunlib.libraries.colors.CMIChatColor; @@ -241,9 +241,11 @@ private void filterRecipeEntries(ArrayList entries, Player player, if (filter.isEmpty()) return; if (SlimeAEPlugin.getJustEnoughGuideIntegration().isLoaded()) { boolean isPinyinSearch = JustEnoughGuide.getConfigManager().isPinyinSearch(); - SearchGroup group = new SearchGroup(null, player, filter, isPinyinSearch); - List slimefunItems = group.filterItems(player, filter, isPinyinSearch); - entries.removeIf(entry -> doFilterWithJEG(entry.getItemStack(), slimefunItems, filter)); + // F1: 使用缓存的搜索结果 + List slimefunItemsList = getCachedFilterItems(player, filter, isPinyinSearch); + // F4: List→HashSet 优化 contains 查找为 O(1) + Set slimefunItemSet = new HashSet<>(slimefunItemsList); + entries.removeIf(entry -> doFilterWithJEG(entry.getItemStack(), slimefunItemSet, filter)); } else { entries.removeIf(entry -> doFilterNoJEG(entry.getItemStack(), filter)); } diff --git a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEPatternTerminal.java b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEPatternTerminal.java index 65e2e6a6..67a8c2d0 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEPatternTerminal.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEPatternTerminal.java @@ -210,6 +210,8 @@ public void onBlockBreak(@Nonnull Block b) { blockMenu.dropItems(b.getLocation(), getPatternSlot()); blockMenu.dropItems(b.getLocation(), getPatternOutputSlot()); } + // 清理缓存,防止内存泄漏 + clearSortedItemsCache(b.getLocation()); } }; } diff --git a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/METerminal.java b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/METerminal.java index fd644246..8c9aa379 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/METerminal.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/METerminal.java @@ -2,8 +2,6 @@ import com.balugaq.jeg.api.groups.SearchGroup; import com.balugaq.jeg.implementation.JustEnoughGuide; -import com.github.houbb.pinyin.constant.enums.PinyinStyleEnum; -import com.github.houbb.pinyin.util.PinyinHelper; import com.xzavier0722.mc.plugin.slimefun4.storage.controller.SlimefunBlockData; import com.xzavier0722.mc.plugin.slimefun4.storage.util.StorageCacheUtils; import io.github.thebusybiscuit.slimefun4.api.items.ItemGroup; @@ -18,6 +16,7 @@ import io.github.thebusybiscuit.slimefun4.utils.SlimefunUtils; import java.text.Collator; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.IntStream; import javax.annotation.Nonnull; import javax.annotation.OverridingMethodsMustInvokeSuper; @@ -37,12 +36,14 @@ import me.ddggdd135.slimeae.core.items.SlimeAEItems; import me.ddggdd135.slimeae.core.managers.PinnedManager; import me.ddggdd135.slimeae.utils.ItemUtils; +import me.ddggdd135.slimeae.utils.PinyinCache; import me.mrCookieSlime.CSCoreLibPlugin.general.Inventory.ChestMenu; import me.mrCookieSlime.CSCoreLibPlugin.general.Inventory.ClickAction; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenu; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenuPreset; import net.guizhanss.minecraft.guizhanlib.gugu.minecraft.helpers.inventory.ItemStackHelper; import org.bukkit.ChatColor; +import org.bukkit.Location; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; @@ -61,6 +62,18 @@ public class METerminal extends TickingBlock implements IMEObject, InventoryBloc public static final String SORT_KEY = "sort"; public static final String FILTER_KEY = "filter"; + // === F1: 搜索结果缓存 === + private static final Map> searchCache = new ConcurrentHashMap<>(); + private static final Map searchCacheTime = new ConcurrentHashMap<>(); + private static final long SEARCH_CACHE_TTL = 5000; // 5秒缓存生存期 + private static final int SEARCH_CACHE_MAX_SIZE = 50; // 最多缓存50个不同的filter + + // === F7: 排序结果缓存(每个终端位置一份) === + private static final Map sortedItemsCacheMap = new ConcurrentHashMap<>(); + + // === F8: 每个槽位的上一次显示状态缓存 === + private static final Map displaySlotCacheMap = new ConcurrentHashMap<>(); + public int[] getBorderSlots() { return new int[] {17, 26}; } @@ -129,6 +142,8 @@ public void onBlockBreak(@Nonnull Block b) { if (blockMenu != null) { blockMenu.dropItems(b.getLocation(), getInputSlot()); } + // 清理缓存,防止内存泄漏 + clearSortedItemsCache(b.getLocation()); } }; } @@ -198,34 +213,61 @@ public void updateGui(@Nonnull Block block) { // 获取过滤器 String filter = getFilter(block).toLowerCase(Locale.ROOT); + String sortKey = StorageCacheUtils.getData(block.getLocation(), SORT_KEY); + int sortId = (sortKey != null) ? Integer.parseInt(sortKey) : 0; + + // F5: 计算存储内容的轻量级哈希(size + 总量和)用于脏检测 + Location loc = block.getLocation(); + int storageSize = storage.size(); + long storageTotalAmount = 0; + for (Map.Entry e : storage.entrySet()) { + storageTotalAmount += e.getValue(); + } - // 过滤和排序逻辑 - List> items = new ArrayList<>(storage.entrySet()); - if (!filter.isEmpty()) { - if (!SlimeAEPlugin.getJustEnoughGuideIntegration().isLoaded()) - items.removeIf(x -> doFilterNoJEG(x, filter)); - else { - boolean isPinyinSearch = JustEnoughGuide.getConfigManager().isPinyinSearch(); - SearchGroup group = new SearchGroup(null, player, filter, isPinyinSearch); - List slimefunItems = group.filterItems(player, filter, isPinyinSearch); - items.removeIf(x -> doFilterWithJEG(x, slimefunItems, filter)); + // F7: 检查排序结果缓存 + SortedItemsCache cachedResult = sortedItemsCacheMap.get(loc); + List> items; + int pinnedCount = 0; + + if (cachedResult != null + && cachedResult.isValid(filter, sortId, storageSize, storageTotalAmount, storage instanceof CreativeItemMap)) { + // F5+F7: 缓存命中,数据未变化,复用上次的过滤+排序结果 + items = cachedResult.items; + pinnedCount = cachedResult.pinnedCount; + } else { + // 过滤和排序逻辑 + items = new ArrayList<>(storage.entrySet()); + if (!filter.isEmpty()) { + if (!SlimeAEPlugin.getJustEnoughGuideIntegration().isLoaded()) + items.removeIf(x -> doFilterNoJEG(x, filter)); + else { + boolean isPinyinSearch = JustEnoughGuide.getConfigManager().isPinyinSearch(); + // F1: 使用缓存的搜索结果 + List slimefunItemsList = getCachedFilterItems(player, filter, isPinyinSearch); + // F4: List→HashSet 优化 contains 查找为 O(1) + Set slimefunItemSet = new HashSet<>(slimefunItemsList); + items.removeIf(x -> doFilterWithJEG(x, slimefunItemSet, filter)); + } } - } - if (storage instanceof CreativeItemMap) items.sort(MATERIAL_SORT); - else items.sort(getSort(block)); + if (storage instanceof CreativeItemMap) items.sort(MATERIAL_SORT); + else items.sort(getSort(block)); - int pinnedCount = 0; - if (filter.isEmpty()) { - PinnedManager pinnedManager = SlimeAEPlugin.getPinnedManager(); - List pinnedItems = pinnedManager.getPinnedItems(player); - if (pinnedItems == null) pinnedItems = new ArrayList<>(); - - for (ItemStack pinned : pinnedItems) { - if (!storage.containsKey(pinned)) continue; - items.add(0, new AbstractMap.SimpleEntry<>(pinned, storage.get(pinned))); - pinnedCount++; + if (filter.isEmpty()) { + PinnedManager pinnedManager = SlimeAEPlugin.getPinnedManager(); + List pinnedItems = pinnedManager.getPinnedItems(player); + if (pinnedItems == null) pinnedItems = new ArrayList<>(); + + for (ItemStack pinned : pinnedItems) { + if (!storage.containsKey(pinned)) continue; + items.add(0, new AbstractMap.SimpleEntry<>(pinned, storage.get(pinned))); + pinnedCount++; + } } + + // 更新缓存 + sortedItemsCacheMap.put(loc, new SortedItemsCache( + filter, sortId, storageSize, storageTotalAmount, storage instanceof CreativeItemMap, items, pinnedCount)); } // 计算分页 @@ -247,27 +289,45 @@ public void updateGui(@Nonnull Block block) { } } + // F8: 获取或创建当前位置的显示槽位缓存 + DisplaySlotCache slotCache = displaySlotCacheMap.computeIfAbsent(loc, k -> new DisplaySlotCache()); + for (int i = 0; i < getDisplaySlots().length && (i + startIndex) < endIndex; i++) { int slot = getDisplaySlots()[i]; if (i + startIndex >= items.size()) { - blockMenu.replaceExistingItem(slot, MenuItems.EMPTY); - blockMenu.addMenuClickHandler(slot, ChestMenuUtils.getEmptyClickHandler()); + // F8: 检查是否已经是空槽 + if (!slotCache.isEmptySlot(slot)) { + blockMenu.replaceExistingItem(slot, MenuItems.EMPTY); + blockMenu.addMenuClickHandler(slot, ChestMenuUtils.getEmptyClickHandler()); + slotCache.markEmpty(slot); + } continue; } Map.Entry entry = items.get(i + startIndex); ItemStack itemStack = entry.getKey(); if (itemStack == null || itemStack.getType().isAir()) { - blockMenu.replaceExistingItem(slot, MenuItems.EMPTY); - blockMenu.addMenuClickHandler(slot, ChestMenuUtils.getEmptyClickHandler()); + if (!slotCache.isEmptySlot(slot)) { + blockMenu.replaceExistingItem(slot, MenuItems.EMPTY); + blockMenu.addMenuClickHandler(slot, ChestMenuUtils.getEmptyClickHandler()); + slotCache.markEmpty(slot); + } continue; } + boolean isPinned = i < pinnedCount - page * getDisplaySlots().length; + long amount = entry.getValue(); + + // F8: 检查此槽位的物品和数量是否有变化 + if (slotCache.isUnchanged(slot, itemStack, amount, isPinned)) { + continue; // 跳过未变化的槽位 + } + blockMenu.replaceExistingItem( slot, - ItemUtils.createDisplayItem( - itemStack, entry.getValue(), true, i < pinnedCount - page * getDisplaySlots().length)); + ItemUtils.createDisplayItem(itemStack, amount, true, isPinned)); blockMenu.addMenuClickHandler(slot, handleGuiClick(block, blockMenu, itemStack)); + slotCache.update(slot, itemStack, amount, isPinned); } } @@ -296,6 +356,8 @@ public boolean onClick( if (pinned == null) pinned = new ArrayList<>(); if (!pinned.contains(template.asOne())) pinnedManager.addPinned(player, template); else pinnedManager.removePinned(player, template); + // F7: 置顶变化,使排序缓存失效 + clearSortedItemsCache(block.getLocation()); updateGui(block); return false; } @@ -464,23 +526,165 @@ protected boolean doFilterNoJEG(Map.Entry x, String filter) { return !cleanName.contains(filter); } - protected boolean doFilterWithJEG(Map.Entry x, List slimefunItems, String filter) { + /** + * 使用 Set 参数的 JEG 过滤方法 (F4: HashSet 优化) + */ + protected boolean doFilterWithJEG(Map.Entry x, Set slimefunItems, String filter) { ItemStack item = x.getKey(); if (item.getType().isItem() && SlimefunItem.getOptionalByItem(item).isEmpty()) { String displayName = ItemStackHelper.getDisplayName(item); String cleanName = ChatColor.stripColor(displayName).toLowerCase(Locale.ROOT); - String pyName = PinyinHelper.toPinyin(cleanName, PinyinStyleEnum.INPUT, ""); - String pyFirstLetter = PinyinHelper.toPinyin(cleanName, PinyinStyleEnum.FIRST_LETTER, ""); + // F2: 使用拼音缓存 + String pyName = PinyinCache.toPinyinFull(cleanName); + String pyFirstLetter = PinyinCache.toPinyinFirstLetter(cleanName); boolean matches = cleanName.contains(filter) || pyName.contains(filter.toLowerCase()) || pyFirstLetter.contains(filter.toLowerCase()); return !matches; } Optional sfItem = SlimefunItem.getOptionalByItem(item); + // F4: Set.contains() 是 O(1) return sfItem.map(s -> !slimefunItems.contains(s)).orElse(true); } + /** + * F1: 获取缓存的搜索结果。 + * 以 filter 字符串为 key,缓存 filterItems() 返回的 List。 + * 当过滤器未变化时(TTL 内),直接复用缓存。 + */ + protected List getCachedFilterItems(Player player, String filter, boolean isPinyinSearch) { + long now = System.currentTimeMillis(); + Long cachedTime = searchCacheTime.get(filter); + if (cachedTime != null && (now - cachedTime) < SEARCH_CACHE_TTL) { + List cached = searchCache.get(filter); + if (cached != null) return cached; + } + + SearchGroup group = new SearchGroup(null, player, filter, isPinyinSearch); + List result = group.filterItems(player, filter, isPinyinSearch); + + // 缓存大小限制,防止内存泄漏 + if (searchCache.size() >= SEARCH_CACHE_MAX_SIZE) { + clearSearchCache(); + } + + searchCache.put(filter, result); + searchCacheTime.put(filter, now); + return result; + } + + /** + * 清空搜索缓存 + */ + public static void clearSearchCache() { + searchCache.clear(); + searchCacheTime.clear(); + } + + /** + * 清空指定位置的排序结果缓存和显示槽位缓存 + */ + public static void clearSortedItemsCache(@Nonnull Location loc) { + sortedItemsCacheMap.remove(loc); + displaySlotCacheMap.remove(loc); + } + + /** + * 清空所有排序结果缓存和显示槽位缓存 + */ + public static void clearAllSortedItemsCache() { + sortedItemsCacheMap.clear(); + displaySlotCacheMap.clear(); + } + + /** + * F5+F7: 排序结果缓存内部类 + * 存储上一次的过滤、排序、置顶结果,避免每 tick 重复计算 + * 使用 storage size + total amount + filter + sort 作为脏检测条件 + */ + private static class SortedItemsCache { + final String filter; + final int sortId; + final int storageSize; + final long storageTotalAmount; + final boolean isCreative; + final List> items; + final int pinnedCount; + final long createTime; + private static final long CACHE_TTL = 500; // 500ms 缓存超时 + + SortedItemsCache(String filter, int sortId, int storageSize, long storageTotalAmount, + boolean isCreative, List> items, int pinnedCount) { + this.filter = filter; + this.sortId = sortId; + this.storageSize = storageSize; + this.storageTotalAmount = storageTotalAmount; + this.isCreative = isCreative; + this.items = items; + this.pinnedCount = pinnedCount; + this.createTime = System.currentTimeMillis(); + } + + boolean isValid(String filter, int sortId, int storageSize, long storageTotalAmount, boolean isCreative) { + if (System.currentTimeMillis() - createTime > CACHE_TTL) return false; + return this.filter.equals(filter) + && this.sortId == sortId + && this.storageSize == storageSize + && this.storageTotalAmount == storageTotalAmount + && this.isCreative == isCreative; + } + } + + /** + * F8: 显示槽位缓存内部类 + * 记录每个槽位上一次显示的物品类型和数量,避免无谓的 replaceExistingItem + createDisplayItem + */ + private static class DisplaySlotCache { + // slot -> 编码:物品类型 + 数量 + 是否置顶 + private final Map slotStates = new HashMap<>(); + + boolean isEmptySlot(int slot) { + SlotState state = slotStates.get(slot); + return state != null && state.isEmpty; + } + + void markEmpty(int slot) { + slotStates.put(slot, SlotState.EMPTY); + } + + boolean isUnchanged(int slot, @Nonnull ItemStack itemStack, long amount, boolean isPinned) { + SlotState state = slotStates.get(slot); + if (state == null || state.isEmpty) return false; + return state.amount == amount + && state.isPinned == isPinned + && state.itemType == itemStack.getType() + && state.itemHash == itemStack.hashCode(); + } + + void update(int slot, @Nonnull ItemStack itemStack, long amount, boolean isPinned) { + slotStates.put(slot, new SlotState(itemStack.getType(), itemStack.hashCode(), amount, isPinned)); + } + + private static class SlotState { + static final SlotState EMPTY = new SlotState(null, 0, 0, false); + + final org.bukkit.Material itemType; + final int itemHash; + final long amount; + final boolean isPinned; + final boolean isEmpty; + + SlotState(org.bukkit.Material itemType, int itemHash, long amount, boolean isPinned) { + this.itemType = itemType; + this.itemHash = itemHash; + this.amount = amount; + this.isPinned = isPinned; + this.isEmpty = (itemType == null); + } + } + } + public boolean fastInsert() { return true; } diff --git a/src/main/java/me/ddggdd135/slimeae/integrations/networks/QuantumStorage.java b/src/main/java/me/ddggdd135/slimeae/integrations/networks/QuantumStorage.java index ff583f81..1957cdd4 100644 --- a/src/main/java/me/ddggdd135/slimeae/integrations/networks/QuantumStorage.java +++ b/src/main/java/me/ddggdd135/slimeae/integrations/networks/QuantumStorage.java @@ -75,8 +75,19 @@ public void pushItem(@Nonnull ItemInfo itemInfo) { return; } if (StackUtils.itemsMatch(quantumCache, itemStack)) { - int leftover = quantumCache.increaseAmount(itemStack.getAmount()); - itemStack.setAmount(leftover); + long toDeposit = itemInfo.getAmount(); + long remaining = toDeposit; + // increaseAmount 接受 int,需要分批推入 + while (remaining > 0) { + int batch = (int) Math.min(remaining, Integer.MAX_VALUE); + int leftover = quantumCache.increaseAmount(batch); + remaining = remaining - batch + leftover; + // 如果有放不下的,说明已满,停止推入 + if (leftover > 0) { + break; + } + } + itemInfo.setAmount(remaining); } } diff --git a/src/main/java/me/ddggdd135/slimeae/utils/ItemUtils.java b/src/main/java/me/ddggdd135/slimeae/utils/ItemUtils.java index 5edcf56c..67ed35d9 100644 --- a/src/main/java/me/ddggdd135/slimeae/utils/ItemUtils.java +++ b/src/main/java/me/ddggdd135/slimeae/utils/ItemUtils.java @@ -799,8 +799,14 @@ public static boolean matchesAll(@Nonnull ItemStack[] left, @Nonnull ItemStack[] if (y == null) y = new ItemStack(Material.AIR); } - if (x.getType().isAir()) x.setAmount(0); - if (y.getType().isAir()) y.setAmount(0); + // 不要直接修改原始 ItemStack 的 amount(会破坏 CraftItemStack 的 hashCode 稳定性), + // 改为创建副本 + if (x.getType().isAir()) { + x = new ItemStack(Material.AIR, 0); + } + if (y.getType().isAir()) { + y = new ItemStack(Material.AIR, 0); + } if (!SlimefunUtils.isItemSimilar(x, y, true, checkAmount)) { return false; diff --git a/src/main/java/me/ddggdd135/slimeae/utils/PinyinCache.java b/src/main/java/me/ddggdd135/slimeae/utils/PinyinCache.java new file mode 100644 index 00000000..cfa8cd85 --- /dev/null +++ b/src/main/java/me/ddggdd135/slimeae/utils/PinyinCache.java @@ -0,0 +1,50 @@ +package me.ddggdd135.slimeae.utils; + +import com.github.houbb.pinyin.constant.enums.PinyinStyleEnum; +import com.github.houbb.pinyin.util.PinyinHelper; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; + +/** + * 拼音转换结果全局缓存 + * 物品名称 → 拼音结果映射是确定性的,可以永久缓存。 + * 在 Minecraft 场景下,物品名称是有限集合,缓存命中率接近 100%。 + */ +public final class PinyinCache { + private static final ConcurrentHashMap pinyinFullCache = new ConcurrentHashMap<>(512); + private static final ConcurrentHashMap pinyinFirstLetterCache = new ConcurrentHashMap<>(512); + + private PinyinCache() {} + + /** + * 获取完整拼音(带缓存) + * + * @param text 要转换的文本 + * @return 完整拼音字符串 + */ + @Nonnull + public static String toPinyinFull(@Nonnull String text) { + return pinyinFullCache.computeIfAbsent(text, + k -> PinyinHelper.toPinyin(k, PinyinStyleEnum.INPUT, "")); + } + + /** + * 获取拼音首字母(带缓存) + * + * @param text 要转换的文本 + * @return 拼音首字母字符串 + */ + @Nonnull + public static String toPinyinFirstLetter(@Nonnull String text) { + return pinyinFirstLetterCache.computeIfAbsent(text, + k -> PinyinHelper.toPinyin(k, PinyinStyleEnum.FIRST_LETTER, "")); + } + + /** + * 清除所有缓存 + */ + public static void clearCache() { + pinyinFullCache.clear(); + pinyinFirstLetterCache.clear(); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index df5866d8..5f7cb520 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -3,6 +3,9 @@ auto-update: true #自动保存间隔(s) auto-save-period: 300 +#调试模式 +debug: false + #接入蓬松科技 support-fluffy-machines: true