Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -1475,8 +1477,23 @@ public static <T, M> MissingIngredients<T, M> compressMissingIngredients(Missing
Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> 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<IPrototypedIngredient<?, ?>> compressed = getCompressedIngredients(ingredientComponent, mixedIngredients);
if (hasMultipleComponents && ingredientComponent == IngredientComponent.ITEMSTACK) {
List<IPrototypedIngredient<?, ?>> 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<IPrototypedIngredient<?, ?>> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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<Int2ObjectMap.Entry<List<Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>>>>> jobsIt =
this.processingCraftingJobsPendingIngredients.int2ObjectEntrySet().iterator();
while (jobsIt.hasNext()) {
Int2ObjectMap.Entry<List<Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>>>>> 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<Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>>> pendingEntries = jobsEntry.getValue();
if (pendingEntries.size() != 1) {
continue;
}
Map<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> pendingEntry = pendingEntries.getFirst();

boolean allPendingOutputsAvailable = true;
for (Map.Entry<IngredientComponent<?, ?>, List<IPrototypedIngredient<?, ?>>> 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<IngredientComponent<?, ?>, PartPos> targetGetter = getTargetGetter(target);
// First check our crafting overrides
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ public static INetworkPositions<PartTypeInterfaceCrafting.State> 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));
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,42 @@ public void testItemsMechanicalSqueezerAttunedBricks(GameTestHelper helper) {
});
}

@GameTest(template = TEMPLATE_EMPTY, timeoutTicks = TIMEOUT)
public void testItemsMechanicalSqueezerFluidAndItemOutputJobCompletion(GameTestHelper helper) {
INetworkPositions<PartTypeInterfaceCrafting.State> 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"));
});
}

}
Loading