package gregtech.api.util; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.function.BiFunction; import java.util.function.Function; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.IFluidTank; import com.gtnewhorizon.gtnhlib.util.map.ItemStackMap; import com.gtnewhorizons.modularui.api.forge.IItemHandler; import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_Hatch; import gregtech.api.metatileentity.implementations.GT_MetaTileEntity_MultiBlockBase; import gregtech.api.multitileentity.multiblock.base.Controller; import gregtech.common.tileentities.machines.GT_MetaTileEntity_Hatch_OutputBus_ME; import gregtech.common.tileentities.machines.GT_MetaTileEntity_Hatch_Output_ME; /** * Helper class to calculate how many parallels of items / fluids can fit in the output buses / hatches. */ public class VoidProtectionHelper { /** * A MetaTileEntity Controller */ private GT_MetaTileEntity_MultiBlockBase machineMeta; /** * A MultiTileEntity Controller */ private Controller machineMulti; /** * The maximum possible parallel possible for the multiblock */ private int maxParallel = 1; /** * The item outputs to check */ private ItemStack[] itemOutputs; /** * The fluid outputs to check */ private FluidStack[] fluidOutputs; /** * Has this helper been built? */ private boolean built; public VoidProtectionHelper() {} /** * Sets MetaTE controller to use for calculation */ public VoidProtectionHelper setController(GT_MetaTileEntity_MultiBlockBase machineMeta) { this.machineMeta = machineMeta; return this; } /** * Sets MuTE controller to use for calculation */ public VoidProtectionHelper setController(Controller machineMulti) { this.machineMulti = machineMulti; return this; } public VoidProtectionHelper setItemOutputs(ItemStack[] itemOutputs) { this.itemOutputs = itemOutputs; return this; } public VoidProtectionHelper setFluidOutputs(FluidStack[] fluidOutputs) { this.fluidOutputs = fluidOutputs; return this; } /** * Sets the MaxParallel a multi can handle */ public VoidProtectionHelper setMaxParallel(int maxParallel) { this.maxParallel = maxParallel; return this; } /** * Finishes the VoidProtectionHelper. Anything changed after this will not affect anything */ public VoidProtectionHelper build() { if (built) { throw new IllegalStateException("Tried to build twice"); } built = true; determineParallel(); return this; } /** * @return The current parallels possible by the multiblock */ public int getMaxParallel() { if (!built) { throw new IllegalStateException("Tried to get parallels before building"); } return maxParallel; } /** * Called by {@link #build()}. Determines the parallels and everything else that needs to be done at build time */ private void determineParallel() { if (itemOutputs == null) { itemOutputs = new ItemStack[0]; } if (fluidOutputs == null) { fluidOutputs = new FluidStack[0]; } if (machineMeta != null) { boolean tMEOutputBus = false; boolean tMEOutputHatch = false; for (GT_MetaTileEntity_Hatch tHatch : machineMeta.mOutputBusses) { if (tHatch instanceof GT_MetaTileEntity_Hatch_OutputBus_ME) { tMEOutputBus = true; break; } } for (GT_MetaTileEntity_Hatch tHatch : machineMeta.mOutputHatches) { if (tHatch instanceof GT_MetaTileEntity_Hatch_Output_ME) { tMEOutputHatch = true; break; } } if (!tMEOutputBus) { maxParallel = Math.min(calculateMaxItemParallelsForMetaTEs(), maxParallel); } if (!tMEOutputHatch) { maxParallel = Math.min(calculateMaxFluidParallelsForMetaTEs(), maxParallel); } } else if (machineMulti != null) { maxParallel = Math.min(calculateMaxItemParallelsForMuTEs(), maxParallel); maxParallel = Math.min(calculateMaxFluidParallelsForMuTEs(), maxParallel); } } /** * Calculates the max parallel for fluids if void protection is turned on */ private int calculateMaxFluidParallelsForMuTEs() { if (machineMulti == null || machineMulti.getOutputTanks() == null) { return 0; } return calculateMaxFluidParallels( Arrays.asList(machineMulti.getOutputTanks()), tHatch -> tHatch.getFluidAmount() == 0, (tHatch, fluidStack) -> true); } /** * Calculates the max parallel for fluids if void protection is turned on */ private int calculateMaxFluidParallelsForMetaTEs() { if (machineMeta == null) { return 0; } return calculateMaxFluidParallels( machineMeta.mOutputHatches, tHatch -> tHatch.mMode == 0 && tHatch.getFluidAmount() == 0, (tHatch, fluidStack) -> { if (GT_ModHandler.isSteam(fluidStack)) { return tHatch.outputsSteam(); } else { if (!tHatch.outputsLiquids()) { return false; } String tLockedFluidName = tHatch.getLockedFluidName(); return !tHatch.isFluidLocked() || tLockedFluidName == null || tLockedFluidName.equals( fluidStack.getFluid() .getName()); } }); } /** * Calculates the max parallel for fluids if void protection is turned on */ private int calculateMaxFluidParallels(List hatches, Function isEmpty, BiFunction acceptsFluid) { if (hatches.size() < fluidOutputs.length) { return 0; } // A map to hold the items we will be 'inputting' into the output hatches. These fluidstacks are actually // the recipe outputs. Map tFluidOutputMap = new HashMap<>(); // Map that keeps track of the number of parallel crafts we can accommodate for each fluid output. // In the pair, we keep track of number of full crafts plus mb of fluid in a partial craft, to avoid // issues with floating point math not being completely accurate when summing. Map tParallels = new HashMap<>(); // Iterate over the outputs, calculating require stack spacing they will require. for (FluidStack aY : fluidOutputs) { if (aY == null) { continue; } tFluidOutputMap.merge(aY, aY.amount, Integer::sum); tParallels.put(aY, new ParallelData(0, 0)); } if (tFluidOutputMap.isEmpty()) { // nothing to output, bail early return maxParallel; } for (T tHatch : hatches) { int tSpaceLeft = tHatch.getCapacity() - tHatch.getFluidAmount(); // check if hatch filled if (tSpaceLeft <= 0) continue; // check if hatch is empty and unrestricted if (isEmpty.apply(tHatch)) continue; for (Map.Entry entry : tParallels.entrySet()) { FluidStack tFluidOutput = entry.getKey(); if (!acceptsFluid.apply(tHatch, tFluidOutput)) continue; // this fluid is not prevented by restrictions on output hatch if (tHatch.getFluidAmount() == 0 || GT_Utility.areFluidsEqual(tHatch.getFluid(), tFluidOutput)) { ParallelData tParallel = entry.getValue(); Integer tCraftSize = tFluidOutputMap.get(tFluidOutput); tParallel.batch += (tParallel.partial + tSpaceLeft) / tCraftSize; tParallel.partial = (tParallel.partial + tSpaceLeft) % tCraftSize; } } } // now that all partial/restricted hatches have been counted, create a priority queue for our outputs // the lowest priority fluid is the number of complete parallel crafts we can support PriorityQueue> aParallelQueue = new PriorityQueue<>( Comparator.comparing(i -> i.batch)); for (Map.Entry entry : tParallels.entrySet()) { aParallelQueue .add(new ParallelStackInfo<>(entry.getValue().batch, entry.getValue().partial, entry.getKey())); } // add extra parallels for open slots as well for (T tHatch : hatches) { // partially filled or restricted hatch. done in last pass if (!isEmpty.apply(tHatch)) continue; ParallelStackInfo tParallel = aParallelQueue.poll(); assert tParallel != null; // will always be true, specifying assert here to avoid IDE/compiler warnings Integer tCraftSize = tFluidOutputMap.get(tParallel.stack); int tSpaceLeft = tHatch.getCapacity(); tParallel.batch += (tParallel.partial + tSpaceLeft) / tCraftSize; tParallel.partial = (tParallel.partial + tSpaceLeft) % tCraftSize; aParallelQueue.add(tParallel); } return aParallelQueue.element().batch; } /** * Calculates the max parallels one can do with items if void protection is on */ private int calculateMaxItemParallelsForMuTEs() { List busStacks = new ArrayList<>(); if (machineMulti != null) { IItemHandler inv = machineMulti.getOutputInventory(); if (inv != null && inv.getSlots() > 0) { for (int i = 0; i < inv.getSlots(); i++) { busStacks.add(inv.getStackInSlot(i)); } } } return calculateMaxItemParallels(busStacks); } /** * Calculates the max parallels one can do with items if void protection is on */ private int calculateMaxItemParallelsForMetaTEs() { List busStacks = new ArrayList<>(); if (machineMeta != null) { for (final GT_MetaTileEntity_Hatch tBus : machineMeta.mOutputBusses) { if (!GT_MetaTileEntity_MultiBlockBase.isValidMetaTileEntity(tBus)) { continue; } final IInventory tBusInv = tBus.getBaseMetaTileEntity(); for (int i = 0; i < tBusInv.getSizeInventory(); i++) { busStacks.add(tBus.getStackInSlot(i)); } } } return calculateMaxItemParallels(busStacks); } /** * Calculates the max parallels one can do with items if void protection is on * * @param busStacks List of itemstacks that are already stored in buses */ private int calculateMaxItemParallels(List busStacks) { // A map to hold the items we will be 'inputting' into the output buses. These itemstacks are actually the // recipe outputs. Map tItemOutputMap = new ItemStackMap<>(); // Map that keeps track of the number of parallel crafts we can accommodate for each item output. // In the pair, we keep track of number of full crafts plus number of items in a partial craft, to avoid // issues with floating point math not being completely accurate when summing. Map tParallels = new ItemStackMap<>(); int tSlotsFree = 0; for (ItemStack tItem : itemOutputs) { tItemOutputMap.merge(tItem, tItem.stackSize, Integer::sum); tParallels.put(tItem, new ParallelData(0, 0)); } if (tItemOutputMap.isEmpty()) { // nothing to output, bail early return maxParallel; } if (itemOutputs.length > 0) { for (ItemStack tBusStack : busStacks) { if (tBusStack == null) { tSlotsFree++; } else { // get the real stack size // we ignore the bus inventory stack limit here as no one set it to anything other than 64 int tMaxBusStackSize = tBusStack.getMaxStackSize(); if (tBusStack.stackSize >= tMaxBusStackSize) // this bus stack is full. no checking continue; int tSpaceLeft = tMaxBusStackSize - tBusStack.stackSize; Integer tCraftSize = tItemOutputMap.get(tBusStack); if (tCraftSize == null) { // we don't have a matching stack to output, ignore this bus stack continue; } ParallelData tParallel = tParallels.get(tBusStack); tParallel.batch += (tParallel.partial + tSpaceLeft) / tCraftSize; tParallel.partial = (tParallel.partial + tSpaceLeft) % tCraftSize; } } // now that all partial stacks have been counted, create a priority queue for our outputs // the lowest priority item is the number of complete parallel crafts we can support PriorityQueue> aParallelQueue = new PriorityQueue<>( Comparator.comparing(i -> i.batch)); for (Map.Entry entry : tParallels.entrySet()) { aParallelQueue .add(new ParallelStackInfo<>(entry.getValue().batch, entry.getValue().partial, entry.getKey())); } while (tSlotsFree > 0) { ParallelStackInfo tParallel = aParallelQueue.poll(); assert tParallel != null; // will always be true, specifying assert here to avoid IDE/compiler warnings Integer tCraftSize = tItemOutputMap.get(tParallel.stack); int tStackSize = tParallel.stack.getMaxStackSize(); tParallel.batch += (tParallel.partial + tStackSize) / tCraftSize; tParallel.partial = (tParallel.partial + tStackSize) % tCraftSize; aParallelQueue.add(tParallel); --tSlotsFree; } return aParallelQueue.element().batch; } return 0; } private static class ParallelData { private int batch; private int partial; private ParallelData(int batch, int partial) { this.batch = batch; this.partial = partial; } } private static class ParallelStackInfo { private int batch; private int partial; private final T stack; private ParallelStackInfo(int batch, int partial, T stack) { this.batch = batch; this.partial = partial; this.stack = stack; } } }