From 7feda38dbd87f6a3f6146b9a48ba1c86d90e71be Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 08:02:37 +0000 Subject: [PATCH] Fix non-blocking jobs losing running operations on insert failure When a target accepted recipe inputs in simulation but not for real, all running operations of the job were forgotten, and the refused inputs were moved to the storage network. In non-blocking mode, this made crafting jobs get stuck forever. Equal inputs are now stacked when simulating, so simulation also fails when they only fit separately. If the real insertion still fails, only that operation is undone, and the refused inputs go back to the job. Related to #252 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01D8EZpiK75euMoLjy5r6suM --- .../core/CraftingHelpers.java | 30 ++++++++++- .../core/CraftingJobHandler.java | 50 ++++++++++++++----- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java index b64a7a473..2f8413e42 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java @@ -1622,6 +1622,13 @@ public static boolean insertIngredientCrafting(IngredientComponent IIngredientMatcher matcher = ingredientComponent.getMatcher(); IIngredientComponentStorage storage = ingredientComponent.getStorage(capabilityProvider, side); List instances = ingredients.getInstances(ingredientComponent); + if (simulate) { + // Stack equal instances, as simulated insertions do not take into account each other. + // Otherwise, instances that each fit separately could be accepted while they don't fit together. + Map, List> instancesMap = Maps.newIdentityHashMap(); + instancesMap.put(ingredientComponent, instances); + instances = compressMixedIngredients(new MixedIngredients(instancesMap)).getInstances(ingredientComponent); + } List failedInstances = Lists.newArrayList(); boolean ok = true; for (T instance : instances) { @@ -1664,6 +1671,25 @@ public static boolean insertCrafting(Function, PartPos IMixedIngredients ingredients, INetwork network, int channel, boolean simulate) { + return insertCrafting(targetGetter, ingredients, getNetworkStorageGetter(network, channel, false), simulate); + } + + /** + * Insert the ingredients of all applicable ingredient component types into the target to make it start crafting. + * + * If insertion in non-simulation mode fails, + * ingredients will be inserted into the given fallback storage. + * + * @param targetGetter A function to get the target position. + * @param ingredients The ingredients to insert. + * @param storageFallbackGetter A function to get the storage to insert failed ingredients into. + * @param simulate If insertion should be simulated. + * @return If all instances could be inserted. + */ + public static boolean insertCrafting(Function, PartPos> targetGetter, + IMixedIngredients ingredients, + Function, IIngredientComponentStorage> storageFallbackGetter, + boolean simulate) { Map, BlockEntity> tileMap = Maps.newIdentityHashMap(); // First, check if we can find valid tiles for all ingredient components @@ -1679,10 +1705,10 @@ public static boolean insertCrafting(Function, PartPos // Next, insert the instances into the respective tiles boolean ok = true; for (Map.Entry, BlockEntity> entry : tileMap.entrySet()) { - IIngredientComponentStorage storageNetwork = simulate ? null : getNetworkStorage(network, channel, entry.getKey(), false); + IIngredientComponentStorage storageFallback = simulate ? null : storageFallbackGetter.apply(entry.getKey()); if (!insertIngredientCrafting((IngredientComponent) entry.getKey(), entry.getValue(), targetGetter.apply(entry.getKey()).getSide(), ingredients, - storageNetwork, simulate)) { + storageFallback, simulate)) { ok = false; } } diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java index 7331c49f5..a094278f3 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java @@ -321,6 +321,28 @@ public void unmarkCraftingJobProcessing(CraftingJob craftingJob) { } } + /** + * Undo a single crafting operation that was added via {@link #addCraftingJobProcessingPendingIngredientsEntry}. + * Other operations of the same job, which may still be running in non-blocking mode, are kept. + * If no operations remain, the job is moved back to the pending jobs. + * @param craftingJob The crafting job. + * @param pendingIngredients The exact pending ingredients entry that was added for the operation. + * @return If the operation was still running. + */ + protected boolean removeCraftingJobProcessingPendingIngredientsEntry(CraftingJob craftingJob, + Map, + List>> pendingIngredients) { + List, List>>> pendingIngredientsEntries = + this.processingCraftingJobsPendingIngredients.get(craftingJob.getId()); + if (pendingIngredientsEntries == null || !pendingIngredientsEntries.removeIf(entry -> entry == pendingIngredients)) { + return false; + } + if (pendingIngredientsEntries.isEmpty()) { + unmarkCraftingJobProcessing(craftingJob); + } + return true; + } + public void addCraftingJobProcessingPendingIngredientsEntry(CraftingJob craftingJob, Map, List>> pendingIngredients) { @@ -578,8 +600,9 @@ protected boolean insertCrafting(PartPos target, IMixedIngredients ingredients, } } - // Fallback to default crafting insertion - return CraftingHelpers.insertCrafting(targetGetter, ingredients, network, channel, simulate); + // Fallback to default crafting insertion, failed ingredients go back to the job + return CraftingHelpers.insertCrafting(targetGetter, ingredients, + CraftingHelpers.getCraftingJobBufferStorageGetter(craftingJob), simulate); } protected void insertLoopNonBlocking(INetwork network, int channel, PartPos targetPos, CraftingJob craftingJob) { @@ -609,8 +632,9 @@ protected boolean consumeAndInsertCrafting(boolean blockingMode, INetwork networ this.pendingCraftingJobs.remove(startingCraftingJob.getId()); // Update state with expected outputs - addCraftingJobProcessingPendingIngredientsEntry(startingCraftingJob, - CraftingHelpers.getRecipeOutputs(startingCraftingJob.getRecipe())); + Map, List>> pendingIngredients = + CraftingHelpers.getRecipeOutputs(startingCraftingJob.getRecipe()); + addCraftingJobProcessingPendingIngredientsEntry(startingCraftingJob, pendingIngredients); // Register listeners for pending ingredients for (IngredientComponent component : startingCraftingJob.getRecipe().getOutput().getComponents()) { @@ -619,17 +643,19 @@ protected boolean consumeAndInsertCrafting(boolean blockingMode, INetwork networ // Push the ingredients to the crafting interface if (!insertCrafting(targetPos, ingredients, recipe, startingCraftingJob, network, channel, false)) { - // Unregister listeners again for pending ingredients - for (IngredientComponent component : startingCraftingJob.getRecipe().getOutput().getComponents()) { - unregisterIngredientObserver(component, network); - } - // If we reach this point, the target does not accept the recipe inputs, // even though they were acceptable in simulation mode. - // The failed ingredients were already re-inserted into the network at this point, - // so we mark the job as failed, and add it again to the queue. + // The failed ingredients were already put back into the job's buffer at this point. + // Only this operation is undone, as others of this job may still be running in non-blocking mode. + if (removeCraftingJobProcessingPendingIngredientsEntry(startingCraftingJob, pendingIngredients)) { + // Unregister listeners again for pending ingredients + for (IngredientComponent component : startingCraftingJob.getRecipe().getOutput().getComponents()) { + unregisterIngredientObserver(component, network); + } + } + + // Mark the job as failed, it will be added again to the queue once no operations are running. startingCraftingJob.setInvalidInputs(true); - unmarkCraftingJobProcessing(startingCraftingJob); return false; } else { return true;