Skip to content

Commit 6ed14f4

Browse files
authored
Fix the reusable ingredients in chains of recipes blocking each other (#187)
If the same reusable ingredient would be used within a chain of dependent recipes, the ingredient could be used in a dependent recipe, thereby blocking the dependency recipe. This change ensures that dependency-less recipes first get to use the reusable ingredients. Closes #182
1 parent 2f89c52 commit 6ed14f4

3 files changed

Lines changed: 99 additions & 31 deletions

File tree

src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,17 @@ public static <T, M> List<T> getIngredientRecipeInputs(IIngredientComponentStora
982982
IngredientCollectionPrototypeMap<T, M> simulatedExtractionMemory,
983983
IIngredientCollectionMutable<T, M> extractionMemoryReusable,
984984
boolean collectMissingIngredients, long recipeOutputQuantity) {
985+
return getIngredientRecipeInputs(storage, ingredientComponent, recipe, simulate, simulatedExtractionMemory,
986+
extractionMemoryReusable, collectMissingIngredients, recipeOutputQuantity, false);
987+
}
988+
989+
public static <T, M> Pair<List<T>, MissingIngredients<T, M>>
990+
getIngredientRecipeInputs(IIngredientComponentStorage<T, M> storage, IngredientComponent<T, M> ingredientComponent,
991+
IRecipeDefinition recipe, boolean simulate,
992+
IngredientCollectionPrototypeMap<T, M> simulatedExtractionMemory,
993+
IIngredientCollectionMutable<T, M> extractionMemoryReusable,
994+
boolean collectMissingIngredients, long recipeOutputQuantity,
995+
boolean skipReusableIngredients) {
985996
IIngredientMatcher<T, M> matcher = ingredientComponent.getMatcher();
986997

987998
// Quickly return if the storage is empty
@@ -1016,6 +1027,15 @@ public static <T, M> List<T> getIngredientRecipeInputs(IIngredientComponentStora
10161027
collectMissingIngredients ? Lists.newArrayList() : null;
10171028
for (int inputIndex = 0; inputIndex < inputAlternativePrototypes.size(); inputIndex++) {
10181029
IPrototypedIngredientAlternatives<T, M> inputPrototypes = inputAlternativePrototypes.get(inputIndex);
1030+
1031+
// If reusable ingredients should be skipped, treat them as empty (not needed yet).
1032+
// This is used when a job has dependencies, so that reusable ingredients remain available
1033+
// for other jobs to use, and will be extracted lazily in the update loop.
1034+
if (skipReusableIngredients && recipe.isInputReusable(ingredientComponent, inputIndex)) {
1035+
inputInstances.add(matcher.getEmptyInstance());
1036+
continue;
1037+
}
1038+
10191039
T firstInputInstance = null;
10201040
boolean setFirstInputInstance = false;
10211041
T inputInstance = null;
@@ -1310,6 +1330,15 @@ public static IMixedIngredients getRecipeInputsFromCraftingJobBuffer(CraftingJob
13101330
Map<IngredientComponent<?, ?>, IngredientCollectionPrototypeMap<?, ?>> simulatedExtractionMemories,
13111331
Map<IngredientComponent<?, ?>, IIngredientCollectionMutable<?, ?>> extractionMemoriesReusable,
13121332
boolean collectMissingIngredients, long recipeOutputQuantity) {
1333+
return getRecipeInputs(storageGetter, recipe, simulate, simulatedExtractionMemories, extractionMemoriesReusable,
1334+
collectMissingIngredients, recipeOutputQuantity, false);
1335+
}
1336+
1337+
public static Pair<Map<IngredientComponent<?, ?>, List<?>>, Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>>>
1338+
getRecipeInputs(Function<IngredientComponent<?, ?>, IIngredientComponentStorage> storageGetter, IRecipeDefinition recipe, boolean simulate,
1339+
Map<IngredientComponent<?, ?>, IngredientCollectionPrototypeMap<?, ?>> simulatedExtractionMemories,
1340+
Map<IngredientComponent<?, ?>, IIngredientCollectionMutable<?, ?>> extractionMemoriesReusable,
1341+
boolean collectMissingIngredients, long recipeOutputQuantity, boolean skipReusableIngredients) {
13131342
// Determine available and missing ingredients
13141343
Map<IngredientComponent<?, ?>, List<?>> ingredientsAvailable = Maps.newIdentityHashMap();
13151344
Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>> ingredientsMissing = Maps.newIdentityHashMap();
@@ -1327,7 +1356,7 @@ public static IMixedIngredients getRecipeInputsFromCraftingJobBuffer(CraftingJob
13271356
}
13281357
Pair<List<?>, MissingIngredients<?, ?>> subIngredients = getIngredientRecipeInputs(storage,
13291358
(IngredientComponent) ingredientComponent, recipe, simulate, simulatedExtractionMemory, extractionMemoryReusable,
1330-
collectMissingIngredients, recipeOutputQuantity);
1359+
collectMissingIngredients, recipeOutputQuantity, skipReusableIngredients);
13311360
List<?> subIngredientAvailable = subIngredients.getLeft();
13321361
MissingIngredients<?, ?> subIngredientsMissing = subIngredients.getRight();
13331362
if (subIngredientAvailable == null && !collectMissingIngredients) {

src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,10 @@ public void fillCraftingJobBufferFromStorage(CraftingJob craftingJob, Function<I
291291
throw new IllegalStateException("Re-filling a non-empty crafting job buffer is illegal");
292292
}
293293
// Determine the ingredients to extract. We can not reuse the ingredientsStorage value from the crafting job, as this may have been modified due to job splitting.
294-
Pair<Map<IngredientComponent<?, ?>, List<?>>, Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>>> inputResult = CraftingHelpers.getRecipeInputs(storageGetter, craftingJob.getRecipe(), false, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), true, craftingJob.getAmount());
294+
// If this job has dependencies, skip reusable ingredients so that they remain available for other jobs.
295+
// They will be lazily extracted in the update loop once the dependencies have finished.
296+
boolean skipReusableIngredients = !craftingJob.getDependencyCraftingJobs().isEmpty();
297+
Pair<Map<IngredientComponent<?, ?>, List<?>>, Map<IngredientComponent<?, ?>, MissingIngredients<?, ?>>> inputResult = CraftingHelpers.getRecipeInputs(storageGetter, craftingJob.getRecipe(), false, Maps.newIdentityHashMap(), Maps.newIdentityHashMap(), true, craftingJob.getAmount(), skipReusableIngredients);
295298
IMixedIngredients buffer = new MixedIngredients(inputResult.getLeft());
296299
craftingJob.setIngredientsStorageBuffer(CraftingHelpers.compressMixedIngredients(buffer));
297300
craftingJob.setLastMissingIngredients(inputResult.getRight());
@@ -504,35 +507,33 @@ public void update(INetwork network, int channel, PartPos targetPos) {
504507
// trigger a crafting job for them if no job is running yet.
505508
// This special case is needed because reusable ingredients are usually durability-based,
506509
// and may be consumed _during_ a bulk crafting job.
507-
if (pendingCraftingJob.getLastMissingIngredients().isEmpty()) {
508-
for (IngredientComponent<?, ?> component : inputs.getRight().keySet()) {
509-
MissingIngredients<?, ?> missingIngredients = inputs.getRight().get(component);
510-
for (MissingIngredients.Element<?, ?> element : missingIngredients.getElements()) {
511-
if (element.isInputReusable()) {
512-
IIngredientComponentStorage storage = CraftingHelpers.getNetworkStorage(network, channel, component, true);
513-
for (MissingIngredients.PrototypedWithRequested alternative : element.getAlternatives()) {
514-
// First check if we can extract it from storage.
515-
Object extractedFromStorage = storage.extract(alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), false);
516-
if (!((IIngredientMatcher) component.getMatcher()).isEmpty(extractedFromStorage)) {
517-
pendingCraftingJob.addToIngredientsStorageBuffer((IngredientComponent<? super Object, ? extends Object>) component, extractedFromStorage);
518-
break;
519-
}
520-
521-
// Try to start crafting jobs for each alternative until one of them succeeds.
522-
if (CraftingHelpers.isCrafting(craftingNetwork, channel,
523-
alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition())) {
524-
// Break loop if we have found an existing job for our dependency
525-
// This may occur if a crafting job was triggered in a parallelized job
526-
break;
527-
}
528-
CraftingJob craftingJob = CraftingHelpers.calculateAndScheduleCraftingJob(network, channel,
529-
alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), true, true,
530-
CraftingHelpers.getGlobalCraftingJobIdentifier(), null);
531-
if (craftingJob != null) {
532-
pendingCraftingJob.addDependency(craftingJob);
533-
// Break loop once we have found a valid job
534-
break;
535-
}
510+
for (IngredientComponent<?, ?> component : inputs.getRight().keySet()) {
511+
MissingIngredients<?, ?> missingIngredients = inputs.getRight().get(component);
512+
for (MissingIngredients.Element<?, ?> element : missingIngredients.getElements()) {
513+
if (element.isInputReusable()) {
514+
IIngredientComponentStorage storage = CraftingHelpers.getNetworkStorage(network, channel, component, true);
515+
for (MissingIngredients.PrototypedWithRequested alternative : element.getAlternatives()) {
516+
// First check if we can extract it from storage.
517+
Object extractedFromStorage = storage.extract(alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), false);
518+
if (!((IIngredientMatcher) component.getMatcher()).isEmpty(extractedFromStorage)) {
519+
pendingCraftingJob.addToIngredientsStorageBuffer((IngredientComponent<? super Object, ? extends Object>) component, extractedFromStorage);
520+
break;
521+
}
522+
523+
// Try to start crafting jobs for each alternative until one of them succeeds.
524+
if (CraftingHelpers.isCrafting(craftingNetwork, channel,
525+
alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition())) {
526+
// Break loop if we have found an existing job for our dependency
527+
// This may occur if a crafting job was triggered in a parallelized job
528+
break;
529+
}
530+
CraftingJob craftingJob = CraftingHelpers.calculateAndScheduleCraftingJob(network, channel,
531+
alternative.getRequestedPrototype().getComponent(), alternative.getRequestedPrototype().getPrototype(), alternative.getRequestedPrototype().getCondition(), true, true,
532+
CraftingHelpers.getGlobalCraftingJobIdentifier(), null);
533+
if (craftingJob != null) {
534+
pendingCraftingJob.addDependency(craftingJob);
535+
// Break loop once we have found a valid job
536+
break;
536537
}
537538
}
538539
}

src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsCraft.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,44 @@ public void testItemsCraftDeadBushTagReusableAsDependencyCraft(GameTestHelper he
786786
});
787787
}
788788

789+
// Reproduces https://github.com/CyclopsMC/IntegratedCrafting/issues/182
790+
// When a job with a reusable ingredient (shears) has a dependency (craft shears from iron ingots),
791+
// the reusable ingredient should be extracted lazily after the dependency finishes.
792+
@GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT)
793+
public void testItemsCraftDeadBushTagReusableNeedsCrafting(GameTestHelper helper) {
794+
GameTestHelpersIntegratedCrafting.INetworkPositions<PartTypeInterfaceCrafting.State> positions = createBasicNetwork(helper, POS, Blocks.CRAFTING_TABLE, Blocks.CRAFTING_TABLE);
795+
796+
// Insert items in interface chest: no shears, but iron ingots to craft them
797+
ChestBlockEntity chestIn = helper.getBlockEntity(POS.east());
798+
chestIn.setItem(0, new ItemStack(Items.IRON_INGOT, 2));
799+
chestIn.setItem(1, new ItemStack(Items.SPRUCE_SAPLING, 10));
800+
801+
// Add dead bush recipe with reusable shears to crafting interface 0
802+
createDeadBushTagReusableRecipe(helper, positions);
803+
804+
// Add shears recipe to crafting interface 1
805+
positions.interfaceRecipeAdders().get(1).accept(Triple.of(0, RecipeType.CRAFTING, ResourceLocation.fromNamespaceAndPath("minecraft", "shears")));
806+
807+
// Speed up crafting interfaces, to craft once every tick
808+
GameTestHelpersIntegratedCrafting.setCraftingInterfaceUpdateInterval(positions.interfaces().get(0), 1);
809+
GameTestHelpersIntegratedCrafting.setCraftingInterfaceUpdateInterval(positions.interfaces().get(1), 1);
810+
811+
// Enable crafting aspect in crafting writer
812+
enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.DEAD_BUSH, 10));
813+
814+
helper.succeedWhen(() -> {
815+
// Check crafting interface state
816+
helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), "Recipe in crafting interface is not valid");
817+
helper.assertTrue(positions.interfaceStates().get(1).isRecipeSlotValid(0), "Recipe in crafting interface is not valid");
818+
819+
// Check if items have been crafted: 10 dead bushes and 1 shears (returned from reusable use)
820+
helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.DEAD_BUSH, "Slot 0 item is incorrect");
821+
helper.assertValueEqual(chestIn.getItem(0).getCount(), 10, "Slot 0 amount is incorrect");
822+
helper.assertValueEqual(chestIn.getItem(1).getItem(), Items.SHEARS, "Slot 1 item is incorrect");
823+
helper.assertValueEqual(chestIn.getItem(1).getCount(), 1, "Slot 1 amount is incorrect");
824+
});
825+
}
826+
789827
@GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT)
790828
public void testItemsCraftCraftingTablesWithExistingPlank(GameTestHelper helper) {
791829
GameTestHelpersIntegratedCrafting.INetworkPositions<PartTypeInterfaceCrafting.State> positions = createBasicNetwork(helper, POS, Blocks.CRAFTING_TABLE);

0 commit comments

Comments
 (0)