From 74f5093d2a29a529360279a8e9aff35b09062bfe Mon Sep 17 00:00:00 2001 From: SquidDev Date: Wed, 21 Feb 2018 15:29:34 +0000 Subject: [PATCH] Add the default implementation of wired networks --- build.gradle | 2 + .../dan200/computercraft/ComputerCraft.java | 34 ++ .../proxy/ComputerCraftProxyCommon.java | 6 +- .../shared/wired/DefaultWiredProvider.java | 23 + .../shared/wired/InvariantChecker.java | 46 ++ .../shared/wired/WiredNetwork.java | 458 ++++++++++++++++ .../shared/wired/WiredNetworkChange.java | 101 ++++ .../computercraft/shared/wired/WiredNode.java | 151 ++++++ .../shared/wired/NetworkTest.java | 506 ++++++++++++++++++ 9 files changed, 1326 insertions(+), 1 deletion(-) create mode 100644 src/main/java/dan200/computercraft/shared/wired/DefaultWiredProvider.java create mode 100644 src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java create mode 100644 src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java create mode 100644 src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java create mode 100644 src/main/java/dan200/computercraft/shared/wired/WiredNode.java create mode 100644 src/test/java/dan200/computercraft/shared/wired/NetworkTest.java diff --git a/build.gradle b/build.gradle index 1d96373a08..1cee602bbb 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,8 @@ dependencies { deobfProvided "mezz.jei:jei_1.12:4.7.5.86:api" runtime "mezz.jei:jei_1.12:4.7.5.86" shade 'org.squiddev:Cobalt:0.3.1' + + testCompile 'junit:junit:4.11' } javadoc { diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java index aa138019fc..4211e7eea1 100644 --- a/src/main/java/dan200/computercraft/ComputerCraft.java +++ b/src/main/java/dan200/computercraft/ComputerCraft.java @@ -14,6 +14,9 @@ import dan200.computercraft.api.media.IMedia; import dan200.computercraft.api.media.IMediaProvider; import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.network.wired.IWiredProvider; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheralProvider; import dan200.computercraft.api.permissions.ITurtlePermissionProvider; @@ -57,6 +60,7 @@ import dan200.computercraft.shared.turtle.blocks.TileTurtle; import dan200.computercraft.shared.turtle.upgrades.*; import dan200.computercraft.shared.util.*; +import dan200.computercraft.shared.wired.WiredNode; import io.netty.buffer.Unpooled; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -69,6 +73,7 @@ import net.minecraft.util.NonNullList; import net.minecraft.util.SoundEvent; import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; import net.minecraftforge.common.config.ConfigCategory; import net.minecraftforge.common.config.Configuration; @@ -259,6 +264,7 @@ public static class Config { private static List permissionProviders = new ArrayList<>(); private static final Map pocketUpgrades = new HashMap<>(); private static final Set apiFactories = new LinkedHashSet<>(); + private static final Set wiredProviders = new LinkedHashSet<>(); // Implementation @Mod.Instance( value = ComputerCraft.MOD_ID ) @@ -730,6 +736,16 @@ public static void registerAPIFactory( ILuaAPIFactory provider ) } } + public static void registerWiredProvider( IWiredProvider provider ) + { + if( provider != null ) wiredProviders.add( provider ); + } + + public static IWiredNode createWiredNodeForElement( IWiredElement element ) + { + return new WiredNode( element ); + } + public static IPeripheral getPeripheralAt( World world, BlockPos pos, EnumFacing side ) { // Try the handlers in order: @@ -751,6 +767,24 @@ public static IPeripheral getPeripheralAt( World world, BlockPos pos, EnumFacing return null; } + public static IWiredElement getWiredElementAt( IBlockAccess world, BlockPos pos, EnumFacing side ) + { + // Try the handlers in order: + for( IWiredProvider provider : wiredProviders ) + { + try + { + IWiredElement element = provider.getElement( world, pos, side ); + if( element != null ) return element; + } + catch( Exception e ) + { + ComputerCraft.log.error( "Wired element provider " + provider + " errored.", e ); + } + } + return null; + } + public static int getDefaultBundledRedstoneOutput( World world, BlockPos pos, EnumFacing side ) { if( WorldUtil.isBlockInWorld( world, pos ) ) diff --git a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java index 34c0fff730..c4309a2067 100644 --- a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java +++ b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java @@ -52,6 +52,7 @@ import dan200.computercraft.shared.turtle.blocks.TileTurtle; import dan200.computercraft.shared.turtle.inventory.ContainerTurtle; import dan200.computercraft.shared.util.*; +import dan200.computercraft.shared.wired.DefaultWiredProvider; import net.minecraft.block.Block; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.entity.player.EntityPlayer; @@ -291,7 +292,7 @@ public void registerItems( RegistryEvent.Register event ) // Command Computer registry.register( new ItemCommandComputer( ComputerCraft.Blocks.commandComputer ).setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "command_computer" ) ) ); - // Command Computer + // Advanced modem registry.register( new ItemAdvancedModem( ComputerCraft.Blocks.advancedModem ).setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "advanced_modem" ) ) ); // Items @@ -482,6 +483,9 @@ private void registerTileEntities() // Register media providers ComputerCraftAPI.registerMediaProvider( new DefaultMediaProvider() ); + + // Register network providers + ComputerCraftAPI.registerWiredProvider( new DefaultWiredProvider() ); } private void registerForgeHandlers() diff --git a/src/main/java/dan200/computercraft/shared/wired/DefaultWiredProvider.java b/src/main/java/dan200/computercraft/shared/wired/DefaultWiredProvider.java new file mode 100644 index 0000000000..a042d70103 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/DefaultWiredProvider.java @@ -0,0 +1,23 @@ +package dan200.computercraft.shared.wired; + +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredElementTile; +import dan200.computercraft.api.network.wired.IWiredProvider; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class DefaultWiredProvider implements IWiredProvider +{ + @Nullable + @Override + public IWiredElement getElement( @Nonnull IBlockAccess world, @Nonnull BlockPos pos, @Nonnull EnumFacing side ) + { + TileEntity te = world.getTileEntity( pos ); + return te instanceof IWiredElementTile ? ((IWiredElementTile) te).getWiredElement( side ) : null; + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java b/src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java new file mode 100644 index 0000000000..ff3a163fe5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/InvariantChecker.java @@ -0,0 +1,46 @@ +package dan200.computercraft.shared.wired; + +import dan200.computercraft.ComputerCraft; + +/** + * Verifies certain elements of a network are "well formed". + * + * This adds substantial overhead to network modification, and so should only be enabled + * in a development environment. + */ +public class InvariantChecker +{ + private static final boolean ENABLED = false; + + public static void checkNode( WiredNode node ) + { + if( !ENABLED ) return; + + WiredNetwork network = node.network; + if( network == null ) + { + ComputerCraft.log.error( "Node's network is null", new Exception() ); + return; + } + + if( network.nodes == null || !network.nodes.contains( node ) ) + { + ComputerCraft.log.error( "Node's network does not contain node", new Exception() ); + } + + for( WiredNode neighbour : node.neighbours ) + { + if( !neighbour.neighbours.contains( node ) ) + { + ComputerCraft.log.error( "Neighbour is missing node", new Exception() ); + } + } + } + + public static void checkNetwork( WiredNetwork network ) + { + if( !ENABLED ) return; + + for( WiredNode node : network.nodes ) checkNode( node ); + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java b/src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java new file mode 100644 index 0000000000..c09391364c --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/WiredNetwork.java @@ -0,0 +1,458 @@ +package dan200.computercraft.shared.wired; + +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<>(); + + public WiredNetwork( WiredNode node ) + { + nodes = new HashSet<>( 1 ); + nodes.add( node ); + } + + private WiredNetwork( HashSet nodes ) + { + this.nodes = nodes; + } + + @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<>( this.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.size() != 0 ) WiredNetworkChange.removed( peripherals ).broadcast( networkU.nodes ); + if( networkU.peripherals.size() != 0 ) + { + 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.size() > 0 ) + { + 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 invalidate( @Nonnull IWiredNode node ) + { + WiredNode wired = checkNode( node ); + + lock.writeLock().lock(); + try + { + if( wired.network != this ) throw new IllegalStateException( "Node is not on this network" ); + + Map oldPeripherals = wired.peripherals; + Map newPeripherals = wired.element.getPeripherals(); + WiredNetworkChange change = WiredNetworkChange.changeOf( oldPeripherals, newPeripherals ); + if( change.isEmpty() ) return; + + wired.peripherals = 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(); + } + } + + 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 ) + { + neighbourPoint = new TransmitPoint( neighbour, newDistance, newInterdimensional ); + points.put( neighbour, neighbourPoint ); + transmitTo.add( neighbourPoint ); + } + 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 ); + } + } + + 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 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 ); + } + } + + private static WiredNode checkNode( IWiredNode node ) + { + if( node instanceof WiredNode ) + { + return (WiredNode) node; + } + else + { + throw new IllegalArgumentException( "Unknown implementation of IWiredNode: " + node ); + } + } + + 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; + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java b/src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java new file mode 100644 index 0000000000..4b53d24da9 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/WiredNetworkChange.java @@ -0,0 +1,101 @@ +package dan200.computercraft.shared.wired; + +import dan200.computercraft.api.network.wired.IWiredNetworkChange; +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class WiredNetworkChange implements IWiredNetworkChange +{ + private final Map removed; + private final Map added; + + private WiredNetworkChange( Map removed, Map added ) + { + this.removed = removed; + this.added = added; + } + + public static WiredNetworkChange changed( Map removed, Map added ) + { + return new WiredNetworkChange( Collections.unmodifiableMap( removed ), Collections.unmodifiableMap( added ) ); + } + + public static WiredNetworkChange added( Map added ) + { + return new WiredNetworkChange( Collections.emptyMap(), Collections.unmodifiableMap( added ) ); + } + + public static WiredNetworkChange removed( Map removed ) + { + return new WiredNetworkChange( Collections.unmodifiableMap( removed ), Collections.emptyMap() ); + } + + public static WiredNetworkChange changeOf( Map oldPeripherals, Map newPeripherals ) + { + Map added = new HashMap<>( newPeripherals ); + Map removed = new HashMap<>(); + + for( Map.Entry entry : oldPeripherals.entrySet() ) + { + String oldKey = entry.getKey(); + IPeripheral oldValue = entry.getValue(); + if( newPeripherals.containsKey( oldKey ) ) + { + IPeripheral rightValue = added.get( oldKey ); + if( oldValue.equals( rightValue ) ) + { + added.remove( oldKey ); + } + else + { + removed.put( oldKey, oldValue ); + } + } + else + { + removed.put( oldKey, oldValue ); + } + } + + return changed( removed, added ); + } + + @Nonnull + @Override + public Map peripheralsAdded() + { + return added; + } + + @Nonnull + @Override + public Map peripheralsRemoved() + { + return removed; + } + + public boolean isEmpty() + { + return added.isEmpty() && removed.isEmpty(); + } + + void broadcast( Iterable nodes ) + { + if( !isEmpty() ) + { + for( WiredNode node : nodes ) node.element.networkChanged( this ); + } + } + + void broadcast( WiredNode node ) + { + if( !isEmpty() ) + { + node.element.networkChanged( this ); + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/wired/WiredNode.java b/src/main/java/dan200/computercraft/shared/wired/WiredNode.java new file mode 100644 index 0000000000..f5485c59b4 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/wired/WiredNode.java @@ -0,0 +1,151 @@ +package dan200.computercraft.shared.wired; + +import com.google.common.base.Preconditions; +import dan200.computercraft.api.network.IPacketReceiver; +import dan200.computercraft.api.network.Packet; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNetwork; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.network.wired.IWiredSender; +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; + +public final class WiredNode implements IWiredNode +{ + private Set receivers; + + final IWiredElement element; + Map peripherals = Collections.emptyMap(); + + final HashSet neighbours = new HashSet<>(); + volatile WiredNetwork network; + + public WiredNode( IWiredElement element ) + { + this.element = element; + this.network = new WiredNetwork( this ); + } + + @Override + public synchronized void addReceiver( @Nonnull IPacketReceiver receiver ) + { + if( receivers == null ) receivers = new HashSet<>(); + receivers.add( receiver ); + } + + @Override + public synchronized void removeReceiver( @Nonnull IPacketReceiver receiver ) + { + if( receivers != null ) receivers.remove( receiver ); + } + + synchronized void tryTransmit( Packet packet, double packetDistance, boolean packetInterdimensional, double range, boolean interdimensional ) + { + if( receivers == null ) return; + + for( IPacketReceiver receiver : receivers ) + { + if( !packetInterdimensional ) + { + double receiveRange = Math.max( range, receiver.getRange() ); // Ensure range is symmetrical + if( interdimensional || receiver.isInterdimensional() || packetDistance < receiveRange ) + { + receiver.receiveSameDimension( packet, packetDistance + element.getPosition().distanceTo( receiver.getPosition() ) ); + } + } + else + { + if( interdimensional || receiver.isInterdimensional() ) + { + receiver.receiveDifferentDimension( packet ); + } + } + } + } + + @Override + public boolean isWireless() + { + return false; + } + + @Override + public void transmitSameDimension( @Nonnull Packet packet, double range ) + { + Preconditions.checkNotNull( packet, "packet cannot be null" ); + if( !(packet.getSender() instanceof IWiredSender) || ((IWiredSender) packet.getSender()).getNode() != this ) + { + throw new IllegalArgumentException( "Sender is not in the network" ); + } + + acquireReadLock(); + try + { + network.transmitPacket( this, packet, range, false ); + } + finally + { + network.lock.readLock().unlock(); + } + } + + @Override + public void transmitInterdimensional( @Nonnull Packet packet ) + { + Preconditions.checkNotNull( packet, "packet cannot be null" ); + if( !(packet.getSender() instanceof IWiredSender) || ((IWiredSender) packet.getSender()).getNode() != this ) + { + throw new IllegalArgumentException( "Sender is not in the network" ); + } + + acquireReadLock(); + try + { + network.transmitPacket( this, packet, 0, true ); + } + finally + { + network.lock.readLock().unlock(); + } + } + + @Nonnull + @Override + public IWiredElement getElement() + { + return element; + } + + @Nonnull + @Override + public IWiredNetwork getNetwork() + { + return network; + } + + @Override + public String toString() + { + return "WiredNode{@" + element.getPosition() + " (" + element.getClass().getSimpleName() + ")}"; + } + + private void acquireReadLock() + { + WiredNetwork currentNetwork = network; + while( true ) + { + Lock lock = currentNetwork.lock.readLock(); + lock.lock(); + if( currentNetwork == network ) return; + + + lock.unlock(); + } + } +} diff --git a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java new file mode 100644 index 0000000000..a2ff905aa4 --- /dev/null +++ b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java @@ -0,0 +1,506 @@ +package dan200.computercraft.shared.wired; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNetwork; +import dan200.computercraft.api.network.wired.IWiredNetworkChange; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; +import org.apache.logging.log4j.LogManager; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.*; + +public class NetworkTest +{ + @Before + public void setup() + { + ComputerCraft.log = LogManager.getLogger(); + } + + @Test + public void testConnect() + { + NetworkElement + aE = new NetworkElement( null, null, "a" ), + bE = new NetworkElement( null, null, "b" ), + cE = new NetworkElement( null, null, "c" ); + + IWiredNode + aN = aE.getNode(), + bN = bE.getNode(), + cN = cE.getNode(); + + assertNotEquals( "A's and B's network must be different", aN.getNetwork(), bN.getNetwork() ); + assertNotEquals( "A's and C's network must be different", aN.getNetwork(), cN.getNetwork() ); + assertNotEquals( "B's and C's network must be different", bN.getNetwork(), cN.getNetwork() ); + + assertTrue( "Must be able to add connection", aN.getNetwork().connect( aN, bN ) ); + assertFalse( "Cannot add connection twice", aN.getNetwork().connect( aN, bN ) ); + + assertEquals( "A's and B's network must be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's network should be A and B", Sets.newHashSet( aN, bN ), nodes( aN.getNetwork() ) ); + + assertEquals( "A's peripheral set should be A, B", Sets.newHashSet( "a", "b" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be A, B", Sets.newHashSet( "a", "b" ), bE.allPeripherals().keySet() ); + + aN.getNetwork().connect( aN, cN ); + + assertEquals( "A's and B's network must be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's and C's network must be equal", aN.getNetwork(), cN.getNetwork() ); + assertEquals( "A's network should be A, B and C", Sets.newHashSet( aN, bN, cN ), nodes( aN.getNetwork() ) ); + + assertEquals( "A's neighbour set should be B, C", Sets.newHashSet( bN, cN ), neighbours( aN ) ); + assertEquals( "B's neighbour set should be A", Sets.newHashSet( aN ), neighbours( bN ) ); + assertEquals( "C's neighbour set should be A", Sets.newHashSet( aN ), neighbours( cN ) ); + + assertEquals( "A's peripheral set should be A, B, C", Sets.newHashSet( "a", "b", "c" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be A, B, C", Sets.newHashSet( "a", "b", "c" ), bE.allPeripherals().keySet() ); + assertEquals( "C's peripheral set should be A, B, C", Sets.newHashSet( "a", "b", "c" ), cE.allPeripherals().keySet() ); + } + + @Test + public void testDisconnectNoChange() + { + NetworkElement + aE = new NetworkElement( null, null, "a" ), + bE = new NetworkElement( null, null, "b" ), + cE = new NetworkElement( null, null, "c" ); + + IWiredNode + aN = aE.getNode(), + bN = bE.getNode(), + cN = cE.getNode(); + + aN.getNetwork().connect( aN, bN ); + aN.getNetwork().connect( aN, cN ); + aN.getNetwork().connect( bN, cN ); + + aN.getNetwork().disconnect( aN, bN ); + + assertEquals( "A's and B's network must be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's and C's network must be equal", aN.getNetwork(), cN.getNetwork() ); + assertEquals( "A's network should be A, B and C", Sets.newHashSet( aN, bN, cN ), nodes( aN.getNetwork() ) ); + + assertEquals( "A's peripheral set should be A, B, C", Sets.newHashSet( "a", "b", "c" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be A, B, C", Sets.newHashSet( "a", "b", "c" ), bE.allPeripherals().keySet() ); + assertEquals( "C's peripheral set should be A, B, C", Sets.newHashSet( "a", "b", "c" ), cE.allPeripherals().keySet() ); + } + + @Test + public void testDisconnectLeaf() + { + NetworkElement + aE = new NetworkElement( null, null, "a" ), + bE = new NetworkElement( null, null, "b" ), + cE = new NetworkElement( null, null, "c" ); + + IWiredNode + aN = aE.getNode(), + bN = bE.getNode(), + cN = cE.getNode(); + + aN.getNetwork().connect( aN, bN ); + aN.getNetwork().connect( aN, cN ); + + aN.getNetwork().disconnect( aN, bN ); + + assertNotEquals( "A's and B's network must not be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's and C's network must be equal", aN.getNetwork(), cN.getNetwork() ); + assertEquals( "A's network should be A and C", Sets.newHashSet( aN, cN ), nodes( aN.getNetwork() ) ); + assertEquals( "B's network should be B", Sets.newHashSet( bN ), nodes( bN.getNetwork() ) ); + + assertEquals( "A's peripheral set should be A, C", Sets.newHashSet( "a", "c" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be B", Sets.newHashSet( "b" ), bE.allPeripherals().keySet() ); + assertEquals( "C's peripheral set should be A, C", Sets.newHashSet( "a", "c" ), cE.allPeripherals().keySet() ); + } + + @Test + public void testDisconnectSplit() + { + NetworkElement + aE = new NetworkElement( null, null, "a" ), + aaE = new NetworkElement( null, null, "a_" ), + bE = new NetworkElement( null, null, "b" ), + bbE = new NetworkElement( null, null, "b_" ); + + IWiredNode + aN = aE.getNode(), + aaN = aaE.getNode(), + bN = bE.getNode(), + bbN = bbE.getNode(); + + aN.getNetwork().connect( aN, aaN ); + bN.getNetwork().connect( bN, bbN ); + + aN.getNetwork().connect( aN, bN ); + + aN.getNetwork().disconnect( aN, bN ); + + assertNotEquals( "A's and B's network must not be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's and A_'s network must be equal", aN.getNetwork(), aaN.getNetwork() ); + assertEquals( "B's and B_'s network must be equal", bN.getNetwork(), bbN.getNetwork() ); + + assertEquals( "A's network should be A and A_", Sets.newHashSet( aN, aaN ), nodes( aN.getNetwork() ) ); + assertEquals( "B's network should be B and B_", Sets.newHashSet( bN, bbN ), nodes( bN.getNetwork() ) ); + + assertEquals( "A's peripheral set should be A and A_", Sets.newHashSet( "a", "a_" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be B and B_", Sets.newHashSet( "b", "b_" ), bE.allPeripherals().keySet() ); + } + + @Test + public void testRemoveSingle() + { + NetworkElement aE = new NetworkElement( null, null, "a" ); + IWiredNode aN = aE.getNode(); + + IWiredNetwork network = aN.getNetwork(); + assertFalse( "Cannot remove node from an empty network", aN.remove() ); + assertEquals( "Networks are same before and after", network, aN.getNetwork() ); + } + + @Test + public void testRemoveLeaf() + { + NetworkElement + aE = new NetworkElement( null, null, "a" ), + bE = new NetworkElement( null, null, "b" ), + cE = new NetworkElement( null, null, "c" ); + + IWiredNode + aN = aE.getNode(), + bN = bE.getNode(), + cN = cE.getNode(); + + aN.getNetwork().connect( aN, bN ); + aN.getNetwork().connect( aN, cN ); + + assertTrue( "Must be able to remove node", aN.getNetwork().remove( bN ) ); + assertFalse( "Cannot remove a second time", aN.getNetwork().remove( bN ) ); + + assertNotEquals( "A's and B's network must not be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's and C's network must be equal", aN.getNetwork(), cN.getNetwork() ); + + assertEquals( "A's network should be A and C", Sets.newHashSet( aN, cN ), nodes( aN.getNetwork() ) ); + assertEquals( "B's network should be B", Sets.newHashSet( bN ), nodes( bN.getNetwork() ) ); + + assertEquals( "A's peripheral set should be A, C", Sets.newHashSet( "a", "c" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be empty", Sets.newHashSet(), bE.allPeripherals().keySet() ); + assertEquals( "C's peripheral set should be A, C", Sets.newHashSet( "a", "c" ), cE.allPeripherals().keySet() ); + } + + @Test + public void testRemoveSplit() + { + NetworkElement + aE = new NetworkElement( null, null, "a" ), + aaE = new NetworkElement( null, null, "a_" ), + bE = new NetworkElement( null, null, "b" ), + bbE = new NetworkElement( null, null, "b_" ), + cE = new NetworkElement( null, null, "c" ); + + IWiredNode + aN = aE.getNode(), + aaN = aaE.getNode(), + bN = bE.getNode(), + bbN = bbE.getNode(), + cN = cE.getNode(); + + aN.getNetwork().connect( aN, aaN ); + bN.getNetwork().connect( bN, bbN ); + + cN.getNetwork().connect( aN, cN ); + cN.getNetwork().connect( bN, cN ); + + cN.getNetwork().remove( cN ); + + assertNotEquals( "A's and B's network must not be equal", aN.getNetwork(), bN.getNetwork() ); + assertEquals( "A's and A_'s network must be equal", aN.getNetwork(), aaN.getNetwork() ); + assertEquals( "B's and B_'s network must be equal", bN.getNetwork(), bbN.getNetwork() ); + + assertEquals( "A's network should be A and A_", Sets.newHashSet( aN, aaN ), nodes( aN.getNetwork() ) ); + assertEquals( "B's network should be B and B_", Sets.newHashSet( bN, bbN ), nodes( bN.getNetwork() ) ); + assertEquals( "C's network should be C", Sets.newHashSet( cN ), nodes( cN.getNetwork() ) ); + + assertEquals( "A's peripheral set should be A and A_", Sets.newHashSet( "a", "a_" ), aE.allPeripherals().keySet() ); + assertEquals( "B's peripheral set should be B and B_", Sets.newHashSet( "b", "b_" ), bE.allPeripherals().keySet() ); + assertEquals( "C's peripheral set should be empty", Sets.newHashSet(), cE.allPeripherals().keySet() ); + } + + @Test + @Ignore("Takes a long time to run, mostly for stress testing") + public void testLarge() + { + final int BRUTE_SIZE = 16; + final int TOGGLE_CONNECTION_TIMES = 5; + final int TOGGLE_NODE_TIMES = 5; + + Grid grid = new Grid<>( BRUTE_SIZE ); + grid.map( ( existing, pos ) -> new NetworkElement( null, null, "n_" + pos ).getNode() ); + + // Test connecting + { + long start = System.nanoTime(); + + grid.forEach( ( existing, pos ) -> { + for( EnumFacing facing : EnumFacing.VALUES ) + { + BlockPos offset = pos.offset( facing ); + if( (offset.getX() > BRUTE_SIZE / 2) == (pos.getX() > BRUTE_SIZE / 2) ) + { + IWiredNode other = grid.get( offset ); + if( other != null ) existing.getNetwork().connect( existing, other ); + } + } + } ); + + long end = System.nanoTime(); + + System.out.printf( "Connecting %s³ nodes took %s seconds\n", BRUTE_SIZE, (end - start) * 1e-9 ); + } + + // Test toggling + { + IWiredNode left = grid.get( new BlockPos( BRUTE_SIZE / 2, 0, 0 ) ); + IWiredNode right = grid.get( new BlockPos( BRUTE_SIZE / 2 + 1, 0, 0 ) ); + assertNotEquals( left.getNetwork(), right.getNetwork() ); + + long start = System.nanoTime(); + for( int i = 0; i < TOGGLE_CONNECTION_TIMES; i++ ) + { + left.getNetwork().connect( left, right ); + left.getNetwork().disconnect( left, right ); + } + + long end = System.nanoTime(); + + System.out.printf( "Toggling connection %s times took %s seconds\n", TOGGLE_CONNECTION_TIMES, (end - start) * 1e-9 ); + } + + { + IWiredNode left = grid.get( new BlockPos( BRUTE_SIZE / 2, 0, 0 ) ); + IWiredNode right = grid.get( new BlockPos( BRUTE_SIZE / 2 + 1, 0, 0 ) ); + IWiredNode centre = new NetworkElement( null, null, "c" ).getNode(); + assertNotEquals( left.getNetwork(), right.getNetwork() ); + + long start = System.nanoTime(); + for( int i = 0; i < TOGGLE_NODE_TIMES; i++ ) + { + left.getNetwork().connect( left, centre ); + right.getNetwork().connect( right, centre ); + + left.getNetwork().remove( centre ); + } + + long end = System.nanoTime(); + + System.out.printf( "Toggling node %s times took %s seconds\n", TOGGLE_NODE_TIMES, (end - start) * 1e-9 ); + } + } + + private static class NetworkElement implements IWiredElement + { + private final World world; + private final Vec3d position; + private final String id; + private final IWiredNode node; + private final Map localPeripherals = Maps.newHashMap(); + private final Map remotePeripherals = Maps.newHashMap(); + + private NetworkElement( World world, Vec3d position, String id ) + { + this.world = world; + this.position = position; + this.id = id; + this.node = ComputerCraftAPI.createWiredNodeForElement( this ); + this.addPeripheral( id ); + } + + @Nonnull + @Override + public World getWorld() + { + return world; + } + + @Nonnull + @Override + public Vec3d getPosition() + { + return position; + } + + @Nonnull + @Override + public String getSenderID() + { + return id; + } + + @Override + public String toString() + { + return "NetworkElement{" + id + "}"; + } + + @Nonnull + @Override + public IWiredNode getNode() + { + return node; + } + + @Override + public void networkChanged( @Nonnull IWiredNetworkChange change ) + { + remotePeripherals.keySet().removeAll( change.peripheralsRemoved().keySet() ); + remotePeripherals.putAll( change.peripheralsAdded() ); + } + + @Nonnull + @Override + public Map getPeripherals() + { + return Collections.unmodifiableMap( localPeripherals ); + } + + public NetworkElement addPeripheral( String name ) + { + localPeripherals.put( name, new NetworkPeripheral() ); + getNode().invalidate(); + return this; + } + + @Nonnull + public Map allPeripherals() + { + return remotePeripherals; + } + } + + private static class NetworkPeripheral implements IPeripheral + { + @Nonnull + @Override + public String getType() + { + return "test"; + } + + @Nonnull + @Override + public String[] getMethodNames() + { + return new String[0]; + } + + @Nullable + @Override + public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + return new Object[0]; + } + + @Override + public boolean equals( @Nullable IPeripheral other ) + { + return this == other; + } + } + + private static class Grid + { + private final int size; + private final T[] box; + + @SuppressWarnings("unchecked") + public Grid( int size ) + { + this.size = size; + this.box = (T[]) new Object[size * size * size]; + } + + public void set( BlockPos pos, T elem ) + { + int x = pos.getX(), y = pos.getY(), z = pos.getZ(); + + if( x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size ) + { + box[x * size * size + y * size + z] = elem; + } + else + { + throw new IndexOutOfBoundsException( pos.toString() ); + } + } + + public T get( BlockPos pos ) + { + int x = pos.getX(), y = pos.getY(), z = pos.getZ(); + + return x >= 0 && x < size && y >= 0 && y < size && z >= 0 && z < size + ? box[x * size * size + y * size + z] + : null; + } + + public void forEach( BiConsumer transform ) + { + for( int x = 0; x < size; x++ ) + { + for( int y = 0; y < size; y++ ) + { + for( int z = 0; z < size; z++ ) + { + transform.accept( box[x * size * size + y * size + z], new BlockPos( x, y, z ) ); + } + } + } + } + + public void map( BiFunction transform ) + { + for( int x = 0; x < size; x++ ) + { + for( int y = 0; y < size; y++ ) + { + for( int z = 0; z < size; z++ ) + { + box[x * size * size + y * size + z] = transform.apply( box[x * size * size + y * size + z], new BlockPos( x, y, z ) ); + } + } + } + } + } + + private static Set nodes( IWiredNetwork network ) + { + return ((WiredNetwork) network).nodes; + } + + private static Set neighbours( IWiredNode node ) + { + return ((WiredNode) node).neighbours; + } +}