diff --git a/build.gradle b/build.gradle
index d40ae19ce..ded8570c1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,6 +6,7 @@ buildscript {
     }
     dependencies {
         classpath 'net.minecraftforge.gradle:ForgeGradle:5.1.+'
+        classpath "org.spongepowered:mixingradle:0.7.+"
         classpath 'org.parchmentmc:librarian:1.+'
     }
 }
@@ -22,6 +23,7 @@ plugins {
 }
 
 apply plugin: 'net.minecraftforge.gradle'
+apply plugin: "org.spongepowered.mixin"
 apply plugin: 'org.parchmentmc.librarian.forgegradle'
 
 version = mod_version
@@ -64,6 +66,8 @@ minecraft {
                     source sourceSets.main
                 }
             }
+
+            arg "-mixin.config=computercraft.mixins.json"
         }
 
         client {
@@ -109,6 +113,10 @@ minecraft {
     accessTransformer file('src/testMod/resources/META-INF/accesstransformer.cfg')
 }
 
+mixin {
+    add sourceSets.main, 'computercraft.mixins.refmap.json'
+}
+
 repositories {
     mavenCentral()
     maven {
@@ -130,6 +138,7 @@ dependencies {
     checkstyle "com.puppycrawl.tools:checkstyle:8.25"
 
     minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
+    annotationProcessor 'org.spongepowered:mixin:0.8.4:processor'
 
     compileOnly fg.deobf("mezz.jei:jei-1.16.5:7.7.0.104:api")
     compileOnly fg.deobf("com.blamejared.crafttweaker:CraftTweaker-1.16.5:7.1.0.313")
@@ -149,7 +158,7 @@ dependencies {
 
     testModImplementation sourceSets.main.output
 
-    cctJavadoc 'cc.tweaked:cct-javadoc:1.4.2'
+    cctJavadoc 'cc.tweaked:cct-javadoc:1.4.4'
 }
 
 // Compile tasks
@@ -181,13 +190,16 @@ task luaJavadoc(type: Javadoc) {
 
 jar {
     manifest {
-        attributes(["Specification-Title"     : "computercraft",
-                    "Specification-Vendor"    : "SquidDev",
-                    "Specification-Version"   : "1",
-                    "Implementation-Title"    : "CC: Tweaked",
-                    "Implementation-Version"  : "${mod_version}",
-                    "Implementation-Vendor"   : "SquidDev",
-                    "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")])
+        attributes([
+            "Specification-Title"     : "computercraft",
+            "Specification-Vendor"    : "SquidDev",
+            "Specification-Version"   : "1",
+            "Implementation-Title"    : "CC: Tweaked",
+            "Implementation-Version"  : "${mod_version}",
+            "Implementation-Vendor"   : "SquidDev",
+            "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
+            "MixinConfigs"            : "computercraft.mixins.json",
+        ])
     }
 
     from configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
diff --git a/src/main/java/dan200/computercraft/mixin/BlockRendererDispatcherMixin.java b/src/main/java/dan200/computercraft/mixin/BlockRendererDispatcherMixin.java
new file mode 100644
index 000000000..1d05c2b59
--- /dev/null
+++ b/src/main/java/dan200/computercraft/mixin/BlockRendererDispatcherMixin.java
@@ -0,0 +1,92 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+package dan200.computercraft.mixin;
+
+import com.mojang.blaze3d.matrix.MatrixStack;
+import com.mojang.blaze3d.vertex.IVertexBuilder;
+import dan200.computercraft.shared.Registry;
+import dan200.computercraft.shared.peripheral.modem.wired.BlockCable;
+import dan200.computercraft.shared.peripheral.modem.wired.CableModemVariant;
+import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
+import dan200.computercraft.shared.util.WorldUtil;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BlockModelRenderer;
+import net.minecraft.client.renderer.BlockModelShapes;
+import net.minecraft.client.renderer.BlockRendererDispatcher;
+import net.minecraft.client.renderer.model.IBakedModel;
+import net.minecraft.client.renderer.texture.OverlayTexture;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.BlockRayTraceResult;
+import net.minecraft.util.math.RayTraceResult;
+import net.minecraft.world.IBlockDisplayReader;
+import net.minecraftforge.client.model.data.IModelData;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.Random;
+
+/**
+ * Provides custom block breaking progress for modems, so it only applies to the current part.
+ *
+ * @see BlockRendererDispatcher#renderBlockDamage(BlockState, BlockPos, IBlockDisplayReader, MatrixStack, IVertexBuilder, IModelData)
+ */
+@Mixin( BlockRendererDispatcher.class )
+public class BlockRendererDispatcherMixin
+{
+    @Shadow
+    private final Random random;
+    @Shadow
+    private final BlockModelShapes blockModelShaper;
+    @Shadow
+    private final BlockModelRenderer modelRenderer;
+
+    public BlockRendererDispatcherMixin( Random random, BlockModelShapes blockModelShaper, BlockModelRenderer modelRenderer )
+    {
+        this.random = random;
+        this.blockModelShaper = blockModelShaper;
+        this.modelRenderer = modelRenderer;
+    }
+
+    @Inject(
+        method = "name=/^renderBlockDamage$/ desc=/IModelData;\\)V$/",
+        at = @At( "HEAD" ),
+        cancellable = true,
+        require = 0 // This isn't critical functionality, so don't worry if we can't apply it.
+    )
+    public void renderBlockDamage(
+        BlockState state, BlockPos pos, IBlockDisplayReader world, MatrixStack pose, IVertexBuilder buffers, IModelData modelData,
+        CallbackInfo info
+    )
+    {
+        // Only apply to cables which have both a cable and modem
+        if( state.getBlock() != Registry.ModBlocks.CABLE.get()
+            || !state.getValue( BlockCable.CABLE )
+            || state.getValue( BlockCable.MODEM ) == CableModemVariant.None
+        )
+        {
+            return;
+        }
+
+        RayTraceResult hit = Minecraft.getInstance().hitResult;
+        if( hit == null || hit.getType() != RayTraceResult.Type.BLOCK ) return;
+        BlockPos hitPos = ((BlockRayTraceResult) hit).getBlockPos();
+
+        if( !hitPos.equals( pos ) ) return;
+
+        info.cancel();
+        BlockState newState = WorldUtil.isVecInside( CableShapes.getModemShape( state ), hit.getLocation().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
+            ? state.getBlock().defaultBlockState().setValue( BlockCable.MODEM, state.getValue( BlockCable.MODEM ) )
+            : state.setValue( BlockCable.MODEM, CableModemVariant.None );
+
+        IBakedModel model = blockModelShaper.getBlockModel( newState );
+        long seed = newState.getSeed( pos );
+        modelRenderer.renderModel( world, model, newState, pos, pose, buffers, true, random, seed, OverlayTexture.NO_OVERLAY, modelData );
+    }
+}
diff --git a/src/main/resources/computercraft.mixins.json b/src/main/resources/computercraft.mixins.json
new file mode 100644
index 000000000..9da186e3b
--- /dev/null
+++ b/src/main/resources/computercraft.mixins.json
@@ -0,0 +1,13 @@
+{
+    "minVersion": "0.8",
+    "required": true,
+    "compatibilityLevel": "JAVA_8",
+    "refmap": "computercraft.mixins.refmap.json",
+    "package": "dan200.computercraft.mixin",
+    "client": [
+        "BlockRendererDispatcherMixin"
+    ],
+    "injectors": {
+        "defaultRequire": 1
+    }
+}