diff --git a/src/main/java/li/cil/oc2/api/bus/device/capabilities/NetworkInterface.java b/src/main/java/li/cil/oc2/api/bus/device/capabilities/NetworkInterface.java
new file mode 100644
index 00000000..aec56a03
--- /dev/null
+++ b/src/main/java/li/cil/oc2/api/bus/device/capabilities/NetworkInterface.java
@@ -0,0 +1,50 @@
+package li.cil.oc2.api.bus.device.capabilities;
+
+import li.cil.oc2.api.bus.device.ItemDevice;
+
+import javax.annotation.Nullable;
+
+/**
+ * This interface provides interaction with the network bus.
+ *
+ * Network connectors will check for this capability on blocks they are placed on.
+ * If found, they will actively poll frames via {@link #readEthernetFrame()} and push
+ * forwarded frames via {@link #writeEthernetFrame(NetworkInterface, byte[], int)}.
+ *
+ * As with all capabilities, this capability can be provided by {@link ItemDevice}s.
+ */
+public interface NetworkInterface {
+ /**
+ * Tries to read an ethernet frame from this network interface.
+ *
+ * The frame should> be a Layer 2 Ethernet frame.
+ *
+ * When no data is available, {@code null} should be returned.
+ *
+ * @return a pending frame or {@code null}.
+ */
+ @Nullable
+ byte[] readEthernetFrame();
+
+ /**
+ * Tries to write an ethernet frame to this network interface.
+ *
+ * The frame should be a Layer 2 Ethernet frame, but this is
+ * not guaranteed. Implementations should not rely on this, and if relying
+ * on this at least add appropriate validation and discard the frame otherwise.
+ *
+ * If the device is not ready to receive data, it may ignore the call.
+ *
+ * The {@code timeToLive} parameter is not to be confused with the IP protocol's
+ * TTL field. This parameter is used when pushing frames through the network bus
+ * to prevent infinite loops in case of cycles. Pure consumers can ignore this
+ * argument. Any implementation forwarding directly (by pushing it to some other
+ * {@link NetworkInterface} implementation) should call {@code writeEthernetFrame}
+ * with the time to live reduced by some value, usually by one.
+ *
+ * @param source the device that last forwarded the frame.
+ * @param frame the frame offered to the network interface.
+ * @param timeToLive the number of hops remaining before the frame should be discarded.
+ */
+ void writeEthernetFrame(NetworkInterface source, byte[] frame, final int timeToLive);
+}
diff --git a/src/main/java/li/cil/oc2/client/ClientSetup.java b/src/main/java/li/cil/oc2/client/ClientSetup.java
index 683d5ec8..5b1ab237 100644
--- a/src/main/java/li/cil/oc2/client/ClientSetup.java
+++ b/src/main/java/li/cil/oc2/client/ClientSetup.java
@@ -1,6 +1,7 @@
package li.cil.oc2.client;
import li.cil.oc2.client.gui.ComputerContainerScreen;
+import li.cil.oc2.client.renderer.NetworkCableRenderer;
import li.cil.oc2.client.renderer.tileentity.ComputerTileEntityRenderer;
import li.cil.oc2.common.container.Containers;
import li.cil.oc2.common.tileentity.TileEntities;
@@ -10,6 +11,8 @@ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
public final class ClientSetup {
public static void run(final FMLClientSetupEvent event) {
+ NetworkCableRenderer.initialize();
+
ScreenManager.registerFactory(Containers.COMPUTER_CONTAINER.get(), ComputerContainerScreen::new);
ClientRegistry.bindTileEntityRenderer(TileEntities.COMPUTER_TILE_ENTITY.get(), ComputerTileEntityRenderer::new);
diff --git a/src/main/java/li/cil/oc2/client/renderer/NetworkCableRenderer.java b/src/main/java/li/cil/oc2/client/renderer/NetworkCableRenderer.java
new file mode 100644
index 00000000..8d80d031
--- /dev/null
+++ b/src/main/java/li/cil/oc2/client/renderer/NetworkCableRenderer.java
@@ -0,0 +1,230 @@
+package li.cil.oc2.client.renderer;
+
+import com.mojang.blaze3d.matrix.MatrixStack;
+import com.mojang.blaze3d.vertex.IVertexBuilder;
+import li.cil.oc2.common.tileentity.NetworkConnectorTileEntity;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.ActiveRenderInfo;
+import net.minecraft.client.renderer.IRenderTypeBuffer;
+import net.minecraft.client.renderer.LightTexture;
+import net.minecraft.client.renderer.RenderType;
+import net.minecraft.client.renderer.culling.ClippingHelper;
+import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.vector.Matrix4f;
+import net.minecraft.util.math.vector.Vector3d;
+import net.minecraft.util.math.vector.Vector3f;
+import net.minecraft.world.LightType;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.RenderWorldLastEvent;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Objects;
+
+public final class NetworkCableRenderer {
+ private static final int MAX_RENDER_DISTANCE = 100;
+ private static final int CABLE_VERTEX_COUNT = 9;
+ private static final float CABLE_THICKNESS = 0.025f;
+ private static final float CABLE_HANG_MIN = 0.1f;
+ private static final float CABLE_HANG_MAX = 0.5f;
+ private static final float CABLE_MAX_LENGTH = 8f;
+ private static final Vector3f CABLE_COLOR = new Vector3f(0.0f, 0.33f, 0.4f);
+
+ private static final ArrayList connectors = new ArrayList<>();
+ private static final ArrayList connections = new ArrayList<>();
+ private static boolean isDirty;
+
+ ///////////////////////////////////////////////////////////////////
+
+ public static void initialize() {
+ MinecraftForge.EVENT_BUS.addListener(NetworkCableRenderer::handleRenderWorld);
+ }
+
+ public static void addNetworkConnector(final NetworkConnectorTileEntity connector) {
+ connectors.add(connector);
+ invalidateConnections();
+ }
+
+ public static void invalidateConnections() {
+ isDirty = true;
+ }
+
+ @SubscribeEvent
+ public static void handleRenderWorld(final RenderWorldLastEvent event) {
+ validateConnectors();
+ validatePairs();
+
+ if (connections.isEmpty()) {
+ return;
+ }
+
+ final World world = Minecraft.getInstance().world;
+ if (world == null) {
+ return;
+ }
+
+ final MatrixStack matrixStack = event.getMatrixStack();
+
+ final ActiveRenderInfo activeRenderInfo = Minecraft.getInstance().gameRenderer.getActiveRenderInfo();
+ final Vector3d eye = activeRenderInfo.getProjectedView();
+
+ final ClippingHelper frustum = new ClippingHelper(matrixStack.getLast().getMatrix(), event.getProjectionMatrix());
+ frustum.setCameraPosition(eye.getX(), eye.getY(), eye.getZ());
+
+ matrixStack.push();
+ matrixStack.translate(-eye.getX(), -eye.getY(), -eye.getZ());
+
+ final Matrix4f viewMatrix = matrixStack.getLast().getMatrix();
+
+ final RenderType renderType = OpenComputersRenderType.getNetworkCable();
+ final IRenderTypeBuffer.Impl bufferSource = Minecraft.getInstance().getRenderTypeBuffers().getBufferSource();
+
+ final float r = CABLE_COLOR.getX();
+ final float g = CABLE_COLOR.getY();
+ final float b = CABLE_COLOR.getZ();
+
+ for (final Connection connection : connections) {
+ final Vector3d p0 = connection.from;
+ final Vector3d p1 = connection.to;
+
+ if (!p0.isWithinDistanceOf(eye, MAX_RENDER_DISTANCE) && !p1.isWithinDistanceOf(eye, MAX_RENDER_DISTANCE)) {
+ continue;
+ }
+
+ // We may easily get false positives here for diagonal cables, but it's good enough for now.
+ if (!frustum.isBoundingBoxInFrustum(connection.bounds)) {
+ continue;
+ }
+
+ final Vector3d p2 = addNoisyMovement(lerp(p0, p1, 0.5f).subtract(0, computeCableHang(p0, p1), 0), connection.hashCode());
+
+ final IVertexBuilder buffer = bufferSource.getBuffer(renderType);
+
+ for (int i = 0; i < CABLE_VERTEX_COUNT; i++) {
+ final float t = i / (CABLE_VERTEX_COUNT - 1f);
+ final Vector3d p = quadraticBezier(p0, p1, p2, t);
+ final Vector3d n = getExtrusionVector(eye, p, connection.forward);
+
+ final BlockPos blockPos = new BlockPos(p);
+ final int blockLight = world.getLightFor(LightType.BLOCK, blockPos);
+ final int skyLight = world.getLightFor(LightType.SKY, blockPos);
+ final int packedLight = LightTexture.packLight(blockLight, skyLight);
+
+ final Vector3f v0 = new Vector3f(p.subtract(n));
+ final Vector3f v1 = new Vector3f(p.add(n));
+
+ buffer.pos(viewMatrix, v0.getX(), v0.getY(), v0.getZ())
+ .color(r, g, b, 1f)
+ .lightmap(packedLight)
+ .endVertex();
+ buffer.pos(viewMatrix, v1.getX(), v1.getY(), v1.getZ())
+ .color(r, g, b, 1f)
+ .lightmap(packedLight)
+ .endVertex();
+ }
+
+ bufferSource.finish(renderType);
+ }
+
+ matrixStack.pop();
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ private static Vector3d lerp(final Vector3d a, final Vector3d b, final float t) {
+ return a.add(b.subtract(a).scale(t)); // a + (b - a)*t = a*(1-t) + b*t
+ }
+
+ private static Vector3d quadraticBezier(final Vector3d a, final Vector3d b, final Vector3d c, final float t) {
+ final Vector3d a1 = lerp(a, c, t);
+ final Vector3d b1 = lerp(c, b, t);
+ return lerp(a1, b1, t);
+ }
+
+ private static Vector3d getExtrusionVector(final Vector3d eye, final Vector3d v, final Vector3d forward) {
+ return forward.crossProduct(eye.subtract(v)).normalize().scale(CABLE_THICKNESS);
+ }
+
+ private static float computeCableHang(final Vector3d a, final Vector3d b) {
+ final double length = a.distanceTo(b);
+ final double hangFactor = MathHelper.clamp(length / CABLE_MAX_LENGTH, 0, 1);
+ return (float) (CABLE_HANG_MIN + (CABLE_HANG_MAX - CABLE_HANG_MIN) * hangFactor);
+ }
+
+ private static Vector3d addNoisyMovement(final Vector3d c, final int seed) {
+ final float relTime = ((System.currentTimeMillis() + seed) % 10000) / 10000f;
+ final float relRadialTime = relTime * 2 * (float) Math.PI;
+
+ return c.add(0.1f * MathHelper.cos(relRadialTime), 0.025f + 0.025f * MathHelper.sin(relRadialTime), 0.1f * MathHelper.cos(relRadialTime));
+ }
+
+ private static void validateConnectors() {
+ for (int i = connectors.size() - 1; i >= 0; i--) {
+ final NetworkConnectorTileEntity connector = connectors.get(i);
+ if (connector.isRemoved()) {
+ connectors.remove(i);
+ invalidateConnections();
+ }
+ }
+ }
+
+ private static void validatePairs() {
+ if (!isDirty) {
+ return;
+ }
+
+ isDirty = false;
+ connections.clear();
+
+ final HashSet seen = new HashSet<>();
+ for (final NetworkConnectorTileEntity connector : connectors) {
+ final BlockPos position = connector.getPos();
+ for (final BlockPos connectedPosition : connector.getConnectedPositions()) {
+ final Connection connection = new Connection(position, connectedPosition);
+ if (seen.add(connection)) {
+ connections.add(connection);
+ }
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ private static final class Connection {
+ public final BlockPos fromPos, toPos;
+ public final Vector3d from, to, forward;
+ public final AxisAlignedBB bounds;
+
+ private Connection(final BlockPos fromPos, final BlockPos toPos) {
+ if (fromPos.compareTo(toPos) > 0) {
+ this.fromPos = toPos;
+ this.toPos = fromPos;
+ } else {
+ this.fromPos = fromPos;
+ this.toPos = toPos;
+ }
+
+ from = Vector3d.copyCentered(fromPos);
+ to = Vector3d.copyCentered(toPos);
+ forward = to.subtract(from).normalize();
+ bounds = new AxisAlignedBB(from, to).grow(0, CABLE_HANG_MAX, 0);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Connection that = (Connection) o;
+ return fromPos.equals(that.fromPos) && toPos.equals(that.toPos);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fromPos, toPos);
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/client/renderer/OpenComputersRenderType.java b/src/main/java/li/cil/oc2/client/renderer/OpenComputersRenderType.java
index 859cc436..4875fc53 100644
--- a/src/main/java/li/cil/oc2/client/renderer/OpenComputersRenderType.java
+++ b/src/main/java/li/cil/oc2/client/renderer/OpenComputersRenderType.java
@@ -1,6 +1,7 @@
package li.cil.oc2.client.renderer;
import li.cil.oc2.api.API;
+import net.minecraft.client.renderer.RenderState;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.client.renderer.vertex.VertexFormat;
@@ -23,6 +24,19 @@ public abstract class OpenComputersRenderType extends RenderType {
state);
}
+ public static RenderType getNetworkCable() {
+ final State state = State.getBuilder()
+ .transparency(RenderState.NO_TRANSPARENCY)
+ .diffuseLighting(RenderState.DIFFUSE_LIGHTING_ENABLED)
+ .lightmap(RenderState.LIGHTMAP_ENABLED)
+ .build(false);
+ return RenderType.makeType(API.MOD_ID + ":network_cable",
+ DefaultVertexFormats.POSITION_COLOR_LIGHTMAP,
+ GL11.GL_QUAD_STRIP,
+ 256,
+ state);
+ }
+
///////////////////////////////////////////////////////////////////
private OpenComputersRenderType(final String name, final VertexFormat format, final int drawMode, final int bufferSize, final boolean useDelegate, final boolean needsSorting, final Runnable setupTask, final Runnable clearTask) {
diff --git a/src/main/java/li/cil/oc2/common/Constants.java b/src/main/java/li/cil/oc2/common/Constants.java
index e675a5b8..c7f056c8 100644
--- a/src/main/java/li/cil/oc2/common/Constants.java
+++ b/src/main/java/li/cil/oc2/common/Constants.java
@@ -16,17 +16,22 @@ public final class Constants {
public static final String COMPUTER_BLOCK_NAME = "computer";
public static final String BUS_CABLE_BLOCK_NAME = "bus_cable";
+ public static final String NETWORK_CONNECTOR_BLOCK_NAME = "network_connector";
public static final String REDSTONE_INTERFACE_BLOCK_NAME = "redstone_interface";
public static final String SCREEN_BLOCK_NAME = "screen";
///////////////////////////////////////////////////////////////////
public static final String WRENCH_ITEM_NAME = "wrench";
+
public static final String BUS_INTERFACE_ITEM_NAME = "bus_interface";
+ public static final String NETWORK_CABLE_NAME = "network_cable";
+
public static final String FLASH_MEMORY_ITEM_NAME = "flash_memory";
public static final String MEMORY_ITEM_NAME = "memory";
public static final String HARD_DRIVE_ITEM_NAME = "hard_drive";
public static final String REDSTONE_INTERFACE_CARD_NAME = "redstone_interface_card";
+ public static final String NETWORK_INTERFACE_CARD_NAME = "network_interface_card";
///////////////////////////////////////////////////////////////////
@@ -46,4 +51,10 @@ public final class Constants {
public static final String COMPUTER_BUS_STATE_INCOMPLETE = "gui.oc2.computer.bus_state.incomplete";
public static final String COMPUTER_BUS_STATE_TOO_COMPLEX = "gui.oc2.computer.bus_state.too_complex";
public static final String COMPUTER_BUS_STATE_MULTIPLE_CONTROLLERS = "gui.oc2.computer.bus_state.multiple_controllers";
+
+ ///////////////////////////////////////////////////////////////////
+
+ public static final String CONNECTOR_ERROR_FULL = "message.oc2.connector.error.full";
+ public static final String CONNECTOR_ERROR_TOO_FAR = "message.oc2.connector.error.too_far";
+ public static final String CONNECTOR_ERROR_OBSTRUCTED = "message.oc2.connector.error.obstructed";
}
diff --git a/src/main/java/li/cil/oc2/common/block/Blocks.java b/src/main/java/li/cil/oc2/common/block/Blocks.java
index d2a9f470..c50e0657 100644
--- a/src/main/java/li/cil/oc2/common/block/Blocks.java
+++ b/src/main/java/li/cil/oc2/common/block/Blocks.java
@@ -15,6 +15,7 @@ public final class Blocks {
public static final RegistryObject COMPUTER_BLOCK = BLOCKS.register(Constants.COMPUTER_BLOCK_NAME, ComputerBlock::new);
public static final RegistryObject BUS_CABLE_BLOCK = BLOCKS.register(Constants.BUS_CABLE_BLOCK_NAME, BusCableBlock::new);
+ public static final RegistryObject NETWORK_CONNECTOR_BLOCK = BLOCKS.register(Constants.NETWORK_CONNECTOR_BLOCK_NAME, NetworkConnectorBlock::new);
public static final RegistryObject REDSTONE_INTERFACE_BLOCK = BLOCKS.register(Constants.REDSTONE_INTERFACE_BLOCK_NAME, RedstoneInterfaceBlock::new);
public static final RegistryObject SCREEN_BLOCK = BLOCKS.register(Constants.SCREEN_BLOCK_NAME, ScreenBlock::new);
diff --git a/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java b/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java
new file mode 100644
index 00000000..92b982d7
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/block/NetworkConnectorBlock.java
@@ -0,0 +1,101 @@
+package li.cil.oc2.common.block;
+
+import li.cil.oc2.common.tileentity.NetworkConnectorTileEntity;
+import li.cil.oc2.common.tileentity.TileEntities;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.HorizontalFaceBlock;
+import net.minecraft.block.SoundType;
+import net.minecraft.block.material.Material;
+import net.minecraft.state.StateContainer;
+import net.minecraft.state.properties.AttachFace;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.shapes.ISelectionContext;
+import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.world.IBlockReader;
+import net.minecraft.world.World;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+public final class NetworkConnectorBlock extends HorizontalFaceBlock {
+ private static final VoxelShape NEG_Z_SHAPE = Block.makeCuboidShape(5, 5, 7, 11, 11, 16);
+ private static final VoxelShape POS_Z_SHAPE = Block.makeCuboidShape(5, 5, 0, 11, 11, 9);
+ private static final VoxelShape NEG_X_SHAPE = Block.makeCuboidShape(7, 5, 5, 16, 11, 11);
+ private static final VoxelShape POS_X_SHAPE = Block.makeCuboidShape(0, 5, 5, 9, 11, 11);
+ private static final VoxelShape NEG_Y_SHAPE = Block.makeCuboidShape(5, 0, 5, 11, 9, 11);
+ private static final VoxelShape POS_Y_SHAPE = Block.makeCuboidShape(5, 7, 5, 11, 16, 11);
+
+ ///////////////////////////////////////////////////////////////////
+
+ public NetworkConnectorBlock() {
+ super(Properties
+ .create(Material.IRON)
+ .sound(SoundType.METAL)
+ .hardnessAndResistance(1.5f, 6.0f));
+ setDefaultState(getStateContainer().getBaseState()
+ .with(HORIZONTAL_FACING, Direction.NORTH)
+ .with(FACE, AttachFace.WALL));
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ public static Direction getFacing(final BlockState state) {
+ return HorizontalFaceBlock.getFacing(state);
+ }
+
+ @Override
+ public boolean hasTileEntity(final BlockState state) {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public TileEntity createTileEntity(final BlockState state, final IBlockReader world) {
+ return TileEntities.NETWORK_CONNECTOR_TILE_ENTITY.get().create();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void neighborChanged(final BlockState state, final World world, final BlockPos pos, final Block changedBlock, final BlockPos changedBlockPos, final boolean isMoving) {
+ if (Objects.equals(changedBlockPos, pos.offset(getFacing(state).getOpposite()))) {
+ final TileEntity tileEntity = world.getTileEntity(pos);
+ if (tileEntity instanceof NetworkConnectorTileEntity) {
+ final NetworkConnectorTileEntity connector = (NetworkConnectorTileEntity) tileEntity;
+ connector.setLocalInterfaceChanged();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public VoxelShape getShape(final BlockState state, final IBlockReader world, final BlockPos pos, final ISelectionContext context) {
+ switch (state.get(FACE)) {
+ case WALL:
+ switch (state.get(HORIZONTAL_FACING)) {
+ case EAST:
+ return POS_X_SHAPE;
+ case WEST:
+ return NEG_X_SHAPE;
+ case SOUTH:
+ return POS_Z_SHAPE;
+ case NORTH:
+ default:
+ return NEG_Z_SHAPE;
+ }
+ case CEILING:
+ return POS_Y_SHAPE;
+ case FLOOR:
+ default:
+ return NEG_Y_SHAPE;
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ protected void fillStateContainer(final StateContainer.Builder builder) {
+ builder.add(FACE, HORIZONTAL_FACING);
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/bus/device/item/NetworkInterfaceCardItemDevice.java b/src/main/java/li/cil/oc2/common/bus/device/item/NetworkInterfaceCardItemDevice.java
new file mode 100644
index 00000000..046038f8
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/bus/device/item/NetworkInterfaceCardItemDevice.java
@@ -0,0 +1,149 @@
+package li.cil.oc2.common.bus.device.item;
+
+import li.cil.oc2.api.bus.device.ItemDevice;
+import li.cil.oc2.api.bus.device.capabilities.NetworkInterface;
+import li.cil.oc2.api.bus.device.vm.*;
+import li.cil.oc2.common.bus.device.util.IdentityProxy;
+import li.cil.oc2.common.bus.device.util.OptionalAddress;
+import li.cil.oc2.common.bus.device.util.OptionalInterrupt;
+import li.cil.oc2.common.capabilities.Capabilities;
+import li.cil.oc2.common.serialization.NBTSerialization;
+import li.cil.oc2.common.util.NBTTagIds;
+import li.cil.sedna.device.virtio.VirtIONetworkDevice;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraft.util.Direction;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.ICapabilityProvider;
+import net.minecraftforge.common.util.LazyOptional;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public final class NetworkInterfaceCardItemDevice extends IdentityProxy implements VMDevice, VMDeviceLifecycleListener, ItemDevice, ICapabilityProvider {
+ private static final String DEVICE_TAG_NAME = "device";
+ private static final String ADDRESS_NBT_TAG_NAME = "address";
+ private static final String INTERRUPT_NBT_TAG_NAME = "interrupt";
+
+ ///////////////////////////////////////////////////////////////
+
+ private VirtIONetworkDevice device;
+ private final NetworkInterface networkInterface = new NetworkInterfaceImpl();
+ private boolean isRunning;
+
+ private final OptionalAddress address = new OptionalAddress();
+ private final OptionalInterrupt interrupt = new OptionalInterrupt();
+ private CompoundNBT deviceNbt;
+
+ ///////////////////////////////////////////////////////////////
+
+ public NetworkInterfaceCardItemDevice(final ItemStack identity) {
+ super(identity);
+ }
+
+ ///////////////////////////////////////////////////////////////
+
+ @NotNull
+ @Override
+ public LazyOptional getCapability(@NotNull final Capability cap, @Nullable final Direction side) {
+ if (cap == Capabilities.NETWORK_INTERFACE && side != null) {
+ return LazyOptional.of(() -> networkInterface).cast();
+ }
+
+ return LazyOptional.empty();
+ }
+
+ @Override
+ public VMDeviceLoadResult load(final VMContext context) {
+ device = new VirtIONetworkDevice(context.getMemoryMap());
+
+ if (!address.claim(context, device)) {
+ return VMDeviceLoadResult.fail();
+ }
+
+ if (interrupt.claim(context)) {
+ device.getInterrupt().set(interrupt.getAsInt(), context.getInterruptController());
+ } else {
+ return VMDeviceLoadResult.fail();
+ }
+
+ if (deviceNbt != null) {
+ NBTSerialization.deserialize(deviceNbt, device);
+ }
+
+ return VMDeviceLoadResult.success();
+ }
+
+ @Override
+ public void handleLifecycleEvent(final VMDeviceLifecycleEventType event) {
+ switch (event) {
+ case RESUMED_RUNNING:
+ isRunning = true;
+ break;
+ case UNLOAD:
+ unload();
+ break;
+ }
+ }
+
+ @Override
+ public CompoundNBT serializeNBT() {
+ final CompoundNBT tag = new CompoundNBT();
+
+ if (device != null) {
+ deviceNbt = NBTSerialization.serialize(device);
+ }
+ if (deviceNbt != null) {
+ tag.put(DEVICE_TAG_NAME, deviceNbt);
+ }
+ if (address.isPresent()) {
+ tag.putLong(ADDRESS_NBT_TAG_NAME, address.getAsLong());
+ }
+ if (interrupt.isPresent()) {
+ tag.putInt(INTERRUPT_NBT_TAG_NAME, interrupt.getAsInt());
+ }
+
+ return tag;
+ }
+
+ @Override
+ public void deserializeNBT(final CompoundNBT tag) {
+ if (tag.contains(DEVICE_TAG_NAME, NBTTagIds.TAG_COMPOUND)) {
+ deviceNbt = tag.getCompound(DEVICE_TAG_NAME);
+ }
+ if (tag.contains(ADDRESS_NBT_TAG_NAME, NBTTagIds.TAG_LONG)) {
+ address.set(tag.getLong(ADDRESS_NBT_TAG_NAME));
+ }
+ if (tag.contains(INTERRUPT_NBT_TAG_NAME, NBTTagIds.TAG_INT)) {
+ interrupt.set(tag.getInt(INTERRUPT_NBT_TAG_NAME));
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////
+
+ private void unload() {
+ device = null;
+ isRunning = false;
+ address.clear();
+ interrupt.clear();
+ }
+
+ ///////////////////////////////////////////////////////////////
+
+ private final class NetworkInterfaceImpl implements NetworkInterface {
+ @Override
+ public byte[] readEthernetFrame() {
+ if (device != null && isRunning) {
+ return device.readEthernetFrame();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void writeEthernetFrame(final NetworkInterface source, final byte[] frame, final int timeToLive) {
+ if (device != null && isRunning) {
+ device.writeEthernetFrame(frame);
+ }
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java
index 8aa80832..d9877746 100644
--- a/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java
+++ b/src/main/java/li/cil/oc2/common/bus/device/provider/Providers.java
@@ -4,10 +4,7 @@ import li.cil.oc2.api.API;
import li.cil.oc2.api.bus.device.provider.BlockDeviceProvider;
import li.cil.oc2.api.bus.device.provider.ItemDeviceProvider;
import li.cil.oc2.common.bus.device.provider.block.*;
-import li.cil.oc2.common.bus.device.provider.item.FlashMemoryItemDeviceProvider;
-import li.cil.oc2.common.bus.device.provider.item.HardDriveItemDeviceProvider;
-import li.cil.oc2.common.bus.device.provider.item.MemoryItemDeviceProvider;
-import li.cil.oc2.common.bus.device.provider.item.RedstoneInterfaceCardItemDeviceProvider;
+import li.cil.oc2.common.bus.device.provider.item.*;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.IForgeRegistry;
@@ -38,6 +35,7 @@ public final class Providers {
ITEM_DEVICE_PROVIDERS.register("item_hard_drive", HardDriveItemDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("item_flash_memory", FlashMemoryItemDeviceProvider::new);
ITEM_DEVICE_PROVIDERS.register("item_redstone_interface_card", RedstoneInterfaceCardItemDeviceProvider::new);
+ ITEM_DEVICE_PROVIDERS.register("item_network_interface_card", NetworkInterfaceCardItemDeviceProvider::new);
BLOCK_DEVICE_PROVIDERS.register(FMLJavaModLoadingContext.get().getModEventBus());
ITEM_DEVICE_PROVIDERS.register(FMLJavaModLoadingContext.get().getModEventBus());
diff --git a/src/main/java/li/cil/oc2/common/bus/device/provider/item/NetworkInterfaceCardItemDeviceProvider.java b/src/main/java/li/cil/oc2/common/bus/device/provider/item/NetworkInterfaceCardItemDeviceProvider.java
new file mode 100644
index 00000000..f73edc41
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/bus/device/provider/item/NetworkInterfaceCardItemDeviceProvider.java
@@ -0,0 +1,23 @@
+package li.cil.oc2.common.bus.device.provider.item;
+
+import li.cil.oc2.api.bus.device.ItemDevice;
+import li.cil.oc2.api.bus.device.provider.ItemDeviceQuery;
+import li.cil.oc2.common.bus.device.item.NetworkInterfaceCardItemDevice;
+import li.cil.oc2.common.bus.device.provider.util.AbstractItemDeviceProvider;
+import li.cil.oc2.common.item.Items;
+
+import java.util.Optional;
+
+public final class NetworkInterfaceCardItemDeviceProvider extends AbstractItemDeviceProvider {
+ public NetworkInterfaceCardItemDeviceProvider() {
+ super(Items.NETWORK_INTERFACE_CARD_ITEM);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ @Override
+ protected Optional getItemDevice(final ItemDeviceQuery query) {
+ return query.getContainerTileEntity().map(tileEntity ->
+ new NetworkInterfaceCardItemDevice(query.getItemStack()));
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java b/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java
index 1151d578..c0e4a330 100644
--- a/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java
+++ b/src/main/java/li/cil/oc2/common/capabilities/Capabilities.java
@@ -1,6 +1,7 @@
package li.cil.oc2.common.capabilities;
import li.cil.oc2.api.bus.DeviceBusElement;
+import li.cil.oc2.api.bus.device.capabilities.NetworkInterface;
import li.cil.oc2.api.bus.device.capabilities.RedstoneEmitter;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityInject;
@@ -25,11 +26,15 @@ public final class Capabilities {
@CapabilityInject(RedstoneEmitter.class)
public static Capability REDSTONE_EMITTER = null;
+ @CapabilityInject(NetworkInterface.class)
+ public static Capability NETWORK_INTERFACE = null;
+
///////////////////////////////////////////////////////////////////
public static void initialize() {
register(DeviceBusElement.class);
register(RedstoneEmitter.class);
+ register(NetworkInterface.class);
}
///////////////////////////////////////////////////////////////////
diff --git a/src/main/java/li/cil/oc2/common/item/Items.java b/src/main/java/li/cil/oc2/common/item/Items.java
index 6275da18..65eeac3d 100644
--- a/src/main/java/li/cil/oc2/common/item/Items.java
+++ b/src/main/java/li/cil/oc2/common/item/Items.java
@@ -20,17 +20,22 @@ public final class Items {
public static final RegistryObject- COMPUTER_ITEM = register(Constants.COMPUTER_BLOCK_NAME, Blocks.COMPUTER_BLOCK);
public static final RegistryObject
- BUS_CABLE_ITEM = register(Constants.BUS_CABLE_BLOCK_NAME, Blocks.BUS_CABLE_BLOCK);
+ public static final RegistryObject
- NETWORK_CONNECTOR_ITEM = register(Constants.NETWORK_CONNECTOR_BLOCK_NAME, Blocks.NETWORK_CONNECTOR_BLOCK);
public static final RegistryObject
- REDSTONE_INTERFACE_ITEM = register(Constants.REDSTONE_INTERFACE_BLOCK_NAME, Blocks.REDSTONE_INTERFACE_BLOCK);
public static final RegistryObject
- SCREEN_ITEM = register(Constants.SCREEN_BLOCK_NAME, Blocks.SCREEN_BLOCK);
///////////////////////////////////////////////////////////////////
- public static final RegistryObject
- BUS_INTERFACE_ITEM = register(Constants.BUS_INTERFACE_ITEM_NAME, BusInterfaceItem::new);
public static final RegistryObject
- WRENCH_ITEM = register(Constants.WRENCH_ITEM_NAME, WrenchItem::new);
+
+ public static final RegistryObject
- BUS_INTERFACE_ITEM = register(Constants.BUS_INTERFACE_ITEM_NAME, BusInterfaceItem::new);
+ public static final RegistryObject
- NETWORK_CABLE_ITEM = register(Constants.NETWORK_CABLE_NAME, NetworkCableItem::new);
+
public static final RegistryObject
- MEMORY_ITEM = register(Constants.MEMORY_ITEM_NAME, MemoryItem::new, new Item.Properties());
public static final RegistryObject
- HARD_DRIVE_ITEM = register(Constants.HARD_DRIVE_ITEM_NAME, HardDriveItem::new, new Item.Properties());
public static final RegistryObject
- FLASH_MEMORY_ITEM = register(Constants.FLASH_MEMORY_ITEM_NAME, FlashMemoryItem::new, new Item.Properties());
public static final RegistryObject
- REDSTONE_INTERFACE_CARD_ITEM = register(Constants.REDSTONE_INTERFACE_CARD_NAME);
+ public static final RegistryObject
- NETWORK_INTERFACE_CARD_ITEM = register(Constants.NETWORK_INTERFACE_CARD_NAME);
///////////////////////////////////////////////////////////////////
diff --git a/src/main/java/li/cil/oc2/common/item/NetworkCableItem.java b/src/main/java/li/cil/oc2/common/item/NetworkCableItem.java
new file mode 100644
index 00000000..eea1007d
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/item/NetworkCableItem.java
@@ -0,0 +1,113 @@
+package li.cil.oc2.common.item;
+
+import li.cil.oc2.common.Constants;
+import li.cil.oc2.common.tileentity.NetworkConnectorTileEntity;
+import li.cil.oc2.common.tileentity.NetworkConnectorTileEntity.ConnectionResult;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.ServerPlayerEntity;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.ItemUseContext;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.ActionResultType;
+import net.minecraft.util.Hand;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.text.TranslationTextComponent;
+import net.minecraft.world.World;
+
+import java.util.Objects;
+import java.util.WeakHashMap;
+
+public final class NetworkCableItem extends Item {
+ private static final WeakHashMap LINK_STARTS = new WeakHashMap<>();
+
+ ///////////////////////////////////////////////////////////////////
+
+ public NetworkCableItem(final Properties properties) {
+ super(properties);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ @Override
+ public ActionResult onItemRightClick(final World world, final PlayerEntity player, final Hand hand) {
+ if (player.isSneaking()) {
+ if (player instanceof ServerPlayerEntity) {
+ LINK_STARTS.remove(player);
+ }
+
+ return ActionResult.resultSuccess(player.getHeldItem(hand));
+ }
+
+ return super.onItemRightClick(world, player, hand);
+ }
+
+ @Override
+ public ActionResultType onItemUse(final ItemUseContext context) {
+ final PlayerEntity player = context.getPlayer();
+ if (player == null) {
+ return super.onItemUse(context);
+ }
+
+ final ItemStack stack = player.getHeldItem(context.getHand());
+ if (stack.isEmpty() || stack.getItem() != this) {
+ return super.onItemUse(context);
+ }
+
+ final World world = context.getWorld();
+ final BlockPos currentPos = context.getPos();
+
+ final TileEntity currentTileEntity = world.getTileEntity(currentPos);
+ if (!(currentTileEntity instanceof NetworkConnectorTileEntity)) {
+ return super.onItemUse(context);
+ }
+
+ if (!world.isRemote() && player instanceof ServerPlayerEntity) {
+ final BlockPos startPos = LINK_STARTS.remove(player);
+ if (startPos == null || Objects.equals(startPos, currentPos)) {
+ if (((NetworkConnectorTileEntity) currentTileEntity).canConnectMore()) {
+ LINK_STARTS.put((ServerPlayerEntity) player, currentPos);
+ } else {
+ player.sendStatusMessage(new TranslationTextComponent(Constants.CONNECTOR_ERROR_FULL), true);
+ }
+ } else {
+ final TileEntity startTileEntity = world.getTileEntity(startPos);
+ if (!(startTileEntity instanceof NetworkConnectorTileEntity)) {
+ // Starting connector was removed in the meantime.
+ return super.onItemUse(context);
+ }
+
+ final NetworkConnectorTileEntity connectorA = (NetworkConnectorTileEntity) startTileEntity;
+ final NetworkConnectorTileEntity connectorB = (NetworkConnectorTileEntity) currentTileEntity;
+
+ final ConnectionResult connectionResult = NetworkConnectorTileEntity.connect(connectorA, connectorB);
+ switch (connectionResult) {
+ case SUCCESS:
+ if (!player.isCreative()) {
+ stack.shrink(1);
+ }
+ break;
+
+ case FAILURE:
+ LINK_STARTS.put((ServerPlayerEntity) player, startPos);
+ break;
+ case FAILURE_FULL:
+ LINK_STARTS.put((ServerPlayerEntity) player, startPos);
+ player.sendStatusMessage(new TranslationTextComponent(Constants.CONNECTOR_ERROR_FULL), true);
+ break;
+ case FAILURE_TOO_FAR:
+ LINK_STARTS.put((ServerPlayerEntity) player, startPos);
+ player.sendStatusMessage(new TranslationTextComponent(Constants.CONNECTOR_ERROR_TOO_FAR), true);
+ break;
+ case FAILURE_OBSTRUCTED:
+ LINK_STARTS.put((ServerPlayerEntity) player, startPos);
+ player.sendStatusMessage(new TranslationTextComponent(Constants.CONNECTOR_ERROR_OBSTRUCTED), true);
+ break;
+ }
+ }
+ }
+
+ return ActionResultType.SUCCESS;
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/network/Network.java b/src/main/java/li/cil/oc2/common/network/Network.java
index 1cea193c..e0ac7745 100644
--- a/src/main/java/li/cil/oc2/common/network/Network.java
+++ b/src/main/java/li/cil/oc2/common/network/Network.java
@@ -61,6 +61,12 @@ public final class Network {
.decoder(ComputerPowerMessage::new)
.consumer(ComputerPowerMessage::handleMessage)
.add();
+
+ INSTANCE.messageBuilder(NetworkConnectorConnectionsMessage.class, getNextPacketId(), NetworkDirection.PLAY_TO_CLIENT)
+ .encoder(NetworkConnectorConnectionsMessage::toBytes)
+ .decoder(NetworkConnectorConnectionsMessage::new)
+ .consumer(NetworkConnectorConnectionsMessage::handleMessage)
+ .add();
}
public static void sendToClientsTrackingChunk(final T message, final Chunk chunk) {
diff --git a/src/main/java/li/cil/oc2/common/network/message/NetworkConnectorConnectionsMessage.java b/src/main/java/li/cil/oc2/common/network/message/NetworkConnectorConnectionsMessage.java
new file mode 100644
index 00000000..c675a2c8
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/network/message/NetworkConnectorConnectionsMessage.java
@@ -0,0 +1,52 @@
+package li.cil.oc2.common.network.message;
+
+import li.cil.oc2.common.network.MessageUtils;
+import li.cil.oc2.common.tileentity.NetworkConnectorTileEntity;
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.math.BlockPos;
+import net.minecraftforge.fml.network.NetworkEvent;
+
+import java.util.ArrayList;
+import java.util.function.Supplier;
+
+public final class NetworkConnectorConnectionsMessage {
+ private BlockPos pos;
+ private ArrayList connectedPositions;
+
+ ///////////////////////////////////////////////////////////////////
+
+ public NetworkConnectorConnectionsMessage(final NetworkConnectorTileEntity connector) {
+ this.pos = connector.getPos();
+ this.connectedPositions = new ArrayList<>(connector.getConnectedPositions());
+ }
+
+ public NetworkConnectorConnectionsMessage(final PacketBuffer buffer) {
+ fromBytes(buffer);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ public static boolean handleMessage(final NetworkConnectorConnectionsMessage message, final Supplier context) {
+ context.get().enqueueWork(() -> MessageUtils.withClientTileEntityAt(message.pos, NetworkConnectorTileEntity.class,
+ (tileEntity) -> tileEntity.setConnectedPositionsClient(message.connectedPositions)));
+ return true;
+ }
+
+ public void fromBytes(final PacketBuffer buffer) {
+ pos = buffer.readBlockPos();
+ connectedPositions = new ArrayList<>();
+ final int positionCount = buffer.readVarInt();
+ for (int i = 0; i < positionCount; i++) {
+ final BlockPos pos = buffer.readBlockPos();
+ connectedPositions.add(pos);
+ }
+ }
+
+ public static void toBytes(final NetworkConnectorConnectionsMessage message, final PacketBuffer buffer) {
+ buffer.writeBlockPos(message.pos);
+ buffer.writeVarInt(message.connectedPositions.size());
+ for (final BlockPos pos : message.connectedPositions) {
+ buffer.writeBlockPos(pos);
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/tileentity/NetworkConnectorTileEntity.java b/src/main/java/li/cil/oc2/common/tileentity/NetworkConnectorTileEntity.java
new file mode 100644
index 00000000..1d40700e
--- /dev/null
+++ b/src/main/java/li/cil/oc2/common/tileentity/NetworkConnectorTileEntity.java
@@ -0,0 +1,452 @@
+package li.cil.oc2.common.tileentity;
+
+import li.cil.oc2.api.bus.device.capabilities.NetworkInterface;
+import li.cil.oc2.client.renderer.NetworkCableRenderer;
+import li.cil.oc2.common.Constants;
+import li.cil.oc2.common.block.NetworkConnectorBlock;
+import li.cil.oc2.common.capabilities.Capabilities;
+import li.cil.oc2.common.item.Items;
+import li.cil.oc2.common.network.Network;
+import li.cil.oc2.common.network.message.NetworkConnectorConnectionsMessage;
+import li.cil.oc2.common.util.ItemStackUtils;
+import li.cil.oc2.common.util.NBTTagIds;
+import li.cil.oc2.common.util.ServerScheduler;
+import net.minecraft.block.BlockState;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.CompoundNBT;
+import net.minecraft.nbt.ListNBT;
+import net.minecraft.nbt.NBTUtil;
+import net.minecraft.tileentity.ITickableTileEntity;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.Direction;
+import net.minecraft.util.math.*;
+import net.minecraft.util.math.vector.Vector3d;
+import net.minecraft.world.World;
+import net.minecraft.world.chunk.Chunk;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+import net.minecraftforge.common.util.LazyOptional;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+
+public final class NetworkConnectorTileEntity extends AbstractTileEntity implements ITickableTileEntity {
+ public enum ConnectionResult {
+ SUCCESS,
+ FAILURE,
+ FAILURE_FULL,
+ FAILURE_TOO_FAR,
+ FAILURE_OBSTRUCTED,
+ ALREADY_CONNECTED
+ }
+
+ private static final String CONNECTIONS_TAG_NAME = "connections";
+ private static final String IS_OWNER_TAG_NAME = "is_owner";
+
+ private static final int RETRY_UNLOADED_CHUNK_INTERVAL = 5 * Constants.TICK_SECONDS;
+ private static final int MAX_CONNECTION_COUNT = 2;
+ private static final int MAX_CONNECTION_DISTANCE = 16;
+ private static final int INITIAL_PACKET_TIME_TO_LIVE = 8;
+ private static final int BYTES_PER_SECOND = 64 * 1024;
+ private static final int BYTES_PER_TICK = BYTES_PER_SECOND / Constants.TICK_SECONDS;
+ private static final int MIN_ETHERNET_FRAME_SIZE = 42;
+
+ ///////////////////////////////////////////////////////////////////
+
+ private final NetworkConnectorNetworkInterface networkInterface = new NetworkConnectorNetworkInterface();
+
+ private LazyOptional localInterface = LazyOptional.empty();
+ private boolean isLocalConnectionDirty = true;
+
+ private final HashSet connectorPositions = new HashSet<>();
+ private final HashSet ownedCables = new HashSet<>();
+ private final HashSet dirtyConnectors = new HashSet<>();
+ private final HashMap connectors = new HashMap<>();
+
+ ///////////////////////////////////////////////////////////////////
+
+ public NetworkConnectorTileEntity() {
+ super(TileEntities.NETWORK_CONNECTOR_TILE_ENTITY.get());
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ public static ConnectionResult connect(final NetworkConnectorTileEntity connectorA, final NetworkConnectorTileEntity connectorB) {
+ if (connectorA == connectorB || connectorA.isRemoved() || connectorB.isRemoved()) {
+ return ConnectionResult.FAILURE;
+ }
+
+ final World world = connectorA.getWorld();
+ if (world == null || world.isRemote()) {
+ return ConnectionResult.FAILURE;
+ }
+
+ if (connectorB.getWorld() != world) {
+ return ConnectionResult.FAILURE;
+ }
+
+ if (!connectorA.canConnectMore() || !connectorB.canConnectMore()) {
+ return ConnectionResult.FAILURE_FULL;
+ }
+
+ final BlockPos posA = connectorA.getPos();
+ final BlockPos posB = connectorB.getPos();
+
+ if (!posA.withinDistance(posB, MAX_CONNECTION_DISTANCE)) {
+ return ConnectionResult.FAILURE_TOO_FAR;
+ }
+
+ if (isObstructed(world, posA, posB)) {
+ return ConnectionResult.FAILURE_OBSTRUCTED;
+ }
+
+ if (connectorA.connectorPositions.add(posB)) {
+ connectorA.dirtyConnectors.add(posB);
+ connectorA.onConnectedPositionsChanged();
+ }
+
+ if (connectorB.connectorPositions.add(posA)) {
+ connectorB.dirtyConnectors.add(posA);
+ connectorB.onConnectedPositionsChanged();
+ }
+
+ final ConnectionResult result;
+ if (connectorA.ownedCables.contains(posB) || connectorB.ownedCables.contains(posA)) {
+ connectorA.ownedCables.add(posB);
+ connectorB.ownedCables.remove(posA);
+ result = ConnectionResult.ALREADY_CONNECTED;
+ } else {
+ connectorA.ownedCables.add(posB);
+ result = ConnectionResult.SUCCESS;
+ }
+
+ connectorA.markDirty();
+ connectorB.markDirty();
+
+ return result;
+ }
+
+ public void disconnectFrom(final BlockPos pos) {
+ dirtyConnectors.remove(pos);
+ connectors.remove(pos);
+
+ if (ownedCables.remove(pos)) {
+ final World world = getWorld();
+ if (world != null) {
+ final Vector3d middle = Vector3d.copyCentered(getPos().add(pos)).scale(0.5f);
+ ItemStackUtils.spawnAsEntity(world, middle, new ItemStack(Items.NETWORK_CABLE_ITEM.get()));
+ }
+ }
+
+ if (!isRemoved()) {
+ if (connectorPositions.remove(pos)) {
+ onConnectedPositionsChanged();
+ }
+
+ markDirty();
+ }
+ }
+
+ public boolean canConnectMore() {
+ return connectorPositions.size() < MAX_CONNECTION_COUNT;
+ }
+
+ public Collection getConnectedPositions() {
+ return connectorPositions;
+ }
+
+ public void setLocalInterfaceChanged() {
+ isLocalConnectionDirty = true;
+ }
+
+ @OnlyIn(Dist.CLIENT)
+ public void setConnectedPositionsClient(final ArrayList positions) {
+ connectorPositions.clear();
+ connectorPositions.addAll(positions);
+ NetworkCableRenderer.invalidateConnections();
+ }
+
+ @Override
+ public void tick() {
+ if (isLocalConnectionDirty) {
+ isLocalConnectionDirty = false;
+ resolveLocalInterface();
+ }
+
+ if (!dirtyConnectors.isEmpty()) {
+ final ArrayList list = new ArrayList<>(dirtyConnectors);
+ dirtyConnectors.clear();
+ for (final BlockPos connectedPosition : list) {
+ resolveConnectedInterface(connectedPosition);
+ }
+ }
+
+ final NetworkInterface src = localInterface.orElse(NullNetworkInterface.INSTANCE);
+
+ int byteBudget = BYTES_PER_TICK;
+ byte[] frame;
+ while ((frame = src.readEthernetFrame()) != null && byteBudget > 0) {
+ byteBudget -= Math.max(frame.length, MIN_ETHERNET_FRAME_SIZE); // Avoid bogus packets messing with us.
+ networkInterface.writeEthernetFrame(src, frame, INITIAL_PACKET_TIME_TO_LIVE);
+ }
+ }
+
+ @Override
+ public CompoundNBT getUpdateTag() {
+ final CompoundNBT tag = super.getUpdateTag();
+
+ final ListNBT connections = new ListNBT();
+ for (final BlockPos position : connectorPositions) {
+ final CompoundNBT connectionTag = NBTUtil.writeBlockPos(position);
+ connections.add(connectionTag);
+ }
+ tag.put(CONNECTIONS_TAG_NAME, connections);
+
+ return tag;
+ }
+
+ @Override
+ public void handleUpdateTag(final BlockState state, final CompoundNBT tag) {
+ super.handleUpdateTag(state, tag);
+
+ final ListNBT connections = tag.getList(CONNECTIONS_TAG_NAME, NBTTagIds.TAG_COMPOUND);
+ for (int i = 0; i < Math.min(connections.size(), MAX_CONNECTION_COUNT); i++) {
+ final CompoundNBT connectionTag = connections.getCompound(i);
+ final BlockPos position = NBTUtil.readBlockPos(connectionTag);
+ connectorPositions.add(position);
+ dirtyConnectors.add(position);
+ }
+ }
+
+ @Override
+ public CompoundNBT write(CompoundNBT tag) {
+ tag = super.write(tag);
+
+ final ListNBT connections = new ListNBT();
+ for (final BlockPos position : connectorPositions) {
+ final CompoundNBT connectionTag = NBTUtil.writeBlockPos(position);
+ if (ownedCables.contains(position)) {
+ connectionTag.putBoolean(IS_OWNER_TAG_NAME, true);
+ }
+ connections.add(connectionTag);
+ }
+ tag.put(CONNECTIONS_TAG_NAME, connections);
+
+ return tag;
+ }
+
+ @Override
+ public void read(final BlockState state, final CompoundNBT tag) {
+ super.read(state, tag);
+
+ final ListNBT connections = tag.getList(CONNECTIONS_TAG_NAME, NBTTagIds.TAG_COMPOUND);
+ for (int i = 0; i < Math.min(connections.size(), MAX_CONNECTION_COUNT); i++) {
+ final CompoundNBT connectionTag = connections.getCompound(i);
+ final BlockPos position = NBTUtil.readBlockPos(connectionTag);
+ connectorPositions.add(position);
+ dirtyConnectors.add(position);
+ if (connectionTag.getBoolean(IS_OWNER_TAG_NAME)) {
+ ownedCables.add(position);
+ }
+ }
+ }
+
+ @Override
+ protected void loadClient() {
+ super.loadClient();
+
+ NetworkCableRenderer.addNetworkConnector(this);
+ }
+
+ @Override
+ public void remove() {
+ super.remove();
+
+ // When we're being removed we want to break the actual link to any connected
+ // connectors. This will also cause cables to be dropped.
+ final ArrayList list = new ArrayList<>(connectors.values());
+ connectors.clear();
+ for (final NetworkConnectorTileEntity connector : list) {
+ disconnectFrom(connector.getPos());
+ connector.disconnectFrom(getPos());
+ }
+ }
+
+ @Override
+ protected void unloadServer() {
+ super.unloadServer();
+
+ // When unloading, we just want to remove the reference to this tile entity
+ // from connected connectors; we don't want to actually break the link.
+ final BlockPos pos = getPos();
+ for (final NetworkConnectorTileEntity connector : connectors.values()) {
+ connector.connectors.remove(pos);
+ if (connector.connectorPositions.contains(pos)) {
+ connector.dirtyConnectors.add(pos);
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ @Override
+ protected void collectCapabilities(final CapabilityCollector collector, @org.jetbrains.annotations.Nullable final Direction direction) {
+ collector.offer(Capabilities.NETWORK_INTERFACE, networkInterface);
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ private void resolveLocalInterface() {
+ localInterface = LazyOptional.empty();
+
+ if (isRemoved()) {
+ return;
+ }
+
+ final World world = getWorld();
+ if (world == null || world.isRemote()) {
+ return;
+ }
+
+ final Direction facing = NetworkConnectorBlock.getFacing(getBlockState());
+ final BlockPos sourcePos = getPos().offset(facing.getOpposite());
+
+ final ChunkPos sourceChunk = new ChunkPos(sourcePos);
+ if (!world.chunkExists(sourceChunk.x, sourceChunk.z)) {
+ ServerScheduler.schedule(world, this::setLocalInterfaceChanged, RETRY_UNLOADED_CHUNK_INTERVAL);
+ return;
+ }
+
+ final TileEntity tileEntity = world.getTileEntity(sourcePos);
+ if (tileEntity == null) {
+ return;
+ }
+
+ localInterface = tileEntity.getCapability(Capabilities.NETWORK_INTERFACE, facing);
+ if (localInterface.isPresent()) {
+ localInterface.addListener(unused -> setLocalInterfaceChanged());
+ }
+ }
+
+ private void resolveConnectedInterface(final BlockPos connectedPosition) {
+ connectors.remove(connectedPosition);
+
+ if (isRemoved()) {
+ return;
+ }
+
+ final World world = getWorld();
+ if (world == null || world.isRemote()) {
+ return;
+ }
+
+ final ChunkPos destinationChunk = new ChunkPos(connectedPosition);
+ if (!world.chunkExists(destinationChunk.x, destinationChunk.z)) {
+ ServerScheduler.schedule(world, () -> dirtyConnectors.add(connectedPosition), RETRY_UNLOADED_CHUNK_INTERVAL);
+ return;
+ }
+
+ final TileEntity tileEntity = world.getTileEntity(connectedPosition);
+ if (!(tileEntity instanceof NetworkConnectorTileEntity)) {
+ disconnectFrom(connectedPosition);
+ return;
+ }
+
+ final NetworkConnectorTileEntity connector = (NetworkConnectorTileEntity) tileEntity;
+
+ if (!connectedPosition.withinDistance(getPos(), MAX_CONNECTION_DISTANCE)) {
+ disconnectFrom(connectedPosition);
+ connector.disconnectFrom(getPos());
+ return;
+ }
+
+ if (isObstructed(world, getPos(), connectedPosition)) {
+ disconnectFrom(connectedPosition);
+ connector.disconnectFrom(getPos());
+ return;
+ }
+
+ connectors.put(connectedPosition, connector);
+ }
+
+ private static boolean isObstructed(final World world, final BlockPos a, final BlockPos b) {
+ final Vector3d va = Vector3d.copyCentered(a);
+ final Vector3d vb = Vector3d.copyCentered(b);
+ final Vector3d ab = vb.subtract(va).normalize().scale(0.5);
+
+ // Because of floating point inaccuracies the raytrace is not necessarily
+ // symmetric. In particular when grazing corners perfectly, e.g. two connectors
+ // attached to the same block at a 90 degree angle. So we check both ways.
+ final BlockRayTraceResult hitAB = world.rayTraceBlocks(new RayTraceContext(
+ va.add(ab),
+ vb.subtract(ab),
+ RayTraceContext.BlockMode.COLLIDER,
+ RayTraceContext.FluidMode.NONE,
+ null
+ ));
+ final BlockRayTraceResult hitBA = world.rayTraceBlocks(new RayTraceContext(
+ vb.subtract(ab),
+ va.add(ab),
+ RayTraceContext.BlockMode.COLLIDER,
+ RayTraceContext.FluidMode.NONE,
+ null
+ ));
+
+ return hitAB.getType() != RayTraceResult.Type.MISS ||
+ hitBA.getType() != RayTraceResult.Type.MISS;
+ }
+
+ private void onConnectedPositionsChanged() {
+ final World world = getWorld();
+ if (world != null && !world.isRemote()) {
+ final NetworkConnectorConnectionsMessage message = new NetworkConnectorConnectionsMessage(this);
+ final Chunk chunk = world.getChunkAt(getPos());
+ Network.sendToClientsTrackingChunk(message, chunk);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////
+
+ private static final class NullNetworkInterface implements NetworkInterface {
+ public static final NetworkInterface INSTANCE = new NullNetworkInterface();
+
+ @Override
+ public byte[] readEthernetFrame() {
+ return null;
+ }
+
+ @Override
+ public void writeEthernetFrame(final NetworkInterface source, final byte[] frame, final int timeToLive) {
+ }
+ }
+
+ private final class NetworkConnectorNetworkInterface implements NetworkInterface {
+ @Override
+ public byte[] readEthernetFrame() {
+ return null;
+ }
+
+ @Override
+ public void writeEthernetFrame(final NetworkInterface source, final byte[] frame, final int timeToLive) {
+ if (timeToLive <= 0) {
+ return;
+ }
+
+ localInterface.ifPresent(dst -> {
+ if (dst == source) {
+ return;
+ }
+ dst.writeEthernetFrame(this, frame, timeToLive - 1);
+ });
+
+ for (final NetworkConnectorTileEntity dst : connectors.values()) {
+ if (dst.isRemoved() || dst.networkInterface == source) {
+ continue;
+ }
+ dst.networkInterface.writeEthernetFrame(this, frame, timeToLive - 1);
+ }
+ }
+ }
+}
diff --git a/src/main/java/li/cil/oc2/common/tileentity/TileEntities.java b/src/main/java/li/cil/oc2/common/tileentity/TileEntities.java
index 166aba00..0c6a2484 100644
--- a/src/main/java/li/cil/oc2/common/tileentity/TileEntities.java
+++ b/src/main/java/li/cil/oc2/common/tileentity/TileEntities.java
@@ -1,7 +1,7 @@
package li.cil.oc2.common.tileentity;
-import li.cil.oc2.common.Constants;
import li.cil.oc2.api.API;
+import li.cil.oc2.common.Constants;
import li.cil.oc2.common.block.Blocks;
import net.minecraft.block.Block;
import net.minecraft.tileentity.TileEntity;
@@ -21,6 +21,7 @@ public final class TileEntities {
public static final RegistryObject> REDSTONE_INTERFACE_TILE_ENTITY = register(Constants.REDSTONE_INTERFACE_BLOCK_NAME, Blocks.REDSTONE_INTERFACE_BLOCK, RedstoneInterfaceTileEntity::new);
public static final RegistryObject> BUS_CABLE_TILE_ENTITY = register(Constants.BUS_CABLE_BLOCK_NAME, Blocks.BUS_CABLE_BLOCK, BusCableTileEntity::new);
public static final RegistryObject> COMPUTER_TILE_ENTITY = register(Constants.COMPUTER_BLOCK_NAME, Blocks.COMPUTER_BLOCK, ComputerTileEntity::new);
+ public static final RegistryObject> NETWORK_CONNECTOR_TILE_ENTITY = register(Constants.NETWORK_CONNECTOR_BLOCK_NAME, Blocks.NETWORK_CONNECTOR_BLOCK, NetworkConnectorTileEntity::new);
///////////////////////////////////////////////////////////////////
diff --git a/src/main/java/li/cil/oc2/data/BlockStates.java b/src/main/java/li/cil/oc2/data/BlockStates.java
index 65486a9a..2f091b98 100644
--- a/src/main/java/li/cil/oc2/data/BlockStates.java
+++ b/src/main/java/li/cil/oc2/data/BlockStates.java
@@ -31,6 +31,18 @@ public class BlockStates extends BlockStateProvider {
horizontalBlock(Blocks.COMPUTER_BLOCK, Items.COMPUTER_ITEM);
horizontalBlock(Blocks.REDSTONE_INTERFACE_BLOCK, Items.REDSTONE_INTERFACE_ITEM);
horizontalBlock(Blocks.SCREEN_BLOCK, Items.SCREEN_ITEM);
+ horizontalFaceBlock(Blocks.NETWORK_CONNECTOR_BLOCK, Items.NETWORK_CONNECTOR_ITEM)
+ .transforms()
+ .transform(ModelBuilder.Perspective.GUI)
+ .rotation(30, 315, 0)
+ .translation(0, 2, 0)
+ .scale(0.75f, 0.75f, 0.75f)
+ .end()
+ .transform(ModelBuilder.Perspective.FIXED)
+ .rotation(270, 0, 0)
+ .translation(0, 0, -5)
+ .end()
+ .end();
registerCableStates();
}
@@ -187,8 +199,13 @@ public class BlockStates extends BlockStateProvider {
.end();
}
- private void horizontalBlock(final RegistryObject block, final RegistryObject
- item) {
+ private ItemModelBuilder horizontalBlock(final RegistryObject block, final RegistryObject
- item) {
horizontalBlock(block.get(), models().getBuilder(block.getId().getPath()));
- itemModels().getBuilder(item.getId().getPath()).parent(models().getExistingFile(block.getId()));
+ return itemModels().getBuilder(item.getId().getPath()).parent(models().getExistingFile(block.getId()));
+ }
+
+ private ItemModelBuilder horizontalFaceBlock(final RegistryObject block, final RegistryObject
- item) {
+ horizontalFaceBlock(block.get(), models().getBuilder(block.getId().getPath()));
+ return itemModels().getBuilder(item.getId().getPath()).parent(models().getExistingFile(block.getId()));
}
}
diff --git a/src/main/java/li/cil/oc2/data/ItemModels.java b/src/main/java/li/cil/oc2/data/ItemModels.java
index 4e803756..a7e468ef 100644
--- a/src/main/java/li/cil/oc2/data/ItemModels.java
+++ b/src/main/java/li/cil/oc2/data/ItemModels.java
@@ -1,7 +1,7 @@
package li.cil.oc2.data;
-import li.cil.oc2.common.Constants;
import li.cil.oc2.api.API;
+import li.cil.oc2.common.Constants;
import li.cil.oc2.common.item.HardDriveItem;
import li.cil.oc2.common.item.Items;
import li.cil.oc2.common.item.MemoryItem;
@@ -22,6 +22,8 @@ public final class ItemModels extends ItemModelProvider {
protected void registerModels() {
simple(Items.WRENCH_ITEM, "items/wrench");
+ simple(Items.NETWORK_CABLE_ITEM, "items/network_cable");
+
simple(Items.MEMORY_ITEM, "items/memory1")
.override()
.predicate(MemoryItem.CAPACITY_PROPERTY, 4 * Constants.MEGABYTE)
@@ -31,7 +33,6 @@ public final class ItemModels extends ItemModelProvider {
.predicate(MemoryItem.CAPACITY_PROPERTY, 8 * Constants.MEGABYTE)
.model(simple(Items.MEMORY_ITEM, "items/memory3", "3"))
.end();
-
simple(Items.HARD_DRIVE_ITEM, "items/hard_drive1")
.override()
.predicate(HardDriveItem.CAPACITY_PROPERTY, 4 * Constants.MEGABYTE)
@@ -41,9 +42,9 @@ public final class ItemModels extends ItemModelProvider {
.predicate(HardDriveItem.CAPACITY_PROPERTY, 8 * Constants.MEGABYTE)
.model(simple(Items.HARD_DRIVE_ITEM, "items/hard_drive3", "3"))
.end();
-
simple(Items.FLASH_MEMORY_ITEM, "items/flash_memory");
simple(Items.REDSTONE_INTERFACE_CARD_ITEM, "items/redstone_interface_card");
+ simple(Items.NETWORK_INTERFACE_CARD_ITEM, "items/network_interface_card");
}
private ItemModelBuilder simple(final RegistryObject item, final String texturePath) {
diff --git a/src/main/java/li/cil/oc2/data/LootTables.java b/src/main/java/li/cil/oc2/data/LootTables.java
index e9aecb68..5c5321d9 100644
--- a/src/main/java/li/cil/oc2/data/LootTables.java
+++ b/src/main/java/li/cil/oc2/data/LootTables.java
@@ -45,6 +45,7 @@ public final class LootTables extends LootTableProvider {
registerDropSelfLootTable(Blocks.BUS_CABLE_BLOCK.get());
registerDropSelfLootTable(Blocks.REDSTONE_INTERFACE_BLOCK.get());
registerDropSelfLootTable(Blocks.SCREEN_BLOCK.get());
+ registerDropSelfLootTable(Blocks.NETWORK_CONNECTOR_BLOCK.get());
registerLootTable(Blocks.COMPUTER_BLOCK.get(), ModBlockLootTables::droppingWithInventory);
}
diff --git a/src/main/resources/assets/oc2/blockstates/network_connector.json b/src/main/resources/assets/oc2/blockstates/network_connector.json
new file mode 100644
index 00000000..a4003de3
--- /dev/null
+++ b/src/main/resources/assets/oc2/blockstates/network_connector.json
@@ -0,0 +1,57 @@
+{
+ "variants": {
+ "face=floor,facing=north": {
+ "model": "oc2:block/network_connector"
+ },
+ "face=wall,facing=north": {
+ "model": "oc2:block/network_connector",
+ "x": 90
+ },
+ "face=ceiling,facing=north": {
+ "model": "oc2:block/network_connector",
+ "x": 180,
+ "y": 180
+ },
+ "face=floor,facing=south": {
+ "model": "oc2:block/network_connector",
+ "y": 180
+ },
+ "face=wall,facing=south": {
+ "model": "oc2:block/network_connector",
+ "x": 90,
+ "y": 180
+ },
+ "face=ceiling,facing=south": {
+ "model": "oc2:block/network_connector",
+ "x": 180
+ },
+ "face=floor,facing=west": {
+ "model": "oc2:block/network_connector",
+ "y": 270
+ },
+ "face=wall,facing=west": {
+ "model": "oc2:block/network_connector",
+ "x": 90,
+ "y": 270
+ },
+ "face=ceiling,facing=west": {
+ "model": "oc2:block/network_connector",
+ "x": 180,
+ "y": 90
+ },
+ "face=floor,facing=east": {
+ "model": "oc2:block/network_connector",
+ "y": 90
+ },
+ "face=wall,facing=east": {
+ "model": "oc2:block/network_connector",
+ "x": 90,
+ "y": 90
+ },
+ "face=ceiling,facing=east": {
+ "model": "oc2:block/network_connector",
+ "x": 180,
+ "y": 270
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/lang/en_us.json b/src/main/resources/assets/oc2/lang/en_us.json
index 8bd4189b..f894bed2 100644
--- a/src/main/resources/assets/oc2/lang/en_us.json
+++ b/src/main/resources/assets/oc2/lang/en_us.json
@@ -3,15 +3,18 @@
"block.oc2.computer": "Computer",
"block.oc2.bus_cable": "Bus Cable",
+ "block.oc2.network_connector": "Network Connector",
"block.oc2.redstone_interface": "Redstone Interface",
"block.oc2.screen": "Screen",
"item.oc2.wrench": "Scrench",
"item.oc2.bus_interface": "Bus Interface",
+ "item.oc2.network_cable": "Network Cable",
"item.oc2.memory": "Memory",
"item.oc2.hard_drive": "Hard Drive",
"item.oc2.flash_memory": "Flash Memory",
"item.oc2.redstone_interface_card": "Redstone Interface Card",
+ "item.oc2.network_interface_card": "Network Interface Card",
"gui.oc2.computer.boot_error.unknown": "Unknown Error",
"gui.oc2.computer.boot_error.no_memory": "Insufficient Memory",
@@ -27,5 +30,9 @@
"gui.oc2.device_type.memory": "Memory",
"gui.oc2.device_type.hard_drive": "Hard Drive",
"gui.oc2.device_type.flash_memory": "Flash Memory",
- "gui.oc2.device_type.card": "Card"
+ "gui.oc2.device_type.card": "Card",
+
+ "message.oc2.connector.error.full": "Cannot attach more cables.",
+ "message.oc2.connector.error.too_far": "Distance between connectors is too large.",
+ "message.oc2.connector.error.obstructed": "No clear line of sight between connectors."
}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/models/block/network_connector.json b/src/main/resources/assets/oc2/models/block/network_connector.json
new file mode 100644
index 00000000..b86ee7a2
--- /dev/null
+++ b/src/main/resources/assets/oc2/models/block/network_connector.json
@@ -0,0 +1 @@
+{"parent":"block/block","textures":{"east":"oc2:blocks/network_connector/network_connector_east","west":"oc2:blocks/network_connector/network_connector_west","up":"oc2:blocks/network_connector/network_connector_up","down":"oc2:blocks/network_connector/network_connector_down","north":"oc2:blocks/network_connector/network_connector_north","south":"oc2:blocks/network_connector/network_connector_south","atlas0":"oc2:blocks/network_connector/network_connector_atlas0","particle":"#north"},"elements":[{"from":[5,0,5],"to":[11,1,11],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"down","cullface":"down"},"north":{"texture":"north"},"south":{"texture":"south"}}},{"from":[6,1,10],"to":[10,3,11],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"atlas0","uv":[0.0,2.5,1.0,2.75]},"south":{"texture":"south"}}},{"from":[5,1,6],"to":[11,3,10],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"atlas0","uv":[0.0,0.0,1.5,1.0]},"north":{"texture":"north"},"south":{"texture":"south"}}},{"from":[6,1,5],"to":[10,3,6],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"atlas0","uv":[0.0,2.75,1.0,3.0]},"north":{"texture":"north"}}},{"from":[6,3,10],"to":[7,4,11],"faces":{"east":{"texture":"atlas0","uv":[0.0,3.5,0.25,3.75]},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"down"},"south":{"texture":"south"}}},{"from":[9,3,10],"to":[10,4,11],"faces":{"east":{"texture":"east"},"west":{"texture":"atlas0","uv":[0.0,3.75,0.25,4.0]},"up":{"texture":"up"},"down":{"texture":"down"},"south":{"texture":"south"}}},{"from":[5,3,9],"to":[11,4,10],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"down"},"north":{"texture":"atlas0","uv":[0.0,1.0,1.5,1.25]},"south":{"texture":"south"}}},{"from":[6,3,7],"to":[10,4,9],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"down"},"north":{"texture":"north"},"south":{"texture":"south"}}},{"from":[5,3,6],"to":[11,4,7],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"down"},"north":{"texture":"north"},"south":{"texture":"atlas0","uv":[0.0,1.25,1.5,1.5]}}},{"from":[6,3,5],"to":[7,4,6],"faces":{"east":{"texture":"atlas0","uv":[0.0,4.0,0.25,4.25]},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"down"},"north":{"texture":"north"}}},{"from":[9,3,5],"to":[10,4,6],"faces":{"east":{"texture":"east"},"west":{"texture":"atlas0","uv":[0.0,4.25,0.25,4.5]},"up":{"texture":"up"},"down":{"texture":"down"},"north":{"texture":"north"}}},{"from":[6,4,10],"to":[10,7,11],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"atlas0","uv":[0.0,3.0,1.0,3.25]},"south":{"texture":"south"}}},{"from":[5,4,6],"to":[11,7,10],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"atlas0","uv":[0.0,1.5,1.5,2.5]},"north":{"texture":"north"},"south":{"texture":"south"}}},{"from":[6,4,5],"to":[10,7,6],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"down":{"texture":"atlas0","uv":[0.0,3.25,1.0,3.5]},"north":{"texture":"north"}}},{"from":[6,7,6],"to":[10,8,10],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"north":{"texture":"north"},"south":{"texture":"south"}}},{"from":[7,8,7],"to":[9,9,9],"faces":{"east":{"texture":"east"},"west":{"texture":"west"},"up":{"texture":"up"},"north":{"texture":"north"},"south":{"texture":"south"}}}]}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/models/item/network_cable.json b/src/main/resources/assets/oc2/models/item/network_cable.json
new file mode 100644
index 00000000..71b0cfc9
--- /dev/null
+++ b/src/main/resources/assets/oc2/models/item/network_cable.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/handheld",
+ "textures": {
+ "layer0": "oc2:items/network_cable"
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/models/item/network_connector.json b/src/main/resources/assets/oc2/models/item/network_connector.json
new file mode 100644
index 00000000..43e30736
--- /dev/null
+++ b/src/main/resources/assets/oc2/models/item/network_connector.json
@@ -0,0 +1,32 @@
+{
+ "parent": "oc2:block/network_connector",
+ "display": {
+ "gui": {
+ "rotation": [
+ 30,
+ 315,
+ 0
+ ],
+ "translation": [
+ 0, 2, 0
+ ],
+ "scale": [
+ 0.75,
+ 0.75,
+ 0.75
+ ]
+ },
+ "fixed": {
+ "rotation": [
+ 270,
+ 0,
+ 0
+ ],
+ "translation": [
+ 0,
+ 0,
+ -5
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/models/item/network_interface_card.json b/src/main/resources/assets/oc2/models/item/network_interface_card.json
new file mode 100644
index 00000000..2c99f043
--- /dev/null
+++ b/src/main/resources/assets/oc2/models/item/network_interface_card.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/handheld",
+ "textures": {
+ "layer0": "oc2:items/network_interface_card"
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_atlas0.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_atlas0.png
new file mode 100644
index 00000000..adcefdf8
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_atlas0.png differ
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_down.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_down.png
new file mode 100644
index 00000000..735d80f4
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_down.png differ
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_east.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_east.png
new file mode 100644
index 00000000..ba43447d
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_east.png differ
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_north.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_north.png
new file mode 100644
index 00000000..9673caa1
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_north.png differ
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_south.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_south.png
new file mode 100644
index 00000000..0fb23e5d
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_south.png differ
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_up.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_up.png
new file mode 100644
index 00000000..7cbf5a74
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_up.png differ
diff --git a/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_west.png b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_west.png
new file mode 100644
index 00000000..52128bc4
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/blocks/network_connector/network_connector_west.png differ
diff --git a/src/main/resources/assets/oc2/textures/items/network_cable.png b/src/main/resources/assets/oc2/textures/items/network_cable.png
new file mode 100644
index 00000000..cf527462
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/items/network_cable.png differ
diff --git a/src/main/resources/assets/oc2/textures/items/network_interface_card.png b/src/main/resources/assets/oc2/textures/items/network_interface_card.png
new file mode 100644
index 00000000..6a0df6f1
Binary files /dev/null and b/src/main/resources/assets/oc2/textures/items/network_interface_card.png differ
diff --git a/src/main/resources/data/oc2/loot_tables/blocks/network_connector.json b/src/main/resources/data/oc2/loot_tables/blocks/network_connector.json
new file mode 100644
index 00000000..74f40252
--- /dev/null
+++ b/src/main/resources/data/oc2/loot_tables/blocks/network_connector.json
@@ -0,0 +1,19 @@
+{
+ "type": "minecraft:block",
+ "pools": [
+ {
+ "rolls": 1,
+ "entries": [
+ {
+ "type": "minecraft:item",
+ "name": "oc2:network_connector"
+ }
+ ],
+ "conditions": [
+ {
+ "condition": "minecraft:survives_explosion"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file