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

import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.block.entity.BlockEntityType;
import net.minecraft.nbt.*;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.util.Pair;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.PersistentState;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.Oritech;
import rearth.oritech.block.blocks.pipes.AbstractPipeBlock;
import rearth.oritech.block.blocks.pipes.GenericPipeBlock;
import rearth.oritech.block.entity.interaction.PipeBoosterBlockEntity;

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

import static rearth.oritech.block.blocks.pipes.GenericPipeBlock.NO_CONNECTION;

public abstract class GenericPipeInterfaceEntity extends BlockEntity implements BlockEntityTicker<GenericPipeInterfaceEntity> {
    
    public static final int MAX_SEARCH_COUNT = 2048;
    
    public BlockPos connectedBooster = BlockPos.ORIGIN;

    private PipeBoosterBlockEntity cachedBooster;

    public GenericPipeInterfaceEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
        super(type, pos, state);
    }

	public boolean isBoostAvailable() {
		var booster = tryGetCachedBooster();
		return booster != null && booster.canUseBoost();
	}

	public void onBoostUsed() {
		var booster = tryGetCachedBooster();
		if (booster != null) booster.useBoost();
	}

	@Nullable
	private PipeBoosterBlockEntity tryGetCachedBooster() {

		// booster was removed
		if (cachedBooster != null && cachedBooster.isRemoved()) {
			cachedBooster = null;
			connectedBooster = BlockPos.ORIGIN;
			return null;
		}

		if (connectedBooster == BlockPos.ORIGIN) {  // no booster set
			if (cachedBooster != null) cachedBooster = null;
			return null;
		} else if (cachedBooster == null) { // booster freshly set
			var candidate = Objects.requireNonNull(world).getBlockEntity(connectedBooster);
			if (candidate instanceof PipeBoosterBlockEntity booster) {
				cachedBooster = booster;
				return cachedBooster;
			} else {
				connectedBooster = BlockPos.ORIGIN;
				return null;
			}
		} else {    // no change
			return cachedBooster;
		}
	}

    public static void addNode(World world, BlockPos pos, boolean isInterface, BlockState newState, PipeNetworkData data) {
        Oritech.LOGGER.debug("registering/updating node: " + pos);

        data.pipes.add(pos);
        var connectedMachines = new HashSet<BlockPos>(6);
        var block = (AbstractPipeBlock) newState.getBlock();
        for (var neighbor : Direction.values()) {
            var neighborPos = pos.offset(neighbor);
            var neighborMap = data.machinePipeNeighbors.getOrDefault(neighborPos, new HashSet<>());
            if (block.hasMachineInDirection(neighbor, world, pos, block.apiValidationFunction())) {
                if (block.isConnectingInDirection(newState, neighbor, pos, world, false))
                    connectedMachines.add(pos.offset(neighbor));

                neighborMap.add(neighbor.getOpposite());
            } else {
                neighborMap.remove(neighbor.getOpposite());
            }

            if (!neighborMap.isEmpty()) data.machinePipeNeighbors.put(neighborPos, neighborMap);
            else data.machinePipeNeighbors.remove(neighborPos);
        }

        if (isInterface) {
            data.machineInterfaces.put(pos, connectedMachines);
        } else {
            data.machineInterfaces.remove(pos);
        }

        updateFromNode(world, pos, data);
    }

    public static void removeNode(World world, BlockPos pos, boolean wasInterface, BlockState oldState, PipeNetworkData data) {
        Oritech.LOGGER.debug("removing node: " + pos + " | " + wasInterface);

        var oldNetwork = data.pipeNetworkLinks.getOrDefault(pos, -1);

        data.pipes.remove(pos);
        if (wasInterface) data.machineInterfaces.remove(pos);

        removeStaleMachinePipeNeighbors(pos, data);

        data.pipeNetworks.remove(oldNetwork);
        data.pipeNetworkInterfaces.remove(oldNetwork);
        data.pipeNetworkLinks.remove(pos);

        // re-calculate old network, is either shorter or split into multiple ones (starting from ones this block was connected to)
        if (oldNetwork != -1) {
            var block = oldState.getBlock();
            for (var direction : Direction.values()) {
                if (block instanceof GenericPipeBlock pipeBlock && oldState.get(pipeBlock.directionToProperty(direction)) == NO_CONNECTION) {
                    continue;
                }

                updateFromNode(world, pos.offset(direction), data);
            }
        }

        data.markDirty();
    }

    private static void updateFromNode(World world, BlockPos pos, PipeNetworkData data) {

        var searchInstance = new FloodFillSearch(pos, data.pipes, world);
        var foundNetwork = new HashSet<>(searchInstance.complete());
        var foundMachines = findConnectedMachines(foundNetwork, data);

        Oritech.LOGGER.debug("Nodes:    " + foundNetwork.size() + " | " + foundNetwork);
        Oritech.LOGGER.debug("Machines: " + foundMachines.size() + " | " + foundMachines.stream().map(elem -> elem.getLeft() + ":" + elem.getRight()).toList());

        var netID = foundNetwork.hashCode();
        data.pipeNetworks.put(netID, foundNetwork);
        data.pipeNetworkInterfaces.put(netID, foundMachines);

        // these networks will be replaced, since these nodes now belong to the new network
        var networksToRemove = new HashSet<Integer>();

        for (var node : foundNetwork) {
            networksToRemove.add(data.pipeNetworkLinks.getOrDefault(node, -1));
            data.pipeNetworkLinks.put(node, netID);
        }

        networksToRemove.stream().filter(i -> i != -1 && i != netID).forEach(i -> {
            data.pipeNetworks.remove(i);
            data.pipeNetworkInterfaces.remove(i);
        });

        data.markDirty();
    }

    private static Set<Pair<BlockPos, Direction>> findConnectedMachines(Set<BlockPos> network, PipeNetworkData data) {

        var res = new HashSet<Pair<BlockPos, Direction>>();

        for (var node : network) {
            if (data.machineInterfaces.containsKey(node)) {
                for (var machinePos : data.machineInterfaces.get(node)) {
                    var offset = machinePos.subtract(node);
                    var direction = Direction.fromVector(offset.getX(), offset.getY(), offset.getZ()).getOpposite();
                    res.add(new Pair<>(machinePos, direction));
                }
            }
        }

        return res;
    }

    public static Set<Pair<BlockPos, Direction>> findNetworkTargets(BlockPos from, PipeNetworkData data) {
        var connectedNetwork = data.pipeNetworkLinks.getOrDefault(from, -1);
        if (connectedNetwork == -1) return new HashSet<>();

        return data.pipeNetworkInterfaces.get(connectedNetwork);
    }

    /**
     * Removes any stale machine -> neighboring pipes mappings
     * Used when a pipe node is destroyed
     *
     * @param pos  position of the destroyed node
     * @param data network data
     */
    public static void removeStaleMachinePipeNeighbors(BlockPos pos, PipeNetworkData data) {
        for (var neighbor : Direction.values()) {
            var machine = pos.offset(neighbor);
            var machineNeighbors = data.machinePipeNeighbors.get(machine);
            if (machineNeighbors == null) continue;

            machineNeighbors.remove(Direction.getFacing(Vec3d.of(pos.subtract(machine))));
            if (machineNeighbors.isEmpty())
                data.machinePipeNeighbors.remove(machine);
            else
                data.machinePipeNeighbors.put(machine, machineNeighbors);
        }
    }

    private static class FloodFillSearch {

        final HashSet<BlockPos> checkedPositions = new HashSet<>();
        final HashSet<BlockPos> nextTargets = new HashSet<>();
        final Deque<BlockPos> foundTargets = new ArrayDeque<>();
        final HashSet<BlockPos> pipes;
        final World world;

        public FloodFillSearch(BlockPos startPosition, HashSet<BlockPos> pipes, World world) {
            this.pipes = pipes;
            this.world = world;
            nextTargets.add(startPosition);
        }

        public Deque<BlockPos> complete() {
            var active = true;
            while (active) {
                active = !nextGeneration();
            }

            return foundTargets;
        }

        // returns true when done
        @SuppressWarnings("unchecked")
        public boolean nextGeneration() {

            var currentGeneration = (HashSet<BlockPos>) nextTargets.clone();

            for (var target : currentGeneration) {
                if (isValidTarget(target)) {
                    foundTargets.addLast(target);
                    addNeighborsToQueue(target);
                }

                checkedPositions.add(target);
                nextTargets.remove(target);
            }

            if (cutoffSearch()) nextTargets.clear();

            return nextTargets.isEmpty();
        }

        private boolean cutoffSearch() {
            return foundTargets.size() >= MAX_SEARCH_COUNT;
        }

        private boolean isValidTarget(BlockPos target) {
            return pipes.contains(target);
        }

        private void addNeighborsToQueue(BlockPos self) {
            var targetState = world.getBlockState(self);

            if (!(targetState.getBlock() instanceof AbstractPipeBlock targetBlock)) return;
            for (var direction : Direction.values()) {
                var neighbor = self.offset(direction);
                if (checkedPositions.contains(neighbor)) continue;
                if (!isValidTarget(neighbor)) {
                    checkedPositions.add(neighbor);
                    continue;
                }

                // check if the target can connect to the neighbor
                if (!targetBlock.isConnectingInDirection(targetState, direction, self, world, false)) continue;

                nextTargets.add(neighbor);
            }
        }
    }

    public static final class PipeNetworkData extends PersistentState {
        public final HashMap<BlockPos, Integer> pipeNetworkLinks = new HashMap<>(); // which blockpos belongs to which network (ID)
        public final HashSet<BlockPos> pipes = new HashSet<>();
        public final HashMap<BlockPos, Set<BlockPos>> machineInterfaces = new HashMap<>(); // list of machines per interface/connection block
        public final HashMap<Integer, Set<BlockPos>> pipeNetworks = new HashMap<>();   // networks are never updated, and instead always replaced by new ones with different ids
        public final HashMap<Integer, Set<Pair<BlockPos, Direction>>> pipeNetworkInterfaces = new HashMap<>(); // list of machines that are connected to the network

        public final HashMap<BlockPos, Set<Direction>> machinePipeNeighbors = new HashMap<>(); // List of neighboring pipes per machine, and the direction they are in. Missing direction means no connection

        @Override
        public int hashCode() {
            int result = pipeNetworkLinks.hashCode();
            result = 31 * result + pipes.hashCode();
            result = 31 * result + machineInterfaces.hashCode();
            result = 31 * result + pipeNetworks.hashCode();
            result = 31 * result + pipeNetworkInterfaces.hashCode();
            return result;
        }

        public static Type<PipeNetworkData> TYPE = new Type<>(PipeNetworkData::new, PipeNetworkData::fromNbt, null);

        public static PipeNetworkData fromNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {

            var result = new PipeNetworkData();

            if (nbt.contains("pipeNetworkLinks", NbtElement.LIST_TYPE)) {
                var pipeNetworkLinksList = nbt.getList("pipeNetworkLinks", NbtElement.COMPOUND_TYPE);
                for (var element : pipeNetworkLinksList) {
                    var entry = (NbtCompound) element;
                    var pos = BlockPos.fromLong(entry.getLong("pos"));
                    var id = entry.getInt("id");
                    result.pipeNetworkLinks.put(pos, id);
                }
            }

            // Deserialize pipes
            if (nbt.contains("pipes", NbtElement.LIST_TYPE)) {
                var pipesList = nbt.getList("pipes", NbtElement.LONG_TYPE);
                pipesList.stream().map(element -> BlockPos.fromLong(((NbtLong) element).longValue())).forEach(result.pipes::add);
            }

            // Deserialize machineInterfaces
            if (nbt.contains("machineInterfaces", NbtElement.COMPOUND_TYPE)) {
                var machineInterfacesNbt = nbt.getCompound("machineInterfaces");
                for (var key : machineInterfacesNbt.getKeys()) {
                    var interfacePos = BlockPos.fromLong(Long.parseLong(key));
                    var machinesArray = machineInterfacesNbt.getLongArray(key);
                    var machines = Arrays.stream(machinesArray)
                                     .mapToObj(BlockPos::fromLong)
                                     .collect(Collectors.toSet());
                    result.machineInterfaces.put(interfacePos, machines);
                }
            }

            // Deserialize pipeNetworks
            if (nbt.contains("pipeNetworks", NbtElement.COMPOUND_TYPE)) {
                var pipeNetworksNbt = nbt.getCompound("pipeNetworks");
                for (var key : pipeNetworksNbt.getKeys()) {
                    var id = Integer.parseInt(key);
                    var networkArray = pipeNetworksNbt.getLongArray(key);
                    var network = Arrays.stream(networkArray)
                                    .mapToObj(BlockPos::fromLong)
                                    .collect(Collectors.toSet());
                    result.pipeNetworks.put(id, network);
                }
            }

            // Deserialize pipeNetworkInterfaces
            if (nbt.contains("pipeNetworkInterfaces", NbtElement.COMPOUND_TYPE)) {
                var pipeNetworkInterfacesNbt = nbt.getCompound("pipeNetworkInterfaces");
                for (var key : pipeNetworkInterfacesNbt.getKeys()) {
                    var id = Integer.parseInt(key);
                    var interfacesList = pipeNetworkInterfacesNbt.getList(key, NbtElement.COMPOUND_TYPE);
                    var interfaces = new HashSet<Pair<BlockPos, Direction>>();
                    for (var interfaceElement : interfacesList) {
                        var pairNbt = (NbtCompound) interfaceElement;
                        var pos = BlockPos.fromLong(pairNbt.getLong("pos"));
                        var direction = Direction.byName(pairNbt.getString("direction"));
                        interfaces.add(new Pair<>(pos, direction));
                    }
                    result.pipeNetworkInterfaces.put(id, interfaces);
                }
            }

            // Deserialize machinePipeNeighbors
            if (nbt.contains("machinePipeNeighbors", NbtElement.COMPOUND_TYPE)) {
                var connectionPipeNeighborsNbt = nbt.getCompound("machinePipeNeighbors");
                for (var key : connectionPipeNeighborsNbt.getKeys()) {
                    var pos = BlockPos.fromLong(Long.parseLong(key));
                    var neighborsList = connectionPipeNeighborsNbt.getList(key, NbtElement.STRING_TYPE);
                    var neighbors = new HashSet<Direction>();
                    for (var neighborElement : neighborsList) {
                        var direction = Direction.byName(neighborElement.asString());
                        neighbors.add(direction);
                    }
                    result.machinePipeNeighbors.put(pos, neighbors);
                }
            }

            result.markDirty();

            return result;
        }

        @Override
        public NbtCompound writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {

            // Serialize pipeNetworkLinks
            var pipeNetworkLinksList = new NbtList();
            pipeNetworkLinks.forEach((pos, id) -> {
                var entry = new NbtCompound();
                entry.putLong("pos", pos.asLong());
                entry.putInt("id", id);
                pipeNetworkLinksList.add(entry);
            });
            nbt.put("pipeNetworkLinks", pipeNetworkLinksList);

            // Serialize pipes
            var pipesList = new NbtList();
            pipes.forEach(pos -> pipesList.add(NbtLong.of(pos.asLong())));
            nbt.put("pipes", pipesList);

            // Serialize machineInterfaces
            var machineInterfacesNbt = new NbtCompound();
            machineInterfaces.forEach((interfacePos, machines) -> {
                machineInterfacesNbt.putLongArray(Long.toString(interfacePos.asLong()), machines.stream().map(BlockPos::asLong).collect(Collectors.toList()));
            });
            nbt.put("machineInterfaces", machineInterfacesNbt);

            // Serialize pipeNetworks
            var pipeNetworksNbt = new NbtCompound();
            pipeNetworks.forEach((id, network) -> {
                pipeNetworksNbt.putLongArray(id.toString(), network.stream().map(BlockPos::asLong).collect(Collectors.toList()));
            });
            nbt.put("pipeNetworks", pipeNetworksNbt);

            // Serialize pipeNetworkInterfaces
            var pipeNetworkInterfacesNbt = new NbtCompound();
            pipeNetworkInterfaces.forEach((id, interfaces) -> {
                var interfacesList = new NbtList();
                interfaces.forEach(pair -> {
                    var pairNbt = new NbtCompound();
                    pairNbt.putLong("pos", pair.getLeft().asLong());
                    pairNbt.putString("direction", pair.getRight().getName());
                    interfacesList.add(pairNbt);
                });
                pipeNetworkInterfacesNbt.put(id.toString(), interfacesList);
            });
            nbt.put("pipeNetworkInterfaces", pipeNetworkInterfacesNbt);

            // Serialize machinePipeNeighbors
            var connectionPipeNeighborsNbt = new NbtCompound();
            machinePipeNeighbors.forEach((pos, neighbors) -> {
                var neighborsList = new NbtList();
                neighbors.forEach(direction -> {
                    var nbtElement = NbtString.of(direction.getName());
                    neighborsList.add(nbtElement);
                });
                connectionPipeNeighborsNbt.put(Long.toString(pos.asLong()), neighborsList);
            });
            nbt.put("machinePipeNeighbors", connectionPipeNeighborsNbt);

            return nbt;
        }
    }

}