diff --git a/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java b/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java index 76b0fa6..16486e8 100644 --- a/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java +++ b/src/main/java/me/ddggdd135/slimeae/SlimeAEPlugin.java @@ -6,6 +6,7 @@ import javax.annotation.Nullable; import me.ddggdd135.guguslimefunlib.libraries.colors.CMIChatColor; import me.ddggdd135.slimeae.api.abstracts.MEChainedBus; +import me.ddggdd135.slimeae.api.database.v3.CraftTaskPersistence; import me.ddggdd135.slimeae.api.database.v3.StorageConfig; import me.ddggdd135.slimeae.api.database.v3.V3DatabaseManager; import me.ddggdd135.slimeae.api.database.v3.V3FilterController; @@ -56,10 +57,12 @@ public final class SlimeAEPlugin extends JavaPlugin implements SlimefunAddon { private FinalTechIntegration finalTechIntegration; private V3DatabaseManager v3DatabaseManager; + private CraftTaskPersistence craftTaskPersistence; private NetworkTickerTask networkTicker; private NetworkTimeConsumingTask networkTimeConsumingTask; private DataSavingTask dataSavingTask; + private TaskBackupTask taskBackupTask; private SlimeAECommand slimeAECommand = new SlimeAECommand(); private PinnedManager pinnedManager; private Metrics metrics; @@ -85,6 +88,7 @@ public void onEnable() { networkTicker = new NetworkTickerTask(); networkTimeConsumingTask = new NetworkTimeConsumingTask(); dataSavingTask = new DataSavingTask(); + taskBackupTask = new TaskBackupTask(); slimeAECommand = new SlimeAECommand(); pinnedManager = new PinnedManager(); @@ -138,6 +142,9 @@ public void onEnable() { v3DatabaseManager.init(); + craftTaskPersistence = new CraftTaskPersistence(v3DatabaseManager); + craftTaskPersistence.initSchema(); + Bukkit.getPluginManager().registerEvents(new ReskinListener(), this); for (World world : Bukkit.getWorlds()) { @@ -147,6 +154,7 @@ public void onEnable() { networkTicker.start(this); networkTimeConsumingTask.start(this); dataSavingTask.start(this); + taskBackupTask.start(this); slimeAECommand.addSubCommand(new ApplyUUIDCommand()); slimeAECommand.addSubCommand(new CleardataCommand()); @@ -185,6 +193,17 @@ public void onDisable() { networkTimeConsumingTask.halt(); dataSavingTask.setPaused(true); dataSavingTask.halt(); + taskBackupTask.setPaused(true); + taskBackupTask.halt(); + + if (craftTaskPersistence != null) { + for (NetworkInfo info : new java.util.HashSet<>(networkData.AllNetworkData)) { + for (me.ddggdd135.slimeae.api.autocraft.AutoCraftingTask task : + new java.util.HashSet<>(info.getAutoCraftingSessions())) { + task.suspend(); + } + } + } int waitCount = 0; while (dataSavingTask.isRunning() && waitCount < 30) { @@ -348,6 +367,10 @@ public static V3DatabaseManager getV3DatabaseManager() { return getInstance().v3DatabaseManager; } + @javax.annotation.Nullable public static CraftTaskPersistence getCraftTaskPersistence() { + return getInstance().craftTaskPersistence; + } + @Nonnull public static NetworkTickerTask getNetworkTicker() { return getInstance().networkTicker; 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 7152658..145a468 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/AutoCraftingTask.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; import me.ddggdd135.guguslimefunlib.api.AEMenu; @@ -45,6 +46,7 @@ public class AutoCraftingTask implements IDisposable { private static final String MORE_ITEMS_INDICATOR = "&7... 还有%d项未显示"; public static final String CRAFTING_KEY = "auto_crafting"; + private final UUID taskId; private final CraftingRecipe recipe; private final NetworkInfo info; private final long count; @@ -52,14 +54,18 @@ public class AutoCraftingTask implements IDisposable { private final Map> stepDependencies; private final Set completedSteps = new HashSet<>(); private int globalFailTimes; + private int cancelFailTimes; private volatile List lastActiveSteps = Collections.emptyList(); private final AEMenu menu; private boolean isCancelling = false; private final Set craftingPath = new HashSet<>(); private ItemStorage storage; - private volatile boolean disposed; + private volatile TaskState taskState = TaskState.RUNNING; + private final long createdAt; public AutoCraftingTask(@Nonnull NetworkInfo info, @Nonnull CraftingRecipe recipe, long count) { + this.taskId = UUID.randomUUID(); + this.createdAt = System.currentTimeMillis(); this.info = info; this.recipe = recipe; this.count = count; @@ -179,7 +185,38 @@ public AutoCraftingTask(@Nonnull NetworkInfo info, @Nonnull CraftingRecipe recip } } - storage = info.getStorage().takeItem(ItemUtils.createRequests(storage.copyStorage())); + for (int i = 0; i < craftingSteps.size() - 1; i++) { + CraftStep step = craftingSteps.get(i); + for (Map.Entry entry : + step.getRecipe().getOutputAmounts().keyEntrySet()) { + storage.takeItem(new ItemRequest(entry.getKey(), entry.getValue() * step.getAmount())); + } + } + + ItemHashMap requested = storage.copyStorage(); + storage = info.getStorage().takeItem(ItemUtils.createRequests(requested)); + + ItemHashMap actual = storage.getStorageUnsafe(); + ItemStorage missingStorage = new ItemStorage(); + for (Map.Entry entry : requested.keyEntrySet()) { + long need = entry.getValue(); + long got = actual.getOrDefault(entry.getKey(), 0L); + if (got < need) { + missingStorage.addItem(entry.getKey(), need - got); + } + } + if (!missingStorage.getStorageUnsafe().isEmpty()) { + ItemHashMap toReturn = new ItemHashMap<>(actual); + info.getStorage().pushItem(toReturn); + ItemUtils.trim(toReturn); + if (!toReturn.isEmpty()) { + synchronized (info) { + info.getTempStorage().addItem(toReturn, true); + } + } + storage = new ItemStorage(); + throw new NoEnoughMaterialsException(missingStorage.getStorageUnsafe()); + } String TitleInfo = craftingSteps == newSteps ? "&2(新版算法)" : "&7(旧版算法)"; menu = new AEMenu("&e合成任务" + TitleInfo); @@ -187,6 +224,129 @@ public AutoCraftingTask(@Nonnull NetworkInfo info, @Nonnull CraftingRecipe recip menu.addMenuCloseHandler(player -> dispose()); } + private AutoCraftingTask( + UUID taskId, + NetworkInfo info, + CraftingRecipe recipe, + long count, + List steps, + Map> deps, + Set completed, + ItemStorage storage, + int globalFailTimes, + int cancelFailTimes, + boolean isCancelling, + long createdAt) { + this.taskId = taskId; + this.info = info; + this.recipe = recipe; + this.count = count; + this.craftingSteps = steps; + this.stepDependencies = deps; + this.completedSteps.addAll(completed); + this.storage = storage; + this.globalFailTimes = globalFailTimes; + this.cancelFailTimes = cancelFailTimes; + this.isCancelling = isCancelling; + this.createdAt = createdAt; + this.menu = new AEMenu("&e合成任务 &d(恢复)"); + this.menu.setSize(54); + this.menu.addMenuCloseHandler(player -> dispose()); + } + + public static AutoCraftingTask restore( + UUID taskId, + NetworkInfo info, + CraftingRecipe recipe, + long count, + List steps, + Map> deps, + Set completed, + ItemStorage storage, + int globalFailTimes, + int cancelFailTimes, + boolean isCancelling, + long createdAt) { + return new AutoCraftingTask( + taskId, + info, + recipe, + count, + steps, + deps, + completed, + storage, + globalFailTimes, + cancelFailTimes, + isCancelling, + createdAt); + } + + public synchronized void suspend() { + if (taskState != TaskState.RUNNING && taskState != TaskState.CANCELLING) return; + taskState = TaskState.SUSPENDED; + + for (CraftStep step : craftingSteps) { + if (step.getVirtualRunning() > 0) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + step.getRecipe().getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue() * step.getVirtualRunning()); + } + storage.addItem(refund); + step.setAmount(step.getAmount() + step.getVirtualRunning()); + step.setVirtualRunning(0); + step.setVirtualProcess(0); + } + + if (step.getRecipe().getCraftType().isProcess()) { + Set toRemove = new HashSet<>(); + for (Location deviceLoc : step.getRunningDevices()) { + Block deviceBlock = deviceLoc.getBlock(); + var blockData = StorageCacheUtils.getBlock(deviceLoc); + if (blockData == null) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + step.getRecipe().getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue()); + } + storage.addItem(refund); + toRemove.add(deviceLoc); + continue; + } + SlimefunItem sfItem = SlimefunItem.getById(blockData.getSfId()); + if (!(sfItem instanceof IMERealCraftDevice device)) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + step.getRecipe().getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue()); + } + storage.addItem(refund); + toRemove.add(deviceLoc); + continue; + } + 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); + } + } + } + for (Location loc : toRemove) { + step.removeRunningDevice(loc); + } + } + } + + if (SlimeAEPlugin.getCraftTaskPersistence() != null) { + SlimeAEPlugin.getCraftTaskPersistence().save(this); + } + + info.getAutoCraftingSessions().remove(this); + } + @Nonnull public CraftingRecipe getRecipe() { return recipe; @@ -201,6 +361,42 @@ public long getCount() { return count; } + public UUID getTaskId() { + return taskId; + } + + public TaskState getTaskState() { + return taskState; + } + + public Map> getStepDependencies() { + return stepDependencies; + } + + public Set getCompletedSteps() { + return completedSteps; + } + + public ItemStorage getStorage() { + return storage; + } + + public int getGlobalFailTimes() { + return globalFailTimes; + } + + public int getCancelFailTimes() { + return cancelFailTimes; + } + + public boolean isCancelling() { + return isCancelling; + } + + public long getCreatedAt() { + return createdAt; + } + @Nonnull public List getCraftingSteps() { return craftingSteps; @@ -419,30 +615,43 @@ private void handleCancellation() { CraftType craftType = nextRecipe.getCraftType(); if (craftType.isProcess()) { - List holderLocations = - info.getRecipeToHolders().getOrDefault(nextRecipe, Collections.emptyList()); - Map deviceCache = info.getCachedCraftingDevices(); - for (Location location : holderLocations) { - IMECraftHolder holder = - SlimeAEPlugin.getNetworkData().AllCraftHolders.get(location); - if (holder == null) continue; - Block[] devices = deviceCache.get(location); - if (devices == null) devices = holder.getCraftingDevices(location.getBlock()); - for (Block deviceBlock : devices) { - var blockData = StorageCacheUtils.getBlock(deviceBlock.getLocation()); - if (blockData == null) continue; - SlimefunItem sfItem = SlimefunItem.getById(blockData.getSfId()); - if (!(sfItem instanceof IMERealCraftDevice device)) continue; - if (step.getRunning() > 0 - && device.isFinished(deviceBlock) - && device.getFinishedCraftingRecipe(deviceBlock) - .equals(nextRecipe)) { + Set toRemove = new HashSet<>(); + for (Location deviceLoc : step.getRunningDevices()) { + Block deviceBlock = deviceLoc.getBlock(); + var blockData = StorageCacheUtils.getBlock(deviceLoc); + if (blockData == null) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + nextRecipe.getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue()); + } + storage.addItem(refund); + toRemove.add(deviceLoc); + continue; + } + SlimefunItem sfItem = SlimefunItem.getById(blockData.getSfId()); + if (!(sfItem instanceof IMERealCraftDevice device)) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + nextRecipe.getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue()); + } + storage.addItem(refund); + toRemove.add(deviceLoc); + continue; + } + if (device.isFinished(deviceBlock)) { + CraftingRecipe finished = device.getFinishedCraftingRecipe(deviceBlock); + if (finished != null && finished.equals(nextRecipe)) { device.finishCrafting(deviceBlock); storage.addItem(nextRecipe.getOutput()); - step.decrementRunning(); + toRemove.add(deviceLoc); } } } + for (Location loc : toRemove) { + step.removeRunningDevice(loc); + } } if (step.getVirtualRunning() > 0) { @@ -459,6 +668,27 @@ private void handleCancellation() { if (!anyRunning) { dispose(); + return; + } + + cancelFailTimes++; + if (cancelFailTimes >= 64) { + for (CraftStep step : craftingSteps) { + if (step.getRunning() > 0) { + CraftingRecipe nextRecipe = step.getRecipe(); + int remaining = step.getRunning(); + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + nextRecipe.getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue() * remaining); + } + storage.addItem(refund); + for (Location loc : new HashSet<>(step.getRunningDevices())) { + step.removeRunningDevice(loc); + } + } + } + dispose(); } } @@ -478,40 +708,69 @@ private boolean processStep(CraftStep step, int maxDevices) { List holderLocations = info.getRecipeToHolders().getOrDefault(nextRecipe, Collections.emptyList()); if (craftType.isProcess()) { - Map deviceCache = info.getCachedCraftingDevices(); - for (Location location : holderLocations) { - IMECraftHolder holder = - SlimeAEPlugin.getNetworkData().AllCraftHolders.get(location); - if (holder == null) continue; - Block[] devices = deviceCache.get(location); - if (devices == null) devices = holder.getCraftingDevices(location.getBlock()); - for (Block deviceBlock : devices) { - var blockData = StorageCacheUtils.getBlock(deviceBlock.getLocation()); - if (blockData == null) continue; - SlimefunItem sfItem = SlimefunItem.getById(blockData.getSfId()); - if (!(sfItem instanceof IMERealCraftDevice device)) continue; - if (!device.isSupport(deviceBlock, nextRecipe)) continue; - if (step.getRunning() < maxDevices && doCraft && device.canStartCrafting(deviceBlock, nextRecipe)) { - ItemRequest[] inputRequests = ItemUtils.createRequests(nextRecipe.getInputAmounts()); - if (storage.contains(inputRequests)) { - storage.takeItem(inputRequests); - device.startCrafting(deviceBlock, nextRecipe); - step.incrementRunning(); - step.decreaseAmount(1); - hasProgress = true; - if (step.getAmount() <= 0) doCraft = false; - } - } else if (step.getRunning() > 0 - && device.isFinished(deviceBlock) - && device.getFinishedCraftingRecipe(deviceBlock).equals(nextRecipe)) { - CraftingRecipe finished = device.getFinishedCraftingRecipe(deviceBlock); + Set invalidDevices = new HashSet<>(); + for (Location deviceLoc : step.getRunningDevices()) { + Block deviceBlock = deviceLoc.getBlock(); + var blockData = StorageCacheUtils.getBlock(deviceLoc); + if (blockData == null) { + invalidDevices.add(deviceLoc); + continue; + } + SlimefunItem sfItem = SlimefunItem.getById(blockData.getSfId()); + if (!(sfItem instanceof IMERealCraftDevice device)) { + invalidDevices.add(deviceLoc); + continue; + } + if (device.isFinished(deviceBlock)) { + CraftingRecipe finished = device.getFinishedCraftingRecipe(deviceBlock); + if (finished != null && finished.equals(nextRecipe)) { device.finishCrafting(deviceBlock); storage.addItem(finished.getOutput()); - step.decrementRunning(); + invalidDevices.add(deviceLoc); hasProgress = true; } } } + for (Location loc : invalidDevices) { + step.removeRunningDevice(loc); + } + + if (doCraft) { + Map deviceCache = info.getCachedCraftingDevices(); + for (Location location : holderLocations) { + IMECraftHolder holder = + SlimeAEPlugin.getNetworkData().AllCraftHolders.get(location); + if (holder == null) continue; + Block[] devices = deviceCache.get(location); + if (devices == null) devices = holder.getCraftingDevices(location.getBlock()); + for (Block deviceBlock : devices) { + if (step.getAmount() <= 0 || step.getRunning() >= maxDevices) break; + var blockData = StorageCacheUtils.getBlock(deviceBlock.getLocation()); + if (blockData == null) continue; + SlimefunItem sfItem = SlimefunItem.getById(blockData.getSfId()); + if (!(sfItem instanceof IMERealCraftDevice device)) continue; + if (!device.isSupport(deviceBlock, nextRecipe)) continue; + if (device.canStartCrafting(deviceBlock, nextRecipe)) { + ItemRequest[] inputRequests = ItemUtils.createRequests(nextRecipe.getInputAmounts()); + if (storage.contains(inputRequests)) { + storage.takeItem(inputRequests); + if (device.startCrafting(deviceBlock, nextRecipe)) { + step.addRunningDevice(deviceBlock.getLocation()); + step.decreaseAmount(1); + hasProgress = true; + } else { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry re : + nextRecipe.getInputAmounts().keyEntrySet()) { + refund.putKey(re.getKey(), re.getValue()); + } + storage.addItem(refund); + } + } + } + } + } + } } int totalSpeed = info.getVirtualCraftingDeviceSpeeds().getOrDefault(craftType, 0); @@ -577,8 +836,6 @@ private boolean processStep(CraftStep step, int maxDevices) { } } - if (!step.isIdle()) hasProgress = true; - return hasProgress; } @@ -712,8 +969,12 @@ public AEMenu getMenu() { @Override public synchronized void dispose() { - if (disposed) return; - disposed = true; + if (taskState == TaskState.DISPOSED) return; + taskState = TaskState.DISPOSED; + + if (SlimeAEPlugin.getCraftTaskPersistence() != null) { + SlimeAEPlugin.getCraftTaskPersistence().delete(taskId); + } AutoCraftingTaskDisposingEvent e = new AutoCraftingTaskDisposingEvent(this); Bukkit.getPluginManager().callEvent(e); @@ -757,7 +1018,9 @@ public synchronized void dispose() { + ItemUtils.getItemName(de.getKey().getItemStack()) + " = " + de.getValue()); } } - info.getTempStorage().addItem(toReturn, true); + synchronized (info) { + info.getTempStorage().addItem(toReturn, true); + } } else { if (SlimeAEPlugin.isDebug()) { debugLog.info("[AutoCraft-Debug] 所有物品已成功推回存储"); diff --git a/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftStep.java b/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftStep.java index ffe6dda..4c94936 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftStep.java +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftStep.java @@ -1,11 +1,15 @@ package me.ddggdd135.slimeae.api.autocraft; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; +import org.bukkit.Location; public class CraftStep { private final CraftingRecipe recipe; private long amount; - private int running; + private final Set runningDevices = Collections.newSetFromMap(new ConcurrentHashMap<>()); private int virtualRunning; private int virtualProcess; @@ -32,19 +36,32 @@ public void decreaseAmount(long value) { } public int getRunning() { - return running; + return runningDevices.size(); + } + + public void addRunningDevice(@Nonnull Location location) { + runningDevices.add(location); + } + + public void removeRunningDevice(@Nonnull Location location) { + runningDevices.remove(location); + } + + @Nonnull + public Set getRunningDevices() { + return Collections.unmodifiableSet(runningDevices); } public void setRunning(int running) { - this.running = running; + // no-op kept for compatibility } public void incrementRunning() { - this.running++; + // no-op - use addRunningDevice instead } public void decrementRunning() { - this.running--; + // no-op - use removeRunningDevice instead } public int getVirtualRunning() { @@ -68,7 +85,7 @@ public void addVirtualProcess(int value) { } public boolean isIdle() { - return running <= 0 && virtualRunning <= 0; + return runningDevices.isEmpty() && virtualRunning <= 0; } public boolean isCompleted() { diff --git a/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftTaskSerializer.java b/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftTaskSerializer.java new file mode 100644 index 0000000..f47ec56 --- /dev/null +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/CraftTaskSerializer.java @@ -0,0 +1,231 @@ +package me.ddggdd135.slimeae.api.autocraft; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.nio.charset.StandardCharsets; +import java.util.*; +import me.ddggdd135.guguslimefunlib.items.ItemKey; +import me.ddggdd135.slimeae.api.items.ItemStorage; +import me.ddggdd135.slimeae.utils.SerializeUtils; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; + +public final class CraftTaskSerializer { + + private CraftTaskSerializer() {} + + public static byte[] serializeRecipe(CraftingRecipe recipe) { + JsonObject obj = new JsonObject(); + obj.addProperty("craftType", recipe.getCraftType().name()); + obj.add("input", serializeItemArray(recipe.getInput())); + obj.add("output", serializeItemArray(recipe.getOutput())); + return obj.toString().getBytes(StandardCharsets.UTF_8); + } + + public static CraftingRecipe deserializeRecipe(byte[] data) { + JsonObject obj = + JsonParser.parseString(new String(data, StandardCharsets.UTF_8)).getAsJsonObject(); + String typeName = obj.get("craftType").getAsString(); + CraftType craftType = CraftType.fromName(typeName); + if (craftType == null) return null; + ItemStack[] input = deserializeItemArray(obj.getAsJsonArray("input")); + ItemStack[] output = deserializeItemArray(obj.getAsJsonArray("output")); + if (input == null || output == null) return null; + return new CraftingRecipe(craftType, input, output); + } + + public static byte[] serializeSteps(List steps) { + JsonArray arr = new JsonArray(); + for (int i = 0; i < steps.size(); i++) { + CraftStep step = steps.get(i); + JsonObject obj = new JsonObject(); + obj.addProperty("index", i); + obj.add( + "recipe", + JsonParser.parseString(new String(serializeRecipe(step.getRecipe()), StandardCharsets.UTF_8))); + obj.addProperty("amount", step.getAmount()); + JsonArray devices = new JsonArray(); + for (Location loc : step.getRunningDevices()) { + devices.add(serializeLocation(loc)); + } + obj.add("runningDevices", devices); + obj.addProperty("virtualRunning", step.getVirtualRunning()); + obj.addProperty("virtualProcess", step.getVirtualProcess()); + arr.add(obj); + } + return arr.toString().getBytes(StandardCharsets.UTF_8); + } + + public static List deserializeSteps(byte[] data) { + JsonArray arr = + JsonParser.parseString(new String(data, StandardCharsets.UTF_8)).getAsJsonArray(); + List steps = new ArrayList<>(); + for (JsonElement elem : arr) { + JsonObject obj = elem.getAsJsonObject(); + byte[] recipeBytes = obj.get("recipe").toString().getBytes(StandardCharsets.UTF_8); + CraftingRecipe recipe = deserializeRecipe(recipeBytes); + if (recipe == null) return null; + long amount = obj.get("amount").getAsLong(); + CraftStep step = new CraftStep(recipe, amount); + if (obj.has("runningDevices")) { + for (JsonElement devElem : obj.getAsJsonArray("runningDevices")) { + Location loc = deserializeLocation(devElem.getAsJsonObject()); + if (loc != null) step.addRunningDevice(loc); + } + } + step.setVirtualRunning( + obj.has("virtualRunning") ? obj.get("virtualRunning").getAsInt() : 0); + step.setVirtualProcess( + obj.has("virtualProcess") ? obj.get("virtualProcess").getAsInt() : 0); + steps.add(step); + } + return steps; + } + + public static byte[] serializeDeps(Map> deps, List steps) { + Map stepIndex = new IdentityHashMap<>(); + for (int i = 0; i < steps.size(); i++) { + stepIndex.put(steps.get(i), i); + } + JsonObject obj = new JsonObject(); + for (Map.Entry> entry : deps.entrySet()) { + Integer idx = stepIndex.get(entry.getKey()); + if (idx == null) continue; + JsonArray depArr = new JsonArray(); + for (CraftStep dep : entry.getValue()) { + Integer depIdx = stepIndex.get(dep); + if (depIdx != null) depArr.add(depIdx); + } + obj.add(String.valueOf(idx), depArr); + } + return obj.toString().getBytes(StandardCharsets.UTF_8); + } + + public static Map> deserializeDeps(byte[] data, List steps) { + Map> deps = new HashMap<>(); + JsonObject obj = + JsonParser.parseString(new String(data, StandardCharsets.UTF_8)).getAsJsonObject(); + for (Map.Entry entry : obj.entrySet()) { + int idx = Integer.parseInt(entry.getKey()); + if (idx < 0 || idx >= steps.size()) continue; + CraftStep step = steps.get(idx); + Set depSet = new HashSet<>(); + for (JsonElement e : entry.getValue().getAsJsonArray()) { + int depIdx = e.getAsInt(); + if (depIdx >= 0 && depIdx < steps.size()) { + depSet.add(steps.get(depIdx)); + } + } + if (!depSet.isEmpty()) deps.put(step, depSet); + } + return deps; + } + + public static byte[] serializeStorage(ItemStorage storage) { + JsonArray arr = new JsonArray(); + for (Map.Entry entry : storage.getStorageUnsafe().keyEntrySet()) { + if (entry.getValue() <= 0) continue; + JsonObject obj = new JsonObject(); + obj.addProperty("item", SerializeUtils.object2String(entry.getKey().getItemStack())); + obj.addProperty("amount", entry.getValue()); + arr.add(obj); + } + return arr.toString().getBytes(StandardCharsets.UTF_8); + } + + public static ItemStorage deserializeStorage(byte[] data) { + ItemStorage storage = new ItemStorage(); + JsonArray arr = + JsonParser.parseString(new String(data, StandardCharsets.UTF_8)).getAsJsonArray(); + for (JsonElement elem : arr) { + JsonObject obj = elem.getAsJsonObject(); + String itemStr = obj.get("item").getAsString(); + long amount = obj.get("amount").getAsLong(); + Object deserialized = SerializeUtils.string2Object(itemStr); + if (deserialized instanceof ItemStack itemStack) { + storage.addItem(new ItemKey(itemStack), amount); + } + } + return storage; + } + + public static String serializeCompletedSteps(Set completed, List allSteps) { + Map stepIndex = new IdentityHashMap<>(); + for (int i = 0; i < allSteps.size(); i++) { + stepIndex.put(allSteps.get(i), i); + } + StringBuilder sb = new StringBuilder(); + for (CraftStep step : completed) { + Integer idx = stepIndex.get(step); + if (idx != null) { + if (sb.length() > 0) sb.append(","); + sb.append(idx); + } + } + return sb.toString(); + } + + public static Set deserializeCompletedIndices(String data) { + Set indices = new HashSet<>(); + if (data == null || data.isEmpty()) return indices; + for (String s : data.split(",")) { + try { + indices.add(Integer.parseInt(s.trim())); + } catch (NumberFormatException ignored) { + } + } + return indices; + } + + private static JsonArray serializeItemArray(ItemStack[] items) { + JsonArray arr = new JsonArray(); + for (ItemStack item : items) { + if (item == null || item.getType().isAir()) continue; + JsonObject obj = new JsonObject(); + obj.addProperty("item", SerializeUtils.object2String(item)); + obj.addProperty("amount", item.getAmount()); + arr.add(obj); + } + return arr; + } + + private static ItemStack[] deserializeItemArray(JsonArray arr) { + List list = new ArrayList<>(); + for (JsonElement elem : arr) { + JsonObject obj = elem.getAsJsonObject(); + String itemStr = obj.get("item").getAsString(); + int amount = obj.get("amount").getAsInt(); + Object deserialized = SerializeUtils.string2Object(itemStr); + if (deserialized instanceof ItemStack itemStack) { + itemStack.setAmount(amount); + list.add(itemStack); + } else { + return null; + } + } + return list.toArray(new ItemStack[0]); + } + + private static JsonObject serializeLocation(Location loc) { + JsonObject obj = new JsonObject(); + obj.addProperty("world", loc.getWorld().getName()); + obj.addProperty("x", loc.getBlockX()); + obj.addProperty("y", loc.getBlockY()); + obj.addProperty("z", loc.getBlockZ()); + return obj; + } + + private static Location deserializeLocation(JsonObject obj) { + String worldName = obj.get("world").getAsString(); + World world = Bukkit.getWorld(worldName); + if (world == null) return null; + int x = obj.get("x").getAsInt(); + int y = obj.get("y").getAsInt(); + int z = obj.get("z").getAsInt(); + return new Location(world, x, y, z); + } +} diff --git a/src/main/java/me/ddggdd135/slimeae/api/autocraft/TaskState.java b/src/main/java/me/ddggdd135/slimeae/api/autocraft/TaskState.java new file mode 100644 index 0000000..19375dc --- /dev/null +++ b/src/main/java/me/ddggdd135/slimeae/api/autocraft/TaskState.java @@ -0,0 +1,8 @@ +package me.ddggdd135.slimeae.api.autocraft; + +public enum TaskState { + RUNNING, + SUSPENDED, + CANCELLING, + DISPOSED +} 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 new file mode 100644 index 0000000..536daed --- /dev/null +++ b/src/main/java/me/ddggdd135/slimeae/api/database/v3/CraftTaskPersistence.java @@ -0,0 +1,336 @@ +package me.ddggdd135.slimeae.api.database.v3; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import me.ddggdd135.guguslimefunlib.api.ItemHashMap; +import me.ddggdd135.guguslimefunlib.items.ItemKey; +import me.ddggdd135.slimeae.SlimeAEPlugin; +import me.ddggdd135.slimeae.api.autocraft.*; +import me.ddggdd135.slimeae.api.items.ItemStorage; +import me.ddggdd135.slimeae.api.items.StorageCollection; +import me.ddggdd135.slimeae.core.NetworkInfo; +import me.ddggdd135.slimeae.utils.ItemUtils; +import org.bukkit.Location; + +public class CraftTaskPersistence { + private static final Logger logger = Logger.getLogger("SlimeAE-CraftTask"); + private final ConnectionManager connMgr; + private final boolean mysql; + + public CraftTaskPersistence(V3DatabaseManager dbManager) { + this.connMgr = dbManager.getConnectionManager(); + this.mysql = connMgr.isMysql(); + } + + public void initSchema() { + try (Connection conn = connMgr.getWriteConnection()) { + String ddl; + if (mysql) { + ddl = "CREATE TABLE IF NOT EXISTS ae_v3_craft_tasks (" + + "task_id VARCHAR(36) PRIMARY KEY, " + + "controller_world VARCHAR(128) NOT NULL, " + + "controller_x INT NOT NULL, " + + "controller_y INT NOT NULL, " + + "controller_z INT NOT NULL, " + + "recipe_data MEDIUMBLOB NOT NULL, " + + "task_count BIGINT NOT NULL, " + + "steps_data MEDIUMBLOB NOT NULL, " + + "deps_data MEDIUMBLOB NOT NULL, " + + "completed_idx TEXT NOT NULL, " + + "storage_data MEDIUMBLOB NOT NULL, " + + "global_fail INT NOT NULL DEFAULT 0, " + + "cancel_fail INT NOT NULL DEFAULT 0, " + + "is_cancelling TINYINT NOT NULL DEFAULT 0, " + + "created_at BIGINT NOT NULL, " + + "suspended_at BIGINT NOT NULL" + + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; + } else { + ddl = "CREATE TABLE IF NOT EXISTS ae_v3_craft_tasks (" + + "task_id TEXT PRIMARY KEY, " + + "controller_world TEXT NOT NULL, " + + "controller_x INTEGER NOT NULL, " + + "controller_y INTEGER NOT NULL, " + + "controller_z INTEGER NOT NULL, " + + "recipe_data BLOB NOT NULL, " + + "task_count INTEGER NOT NULL, " + + "steps_data BLOB NOT NULL, " + + "deps_data BLOB NOT NULL, " + + "completed_idx TEXT NOT NULL, " + + "storage_data BLOB NOT NULL, " + + "global_fail INTEGER NOT NULL DEFAULT 0, " + + "cancel_fail INTEGER NOT NULL DEFAULT 0, " + + "is_cancelling INTEGER NOT NULL DEFAULT 0, " + + "created_at INTEGER NOT NULL, " + + "suspended_at INTEGER NOT NULL)"; + } + conn.createStatement().execute(ddl); + + String idx; + if (mysql) { + idx = + "CREATE INDEX idx_craft_tasks_ctrl ON ae_v3_craft_tasks(controller_world, controller_x, controller_y, controller_z)"; + try { + conn.createStatement().execute(idx); + } catch (SQLException ignored) { + } + } else { + idx = + "CREATE INDEX IF NOT EXISTS idx_craft_tasks_ctrl ON ae_v3_craft_tasks(controller_world, controller_x, controller_y, controller_z)"; + conn.createStatement().execute(idx); + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to create ae_v3_craft_tasks table", e); + } + } + + public void save(AutoCraftingTask task) { + String upsert; + if (mysql) { + upsert = "INSERT INTO ae_v3_craft_tasks " + + "(task_id, controller_world, controller_x, controller_y, controller_z, " + + "recipe_data, task_count, steps_data, deps_data, completed_idx, " + + "storage_data, global_fail, cancel_fail, is_cancelling, created_at, suspended_at) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) " + + "ON DUPLICATE KEY UPDATE " + + "recipe_data=VALUES(recipe_data), task_count=VALUES(task_count), " + + "steps_data=VALUES(steps_data), deps_data=VALUES(deps_data), " + + "completed_idx=VALUES(completed_idx), storage_data=VALUES(storage_data), " + + "global_fail=VALUES(global_fail), cancel_fail=VALUES(cancel_fail), " + + "is_cancelling=VALUES(is_cancelling), suspended_at=VALUES(suspended_at)"; + } else { + upsert = "INSERT OR REPLACE INTO ae_v3_craft_tasks " + + "(task_id, controller_world, controller_x, controller_y, controller_z, " + + "recipe_data, task_count, steps_data, deps_data, completed_idx, " + + "storage_data, global_fail, cancel_fail, is_cancelling, created_at, suspended_at) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + } + try (Connection conn = connMgr.getWriteConnection(); + PreparedStatement ps = conn.prepareStatement(upsert)) { + Location ctrl = task.getNetworkInfo().getController(); + ps.setString(1, task.getTaskId().toString()); + ps.setString(2, ctrl.getWorld().getName()); + ps.setInt(3, ctrl.getBlockX()); + ps.setInt(4, ctrl.getBlockY()); + ps.setInt(5, ctrl.getBlockZ()); + ps.setBytes(6, CraftTaskSerializer.serializeRecipe(task.getRecipe())); + ps.setLong(7, task.getCount()); + ps.setBytes(8, CraftTaskSerializer.serializeSteps(task.getCraftingSteps())); + ps.setBytes(9, CraftTaskSerializer.serializeDeps(task.getStepDependencies(), task.getCraftingSteps())); + ps.setString( + 10, CraftTaskSerializer.serializeCompletedSteps(task.getCompletedSteps(), task.getCraftingSteps())); + ps.setBytes(11, CraftTaskSerializer.serializeStorage(task.getStorage())); + ps.setInt(12, task.getGlobalFailTimes()); + ps.setInt(13, task.getCancelFailTimes()); + ps.setInt(14, task.isCancelling() ? 1 : 0); + ps.setLong(15, task.getCreatedAt()); + ps.setLong(16, System.currentTimeMillis()); + ps.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to save craft task " + task.getTaskId(), e); + } + } + + public void saveAllSync(Collection tasks) { + for (AutoCraftingTask task : tasks) { + save(task); + } + } + + public void delete(UUID taskId) { + try (Connection conn = connMgr.getWriteConnection(); + PreparedStatement ps = conn.prepareStatement("DELETE FROM ae_v3_craft_tasks WHERE task_id = ?")) { + ps.setString(1, taskId.toString()); + ps.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to delete craft task " + taskId, e); + } + } + + public int tryRestore(NetworkInfo info) { + Location ctrl = info.getController(); + String worldName = ctrl.getWorld().getName(); + int restored = 0; + + List records = new ArrayList<>(); + try (Connection conn = connMgr.getReadConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT * FROM ae_v3_craft_tasks WHERE controller_world = ? AND controller_x = ? AND controller_y = ? AND controller_z = ?")) { + ps.setString(1, worldName); + ps.setInt(2, ctrl.getBlockX()); + ps.setInt(3, ctrl.getBlockY()); + ps.setInt(4, ctrl.getBlockZ()); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + TaskRecord rec = new TaskRecord(); + rec.taskId = UUID.fromString(rs.getString("task_id")); + rec.recipeData = rs.getBytes("recipe_data"); + rec.taskCount = rs.getLong("task_count"); + rec.stepsData = rs.getBytes("steps_data"); + rec.depsData = rs.getBytes("deps_data"); + rec.completedIdx = rs.getString("completed_idx"); + rec.storageData = rs.getBytes("storage_data"); + rec.globalFail = rs.getInt("global_fail"); + rec.cancelFail = rs.getInt("cancel_fail"); + rec.isCancelling = rs.getInt("is_cancelling") != 0; + rec.createdAt = rs.getLong("created_at"); + records.add(rec); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to query suspended tasks for " + worldName + " " + ctrl, e); + return 0; + } + + for (TaskRecord rec : records) { + try { + CraftingRecipe recipe = CraftTaskSerializer.deserializeRecipe(rec.recipeData); + if (recipe == null) { + logger.warning("Cannot restore task " + rec.taskId + ": recipe deserialization failed"); + delete(rec.taskId); + continue; + } + + List steps = CraftTaskSerializer.deserializeSteps(rec.stepsData); + if (steps == null || steps.isEmpty()) { + logger.warning("Cannot restore task " + rec.taskId + ": steps deserialization failed"); + delete(rec.taskId); + continue; + } + + Map> deps = CraftTaskSerializer.deserializeDeps(rec.depsData, steps); + Set completedIndices = CraftTaskSerializer.deserializeCompletedIndices(rec.completedIdx); + Set completedSteps = new HashSet<>(); + for (int idx : completedIndices) { + if (idx >= 0 && idx < steps.size()) { + completedSteps.add(steps.get(idx)); + } + } + ItemStorage storage = CraftTaskSerializer.deserializeStorage(rec.storageData); + + for (CraftStep step : steps) { + if (step.getVirtualRunning() > 0) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + step.getRecipe().getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue() * step.getVirtualRunning()); + } + storage.addItem(refund); + step.setAmount(step.getAmount() + step.getVirtualRunning()); + step.setVirtualRunning(0); + step.setVirtualProcess(0); + } + + Set toRemove = new HashSet<>(); + for (Location deviceLoc : step.getRunningDevices()) { + if (!deviceLoc.isChunkLoaded()) { + ItemHashMap refund = new ItemHashMap<>(); + for (Map.Entry entry : + step.getRecipe().getInputAmounts().keyEntrySet()) { + refund.putKey(entry.getKey(), entry.getValue()); + } + storage.addItem(refund); + toRemove.add(deviceLoc); + } + } + for (Location loc : toRemove) { + step.removeRunningDevice(loc); + } + } + + AutoCraftingTask task = AutoCraftingTask.restore( + rec.taskId, + info, + recipe, + rec.taskCount, + steps, + deps, + completedSteps, + storage, + rec.globalFail, + rec.cancelFail, + rec.isCancelling, + rec.createdAt); + + 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); + delete(rec.taskId); + } + } + return restored; + } + + public void backupAll() { + Set allNetworks = new HashSet<>(SlimeAEPlugin.getNetworkData().AllNetworkData); + for (NetworkInfo info : allNetworks) { + if (info.isDisposed()) continue; + for (AutoCraftingTask task : info.getAutoCraftingSessions()) { + if (task.getTaskState() == TaskState.RUNNING) { + try { + save(task); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to backup task " + task.getTaskId(), e); + } + } + } + } + } + + public void cleanupExpired(long maxAgeMs) { + long cutoff = System.currentTimeMillis() - maxAgeMs; + try (Connection conn = connMgr.getWriteConnection(); + PreparedStatement ps = conn.prepareStatement( + "DELETE FROM ae_v3_craft_tasks WHERE suspended_at < ? AND suspended_at > 0")) { + ps.setLong(1, cutoff); + int deleted = ps.executeUpdate(); + if (deleted > 0) { + logger.info("Cleaned up " + deleted + " expired suspended craft tasks"); + } + } catch (SQLException e) { + logger.log(Level.WARNING, "Failed to cleanup expired tasks", e); + } + } + + private void returnMaterialsFromRecord(TaskRecord rec, NetworkInfo info) { + try { + ItemStorage storage = CraftTaskSerializer.deserializeStorage(rec.storageData); + ItemHashMap toReturn = new ItemHashMap<>(storage.getStorageUnsafe()); + StorageCollection currentStorage = info.getStorage(); + currentStorage.clearNotIncluded(); + currentStorage.clearTakeAndPushCache(); + currentStorage.pushItem(toReturn); + ItemUtils.trim(toReturn); + if (!toReturn.isEmpty()) { + synchronized (info) { + info.getTempStorage().addItem(toReturn, true); + } + } + currentStorage.invalidateStorageCache(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to return materials for task " + rec.taskId, e); + } + } + + private static class TaskRecord { + UUID taskId; + byte[] recipeData; + long taskCount; + byte[] stepsData; + byte[] depsData; + String completedIdx; + byte[] storageData; + int globalFail; + int cancelFail; + boolean isCancelling; + long createdAt; + } +} 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 8a9bdba..5d78cad 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/interfaces/IMERealCraftDevice.java +++ b/src/main/java/me/ddggdd135/slimeae/api/interfaces/IMERealCraftDevice.java @@ -8,7 +8,7 @@ public interface IMERealCraftDevice extends IMECraftDevice { boolean canStartCrafting(@Nonnull Block block, @Nonnull CraftingRecipe recipe); - void startCrafting(@Nonnull Block block, @Nonnull CraftingRecipe recipe); + boolean startCrafting(@Nonnull Block block, @Nonnull CraftingRecipe recipe); boolean isFinished(@Nonnull Block block); 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 6404d29..af496e3 100644 --- a/src/main/java/me/ddggdd135/slimeae/api/items/StorageCollection.java +++ b/src/main/java/me/ddggdd135/slimeae/api/items/StorageCollection.java @@ -62,27 +62,8 @@ public boolean removeStorage(@Nonnull IStorage storage) { return result; } - Map.Entry toRemove = null; - for (Map.Entry entry : takeCache.entrySet()) { - if (entry.getValue() == storage) { - toRemove = entry; - break; - } - } - if (toRemove != null) { - takeCache.remove(toRemove.getKey()); - } - - toRemove = null; - for (Map.Entry entry : pushCache.entrySet()) { - if (entry.getValue() == storage) { - toRemove = entry; - break; - } - } - if (toRemove != null) { - pushCache.remove(toRemove.getKey()); - } + takeCache.values().removeIf(v -> v == storage); + pushCache.values().removeIf(v -> v == storage); boolean removed = storages.remove(storage); if (removed) invalidateStorageCache(); @@ -186,7 +167,7 @@ public ItemStorage takeItem(@Nonnull ItemRequest[] requests) { long[] remaining = new long[requests.length]; for (int i = 0; i < requests.length; i++) { - remaining[i] = notIncluded.contains(requests[i].getKey()) ? 0 : requests[i].getAmount(); + remaining[i] = requests[i].getAmount(); } Map> cacheGroups = new HashMap<>(); @@ -314,6 +295,8 @@ public ItemStorage takeItem(@Nonnull ItemRequest[] requests) { for (int i = 0; i < requests.length; i++) { if (remaining[i] > 0) { notIncluded.add(requests[i].getKey()); + } else { + notIncluded.remove(requests[i].getKey()); } } @@ -332,15 +315,19 @@ public void invalidateStorageCache() { changeVersion++; } + private final Object cacheLock = new Object(); + private void adjustCache(@Nonnull ItemKey key, long delta) { - ItemHashMap cached = cachedStorage; - if (cached == null || cached instanceof CreativeItemMap) return; - Long current = cached.getKey(key); - long newValue = (current != null ? current : 0L) + delta; - if (newValue <= 0) { - cached.removeKey(key); - } else { - cached.putKey(key, newValue); + synchronized (cacheLock) { + ItemHashMap cached = cachedStorage; + if (cached == null || cached instanceof CreativeItemMap) return; + Long current = cached.getKey(key); + long newValue = (current != null ? current : 0L) + delta; + if (newValue <= 0) { + cached.removeKey(key); + } else { + cached.putKey(key, newValue); + } } } diff --git a/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java b/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java index ce95b98..79c28c9 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java +++ b/src/main/java/me/ddggdd135/slimeae/core/NetworkInfo.java @@ -166,8 +166,19 @@ public void setStorageNoNetworks(@Nonnull IStorage storage) { } @Override - public void dispose() { + public synchronized void dispose() { if (disposed) return; + + for (AutoCraftingTask task : autoCraftingTasks) { + task.suspend(); + } + + for (AutoCraftingTask task : autoCraftingTasks) { + task.dispose(); + } + + updateTempStorage(); + disposed = true; NetworkData networkData = SlimeAEPlugin.getNetworkData(); @@ -177,12 +188,6 @@ public void dispose() { networkData.locationToNetwork.remove(loc, this); } networkData.locationToNetwork.remove(controller, this); - - for (AutoCraftingTask task : autoCraftingTasks) { - task.dispose(); - } - - updateTempStorage(); } public boolean isDisposed() { @@ -395,7 +400,7 @@ public ItemStorage getTempStorage() { return tempStorage; } - public void updateTempStorage() { + public synchronized void updateTempStorage() { Set toPush = new HashSet<>(tempStorage.getStorageUnsafe().sourceKeySet()); for (ItemKey key : toPush) { ItemHashMap items = tempStorage 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 a40d1b1..9fa8dc5 100644 --- a/src/main/java/me/ddggdd135/slimeae/core/slimefun/CookingAllocator.java +++ b/src/main/java/me/ddggdd135/slimeae/core/slimefun/CookingAllocator.java @@ -83,7 +83,54 @@ private void clearState(@Nonnull Location location) { } private void restoreState(@Nonnull Location location) { - clearState(location); + SlimefunBlockData blockData = StorageCacheUtils.getBlock(location); + if (blockData == null) { + clearState(location); + return; + } + String running = blockData.getData(DATA_KEY_RUNNING); + if (!"true".equals(running)) { + clearState(location); + return; + } + String recipeTypeName = blockData.getData(DATA_KEY_RECIPE_TYPE); + String inputStr = blockData.getData(DATA_KEY_RECIPE_INPUT); + String outputStr = blockData.getData(DATA_KEY_RECIPE_OUTPUT); + if (recipeTypeName == null || inputStr == null || outputStr == null) { + clearState(location); + return; + } + try { + me.ddggdd135.slimeae.api.autocraft.CraftType craftType = + me.ddggdd135.slimeae.api.autocraft.CraftType.fromName(recipeTypeName); + if (craftType == null) { + clearState(location); + return; + } + List inputList = new ArrayList<>(); + for (String s : inputStr.split("\\|")) { + ItemStack item = (ItemStack) SerializeUtils.string2Object(s); + if (item != null) inputList.add(item); + } + List outputList = new ArrayList<>(); + for (String s : outputStr.split("\\|")) { + ItemStack item = (ItemStack) SerializeUtils.string2Object(s); + if (item != null) outputList.add(item); + } + if (inputList.isEmpty() || outputList.isEmpty()) { + clearState(location); + return; + } + CraftingRecipe recipe = new CraftingRecipe( + craftType, inputList.toArray(new ItemStack[0]), outputList.toArray(new ItemStack[0])); + recipeCache.put(location, recipe); + runningCache.add(location); + } catch (Exception e) { + SlimeAEPlugin.getInstance() + .getLogger() + .log(Level.WARNING, "Failed to restore CookingAllocator state at " + location, e); + clearState(location); + } } @Nonnull @@ -198,20 +245,31 @@ public boolean canStartCrafting(@Nonnull Block block, @Nonnull CraftingRecipe re } @Override - public void startCrafting(@Nonnull Block block, @Nonnull CraftingRecipe recipe) { + public boolean startCrafting(@Nonnull Block block, @Nonnull CraftingRecipe recipe) { Location loc = block.getLocation(); Block target = getTargetBlock(block); - if (target == null) return; + if (target == null) return false; IStorage targetStorage = ItemUtils.getStorage(target, false, false); - if (targetStorage == null) return; + if (targetStorage == null) return false; - targetStorage.pushItem(Arrays.stream(ItemUtils.trimItems(recipe.getInput())) + ItemStack[] inputs = Arrays.stream(ItemUtils.trimItems(recipe.getInput())) .map(ItemStack::clone) - .toArray(ItemStack[]::new)); + .toArray(ItemStack[]::new); + targetStorage.pushItem(inputs); + + for (ItemStack input : inputs) { + if (input != null && !input.getType().isAir() && input.getAmount() > 0) { + recipeCache.remove(loc); + runningCache.remove(loc); + return false; + } + } + recipeCache.put(loc, recipe); runningCache.add(loc); saveState(loc, recipe); + return true; } @Override diff --git a/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java b/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java index e21b2cf..870c2e0 100644 --- a/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java +++ b/src/main/java/me/ddggdd135/slimeae/tasks/NetworkTickerTask.java @@ -12,6 +12,7 @@ import javax.annotation.Nonnull; import me.ddggdd135.slimeae.SlimeAEPlugin; import me.ddggdd135.slimeae.api.autocraft.AutoCraftingTask; +import me.ddggdd135.slimeae.api.database.v3.CraftTaskPersistence; import me.ddggdd135.slimeae.api.enums.AETaskType; import me.ddggdd135.slimeae.api.events.AEPostTaskEvent; import me.ddggdd135.slimeae.api.events.AEPreTaskEvent; @@ -73,6 +74,16 @@ public void run0() { if (tick % 160 == 0) { info = SlimeAEPlugin.getNetworkData().refreshNetwork(info.getController()); if (info == null) continue; + CraftTaskPersistence persistence = SlimeAEPlugin.getCraftTaskPersistence(); + if (persistence != null) { + try { + persistence.tryRestore(info); + } catch (Exception e) { + SlimeAEPlugin.getInstance() + .getLogger() + .log(Level.WARNING, "Failed to restore tasks for network", e); + } + } } else if (info.needsStorageUpdate() || info.needsRecipeUpdate()) { if (info.needsStorageUpdate()) SlimeAEPlugin.getNetworkData().updateStorage(info); diff --git a/src/main/java/me/ddggdd135/slimeae/tasks/TaskBackupTask.java b/src/main/java/me/ddggdd135/slimeae/tasks/TaskBackupTask.java new file mode 100644 index 0000000..af8c93c --- /dev/null +++ b/src/main/java/me/ddggdd135/slimeae/tasks/TaskBackupTask.java @@ -0,0 +1,57 @@ +package me.ddggdd135.slimeae.tasks; + +import java.util.logging.Level; +import javax.annotation.Nonnull; +import me.ddggdd135.slimeae.SlimeAEPlugin; +import org.bukkit.scheduler.BukkitScheduler; + +public class TaskBackupTask implements Runnable { + private int tickRate; + private boolean halted = false; + private volatile boolean running = false; + private volatile boolean paused = false; + + public void start(@Nonnull SlimeAEPlugin plugin) { + int seconds = plugin.getConfig().getInt("auto-crafting.persistence.backup-interval-seconds", 60); + if (seconds <= 0) return; + this.tickRate = seconds * 20; + BukkitScheduler scheduler = plugin.getServer().getScheduler(); + scheduler.runTaskTimerAsynchronously(plugin, this, tickRate, tickRate); + } + + @Override + public void run() { + if (paused || halted) return; + synchronized (this) { + if (running) return; + running = true; + } + try { + if (SlimeAEPlugin.getCraftTaskPersistence() != null) { + SlimeAEPlugin.getCraftTaskPersistence().backupAll(); + } + } catch (Exception | LinkageError e) { + SlimeAEPlugin.getInstance().getLogger().log(Level.SEVERE, "Exception while backing up craft tasks", e); + } finally { + synchronized (this) { + running = false; + } + } + } + + public void halt() { + halted = true; + } + + public boolean isPaused() { + return paused; + } + + public void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean isRunning() { + return running; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 14469ef..6ff6096 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -23,6 +23,11 @@ auto-crafting: parallel: enabled: true max-parallelism: 16 + # 持久化设置 + persistence: + enabled: true + # 定期备份间隔(秒), 0=不定期备份 + backup-interval-seconds: 60 # 链式总线相关设置 chained-bus: