diff --git a/.gitignore b/.gitignore
index eb716c7..45b9569 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 6f2d1c7..956c890 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 5945a06..020b4dc 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 5147aea..916203a 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 296b9c3..4f4fa5b 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 b0a7651..c9f7aa5 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 709b79c..b0ce3bb 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 5d78cad..b8da260 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 ea2bb4b..4c0947e 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 79c28c9..ff01e30 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 9fa8dc5..1d2d52d 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 78a31dd..6696fc5 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 3584030..2cb183e 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 7b88c68..aac84e2 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 870c2e0..ee06a29 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 fd6256f..6688605 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 0000000..d6b9f86
--- /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 0000000..eec53e1
--- /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 0000000..8a6bc88
--- /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));
+ }
+}