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..013b203ca3 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; @@ -44,6 +47,7 @@ import dan200.computercraft.shared.network.PacketHandler; import dan200.computercraft.shared.peripheral.common.BlockCable; import dan200.computercraft.shared.peripheral.common.BlockPeripheral; +import dan200.computercraft.shared.peripheral.common.BlockWiredModemFull; import dan200.computercraft.shared.peripheral.diskdrive.TileDiskDrive; import dan200.computercraft.shared.peripheral.modem.BlockAdvancedModem; import dan200.computercraft.shared.peripheral.modem.WirelessNetwork; @@ -57,6 +61,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 +74,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; @@ -175,6 +181,7 @@ public static class Blocks public static BlockTurtle turtleAdvanced; public static BlockCommandComputer commandComputer; public static BlockAdvancedModem advancedModem; + public static BlockWiredModemFull wiredModemFull; } public static class Items @@ -259,6 +266,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 +738,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 +769,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/api/ComputerCraftAPI.java b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java index 88dd4bcde0..fd00693edf 100644 --- a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java +++ b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java @@ -12,6 +12,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.IComputerAccess; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.api.peripheral.IPeripheralProvider; @@ -21,6 +24,7 @@ import dan200.computercraft.api.turtle.ITurtleUpgrade; import net.minecraft.util.EnumFacing; import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; import javax.annotation.Nonnull; @@ -328,6 +332,81 @@ public static void registerAPIFactory( @Nonnull ILuaAPIFactory upgrade ) } } + /** + * Registers a peripheral handler to convert blocks into {@link IPeripheral} implementations. + * + * @param handler The peripheral provider to register. + * @see dan200.computercraft.api.peripheral.IPeripheral + * @see dan200.computercraft.api.peripheral.IPeripheralProvider + */ + public static void registerWiredProvider( @Nonnull IWiredProvider handler ) + { + findCC(); + if ( computerCraft_registerWiredProvider != null) + { + try { + computerCraft_registerWiredProvider.invoke( null, handler ); + } catch (Exception e){ + // It failed + } + } + } + + /** + * Construct a new wired node for a given wired element + * + * @param element The element to construct it for + * @return The element's node + * @see IWiredElement#getNode() + */ + @Nonnull + public static IWiredNode createWiredNodeForElement( @Nonnull IWiredElement element ) + { + findCC(); + if( computerCraft_createWiredNodeForElement != null ) + { + try + { + return (IWiredNode) computerCraft_createWiredNodeForElement.invoke( null, element ); + } + catch( ReflectiveOperationException e ) + { + throw new IllegalStateException( "Error creating wired node", e ); + } + } + else + { + throw new IllegalStateException( "ComputerCraft cannot be found" ); + } + } + + /** + * Get the wired network element for a block in world + * + * @param world The world the block exists in + * @param pos The position the block exists in + * @param side The side to extract the network element from + * @return The element's node + * @see IWiredElement#getNode() + */ + @Nullable + public static IWiredElement getWiredElementAt( @Nonnull IBlockAccess world, @Nonnull BlockPos pos, @Nonnull EnumFacing side ) + { + findCC(); + if( computerCraft_getWiredElementAt != null ) + { + try + { + return (IWiredElement) computerCraft_getWiredElementAt.invoke( null, world, pos, side ); + } + catch( ReflectiveOperationException ignored ) + { + } + } + + return null; + } + // The functions below here are private, and are used to interface with the non-API ComputerCraft classes. // Reflection is used here so you can develop your mod without decompiling ComputerCraft and including // it in your solution, and so your mod won't crash if ComputerCraft is installed. @@ -374,6 +453,15 @@ private static void findCC() computerCraft_registerAPIFactory = findCCMethod( "registerAPIFactory", new Class[] { ILuaAPIFactory.class } ); + computerCraft_registerWiredProvider = findCCMethod( "registerWiredProvider", new Class[] { + IWiredProvider.class + } ); + computerCraft_createWiredNodeForElement = findCCMethod( "createWiredNodeForElement", new Class[] { + IWiredElement.class + } ); + computerCraft_getWiredElementAt = findCCMethod( "getWiredElementAt", new Class[]{ + IBlockAccess.class, BlockPos.class, EnumFacing.class + } ); } catch( Exception e ) { System.out.println( "ComputerCraftAPI: ComputerCraft not found." ); } finally { @@ -411,4 +499,7 @@ private static Method findCCMethod( String name, Class[] args ) private static Method computerCraft_registerPocketUpgrade = null; private static Method computerCraft_getWirelessNetwork = null; private static Method computerCraft_registerAPIFactory = null; + private static Method computerCraft_registerWiredProvider = null; + private static Method computerCraft_createWiredNodeForElement = null; + private static Method computerCraft_getWiredElementAt = null; } diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredElement.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredElement.java new file mode 100644 index 0000000000..8f078bb2f7 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredElement.java @@ -0,0 +1,51 @@ +package dan200.computercraft.api.network.wired; + +import dan200.computercraft.api.ComputerCraftAPI; +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Map; + +/** + * An object which may be part of a wired network. + * + * Elements should construct a node using {@link ComputerCraftAPI#createWiredNodeForElement(IWiredElement)}. This acts + * as a proxy for all network objects. Whilst the node may change networks, an element's node should remain constant + * for its lifespan. + * + * Elements are generally tied to a block or tile entity in world. One should either register an {@link IWiredProvider} + * or implement {@link IWiredElementTile} on your tile entity. + * + * @see IWiredProvider + * @see ComputerCraftAPI#registerWiredProvider(IWiredProvider) + * @see IWiredElementTile + */ +public interface IWiredElement extends IWiredSender +{ + /** + * Fetch the peripherals this network element provides. + * + * This is only called when initially attaching to a network and after a call to {@link IWiredNode#invalidate()}}, so + * one does not need to cache the return value. + * + * @return The peripherals this node provides. + * @see IWiredNode#invalidate() + */ + @Nonnull + default Map getPeripherals() + { + return Collections.emptyMap(); + } + + /** + * Called when objects on the network change. This may occur when network nodes are added or removed, or when + * peripherals change. + * + * @param change The change which occurred. + * @see IWiredNetworkChange + */ + default void networkChanged( @Nonnull IWiredNetworkChange change ) + { + } +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredElementTile.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredElementTile.java new file mode 100644 index 0000000000..03e2f2f109 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredElementTile.java @@ -0,0 +1,22 @@ +package dan200.computercraft.api.network.wired; + +import net.minecraft.util.EnumFacing; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A {@link net.minecraft.tileentity.TileEntity} which provides a {@link IWiredElement}. This acts + * as a simpler alternative to a full-blown {@link IWiredProvider}. + */ +public interface IWiredElementTile +{ + /** + * Get the wired element of this tile for a given side. + * + * @param side The side to get the network element from. + * @return A network element, or {@code null} if there is no element here. + */ + @Nullable + IWiredElement getWiredElement( @Nonnull EnumFacing side ); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredNetwork.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetwork.java new file mode 100644 index 0000000000..3ab387e6cf --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetwork.java @@ -0,0 +1,74 @@ +package dan200.computercraft.api.network.wired; + +import javax.annotation.Nonnull; + +/** + * A wired network is composed of one of more {@link IWiredNode}s, a set of connections between them, and a series + * of peripherals. + * + * Networks from a connected graph. This means there is some path between all nodes on the network. Further more, if + * there is some path between two nodes then they must be on the same network. {@link IWiredNetwork} will automatically + * handle the merging and splitting of networks (and thus changing of available nodes and peripherals) as connections + * change. + * + * This does mean one can not rely on the network remaining consistent between subsequent operations. Consequently, + * it is generally preferred to use the methods provided by {@link IWiredNode}. + * + * @see IWiredNode#getNetwork() + */ +public interface IWiredNetwork +{ + /** + * Create a connection between two nodes. + * + * This should only be used on the server thread. + * + * @param left The first node to connect + * @param right The second node to connect + * @return {@code true} if a connection was created or {@code false} if the connection already exists. + * @throws IllegalStateException If neither node is on the network. + * @throws IllegalArgumentException If {@code left} and {@code right} are equal. + * @see IWiredNode#connectTo(IWiredNode) + * @see IWiredNetwork#connect(IWiredNode, IWiredNode) + */ + boolean connect( @Nonnull IWiredNode left, @Nonnull IWiredNode right ); + + /** + * Destroy a connection between this node and another. + * + * This should only be used on the server thread. + * + * @param left The first node in the connection. + * @param right The second node in the connection. + * @return {@code true} if a connection was destroyed or {@code false} if no connection exists. + * @throws IllegalArgumentException If either node is not on the network. + * @throws IllegalArgumentException If {@code left} and {@code right} are equal. + * @see IWiredNode#disconnectFrom(IWiredNode) + * @see IWiredNetwork#connect(IWiredNode, IWiredNode) + */ + boolean disconnect( @Nonnull IWiredNode left, @Nonnull IWiredNode right ); + + /** + * Sever all connections this node has, removing it from this network. + * + * This should only be used on the server thread. + * + * @param node The node to remove + * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the + * only element. + * @throws IllegalArgumentException If the node is not in the network. + * @see IWiredNode#remove() + */ + boolean remove( @Nonnull IWiredNode node ); + + /** + * Mark this node's peripherals as having changed. + * + * This should only be used on the server thread. + * + * @param node The node to mark as invalid. + * @throws IllegalArgumentException If the node is not in the network. + * @see IWiredElement#getPeripherals() + */ + void invalidate( @Nonnull IWiredNode node ); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredNetworkChange.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetworkChange.java new file mode 100644 index 0000000000..1f088b6690 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredNetworkChange.java @@ -0,0 +1,32 @@ +package dan200.computercraft.api.network.wired; + +import dan200.computercraft.api.peripheral.IPeripheral; + +import javax.annotation.Nonnull; +import java.util.Map; + +/** + * Represents a change to the objects on a wired network. + * + * @see IWiredElement#networkChanged(IWiredNetworkChange) + */ +public interface IWiredNetworkChange +{ + /** + * A set of peripherals which have been removed. Note that there may be entries with the same name + * in the added and removed set, but with a different peripheral. + * + * @return The set of removed peripherals. + */ + @Nonnull + Map peripheralsRemoved(); + + /** + * A set of peripherals which have been added. Note that there may be entries with the same name + * in the added and removed set, but with a different peripheral. + * + * @return The set of added peripherals. + */ + @Nonnull + Map peripheralsAdded(); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredNode.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredNode.java new file mode 100644 index 0000000000..076c7bc03f --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredNode.java @@ -0,0 +1,98 @@ +package dan200.computercraft.api.network.wired; + +import dan200.computercraft.api.network.IPacketNetwork; + +import javax.annotation.Nonnull; + +/** + * Wired nodes act as a layer between {@link IWiredElement}s and {@link IWiredNetwork}s. + * + * Firstly, a node acts as a packet network, capable of sending and receiving modem messages to connected nodes. These + * methods may be safely used on any thread. + * + * When sending a packet, the system will attempt to find the shortest path between the two nodes based on their + * element's position. Note that packet senders and receivers can have different locations from their associated + * element: the distance between the two will be added to the total packet's distance. + * + * Wired nodes also provide several convenience methods for interacting with a wired network. These should only ever + * be used on the main server thread. + */ +public interface IWiredNode extends IPacketNetwork +{ + /** + * The associated element for this network node. + * + * @return This node's element. + */ + @Nonnull + IWiredElement getElement(); + + /** + * The network this node is currently connected to. Note that this may change + * after any network operation, so it should not be cached. + * + * This should only be used on the server thread. + * + * @return This node's network. + */ + @Nonnull + IWiredNetwork getNetwork(); + + /** + * Create a connection from this node to another. + * + * This should only be used on the server thread. + * + * @param node The other node to connect to. + * @return {@code true} if a connection was created or {@code false} if the connection already exists. + * @see IWiredNetwork#connect(IWiredNode, IWiredNode) + * @see IWiredNode#disconnectFrom(IWiredNode) + */ + default boolean connectTo( @Nonnull IWiredNode node ) + { + return getNetwork().connect( this, node ); + } + + /** + * Destroy a connection between this node and another. + * + * This should only be used on the server thread. + * + * @param node The other node to disconnect from. + * @return {@code true} if a connection was destroyed or {@code false} if no connection exists. + * @throws IllegalArgumentException If {@code node} is not on the same network. + * @see IWiredNetwork#disconnect(IWiredNode, IWiredNode) + * @see IWiredNode#connectTo(IWiredNode) + */ + default boolean disconnectFrom( @Nonnull IWiredNode node ) + { + return getNetwork().disconnect( this, node ); + } + + /** + * Sever all connections this node has, removing it from this network. + * + * This should only be used on the server thread. + * + * @return Whether this node was removed from the network. One cannot remove a node from a network where it is the + * only element. + * @throws IllegalArgumentException If the node is not in the network. + * @see IWiredNetwork#remove(IWiredNode) + */ + default boolean remove() + { + return getNetwork().remove( this ); + } + + /** + * Mark this node's peripherals as having changed. + * + * This should only be used on the server thread. + * + * @see IWiredElement#getPeripherals() + */ + default void invalidate() + { + getNetwork().invalidate( this ); + } +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredProvider.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredProvider.java new file mode 100644 index 0000000000..10afae3e31 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredProvider.java @@ -0,0 +1,29 @@ +package dan200.computercraft.api.network.wired; + +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockAccess; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Fetch or create an {@link IWiredElement} for a block at a given position. + * + * @see dan200.computercraft.api.ComputerCraftAPI#registerWiredProvider(IWiredProvider) + * @see IWiredElementTile + */ +@FunctionalInterface +public interface IWiredProvider +{ + /** + * Extract a wired network element from a block location. + * + * @param world The world the block is in. + * @param pos The position the block is at. + * @param side The side to get the network element from. + * @return A network element, or {@code null} if there is not an element here you'd like to handle. + */ + @Nullable + IWiredElement getElement( @Nonnull IBlockAccess world, @Nonnull BlockPos pos, @Nonnull EnumFacing side ); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/IWiredSender.java b/src/main/java/dan200/computercraft/api/network/wired/IWiredSender.java new file mode 100644 index 0000000000..d29316799a --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/IWiredSender.java @@ -0,0 +1,25 @@ +package dan200.computercraft.api.network.wired; + +import dan200.computercraft.api.network.IPacketSender; + +import javax.annotation.Nonnull; + +/** + * An object on a {@link IWiredNetwork} capable of sending packets. + * + * Unlike a regular {@link IPacketSender}, this must be associated with the node you are attempting to + * to send the packet from. + */ +public interface IWiredSender extends IPacketSender +{ + /** + * The node in the network representing this object. + * + * This should be used as a proxy for the main network. One should send packets + * and register receivers through this object. + * + * @return The node for this element. + */ + @Nonnull + IWiredNode getNode(); +} diff --git a/src/main/java/dan200/computercraft/api/network/wired/package-info.java b/src/main/java/dan200/computercraft/api/network/wired/package-info.java new file mode 100644 index 0000000000..3a81850867 --- /dev/null +++ b/src/main/java/dan200/computercraft/api/network/wired/package-info.java @@ -0,0 +1,10 @@ +/* + * This file is part of the public ComputerCraft API - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2017. This API may be redistributed unmodified and in full only. + * For help using the API, and posting your mods, visit the forums at computercraft.info. + */ + +@API( owner="ComputerCraft", provides="ComputerCraft|API|Network|Wired", apiVersion="${version}" ) +package dan200.computercraft.api.network.wired; + +import net.minecraftforge.fml.common.API; diff --git a/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java b/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java index 306250936a..f3a063c22d 100644 --- a/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java +++ b/src/main/java/dan200/computercraft/api/peripheral/IComputerAccess.java @@ -13,6 +13,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; /** * The interface passed to peripherals by computers or turtles, providing methods @@ -154,4 +156,33 @@ public interface IComputerAccess */ @Nonnull String getAttachmentName(); + + /** + * Get a set of peripherals that this computer access can "see", along with their attachment name. + * + * This may include other peripherals on the wired network or peripherals on other sides of the computer. + * + * @return All reachable peripherals + * @see #getAttachmentName() + * @see #getAvailablePeripheral(String) + */ + @Nonnull + default Map getAvailablePeripherals() + { + return Collections.emptyMap(); + } + + /** + * Get a reachable peripheral with the given attachement name. This is a equivalent to + * {@link #getAvailablePeripherals()}{@code .get(name)}, though may be more performant. + * + * @param name The peripheral's attached name + * @return The reachable peripheral, or {@code null} if none can be found. + * @see #getAvailablePeripherals() + */ + @Nullable + default IPeripheral getAvailablePeripheral( @Nonnull String name ) + { + return null; + } } diff --git a/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java b/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java index af2ecc9648..8c44424e2a 100644 --- a/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java +++ b/src/main/java/dan200/computercraft/api/peripheral/IPeripheral.java @@ -114,6 +114,18 @@ default void detach( @Nonnull IComputerAccess computer ) { } + /** + * Get the object that this peripheral provides methods for. This will generally be the tile entity + * or block, but may be an inventory, entity, etc... + * + * @return The object this peripheral targets + */ + @Nonnull + default Object getTarget() + { + return this; + } + /** * Determine whether this peripheral is equivalent to another one. * diff --git a/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java b/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java index 7ce9e97033..305f0bcacb 100644 --- a/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java +++ b/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java @@ -115,6 +115,7 @@ public ModelResourceLocation getModelLocation( @Nonnull ItemStack stack ) registerItemModel( ComputerCraft.Blocks.commandComputer, "command_computer" ); registerItemModel( ComputerCraft.Blocks.advancedModem, "advanced_modem" ); registerItemModel( ComputerCraft.Blocks.peripheral, 5, "speaker" ); + registerItemModel( ComputerCraft.Blocks.wiredModemFull, "wired_modem_full" ); registerItemModel( ComputerCraft.Items.disk, "disk" ); registerItemModel( ComputerCraft.Items.diskExpanded, "disk_expanded" ); diff --git a/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java b/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java index 5ee9e430fe..138ad3fffe 100644 --- a/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java +++ b/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java @@ -54,8 +54,6 @@ public void drawHighlight( DrawBlockHighlightEvent event ) GlStateManager.depthMask( false ); GlStateManager.pushMatrix(); - EnumFacing direction = type != PeripheralType.Cable ? cable.getDirection() : null; - { EntityPlayer player = event.getPlayer(); double x = player.lastTickPosX + (player.posX - player.lastTickPosX) * event.getPartialTicks(); @@ -78,7 +76,7 @@ public void drawHighlight( DrawBlockHighlightEvent event ) for( EnumFacing facing : EnumFacing.VALUES ) { - if( direction == facing || BlockCable.isCable( world, pos.offset( facing ) ) ) + if( BlockCable.doesConnectVisually( state, world, pos, facing ) ) { flags |= 1 << facing.ordinal(); diff --git a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java index d22328ea62..e5d71f7580 100644 --- a/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java +++ b/src/main/java/dan200/computercraft/core/apis/PeripheralAPI.java @@ -15,10 +15,11 @@ import dan200.computercraft.core.computer.Computer; import dan200.computercraft.core.computer.ComputerThread; import dan200.computercraft.core.computer.ITask; -import dan200.computercraft.core.filesystem.FileSystem; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -180,10 +181,49 @@ public String getAttachmentName() } return m_side; } + + @Nonnull + @Override + public Map getAvailablePeripherals() + { + if( !m_attached ) + { + throw new RuntimeException( "You are not attached to this Computer" ); + } + + Map peripherals = new HashMap<>(); + for( PeripheralWrapper wrapper : m_peripherals ) + { + if( wrapper != null && wrapper.isAttached() ) + { + peripherals.put( wrapper.getAttachmentName(), wrapper.getPeripheral() ); + } + } + + return Collections.unmodifiableMap( peripherals ); + } + + @Nullable + @Override + public IPeripheral getAvailablePeripheral( @Nonnull String name ) + { + if( !m_attached ) + { + throw new RuntimeException( "You are not attached to this Computer" ); + } + + for( PeripheralWrapper wrapper : m_peripherals ) + { + if( wrapper != null && wrapper.isAttached() && wrapper.getAttachmentName().equals( name ) ) + { + return wrapper.getPeripheral(); + } + } + return null; + } } private final IAPIEnvironment m_environment; - private FileSystem m_fileSystem; private final PeripheralWrapper[] m_peripherals; private boolean m_running; @@ -285,7 +325,6 @@ public void startup( ) { synchronized( m_peripherals ) { - m_fileSystem = m_environment.getFileSystem(); m_running = true; for( int i=0; i<6; ++i ) { @@ -312,7 +351,6 @@ public void shutdown( ) wrapper.detach(); } } - m_fileSystem = null; } } diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java index 3b5469c1bc..b401845a34 100644 --- a/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/computer/blocks/ComputerPeripheral.java @@ -97,4 +97,11 @@ public boolean equals( IPeripheral other ) { return (other != null && other.getClass() == this.getClass()); } + + @Nonnull + @Override + public Object getTarget() + { + return m_computer.getTile(); + } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/PeripheralType.java b/src/main/java/dan200/computercraft/shared/peripheral/PeripheralType.java index 2cfaaf0703..b209a60c2b 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/PeripheralType.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/PeripheralType.java @@ -21,7 +21,8 @@ public enum PeripheralType implements IStringSerializable Cable( "cable" ), WiredModemWithCable( "wired_modem_with_cable" ), AdvancedModem( "advanced_modem" ), - Speaker( "speaker" ); + Speaker( "speaker" ), + WiredModemFull( "wired_modem_full" ); private String m_name; diff --git a/src/main/java/dan200/computercraft/shared/peripheral/common/BlockCable.java b/src/main/java/dan200/computercraft/shared/peripheral/common/BlockCable.java index 801f001cbc..f0e04a4db9 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/common/BlockCable.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/common/BlockCable.java @@ -11,7 +11,6 @@ import dan200.computercraft.shared.peripheral.PeripheralType; import dan200.computercraft.shared.peripheral.modem.TileCable; import dan200.computercraft.shared.util.WorldUtil; -import net.minecraft.block.Block; import net.minecraft.block.properties.PropertyBool; import net.minecraft.block.properties.PropertyEnum; import net.minecraft.block.state.BlockFaceShape; @@ -51,23 +50,6 @@ public static class Properties public static final PropertyBool DOWN = PropertyBool.create( "down" ); } - public static boolean isCable( IBlockAccess world, BlockPos pos ) - { - Block block = world.getBlockState( pos ).getBlock(); - if( block == ComputerCraft.Blocks.cable ) - { - switch( ComputerCraft.Blocks.cable.getPeripheralType( world, pos ) ) - { - case Cable: - case WiredModemWithCable: - { - return true; - } - } - } - return false; - } - // Members public BlockCable() @@ -175,20 +157,17 @@ public IBlockState getDefaultBlockState( PeripheralType type, EnumFacing placedS } } - private boolean doesConnect( IBlockState state, IBlockAccess world, BlockPos pos, EnumFacing dir ) + public static boolean canConnectIn( IBlockState state, EnumFacing direction ) { - if( state.getValue( Properties.CABLE ) == BlockCableCableVariant.NONE ) - { - return false; - } - else if( state.getValue( Properties.MODEM ).getFacing() == dir ) - { - return true; - } - else - { - return isCable( world, pos.offset( dir ) ); - } + return state.getValue( BlockCable.Properties.CABLE ) != BlockCableCableVariant.NONE + && state.getValue( BlockCable.Properties.MODEM ).getFacing() != direction; + } + + public static boolean doesConnectVisually( IBlockState state, IBlockAccess world, BlockPos pos, EnumFacing direction ) + { + if( state.getValue( Properties.CABLE ) == BlockCableCableVariant.NONE ) return false; + if( state.getValue( Properties.MODEM ).getFacing() == direction ) return true; + return ComputerCraft.getWiredElementAt( world, pos.offset( direction ), direction.getOpposite() ) != null; } @Nonnull @@ -196,12 +175,12 @@ else if( state.getValue( Properties.MODEM ).getFacing() == dir ) @Deprecated public IBlockState getActualState( @Nonnull IBlockState state, IBlockAccess world, BlockPos pos ) { - state = state.withProperty( Properties.NORTH, doesConnect( state, world, pos, EnumFacing.NORTH ) ); - state = state.withProperty( Properties.SOUTH, doesConnect( state, world, pos, EnumFacing.SOUTH ) ); - state = state.withProperty( Properties.EAST, doesConnect( state, world, pos, EnumFacing.EAST ) ); - state = state.withProperty( Properties.WEST, doesConnect( state, world, pos, EnumFacing.WEST ) ); - state = state.withProperty( Properties.UP, doesConnect( state, world, pos, EnumFacing.UP ) ); - state = state.withProperty( Properties.DOWN, doesConnect( state, world, pos, EnumFacing.DOWN ) ); + state = state.withProperty( Properties.NORTH, doesConnectVisually( state, world, pos, EnumFacing.NORTH ) ); + state = state.withProperty( Properties.SOUTH, doesConnectVisually( state, world, pos, EnumFacing.SOUTH ) ); + state = state.withProperty( Properties.EAST, doesConnectVisually( state, world, pos, EnumFacing.EAST ) ); + state = state.withProperty( Properties.WEST, doesConnectVisually( state, world, pos, EnumFacing.WEST ) ); + state = state.withProperty( Properties.UP, doesConnectVisually( state, world, pos, EnumFacing.UP ) ); + state = state.withProperty( Properties.DOWN, doesConnectVisually( state, world, pos, EnumFacing.DOWN ) ); if( state.getValue( Properties.CABLE ) != BlockCableCableVariant.NONE ) { @@ -345,7 +324,6 @@ public boolean removedByPlayer( @Nonnull IBlockState state, World world, @Nonnul if( WorldUtil.isVecInsideInclusive( bb, hit.hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) ) ) { world.setBlockState( pos, state.withProperty( Properties.MODEM, BlockCableModemVariant.None ), 3 ); - cable.modemChanged(); item = PeripheralItemFactory.create( PeripheralType.WiredModem, null, 1 ); } else @@ -365,6 +343,7 @@ public boolean removedByPlayer( @Nonnull IBlockState state, World world, @Nonnul return super.removedByPlayer( state, world, pos, player, willHarvest ); } + @Nonnull @Override public ItemStack getPickBlock( @Nonnull IBlockState state, RayTraceResult hit, @Nonnull World world, @Nonnull BlockPos pos, EntityPlayer player ) { @@ -373,7 +352,7 @@ public ItemStack getPickBlock( @Nonnull IBlockState state, RayTraceResult hit, @ { TileCable cable = (TileCable) tile; PeripheralType type = getPeripheralType( state ); - + if( type == PeripheralType.WiredModemWithCable ) { if( hit == null || WorldUtil.isVecInsideInclusive( cable.getModemBounds(), hit.hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) ) ) diff --git a/src/main/java/dan200/computercraft/shared/peripheral/common/BlockWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/common/BlockWiredModemFull.java new file mode 100644 index 0000000000..6936b12ca2 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/common/BlockWiredModemFull.java @@ -0,0 +1,102 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2017. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.common; + +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.shared.peripheral.PeripheralType; +import dan200.computercraft.shared.peripheral.modem.TileWiredModemFull; +import net.minecraft.block.properties.PropertyBool; +import net.minecraft.block.state.BlockStateContainer; +import net.minecraft.block.state.IBlockState; +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; + +public class BlockWiredModemFull extends BlockPeripheralBase +{ + // Statics + + public static class Properties + { + public static final PropertyBool MODEM_ON = PropertyBool.create( "modem" ); + public static final PropertyBool PERIPHERAL_ON = PropertyBool.create( "peripheral" ); + } + + // Members + + public BlockWiredModemFull() + { + setHardness( 1.5f ); + setUnlocalizedName( "computercraft:wired_modem_full" ); + setCreativeTab( ComputerCraft.mainCreativeTab ); + setDefaultState( blockState.getBaseState() + .withProperty( Properties.MODEM_ON, false ) + .withProperty( Properties.PERIPHERAL_ON, false ) + ); + } + + @Override + protected IBlockState getDefaultBlockState( PeripheralType type, EnumFacing placedSide ) + { + return getDefaultState(); + } + + @Nonnull + @Override + protected BlockStateContainer createBlockState() + { + return new BlockStateContainer( this, + Properties.MODEM_ON, + Properties.PERIPHERAL_ON + ); + } + + @Override + public int getMetaFromState( IBlockState state ) + { + return 0; + } + + @Nonnull + @Override + @Deprecated + public IBlockState getActualState( @Nonnull IBlockState state, IBlockAccess world, BlockPos pos ) + { + TileEntity te = world.getTileEntity( pos ); + if( te instanceof TileWiredModemFull ) + { + TileWiredModemFull modem = (TileWiredModemFull) te; + int anim = modem.getAnim(); + state = state + .withProperty( Properties.MODEM_ON, (anim & 1) != 0 ) + .withProperty( Properties.PERIPHERAL_ON, (anim & 2) != 0 ); + } + + return state; + } + + @Override + public PeripheralType getPeripheralType( int damage ) + { + return PeripheralType.WiredModemFull; + } + + @Override + public PeripheralType getPeripheralType( IBlockState state ) + { + return PeripheralType.WiredModemFull; + } + + @Override + public TilePeripheralBase createTile( PeripheralType type ) + { + return new TileWiredModemFull(); + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/common/ItemPeripheralBase.java b/src/main/java/dan200/computercraft/shared/peripheral/common/ItemPeripheralBase.java index cd39a1b0f5..a08bbd5b03 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/common/ItemPeripheralBase.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/common/ItemPeripheralBase.java @@ -11,8 +11,8 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemBlock; import net.minecraft.item.ItemStack; -import net.minecraft.util.math.BlockPos; import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import javax.annotation.Nonnull; @@ -102,6 +102,8 @@ public String getUnlocalizedName( @Nonnull ItemStack stack ) { return "tile.computercraft:speaker"; } + case WiredModemFull: + return "tile.computercraft:wired_modem"; } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/common/ItemWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/common/ItemWiredModemFull.java new file mode 100644 index 0000000000..11e8a399df --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/common/ItemWiredModemFull.java @@ -0,0 +1,18 @@ +package dan200.computercraft.shared.peripheral.common; + +import dan200.computercraft.shared.peripheral.PeripheralType; +import net.minecraft.block.Block; + +public class ItemWiredModemFull extends ItemPeripheralBase +{ + public ItemWiredModemFull( Block block ) + { + super( block ); + } + + @Override + public PeripheralType getPeripheralType( int damage ) + { + return PeripheralType.WiredModemFull; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/common/PeripheralItemFactory.java b/src/main/java/dan200/computercraft/shared/peripheral/common/PeripheralItemFactory.java index 1585521ff2..a9ea01310c 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/common/PeripheralItemFactory.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/common/PeripheralItemFactory.java @@ -47,6 +47,8 @@ public static ItemStack create( PeripheralType type, String label, int quantity { return advancedModem.create( type, label, quantity ); } + case WiredModemFull: + return new ItemStack( ComputerCraft.Blocks.wiredModemFull, quantity ); } return ItemStack.EMPTY; } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java index 59ffea4bfc..33459ee6e6 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDrivePeripheral.java @@ -196,4 +196,11 @@ public boolean equals( IPeripheral other ) } return false; } + + @Nonnull + @Override + public Object getTarget() + { + return m_diskDrive; + } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/TileCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/TileCable.java index c7aa49efb8..8bad648485 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/modem/TileCable.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/TileCable.java @@ -8,14 +8,8 @@ import com.google.common.base.Objects; import dan200.computercraft.ComputerCraft; -import dan200.computercraft.api.filesystem.IMount; -import dan200.computercraft.api.filesystem.IWritableMount; -import dan200.computercraft.api.lua.ILuaContext; -import dan200.computercraft.api.lua.LuaException; -import dan200.computercraft.api.network.IPacketNetwork; -import dan200.computercraft.api.network.IPacketReceiver; -import dan200.computercraft.api.network.Packet; -import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; import dan200.computercraft.api.peripheral.IPeripheral; import dan200.computercraft.shared.common.BlockGeneric; import dan200.computercraft.shared.peripheral.PeripheralType; @@ -24,11 +18,12 @@ import dan200.computercraft.shared.peripheral.common.PeripheralItemFactory; import dan200.computercraft.shared.util.IDAssigner; import dan200.computercraft.shared.util.PeripheralUtil; +import dan200.computercraft.api.network.wired.IWiredElementTile; +import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.tileentity.TileEntity; import net.minecraft.util.EnumFacing; import net.minecraft.util.NonNullList; import net.minecraft.util.math.AxisAlignedBB; @@ -38,13 +33,13 @@ import net.minecraft.world.World; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.File; -import java.util.*; +import java.util.Collections; +import java.util.List; +import java.util.Map; -import static dan200.computercraft.core.apis.ArgumentHelper.getString; - -public class TileCable extends TileModemBase - implements IPacketNetwork +public class TileCable extends TileModemBase implements IWiredElementTile { public static final double MIN = 0.375; public static final double MAX = 1 - MIN; @@ -59,33 +54,13 @@ public class TileCable extends TileModemBase new AxisAlignedBB( MAX, MIN, MIN, 1, MAX, MAX ), // East }; - // Statics - - private static class Peripheral extends ModemPeripheral + private static class CableElement extends WiredModemElement { - private TileCable m_entity; - - public Peripheral( TileCable entity ) - { - m_entity = entity; - } - - @Override - public boolean isInterdimensional() - { - return false; - } - - @Override - public double getRange() - { - return 256.0; - } + private final TileCable m_entity; - @Override - protected IPacketNetwork getNetwork() + private CableElement( TileCable m_entity ) { - return m_entity; + this.m_entity = m_entity; } @Nonnull @@ -99,168 +74,81 @@ public World getWorld() @Override public Vec3d getPosition() { - EnumFacing direction = m_entity.getCachedDirection(); - BlockPos pos = m_entity.getPos().offset( direction ); - return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + BlockPos pos = m_entity.getPos(); + return new Vec3d( (double) pos.getX() + 0.5, (double) pos.getY() + 0.5, (double) pos.getZ() + 0.5 ); } @Nonnull @Override - public String[] getMethodNames() - { - String[] methods = super.getMethodNames(); - String[] newMethods = new String[ methods.length + 5 ]; - System.arraycopy( methods, 0, newMethods, 0, methods.length ); - newMethods[ methods.length ] = "getNamesRemote"; - newMethods[ methods.length + 1 ] = "isPresentRemote"; - newMethods[ methods.length + 2 ] = "getTypeRemote"; - newMethods[ methods.length + 3 ] = "getMethodsRemote"; - newMethods[ methods.length + 4 ] = "callRemote"; - return newMethods; - } - - @Override - public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException - { - String[] methods = super.getMethodNames(); - switch( method - methods.length ) - { - case 0: - { - // getNamesRemote - synchronized( m_entity.m_peripheralsByName ) - { - int idx = 1; - Map table = new HashMap<>(); - for( String name : m_entity.m_peripheralWrappersByName.keySet() ) - { - table.put( idx++, name ); - } - return new Object[] { table }; - } - } - case 1: - { - // isPresentRemote - String type = m_entity.getTypeRemote( getString( arguments, 0 ) ); - return new Object[] { type != null }; - } - case 2: - { - // getTypeRemote - String type = m_entity.getTypeRemote( getString( arguments, 0 ) ); - if( type != null ) - { - return new Object[] { type }; - } - return null; - } - case 3: - { - // getMethodsRemote - String[] methodNames = m_entity.getMethodNamesRemote( getString( arguments, 0 ) ); - if( methodNames != null ) - { - Map table = new HashMap<>(); - for(int i=0; i getPeripherals() { - super.attach( computer ); - synchronized( m_entity.m_peripheralsByName ) - { - for (String periphName : m_entity.m_peripheralsByName.keySet()) - { - IPeripheral peripheral = m_entity.m_peripheralsByName.get( periphName ); - if( peripheral != null ) - { - m_entity.attachPeripheral( periphName, peripheral ); - } - } - } + IPeripheral peripheral = m_entity.getConnectedPeripheral(); + return peripheral != null + ? Collections.singletonMap( m_entity.getConnectedPeripheralName(), peripheral ) + : Collections.emptyMap(); } @Override - public synchronized void detach( @Nonnull IComputerAccess computer ) + protected void attachPeripheral( String name, IPeripheral peripheral ) { - synchronized( m_entity.m_peripheralsByName ) + if( !name.equals( m_entity.getConnectedPeripheralName() ) ) { - for (String periphName : m_entity.m_peripheralsByName.keySet()) - { - m_entity.detachPeripheral( periphName ); - } + ((WiredModemPeripheral) m_entity.m_modem).attachPeripheral( name, peripheral ); } - super.detach( computer ); } @Override - public boolean equals( IPeripheral other ) + protected void detachPeripheral( String name ) { - if( other instanceof Peripheral ) - { - Peripheral otherModem = (Peripheral)other; - return otherModem.m_entity == m_entity; - } - return false; + ((WiredModemPeripheral) m_entity.m_modem).detachPeripheral( name ); } } - private static int s_nextUniqueSearchID = 1; - // Members - private final Set m_receivers; - private final Queue m_transmitQueue; - private boolean m_peripheralAccessAllowed; private int m_attachedPeripheralID; - - private final Map m_peripheralsByName; - private Map m_peripheralWrappersByName; - private boolean m_peripheralsKnown; + private boolean m_destroyed; - - private int m_lastSearchID; private boolean m_hasDirection = false; - + private boolean m_connectionsFormed = false; + + private WiredModemElement m_cable; + private IWiredNode m_node; + public TileCable() { - m_receivers = new HashSet<>(); - m_transmitQueue = new LinkedList<>(); - m_peripheralAccessAllowed = false; m_attachedPeripheralID = -1; - - m_peripheralsByName = new HashMap<>(); - m_peripheralWrappersByName = new HashMap<>(); - m_peripheralsKnown = false; + m_destroyed = false; - - m_lastSearchID = 0; + } + + @Override + protected ModemPeripheral createPeripheral() + { + m_cable = new CableElement( this ); + m_node = m_cable.getNode(); + return new WiredModemPeripheral( m_cable ) + { + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = getPos().offset( getCachedDirection() ); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + }; + } + + private void remove() + { + if( world == null || !world.isRemote ) + { + m_node.remove(); + m_connectionsFormed = false; + } } @Override @@ -269,11 +157,25 @@ public void destroy() if( !m_destroyed ) { m_destroyed = true; - networkChanged(); + remove(); } super.destroy(); } + @Override + public void onChunkUnload() + { + super.onChunkUnload(); + remove(); + } + + @Override + public void invalidate() + { + super.invalidate(); + remove(); + } + @Override public void onLoad() { @@ -373,17 +275,20 @@ public void onNeighbourChange() case WiredModem: { // Drop everything and remove block - ((BlockGeneric)getBlockType()).dropAllItems( getWorld(), getPos(), false ); + ((BlockGeneric) getBlockType()).dropAllItems( getWorld(), getPos(), false ); getWorld().setBlockToAir( getPos() ); - break; + + // This'll call #destroy(), so we don't need to reset the network here. + return; } case WiredModemWithCable: { // Drop the modem and convert to cable - ((BlockGeneric)getBlockType()).dropItem( getWorld(), getPos(), PeripheralItemFactory.create( PeripheralType.WiredModem, getLabel(), 1 ) ); + ((BlockGeneric) getBlockType()).dropItem( getWorld(), getPos(), PeripheralItemFactory.create( PeripheralType.WiredModem, getLabel(), 1 ) ); setLabel( null ); setBlockState( getBlockState().withProperty( BlockCable.Properties.MODEM, BlockCableModemVariant.None ) ); - if( modemChanged() ) networkChanged(); + networkChanged(); + break; } } @@ -392,10 +297,10 @@ public void onNeighbourChange() public AxisAlignedBB getModemBounds() { - return super.getBounds(); + return super.getBounds(); } - - public AxisAlignedBB getCableBounds() + + private AxisAlignedBB getCableBounds() { double xMin = 0.375; double yMin = 0.375; @@ -405,33 +310,35 @@ public AxisAlignedBB getCableBounds() double zMax = 0.625; BlockPos pos = getPos(); World world = getWorld(); - if( BlockCable.isCable( world, pos.west() ) ) + + IBlockState state = getBlockState(); + if( BlockCable.doesConnectVisually( state, world, pos, EnumFacing.WEST ) ) { xMin = 0.0; } - if( BlockCable.isCable( world, pos.east() ) ) + if( BlockCable.doesConnectVisually( state, world, pos, EnumFacing.EAST ) ) { xMax = 1.0; } - if( BlockCable.isCable( world, pos.down() ) ) + if( BlockCable.doesConnectVisually( state, world, pos, EnumFacing.DOWN ) ) { yMin = 0.0; } - if( BlockCable.isCable( world, pos.up() ) ) + if( BlockCable.doesConnectVisually( state, world, pos, EnumFacing.UP ) ) { yMax = 1.0; } - if( BlockCable.isCable( world, pos.north() ) ) + if( BlockCable.doesConnectVisually( state, world, pos, EnumFacing.NORTH ) ) { zMin = 0.0; } - if( BlockCable.isCable( world, pos.south() ) ) + if( BlockCable.doesConnectVisually( state, world, pos, EnumFacing.SOUTH ) ) { zMax = 1.0; } return new AxisAlignedBB( xMin, yMin, zMin, xMax, yMax, zMax ); } - + @Nonnull @Override public AxisAlignedBB getBounds() @@ -468,12 +375,13 @@ public void getCollisionBounds( @Nonnull List bounds ) if( type == PeripheralType.Cable || type == PeripheralType.WiredModemWithCable ) { bounds.add( BOX_CENTRE ); - BlockPos pos = getPos(); - for (EnumFacing facing : EnumFacing.VALUES) + + IBlockState state = getBlockState(); + for( EnumFacing facing : EnumFacing.VALUES ) { - if( BlockCable.isCable( getWorld(), pos.offset( facing ) ) ) + if( BlockCable.doesConnectVisually( state, world, pos, facing ) ) { - bounds.add( BOXES[ facing.ordinal() ] ); + bounds.add( BOXES[facing.ordinal()] ); } } } @@ -519,30 +427,24 @@ public boolean onActivate( EntityPlayer player, EnumFacing side, float hitX, flo } @Override - public void readFromNBT(NBTTagCompound nbttagcompound) + public void readFromNBT( NBTTagCompound nbttagcompound ) { // Read properties - super.readFromNBT(nbttagcompound); + super.readFromNBT( nbttagcompound ); m_peripheralAccessAllowed = nbttagcompound.getBoolean( "peripheralAccess" ); m_attachedPeripheralID = nbttagcompound.getInteger( "peripheralID" ); } @Nonnull @Override - public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) + public NBTTagCompound writeToNBT( NBTTagCompound nbttagcompound ) { // Write properties - nbttagcompound = super.writeToNBT(nbttagcompound); + nbttagcompound = super.writeToNBT( nbttagcompound ); nbttagcompound.setBoolean( "peripheralAccess", m_peripheralAccessAllowed ); nbttagcompound.setInteger( "peripheralID", m_attachedPeripheralID ); return nbttagcompound; } - - @Override - protected ModemPeripheral createPeripheral() - { - return new Peripheral( this ); - } @Override protected void updateAnim() @@ -559,190 +461,55 @@ protected void updateAnim() setAnim( anim ); } - // IPeripheralTile - - @Override - public IPeripheral getPeripheral( EnumFacing side ) - { - if( getPeripheralType() != PeripheralType.Cable ) - { - return super.getPeripheral( side ); - } - return null; - } - @Override public void update() { super.update(); updateDirection(); if( !getWorld().isRemote ) - { - synchronized( m_peripheralsByName ) + { + if( !m_connectionsFormed ) { - if( !m_peripheralsKnown ) - { - findPeripherals(); - m_peripheralsKnown = true; - } + networkChanged(); + if( m_peripheralAccessAllowed ) m_node.invalidate(); + m_connectionsFormed = true; } - synchronized( m_transmitQueue ) - { - while( m_transmitQueue.peek() != null ) - { - PacketWrapper p = m_transmitQueue.remove(); - if( p != null ) - { - dispatchPacket( p ); - } - } - } - } - } - - // IPacketNetwork implementation - - @Override - public void addReceiver( @Nonnull IPacketReceiver receiver ) - { - synchronized( m_receivers ) - { - m_receivers.add( receiver ); - } - } - - @Override - public void removeReceiver( @Nonnull IPacketReceiver receiver ) - { - synchronized( m_receivers ) - { - m_receivers.remove( receiver ); - } - } - - @Override - public void transmitSameDimension( @Nonnull Packet packet, double range ) - { - synchronized( m_transmitQueue ) - { - m_transmitQueue.offer( new PacketWrapper( packet, range ) ); - } - } - - @Override - public void transmitInterdimensional( @Nonnull Packet packet ) - { - synchronized( m_transmitQueue ) - { - m_transmitQueue.offer( new PacketWrapper( packet, Double.MAX_VALUE ) ); } } - @Override - public boolean isWireless() - { - return false; - } - - private void attachPeripheral( String periphName, IPeripheral peripheral ) + public void networkChanged() { - if( !m_peripheralWrappersByName.containsKey( periphName ) ) - { - RemotePeripheralWrapper wrapper = new RemotePeripheralWrapper( peripheral, m_modem.getComputer(), periphName ); - m_peripheralWrappersByName.put( periphName, wrapper ); - wrapper.attach(); - } - } + if( getWorld().isRemote ) return; - private void detachPeripheral( String periphName ) - { - if( m_peripheralWrappersByName.containsKey( periphName ) ) - { - RemotePeripheralWrapper wrapper = m_peripheralWrappersByName.get( periphName ); - m_peripheralWrappersByName.remove( periphName ); - wrapper.detach(); - } - } + if( modemChanged() ) m_node.invalidate(); - private String getTypeRemote( String remoteName ) - { - synchronized( m_peripheralsByName ) - { - RemotePeripheralWrapper wrapper = m_peripheralWrappersByName.get( remoteName ); - if( wrapper != null ) - { - return wrapper.getType(); - } - } - return null; - } - - private String[] getMethodNamesRemote( String remoteName ) - { - synchronized( m_peripheralsByName ) - { - RemotePeripheralWrapper wrapper = m_peripheralWrappersByName.get( remoteName ); - if( wrapper != null ) - { - return wrapper.getMethodNames(); - } - } - return null; - } - - private Object[] callMethodRemote( String remoteName, ILuaContext context, String method, Object[] arguments ) throws LuaException, InterruptedException - { - RemotePeripheralWrapper wrapper; - synchronized( m_peripheralsByName ) - { - wrapper = m_peripheralWrappersByName.get( remoteName ); - } - if( wrapper != null ) + IBlockState state = getBlockState(); + World world = getWorld(); + BlockPos current = getPos(); + for( EnumFacing facing : EnumFacing.VALUES ) { - return wrapper.callMethod( context, method, arguments ); - } - throw new LuaException( "No peripheral: "+remoteName ); - } + if( !world.isBlockLoaded( pos ) ) continue; + + IWiredElement element = ComputerCraft.getWiredElementAt( world, current.offset( facing ), facing.getOpposite() ); + if( element == null ) continue; - public void networkChanged() - { - if( !getWorld().isRemote ) - { - if( !m_destroyed && getPeripheralType() != PeripheralType.WiredModem) + if( BlockCable.canConnectIn( state, facing ) ) { - // If this modem is alive, rebuild the network - searchNetwork( ( modem, distance ) -> - { - synchronized( modem.m_peripheralsByName ) - { - modem.m_peripheralsKnown = false; - } - } ); + // If we can connect to it then do so + m_node.connectTo( element.getNode() ); } - else + else if( m_node.getNetwork() == element.getNode().getNetwork() ) { - // If this modem is dead, rebuild the neighbours' networks - for( EnumFacing dir : EnumFacing.values() ) - { - BlockPos offset = getPos().offset( dir ); - if( offset.getY() >= 0 && offset.getY() < getWorld().getHeight() && BlockCable.isCable( getWorld(), offset ) ) - { - TileEntity tile = getWorld().getTileEntity( offset ); - if( tile != null && tile instanceof TileCable ) - { - TileCable modem = (TileCable)tile; - modem.networkChanged(); - } - } - } + // Otherwise if we're on the same network then attempt to void it. + m_node.disconnectFrom( element.getNode() ); } } } - public boolean modemChanged() + private boolean modemChanged() { if( getWorld().isRemote ) return false; - + boolean requiresUpdate = false; PeripheralType type = getPeripheralType(); @@ -758,220 +525,12 @@ public boolean modemChanged() markDirty(); updateAnim(); } - - return requiresUpdate; - } - - // private stuff - - // Packet sending - - private static class PacketWrapper - { - final Packet m_packet; - final double m_range; - - private PacketWrapper( Packet m_packet, double m_range ) - { - this.m_packet = m_packet; - this.m_range = m_range; - } - } - - private void dispatchPacket( final PacketWrapper packet ) - { - searchNetwork( ( modem, distance ) -> - { - if( distance <= packet.m_range) - { - modem.receivePacket( packet.m_packet, distance ); - } - } ); - } - - private void receivePacket( Packet packet, int distanceTravelled ) - { - synchronized( m_receivers ) - { - for (IPacketReceiver device : m_receivers) - { - device.receiveSameDimension( packet, distanceTravelled ); - } - } - } - - // Remote peripheral control - - private static class RemotePeripheralWrapper implements IComputerAccess - { - private IPeripheral m_peripheral; - private IComputerAccess m_computer; - private String m_name; - - private String m_type; - private String[] m_methods; - private Map m_methodMap; - - public RemotePeripheralWrapper( IPeripheral peripheral, IComputerAccess computer, String name ) - { - m_peripheral = peripheral; - m_computer = computer; - m_name = name; - - m_type = peripheral.getType(); - m_methods = peripheral.getMethodNames(); - assert( m_type != null ); - assert( m_methods != null ); - - m_methodMap = new HashMap<>(); - for( int i=0; i newPeripheralsByName = new HashMap<>(); - if( getPeripheralType() == PeripheralType.WiredModemWithCable ) - { - searchNetwork( ( modem, distance ) -> - { - if( modem != origin ) - { - IPeripheral peripheral = modem.getConnectedPeripheral(); - String periphName = modem.getConnectedPeripheralName(); - if( peripheral != null && periphName != null ) - { - newPeripheralsByName.put( periphName, peripheral ); - } - } - } ); - } - //System.out.println( newPeripheralsByName.size()+" peripherals discovered" ); - - // Detach all the old peripherals - Iterator it = m_peripheralsByName.keySet().iterator(); - while( it.hasNext() ) - { - String periphName = it.next(); - if( !newPeripheralsByName.containsKey( periphName ) ) - { - detachPeripheral( periphName ); - it.remove(); - } - } - - // Attach all the new peripherals - for( String periphName : newPeripheralsByName.keySet() ) - { - if( !m_peripheralsByName.containsKey( periphName ) ) - { - IPeripheral peripheral = newPeripheralsByName.get( periphName ); - if( peripheral != null ) - { - m_peripheralsByName.put( periphName, peripheral ); - if( isAttached() ) - { - attachPeripheral( periphName, peripheral ); - } - } - } - } - //System.out.println( m_peripheralsByName.size()+" connected" ); - } - } - - public void togglePeripheralAccess() + // private stuff + private void togglePeripheralAccess() { if( !m_peripheralAccessAllowed ) { @@ -986,11 +545,12 @@ public void togglePeripheralAccess() { m_peripheralAccessAllowed = false; } - updateAnim(); - networkChanged(); + + updateAnim(); + m_node.invalidate(); } - - public String getConnectedPeripheralName() + + private String getConnectedPeripheralName() { IPeripheral periph = getConnectedPeripheral(); if( periph != null ) @@ -998,16 +558,16 @@ public String getConnectedPeripheralName() String type = periph.getType(); if( m_attachedPeripheralID < 0 ) { - m_attachedPeripheralID = IDAssigner.getNextIDFromFile(new File( - ComputerCraft.getWorldDir(getWorld()), + m_attachedPeripheralID = IDAssigner.getNextIDFromFile( new File( + ComputerCraft.getWorldDir( getWorld() ), "computer/lastid_" + type + ".txt" - )); + ) ); } return type + "_" + m_attachedPeripheralID; } return null; } - + private IPeripheral getConnectedPeripheral() { if( m_peripheralAccessAllowed ) @@ -1016,84 +576,45 @@ private IPeripheral getConnectedPeripheral() { EnumFacing facing = getDirection(); BlockPos neighbour = getPos().offset( facing ); - return PeripheralUtil.getPeripheral( getWorld(), neighbour, facing.getOpposite() ); + IPeripheral peripheral = getPeripheral( getWorld(), neighbour, facing.getOpposite() ); + return peripheral == null || peripheral instanceof WiredModemPeripheral ? null : peripheral; } } return null; } - - // Generic network search stuff - - private interface ICableVisitor - { - void visit( TileCable modem, int distance ); - } - - private static class SearchLoc - { - public World world; - public BlockPos pos; - public int distanceTravelled; - } - - private static void enqueue( Queue queue, World world, BlockPos pos, int distanceTravelled ) + + public static IPeripheral getPeripheral( World world, BlockPos pos, EnumFacing facing ) { - int y = pos.getY(); - if( y >= 0 && y < world.getHeight() && BlockCable.isCable( world, pos ) ) - { - SearchLoc loc = new SearchLoc(); - loc.world = world; - loc.pos = pos; - loc.distanceTravelled = distanceTravelled; - queue.offer( loc ); - } + Block block = world.getBlockState( pos ).getBlock(); + if( block == ComputerCraft.Blocks.wiredModemFull || block == ComputerCraft.Blocks.cable ) return null; + + return PeripheralUtil.getPeripheral( world, pos, facing ); } - - private static void visitBlock( Queue queue, SearchLoc location, int searchID, ICableVisitor visitor ) + + @Override + public boolean canRenderBreaking() { - if( location.distanceTravelled >= 256 ) - { - return; - } - - TileEntity tile = location.world.getTileEntity( location.pos ); - if( tile != null && tile instanceof TileCable ) - { - TileCable modem = (TileCable)tile; - if( !modem.m_destroyed && modem.m_lastSearchID != searchID ) - { - modem.m_lastSearchID = searchID; - visitor.visit( modem, location.distanceTravelled + 1 ); - - enqueue( queue, location.world, location.pos.up(), location.distanceTravelled + 1 ); - enqueue( queue, location.world, location.pos.down(), location.distanceTravelled + 1 ); - enqueue( queue, location.world, location.pos.south(), location.distanceTravelled + 1 ); - enqueue( queue, location.world, location.pos.north(), location.distanceTravelled + 1 ); - enqueue( queue, location.world, location.pos.east(), location.distanceTravelled + 1 ); - enqueue( queue, location.world, location.pos.west(), location.distanceTravelled + 1 ); - } - } + return true; } - private void searchNetwork( ICableVisitor visitor ) + // IWiredElement tile + + @Nullable + @Override + public IWiredElement getWiredElement( @Nonnull EnumFacing side ) { - int searchID = ++s_nextUniqueSearchID; - Queue queue = new LinkedList<>(); - enqueue( queue, getWorld(), getPos(), 1 ); - - //int visited = 0; - while( queue.peek() != null ) - { - SearchLoc loc = queue.remove(); - visitBlock( queue, loc, searchID, visitor ); - //visited++; - } - //System.out.println( "Visited "+visited+" common" ); + return BlockCable.canConnectIn( getBlockState(), side ) ? m_cable : null; } + // IPeripheralTile + @Override - public boolean canRenderBreaking() + public IPeripheral getPeripheral( EnumFacing side ) { - return true; + if( getPeripheralType() != PeripheralType.Cable ) + { + return super.getPeripheral( side ); + } + return null; } } diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/TileWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/TileWiredModemFull.java new file mode 100644 index 0000000000..d46ab6a166 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/TileWiredModemFull.java @@ -0,0 +1,416 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2017. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ + +package dan200.computercraft.shared.peripheral.modem; + +import com.google.common.base.Objects; +import dan200.computercraft.ComputerCraft; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredElementTile; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.peripheral.common.BlockCable; +import dan200.computercraft.shared.peripheral.common.TilePeripheralBase; +import dan200.computercraft.shared.util.IDAssigner; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.text.TextComponentTranslation; +import net.minecraft.world.World; +import net.minecraftforge.common.util.Constants; + +import javax.annotation.Nonnull; +import java.io.File; +import java.util.*; + +public class TileWiredModemFull extends TilePeripheralBase implements IWiredElementTile +{ + private static class FullElement extends WiredModemElement + { + private final TileWiredModemFull m_entity; + + private FullElement( TileWiredModemFull m_entity ) + { + this.m_entity = m_entity; + } + + @Override + protected void attachPeripheral( String name, IPeripheral peripheral ) + { + for( int i = 0; i < 6; i++ ) + { + WiredModemPeripheral modem = m_entity.m_modems[i]; + if( modem != null && !name.equals( m_entity.getCachedPeripheralName( EnumFacing.VALUES[i] ) ) ) + { + modem.attachPeripheral( name, peripheral ); + } + } + } + + @Override + protected void detachPeripheral( String name ) + { + for( int i = 0; i < 6; i++ ) + { + WiredModemPeripheral modem = m_entity.m_modems[i]; + if( modem != null ) modem.detachPeripheral( name ); + } + } + + @Nonnull + @Override + public World getWorld() + { + return m_entity.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = m_entity.getPos(); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + + @Nonnull + @Override + public Map getPeripherals() + { + return m_entity.getPeripherals(); + } + } + + private WiredModemPeripheral[] m_modems = new WiredModemPeripheral[6]; + + private boolean m_peripheralAccessAllowed = false; + private int[] m_attachedPeripheralIDs = new int[6]; + private String[] m_attachedPeripheralTypes = new String[6]; + + private boolean m_destroyed = false; + private boolean m_connectionsFormed = false; + + private final WiredModemElement m_element = new FullElement( this ); + private final IWiredNode node = m_element.getNode(); + + public TileWiredModemFull() + { + Arrays.fill( m_attachedPeripheralIDs, -1 ); + } + + private void remove() + { + if( world == null || !world.isRemote ) + { + node.remove(); + m_connectionsFormed = false; + } + } + + @Override + public void destroy() + { + if( !m_destroyed ) + { + m_destroyed = true; + remove(); + } + super.destroy(); + } + + @Override + public void onChunkUnload() + { + super.onChunkUnload(); + remove(); + } + + @Override + public void invalidate() + { + super.invalidate(); + remove(); + } + + @Override + public EnumFacing getDirection() + { + return EnumFacing.NORTH; + } + + @Override + public void setDirection( EnumFacing dir ) + { + } + + @Override + public void onNeighbourChange() + { + if( !world.isRemote && m_peripheralAccessAllowed ) + { + Map updated = getPeripherals(); + + if( updated.isEmpty() ) + { + // If there are no peripherals then disable access and update the display state. + m_peripheralAccessAllowed = false; + updateAnim(); + } + + // Always invalidate the node: it's more accurate than checking if the peripherals + // have changed + node.invalidate(); + } + } + + @Nonnull + @Override + public AxisAlignedBB getBounds() + { + return BlockCable.FULL_BLOCK_AABB; + } + + @Override + public boolean onActivate( EntityPlayer player, EnumFacing side, float hitX, float hitY, float hitZ ) + { + if( !getWorld().isRemote ) + { + // On server, we interacted if a peripheral was found + Set oldPeriphName = getPeripherals().keySet(); + togglePeripheralAccess(); + Set periphName = getPeripherals().keySet(); + + if( !Objects.equal( periphName, oldPeriphName ) ) + { + if( !oldPeriphName.isEmpty() ) + { + List names = new ArrayList<>( oldPeriphName ); + names.sort( Comparator.naturalOrder() ); + + player.sendMessage( + new TextComponentTranslation( "gui.computercraft:wired_modem.peripheral_disconnected", String.join( ", ", names ) ) + ); + } + if( !periphName.isEmpty() ) + { + List names = new ArrayList<>( periphName ); + names.sort( Comparator.naturalOrder() ); + player.sendMessage( + new TextComponentTranslation( "gui.computercraft:wired_modem.peripheral_connected", String.join( ", ", names ) ) + ); + } + } + + return true; + } + else + { + // On client, we can't know this, so we assume so to be safe + // The server will correct us if we're wrong + return true; + } + } + + @Override + public void readFromNBT( NBTTagCompound tag ) + { + super.readFromNBT( tag ); + m_peripheralAccessAllowed = tag.getBoolean( "peripheralAccess" ); + for( int i = 0; i < m_attachedPeripheralIDs.length; i++ ) + { + if( tag.hasKey( "peripheralID_" + i, Constants.NBT.TAG_ANY_NUMERIC ) ) + { + m_attachedPeripheralIDs[i] = tag.getInteger( "peripheralID_" + i ); + } + if( tag.hasKey( "peripheralType_" + i, Constants.NBT.TAG_STRING ) ) + { + m_attachedPeripheralTypes[i] = tag.getString( "peripheralType_" + i ); + } + } + } + + @Nonnull + @Override + public NBTTagCompound writeToNBT( NBTTagCompound tag ) + { + tag = super.writeToNBT( tag ); + tag.setBoolean( "peripheralAccess", m_peripheralAccessAllowed ); + for( int i = 0; i < m_attachedPeripheralIDs.length; i++ ) + { + if( m_attachedPeripheralIDs[i] >= 0 ) + { + tag.setInteger( "peripheralID_" + i, m_attachedPeripheralIDs[i] ); + } + if( m_attachedPeripheralTypes[i] != null ) + { + tag.setString( "peripheralType_" + i, m_attachedPeripheralTypes[i] ); + } + } + return tag; + } + + protected void updateAnim() + { + int anim = 0; + for( WiredModemPeripheral modem : m_modems ) + { + if( modem != null && modem.isActive() ) + { + anim += 1; + break; + } + } + + if( m_peripheralAccessAllowed ) + { + anim += 2; + } + setAnim( anim ); + } + + @Override + public final void readDescription( @Nonnull NBTTagCompound tag ) + { + super.readDescription( tag ); + updateBlock(); + } + + @Override + public void update() + { + if( !getWorld().isRemote ) + { + boolean changed = false; + for( WiredModemPeripheral peripheral : m_modems ) + { + if( peripheral != null && peripheral.pollChanged() ) changed = true; + } + if( changed ) updateAnim(); + + if( !m_connectionsFormed ) + { + networkChanged(); + m_connectionsFormed = true; + } + } + + super.update(); + } + + private void networkChanged() + { + if( getWorld().isRemote ) return; + + World world = getWorld(); + BlockPos current = getPos(); + for( EnumFacing facing : EnumFacing.VALUES ) + { + if( !world.isBlockLoaded( pos ) ) continue; + + IWiredElement element = ComputerCraft.getWiredElementAt( world, current.offset( facing ), facing.getOpposite() ); + if( element == null ) continue; + + // If we can connect to it then do so + node.connectTo( element.getNode() ); + } + + node.invalidate(); + } + + // private stuff + private void togglePeripheralAccess() + { + if( !m_peripheralAccessAllowed ) + { + m_peripheralAccessAllowed = true; + if( getPeripherals().isEmpty() ) + { + m_peripheralAccessAllowed = false; + return; + } + } + else + { + m_peripheralAccessAllowed = false; + } + + updateAnim(); + node.invalidate(); + } + + @Nonnull + private Map getPeripherals() + { + if( !m_peripheralAccessAllowed ) return Collections.emptyMap(); + + Map peripherals = new HashMap<>( 6 ); + for( EnumFacing facing : EnumFacing.VALUES ) + { + BlockPos neighbour = getPos().offset( facing ); + IPeripheral peripheral = TileCable.getPeripheral( getWorld(), neighbour, facing.getOpposite() ); + if( peripheral != null && !(peripheral instanceof WiredModemPeripheral) ) + { + String type = peripheral.getType(); + int id = m_attachedPeripheralIDs[facing.ordinal()]; + String oldType = m_attachedPeripheralTypes[facing.ordinal()]; + if( id < 0 || !type.equals( oldType ) ) + { + m_attachedPeripheralTypes[facing.ordinal()] = type; + id = m_attachedPeripheralIDs[facing.ordinal()] = IDAssigner.getNextIDFromFile( new File( + ComputerCraft.getWorldDir( getWorld() ), + "computer/lastid_" + type + ".txt" + ) ); + } + + peripherals.put( type + "_" + id, peripheral ); + } + } + + return peripherals; + } + + private String getCachedPeripheralName( EnumFacing facing ) + { + if( !m_peripheralAccessAllowed ) return null; + + int id = m_attachedPeripheralIDs[facing.ordinal()]; + String type = m_attachedPeripheralTypes[facing.ordinal()]; + return id < 0 || type == null ? null : type + "_" + id; + } + + // IWiredElementTile + + @Nonnull + @Override + public IWiredElement getWiredElement( @Nonnull EnumFacing side ) + { + return m_element; + } + + // IPeripheralTile + + @Override + public IPeripheral getPeripheral( EnumFacing side ) + { + WiredModemPeripheral peripheral = m_modems[side.ordinal()]; + if( peripheral == null ) + { + peripheral = m_modems[side.ordinal()] = new WiredModemPeripheral( m_element ) + { + @Nonnull + @Override + public Vec3d getPosition() + { + BlockPos pos = getPos().offset( side ); + return new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ); + } + }; + } + return peripheral; + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/WiredModemElement.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/WiredModemElement.java new file mode 100644 index 0000000000..682e8b1cf5 --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/WiredModemElement.java @@ -0,0 +1,59 @@ +package dan200.computercraft.shared.peripheral.modem; + +import dan200.computercraft.api.network.wired.IWiredNetworkChange; +import dan200.computercraft.api.network.wired.IWiredElement; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.peripheral.IPeripheral; +import dan200.computercraft.shared.wired.WiredNode; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +public abstract class WiredModemElement implements IWiredElement +{ + private final IWiredNode node = new WiredNode( this ); + private final Map remotePeripherals = new HashMap<>(); + + @Nonnull + @Override + public IWiredNode getNode() + { + return node; + } + + @Nonnull + @Override + public String getSenderID() + { + return "modem"; + } + + @Override + public void networkChanged( @Nonnull IWiredNetworkChange change ) + { + synchronized( remotePeripherals ) + { + remotePeripherals.keySet().removeAll( change.peripheralsRemoved().keySet() ); + for( String name : change.peripheralsRemoved().keySet() ) + { + detachPeripheral( name ); + } + + for( Map.Entry peripheral : change.peripheralsAdded().entrySet() ) + { + attachPeripheral( peripheral.getKey(), peripheral.getValue() ); + } + remotePeripherals.putAll( change.peripheralsAdded() ); + } + } + + public Map getRemotePeripherals() + { + return remotePeripherals; + } + + protected abstract void attachPeripheral( String name, IPeripheral peripheral ); + + protected abstract void detachPeripheral( String name ); +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/WiredModemPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/WiredModemPeripheral.java new file mode 100644 index 0000000000..f139dfc4fc --- /dev/null +++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/WiredModemPeripheral.java @@ -0,0 +1,410 @@ +package dan200.computercraft.shared.peripheral.modem; + +import com.google.common.collect.ImmutableMap; +import dan200.computercraft.api.filesystem.IMount; +import dan200.computercraft.api.filesystem.IWritableMount; +import dan200.computercraft.api.lua.ILuaContext; +import dan200.computercraft.api.lua.LuaException; +import dan200.computercraft.api.network.IPacketNetwork; +import dan200.computercraft.api.network.wired.IWiredNode; +import dan200.computercraft.api.network.wired.IWiredSender; +import dan200.computercraft.api.peripheral.IComputerAccess; +import dan200.computercraft.api.peripheral.IPeripheral; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.World; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +import static dan200.computercraft.core.apis.ArgumentHelper.getString; + +public class WiredModemPeripheral extends ModemPeripheral implements IWiredSender +{ + private final WiredModemElement modem; + + private final Map peripheralWrappers = new HashMap<>(); + + public WiredModemPeripheral( WiredModemElement modem ) + { + this.modem = modem; + } + + //region IPacketSender implementation + @Override + public boolean isInterdimensional() + { + return false; + } + + @Override + public double getRange() + { + return 256.0; + } + + @Override + protected IPacketNetwork getNetwork() + { + return modem.getNode(); + } + + @Nonnull + @Override + public World getWorld() + { + return modem.getWorld(); + } + + @Nonnull + @Override + public Vec3d getPosition() + { + return modem.getPosition(); + } + //endregion + + //region IPeripheral + @Nonnull + @Override + public String[] getMethodNames() + { + String[] methods = super.getMethodNames(); + String[] newMethods = new String[methods.length + 5]; + System.arraycopy( methods, 0, newMethods, 0, methods.length ); + newMethods[methods.length] = "getNamesRemote"; + newMethods[methods.length + 1] = "isPresentRemote"; + newMethods[methods.length + 2] = "getTypeRemote"; + newMethods[methods.length + 3] = "getMethodsRemote"; + newMethods[methods.length + 4] = "callRemote"; + return newMethods; + } + + @Override + public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaContext context, int method, @Nonnull Object[] arguments ) throws LuaException, InterruptedException + { + String[] methods = super.getMethodNames(); + switch( method - methods.length ) + { + case 0: + { + // getNamesRemote + synchronized( peripheralWrappers ) + { + int idx = 1; + Map table = new HashMap<>(); + for( String name : peripheralWrappers.keySet() ) + { + table.put( idx++, name ); + } + return new Object[]{ table }; + } + } + case 1: + { + // isPresentRemote + String type = getTypeRemote( getString( arguments, 0 ) ); + return new Object[]{ type != null }; + } + case 2: + { + // getTypeRemote + String type = getTypeRemote( getString( arguments, 0 ) ); + if( type != null ) + { + return new Object[]{ type }; + } + return null; + } + case 3: + { + // getMethodsRemote + String[] methodNames = getMethodNamesRemote( getString( arguments, 0 ) ); + if( methodNames != null ) + { + Map table = new HashMap<>(); + for( int i = 0; i < methodNames.length; ++i ) + { + table.put( i + 1, methodNames[i] ); + } + return new Object[]{ table }; + } + return null; + } + case 4: + { + // callRemote + String remoteName = getString( arguments, 0 ); + String methodName = getString( arguments, 1 ); + Object[] methodArgs = new Object[arguments.length - 2]; + System.arraycopy( arguments, 2, methodArgs, 0, arguments.length - 2 ); + return callMethodRemote( remoteName, context, methodName, methodArgs ); + } + default: + { + // The regular modem methods + return super.callMethod( computer, context, method, arguments ); + } + } + } + + @Override + public void attach( @Nonnull IComputerAccess computer ) + { + super.attach( computer ); + synchronized( modem.getRemotePeripherals() ) + { + synchronized( peripheralWrappers ) + { + for( Map.Entry entry : modem.getRemotePeripherals().entrySet() ) + { + attachPeripheralImpl( entry.getKey(), entry.getValue() ); + } + } + } + } + + @Override + public synchronized void detach( @Nonnull IComputerAccess computer ) + { + synchronized( peripheralWrappers ) + { + for( RemotePeripheralWrapper wrapper : peripheralWrappers.values() ) + { + wrapper.detach(); + } + peripheralWrappers.clear(); + } + super.detach( computer ); + } + + @Override + public boolean equals( IPeripheral other ) + { + if( other instanceof WiredModemPeripheral ) + { + WiredModemPeripheral otherModem = (WiredModemPeripheral) other; + return otherModem.modem == modem; + } + return false; + } + //endregion + + @Nonnull + @Override + public IWiredNode getNode() + { + return modem.getNode(); + } + + public void attachPeripheral( String name, IPeripheral peripheral ) + { + if( getComputer() == null ) return; + + synchronized( peripheralWrappers ) + { + attachPeripheralImpl( name, peripheral ); + } + } + + public void detachPeripheral( String name ) + { + synchronized( peripheralWrappers ) + { + RemotePeripheralWrapper wrapper = peripheralWrappers.get( name ); + if( wrapper != null ) + { + peripheralWrappers.remove( name ); + wrapper.detach(); + } + } + } + + private void attachPeripheralImpl( String periphName, IPeripheral peripheral ) + { + if( !peripheralWrappers.containsKey( periphName ) ) + { + RemotePeripheralWrapper wrapper = new RemotePeripheralWrapper( modem, peripheral, getComputer(), periphName ); + peripheralWrappers.put( periphName, wrapper ); + wrapper.attach(); + } + } + + private String getTypeRemote( String remoteName ) + { + synchronized( peripheralWrappers ) + { + RemotePeripheralWrapper wrapper = peripheralWrappers.get( remoteName ); + if( wrapper != null ) + { + return wrapper.getType(); + } + } + return null; + } + + private String[] getMethodNamesRemote( String remoteName ) + { + synchronized( peripheralWrappers ) + { + RemotePeripheralWrapper wrapper = peripheralWrappers.get( remoteName ); + if( wrapper != null ) + { + return wrapper.getMethodNames(); + } + } + return null; + } + + private Object[] callMethodRemote( String remoteName, ILuaContext context, String method, Object[] arguments ) throws LuaException, InterruptedException + { + RemotePeripheralWrapper wrapper; + synchronized( peripheralWrappers ) + { + wrapper = peripheralWrappers.get( remoteName ); + } + if( wrapper != null ) + { + return wrapper.callMethod( context, method, arguments ); + } + throw new LuaException( "No peripheral: " + remoteName ); + } + + private static class RemotePeripheralWrapper implements IComputerAccess + { + private final WiredModemElement m_element; + private final IPeripheral m_peripheral; + private final IComputerAccess m_computer; + private final String m_name; + + private final String m_type; + private final String[] m_methods; + private final Map m_methodMap; + + public RemotePeripheralWrapper( WiredModemElement element, IPeripheral peripheral, IComputerAccess computer, String name ) + { + m_element = element; + m_peripheral = peripheral; + m_computer = computer; + m_name = name; + + m_type = peripheral.getType(); + m_methods = peripheral.getMethodNames(); + assert (m_type != null); + assert (m_methods != null); + + m_methodMap = new HashMap<>(); + for( int i = 0; i < m_methods.length; ++i ) + { + if( m_methods[i] != null ) + { + m_methodMap.put( m_methods[i], i ); + } + } + } + + public void attach() + { + m_peripheral.attach( this ); + m_computer.queueEvent( "peripheral", new Object[]{ getAttachmentName() } ); + } + + public void detach() + { + m_peripheral.detach( this ); + m_computer.queueEvent( "peripheral_detach", new Object[]{ getAttachmentName() } ); + } + + public String getType() + { + return m_type; + } + + public String[] getMethodNames() + { + return m_methods; + } + + public Object[] callMethod( ILuaContext context, String methodName, Object[] arguments ) throws LuaException, InterruptedException + { + if( m_methodMap.containsKey( methodName ) ) + { + int method = m_methodMap.get( methodName ); + return m_peripheral.callMethod( this, context, method, arguments ); + } + throw new LuaException( "No such method " + methodName ); + } + + // IComputerAccess implementation + + @Override + public String mount( @Nonnull String desiredLocation, @Nonnull IMount mount ) + { + return m_computer.mount( desiredLocation, mount, m_name ); + } + + @Override + public String mount( @Nonnull String desiredLocation, @Nonnull IMount mount, @Nonnull String driveName ) + { + return m_computer.mount( desiredLocation, mount, driveName ); + } + + @Override + public String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount ) + { + return m_computer.mountWritable( desiredLocation, mount, m_name ); + } + + @Override + public String mountWritable( @Nonnull String desiredLocation, @Nonnull IWritableMount mount, @Nonnull String driveName ) + { + return m_computer.mountWritable( desiredLocation, mount, driveName ); + } + + @Override + public void unmount( String location ) + { + m_computer.unmount( location ); + } + + @Override + public int getID() + { + return m_computer.getID(); + } + + @Override + public void queueEvent( @Nonnull String event, Object[] arguments ) + { + m_computer.queueEvent( event, arguments ); + } + + @Nonnull + @Override + public String getAttachmentName() + { + return m_name; + } + + @Nonnull + @Override + public Map getAvailablePeripherals() + { + synchronized( m_element.getRemotePeripherals() ) + { + return ImmutableMap.copyOf( m_element.getRemotePeripherals() ); + } + } + + @Nullable + @Override + public IPeripheral getAvailablePeripheral( @Nonnull String name ) + { + synchronized( m_element.getRemotePeripherals() ) + { + return m_element.getRemotePeripherals().get( name ); + } + } + } +} diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java index 1a97be3d13..17d0713b80 100644 --- a/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java +++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/PrinterPeripheral.java @@ -145,6 +145,13 @@ public boolean equals( IPeripheral other ) return false; } + @Nonnull + @Override + public Object getTarget() + { + return m_printer; + } + private Terminal getCurrentPage() throws LuaException { Terminal currentPage = m_printer.getCurrentPage(); diff --git a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java index 34c0fff730..a03d90660a 100644 --- a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java +++ b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java @@ -35,10 +35,7 @@ import dan200.computercraft.shared.peripheral.common.*; import dan200.computercraft.shared.peripheral.diskdrive.ContainerDiskDrive; import dan200.computercraft.shared.peripheral.diskdrive.TileDiskDrive; -import dan200.computercraft.shared.peripheral.modem.BlockAdvancedModem; -import dan200.computercraft.shared.peripheral.modem.TileAdvancedModem; -import dan200.computercraft.shared.peripheral.modem.TileCable; -import dan200.computercraft.shared.peripheral.modem.TileWirelessModem; +import dan200.computercraft.shared.peripheral.modem.*; import dan200.computercraft.shared.peripheral.monitor.TileMonitor; import dan200.computercraft.shared.peripheral.printer.ContainerPrinter; import dan200.computercraft.shared.peripheral.printer.TilePrinter; @@ -52,6 +49,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; @@ -272,6 +270,10 @@ public void registerBlocks( RegistryEvent.Register event ) // Command Computer ComputerCraft.Blocks.advancedModem = new BlockAdvancedModem(); registry.register( ComputerCraft.Blocks.advancedModem.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "advanced_modem" ) ) ); + + // Full block modem + ComputerCraft.Blocks.wiredModemFull = new BlockWiredModemFull(); + registry.register( ComputerCraft.Blocks.wiredModemFull.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "wired_modem_full" ) ) ); } @SubscribeEvent @@ -291,9 +293,12 @@ 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" ) ) ); - + + // Full block modem + registry.register( new ItemWiredModemFull( ComputerCraft.Blocks.wiredModemFull ).setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "wired_modem_full" ) ) ); + // Items // Floppy Disk ComputerCraft.Items.disk = new ItemDiskLegacy(); @@ -469,6 +474,7 @@ private void registerTileEntities() GameRegistry.registerTileEntity( TileCommandComputer.class, ComputerCraft.LOWER_ID + " : " + "command_computer" ); GameRegistry.registerTileEntity( TileAdvancedModem.class, ComputerCraft.LOWER_ID + " : " + "advanced_modem" ); GameRegistry.registerTileEntity( TileSpeaker.class, ComputerCraft.LOWER_ID + " : " + "speaker" ); + GameRegistry.registerTileEntity( TileWiredModemFull.class, ComputerCraft.LOWER_ID + " : " + "wired_modem_full" ); // Register peripheral providers ComputerCraftAPI.registerPeripheralProvider( new DefaultPeripheralProvider() ); @@ -482,6 +488,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/main/resources/assets/computercraft/advancements/recipes/wired_modem.json b/src/main/resources/assets/computercraft/advancements/recipes/wired_modem.json index 88d3a1ea25..8b7110e693 100644 --- a/src/main/resources/assets/computercraft/advancements/recipes/wired_modem.json +++ b/src/main/resources/assets/computercraft/advancements/recipes/wired_modem.json @@ -1,7 +1,11 @@ { "parent": "minecraft:recipes/root", "rewards": { - "recipes": [ "computercraft:wired_modem" ] + "recipes": [ + "computercraft:wired_modem", + "computercraft:wired_modem_full_to", + "computercraft:wired_modem_full_from" + ] }, "criteria": { "has_normal": { @@ -22,6 +26,12 @@ "items": [ { "item": "computercraft:cable", "data": 0 } ] } }, + "has_modem_full": { + "trigger": "minecraft:inventory_changed", + "conditions": { + "items": [ { "item": "computercraft:wired_modem_full", "data": 0 } ] + } + }, "has_the_recipe": { "trigger": "minecraft:recipe_unlocked", "conditions": { "recipe": "computercraft:wired_modem" } @@ -32,6 +42,7 @@ "has_normal", "has_advanced", "has_cable", + "has_modem_full", "has_the_recipe" ] ] diff --git a/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json b/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json new file mode 100644 index 0000000000..d2f6fb31ce --- /dev/null +++ b/src/main/resources/assets/computercraft/blockstates/wired_modem_full.json @@ -0,0 +1,9 @@ +{ + "variants": { + "modem=false,peripheral=false": { "model": "computercraft:wired_modem_full_off" }, + "modem=false,peripheral=true": { "model": "computercraft:wired_modem_full_off_peripheral" }, + "modem=true,peripheral=false": { "model": "computercraft:wired_modem_full_on" }, + "modem=true,peripheral=true": { "model": "computercraft:wired_modem_full_on_peripheral" } + } +} + diff --git a/src/main/resources/assets/computercraft/models/block/wired_modem_full_off.json b/src/main/resources/assets/computercraft/models/block/wired_modem_full_off.json new file mode 100644 index 0000000000..e6ea77dfa4 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/wired_modem_full_off.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "computercraft:blocks/wired_modem_face" + } +} diff --git a/src/main/resources/assets/computercraft/models/block/wired_modem_full_off_peripheral.json b/src/main/resources/assets/computercraft/models/block/wired_modem_full_off_peripheral.json new file mode 100644 index 0000000000..318233585b --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/wired_modem_full_off_peripheral.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "computercraft:blocks/wired_modem_face_peripheral" + } +} diff --git a/src/main/resources/assets/computercraft/models/block/wired_modem_full_on.json b/src/main/resources/assets/computercraft/models/block/wired_modem_full_on.json new file mode 100644 index 0000000000..241e39ca04 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/wired_modem_full_on.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "computercraft:blocks/wired_modem_face_on" + } +} diff --git a/src/main/resources/assets/computercraft/models/block/wired_modem_full_on_peripheral.json b/src/main/resources/assets/computercraft/models/block/wired_modem_full_on_peripheral.json new file mode 100644 index 0000000000..c8ef3c9d47 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/block/wired_modem_full_on_peripheral.json @@ -0,0 +1,6 @@ +{ + "parent": "block/cube_all", + "textures": { + "all": "computercraft:blocks/wired_modem_face_peripheral_on" + } +} diff --git a/src/main/resources/assets/computercraft/models/item/wired_modem_full.json b/src/main/resources/assets/computercraft/models/item/wired_modem_full.json new file mode 100644 index 0000000000..77237e07a4 --- /dev/null +++ b/src/main/resources/assets/computercraft/models/item/wired_modem_full.json @@ -0,0 +1,3 @@ +{ + "parent": "computercraft:block/wired_modem_full_off" +} diff --git a/src/main/resources/assets/computercraft/recipes/wired_modem_full_from.json b/src/main/resources/assets/computercraft/recipes/wired_modem_full_from.json new file mode 100644 index 0000000000..9e1a326fa9 --- /dev/null +++ b/src/main/resources/assets/computercraft/recipes/wired_modem_full_from.json @@ -0,0 +1,7 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { "item": "computercraft:wired_modem_full", "data": 0 } + ], + "result": { "item": "computercraft:cable", "data": 1 } +} diff --git a/src/main/resources/assets/computercraft/recipes/wired_modem_full_to.json b/src/main/resources/assets/computercraft/recipes/wired_modem_full_to.json new file mode 100644 index 0000000000..3537e326d0 --- /dev/null +++ b/src/main/resources/assets/computercraft/recipes/wired_modem_full_to.json @@ -0,0 +1,7 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { "item": "computercraft:cable", "data": 1 } + ], + "result": { "item": "computercraft:wired_modem_full", "data": 0 } +} 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; + } +}