From 795c200ac7bd965898e9e1885da2293a12d05eb0 Mon Sep 17 00:00:00 2001 From: Exaxxion Date: Sat, 2 Oct 2021 00:58:13 -0700 Subject: [PATCH] New algorithm for Fluid Regulator Keep Exact Mode (#1717) --- .../common/covers/CoverFluidRegulator.java | 147 ++++++++-- .../covers/CoverFluidRegulatorTest.java | 263 ++++++++++++++++++ 2 files changed, 385 insertions(+), 25 deletions(-) create mode 100644 src/test/java/gregtech/common/covers/CoverFluidRegulatorTest.java diff --git a/src/main/java/gregtech/common/covers/CoverFluidRegulator.java b/src/main/java/gregtech/common/covers/CoverFluidRegulator.java index 803d550db6..178615dc00 100644 --- a/src/main/java/gregtech/common/covers/CoverFluidRegulator.java +++ b/src/main/java/gregtech/common/covers/CoverFluidRegulator.java @@ -17,8 +17,9 @@ import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.capability.IFluidHandler; import net.minecraftforge.fluids.capability.IFluidTankProperties; +import org.apache.logging.log4j.message.FormattedMessage; -import java.util.List; +import java.util.*; import java.util.function.Predicate; @@ -73,32 +74,128 @@ protected int doTransferExact(int transferLimit, IFluidHandler sourceHandler, IF return transferLimit - fluidLeftToTransfer; } - protected int doKeepExact(int transferLimit, IFluidHandler sourceHandler, IFluidHandler destHandler, Predicate fluidFilter, int keepAmount) { - int fluidLeftToTransfer = transferLimit; - for (IFluidTankProperties tankProperties : sourceHandler.getTankProperties()) { - FluidStack sourceFluid = tankProperties.getContents(); - if (sourceFluid == null || sourceFluid.amount == 0 || !fluidFilter.test(sourceFluid)) continue; - sourceFluid.amount = keepAmount; - FluidStack destFluid = destHandler.drain(sourceFluid, false); - int amountToDrainAndFill; - //no fluid in destination - if (destFluid == null) { - amountToDrainAndFill = Math.min(keepAmount, fluidLeftToTransfer); - //if the amount of fluid in the destination is sufficient or the destinations fluid isnt equal to the sources - //how to check if destHandler is full? - } else if (destFluid.amount >= keepAmount || !destFluid.isFluidEqual(sourceFluid)) { - continue; - } else { - //if keepAmount is larger than the transferLimit we will have to stock it over several ticks (seconds?) - amountToDrainAndFill = Math.min(keepAmount - destFluid.amount, fluidLeftToTransfer); - } - sourceFluid.amount = amountToDrainAndFill; - if (GTFluidUtils.transferExactFluidStack(sourceHandler, destHandler, sourceFluid.copy())) { - fluidLeftToTransfer -= sourceFluid.amount; + /** + * Performs one tick worth of Keep Exact behavior. + * @param transferLimit the maximum amount in milliBuckets that may be transferred in one tick + * @param sourceHandler source(s) to move fluids from + * @param destHandler destination(s) to move fluids to + * @param fluidFilter a predicate which determines what fluids may be moved + * @param keepAmount the desired amount in milliBuckets of a particular fluid in the destination + * @return the total amount in milliBuckets of all fluids transferred from source to dest by this method + */ + protected int doKeepExact(final int transferLimit, + final IFluidHandler sourceHandler, + final IFluidHandler destHandler, + final Predicate fluidFilter, + final int keepAmount) { + + if(sourceHandler == null || destHandler == null || fluidFilter == null || keepAmount <= 0) + return 0; + + final Map sourceFluids = + collectDistinctFluids(sourceHandler, IFluidTankProperties::canDrain, fluidFilter); + final Map destFluids = + collectDistinctFluids(destHandler, IFluidTankProperties::canFill, fluidFilter); + + int transferred = 0; + for(FluidStack fluidStack : sourceFluids.keySet()) { + if(transferred >= transferLimit) + break; + + // if fluid needs to be moved to meet the Keep Exact value + int amountInDest; + if((amountInDest = destFluids.getOrDefault(fluidStack, 0)) < keepAmount) { + + // move the lesser of the remaining transfer limit and the difference in actual vs keep exact amount + int amountToMove = Math.min(transferLimit - transferred, + keepAmount - amountInDest); + + // Nothing to do here, try the next fluid. + if(amountToMove <= 0) + continue; + + // Simulate a drain of this fluid from the source tanks + FluidStack drainedResult = sourceHandler.drain(copyFluidStackWithAmount(fluidStack, amountToMove), false); + + // Can't drain this fluid. Try the next one. + if(drainedResult == null || drainedResult.amount <= 0 || !fluidStack.equals(drainedResult)) + continue; + + // account for the possibility that the drain might give us less than requested + final int drainable = Math.min(amountToMove, drainedResult.amount); + + // Simulate a fill of the drained amount + int fillResult = destHandler.fill(copyFluidStackWithAmount(fluidStack, drainable), false); + + // Can't fill, try the next fluid. + if(fillResult <= 0) + continue; + + // This Fluid can be drained and filled, so let's move the most that will actually work. + int fluidToMove = Math.min(drainable, fillResult); + FluidStack drainedActual = sourceHandler.drain(copyFluidStackWithAmount(fluidStack, fluidToMove), true); + + // Account for potential error states from the drain + if(drainedActual == null) + throw new RuntimeException("Misbehaving fluid container: drain produced null after simulation succeeded"); + + if(!fluidStack.equals(drainedActual)) + throw new RuntimeException("Misbehaving fluid container: drain produced a different fluid than the simulation"); + + if(drainedActual.amount != fluidToMove) + throw new RuntimeException(new FormattedMessage( + "Misbehaving fluid container: drain expected: {}, actual: {}", + fluidToMove, + drainedActual.amount).getFormattedMessage()); + + + // Perform Fill + int filledActual = destHandler.fill(copyFluidStackWithAmount(fluidStack, fluidToMove), true); + + // Account for potential error states from the fill + if(filledActual != fluidToMove) + throw new RuntimeException(new FormattedMessage( + "Misbehaving fluid container: fill expected: {}, actual: {}", + fluidToMove, + filledActual).getFormattedMessage()); + + // update the transferred amount + transferred += fluidToMove; } - if (fluidLeftToTransfer == 0) break; } - return transferLimit - fluidLeftToTransfer; + + return transferred; + } + + /** + * Copies a FluidStack and sets its amount to the specified value. + * + * @param fs the original fluid stack to copy + * @param amount the amount to set the copied FluidStack to + * @return the copied FluidStack with the specified amount + */ + private static FluidStack copyFluidStackWithAmount(FluidStack fs, int amount) { + FluidStack fs2 = fs.copy(); + fs2.amount = amount; + return fs2; + } + + private Map collectDistinctFluids(IFluidHandler handler, + Predicate tankTypeFilter, + Predicate fluidTypeFilter) { + + final Map summedFluids = new HashMap<>(); + Arrays.stream(handler.getTankProperties()) + .filter(tankTypeFilter) + .map(IFluidTankProperties::getContents) + .filter(Objects::nonNull) + .filter(fluidTypeFilter) + .forEach(fs -> { + summedFluids.putIfAbsent(fs, 0); + summedFluids.computeIfPresent(fs, (k,v) -> v + fs.amount); + }); + + return summedFluids; } public void setTransferMode(TransferMode transferMode) { diff --git a/src/test/java/gregtech/common/covers/CoverFluidRegulatorTest.java b/src/test/java/gregtech/common/covers/CoverFluidRegulatorTest.java new file mode 100644 index 0000000000..ab4d9fa758 --- /dev/null +++ b/src/test/java/gregtech/common/covers/CoverFluidRegulatorTest.java @@ -0,0 +1,263 @@ +package gregtech.common.covers; + +import gregtech.api.capability.impl.*; +import net.minecraft.init.*; +import net.minecraft.util.*; +import net.minecraftforge.fluids.*; +import net.minecraftforge.fluids.capability.*; +import org.junit.*; + +import java.util.function.*; + +import static org.junit.Assert.*; + +public class CoverFluidRegulatorTest { + public static final Predicate isWater = fs -> fs.getFluid() == FluidRegistry.WATER; + + /** + * Required. Without this all item-related operations will fail because registries haven't been initialized. + */ + @BeforeClass + public static void bootStrap() { + Bootstrap.register(); + } + + @Test + public void doKeepExact_does_nothing_if_no_destination_tank_exists() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + FluidStack water = new FluidStack(FluidRegistry.WATER, 1234); + + // Source consists of only an output tank containing a bit of water + IFluidHandler source = + new FluidHandlerProxy(new FluidTankList(false), + new FluidTankList(false, new FluidTank(water.copy(), 64000))); + + // Tell it to keep exact from a machine with an empty fluid tank and null target fluid tank + int amountTransferred = cfr.doKeepExact(1000, source, null, isWater, 1000); + + assertEquals("Unexpectedly moved fluids, nothing is supposed to happen", 0, amountTransferred); + } + + @Test + public void doKeepExact_moves_one_fluid_into_an_empty_tank() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + FluidStack water = new FluidStack(FluidRegistry.WATER, 1234); + + IFluidHandler source = + new FluidHandlerProxy(new FluidTankList(false), + new FluidTankList(false, new FluidTank(water.copy(), 64000))); + + // Dest consists of one empty input tank + IFluidHandler dest = + new FluidHandlerProxy(new FluidTankList(false, new FluidTank(64000)), + new FluidTankList(false)); + + // Tell it to keep exact from a machine with an empty fluid tank and no target fluid tank + int amountTransferred = cfr.doKeepExact(1000, source, dest, isWater, 1000); + + assertEquals("Wrong fluid amount moved", 1000, amountTransferred); + } + + @Test + public void doKeepExact_moves_only_as_much_fluid_as_exists_in_the_source() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + IFluidHandler source = + new FluidHandlerProxy(new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 1234), 64000))); + + IFluidHandler dest = + new FluidHandlerProxy(new FluidTankList(false, new FluidTank(64000)), + new FluidTankList(false)); + + int amountTransferred = cfr.doKeepExact(10000, source, dest, isWater, 10000); + + assertEquals("Wrong fluid amount moved", 1234, amountTransferred); + } + + @Test + public void doKeepExact_moves_only_the_fluid_required_if_more_could_be_moved() { + + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + IFluidHandler source = + new FluidHandlerProxy( + new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 64000), 64000))); + + IFluidHandler dest = + new FluidHandlerProxy( + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 100), 64000)), + new FluidTankList(false)); + + int amountTransferred = cfr.doKeepExact(10000, source, dest, isWater, 144); + + assertEquals("Wrong fluid amount moved", 44, amountTransferred); + } + + @Test + public void doKeepExact_moves_multiple_valid_fluids() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + IFluidHandler source = + new FluidHandlerProxy( + new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 64000), 64000), + new FluidTank(new FluidStack(FluidRegistry.LAVA, 64000), 64000))); + + // One tank with 100mB water, another with nothing + IFluidHandler dest = + new FluidHandlerProxy( + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 100), 64000), + new FluidTank(64000)), + new FluidTankList(false)); + + // accept any fluid this time + int amountTransferred = cfr.doKeepExact(10000, source, dest, fs -> true, 144); + + // expect that 44mB of water and 144mB of lava will be moved + assertEquals("Wrong fluid amount moved", 44+144, amountTransferred); + + // verify final fluid quantities + assertEquals(2, dest.getTankProperties().length); + IFluidTankProperties tank1 = dest.getTankProperties()[0]; + IFluidTankProperties tank2 = dest.getTankProperties()[1]; + assertNotNull(tank1.getContents()); + assertNotNull(tank2.getContents()); + assertTrue(tank1.getContents().isFluidStackIdentical(new FluidStack(FluidRegistry.WATER, 144))); + assertTrue(tank2.getContents().isFluidStackIdentical(new FluidStack(FluidRegistry.LAVA, 144))); + } + + @Test + public void doKeepExact_respects_transfer_limit_with_one_fluid() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + // One output tank full of water + IFluidHandler source = + new FluidHandlerProxy( + new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 64000), 64000))); + + // One input tank with nothing in it + IFluidHandler dest = + new FluidHandlerProxy( + new FluidTankList(false, new FluidTank(64000)), + new FluidTankList(false)); + + // accept any fluid this time + int amountTransferred = cfr.doKeepExact(100, source, dest, fs -> true, 144); + + // expect that at most 100mB of fluids total will be moved this tick, as if possible it would do 144mB + assertEquals("Wrong fluid amount moved", 100, amountTransferred); + } + + @Test + public void doKeepExact_respects_transfer_limit_with_multiple_fluids() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + IFluidHandler source = + new FluidHandlerProxy( + new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 64000), 64000), + new FluidTank(new FluidStack(FluidRegistry.LAVA, 64000), 64000))); + + // One tank with 100mB water, another with nothing + IFluidHandler dest = + new FluidHandlerProxy( + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 100), 64000), + new FluidTank(64000)), + new FluidTankList(false)); + + // accept any fluid this time + int amountTransferred = cfr.doKeepExact(100, source, dest, fs -> true, 144); + + // expect that at most 100mB of fluids total will be moved this tick, as if possible it would do 188mB + assertEquals("Wrong fluid amount moved", 100, amountTransferred); + } + + @Test + public void doKeepExact_does_nothing_if_levels_are_already_correct_in_dest() { + + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + IFluidHandler source = + new FluidHandlerProxy( + new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 64000), 64000), + new FluidTank(new FluidStack(FluidRegistry.LAVA, 64000), 64000))); + + // One tank with 144mB water, another with 144mB lava + IFluidHandler dest = + new FluidHandlerProxy( + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 144), 64000), + new FluidTank(new FluidStack(FluidRegistry.LAVA, 144), 64000)), + new FluidTankList(false)); + + // accept any fluid this time + int amountTransferred = cfr.doKeepExact(10000, source, dest, fs -> true, 144); + + // expect that no fluids are moved because Keep Exact levels are already met + assertEquals("Wrong fluid amount moved", 0, amountTransferred); + } + + @Test + public void doKeepExact_ignores_fluids_not_in_filter() { + // Create a regulator for testing with, and set it to "Keep Exact" mode + CoverFluidRegulator cfr = new CoverFluidRegulator(null, EnumFacing.UP, 0, 1000); + cfr.transferMode = TransferMode.KEEP_EXACT; + + IFluidHandler source = + new FluidHandlerProxy( + new FluidTankList(false), + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 64000), 64000), + new FluidTank(new FluidStack(FluidRegistry.LAVA, 64000), 64000))); + + // One tank with 144mB water, another with 100mB lava + IFluidHandler dest = + new FluidHandlerProxy( + new FluidTankList(false, + new FluidTank(new FluidStack(FluidRegistry.WATER, 144), 64000), + new FluidTank(new FluidStack(FluidRegistry.LAVA, 100), 64000)), + new FluidTankList(false)); + + // accept any fluid this time + int amountTransferred = cfr.doKeepExact(10000, source, dest, isWater, 144); + + // expect that no fluids are moved because already have enough water and lava isn't in the filter + assertEquals("Wrong fluid amount moved", 0, amountTransferred); + } +} \ No newline at end of file