diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java index f6b81e68..187a84dc 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java @@ -982,6 +982,17 @@ public static List getIngredientRecipeInputs(IIngredientComponentStora IngredientCollectionPrototypeMap simulatedExtractionMemory, IIngredientCollectionMutable extractionMemoryReusable, boolean collectMissingIngredients, long recipeOutputQuantity) { + return getIngredientRecipeInputs(storage, ingredientComponent, recipe, simulate, simulatedExtractionMemory, + extractionMemoryReusable, collectMissingIngredients, recipeOutputQuantity, false); + } + + public static Pair, MissingIngredients> + getIngredientRecipeInputs(IIngredientComponentStorage storage, IngredientComponent ingredientComponent, + IRecipeDefinition recipe, boolean simulate, + IngredientCollectionPrototypeMap simulatedExtractionMemory, + IIngredientCollectionMutable extractionMemoryReusable, + boolean collectMissingIngredients, long recipeOutputQuantity, + boolean skipReusableIngredients) { IIngredientMatcher matcher = ingredientComponent.getMatcher(); // Quickly return if the storage is empty @@ -1016,6 +1027,15 @@ public static List getIngredientRecipeInputs(IIngredientComponentStora collectMissingIngredients ? Lists.newArrayList() : null; for (int inputIndex = 0; inputIndex < inputAlternativePrototypes.size(); inputIndex++) { IPrototypedIngredientAlternatives inputPrototypes = inputAlternativePrototypes.get(inputIndex); + + // If reusable ingredients should be skipped, treat them as empty (not needed yet). + // This is used when a job has dependencies, so that reusable ingredients remain available + // for other jobs to use, and will be extracted lazily in the update loop. + if (skipReusableIngredients && recipe.isInputReusable(ingredientComponent, inputIndex)) { + inputInstances.add(matcher.getEmptyInstance()); + continue; + } + T firstInputInstance = null; boolean setFirstInputInstance = false; T inputInstance = null; @@ -1310,6 +1330,15 @@ public static IMixedIngredients getRecipeInputsFromCraftingJobBuffer(CraftingJob Map, IngredientCollectionPrototypeMap> simulatedExtractionMemories, Map, IIngredientCollectionMutable> extractionMemoriesReusable, boolean collectMissingIngredients, long recipeOutputQuantity) { + return getRecipeInputs(storageGetter, recipe, simulate, simulatedExtractionMemories, extractionMemoriesReusable, + collectMissingIngredients, recipeOutputQuantity, false); + } + + public static Pair, List>, Map, MissingIngredients>> + getRecipeInputs(Function, IIngredientComponentStorage> storageGetter, IRecipeDefinition recipe, boolean simulate, + Map, IngredientCollectionPrototypeMap> simulatedExtractionMemories, + Map, IIngredientCollectionMutable> extractionMemoriesReusable, + boolean collectMissingIngredients, long recipeOutputQuantity, boolean skipReusableIngredients) { // Determine available and missing ingredients Map, List> ingredientsAvailable = Maps.newIdentityHashMap(); Map, MissingIngredients> ingredientsMissing = Maps.newIdentityHashMap(); @@ -1327,7 +1356,7 @@ public static IMixedIngredients getRecipeInputsFromCraftingJobBuffer(CraftingJob } Pair, MissingIngredients> subIngredients = getIngredientRecipeInputs(storage, (IngredientComponent) ingredientComponent, recipe, simulate, simulatedExtractionMemory, extractionMemoryReusable, - collectMissingIngredients, recipeOutputQuantity); + collectMissingIngredients, recipeOutputQuantity, skipReusableIngredients); List subIngredientAvailable = subIngredients.getLeft(); MissingIngredients subIngredientsMissing = subIngredients.getRight(); if (subIngredientAvailable == null && !collectMissingIngredients) { diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java index 3e813a08..e72a8d59 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java @@ -291,11 +291,10 @@ public void fillCraftingJobBufferFromStorage(CraftingJob craftingJob, Function add a flag to CraftingHelpers.getRecipeInputs? - } - Pair, List>, Map, MissingIngredients>> inputResult = CraftingHelpers.getRecipeInputs(storageGetter, craftingJob.getRecipe(), false, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), true, craftingJob.getAmount()); + // If this job has dependencies, skip reusable ingredients so that they remain available for other jobs. + // They will be lazily extracted in the update loop once the dependencies have finished. + boolean skipReusableIngredients = !craftingJob.getDependencyCraftingJobs().isEmpty(); + Pair, List>, Map, MissingIngredients>> inputResult = CraftingHelpers.getRecipeInputs(storageGetter, craftingJob.getRecipe(), false, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), true, craftingJob.getAmount(), skipReusableIngredients); IMixedIngredients buffer = new MixedIngredients(inputResult.getLeft()); craftingJob.setIngredientsStorageBuffer(CraftingHelpers.compressMixedIngredients(buffer)); craftingJob.setLastMissingIngredients(inputResult.getRight()); @@ -508,35 +507,33 @@ public void update(INetwork network, int channel, PartPos targetPos) { // trigger a crafting job for them if no job is running yet. // This special case is needed because reusable ingredients are usually durability-based, // and may be consumed _during_ a bulk crafting job. - if (pendingCraftingJob.getLastMissingIngredients().isEmpty() || true) { // TODO: after fixing the issue, remove this if statement completely - for (IngredientComponent component : inputs.getRight().keySet()) { - MissingIngredients missingIngredients = inputs.getRight().get(component); - for (MissingIngredients.Element element : missingIngredients.getElements()) { - if (element.isInputReusable()) { - IIngredientComponentStorage storage = CraftingHelpers.getNetworkStorage(network, channel, component, true); - for (MissingIngredients.PrototypedWithRequested alternative : element.getAlternatives()) { - // First check if we can extract it from storage. - Object extractedFromStorage = storage.extract(alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), false); - if (!((IIngredientMatcher) component.getMatcher()).isEmpty(extractedFromStorage)) { - pendingCraftingJob.addToIngredientsStorageBuffer((IngredientComponent) component, extractedFromStorage); - break; - } - - // Try to start crafting jobs for each alternative until one of them succeeds. - if (CraftingHelpers.isCrafting(craftingNetwork, channel, - alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition())) { - // Break loop if we have found an existing job for our dependency - // This may occur if a crafting job was triggered in a parallelized job - break; - } - CraftingJob craftingJob = CraftingHelpers.calculateAndScheduleCraftingJob(network, channel, - alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), true, true, - CraftingHelpers.getGlobalCraftingJobIdentifier(), null); - if (craftingJob != null) { - pendingCraftingJob.addDependency(craftingJob); - // Break loop once we have found a valid job - break; - } + for (IngredientComponent component : inputs.getRight().keySet()) { + MissingIngredients missingIngredients = inputs.getRight().get(component); + for (MissingIngredients.Element element : missingIngredients.getElements()) { + if (element.isInputReusable()) { + IIngredientComponentStorage storage = CraftingHelpers.getNetworkStorage(network, channel, component, true); + for (MissingIngredients.PrototypedWithRequested alternative : element.getAlternatives()) { + // First check if we can extract it from storage. + Object extractedFromStorage = storage.extract(alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), false); + if (!((IIngredientMatcher) component.getMatcher()).isEmpty(extractedFromStorage)) { + pendingCraftingJob.addToIngredientsStorageBuffer((IngredientComponent) component, extractedFromStorage); + break; + } + + // Try to start crafting jobs for each alternative until one of them succeeds. + if (CraftingHelpers.isCrafting(craftingNetwork, channel, + alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition())) { + // Break loop if we have found an existing job for our dependency + // This may occur if a crafting job was triggered in a parallelized job + break; + } + CraftingJob craftingJob = CraftingHelpers.calculateAndScheduleCraftingJob(network, channel, + alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), true, true, + CraftingHelpers.getGlobalCraftingJobIdentifier(), null); + if (craftingJob != null) { + pendingCraftingJob.addDependency(craftingJob); + // Break loop once we have found a valid job + break; } } } diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java index 3073be0e..38a29784 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java @@ -786,6 +786,44 @@ public void testItemsCraftDeadBushTagReusableAsDependencyCraft(GameTestHelper he }); } + // Reproduces https://github.com/CyclopsMC/IntegratedCrafting/issues/182 + // When a job with a reusable ingredient (shears) has a dependency (craft shears from iron ingots), + // the reusable ingredient should be extracted lazily after the dependency finishes. + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsCraftDeadBushTagReusableNeedsCrafting(GameTestHelper helper) { + GameTestHelpersIntegratedCrafting.INetworkPositions positions = createBasicNetwork(helper, POS, Blocks.CRAFTING_TABLE, Blocks.CRAFTING_TABLE); + + // Insert items in interface chest: no shears, but iron ingots to craft them + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east()); + chestIn.setItem(0, new ItemStack(Items.IRON_INGOT, 2)); + chestIn.setItem(1, new ItemStack(Items.SPRUCE_SAPLING, 10)); + + // Add dead bush recipe with reusable shears to crafting interface 0 + createDeadBushTagReusableRecipe(helper, positions); + + // Add shears recipe to crafting interface 1 + positions.interfaceRecipeAdders().get(1).accept(Triple.of(0, RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "shears"))); + + // Speed up crafting interfaces, to craft once every tick + GameTestHelpersIntegratedCrafting.setCraftingInterfaceUpdateInterval(positions.interfaces().get(0), 1); + GameTestHelpersIntegratedCrafting.setCraftingInterfaceUpdateInterval(positions.interfaces().get(1), 1); + + // Enable crafting aspect in crafting writer + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.DEAD_BUSH, 10)); + + helper.succeedWhen(() -> { + // Check crafting interface state + helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe in crafting interface is not valid"); + helper.assertTrue(positions.interfaceStates().get(1).isRecipeSlotValid(0), "Recipe in crafting interface is not valid"); + + // Check if items have been crafted: 10 dead bushes and 1 shears (returned from reusable use) + helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.DEAD_BUSH, "Slot 0 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(0).getCount(), 10, "Slot 0 amount is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.SHEARS, "Slot 1 item is incorrect"); + helper.assertValueEqual(chestIn.getItem(1).getCount(), 1, "Slot 1 amount is incorrect"); + }); + } + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) public void testItemsCraftCraftingTablesWithExistingPlank(GameTestHelper helper) { GameTestHelpersIntegratedCrafting.INetworkPositions positions = createBasicNetwork(helper, POS, Blocks.CRAFTING_TABLE);