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 93d777ba30..de81ca4801 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTMachines.java @@ -442,8 +442,8 @@ public class GTMachines { Component.translatable("gtceu.universal.tooltip.fluid_storage_capacity", FormattingUtil.formatNumbers(16 * FluidHelper.getBucket() * Math.max(1, tier))), Component.translatable("gtceu.universal.tooltip.working_area", - PumpMachine.BASE_PUMP_RANGE + PumpMachine.EXTRA_PUMP_RANGE * tier, - PumpMachine.BASE_PUMP_RANGE + PumpMachine.EXTRA_PUMP_RANGE * tier)) + PumpMachine.getMaxPumpRadius(tier) * 2, + PumpMachine.getMaxPumpRadius(tier) * 2)) .compassNode("pump") .register(), LV, MV, HV, EV); diff --git a/src/main/java/com/gregtechceu/gtceu/common/machine/electric/PumpMachine.java b/src/main/java/com/gregtechceu/gtceu/common/machine/electric/PumpMachine.java index 87eb128b87..405f728d0f 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/machine/electric/PumpMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/common/machine/electric/PumpMachine.java @@ -13,7 +13,6 @@ import com.gregtechceu.gtceu.api.machine.feature.IUIMachine; import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; import com.gregtechceu.gtceu.common.data.GTBlocks; -import com.gregtechceu.gtceu.utils.GTUtil; import com.lowdragmc.lowdraglib.gui.modular.ModularUI; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; @@ -28,21 +27,33 @@ import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraftforge.fluids.FluidType; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import lombok.Getter; import lombok.Setter; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; +import java.util.List; +import java.util.Queue; import java.util.Set; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; /** @@ -56,12 +67,11 @@ public class PumpMachine extends TieredEnergyMachine implements IAutoOutputFluid protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(PumpMachine.class, TieredEnergyMachine.MANAGED_FIELD_HOLDER); - public static final int BASE_PUMP_RANGE = 32; - public static final int EXTRA_PUMP_RANGE = 8; + public static final int BASE_PUMP_RADIUS = 16; + public static final int EXTRA_PUMP_RADIUS = 4; public static final int PUMP_SPEED_BASE = 80; - private final Deque fluidSourceBlocks = new ArrayDeque<>(); - private final Deque blocksToCheck = new ArrayDeque<>(); - private boolean initializedQueue = false; + private final Set forbiddenBlocks = new ObjectOpenHashSet<>(); + private PumpQueue pumpQueue = null; @Getter @Persisted private int pumpHeadY; @@ -124,63 +134,285 @@ public boolean shouldWeatherOrTerrainExplosion() { ////////////////////////////////////// // ********* Logic **********// ////////////////////////////////////// - private int getMaxPumpRange() { - return BASE_PUMP_RANGE + EXTRA_PUMP_RANGE * getTier(); + public static int getMaxPumpRadius(int tier) { + return BASE_PUMP_RADIUS + EXTRA_PUMP_RADIUS * tier; } - private boolean isStraightInPumpRange(BlockPos checkPos) { - BlockPos pos = getPos(); - return checkPos.getX() == pos.getX() && - checkPos.getZ() == pos.getZ() && - pos.getY() < checkPos.getY() && - pos.getY() + pumpHeadY >= checkPos.getY(); + /** + * Returns a list of directions, starting with Up and then horizontal directions with the directions most matching + * the vector first. + */ + private List biasedInVecDirections(RandomSource randomSource, Vec3i vec, boolean goUp) { + List searchList = new ArrayList<>(); + if (goUp) { + searchList.add(Direction.UP); + } + + ObjectArrayList axes = new ObjectArrayList<>(); + int zValue = Math.abs(vec.getZ()); + int xValue = Math.abs(vec.getX()); + if (zValue > xValue) { + axes.add(Direction.Axis.Z); + axes.add(Direction.Axis.X); + } else if (zValue < xValue) { + axes.add(Direction.Axis.X); + axes.add(Direction.Axis.Z); + } else { + axes.add(Direction.Axis.Z); + axes.add(Direction.Axis.X); + Util.shuffle(axes, randomSource); + } + + Direction lastDirection = null; + for (int i = 0; i < 2; i++) { + Direction.Axis axis = axes.get(i); + int value; + if (axis.equals(Direction.Axis.Z)) { + value = vec.getZ(); + } else { + value = vec.getX(); + } + + Direction direction; + if (value < 0) { + direction = Direction.fromAxisAndDirection(axis, Direction.AxisDirection.NEGATIVE); + } else if (value > 0) { + direction = Direction.fromAxisAndDirection(axis, Direction.AxisDirection.POSITIVE); + } else { + direction = Direction.fromAxisAndDirection(axis, + Util.getRandom(Direction.AxisDirection.values(), randomSource)); + } + searchList.add(direction); + if (i == 0) { + lastDirection = direction.getOpposite(); + } else { + searchList.add(direction.getOpposite()); + } + + } + searchList.add(lastDirection); + + return searchList; } - private void updateQueueState(int blocksToCheckAmount) { - BlockPos selfPos = getPos().below(pumpHeadY); + protected record PumpQueue(Queue> queue, FluidType fluidType) {} - for (int i = 0; i < blocksToCheckAmount; i++) { - BlockPos checkPos = null; - int amountIterated = 0; - do { - if (checkPos != null) { - blocksToCheck.push(checkPos); - amountIterated++; + protected record SearchResult(BlockPos pos, boolean isSource) {} + + /** + * Returns the next block to search at. + */ + @Nullable + private SearchResult searchNext(Level level, BlockPos headPosBelow, BlockPos searchHead, FluidType fluidType, + int maxPumpRange, boolean goUp, Set checked) { + // Vector from the pump head to the search head, so points in the direction away from the pump head + Vec3i subVec = searchHead.subtract(headPosBelow); + + List searchList = biasedInVecDirections(level.getRandom(), subVec, goUp); + + for (Direction direction : searchList) { + BlockPos check = searchHead.relative(direction); + // The pos at the same y-level as the spot to check, but the x and z of the pump + // This is to compute the square distance only in the horizontal plane + BlockPos pumpY = headPosBelow.atY(check.getY()); + + // Skip if outside pump range or not loaded or already checked + if (check.distSqr(pumpY) > maxPumpRange * maxPumpRange || checked.contains(check) || + !level.isLoaded(check) || forbiddenBlocks.contains(check)) { + continue; + } + + // Make sure we don't look at it again + checked.add(check); + + BlockState state = level.getBlockState(check); + FluidState fluidState; + + // If it's not a fluid of the right type, we stop + if ((fluidState = state.getFluidState()).getFluidType() == fluidType && + state.getBlock() instanceof LiquidBlock liquidBlock) { + // Remember all the sources we find + boolean isSource = fluidState.isSource(); + if (isSource) { + var fluidHandler = new FluidBlockTransfer(liquidBlock, level, check); + FluidStack drainStack = fluidHandler.drain(Integer.MAX_VALUE, true); + if (!drainStack.isEmpty()) { + return new SearchResult(check, true); + } } - checkPos = blocksToCheck.poll(); - - } while (checkPos != null && - !getLevel().isLoaded(checkPos) && - amountIterated < blocksToCheck.size()); - if (checkPos != null) { - checkFluidBlockAt(selfPos, checkPos); - } else break; + return new SearchResult(check, false); + } + } + + return null; + } + + /** + * Update the pump queue if it is empty. + * + * @param fluidType Use this if the pump queue must have the same fluid type because it was already decided in the + * pump cycle. + */ + private void updatePumpQueue(@Nullable FluidType fluidType) { + if (getLevel() == null) return; + + if (pumpQueue != null && !pumpQueue.queue().isEmpty()) { + return; + } + + BlockPos headPos = getPos().below(pumpHeadY); + + BlockPos downPos = headPos.below(1); + var downBlock = getLevel().getBlockState(downPos); + + if (!(downBlock.getBlock() instanceof LiquidBlock)) { + pumpQueue = null; + return; + } + + if (fluidType != null && downBlock.getFluidState().getFluidType() != fluidType) { + pumpQueue = null; + return; } - if (fluidSourceBlocks.isEmpty()) { - if (getOffsetTimer() % 20 == 0) { - BlockPos downPos = selfPos.below(1); - if (downPos.getY() >= getLevel().getMinBuildHeight()) { - var downBlock = getLevel().getBlockState(downPos); - if (downBlock.getBlock() instanceof LiquidBlock) { - this.pumpHeadY++; - if (getLevel() instanceof ServerLevel serverLevel && - serverLevel.getBlockState(selfPos).isAir()) { - serverLevel.setBlockAndUpdate(selfPos, GTBlocks.MINER_PIPE.getDefaultState()); + pumpQueue = buildPumpQueue(getLevel(), headPos, downBlock.getFluidState().getFluidType(), queueSize(), true); + } + + /** + * Does a "depth-first"-ish search to find a path to a source. It prioritizes going up and away from the pump head. + * If the path it finds only contains sources at the level below the pump head, it will keep looking until it finds + * one that has a source at a higher location. If it cannot find one, it will return the original path. + */ + private PumpQueue buildPumpQueue(Level level, BlockPos headPos, FluidType fluidType, int queueSourceAmount, + boolean upSources) { + Set checked = new ObjectOpenHashSet<>(); + + BlockPos headPosBelow = headPos.below(); + + checked.add(headPos); + checked.add(headPosBelow); + + int maxPumpRange = getMaxPumpRadius(getTier()); + + List pathStack = new ArrayList<>(); + + Deque nonSources = new ArrayDeque<>(); + Deque pathToLastSource = new ArrayDeque<>(); + Deque sourceStack = new ArrayDeque<>(); + + pathStack.add(headPosBelow); + nonSources.add(headPosBelow); + + int iterations = 0; + int previousSources = 0; + Queue> paths = new ArrayDeque<>(); + List sources = new ArrayList<>(); + // We do at most 1000 iterations to try and find source blocks + while (!pathStack.isEmpty() && iterations < 1000) { + // Peeks at the tail + BlockPos searchHead = pathStack.get(pathStack.size() - 1); + + SearchResult next = searchNext(level, headPosBelow, searchHead, fluidType, maxPumpRange, upSources, + checked); + + iterations++; + + if (next == null) { + boolean continueSearch = sources.size() < queueSourceAmount; + + int addedSources = sources.size() - previousSources; + previousSources = sources.size(); + if (addedSources > 0) { + var toAdd = new ArrayDeque<>(pathToLastSource); + // This is always the headPosBelow, which we do not want to include + toAdd.removeFirst(); + paths.add(toAdd); + } + + if (!continueSearch) { + return new PumpQueue(paths, fluidType); + } + + // Now we need to rewind our stack + BlockPos last = pathStack.remove(pathStack.size() - 1); + BlockPos lastSource = sourceStack.peekLast(); + if (last.equals(lastSource)) { + BlockPos prevSource = sourceStack.removeLast(); + // Rebuild nonSources until previous source + for (int i = pathStack.size() - 1; i >= 0; i--) { + BlockPos p = pathStack.get(i); + if (!p.equals(prevSource)) { + nonSources.addFirst(p); + } else { + break; } } + // If the last is a source, then nonSources will be empty regardless + } else if (!nonSources.isEmpty()) { + nonSources.removeLast(); + } + } else { + // Add the next + pathStack.add(next.pos()); + // If we are in search up mode, we only count it as a source if it's up + if (next.isSource() && (!upSources || next.pos().getY() > headPosBelow.getY())) { + sources.add(next.pos()); + // Found a source, so add all the non-source blocks we passed since the last one + pathToLastSource.addAll(nonSources); + // Also add the source itself + pathToLastSource.add(next.pos()); + // Reset non-sources because we just added them and found a source + nonSources.clear(); + sources.add(next.pos()); + } else { + // Not a source, but we want to track it + nonSources.add(next.pos()); } - // schedule queue rebuild because we changed our position and no fluid is available - this.initializedQueue = false; } + } + if (upSources) { + // If we found none, we try again without the restriction + if (paths.isEmpty()) { + return buildPumpQueue(level, headPos, fluidType, queueSourceAmount, false); + } + + return new PumpQueue(paths, fluidType); + } - if (!initializedQueue || getOffsetTimer() % 6000 == 0) { - this.initializedQueue = true; - // just add ourselves to check list and see how this will go - this.blocksToCheck.add(selfPos); + // Only after everything except the block directly below the pipe is pumped, do we want to pump it + // Otherwise we might advance the pump head prematurely + if (paths.isEmpty() && level.getBlockState(headPosBelow).getFluidState().isSource()) { + return new PumpQueue(new ArrayDeque<>(List.of(new ArrayDeque<>(List.of(headPosBelow)))), fluidType); + } + + return new PumpQueue(paths, fluidType); + } + + /** + * Advances the pump head if the block below is air and the pump queue is empty. + */ + private boolean canAdvancePumpHead() { + // position of the pump head, i.e. the position of the lowest mining pipe + BlockPos headPos = getPos().below(pumpHeadY); + + if (pumpQueue == null || pumpQueue.queue.isEmpty()) { + Level level; + if ((level = getLevel()) != null) { + BlockPos downPos = headPos.below(1); + var downBlock = level.getBlockState(downPos); + + if (downBlock.isAir()) { + this.pumpHeadY++; + + if (level instanceof ServerLevel serverLevel) { + serverLevel.setBlockAndUpdate(downPos, GTBlocks.MINER_PIPE.getDefaultState()); + } + return true; + } } } + return false; } @Override @@ -194,50 +426,95 @@ public void onMachineRemoved() { } } - private void checkFluidBlockAt(BlockPos pumpHeadPos, BlockPos checkPos) { - var blockHere = getLevel().getBlockState(checkPos); - boolean shouldCheckNeighbours = isStraightInPumpRange(checkPos); + protected record SourceState(BlockState state, BlockPos pos) {} - if (blockHere.getBlock() instanceof LiquidBlock liquidBlock && - liquidBlock.getFluidState(blockHere).isSource()) { - var fluidHandler = new FluidBlockTransfer(liquidBlock, getLevel(), checkPos); - FluidStack drainStack = fluidHandler.drain(Integer.MAX_VALUE, true); - if (!drainStack.isEmpty()) { - this.fluidSourceBlocks.add(checkPos); - } - shouldCheckNeighbours = true; + /** + * Does a full pump cycle, trying to do the required number of pumps. It will rebuild the queue if it becomes + * empty without having fulfilled its required number of pumps. All paths computed in the queue are checked + * if they are still valid and consist only of the right fluid. + */ + private void pumpCycle() { + Level level; + if ((level = getLevel()) == null) { + return; } + // Will only update if the queue is empty + updatePumpQueue(null); + int pumps = pumpsPerCycle(); + + // We try to pump `pumps` amount of source blocks, using multiple paths if necessary + boolean pumped = false; + int iterations = 0; + // We keep looking at paths as long as we still have pumps to go + // We put the iterations at max 10 just to be sure + while (pumps > 0 && pumpQueue != null && !pumpQueue.queue().isEmpty() && iterations < 10) { + iterations++; + + Deque pumpPath = pumpQueue.queue().peek(); + Deque states = new ArrayDeque<>(); + + // We iterate through the positions to check if it is still a valid path, saving the states + for (BlockPos pos : pumpPath) { + // Stop once an unloaded block is found + if (!level.isLoaded(pos)) { + break; + } + BlockState state = level.getBlockState(pos); + if (state.getBlock() instanceof LiquidBlock liquidBlock && + (liquidBlock.getFluidState(state)).getFluidType() == pumpQueue.fluidType()) { + states.add(new SourceState(state, pos)); + } else { + break; + } + } - if (shouldCheckNeighbours) { - int maxPumpRange = getMaxPumpRange(); - for (var facing : GTUtil.DIRECTIONS) { - BlockPos offsetPos = checkPos.relative(facing); - if (offsetPos.distSqr(pumpHeadPos) > maxPumpRange * maxPumpRange) - continue; // do not add blocks outside bounds - if (!fluidSourceBlocks.contains(offsetPos) && - !blocksToCheck.contains(offsetPos)) { - this.blocksToCheck.add(offsetPos); + // We remove from the end until we find a matching state, everything after must be no longer valid + while (pumps > 0 && !pumpPath.isEmpty()) { + BlockPos pos = pumpPath.removeLast(); + SourceState sourceState = states.peekLast(); + if (sourceState != null && pos.equals(sourceState.pos())) { + states.removeLast(); + FluidState fluidState = sourceState.state().getFluidState(); + if (sourceState.state().getBlock() instanceof LiquidBlock liquidBlock && fluidState.isSource()) { + var fluidHandler = new FluidBlockTransfer(liquidBlock, getLevel(), pos); + FluidStack drainStack = fluidHandler.drain(Integer.MAX_VALUE, true); + if (!drainStack.isEmpty() && cache.fillInternal(drainStack, true) == drainStack.getAmount()) { + cache.fillInternal(drainStack, false); + fluidHandler.drain(drainStack, false); + getLevel().setBlockAndUpdate(pos, Blocks.AIR.defaultBlockState()); + pumped = true; + pumps--; + } else if (!drainStack.isEmpty()) { + // In this case we just couldn't fill the internal tank, it's most likely full + // So we add back to the pump path and return + pumpPath.add(pos); + return; + } else { + // drain stack is empty even though it's a fluid source, probably something went wrong + // ignore block for a while + forbiddenBlocks.add(pos); + return; + } + } } } - } - } - private void tryPumpFirstBlock() { - BlockPos fluidBlockPos = fluidSourceBlocks.poll(); - if (fluidBlockPos == null) return; - var blockHere = getLevel().getBlockState(fluidBlockPos); - if (blockHere.getBlock() instanceof LiquidBlock liquidBlock && - liquidBlock.getFluidState(blockHere).isSource()) { - var fluidHandler = new FluidBlockTransfer(liquidBlock, getLevel(), fluidBlockPos); - FluidStack drainStack = fluidHandler.drain(Integer.MAX_VALUE, true); - if (!drainStack.isEmpty() && cache.fillInternal(drainStack, true) == drainStack.getAmount()) { - cache.fillInternal(drainStack, false); - fluidHandler.drain(drainStack, false); - getLevel().setBlockAndUpdate(fluidBlockPos, Blocks.AIR.defaultBlockState()); - this.fluidSourceBlocks.remove(fluidBlockPos); - energyContainer.changeEnergy(-GTValues.V[getTier()] * 2); + if (pumpPath.isEmpty()) { + pumpQueue.queue().remove(); + } + + // If we have pumps left over and there is still more to be pumped at the current level + // (But it wasn't in the queue because maybe it's the final source block below the pump head) + // We still want to be able to pump + if (pumps > 0 && pumpQueue.queue().isEmpty()) { + updatePumpQueue(pumpQueue.fluidType()); } } + + // Use energy if any pumps happened at all + if (pumped) { + energyContainer.changeEnergy(-GTValues.V[getTier()] * 2); + } } public void update() { @@ -249,14 +526,44 @@ public void update() { if (energyContainer.getEnergyStored() < GTValues.V[getTier()] * 2) { return; } - updateQueueState(getTier()); - if (getOffsetTimer() % getPumpingCycleLength() == 0 && !fluidSourceBlocks.isEmpty()) { - tryPumpFirstBlock(); + // Try to put 5 times as many in the queue as there are pumps in the cycle + // In practice only EV tier has more than 1 pump per cycle + // The queue can contain at most the y-levels at the pump head or just the y-level below, so for many oil veins + // It will not be the ideal size + boolean advanced = false; + if (getOffsetTimer() % (getPumpingCycleLength() * 2L) == 0) { + advanced = canAdvancePumpHead(); + } + if (!advanced && getOffsetTimer() % getPumpingCycleLength() == 0) { + pumpCycle(); + } + if (getOffsetTimer() % (20 * 60) == 0) { + forbiddenBlocks.clear(); } } + private int queueSize() { + return 5 * pumpsPerCycle(); + } + + private float ticksPerPump() { + // How many ticks pass per pump. This is the ideal amount and thus can be less than 1 + // For LV this is 80/1 = 80 + float tierMultiplier = (float) (1 << (getTier() - 1)); + return PUMP_SPEED_BASE / tierMultiplier; + } + + private int pumpsPerCycle() { + // The pumping cycle length can not be less than 20, so to ensure we still have the right amount of pumps + // We need to compensate with pumps per cycle + + return (int) (getPumpingCycleLength() / ticksPerPump()); + } + private int getPumpingCycleLength() { - return PUMP_SPEED_BASE / (1 << (getTier() - 1)); + // For basic pumps this means once every 80 ticks + // It never pumps more than once every 20 ticks, but pumps more per cycle to compensate + return Math.max(20, (int) ticksPerPump()); } //////////////////////////////////////