From bf071031a54604d4fec3bc157ff7700cb852d0c7 Mon Sep 17 00:00:00 2001 From: shixinzia <3054623880@qq.com> Date: Sat, 9 May 2026 09:38:11 +0800 Subject: [PATCH] test fix bug --- .gitignore | 1 + pom.xml | 11 ++ .../slimeae/api/MEStorageCellStorageData.java | 6 + .../api/autocraft/AutoCraftingTask.java | 72 ++++--- .../api/database/v3/CraftTaskPersistence.java | 9 +- .../slimeae/api/database/v3/DirtyTracker.java | 42 +++-- .../database/v3/V3StorageCellController.java | 14 +- .../api/interfaces/IMERealCraftDevice.java | 4 +- .../slimeae/api/items/MEStorageCellCache.java | 37 +--- .../ddggdd135/slimeae/core/NetworkInfo.java | 10 + .../core/slimefun/CookingAllocator.java | 176 +++++++++++++++--- .../core/slimefun/MECellWorkbench.java | 21 ++- .../terminals/MEAllInOneTerminal.java | 10 +- .../terminals/MECraftPlanningTerminal.java | 10 +- .../slimeae/tasks/NetworkTickerTask.java | 10 +- .../ddggdd135/slimeae/utils/NetworkUtils.java | 63 ++++--- .../AutoCraftingTaskProgressTest.java | 20 ++ .../api/database/v3/DirtyTrackerTest.java | 40 ++++ .../core/slimefun/MECellWorkbenchTest.java | 25 +++ 19 files changed, 457 insertions(+), 124 deletions(-) create mode 100644 src/test/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTaskProgressTest.java create mode 100644 src/test/java/me/ddggdd135/slimeae/api/database/v3/DirtyTrackerTest.java create mode 100644 src/test/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbenchTest.java diff --git a/.gitignore b/.gitignore index eb716c70..45b95698 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ dependency-reduced-pom.xml /LogiTech-master/ /GuguSlimefunLib/ /FinalTECH-Changed/ +/AEbuild/ # docs /docs/ diff --git a/pom.xml b/pom.xml index 6f2d1c72..956c890d 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,11 @@ 16 + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + org.apache.maven.plugins maven-shade-plugin @@ -250,5 +255,11 @@ 3.45.3.0 provided + + org.junit.jupiter + junit-jupiter + 5.10.2 + test + diff --git a/src/main/java/me/ddggdd135/slimeae/api/MEStorageCellStorageData.java b/src/main/java/me/ddggdd135/slimeae/api/MEStorageCellStorageData.java index 5945a066..020b4dca 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/MEStorageCellStorageData.java +++ b/src/main/java/me/ddggdd135/slimeae/api/MEStorageCellStorageData.java @@ -4,6 +4,7 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.ddggdd135.guguslimefunlib.api.ItemHashMap; @@ -20,6 +21,7 @@ public class MEStorageCellStorageData { private ItemHashMap storages; private long stored; private long size; + private final AtomicLong dirtySequence = new AtomicLong(); public MEStorageCellStorageData() {} @@ -75,6 +77,10 @@ public void setStored(long stored) { this.stored = stored; } + public long nextDirtySequence() { + return dirtySequence.incrementAndGet(); + } + @Nullable public static MEStorageCellStorageData getMEStorageCellStorageData(@Nonnull UUID uuid) { return cache.getOrDefault(uuid, null); } 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 5147aeae..916203a7 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java @@ -45,6 +45,12 @@ public class AutoCraftingTask implements IDisposable { private static final int MAX_LORE_LINES = 15; private static final String MORE_ITEMS_INDICATOR = "&7... 还有%d项未显示"; + public enum StepProcessResult { + PROGRESSED, + WAITING, + BLOCKED + } + public static final String CRAFTING_KEY = "auto_crafting"; private final UUID taskId; private final CraftingRecipe recipe; @@ -63,6 +69,10 @@ public class AutoCraftingTask implements IDisposable { private volatile TaskState taskState = TaskState.RUNNING; private final long createdAt; + public static boolean shouldIncrementGlobalFail(@Nonnull List results) { + return !results.isEmpty() && results.stream().allMatch(result -> result == StepProcessResult.BLOCKED); + } + public AutoCraftingTask(@Nonnull NetworkInfo info, @Nonnull CraftingRecipe recipe, long count) { this.taskId = UUID.randomUUID(); this.createdAt = System.currentTimeMillis(); @@ -328,9 +338,9 @@ public synchronized void suspend() { if (device.isFinished(deviceBlock)) { CraftingRecipe finished = device.getFinishedCraftingRecipe(deviceBlock); if (finished != null && finished.equals(step.getRecipe())) { - device.finishCrafting(deviceBlock); - storage.addItem(step.getRecipe().getOutput()); - toRemove.add(deviceLoc); + if (collectFinishedOutput(device, deviceBlock, step.getRecipe())) { + toRemove.add(deviceLoc); + } } } } @@ -549,16 +559,15 @@ public synchronized void moveNext(int maxDevices) { return; } - boolean anySuccess = false; + List results = new ArrayList<>(readySteps.size()); for (CraftStep step : readySteps) { - boolean success = processStep(step, maxDevices); - if (success) anySuccess = true; + results.add(processStep(step, maxDevices)); } - if (anySuccess) { + if (results.contains(StepProcessResult.PROGRESSED)) { globalFailTimes = 0; - } else { + } else if (shouldIncrementGlobalFail(results)) { globalFailTimes++; } @@ -606,6 +615,15 @@ private void cleanupCompletedSteps() { } } + private boolean collectFinishedOutput( + @Nonnull IMERealCraftDevice device, @Nonnull Block deviceBlock, @Nonnull CraftingRecipe recipe) { + ItemStorage output = device.finishCrafting(deviceBlock); + ItemRequest[] outputRequests = ItemUtils.createRequests(recipe.getOutputAmounts()); + if (!output.contains(outputRequests)) return false; + storage.addItem(output.getStorageUnsafe()); + return true; + } + private void handleCancellation() { boolean anyRunning = false; for (CraftStep step : craftingSteps) { @@ -643,9 +661,9 @@ private void handleCancellation() { if (device.isFinished(deviceBlock)) { CraftingRecipe finished = device.getFinishedCraftingRecipe(deviceBlock); if (finished != null && finished.equals(nextRecipe)) { - device.finishCrafting(deviceBlock); - storage.addItem(nextRecipe.getOutput()); - toRemove.add(deviceLoc); + if (collectFinishedOutput(device, deviceBlock, nextRecipe)) { + toRemove.add(deviceLoc); + } } } } @@ -692,15 +710,16 @@ private void handleCancellation() { } } - private boolean processStep(CraftStep step, int maxDevices) { + private StepProcessResult processStep(CraftStep step, int maxDevices) { CraftingRecipe nextRecipe = step.getRecipe(); CraftType craftType = nextRecipe.getCraftType(); boolean doCraft = !isCancelling; boolean hasProgress = false; + boolean hasWaiting = false; if (step.getAmount() <= 0) { if (step.isIdle()) { - return false; + return StepProcessResult.BLOCKED; } doCraft = false; } @@ -724,11 +743,17 @@ private boolean processStep(CraftStep step, int maxDevices) { if (device.isFinished(deviceBlock)) { CraftingRecipe finished = device.getFinishedCraftingRecipe(deviceBlock); if (finished != null && finished.equals(nextRecipe)) { - device.finishCrafting(deviceBlock); - storage.addItem(finished.getOutput()); - invalidDevices.add(deviceLoc); - hasProgress = true; + if (collectFinishedOutput(device, deviceBlock, finished)) { + invalidDevices.add(deviceLoc); + hasProgress = true; + } else { + hasWaiting = true; + } + } else { + hasWaiting = true; } + } else { + hasWaiting = true; } } for (Location loc : invalidDevices) { @@ -831,12 +856,14 @@ private boolean processStep(CraftStep step, int maxDevices) { storage.takeItem(requests); step.setVirtualRunning(step.getVirtualRunning() + (int) actualAmount); step.decreaseAmount(actualAmount); - return true; + return StepProcessResult.PROGRESSED; } } } - return hasProgress; + if (hasProgress) return StepProcessResult.PROGRESSED; + if (hasWaiting || step.getVirtualRunning() > 0) return StepProcessResult.WAITING; + return StepProcessResult.BLOCKED; } public void showGUI(Player player) { @@ -952,10 +979,10 @@ public void refreshGUI(int maxSize, boolean cancelButton) { menu.getContents(); } - public void start() { - if (!SlimeAEPlugin.getNetworkData().AllNetworkData.contains(info)) return; + public boolean start() { + if (!SlimeAEPlugin.getNetworkData().AllNetworkData.contains(info)) return false; - if (info.getAutoCraftingSessions().contains(this)) return; + if (info.getAutoCraftingSessions().contains(this)) return true; AutoCraftingTaskStartingEvent e = new AutoCraftingTaskStartingEvent(this); Bukkit.getPluginManager().callEvent(e); @@ -963,6 +990,7 @@ public void start() { menu.addMenuCloseHandler(player -> {}); info.getAutoCraftingSessions().add(this); + return true; } public AEMenu getMenu() { diff --git a/src/main/java/me/ddggdd135/slimeae/api/database/v3/CraftTaskPersistence.java b/src/main/java/me/ddggdd135/slimeae/api/database/v3/CraftTaskPersistence.java index 296b9c31..4f4fa5bd 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/database/v3/CraftTaskPersistence.java +++ b/src/main/java/me/ddggdd135/slimeae/api/database/v3/CraftTaskPersistence.java @@ -319,10 +319,11 @@ public int tryRestore(NetworkInfo info) { rec.isCancelling, rec.createdAt); - task.start(); - delete(rec.taskId); - restored++; - logger.info("Restored craft task " + rec.taskId); + if (task.start()) { + delete(rec.taskId); + restored++; + logger.info("Restored craft task " + rec.taskId); + } } catch (Exception e) { logger.log(Level.WARNING, "Failed to restore task " + rec.taskId + ", returning materials", e); returnMaterialsFromRecord(rec, info); diff --git a/src/main/java/me/ddggdd135/slimeae/api/database/v3/DirtyTracker.java b/src/main/java/me/ddggdd135/slimeae/api/database/v3/DirtyTracker.java index b0a7651e..c9f7aa51 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/database/v3/DirtyTracker.java +++ b/src/main/java/me/ddggdd135/slimeae/api/database/v3/DirtyTracker.java @@ -2,36 +2,48 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.Nonnull; public class DirtyTracker { - private final ConcurrentHashMap> dirtyMap = new ConcurrentHashMap<>(); + private final AtomicReference>> dirtyMapRef = + new AtomicReference<>(new ConcurrentHashMap<>()); + private final AtomicLong fallbackSequence = new AtomicLong(); private volatile Map> pendingFlush = null; private final ReentrantLock flushLock = new ReentrantLock(); - public record DirtyEntry(char op, long newAmount, long timestamp) {} + public record DirtyEntry(char op, long newAmount, long timestamp, long sequence) {} public void record(@Nonnull UUID cellUUID, long tplId, long newAmount, char op) { - dirtyMap.computeIfAbsent(cellUUID, k -> new ConcurrentHashMap<>()) - .put(tplId, new DirtyEntry(op, newAmount, System.currentTimeMillis())); + record(cellUUID, tplId, newAmount, op, fallbackSequence.incrementAndGet()); + } + + public void record(@Nonnull UUID cellUUID, long tplId, long newAmount, char op, long sequence) { + DirtyEntry entry = new DirtyEntry(op, newAmount, System.currentTimeMillis(), sequence); + dirtyMapRef + .get() + .computeIfAbsent(cellUUID, k -> new ConcurrentHashMap<>()) + .merge(tplId, entry, DirtyTracker::newerEntry); } public void recordDeleteCell(@Nonnull UUID cellUUID) { ConcurrentHashMap cellMap = new ConcurrentHashMap<>(); - cellMap.put(-1L, new DirtyEntry('D', 0, System.currentTimeMillis())); - dirtyMap.put(cellUUID, cellMap); + cellMap.put(-1L, new DirtyEntry('D', 0, System.currentTimeMillis(), fallbackSequence.incrementAndGet())); + dirtyMapRef.get().put(cellUUID, cellMap); } public List drainPhase1() { flushLock.lock(); try { + ConcurrentHashMap> dirtyMap = + dirtyMapRef.getAndSet(new ConcurrentHashMap<>()); if (dirtyMap.isEmpty()) return Collections.emptyList(); Map> snapshot = new HashMap<>(); for (var entry : dirtyMap.entrySet()) { snapshot.put(entry.getKey(), new HashMap<>(entry.getValue())); } - dirtyMap.clear(); pendingFlush = snapshot; return toJournalRows(snapshot); } finally { @@ -55,12 +67,9 @@ public void rollbackFlush() { for (var cellEntry : pendingFlush.entrySet()) { UUID cellUUID = cellEntry.getKey(); ConcurrentHashMap cellMap = - dirtyMap.computeIfAbsent(cellUUID, k -> new ConcurrentHashMap<>()); + dirtyMapRef.get().computeIfAbsent(cellUUID, k -> new ConcurrentHashMap<>()); for (var itemEntry : cellEntry.getValue().entrySet()) { - cellMap.merge( - itemEntry.getKey(), - itemEntry.getValue(), - (existing, pending) -> existing.timestamp() >= pending.timestamp() ? existing : pending); + cellMap.merge(itemEntry.getKey(), itemEntry.getValue(), DirtyTracker::newerEntry); } } pendingFlush = null; @@ -70,13 +79,13 @@ public void rollbackFlush() { } public boolean hasPendingChanges(UUID cellUUID) { - return dirtyMap.containsKey(cellUUID); + return dirtyMapRef.get().containsKey(cellUUID); } public List drainCell(UUID cellUUID) { flushLock.lock(); try { - ConcurrentHashMap cellMap = dirtyMap.remove(cellUUID); + ConcurrentHashMap cellMap = dirtyMapRef.get().remove(cellUUID); if (cellMap == null) return Collections.emptyList(); return toJournalRows(Map.of(cellUUID, new HashMap<>(cellMap))); } finally { @@ -84,6 +93,11 @@ public List drainCell(UUID cellUUID) { } } + private static DirtyEntry newerEntry(DirtyEntry a, DirtyEntry b) { + if (a.sequence() != b.sequence()) return a.sequence() > b.sequence() ? a : b; + return a.timestamp() >= b.timestamp() ? a : b; + } + private List toJournalRows(Map> data) { List rows = new ArrayList<>(); for (var cellEntry : data.entrySet()) { diff --git a/src/main/java/me/ddggdd135/slimeae/api/database/v3/V3StorageCellController.java b/src/main/java/me/ddggdd135/slimeae/api/database/v3/V3StorageCellController.java index 709b79c9..b0ce3bb8 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/database/v3/V3StorageCellController.java +++ b/src/main/java/me/ddggdd135/slimeae/api/database/v3/V3StorageCellController.java @@ -54,16 +54,26 @@ public MEStorageCellStorageData loadData(@Nonnull ItemStack itemStack) { } public void markDirty(@Nonnull MEStorageCellStorageData data, @Nonnull ItemKey key, long finalAmount) { + markDirty(data, key, finalAmount, data.nextDirtySequence()); + } + + public void markDirty( + @Nonnull MEStorageCellStorageData data, @Nonnull ItemKey key, long finalAmount, long sequence) { long tplId = bridge.getOrResolve(key); - dirtyTracker.record(data.getUuid(), tplId, finalAmount, finalAmount > 0 ? 'P' : 'R'); + dirtyTracker.record(data.getUuid(), tplId, finalAmount, finalAmount > 0 ? 'P' : 'R', sequence); } public void markDirtyBatch( @Nonnull MEStorageCellStorageData data, @Nonnull List> entries) { + markDirtyBatch(data, entries, data.nextDirtySequence()); + } + + public void markDirtyBatch( + @Nonnull MEStorageCellStorageData data, @Nonnull List> entries, long sequence) { UUID cellUUID = data.getUuid(); for (Map.Entry entry : entries) { long tplId = bridge.getOrResolve(entry.getKey()); - dirtyTracker.record(cellUUID, tplId, entry.getValue(), entry.getValue() > 0 ? 'P' : 'R'); + dirtyTracker.record(cellUUID, tplId, entry.getValue(), entry.getValue() > 0 ? 'P' : 'R', sequence); } } diff --git a/src/main/java/me/ddggdd135/slimeae/api/interfaces/IMERealCraftDevice.java b/src/main/java/me/ddggdd135/slimeae/api/interfaces/IMERealCraftDevice.java index 5d78cad5..b8da2602 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/interfaces/IMERealCraftDevice.java +++ b/src/main/java/me/ddggdd135/slimeae/api/interfaces/IMERealCraftDevice.java @@ -3,6 +3,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.ddggdd135.slimeae.api.autocraft.CraftingRecipe; +import me.ddggdd135.slimeae.api.items.ItemStorage; import org.bukkit.block.Block; public interface IMERealCraftDevice extends IMECraftDevice { @@ -14,5 +15,6 @@ public interface IMERealCraftDevice extends IMECraftDevice { @Nullable CraftingRecipe getFinishedCraftingRecipe(@Nonnull Block block); - void finishCrafting(@Nonnull Block block); + @Nonnull + ItemStorage finishCrafting(@Nonnull Block block); } 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 ea2bb4b7..4c0947e0 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/items/MEStorageCellCache.java +++ b/src/main/java/me/ddggdd135/slimeae/api/items/MEStorageCellCache.java @@ -105,16 +105,11 @@ private void trim(@Nonnull ItemKey key) { if (storages.containsKey(key) && storages.getKey(key) == 0) { storages.removeKey(key); - SlimeAEPlugin.getStorageCellStorageDataController().markDirty(storageData, key, 0); } } @Override public void pushItem(@Nonnull ItemStackCache itemStackCache) { - ItemKey dirtyKey = null; - long dirtyAmount = 0; - boolean needsDirty = false; - rwLock.writeLock().lock(); try { ItemStack itemStack = itemStackCache.getItemStack(); @@ -137,29 +132,21 @@ public void pushItem(@Nonnull ItemStackCache itemStackCache) { long toAdd; if (stored + itemStack.getAmount() > size) toAdd = size - stored; else toAdd = itemStack.getAmount(); + if (toAdd <= 0) return; stored += toAdd; storageData.setStored(stored); storages.putKey(key, amount + toAdd); - dirtyKey = key; - dirtyAmount = amount + toAdd; - needsDirty = true; itemStack.setAmount((int) (itemStack.getAmount() - toAdd)); trim(key); + SlimeAEPlugin.getStorageCellStorageDataController() + .markDirty(storageData, key, amount + toAdd, storageData.nextDirtySequence()); } finally { rwLock.writeLock().unlock(); } - - if (needsDirty) { - SlimeAEPlugin.getStorageCellStorageDataController().markDirty(storageData, dirtyKey, dirtyAmount); - } } @Override public void pushItem(@Nonnull ItemInfo itemInfo) { - ItemKey dirtyKey = null; - long dirtyAmount = 0; - boolean needsDirty = false; - rwLock.writeLock().lock(); try { ItemKey key = itemInfo.getItemKey(); @@ -183,21 +170,17 @@ public void pushItem(@Nonnull ItemInfo itemInfo) { long toAdd; if (stored + itemInfo.getAmount() > size) toAdd = size - stored; else toAdd = itemInfo.getAmount(); + if (toAdd <= 0) return; stored += toAdd; storageData.setStored(stored); storages.putKey(key, amount + toAdd); itemInfo.setAmount(itemInfo.getAmount() - toAdd); - dirtyKey = key; - dirtyAmount = amount + toAdd; - needsDirty = true; trim(key); + SlimeAEPlugin.getStorageCellStorageDataController() + .markDirty(storageData, key, amount + toAdd, storageData.nextDirtySequence()); } finally { rwLock.writeLock().unlock(); } - - if (needsDirty) { - SlimeAEPlugin.getStorageCellStorageDataController().markDirty(storageData, dirtyKey, dirtyAmount); - } } @Override @@ -273,14 +256,14 @@ public ItemStorage takeItem(@Nonnull ItemRequest[] requests) { } } storageData.setStored(stored); + if (!dirtyBatch.isEmpty()) { + SlimeAEPlugin.getStorageCellStorageDataController() + .markDirtyBatch(storageData, dirtyBatch, storageData.nextDirtySequence()); + } } finally { rwLock.writeLock().unlock(); } - if (dirtyBatch != null && !dirtyBatch.isEmpty()) { - SlimeAEPlugin.getStorageCellStorageDataController().markDirtyBatch(storageData, dirtyBatch); - } - return itemStacks; } diff --git a/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java b/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java index 79c28c98..ff01e305 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java +++ b/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java @@ -4,6 +4,7 @@ import java.util.*; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.ddggdd135.guguslimefunlib.api.AEMenu; @@ -56,6 +57,7 @@ public class NetworkInfo implements IDisposable { private volatile boolean needsStorageUpdate = false; private volatile boolean needsRecipeUpdate = false; + private final AtomicInteger controllerUnavailableTicks = new AtomicInteger(); private volatile int parallelProcessorCount = 0; @@ -223,6 +225,14 @@ public void clearDirtyFlags() { this.needsRecipeUpdate = false; } + public int markControllerUnavailable() { + return controllerUnavailableTicks.incrementAndGet(); + } + + public void markControllerAvailable() { + controllerUnavailableTicks.set(0); + } + @Override public int hashCode() { return controller.hashCode(); diff --git a/src/main/java/me/ddggdd135/slimeae/core/slimefun/CookingAllocator.java b/src/main/java/me/ddggdd135/slimeae/core/slimefun/CookingAllocator.java index 9fa8dc5a..1d2d52d7 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/CookingAllocator.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/CookingAllocator.java @@ -15,11 +15,15 @@ import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import me.ddggdd135.guguslimefunlib.api.ItemHashMap; +import me.ddggdd135.guguslimefunlib.items.ItemKey; import me.ddggdd135.slimeae.SlimeAEPlugin; import me.ddggdd135.slimeae.api.abstracts.MEBus; import me.ddggdd135.slimeae.api.autocraft.CraftingRecipe; import me.ddggdd135.slimeae.api.interfaces.IMERealCraftDevice; import me.ddggdd135.slimeae.api.interfaces.IStorage; +import me.ddggdd135.slimeae.api.items.ItemRequest; +import me.ddggdd135.slimeae.api.items.ItemStorage; import me.ddggdd135.slimeae.core.NetworkInfo; import me.ddggdd135.slimeae.core.items.MenuItems; import me.ddggdd135.slimeae.utils.ItemUtils; @@ -38,9 +42,12 @@ public class CookingAllocator extends MEBus implements IMERealCraftDevice { private static final String DATA_KEY_RECIPE_TYPE = "cooking_recipe_type"; private static final String DATA_KEY_RECIPE_INPUT = "cooking_recipe_input"; private static final String DATA_KEY_RECIPE_OUTPUT = "cooking_recipe_output"; + private static final String DATA_KEY_OUTPUT_BASELINE = "cooking_output_baseline"; private static final Map recipeCache = new ConcurrentHashMap<>(); + private static final Map> baselineCache = new ConcurrentHashMap<>(); + private static final Set runningCache = Collections.newSetFromMap(new ConcurrentHashMap<>()); public CookingAllocator(ItemGroup itemGroup, SlimefunItemStack item, RecipeType recipeType, ItemStack[] recipe) { @@ -49,7 +56,8 @@ public CookingAllocator(ItemGroup itemGroup, SlimefunItemStack item, RecipeType addItemHandler(onBlockBreak()); } - private void saveState(@Nonnull Location location, @Nonnull CraftingRecipe recipe) { + private void saveState( + @Nonnull Location location, @Nonnull CraftingRecipe recipe, @Nonnull ItemHashMap baseline) { SlimefunBlockData blockData = StorageCacheUtils.getBlock(location); if (blockData == null) return; StorageCacheUtils.setData(location, DATA_KEY_RUNNING, "true"); @@ -71,6 +79,7 @@ private void saveState(@Nonnull Location location, @Nonnull CraftingRecipe recip outputBuilder.append(serializeItemStack(outputs[i])); } StorageCacheUtils.setData(location, DATA_KEY_RECIPE_OUTPUT, outputBuilder.toString()); + StorageCacheUtils.setData(location, DATA_KEY_OUTPUT_BASELINE, serializeBaseline(recipe.getOutput(), baseline)); } private void clearState(@Nonnull Location location) { @@ -80,6 +89,7 @@ private void clearState(@Nonnull Location location) { StorageCacheUtils.removeData(location, DATA_KEY_RECIPE_TYPE); StorageCacheUtils.removeData(location, DATA_KEY_RECIPE_INPUT); StorageCacheUtils.removeData(location, DATA_KEY_RECIPE_OUTPUT); + StorageCacheUtils.removeData(location, DATA_KEY_OUTPUT_BASELINE); } private void restoreState(@Nonnull Location location) { @@ -96,6 +106,7 @@ private void restoreState(@Nonnull Location location) { String recipeTypeName = blockData.getData(DATA_KEY_RECIPE_TYPE); String inputStr = blockData.getData(DATA_KEY_RECIPE_INPUT); String outputStr = blockData.getData(DATA_KEY_RECIPE_OUTPUT); + String baselineStr = blockData.getData(DATA_KEY_OUTPUT_BASELINE); if (recipeTypeName == null || inputStr == null || outputStr == null) { clearState(location); return; @@ -124,6 +135,7 @@ private void restoreState(@Nonnull Location location) { CraftingRecipe recipe = new CraftingRecipe( craftType, inputList.toArray(new ItemStack[0]), outputList.toArray(new ItemStack[0])); recipeCache.put(location, recipe); + baselineCache.put(location, deserializeBaseline(recipe.getOutput(), baselineStr)); runningCache.add(location); } catch (Exception e) { SlimeAEPlugin.getInstance() @@ -138,6 +150,96 @@ private String serializeItemStack(@Nonnull ItemStack itemStack) { return SerializeUtils.object2String(itemStack); } + @Nonnull + private String serializeBaseline(@Nonnull ItemStack[] outputs, @Nonnull ItemHashMap baseline) { + ItemStack[] items = ItemUtils.trimItems(outputs); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < items.length; i++) { + if (i > 0) builder.append("|"); + builder.append(baseline.getOrDefault(new ItemKey(items[i]), 0L)); + } + return builder.toString(); + } + + @Nonnull + private ItemHashMap deserializeBaseline(@Nonnull ItemStack[] outputs, @Nullable String data) { + ItemHashMap baseline = new ItemHashMap<>(); + if (data == null || data.isBlank()) return baseline; + String[] values = data.split("\\|"); + ItemStack[] items = ItemUtils.trimItems(outputs); + for (int i = 0; i < items.length && i < values.length; i++) { + try { + baseline.putKey(new ItemKey(items[i]), Long.parseLong(values[i])); + } catch (NumberFormatException ignored) { + baseline.putKey(new ItemKey(items[i]), 0L); + } + } + return baseline; + } + + @Nullable private int[] getTransportSlots( + @Nonnull BlockMenu blockMenu, @Nonnull ItemTransportFlow flow, @Nullable ItemStack itemStack) { + try { + int[] slots = blockMenu.getPreset().getSlotsAccessedByItemTransport(blockMenu, flow, itemStack); + if (slots != null && slots.length > 0) return slots; + } catch (IllegalArgumentException ignored) { + } + try { + int[] slots = blockMenu.getPreset().getSlotsAccessedByItemTransport(flow); + if (slots != null && slots.length > 0) return slots; + } catch (IllegalArgumentException ignored) { + } + return null; + } + + private long getItemAmount(@Nonnull BlockMenu blockMenu, @Nonnull ItemStack itemStack) { + int[] slots = getTransportSlots(blockMenu, ItemTransportFlow.WITHDRAW, itemStack); + if (slots == null) return 0; + ItemKey expected = new ItemKey(itemStack); + long amount = 0; + for (int slot : slots) { + ItemStack item = blockMenu.getItemInSlot(slot); + if (item == null || item.getType().isAir()) continue; + if (new ItemKey(item).equals(expected)) amount += item.getAmount(); + } + return amount; + } + + @Nonnull + private ItemHashMap getOutputAmounts(@Nonnull BlockMenu blockMenu, @Nonnull CraftingRecipe recipe) { + ItemHashMap amounts = new ItemHashMap<>(); + for (ItemStack output : ItemUtils.trimItems(recipe.getOutput())) { + amounts.putKey(new ItemKey(output), getItemAmount(blockMenu, output)); + } + return amounts; + } + + @Nonnull + private Map snapshotSlots( + @Nonnull BlockMenu blockMenu, @Nonnull ItemStack[] items, @Nonnull ItemTransportFlow flow) { + Map snapshots = new HashMap<>(); + for (ItemStack item : items) { + int[] slots = getTransportSlots(blockMenu, flow, item); + if (slots == null) continue; + for (int slot : slots) { + if (!snapshots.containsKey(slot)) { + ItemStack current = blockMenu.getItemInSlot(slot); + snapshots.put(slot, current == null ? null : current.clone()); + } + } + } + return snapshots; + } + + private void restoreSlots(@Nonnull BlockMenu blockMenu, @Nonnull Map snapshots) { + for (Map.Entry entry : snapshots.entrySet()) { + blockMenu.replaceExistingItem( + entry.getKey(), + entry.getValue() == null ? null : entry.getValue().clone()); + } + blockMenu.markDirty(); + } + @Nullable private Block getTargetBlock(@Nonnull Block block) { BlockMenu blockMenu = StorageCacheUtils.getMenu(block.getLocation()); if (blockMenu == null) return null; @@ -155,6 +257,7 @@ private boolean checkAndCleanup(@Nonnull Location location) { .getLogger() .log(Level.WARNING, "CookingAllocator at {0} lost target block, clearing state", location); recipeCache.remove(location); + baselineCache.remove(location); runningCache.remove(location); clearState(location); return false; @@ -171,6 +274,7 @@ protected BlockBreakHandler onBlockBreak() { public void onBlockBreak(@Nonnull Block block) { Location loc = block.getLocation(); recipeCache.remove(loc); + baselineCache.remove(loc); runningCache.remove(loc); clearState(loc); @@ -226,22 +330,22 @@ public boolean canStartCrafting(@Nonnull Block block, @Nonnull CraftingRecipe re ItemStack[] outputs = ItemUtils.trimItems(recipe.getOutput()); if (outputs.length == 0) return false; - int[] inputSlots; - int[] outputSlots; - try { - inputSlots = targetMenu - .getPreset() - .getSlotsAccessedByItemTransport(targetMenu, ItemTransportFlow.INSERT, inputs[0]); - outputSlots = targetMenu - .getPreset() - .getSlotsAccessedByItemTransport(targetMenu, ItemTransportFlow.WITHDRAW, outputs[0]); - } catch (IllegalArgumentException e) { - return false; + for (ItemStack input : inputs) { + int[] inputSlots = getTransportSlots(targetMenu, ItemTransportFlow.INSERT, input); + if (inputSlots == null + || !InvUtils.fitAll(targetMenu.getInventory(), new ItemStack[] {input}, inputSlots)) { + return false; + } + } + for (ItemStack output : outputs) { + int[] outputSlots = getTransportSlots(targetMenu, ItemTransportFlow.WITHDRAW, output); + if (outputSlots == null + || !InvUtils.fitAll(targetMenu.getInventory(), new ItemStack[] {output}, outputSlots)) { + return false; + } } - if (inputSlots == null || outputSlots == null) return false; - return InvUtils.fitAll(targetMenu.getInventory(), inputs, inputSlots) - && InvUtils.fitAll(targetMenu.getInventory(), outputs, outputSlots); + return true; } @Override @@ -250,25 +354,33 @@ public boolean startCrafting(@Nonnull Block block, @Nonnull CraftingRecipe recip Block target = getTargetBlock(block); if (target == null) return false; + BlockMenu targetMenu = StorageCacheUtils.getMenu(target.getLocation()); + if (targetMenu == null) return false; + IStorage targetStorage = ItemUtils.getStorage(target, false, false); if (targetStorage == null) return false; ItemStack[] inputs = Arrays.stream(ItemUtils.trimItems(recipe.getInput())) .map(ItemStack::clone) .toArray(ItemStack[]::new); + Map snapshots = snapshotSlots(targetMenu, inputs, ItemTransportFlow.INSERT); + ItemHashMap baseline = getOutputAmounts(targetMenu, recipe); targetStorage.pushItem(inputs); for (ItemStack input : inputs) { if (input != null && !input.getType().isAir() && input.getAmount() > 0) { + restoreSlots(targetMenu, snapshots); recipeCache.remove(loc); + baselineCache.remove(loc); runningCache.remove(loc); return false; } } recipeCache.put(loc, recipe); + baselineCache.put(loc, baseline); runningCache.add(loc); - saveState(loc, recipe); + saveState(loc, recipe, baseline); return true; } @@ -283,15 +395,22 @@ public boolean isFinished(@Nonnull Block block) { Block target = getTargetBlock(block); if (target == null) { recipeCache.remove(loc); + baselineCache.remove(loc); runningCache.remove(loc); clearState(loc); return false; } - IStorage storage = ItemUtils.getStorage(target, false, false); - if (storage == null) return false; + BlockMenu targetMenu = StorageCacheUtils.getMenu(target.getLocation()); + if (targetMenu == null) return false; - return storage.contains(ItemUtils.createRequests(ItemUtils.getAmounts(recipe.getOutput()))); + ItemHashMap baseline = baselineCache.getOrDefault(loc, new ItemHashMap<>()); + for (Map.Entry entry : recipe.getOutputAmounts().keyEntrySet()) { + long current = getItemAmount(targetMenu, entry.getKey().getItemStack()); + long start = baseline.getOrDefault(entry.getKey(), 0L); + if (current - start < entry.getValue()) return false; + } + return true; } @Override @@ -300,26 +419,35 @@ public boolean isFinished(@Nonnull Block block) { } @Override - public void finishCrafting(@Nonnull Block block) { + @Nonnull + public ItemStorage finishCrafting(@Nonnull Block block) { Location loc = block.getLocation(); CraftingRecipe recipe = recipeCache.get(loc); - if (recipe == null) return; + if (recipe == null) return new ItemStorage(); Block target = getTargetBlock(block); if (target == null) { recipeCache.remove(loc); + baselineCache.remove(loc); runningCache.remove(loc); clearState(loc); - return; + return new ItemStorage(); } IStorage targetStorage = ItemUtils.getStorage(target, false, false); - if (targetStorage != null) { - targetStorage.takeItem(ItemUtils.createRequests(ItemUtils.getAmounts(recipe.getOutput()))); + if (targetStorage == null) return new ItemStorage(); + ItemRequest[] requests = ItemUtils.createRequests(recipe.getOutputAmounts()); + ItemStorage taken = targetStorage.takeItem(requests); + if (!taken.contains(requests)) { + ItemHashMap toReturn = new ItemHashMap<>(taken.getStorageUnsafe()); + targetStorage.pushItem(toReturn); + return new ItemStorage(); } recipeCache.remove(loc); + baselineCache.remove(loc); runningCache.remove(loc); clearState(loc); + return taken; } @Override diff --git a/src/main/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbench.java b/src/main/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbench.java index 78a31dd9..6696fc58 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbench.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbench.java @@ -25,14 +25,17 @@ import me.mrCookieSlime.CSCoreLibPlugin.general.Inventory.ClickAction; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenu; import me.mrCookieSlime.Slimefun.api.inventory.BlockMenuPreset; +import org.bukkit.ChatColor; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; public class MECellWorkbench extends SlimefunItem implements InventoryBlock { + static final String FILTER_DISPLAY_LORE = ChatColor.DARK_GRAY + "禁止刷物喵"; public MECellWorkbench(ItemGroup itemGroup, SlimefunItemStack item, RecipeType recipeType, ItemStack[] recipe) { super(itemGroup, item, recipeType, recipe); @@ -73,6 +76,22 @@ public int[] getOutputSlots() { return new int[0]; } + static List createFilterDisplayLore(List sourceLore) { + List lore = sourceLore == null ? new ArrayList<>() : new ArrayList<>(sourceLore); + if (!lore.contains(FILTER_DISPLAY_LORE)) lore.add(FILTER_DISPLAY_LORE); + return lore; + } + + static ItemStack createFilterDisplayItem(@Nonnull ItemStack itemStack) { + ItemStack display = itemStack.clone(); + display.setAmount(1); + ItemMeta meta = display.getItemMeta(); + if (meta == null) return display; + meta.setLore(createFilterDisplayLore(meta.getLore())); + display.setItemMeta(meta); + return display; + } + @Override public void init(@Nonnull BlockMenuPreset preset) { for (int slot : getBorderSlots()) { @@ -218,7 +237,7 @@ public boolean onClick( for (int i = 0; i < itemKeys.size(); i++) { ItemKey itemKey = itemKeys.get(i); - menu.replaceExistingItem(getSettingSlots()[i], itemKey.getItemStack()); + menu.replaceExistingItem(getSettingSlots()[i], createFilterDisplayItem(itemKey.getItemStack())); menu.addMenuClickHandler(getSettingSlots()[i], (player, i1, cursor, clickAction) -> { data.getFilters().remove(itemKey); data.updateItemTypes(); diff --git a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEAllInOneTerminal.java b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEAllInOneTerminal.java index 3584030f..2cb183e4 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEAllInOneTerminal.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/terminals/MEAllInOneTerminal.java @@ -273,9 +273,13 @@ private void triggerCraftPlanning(Player player, CraftingRecipe recipe, NetworkI task.dispose(); return false; } - player.sendMessage(CMIChatColor.translate("&a&l成功规划了合成任务")); - task.refreshGUI(54); - task.start(); + if (task.start()) { + player.sendMessage(CMIChatColor.translate("&a&l成功规划了合成任务")); + task.refreshGUI(54); + } else { + player.sendMessage(CMIChatColor.translate("&c&l网络状态已变化,合成任务未启动")); + task.dispose(); + } return false; }); menu.replaceExistingItem(51, MenuItems.CANCEL); 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 7b88c68f..aac84e2e 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 @@ -196,9 +196,13 @@ private void handleItemClick(Player player, RecipeEntry recipeEntry, NetworkInfo task.dispose(); return false; } - player.sendMessage(CMIChatColor.translate("&a&l成功规划了合成任务")); - task.refreshGUI(54); - task.start(); + if (task.start()) { + player.sendMessage(CMIChatColor.translate("&a&l成功规划了合成任务")); + task.refreshGUI(54); + } else { + player.sendMessage(CMIChatColor.translate("&c&l网络状态已变化,合成任务未启动")); + task.dispose(); + } return false; }); menu.replaceExistingItem(cancelSlot, MenuItems.CANCEL); diff --git a/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java b/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java index 870c2e01..ee06a29e 100644 --- a/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java +++ b/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java @@ -26,6 +26,7 @@ import org.bukkit.Location; public class NetworkTickerTask implements Runnable { + private static final int MAX_TRANSIENT_CONTROLLER_MISSES = 5; public Object2IntOpenHashMap errorTimes = new Object2IntOpenHashMap<>(); private int tickRate; private int errorResetTime = 2000; @@ -93,15 +94,20 @@ public void run0() { } SlimefunBlockData slimefunBlockData = StorageCacheUtils.getBlock(info.getController()); if (slimefunBlockData == null || !info.getController().isChunkLoaded()) { - info.dispose(); + if (info.markControllerUnavailable() >= MAX_TRANSIENT_CONTROLLER_MISSES) { + info.dispose(); + } continue; } SlimefunItem slimefunItem = SlimefunItem.getById(slimefunBlockData.getSfId()); if (!(slimefunItem instanceof IMEController)) { - info.dispose(); + if (info.markControllerUnavailable() >= MAX_TRANSIENT_CONTROLLER_MISSES) { + info.dispose(); + } continue; } + info.markControllerAvailable(); info.getVirtualCraftingDeviceUsed().clear(); info.updateTempStorage(); diff --git a/src/main/java/me/ddggdd135/slimeae/utils/NetworkUtils.java b/src/main/java/me/ddggdd135/slimeae/utils/NetworkUtils.java index fd6256fa..66886052 100644 --- a/src/main/java/me/ddggdd135/slimeae/utils/NetworkUtils.java +++ b/src/main/java/me/ddggdd135/slimeae/utils/NetworkUtils.java @@ -22,6 +22,44 @@ import org.bukkit.inventory.ItemStack; public class NetworkUtils { + private static boolean loadNetworkObject(@Nonnull Location location) { + if (!location.isChunkLoaded()) return false; + SlimefunBlockData blockData = StorageCacheUtils.getBlock(location); + if (blockData == null) { + removeNetworkObject(location); + return false; + } + SlimefunItem slimefunItem = SlimefunItem.getById(blockData.getSfId()); + if (!(slimefunItem instanceof IMEObject imeObject)) { + removeNetworkObject(location); + return false; + } + SlimeAEPlugin.getNetworkData().AllNetworkBlocks.put(location, imeObject); + if (slimefunItem instanceof IMEController controller) { + SlimeAEPlugin.getNetworkData().AllControllers.put(location, controller); + } else { + SlimeAEPlugin.getNetworkData().AllControllers.remove(location); + } + if (slimefunItem instanceof IMEStorageObject storageObject) { + SlimeAEPlugin.getNetworkData().AllStorageObjects.put(location, storageObject); + } else { + SlimeAEPlugin.getNetworkData().AllStorageObjects.remove(location); + } + if (slimefunItem instanceof IMECraftHolder craftHolder) { + SlimeAEPlugin.getNetworkData().AllCraftHolders.put(location, craftHolder); + } else { + SlimeAEPlugin.getNetworkData().AllCraftHolders.remove(location); + } + return true; + } + + private static void removeNetworkObject(@Nonnull Location location) { + SlimeAEPlugin.getNetworkData().AllNetworkBlocks.remove(location); + SlimeAEPlugin.getNetworkData().AllControllers.remove(location); + SlimeAEPlugin.getNetworkData().AllStorageObjects.remove(location); + SlimeAEPlugin.getNetworkData().AllCraftHolders.remove(location); + } + public static void scan(Block block, Set blocks) { Stack stack = new Stack<>(); stack.push(block.getLocation()); @@ -31,30 +69,12 @@ public static void scan(Block block, Set blocks) { Location testLocation = next.clone().add(blockFace.getDirection()); if (blocks.contains(testLocation)) continue; if (SlimeAEPlugin.getNetworkData().AllNetworkBlocks.containsKey(testLocation)) { + if (!loadNetworkObject(testLocation)) continue; blocks.add(testLocation); stack.push(testLocation); } else { - SlimefunBlockData blockData = StorageCacheUtils.getBlock(testLocation); - if (blockData == null) { - continue; - } - SlimefunItem slimefunItem = SlimefunItem.getById(blockData.getSfId()); - if (slimefunItem instanceof IMEObject IMEObject) { + if (loadNetworkObject(testLocation)) { blocks.add(testLocation); - SlimeAEPlugin.getNetworkData().AllNetworkBlocks.put(testLocation, IMEObject); - - if (slimefunItem instanceof IMEController IMEController) { - SlimeAEPlugin.getNetworkData().AllControllers.put(testLocation, IMEController); - } - - if (slimefunItem instanceof IMEStorageObject IMEStorageObject) { - SlimeAEPlugin.getNetworkData().AllStorageObjects.put(testLocation, IMEStorageObject); - } - - if (slimefunItem instanceof IMECraftHolder IMECraftHolder) { - SlimeAEPlugin.getNetworkData().AllCraftHolders.put(testLocation, IMECraftHolder); - } - stack.push(testLocation); } } @@ -79,6 +99,7 @@ public static Set scan(Block block) { Location testLocation = next.clone().add(blockFace.getDirection()); if (visited.contains(testLocation)) continue; if (!SlimeAEPlugin.getNetworkData().AllNetworkBlocks.containsKey(testLocation)) continue; + if (!loadNetworkObject(testLocation)) continue; visited.add(testLocation); NetworkInfo info = SlimeAEPlugin.getNetworkData().getNetworkInfo(testLocation); if (info != null) return info; @@ -122,7 +143,7 @@ public static void doCraft(@Nonnull NetworkInfo networkInfo, @Nonnull ItemStack try { // 创建并启动新的合成任务 AutoCraftingTask task = new AutoCraftingTask(networkInfo, recipe, amount / onceAmount); - task.start(); + if (!task.start()) task.dispose(); } catch (Exception e) { // 忽略合成失败的情况,等待下一次尝试 } diff --git a/src/test/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTaskProgressTest.java b/src/test/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTaskProgressTest.java new file mode 100644 index 00000000..d6b9f86c --- /dev/null +++ b/src/test/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTaskProgressTest.java @@ -0,0 +1,20 @@ +package me.ddggdd135.slimeae.api.autocraft; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AutoCraftingTaskProgressTest { + + @Test + void waitingResultDoesNotCountAsGlobalFailure() { + assertFalse(AutoCraftingTask.shouldIncrementGlobalFail(List.of(AutoCraftingTask.StepProcessResult.WAITING))); + } + + @Test + void blockedOnlyResultsCountAsGlobalFailure() { + assertTrue(AutoCraftingTask.shouldIncrementGlobalFail(List.of(AutoCraftingTask.StepProcessResult.BLOCKED))); + } +} diff --git a/src/test/java/me/ddggdd135/slimeae/api/database/v3/DirtyTrackerTest.java b/src/test/java/me/ddggdd135/slimeae/api/database/v3/DirtyTrackerTest.java new file mode 100644 index 00000000..eec53e1f --- /dev/null +++ b/src/test/java/me/ddggdd135/slimeae/api/database/v3/DirtyTrackerTest.java @@ -0,0 +1,40 @@ +package me.ddggdd135.slimeae.api.database.v3; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class DirtyTrackerTest { + + @Test + void newerSequenceWinsWhenOlderAmountArrivesLater() { + DirtyTracker tracker = new DirtyTracker(); + UUID cell = UUID.randomUUID(); + + tracker.record(cell, 7L, 260000L, 'P', 2L); + tracker.record(cell, 7L, 4096L, 'P', 1L); + + List rows = tracker.drainPhase1(); + + assertEquals(1, rows.size()); + assertEquals(260000L, rows.get(0).newAmount()); + } + + @Test + void rollbackKeepsNewerDirtyOverPendingFlush() { + DirtyTracker tracker = new DirtyTracker(); + UUID cell = UUID.randomUUID(); + + tracker.record(cell, 8L, 4096L, 'P', 1L); + tracker.drainPhase1(); + tracker.record(cell, 8L, 260000L, 'P', 2L); + tracker.rollbackFlush(); + + List rows = tracker.drainPhase1(); + + assertEquals(1, rows.size()); + assertEquals(260000L, rows.get(0).newAmount()); + } +} diff --git a/src/test/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbenchTest.java b/src/test/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbenchTest.java new file mode 100644 index 00000000..8a6bc884 --- /dev/null +++ b/src/test/java/me/ddggdd135/slimeae/core/slimefun/MECellWorkbenchTest.java @@ -0,0 +1,25 @@ +package me.ddggdd135.slimeae.core.slimefun; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class MECellWorkbenchTest { + + @Test + void filterDisplayLoreHasWorkbenchMarker() { + List lore = MECellWorkbench.createFilterDisplayLore(List.of("原始描述")); + + assertIterableEquals(List.of("原始描述", MECellWorkbench.FILTER_DISPLAY_LORE), lore); + } + + @Test + void filterDisplayLoreKeepsSingleWorkbenchMarker() { + List lore = MECellWorkbench.createFilterDisplayLore(List.of(MECellWorkbench.FILTER_DISPLAY_LORE)); + + assertIterableEquals(List.of(MECellWorkbench.FILTER_DISPLAY_LORE), lore); + assertTrue(lore.contains(MECellWorkbench.FILTER_DISPLAY_LORE)); + } +}