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

import net.minecraft.block.BlockState;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.world.World;
import org.apache.commons.lang3.time.StopWatch;
import rearth.oritech.Oritech;
import rearth.oritech.api.item.ItemApi;
import rearth.oritech.block.blocks.pipes.AbstractPipeBlock;
import rearth.oritech.block.blocks.pipes.ExtractablePipeConnectionBlock;
import rearth.oritech.block.blocks.pipes.item.ItemPipeBlock;
import rearth.oritech.block.blocks.pipes.item.ItemPipeConnectionBlock;
import rearth.oritech.init.BlockContent;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.network.NetworkContent;

import java.util.*;
import java.util.stream.IntStream;

public class ItemPipeInterfaceEntity extends ExtractablePipeInterfaceEntity {
    
    private static final int TRANSFER_AMOUNT = Oritech.CONFIG.itemPipeTransferAmount();
    private static final int TRANSFER_PERIOD = Oritech.CONFIG.itemPipeIntervalDuration();
    
    private List<Pair<ItemApi.InventoryStorage, BlockPos>> filteredTargetItemStorages;
    
    // item path cache (invalidated on network update)
    private final HashMap<BlockPos, Pair<ArrayList<BlockPos>, Integer>> cachedTransferPaths = new HashMap<>();
    private final boolean renderItems;
    
    private static final HashMap<BlockPos, Long> blockedUntil = new HashMap<>();   // used to fake item movement in transparent pipes
    
    // client only
    public Set<RenderStackData> activeStacks = new HashSet<>();
    
    public ItemPipeInterfaceEntity(BlockPos pos, BlockState state) {
        super(BlockEntitiesContent.ITEM_PIPE_ENTITY, pos, state);
        this.renderItems = state.getBlock().equals(BlockContent.TRANSPARENT_ITEM_PIPE_CONNECTION);
        
    }
    
    @SuppressWarnings("DataFlowIssue")
    @Override
    public void tick(World world, BlockPos pos, BlockState state, GenericPipeInterfaceEntity blockEntity) {
        var block = (ExtractablePipeConnectionBlock) state.getBlock();
        if (world.isClient || !block.isExtractable(state))
            return;
        
        // boosted pipe works every tick, otherwise only every N tick
        if ((world.getTime() + this.pos.asLong()) % TRANSFER_PERIOD != 0 && !isBoostAvailable())
            return;
        
        // find first itemstack from connected invs (that can be extracted)
        // try to move it to one of the destinations
        
        var data = ItemPipeBlock.ITEM_PIPE_DATA.getOrDefault(world.getRegistryKey().getValue(), new PipeNetworkData());
        
        var sources = data.machineInterfaces.getOrDefault(pos, new HashSet<>());
        var stackToMove = ItemStack.EMPTY;
        ItemApi.InventoryStorage moveFromInventory = null;
        BlockPos takenFrom = null;
        var moveCapacity = isBoostAvailable() ? 64 : TRANSFER_AMOUNT;
        
        for (var sourcePos : sources) {
            
            var blockedTimer = blockedUntil.getOrDefault(sourcePos, 0L);
            if (world.getTime() < blockedTimer) continue;
            
            if (blockedTimer > 0)   // if timer has expired but was set
                blockedUntil.remove(sourcePos);
            
            var offset = pos.subtract(sourcePos);
            var direction = Direction.fromVector(offset.getX(), offset.getY(), offset.getZ());
            if (!block.isSideExtractable(state, direction.getOpposite())) continue;
            var inventory = ItemApi.BLOCK.find(world, sourcePos, direction);
            if (inventory == null || !inventory.supportsExtraction()) continue;
            
            var firstStack = getFirstStack(inventory, moveCapacity);
            
            if (!firstStack.isEmpty()) {
                stackToMove = firstStack;
                moveFromInventory = inventory;
                takenFrom = sourcePos;
                break;
            }
            
        }
        
        if (stackToMove.isEmpty()) return;
        
        var targets = findNetworkTargets(pos, data);
        if (targets == null) {
            System.err.println("Yeah your pipe network likely is too long. At: " + this.getPos());
            return;
        }
        
        var netHash = targets.hashCode();
        
        if (netHash != filteredTargetsNetHash || filteredTargetItemStorages == null) {
            filteredTargetItemStorages = targets.stream()
                                           .filter(target -> {
                                               var direction = target.getRight();
                                               var pipePos = target.getLeft().add(direction.getVector());
                                               var pipeState = world.getBlockState(pipePos);
                                               if (!(pipeState.getBlock() instanceof ItemPipeConnectionBlock itemBlock))
                                                   return true;   // edge case, this should never happen
                                               var extracting = itemBlock.isSideExtractable(pipeState, direction.getOpposite());
                                               return !extracting;
                                           })
                                           .map(target -> new Pair<>(ItemApi.BLOCK.find(world, target.getLeft(), target.getRight()), target.getLeft()))
                                           .filter(obj -> Objects.nonNull(obj.getLeft()) && obj.getLeft().supportsInsertion())
                                           .sorted(Comparator.comparingInt(a -> a.getRight().getManhattanDistance(pos)))
                                           .toList();
            
            filteredTargetsNetHash = netHash;
            cachedTransferPaths.clear();
        }
        
        var toMove = stackToMove.getCount();
        var moved = 0;
        
        for (var storagePair : filteredTargetItemStorages) {
            if (storagePair.getLeft().equals(moveFromInventory)) continue;    // skip when targeting same machine
            
            var targetStorage = storagePair.getLeft();
            var wasEmptyStorage = IntStream.range(0, targetStorage.getSlotCount()).allMatch(slot -> targetStorage.getStackInSlot(slot).isEmpty());
            
            
            var inserted = targetStorage.insert(stackToMove, false);
            toMove -= inserted;
            moved += inserted;
            
            if (inserted > 0) {
                onItemMoved(this.pos, takenFrom, storagePair.getRight(), data.pipeNetworks.getOrDefault(data.pipeNetworkLinks.getOrDefault(this.pos, 0), new HashSet<>()), world, stackToMove.getItem(), inserted, wasEmptyStorage);
            }
            
            if (toMove <= 0) break;  // target has been found for all items
        }
        var extracted = moveFromInventory.extract(stackToMove.copyWithCount(moved), false);
        
        if (extracted != moved) {
            Oritech.LOGGER.warn("Invalid state while transferring inventory. Caused at position {}", pos);
        }
        
        if (moveCapacity > TRANSFER_AMOUNT) onBoostUsed();
        
    }
    
    private void onItemMoved(BlockPos startPos, BlockPos from, BlockPos to, Set<BlockPos> network, World world, Item moved, int movedCount, boolean wasEmpty) {
        if (!renderItems) return;
        var path = cachedTransferPaths.computeIfAbsent(to, ignored -> calculatePath(startPos, from, to, network, world));
        if (path == null) return;
        
        var codedPath = path.getLeft().stream().map(BlockPos::asLong).toList();
        var packet = new NetworkContent.ItemPipeVisualTransferPacket(startPos, codedPath, new ItemStack(moved, movedCount));
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(packet);
        
        if (wasEmpty) {
            var arrivalTime = world.getTime() + (int) calculatePathLength(path.getRight());
            blockedUntil.putIfAbsent(to, arrivalTime);
        }
        
    }
    
    public static double calculatePathLength(int pathBlocksCount) {
        return Math.pow(pathBlocksCount * 16, 0.6);
    }
    
    // return pair is optimized path and total path length
    private static Pair<ArrayList<BlockPos>, Integer> calculatePath(BlockPos startPos, BlockPos from, BlockPos to, Set<BlockPos> network, World world) {
        
        if (network.isEmpty() || !network.contains(startPos)) {
            Oritech.LOGGER.warn("tried to calculate invalid item pipe from: {} to {} with network size: {}", startPos, to, network.size());
            return null;
        }
        
        var length = 1;
        
        var path = new LinkedList<BlockPos>();
        path.add(startPos);
        
        var visited = new HashSet<BlockPos>();
        
        var watch = new StopWatch();
        watch.start();
        
        for (int i = 0; i < network.size() * 3; i++) {
            
            var currentPos = path.peekLast();
            
            if (currentPos == null || currentPos.getManhattanDistance(to) == 1) { // target reached (or invalid)
                break;
            }
            
            visited.add(currentPos);
            
            var currentPosState = world.getBlockState(currentPos);
            if (!(currentPosState.getBlock() instanceof AbstractPipeBlock pipeBlock)) break;
            
            // collect potential edges in graph, ordered by basic cost heuristic (manhattan dist to target)
            var openEdges = getNeighbors(currentPos).stream()
                              .filter(network::contains)
                              .filter(candidate -> !visited.contains(candidate))
                              .filter(candidate -> pipeBlock.isConnectingInDirection(currentPosState, getDirectionFromOffset(currentPos, candidate), currentPos, world, false))
                              .sorted(Comparator.comparingInt(a -> a.getManhattanDistance(to)))
                              .toArray(BlockPos[]::new);
            
            if (openEdges.length == 0) {    // dead end, go back
                path.pollLast();
            } else {
                path.add(openEdges[0]);
                length++;
            }
            
        }
        
        path.addFirst(from);
        path.add(to);
        
        // compact path (by removing straight segments)
        var result = optimizePath(path);
        
        watch.stop();
        
        Oritech.LOGGER.debug("pathsize: {} success: {} time ms: {}", result.size(), path.size() > 2, watch.getNanoTime() / 1_000_000f);
        return new Pair<>(result, path.size());
    }
    
    private static ArrayList<BlockPos> optimizePath(LinkedList<BlockPos> path) {
        var result = new ArrayList<BlockPos>();
        if (path.isEmpty()) {
            return result;
        }
        
        var iterator = path.iterator();
        var first = iterator.next();
        result.add(first);
        
        if (!iterator.hasNext()) {
            return result;
        }
        
        var current = iterator.next();
        var currentDirection = current.subtract(first);
        
        while (iterator.hasNext()) {
            var next = iterator.next();
            var nextDirection = next.subtract(current);
            
            if (!nextDirection.equals(currentDirection)) {
                result.add(current);
                currentDirection = nextDirection;
            }
            
            current = next;
        }
        
        result.add(current);
        return result;
    }
    
    // returns all neighboring positions except up
    private static List<BlockPos> getNeighbors(BlockPos pos) {
        return Arrays.asList(pos.down(), pos.up(), pos.north(), pos.east(), pos.south(), pos.west());
    }
    
    private static Direction getDirectionFromOffset(BlockPos self, BlockPos target) {
        var offset = target.subtract(self);
        return Direction.fromVector(offset.getX(), offset.getY(), offset.getZ());
    }
    
    public void handleVisualTransferPacket(NetworkContent.ItemPipeVisualTransferPacket packet) {
        var path = packet.codedStops().stream().map(BlockPos::fromLong).toList();
        var pathLength = 0;
        for (int i = 0; i < path.size() - 1; i++) {
            var pathPos = path.get(i);
            var nextPathPos = path.get(i + 1);
            pathLength += nextPathPos.getManhattanDistance(pathPos);
        }
        var data = new RenderStackData(packet.moved(), path, world.getTime(), pathLength);
        activeStacks.add(data);
    }
    
    @Override
    public void markDirty() {
        if (this.world != null)
            world.markDirty(pos);
    }
    
    private static ItemStack getFirstStack(ItemApi.InventoryStorage inventory, int maxTransferAmount) {
        
        for (int i = 0; i < inventory.getSlotCount(); i++) {
            var slotStack = inventory.getStackInSlot(i);
            if (slotStack.isEmpty()) continue;
            var extracted = inventory.extractFromSlot(slotStack.copyWithCount(maxTransferAmount), i, true);
            if (extracted > 0) return slotStack.copyWithCount(extracted);
        }
        
        return ItemStack.EMPTY;
    }
    
    public record RenderStackData(ItemStack rendered, List<BlockPos> path, Long startedAt, int pathLength) {
    }
}