/* * This file is part of ComputerCraft - http://www.computercraft.info * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. * Send enquiries to dratcliffe@gmail.com */ package dan200.computercraft.shared.wired; import com.google.common.collect.ImmutableMap; import dan200.computercraft.api.network.Packet; import dan200.computercraft.api.network.wired.IWiredNetwork; import dan200.computercraft.api.network.wired.IWiredNode; import dan200.computercraft.api.peripheral.IPeripheral; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import javax.annotation.Nonnull; import java.util.*; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public final class WiredNetwork implements IWiredNetwork { final ReadWriteLock lock = new ReentrantReadWriteLock(); HashSet nodes; private HashMap peripherals = new HashMap<>(); WiredNetwork( WiredNode node ) { nodes = new HashSet<>( 1 ); nodes.add( node ); } private WiredNetwork( HashSet nodes ) { this.nodes = nodes; } static void transmitPacket( WiredNode start, Packet packet, double range, boolean interdimensional ) { Map points = new HashMap<>(); TreeSet transmitTo = new TreeSet<>(); { TransmitPoint startEntry = start.element.getWorld() != packet.getSender() .getWorld() ? new TransmitPoint( start, Double.POSITIVE_INFINITY, true ) : new TransmitPoint( start, start.element.getPosition() .distanceTo( packet.getSender() .getPosition() ), false ); points.put( start, startEntry ); transmitTo.add( startEntry ); } { TransmitPoint point; while( (point = transmitTo.pollFirst()) != null ) { World world = point.node.element.getWorld(); Vec3d position = point.node.element.getPosition(); for( WiredNode neighbour : point.node.neighbours ) { TransmitPoint neighbourPoint = points.get( neighbour ); boolean newInterdimensional; double newDistance; if( world != neighbour.element.getWorld() ) { newInterdimensional = true; newDistance = Double.POSITIVE_INFINITY; } else { newInterdimensional = false; newDistance = point.distance + position.distanceTo( neighbour.element.getPosition() ); } if( neighbourPoint == null ) { TransmitPoint nextPoint = new TransmitPoint( neighbour, newDistance, newInterdimensional ); points.put( neighbour, nextPoint ); transmitTo.add( nextPoint ); } else if( newDistance < neighbourPoint.distance ) { transmitTo.remove( neighbourPoint ); neighbourPoint.distance = newDistance; neighbourPoint.interdimensional = newInterdimensional; transmitTo.add( neighbourPoint ); } } } } for( TransmitPoint point : points.values() ) { point.node.tryTransmit( packet, point.distance, point.interdimensional, range, interdimensional ); } } @Override public boolean connect( @Nonnull IWiredNode nodeU, @Nonnull IWiredNode nodeV ) { WiredNode wiredU = checkNode( nodeU ); WiredNode wiredV = checkNode( nodeV ); if( nodeU == nodeV ) { throw new IllegalArgumentException( "Cannot add a connection to oneself." ); } lock.writeLock() .lock(); try { if( nodes == null ) { throw new IllegalStateException( "Cannot add a connection to an empty network." ); } boolean hasU = wiredU.network == this; boolean hasV = wiredV.network == this; if( !hasU && !hasV ) { throw new IllegalArgumentException( "Neither node is in the network." ); } // We're going to assimilate a node. Copy across all edges and vertices. if( !hasU || !hasV ) { WiredNetwork other = hasU ? wiredV.network : wiredU.network; other.lock.writeLock() .lock(); try { // Cache several properties for iterating over later Map otherPeripherals = other.peripherals; Map thisPeripherals = otherPeripherals.isEmpty() ? peripherals : new HashMap<>( peripherals ); Collection thisNodes = otherPeripherals.isEmpty() ? nodes : new ArrayList<>( nodes ); Collection otherNodes = other.nodes; // Move all nodes across into this network, destroying the original nodes. nodes.addAll( otherNodes ); for( WiredNode node : otherNodes ) { node.network = this; } other.nodes = null; // Move all peripherals across, other.peripherals = null; peripherals.putAll( otherPeripherals ); if( !thisPeripherals.isEmpty() ) { WiredNetworkChange.added( thisPeripherals ) .broadcast( otherNodes ); } if( !otherPeripherals.isEmpty() ) { WiredNetworkChange.added( otherPeripherals ) .broadcast( thisNodes ); } } finally { other.lock.writeLock() .unlock(); } } boolean added = wiredU.neighbours.add( wiredV ); if( added ) { wiredV.neighbours.add( wiredU ); } InvariantChecker.checkNetwork( this ); InvariantChecker.checkNode( wiredU ); InvariantChecker.checkNode( wiredV ); return added; } finally { lock.writeLock() .unlock(); } } @Override public boolean disconnect( @Nonnull IWiredNode nodeU, @Nonnull IWiredNode nodeV ) { WiredNode wiredU = checkNode( nodeU ); WiredNode wiredV = checkNode( nodeV ); if( nodeU == nodeV ) { throw new IllegalArgumentException( "Cannot remove a connection to oneself." ); } lock.writeLock() .lock(); try { boolean hasU = wiredU.network == this; boolean hasV = wiredV.network == this; if( !hasU || !hasV ) { throw new IllegalArgumentException( "One node is not in the network." ); } // If there was no connection to remove then split. if( !wiredU.neighbours.remove( wiredV ) ) { return false; } wiredV.neighbours.remove( wiredU ); // Determine if there is still some connection from u to v. // Note this is an inlining of reachableNodes which short-circuits // if all nodes are reachable. Queue enqueued = new ArrayDeque<>(); HashSet reachableU = new HashSet<>(); reachableU.add( wiredU ); enqueued.add( wiredU ); while( !enqueued.isEmpty() ) { WiredNode node = enqueued.remove(); for( WiredNode neighbour : node.neighbours ) { // If we can reach wiredV from wiredU then abort. if( neighbour == wiredV ) { return true; } // Otherwise attempt to enqueue this neighbour as well. if( reachableU.add( neighbour ) ) { enqueued.add( neighbour ); } } } // Create a new network with all U-reachable nodes/edges and remove them // from the existing graph. WiredNetwork networkU = new WiredNetwork( reachableU ); networkU.lock.writeLock() .lock(); try { // Remove nodes from this network nodes.removeAll( reachableU ); // Set network and transfer peripherals for( WiredNode node : reachableU ) { node.network = networkU; networkU.peripherals.putAll( node.peripherals ); peripherals.keySet() .removeAll( node.peripherals.keySet() ); } // Broadcast changes if( !peripherals.isEmpty() ) { WiredNetworkChange.removed( peripherals ) .broadcast( networkU.nodes ); } if( !networkU.peripherals.isEmpty() ) { WiredNetworkChange.removed( networkU.peripherals ) .broadcast( nodes ); } InvariantChecker.checkNetwork( this ); InvariantChecker.checkNetwork( networkU ); InvariantChecker.checkNode( wiredU ); InvariantChecker.checkNode( wiredV ); return true; } finally { networkU.lock.writeLock() .unlock(); } } finally { lock.writeLock() .unlock(); } } @Override public boolean remove( @Nonnull IWiredNode node ) { WiredNode wired = checkNode( node ); lock.writeLock() .lock(); try { // If we're the empty graph then just abort: nodes must have _some_ network. if( nodes == null ) { return false; } if( nodes.size() <= 1 ) { return false; } if( wired.network != this ) { return false; } HashSet neighbours = wired.neighbours; // Remove this node and move into a separate network. nodes.remove( wired ); for( WiredNode neighbour : neighbours ) { neighbour.neighbours.remove( wired ); } WiredNetwork wiredNetwork = new WiredNetwork( wired ); // If we're a leaf node in the graph (only one neighbour) then we don't need to // check for network splitting if( neighbours.size() == 1 ) { // Broadcast our simple peripheral changes removeSingleNode( wired, wiredNetwork ); InvariantChecker.checkNode( wired ); InvariantChecker.checkNetwork( wiredNetwork ); return true; } HashSet reachable = reachableNodes( neighbours.iterator() .next() ); // If all nodes are reachable then exit. if( reachable.size() == nodes.size() ) { // Broadcast our simple peripheral changes removeSingleNode( wired, wiredNetwork ); InvariantChecker.checkNode( wired ); InvariantChecker.checkNetwork( wiredNetwork ); return true; } // A split may cause 2..neighbours.size() separate networks, so we // iterate through our neighbour list, generating child networks. neighbours.removeAll( reachable ); ArrayList maximals = new ArrayList<>( neighbours.size() + 1 ); maximals.add( wiredNetwork ); maximals.add( new WiredNetwork( reachable ) ); while( !neighbours.isEmpty() ) { reachable = reachableNodes( neighbours.iterator() .next() ); neighbours.removeAll( reachable ); maximals.add( new WiredNetwork( reachable ) ); } for( WiredNetwork network : maximals ) { network.lock.writeLock() .lock(); } try { // We special case the original node: detaching all peripherals when needed. wired.network = wiredNetwork; wired.peripherals = Collections.emptyMap(); // Ensure every network is finalised for( WiredNetwork network : maximals ) { for( WiredNode child : network.nodes ) { child.network = network; network.peripherals.putAll( child.peripherals ); } } for( WiredNetwork network : maximals ) { InvariantChecker.checkNetwork( network ); } InvariantChecker.checkNode( wired ); // Then broadcast network changes once all nodes are finalised for( WiredNetwork network : maximals ) { WiredNetworkChange.changeOf( peripherals, network.peripherals ) .broadcast( network.nodes ); } } finally { for( WiredNetwork network : maximals ) { network.lock.writeLock() .unlock(); } } nodes.clear(); peripherals.clear(); return true; } finally { lock.writeLock() .unlock(); } } @Override public void updatePeripherals( @Nonnull IWiredNode node, @Nonnull Map newPeripherals ) { WiredNode wired = checkNode( node ); Objects.requireNonNull( peripherals, "peripherals cannot be null" ); lock.writeLock() .lock(); try { if( wired.network != this ) { throw new IllegalStateException( "Node is not on this network" ); } Map oldPeripherals = wired.peripherals; WiredNetworkChange change = WiredNetworkChange.changeOf( oldPeripherals, newPeripherals ); if( change.isEmpty() ) { return; } wired.peripherals = ImmutableMap.copyOf( newPeripherals ); // Detach the old peripherals then remove them. peripherals.keySet() .removeAll( change.peripheralsRemoved() .keySet() ); // Add the new peripherals and attach them peripherals.putAll( change.peripheralsAdded() ); change.broadcast( nodes ); } finally { lock.writeLock() .unlock(); } } private void removeSingleNode( WiredNode wired, WiredNetwork wiredNetwork ) { wiredNetwork.lock.writeLock() .lock(); try { // Cache all the old nodes. Map wiredPeripherals = new HashMap<>( wired.peripherals ); // Setup the new node's network // Detach the old peripherals then remove them from the old network wired.network = wiredNetwork; wired.neighbours.clear(); wired.peripherals = Collections.emptyMap(); // Broadcast the change if( !peripherals.isEmpty() ) { WiredNetworkChange.removed( peripherals ) .broadcast( wired ); } // Now remove all peripherals from this network and broadcast the change. peripherals.keySet() .removeAll( wiredPeripherals.keySet() ); if( !wiredPeripherals.isEmpty() ) { WiredNetworkChange.removed( wiredPeripherals ) .broadcast( nodes ); } } finally { wiredNetwork.lock.writeLock() .unlock(); } } private static HashSet reachableNodes( WiredNode start ) { Queue enqueued = new ArrayDeque<>(); HashSet reachable = new HashSet<>(); reachable.add( start ); enqueued.add( start ); WiredNode node; while( (node = enqueued.poll()) != null ) { for( WiredNode neighbour : node.neighbours ) { // Otherwise attempt to enqueue this neighbour as well. if( reachable.add( neighbour ) ) { enqueued.add( neighbour ); } } } return reachable; } private static WiredNode checkNode( IWiredNode node ) { if( node instanceof WiredNode ) { return (WiredNode) node; } else { throw new IllegalArgumentException( "Unknown implementation of IWiredNode: " + node ); } } private static class TransmitPoint implements Comparable { final WiredNode node; double distance; boolean interdimensional; TransmitPoint( WiredNode node, double distance, boolean interdimensional ) { this.node = node; this.distance = distance; this.interdimensional = interdimensional; } @Override public int compareTo( @Nonnull TransmitPoint o ) { // Objects with the same distance are not the same object, so we must add an additional layer of ordering. return distance == o.distance ? Integer.compare( node.hashCode(), o.node.hashCode() ) : Double.compare( distance, o.distance ); } } }