diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java index 2d843646a4..33c472ac93 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/EURecipeCapability.java @@ -42,6 +42,8 @@ public List compressIngredients(Collection ingredients) { @Override public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + if (holder instanceof ICustomParallel p) return p.limitParallel(recipe, multiplier); + long maxVoltage = Long.MAX_VALUE; if (holder instanceof IOverclockMachine overclockMachine) { maxVoltage = overclockMachine.getOverclockVoltage(); @@ -71,4 +73,16 @@ public int getMaxParallelRatio(IRecipeCapabilityHolder holder, GTRecipe recipe, } return Math.abs(Ints.saturatedCast(maxVoltage / recipeEUt)); } + + public interface ICustomParallel { + + /** + * Custom impl of the parallel limiter used by ParallelLogic to limit by outputs + * + * @param recipe Recipe + * @param multiplier Initial multiplier + * @return Limited multiplier + */ + int limitParallel(GTRecipe recipe, int multiplier); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java index 8981ea47bf..58f288e292 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/FluidRecipeCapability.java @@ -147,6 +147,8 @@ public boolean isRecipeSearchFilter() { @Override public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + if (holder instanceof ICustomParallel p) return p.limitParallel(recipe, multiplier); + int minMultiplier = 0; int maxMultiplier = multiplier; @@ -387,4 +389,16 @@ public static Either, Integer>>, List> mapFl public Object2IntMap makeChanceCache() { return super.makeChanceCache(); } + + public interface ICustomParallel { + + /** + * Custom impl of the parallel limiter used by ParallelLogic to limit by outputs + * + * @param recipe Recipe + * @param multiplier Initial multiplier + * @return Limited multiplier + */ + int limitParallel(GTRecipe recipe, int multiplier); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java index 15904be700..44dca344c5 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/recipe/ItemRecipeCapability.java @@ -243,6 +243,8 @@ public boolean isRecipeSearchFilter() { @Override public int limitParallel(GTRecipe recipe, IRecipeCapabilityHolder holder, int multiplier) { + if (holder instanceof ICustomParallel p) return p.limitParallel(recipe, multiplier); + int minMultiplier = 0; int maxMultiplier = multiplier; @@ -721,4 +723,16 @@ private static Either, Integer>>, List> mapIte public Object2IntMap makeChanceCache() { return new Object2IntOpenCustomHashMap<>(IngredientEquality.IngredientHashStrategy.INSTANCE); } + + public interface ICustomParallel { + + /** + * Custom impl of the parallel limiter used by ParallelLogic to limit by outputs + * + * @param recipe Recipe + * @param multiplier Initial multiplier + * @return Limited multiplier + */ + int limitParallel(GTRecipe recipe, int multiplier); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java index 804efa69a4..efc8f86346 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java @@ -63,7 +63,7 @@ public enum Status { @Persisted @DescSynced @UpdateListener(methodName = "onActiveSynced") - private boolean isActive; + protected boolean isActive; @Nullable @Persisted @@ -308,7 +308,7 @@ public void findAndHandleRecipe() { recipeDirty = false; } - private void handleSearchingRecipes(Iterator matches) { + protected void handleSearchingRecipes(Iterator matches) { while (matches != null && matches.hasNext()) { GTRecipe match = matches.next(); if (match == null) continue; diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java index d5c75cbe34..3d94c32927 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java @@ -92,10 +92,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Supplier; @@ -1581,27 +1578,64 @@ public static BiConsumer> createTankTooltips(String n .register(); public static final MultiblockMachineDefinition DISTILLATION_TOWER = REGISTRATE - .multiblock("distillation_tower", WorkableElectricMultiblockMachine::new) + .multiblock("distillation_tower", DistillationTowerMachine::new) .rotationState(RotationState.NON_Y_AXIS) .recipeType(GTRecipeTypes.DISTILLATION_RECIPES) .recipeModifiers( GTRecipeModifiers.ELECTRIC_OVERCLOCK.apply(OverclockingLogic.NON_PERFECT_OVERCLOCK_SUBTICK)) .appearanceBlock(CASING_STAINLESS_CLEAN) - .pattern(definition -> FactoryBlockPattern.start(RIGHT, BACK, UP) - .aisle("YSY", "YYY", "YYY") - .aisle("XXX", "X#X", "XXX").setRepeatable(1, 11) - .aisle("XXX", "XXX", "XXX") - .where('S', Predicates.controller(blocks(definition.getBlock()))) - .where('Y', blocks(CASING_STAINLESS_CLEAN.get()) - .or(Predicates.abilities(PartAbility.EXPORT_ITEMS).setMaxGlobalLimited(1)) - .or(Predicates.abilities(PartAbility.INPUT_ENERGY).setMinGlobalLimited(1) - .setMaxGlobalLimited(2)) - .or(Predicates.abilities(PartAbility.IMPORT_FLUIDS).setExactLimit(1))) - .where('X', blocks(CASING_STAINLESS_CLEAN.get()) - .or(Predicates.abilities(PartAbility.EXPORT_FLUIDS_1X).setMinLayerLimited(1) - .setMaxLayerLimited(1))) - .where('#', Predicates.air()) - .build()) + .pattern(definition -> { + TraceabilityPredicate exportPredicate = abilities(PartAbility.EXPORT_FLUIDS_1X); + if (GTCEu.isAE2Loaded()) + exportPredicate = exportPredicate.or(blocks(GTAEMachines.FLUID_EXPORT_HATCH_ME.get())); + exportPredicate.setMaxLayerLimited(1); + return FactoryBlockPattern.start(RIGHT, BACK, UP) + .aisle("YSY", "YYY", "YYY") + .aisle("XXX", "X#X", "XXX").setRepeatable(1, 11) + .aisle("XXX", "XXX", "XXX") + .where('S', Predicates.controller(blocks(definition.getBlock()))) + .where('Y', blocks(CASING_STAINLESS_CLEAN.get()) + .or(Predicates.abilities(PartAbility.EXPORT_ITEMS).setMaxGlobalLimited(1)) + .or(Predicates.abilities(PartAbility.INPUT_ENERGY).setMinGlobalLimited(1) + .setMaxGlobalLimited(2)) + .or(Predicates.abilities(PartAbility.IMPORT_FLUIDS).setExactLimit(1)) + .or(autoAbilities(true, false, false))) + .where('X', blocks(CASING_STAINLESS_CLEAN.get()).or(exportPredicate)) + .where('#', Predicates.air()) + .build(); + }) + .shapeInfos(definition -> { + List shapeInfos = new ArrayList<>(); + var builder = MultiblockShapeInfo.builder() + .where('C', definition, Direction.NORTH) + .where('S', CASING_STAINLESS_CLEAN.getDefaultState()) + .where('X', ITEM_EXPORT_BUS[HV], Direction.NORTH) + .where('I', FLUID_IMPORT_HATCH[HV], Direction.NORTH) + .where('E', ENERGY_INPUT_HATCH[HV], Direction.SOUTH) + .where('M', MAINTENANCE_HATCH, Direction.SOUTH) + .where('#', Blocks.AIR.defaultBlockState()) + .where('F', FLUID_EXPORT_HATCH[HV], Direction.SOUTH); + List front = new ArrayList<>(15); + front.add("XCI"); + front.add("SSS"); + List middle = new ArrayList<>(15); + middle.add("SSS"); + middle.add("SSS"); + List back = new ArrayList<>(15); + back.add("MES"); + back.add("SFS"); + for (int i = 1; i <= 11; ++i) { + front.add("SSS"); + middle.add(1, "S#S"); + back.add("SFS"); + var copy = builder.shallowCopy() + .aisle(front.toArray(String[]::new)) + .aisle(middle.toArray(String[]::new)) + .aisle(back.toArray(String[]::new)); + shapeInfos.add(copy.build()); + } + return shapeInfos; + }) .allowExtendedFacing(false) .partSorter(Comparator.comparingInt(a -> a.self().getPos().getY())) .workableCasingRenderer(GTCEu.id("block/casings/solid/machine_casing_clean_stainless_steel"), diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/machines/GCYMMachines.java b/src/main/java/com/gregtechceu/gtceu/common/data/machines/GCYMMachines.java index 222aebbd66..11b2fdb446 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/machines/GCYMMachines.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/machines/GCYMMachines.java @@ -17,6 +17,7 @@ import com.gregtechceu.gtceu.api.pattern.TraceabilityPredicate; import com.gregtechceu.gtceu.api.recipe.OverclockingLogic; import com.gregtechceu.gtceu.common.data.*; +import com.gregtechceu.gtceu.common.machine.multiblock.electric.DistillationTowerMachine; import com.gregtechceu.gtceu.common.machine.multiblock.part.ParallelHatchPartMachine; import com.gregtechceu.gtceu.utils.FormattingUtil; @@ -643,7 +644,7 @@ public static void init() {} .register(); public final static MultiblockMachineDefinition LARGE_DISTILLERY = REGISTRATE - .multiblock("large_distillery", WorkableElectricMultiblockMachine::new) + .multiblock("large_distillery", DistillationTowerMachine::new) .langValue("Large Fractionating Distillery") .tooltips(Component.translatable("gtceu.multiblock.parallelizable.tooltip")) .tooltips(Component.translatable("gtceu.machine.available_recipe_map_2.tooltip", @@ -655,7 +656,10 @@ public static void init() {} .appearanceBlock(CASING_WATERTIGHT) .pattern(definition -> { TraceabilityPredicate casingPredicate = blocks(CASING_WATERTIGHT.get()).setMinGlobalLimited(40); - + TraceabilityPredicate exportPredicate = abilities(PartAbility.EXPORT_FLUIDS_1X); + if (GTCEu.isAE2Loaded()) + exportPredicate = exportPredicate.or(blocks(GTAEMachines.FLUID_EXPORT_HATCH_ME.get())); + exportPredicate.setMaxLayerLimited(1); return FactoryBlockPattern.start(RIGHT, BACK, UP) .aisle("#YYY#", "YYYYY", "YYYYY", "YYYYY", "#YYY#") .aisle("#YSY#", "YAAAY", "YAAAY", "YAAAY", "#YYY#") @@ -667,8 +671,7 @@ public static void init() {} .or(abilities(IMPORT_FLUIDS).setMinGlobalLimited(1)) .or(abilities(EXPORT_ITEMS)) .or(autoAbilities(true, false, true))) - .where('X', casingPredicate - .or(abilities(EXPORT_FLUIDS_1X).setMinLayerLimited(1).setMaxLayerLimited(1))) + .where('X', casingPredicate.or(exportPredicate)) .where('Z', casingPredicate) .where('P', blocks(CASING_STEEL_PIPE.get())) .where('C', abilities(MUFFLER)) @@ -676,6 +679,56 @@ public static void init() {} .where('#', any()) .build(); }) + .shapeInfos(definition -> { + List shapeInfos = new ArrayList<>(); + var builder = MultiblockShapeInfo.builder() + .where('S', definition, Direction.NORTH) + .where('C', CASING_WATERTIGHT.getDefaultState()) + .where('M', MUFFLER_HATCH[IV], Direction.UP) + .where('X', PARALLEL_HATCH[IV], Direction.NORTH) + .where('H', FLUID_IMPORT_HATCH[IV], Direction.NORTH) + .where('B', ITEM_EXPORT_BUS[IV], Direction.NORTH) + .where('N', MAINTENANCE_HATCH, Direction.NORTH) + .where('P', CASING_STEEL_PIPE.getDefaultState()) + .where('F', FLUID_EXPORT_HATCH[IV], Direction.SOUTH) + .where('E', ENERGY_INPUT_HATCH[IV], Direction.SOUTH) + .where('#', Blocks.AIR.defaultBlockState()); + List aisle1 = new ArrayList<>(16); + aisle1.add("#HCB#"); + aisle1.add("#NSX#"); + aisle1.add("#####"); + List aisle2 = new ArrayList<>(16); + aisle2.add("CCCCC"); + aisle2.add("C###C"); + aisle2.add("#CCC#"); + List aisle3 = new ArrayList<>(16); + aisle3.add("CCCCC"); + aisle3.add("C###C"); + aisle3.add("#CMC#"); + List aisle4 = new ArrayList<>(16); + aisle4.add("CCCCC"); + aisle4.add("C###C"); + aisle4.add("#CCC#"); + List aisle5 = new ArrayList<>(16); + aisle5.add("#CEC#"); + aisle5.add("#CCC#"); + aisle5.add("#####"); + for (int i = 1; i <= 12; ++i) { + aisle1.add(2, "##C##"); + aisle2.add(2, "#C#C#"); + aisle3.add(2, "C#P#C"); + aisle4.add(2, "#C#C#"); + aisle5.add(2, "##F##"); + var copy = builder.shallowCopy() + .aisle(aisle1.toArray(String[]::new)) + .aisle(aisle2.toArray(String[]::new)) + .aisle(aisle3.toArray(String[]::new)) + .aisle(aisle4.toArray(String[]::new)) + .aisle(aisle5.toArray(String[]::new)); + shapeInfos.add(copy.build()); + } + return shapeInfos; + }) .partSorter(Comparator.comparingInt(a -> a.self().getPos().getY())) .workableCasingRenderer(GTCEu.id("block/casings/gcym/watertight_casing"), GTCEu.id("block/multiblock/gcym/large_distillery")) diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java new file mode 100644 index 0000000000..38a36cab0b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/multiblock/electric/DistillationTowerMachine.java @@ -0,0 +1,361 @@ +package com.gregtechceu.gtceu.common.machine.multiblock.electric; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.capability.recipe.*; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.feature.IRecipeLogicMachine; +import com.gregtechceu.gtceu.api.machine.multiblock.PartAbility; +import com.gregtechceu.gtceu.api.machine.multiblock.WorkableElectricMultiblockMachine; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; +import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic; +import com.gregtechceu.gtceu.api.recipe.GTRecipe; +import com.gregtechceu.gtceu.api.recipe.GTRecipeType; +import com.gregtechceu.gtceu.api.recipe.content.Content; +import com.gregtechceu.gtceu.api.recipe.content.ContentModifier; +import com.gregtechceu.gtceu.api.recipe.modifier.ParallelLogic; +import com.gregtechceu.gtceu.common.data.GTRecipeTypes; + +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.network.chat.Component; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandler.FluidAction; +import net.minecraftforge.fluids.capability.templates.VoidFluidHandler; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class DistillationTowerMachine extends WorkableElectricMultiblockMachine + implements FluidRecipeCapability.ICustomParallel { + + @Getter + private List fluidOutputs; + @Getter + @Nullable + private IFluidHandler firstValid = null; + private final int yOffset; + + public DistillationTowerMachine(IMachineBlockEntity holder) { + this(holder, 1); + } + + /** + * Construct DT Machine + * + * @param holder BlockEntity holder + * @param yOffset The Y difference between the controller and the first fluid output + */ + public DistillationTowerMachine(IMachineBlockEntity holder, int yOffset) { + super(holder); + this.yOffset = yOffset; + } + + @Override + protected RecipeLogic createRecipeLogic(Object... args) { + return new DistillationTowerLogic(this); + } + + @Override + public DistillationTowerLogic getRecipeLogic() { + return (DistillationTowerLogic) super.getRecipeLogic(); + } + + @Override + public void onStructureFormed() { + getDefinition().setPartSorter(Comparator.comparingInt(p -> p.self().getPos().getY())); + getDefinition().setAllowExtendedFacing(false); + super.onStructureFormed(); + var parts = getParts().stream() + .filter(part -> PartAbility.EXPORT_FLUIDS.isApplicable(part.self().getBlockState().getBlock())) + .toList(); + + if (!parts.isEmpty()) { + // Loop from controller y + offset -> highest output hatch + int y = getPos().getY() + yOffset; + int maxY = parts.get(parts.size() - 1).self().getPos().getY(); + fluidOutputs = new ObjectArrayList<>(maxY - y); + for (int outputIndex = 0; y <= maxY; ++y) { + if (parts.size() <= outputIndex) { + fluidOutputs.add(VoidFluidHandler.INSTANCE); + continue; + } + + var part = parts.get(outputIndex); + if (part.self().getPos().getY() == y) { + part.getRecipeHandlers().stream() + .filter(IFluidHandler.class::isInstance) + .findFirst() + .ifPresentOrElse(h -> { + fluidOutputs.add((IFluidHandler) h); + if (firstValid == null) firstValid = (IFluidHandler) h; + }, + () -> fluidOutputs.add(VoidFluidHandler.INSTANCE)); + outputIndex++; + } else if (part.self().getPos().getY() > y) { + fluidOutputs.add(VoidFluidHandler.INSTANCE); + } else { + GTCEu.LOGGER.error( + "The Distillation Tower at {} has a fluid export hatch with an unexpected Y position", + getPos()); + onStructureInvalid(); + return; + } + } + } + } + + @Override + public void onStructureInvalid() { + fluidOutputs = null; + firstValid = null; + super.onStructureInvalid(); + } + + public int limitParallel(GTRecipe recipe, int multiplier) { + int minMultiplier = 0; + int maxMultiplier = multiplier; + + var maxAmount = recipe.getOutputContents(FluidRecipeCapability.CAP).stream() + .map(Content::getContent) + .map(FluidRecipeCapability.CAP::of) + .filter(i -> !i.isEmpty()) + .map(i -> i.getStacks()[0]) + .mapToInt(FluidStack::getAmount) + .max() + .orElse(0); + + if (maxAmount == 0) return multiplier; + + while (minMultiplier != maxMultiplier) { + if (multiplier > Integer.MAX_VALUE / maxAmount) multiplier = Integer.MAX_VALUE / maxAmount; + GTRecipe copy = recipe.copy(ContentModifier.multiplier(multiplier), false); + boolean filled = getRecipeLogic().applyFluidOutputs(copy, FluidAction.SIMULATE); + int[] bin = ParallelLogic.adjustMultiplier(filled, minMultiplier, multiplier, maxMultiplier); + minMultiplier = bin[0]; + multiplier = bin[1]; + maxMultiplier = bin[2]; + } + return multiplier; + } + + public static class DistillationTowerLogic extends RecipeLogic { + + @Nullable + @Persisted + @DescSynced + GTRecipe workingRecipe = null; + + public DistillationTowerLogic(IRecipeLogicMachine machine) { + super(machine); + } + + @NotNull + @Override + public DistillationTowerMachine getMachine() { + return (DistillationTowerMachine) super.getMachine(); + } + + // Copy of lastRecipe with fluid outputs trimmed, for output displays like Jade or GUI text + @Override + public @Nullable GTRecipe getLastRecipe() { + return workingRecipe; + } + + @Override + @Nullable + public Iterator searchRecipe() { + var recipeType = machine.getRecipeType(); + if (recipeType == GTRecipeTypes.DISTILLERY_RECIPES) return super.searchRecipe(); + + // Do recipe searching ourselves so we can match the outputs how we want + IRecipeCapabilityHolder holder = this.machine; + if (!holder.hasProxies()) return null; + var iterator = recipeType.getLookup().getRecipeIterator(holder, recipe -> !recipe.isFuel && + this.matchDTRecipe(recipe, holder).isSuccess() && recipe.matchTickRecipe(holder).isSuccess()); + + boolean any = false; + while (iterator.hasNext()) { + GTRecipe recipe = iterator.next(); + if (recipe == null) continue; + any = true; + break; + } + + if (any) { + iterator.reset(); + return iterator; + } + + for (GTRecipeType.ICustomRecipeLogic logic : recipeType.getCustomRecipeLogicRunners()) { + GTRecipe recipe = logic.createCustomRecipe(holder); + if (recipe != null) return Collections.singleton(recipe).iterator(); + } + return Collections.emptyIterator(); + } + + @Override + public void findAndHandleRecipe() { + lastFailedMatches = null; + if (!recipeDirty && lastRecipe != null && + matchDTRecipe(lastRecipe, this.machine).isSuccess() && + lastRecipe.matchTickRecipe(this.machine).isSuccess() && + lastRecipe.checkConditions(this).isSuccess()) { + var recipe = lastRecipe; + lastRecipe = null; + lastOriginRecipe = null; + setupRecipe(recipe); + } else { + workingRecipe = null; + lastRecipe = null; + lastOriginRecipe = null; + handleSearchingRecipes(searchRecipe()); + } + } + + @Override + public boolean checkMatchedRecipeAvailable(GTRecipe match) { + var matchCopy = match.copy(); + var modified = machine.fullModifyRecipe(matchCopy, ocParams, ocResult); + if (modified != null) { + if (modified.checkConditions(this).isSuccess() && + matchDTRecipe(modified, machine).isSuccess() && + modified.matchTickRecipe(machine).isSuccess()) { + setupRecipe(modified); + } + if (lastRecipe != null && getStatus() == Status.WORKING) { + lastOriginRecipe = match; + lastFailedMatches = null; + return true; + } + } + return false; + } + + @Override + public void onRecipeFinish() { + machine.afterWorking(); + if (lastRecipe != null) { + lastRecipe.postWorking(machine); + handleRecipeIO(lastRecipe, IO.OUT); + if (machine.alwaysTryModifyRecipe()) { + if (lastOriginRecipe != null) { + var modified = machine.fullModifyRecipe(lastOriginRecipe.copy(), ocParams, ocResult); + if (modified == null) markLastRecipeDirty(); + else lastRecipe = modified; + } else { + markLastRecipeDirty(); + } + } + + if (!recipeDirty && + matchDTRecipe(lastRecipe, this.machine).isSuccess() && + lastRecipe.matchTickRecipe(this.machine).isSuccess() && + lastRecipe.checkConditions(this).isSuccess()) { + setupRecipe(lastRecipe); + } else { + setStatus(Status.IDLE); + progress = 0; + duration = 0; + isActive = false; + } + } + } + + private GTRecipe.ActionResult matchDTRecipe(GTRecipe recipe, IRecipeCapabilityHolder holder) { + var result = recipe.matchRecipeContents(IO.IN, holder, recipe.inputs, false); + if (!result.isSuccess()) return result; + + var items = recipe.getOutputContents(ItemRecipeCapability.CAP); + if (!items.isEmpty()) { + Map, List> out = Map.of(ItemRecipeCapability.CAP, items); + result = recipe.matchRecipeContents(IO.OUT, holder, out, false); + if (!result.isSuccess()) return result; + } + + if (!applyFluidOutputs(recipe, FluidAction.SIMULATE)) { + return GTRecipe.ActionResult.fail(() -> Component.translatable("gtceu.recipe_logic.insufficient_out") + .append(": ") + .append(FluidRecipeCapability.CAP.getName())); + } + + return GTRecipe.ActionResult.SUCCESS; + } + + private void updateWorkingRecipe(GTRecipe recipe) { + if (recipe.recipeType == GTRecipeTypes.DISTILLERY_RECIPES) { + this.workingRecipe = recipe; + return; + } + + this.workingRecipe = recipe.copy(); + var contents = recipe.getOutputContents(FluidRecipeCapability.CAP); + var outputs = getMachine().getFluidOutputs(); + List trimmed = new ArrayList<>(12); + for (int i = 0; i < Math.min(contents.size(), outputs.size()); ++i) { + if (!(outputs.get(i) instanceof VoidFluidHandler)) trimmed.add(contents.get(i)); + } + this.workingRecipe.outputs.put(FluidRecipeCapability.CAP, trimmed); + } + + @Override + protected boolean handleRecipeIO(GTRecipe recipe, IO io) { + if (io != IO.OUT) { + if (super.handleRecipeIO(recipe, io)) { + updateWorkingRecipe(recipe); + return true; + } + this.workingRecipe = null; + return false; + } + var items = recipe.getOutputContents(ItemRecipeCapability.CAP); + if (!items.isEmpty()) { + Map, List> out = Map.of(ItemRecipeCapability.CAP, items); + recipe.handleRecipe(io, this.machine, false, out, chanceCaches); + } + return applyFluidOutputs(recipe, FluidAction.EXECUTE); + } + + private boolean applyFluidOutputs(GTRecipe recipe, FluidAction action) { + var fluids = recipe.getOutputContents(FluidRecipeCapability.CAP) + .stream() + .map(Content::getContent) + .map(FluidRecipeCapability.CAP::of) + .toList(); + + // Distillery recipes should output to the first non-void handler + if (recipe.recipeType == GTRecipeTypes.DISTILLERY_RECIPES) { + var fluid = fluids.get(0).getStacks()[0]; + var handler = getMachine().getFirstValid(); + if (handler == null) return false; + int filled = (handler instanceof NotifiableFluidTank nft) ? + nft.fillInternal(fluid, action) : + handler.fill(fluid, action); + return filled == fluid.getAmount(); + } + + boolean valid = true; + var outputs = getMachine().getFluidOutputs(); + for (int i = 0; i < Math.min(fluids.size(), outputs.size()); ++i) { + var handler = outputs.get(i); + var fluid = fluids.get(i).getStacks()[0]; + int filled = (handler instanceof NotifiableFluidTank nft) ? + nft.fillInternal(fluid, action) : + handler.fill(fluid, action); + if (filled != fluid.getAmount()) valid = false; + if (action.simulate() && !valid) break; + } + return valid; + } + } +} diff --git a/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower.png b/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower.png index b41f03dd7f..bbb98d7970 100644 Binary files a/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower.png and b/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower.png differ diff --git a/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower_bubbles.png b/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower_bubbles.png new file mode 100644 index 0000000000..56c66e5434 Binary files /dev/null and b/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower_bubbles.png differ diff --git a/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower_coil.png b/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower_coil.png new file mode 100644 index 0000000000..4f0b83418b Binary files /dev/null and b/src/main/resources/assets/gtceu/textures/gui/progress_bar/progress_bar_distillation_tower_coil.png differ diff --git a/src/main/resources/assets/gtceu/ui/recipe_type/distillation_tower.rtui b/src/main/resources/assets/gtceu/ui/recipe_type/distillation_tower.rtui new file mode 100644 index 0000000000..5a00fbca4f Binary files /dev/null and b/src/main/resources/assets/gtceu/ui/recipe_type/distillation_tower.rtui differ