RyanHub – file viewer
filename: common/src/main/java/rearth/oritech/block/entity/interaction/PumpBlockEntity.java
branch: 1.21
back to repo
package rearth.oritech.block.entity.interaction;

import dev.architectury.fluid.FluidStack;
import dev.architectury.hooks.fluid.FluidStackHooks;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.fluid.Fluid;
import net.minecraft.fluid.FluidState;
import net.minecraft.fluid.Fluids;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.registry.Registries;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.text.Text;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import rearth.oritech.Oritech;
import rearth.oritech.api.energy.EnergyApi;
import rearth.oritech.api.energy.containers.SimpleEnergyStorage;
import rearth.oritech.api.fluid.FluidApi;
import rearth.oritech.api.fluid.containers.SimpleFluidStorage;
import rearth.oritech.block.base.entity.MachineBlockEntity;
import rearth.oritech.init.BlockContent;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.init.FluidContent;
import rearth.oritech.network.NetworkContent;
import software.bernie.geckolib.animatable.GeoBlockEntity;
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.animation.AnimatableManager;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.util.GeckoLibUtil;

import java.util.*;
import java.util.stream.Collectors;

public class PumpBlockEntity extends BlockEntity implements BlockEntityTicker<PumpBlockEntity>, FluidApi.BlockProvider, EnergyApi.BlockProvider, GeoBlockEntity {
    
    private static final int MAX_SEARCH_COUNT = 100_000;
    private static final int ENERGY_USAGE = 512;   // per block pumped
    private static final int PUMP_RATE = 5; // pump every n ticks
    
    protected final AnimatableInstanceCache animatableInstanceCache = GeckoLibUtil.createInstanceCache(this);
    private final AnimationController<PumpBlockEntity> animationController = getAnimationController();
    
    // client only
    public Fluid lastPumpedVariant;
    public long lastPumpTime;
    
    private final SimpleFluidStorage fluidStorage = new SimpleFluidStorage(16 * FluidStackHooks.bucketAmount(), this::markDirty);
    
    private final SimpleEnergyStorage energyStorage = new SimpleEnergyStorage(1000, 0, 20_000);
    private boolean initialized = false;
    private boolean toolheadLowered = false;
    private boolean searchActive = false;
    private BlockPos toolheadPosition;
    private FloodFillSearch searchInstance;
    private Deque<BlockPos> pendingLiquidPositions;
    
    public PumpBlockEntity(BlockPos pos, BlockState state) {
        super(BlockEntitiesContent.PUMP_BLOCK, pos, state);
    }
    
    @Override
    protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.writeNbt(nbt, registryLookup);
        fluidStorage.writeNbt(nbt, "");
        nbt.putBoolean("initialized", initialized);
        nbt.putLong("energy", energyStorage.getAmount());
        
        if (pendingLiquidPositions != null)
            nbt.putLongArray("pendingTargets", pendingLiquidPositions.stream().mapToLong(BlockPos::asLong).toArray());
    }
    
    @Override
    protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.readNbt(nbt, registryLookup);
        initialized = nbt.getBoolean("initialized");
        fluidStorage.readNbt(nbt, "");
        energyStorage.setAmount(nbt.getLong("energy"));
        pendingLiquidPositions = Arrays.stream(nbt.getLongArray("pendingTargets")).mapToObj(BlockPos::fromLong).collect(Collectors.toCollection(ArrayDeque::new));
    }
    
    @Override
    public void tick(World world, BlockPos pos, BlockState state, PumpBlockEntity blockEntity) {
        if (world.isClient) {
            if (isBusy())
                spawnWorkingParticles();
            return;
        }
        
        if (!initialized) {
            progressStartup();
            return;
        }
        
        if (world.getTime() % PUMP_RATE == 0 && hasEnoughEnergy()) {
            
            if (pendingLiquidPositions.isEmpty() || tankIsFull()) return;
            
            var targetBlock = pendingLiquidPositions.peekLast();
            
            if (!world.getBlockState(targetBlock).getFluidState().isStill()) {
                pendingLiquidPositions.pollLast();
                return;
            }
            
            var targetState = world.getFluidState(targetBlock);
            if (!targetState.getFluid().matchesType(Fluids.WATER)) {
                drainSourceBlock(targetBlock);
                pendingLiquidPositions.pollLast();
            }
            
            addLiquidToTank(targetState);
            useEnergy();
            this.markDirty();
            setLastPumpTime(world.getTime());
            updateNetwork();
        }
        
    }
    
    public void onUsed(PlayerEntity player) {
        
        var message = Text.translatable("message.oritech.pump.starting");
        if (!initialized) {
            if (!toolheadLowered) {
                message = Text.translatable("message.oritech.pump.extending");
            } else if (searchActive) {
                message= Text.translatable("message.oritech.pump.initializing");
            } else {
                message = Text.translatable("message.oritech.pump.no_fluids");
            }
        } else if (isBusy()) {
            message = Text.translatable("message.oritech.pump.busy");
        } else if (!hasEnoughEnergy()) {
            message = Text.translatable("message.oritech.pump.low_energy");
        } else if (pendingLiquidPositions.isEmpty()) {
            message = Text.translatable("message.oritech.pump.pump_finished");
        } else if (tankIsFull()) {
            message = Text.translatable("message.oritech.pump.full");
        }
        
        player.sendMessage(message, true);
    }
    
    private void spawnWorkingParticles() {
        if (world.getTime() % 5 != 0) return;
        
        var targetPos = pos.toCenterPos();
        var targetType = ParticleTypes.BUBBLE_COLUMN_UP;
        if (lastPumpedVariant.equals(Fluids.LAVA)) targetType = ParticleTypes.LAVA;
        if (lastPumpedVariant.equals(FluidContent.STILL_OIL)) targetType = ParticleTypes.FALLING_OBSIDIAN_TEAR;
        
        world.addParticle(targetType, targetPos.getX(), targetPos.getY(), targetPos.getZ(), 0, 0.3, 0);
    }
    
    private boolean hasEnoughEnergy() {
        return energyStorage.getAmount() >= ENERGY_USAGE;
    }
    
    private void useEnergy() {
        energyStorage.extractIgnoringLimit(ENERGY_USAGE, false);
    }
    
    private boolean tankIsFull() {
        return fluidStorage.getAmount() > fluidStorage.getCapacity() - FluidStackHooks.bucketAmount();
    }
    
    private void addLiquidToTank(FluidState targetState) {
        fluidStorage.insert(FluidStack.create(targetState.getFluid(), FluidStackHooks.bucketAmount()), false);
    }
    
    private void drainSourceBlock(BlockPos targetBlock) {
        world.setBlockState(targetBlock, Blocks.AIR.getDefaultState());
    }
    
    private void progressStartup() {
        
        // startup sequence is:
        // move down until no longer in air
        // check if target is liquid
        // if liquid is water, consider as infinite
        // if liquid, start flood fill to find all liquid blocks. Add all found blocks to queue so that it can be soaked up in reverse
        // search all neighbors per tick
        // if more than N blocks are found, consider the search finished
        // mark startup as completed
        
        if (toolheadPosition == null) {
            toolheadPosition = pos;
        }
        
        if (!toolheadLowered) {
            
            if (world.getTime() % 10 != 0)
                moveToolheadDown();
            
            return;
        }
        
        if (searchActive) {
            if (searchInstance.nextGeneration()) {
                finishSearch();
                searchActive = false;
            }
        }
    }
    
    private void moveToolheadDown() {
        toolheadLowered = checkToolheadEnd(toolheadPosition);
        if (toolheadLowered) {
            startLiquidSearch(toolheadPosition.down());
            return;
        }
        
        toolheadPosition = toolheadPosition.down();
        world.setBlockState(toolheadPosition, BlockContent.PUMP_TRUNK_BLOCK.getDefaultState());
    }
    
    private boolean checkToolheadEnd(BlockPos newPosition) {
        
        var posBelow = newPosition.down();
        var stateBelow = world.getBlockState(posBelow);
        var blockBelow = stateBelow.getBlock();
        
        var isAirOrTrunk = stateBelow.isReplaceable() || blockBelow.equals(BlockContent.PUMP_TRUNK_BLOCK);
        var isStillFluid = stateBelow.getFluidState().isStill();
        
        return isStillFluid || !isAirOrTrunk;
    }
    
    private void startLiquidSearch(BlockPos start) {
        
        var state = world.getFluidState(start);
        if (!state.isStill()) return;
        
        searchInstance = new FloodFillSearch(start, world);
        searchActive = true;
        
        Oritech.LOGGER.debug("starting search at: " + start + " " + state.getFluid() + " " + state.isStill());
    }
    
    private void finishSearch() {
        Oritech.LOGGER.debug("search finished, found: " + searchInstance.foundTargets.size());
        pendingLiquidPositions = searchInstance.foundTargets;
        initialized = true;
        searchInstance = null;
    }
    
    @Override
    public FluidApi.FluidStorage getFluidStorage(Direction direction) {
        return fluidStorage;
    }
    
    @Override
    public EnergyApi.EnergyStorage getEnergyStorage(Direction direction) {
        return energyStorage;
    }
    
    @Override
    public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
        controllers.add(animationController);
    }
    
    private void updateNetwork() {
        var fluid = Registries.FLUID.getId(fluidStorage.getFluid()).toString();
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.PumpWorkSyncPacket(pos, fluid, world.getTime()));
    }
    
    @Override
    public AnimatableInstanceCache getAnimatableInstanceCache() {
        return animatableInstanceCache;
    }
    
    private boolean isBusy() {
        return world.getTime() - lastPumpTime < 40;
    }
    
    private AnimationController<PumpBlockEntity> getAnimationController() {
        return new AnimationController<>(this, state -> {
            if (isBusy()) return state.setAndContinue(MachineBlockEntity.WORKING);
            return PlayState.STOP;
        });
    }
    
    public void setLastPumpedVariant(Fluid lastPumpedVariant) {
        this.lastPumpedVariant = lastPumpedVariant;
    }
    
    public void setLastPumpTime(long lastPumpTime) {
        this.lastPumpTime = lastPumpTime;
    }
    
    private static class FloodFillSearch {
        
        final HashSet<BlockPos> checkedPositions = new HashSet<>();
        final HashSet<BlockPos> nextTargets = new HashSet<>();
        final Deque<BlockPos> foundTargets = new ArrayDeque<>();
        final World world;
        
        public FloodFillSearch(BlockPos startPosition, World world) {
            this.world = world;
            nextTargets.add(startPosition);
        }
        
        // returns true when done
        @SuppressWarnings("unchecked")
        public boolean nextGeneration() {
            
            var currentGeneration = (HashSet<BlockPos>) nextTargets.clone();
            
            var earlyStop = false;
            
            for (var target : currentGeneration) {
                if (isValidTarget(target)) {
                    foundTargets.addLast(target);
                    addNeighborsToQueue(target);
                    if (checkForEarlyStop(target)) earlyStop = true;
                }
                
                checkedPositions.add(target);
                nextTargets.remove(target);
            }
            
            if (cutoffSearch() || earlyStop) nextTargets.clear();
            
            return nextTargets.isEmpty();
        }
        
        private boolean checkForEarlyStop(BlockPos target) {
            return world.getFluidState(target).getFluid().matchesType(Fluids.WATER);
        }
        
        private boolean cutoffSearch() {
            return foundTargets.size() >= MAX_SEARCH_COUNT;
        }
        
        private boolean isValidTarget(BlockPos target) {
            var state = world.getFluidState(target);
            return state.isStill();
        }
        
        private void addNeighborsToQueue(BlockPos self) {
            
            for (var neighbor : getNeighbors(self)) {
                if (checkedPositions.contains(neighbor)) continue;
                nextTargets.add(neighbor);
            }
            
        }
        
        // returns all neighboring positions except up
        private List<BlockPos> getNeighbors(BlockPos pos) {
            return List.of(pos.down(), pos.north(), pos.east(), pos.south(), pos.west());
        }
        
    }
}