diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java index 0737fea7..2b1758d2 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingHelpers.java @@ -9,6 +9,8 @@ import net.minecraft.world.level.block.entity.BlockEntity; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.Level; +import org.cyclops.commoncapabilities.api.capability.fluidhandler.FluidMatch; +import org.cyclops.commoncapabilities.api.capability.itemhandler.ItemMatch; import org.cyclops.commoncapabilities.api.capability.recipehandler.IPrototypedIngredientAlternatives; import org.cyclops.commoncapabilities.api.capability.recipehandler.IRecipeDefinition; import org.cyclops.commoncapabilities.api.ingredient.*; @@ -1475,8 +1477,23 @@ public static MissingIngredients compressMissingIngredients(Missing Map, List>> outputs = Maps.newHashMap(); IMixedIngredients mixedIngredients = recipe.getOutput(); + boolean hasMultipleComponents = mixedIngredients.getComponents().size() > 1; for (IngredientComponent ingredientComponent : mixedIngredients.getComponents()) { - outputs.put(ingredientComponent, getCompressedIngredients(ingredientComponent, mixedIngredients)); + List> compressed = getCompressedIngredients(ingredientComponent, mixedIngredients); + if (hasMultipleComponents && ingredientComponent == IngredientComponent.ITEMSTACK) { + List> normalized = Lists.newArrayListWithCapacity(compressed.size()); + for (IPrototypedIngredient prototyped : compressed) { + normalized.add(new PrototypedIngredient(ingredientComponent, prototyped.getPrototype(), ItemMatch.ITEM)); + } + compressed = normalized; + } else if (hasMultipleComponents && ingredientComponent == IngredientComponent.FLUIDSTACK) { + List> normalized = Lists.newArrayListWithCapacity(compressed.size()); + for (IPrototypedIngredient prototyped : compressed) { + normalized.add(new PrototypedIngredient(ingredientComponent, prototyped.getPrototype(), FluidMatch.FLUID)); + } + compressed = normalized; + } + outputs.put(ingredientComponent, compressed); } return outputs; diff --git a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java index 6fb425f7..31891972 100644 --- a/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java +++ b/src/main/java/org/cyclops/integratedcrafting/core/CraftingJobHandler.java @@ -6,6 +6,7 @@ import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import net.minecraft.core.Direction; @@ -401,6 +402,11 @@ public void update(INetwork network, int channel, PartPos targetPos) { finishedCraftingJobs.clear(); } + // Fallback completion check for processing jobs with mixed output component types. + // This covers cases where output insertion events are not observed, + // while the outputs are present in network storage. + tryResolveMixedOutputCraftingJobsFromStorage(network, channel); + // The actual output observation of processing jobs is done via the ingredient observers int processingJobs = getProcessingCraftingJobs().size(); @@ -512,6 +518,52 @@ public void update(INetwork network, int channel, PartPos targetPos) { } } + protected void tryResolveMixedOutputCraftingJobsFromStorage(INetwork network, int channel) { + ICraftingNetwork craftingNetwork = CraftingHelpers.getCraftingNetworkChecked(network); + ObjectIterator, List>>>>> jobsIt = + this.processingCraftingJobsPendingIngredients.int2ObjectEntrySet().iterator(); + while (jobsIt.hasNext()) { + Int2ObjectMap.Entry, List>>>>> jobsEntry = jobsIt.next(); + int craftingJobId = jobsEntry.getIntKey(); + CraftingJob craftingJob = this.allCraftingJobs.get(craftingJobId); + if (craftingJob == null || craftingJob.getRecipe().getOutput().getComponents().size() <= 1) { + continue; + } + + // Only apply this fallback to single-entry jobs. + // This is sufficient for default blocking mode processing. + List, List>>> pendingEntries = jobsEntry.getValue(); + if (pendingEntries.size() != 1) { + continue; + } + Map, List>> pendingEntry = pendingEntries.getFirst(); + + boolean allPendingOutputsAvailable = true; + for (Map.Entry, List>> componentEntry : pendingEntry.entrySet()) { + IngredientComponent component = componentEntry.getKey(); + IIngredientComponentStorage storage = CraftingHelpers.getNetworkStorage(network, channel, component, true); + IIngredientMatcher matcher = component.getMatcher(); + for (IPrototypedIngredient prototypedIngredient : componentEntry.getValue()) { + Object extracted = storage.extract(prototypedIngredient.getPrototype(), prototypedIngredient.getCondition(), true); + if (matcher.getQuantity(extracted) < matcher.getQuantity(prototypedIngredient.getPrototype())) { + allPendingOutputsAvailable = false; + break; + } + } + if (!allPendingOutputsAvailable) { + break; + } + } + + if (allPendingOutputsAvailable) { + this.observersPendingDeletion.addAll(pendingEntry.keySet()); + this.onCraftingJobEntryFinished(craftingNetwork, craftingJobId); + this.onCraftingJobFinished(craftingJob); + jobsIt.remove(); + } + } + } + protected boolean insertCrafting(PartPos target, IMixedIngredients ingredients, IRecipeDefinition recipe, CraftingJob craftingJob, INetwork network, int channel, boolean simulate) { Function, PartPos> targetGetter = getTargetGetter(target); // First check our crafting overrides @@ -563,6 +615,14 @@ protected boolean consumeAndInsertCrafting(boolean blockingMode, INetwork networ for (IngredientComponent component : startingCraftingJob.getRecipe().getOutput().getComponents()) { registerIngredientObserver(component, network); } + // For recipes that produce multiple ingredient component types, + // schedule observation immediately so very fast machine outputs in the same tick are not missed. + if (startingCraftingJob.getRecipe().getOutput().getComponents().size() > 1) { + for (IngredientComponent component : startingCraftingJob.getRecipe().getOutput().getComponents()) { + IPositionedAddonsNetworkIngredients ingredientsNetwork = CraftingHelpers.getIngredientsNetworkChecked(network, component); + ingredientsNetwork.scheduleObservation(); + } + } // Push the ingredients to the crafting interface if (!insertCrafting(targetPos, ingredients, recipe, startingCraftingJob, network, channel, false)) { diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java index 800adbc4..a0457a4e 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestHelpersIntegratedCrafting.java @@ -132,6 +132,15 @@ public static INetworkPositions createBasicNetw helper.setBlock(posi.below().west(), RegistryEntries.BLOCK_CABLE.value()); PartHelpers.addPart(helper.getLevel(), helper.absolutePos(posi.below().west()), Direction.UP, org.cyclops.integratedtunnels.part.PartTypes.IMPORTER_ITEM, new ItemStack(org.cyclops.integratedtunnels.part.PartTypes.IMPORTER_ITEM.getItem())); placeVariableInWriter(helper, helper.getLevel(), PartPos.of(helper.getLevel(), helper.absolutePos(posi.below().west()), Direction.UP), TunnelAspects.Write.Item.BOOLEAN_IMPORT, new ItemStack(RegistryEntries.ITEM_VARIABLE)); + + // Add a dedicated machine with a fluid tank as fluid storage in the network, + // and extract fluid output from the target machine. + helper.setBlock(posi.below().west().south(), RegistryEntries.BLOCK_CABLE.value()); + helper.setBlock(posi.west().south(), RegistryEntries.BLOCK_CABLE.value()); + helper.setBlock(posi.west().south().above(), RegistryEntries.BLOCK_MECHANICAL_DRYING_BASIN.value()); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(posi.west().south()), Direction.UP, org.cyclops.integratedtunnels.part.PartTypes.INTERFACE_FLUID, new ItemStack(org.cyclops.integratedtunnels.part.PartTypes.INTERFACE_FLUID.getItem())); + PartHelpers.addPart(helper.getLevel(), helper.absolutePos(posi.west().south()), Direction.NORTH, org.cyclops.integratedtunnels.part.PartTypes.IMPORTER_FLUID, new ItemStack(org.cyclops.integratedtunnels.part.PartTypes.IMPORTER_FLUID.getItem())); + placeVariableInWriter(helper, helper.getLevel(), PartPos.of(helper.getLevel(), helper.absolutePos(posi.west().south()), Direction.NORTH), TunnelAspects.Write.Fluid.BOOLEAN_IMPORT, new ItemStack(RegistryEntries.ITEM_VARIABLE)); } interfaces.add(PartPos.of(helper.getLevel(), helper.absolutePos(posi.above().west()), Direction.DOWN)); @@ -250,6 +259,9 @@ public static ItemStack createVariableForRecipe(Level level, RecipeType recip if (!squeezerOutputItems.isEmpty()) { recipeOut.put(IngredientComponents.ITEMSTACK, squeezerOutputItems); } + recipeSqueezer.getOutputFluid().ifPresent(outputFluid -> + recipeOut.put(IngredientComponents.FLUIDSTACK, Lists.newArrayList(outputFluid)) + ); } else { throw new IllegalStateException("Unknown recipe type " + recipeType); } diff --git a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsMechanicalSqueezer.java b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsMechanicalSqueezer.java index 7d796c88..3a28838e 100644 --- a/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsMechanicalSqueezer.java +++ b/src/main/java/org/cyclops/integratedcrafting/gametest/GameTestsItemsMechanicalSqueezer.java @@ -93,4 +93,42 @@ public void testItemsMechanicalSqueezerAttunedBricks(GameTestHelper helper) { }); } + @GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT) + public void testItemsMechanicalSqueezerFluidAndItemOutputJobCompletion(GameTestHelper helper) { + INetworkPositions positions = createBasicNetwork(helper, POS, RegistryEntries.BLOCK_MECHANICAL_SQUEEZER.value()); + + // Insert items in interface chest + ChestBlockEntity chestIn = helper.getBlockEntity(POS.east(), ChestBlockEntity.class); + chestIn.setItem(0, new ItemStack(Items.MUD, 1)); + + // Add recipe with both item and fluid output to crafting interface + positions.interfaceRecipeAdders().get(0).accept(Triple.of(0, RegistryEntries.RECIPETYPE_MECHANICAL_SQUEEZER.get(), Identifier.fromNamespaceAndPath("integrateddynamics", "mechanical_squeezer/convenience/minecraft_water_mud"))); + + // Enable crafting aspect in crafting writer + enableRecipeInWriter(helper, positions.writer(), new ItemStack(Items.DIRT, 1)); + + helper.succeedWhen(() -> { + // Check crafting interface state + helper.assertTrue(positions.interfaceStates().get(0).isRecipeSlotValid(0), Component.literal("Recipe in crafting interface is not valid")); + + // Check crafting writer state + IPartStateWriter partStateWriter = (IPartStateWriter) PartHelpers.getPart(PartPos.of(helper.getLevel(), helper.absolutePos(POS), Direction.NORTH)).getState(); + helper.assertFalse(partStateWriter.isDeactivated(), Component.literal("Crafting writer is deactivated")); + helper.assertValueEqual( + PartTypes.CRAFTING_WRITER.getBlockState(PartHelpers.getPartContainerChecked(PartPos.of(helper.getLevel(), helper.absolutePos(POS), Direction.NORTH)), Direction.NORTH).getValue(IgnoredBlockStatus.STATUS), + IgnoredBlockStatus.Status.ACTIVE, + Component.literal("Block status is incorrect") + ); + helper.assertValueEqual(partStateWriter.getActiveAspect(), CraftingAspects.Write.ITEMSTACK_CRAFT, Component.literal("Active aspect is incorrect")); + helper.assertTrue(partStateWriter.getErrors(CraftingAspects.Write.ITEMSTACK_CRAFT).isEmpty(), Component.literal("Active aspect has errors")); + + // Check if items have been crafted + helper.assertValueEqual(chestIn.getItem(0).getItem(), Items.DIRT, Component.literal("Slot 0 item is incorrect")); + helper.assertValueEqual(chestIn.getItem(0).getCount(), 1, Component.literal("Slot 0 amount is incorrect")); + + // Check if job queue got cleared + helper.assertTrue(positions.interfaceStates().get(0).getCraftingJobsCount() == 0, Component.literal("Crafting job queue is not empty")); + }); + } + }