diff --git a/build.gradle b/build.gradle
index 9e76bee8fd..85e1367dbf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,25 +1,16 @@
 buildscript {
-    repositories {
-        jcenter()
-        mavenCentral()
-        maven {
-            name = "forge"
-            url = "http://files.minecraftforge.net/maven"
-        }
-    }
     dependencies {
         classpath 'com.google.code.gson:gson:2.8.1'
-        classpath 'net.minecraftforge.gradle:ForgeGradle:3.0.115'
         classpath 'net.sf.proguard:proguard-gradle:6.1.0beta2'
         classpath 'org.ajoberstar.grgit:grgit-gradle:3.0.0'
     }
 }
 
 plugins {
+    id 'fabric-loom' version '0.2.0-SNAPSHOT'
     id 'com.matthewprenger.cursegradle' version '1.2.0'
 }
 
-apply plugin: 'net.minecraftforge.gradle'
 apply plugin: 'org.ajoberstar.grgit'
 apply plugin: 'maven-publish'
 apply plugin: 'maven'
@@ -30,38 +21,10 @@ group = "org.squiddev"
 archivesBaseName = "cc-tweaked-${mc_version}"
 
 minecraft {
-    runs {
-        client {
-            workingDirectory project.file('run')
-            property 'forge.logging.markers', 'REGISTRIES'
-            property 'forge.logging.console.level', 'debug'
-
-            mods {
-                computercraft {
-                    source sourceSets.main
-                }
-            }
-        }
-
-        server {
-            workingDirectory project.file('run')
-            property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'
-            property 'forge.logging.console.level', 'debug'
-
-            mods {
-                computercraft {
-                    source sourceSets.main
-                }
-            }
-        }
-    }
-
-    mappings channel: 'snapshot', version: "${mappings_version}".toString()
-
-    accessTransformer file('src/main/resources/META-INF/accesstransformer.cfg')
 }
 
 repositories {
+    mavenCentral()
     maven {
         name "JEI"
         url  "http://dvs1.progwml6.com/files/maven"
@@ -87,15 +50,21 @@ configurations {
 }
 
 dependencies {
-    minecraft "net.minecraftforge:forge:${mc_version}-${forge_version}"
+    minecraft "com.mojang:minecraft:${mc_version}"
+    mappings "net.fabricmc:yarn:${mc_version}.${mappings_version}"
+    modCompile "net.fabricmc:fabric-loader:0.3.7.109"
+    modCompile "net.fabricmc:fabric:0.2.6.117"
 
-    compileOnly "mezz.jei:jei-1.13.2:5.0.0.8:api"
+    // compileOnly "mezz.jei:jei-1.13.2:5.0.0.8:api"
     // deobfProvided "pl.asie:Charset-Lib:0.5.4.6"
     // deobfProvided "MCMultiPart2:MCMultiPart:2.5.3"
 
-    deobf "mezz.jei:jei-1.13.2:5.0.0.8"
+    // deobf "mezz.jei:jei-1.13.2:5.0.0.8"
+
+    implementation 'com.google.code.findbugs:jsr305:3.0.2'
 
     shade 'org.squiddev:Cobalt:0.5.0-SNAPSHOT'
+    shade 'javax.vecmath:vecmath:1.5.2'
 
     testImplementation 'org.junit.jupiter:junit-jupiter-api:5.1.0'
     testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.1.0'
@@ -106,8 +75,7 @@ dependencies {
 sourceSets {
     main {
         java {
-            exclude 'dan200/computercraft/shared/integration/mcmp'
-            exclude 'dan200/computercraft/shared/integration/charset'
+            exclude 'dan200/computercraft/shared/integration'
         }
     }
 }
@@ -212,7 +180,7 @@ processResources {
     inputs.property "commithash", hash
 
     from(sourceSets.main.resources.srcDirs) {
-        include 'META-INF/mods.toml'
+        include 'fabric.mods.json'
         include 'data/computercraft/lua/rom/help/credits.txt'
 
         expand 'version': mod_version,
@@ -221,7 +189,7 @@ processResources {
     }
 
     from(sourceSets.main.resources.srcDirs) {
-        exclude 'META-INF/mods.toml'
+        exclude 'fabric.mods.json'
         exclude 'data/computercraft/lua/rom/help/credits.txt'
     }
 }
@@ -272,7 +240,8 @@ curseforge {
     apiKey = project.hasProperty('curseForgeApiKey') ? project.curseForgeApiKey : ''
     project {
         id = '282001'
-        releaseType = 'beta'
+        addGameVersion '1.14-Snapshot'
+        releaseType = 'alpha'
         changelog = "Release notes can be found on the GitHub repository (https://github.com/SquidDev-CC/CC-Tweaked/releases/tag/v${mc_version}-${mod_version})."
     }
 }
@@ -338,7 +307,7 @@ test {
 }
 
 gradle.projectsEvaluated {
-    reobfJar.dependsOn proguardMove
+    remapJar.dependsOn proguardMove
 
     tasks.withType(JavaCompile) {
         options.compilerArgs << "-Xlint" << "-Xlint:-processing" // Causes Forge build to fail << "-Werror"
diff --git a/gradle.properties b/gradle.properties
index 2c0e51d49f..4c629ac705 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,6 +2,5 @@
 mod_version=1.82.0
 
 # Minecraft properties
-mc_version=1.13.2
-forge_version=25.0.100
-mappings_version=20190327-1.13.2
+mc_version=19w14a
+mappings_version=3
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a95009c3b9..115e6ac0aa 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/settings.gradle b/settings.gradle
index fa77a12b7d..ddce646744 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,12 @@
+pluginManagement {
+    repositories {
+        jcenter()
+        maven {
+            name = 'Fabric'
+            url = 'https://maven.fabricmc.net/'
+        }
+        gradlePluginPortal()
+    }
+}
+
 rootProject.name = "cc-tweaked-${mc_version}"
diff --git a/src/main/java/dan200/computercraft/ComputerCraft.java b/src/main/java/dan200/computercraft/ComputerCraft.java
index a881b4a963..9ca323ac84 100644
--- a/src/main/java/dan200/computercraft/ComputerCraft.java
+++ b/src/main/java/dan200/computercraft/ComputerCraft.java
@@ -8,10 +8,10 @@
 
 import dan200.computercraft.api.filesystem.IMount;
 import dan200.computercraft.api.turtle.event.TurtleAction;
+import dan200.computercraft.client.proxy.ComputerCraftProxyClient;
 import dan200.computercraft.core.apis.AddressPredicate;
 import dan200.computercraft.core.apis.http.websocket.Websocket;
 import dan200.computercraft.core.filesystem.ResourceMount;
-import dan200.computercraft.shared.Config;
 import dan200.computercraft.shared.computer.blocks.BlockComputer;
 import dan200.computercraft.shared.computer.core.ClientComputerRegistry;
 import dan200.computercraft.shared.computer.core.ServerComputerRegistry;
@@ -30,13 +30,14 @@
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
 import dan200.computercraft.shared.pocket.peripherals.PocketModem;
 import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
+import dan200.computercraft.shared.proxy.ComputerCraftProxyCommon;
 import dan200.computercraft.shared.turtle.blocks.BlockTurtle;
 import dan200.computercraft.shared.turtle.items.ItemTurtle;
 import dan200.computercraft.shared.turtle.upgrades.*;
-import net.minecraft.resources.IReloadableResourceManager;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.server.ServerLifecycleHooks;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.ModInitializer;
+import net.minecraft.resource.ReloadableResourceManager;
+import net.minecraft.util.Identifier;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -45,8 +46,7 @@
 import java.util.EnumSet;
 import java.util.concurrent.TimeUnit;
 
-@Mod( ComputerCraft.MOD_ID )
-public final class ComputerCraft
+public final class ComputerCraft implements ModInitializer
 {
     public static final String MOD_ID = "computercraft";
 
@@ -184,9 +184,22 @@ public static final class PocketUpgrades
     // Logging
     public static final Logger log = LogManager.getLogger( MOD_ID );
 
+    // Implementation
+    public static ComputerCraft instance;
+
     public ComputerCraft()
     {
-        Config.load();
+        instance = this;
+    }
+
+    @Override
+    public void onInitialize()
+    {
+        ComputerCraftProxyCommon.setup();
+        if( net.fabricmc.loader.api.FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT )
+        {
+            ComputerCraftProxyClient.setup();
+        }
     }
 
     public static String getVersion()
@@ -196,17 +209,17 @@ public static String getVersion()
 
     static IMount createResourceMount( String domain, String subPath )
     {
-        IReloadableResourceManager manager = ServerLifecycleHooks.getCurrentServer().getResourceManager();
+        ReloadableResourceManager manager = ComputerCraftProxyCommon.getServer().getDataManager();
         ResourceMount mount = new ResourceMount( domain, subPath, manager );
         return mount.exists( "" ) ? mount : null;
     }
 
     public static InputStream getResourceFile( String domain, String subPath )
     {
-        IReloadableResourceManager manager = ServerLifecycleHooks.getCurrentServer().getResourceManager();
+        ReloadableResourceManager manager = ComputerCraftProxyCommon.getServer().getDataManager();
         try
         {
-            return manager.getResource( new ResourceLocation( domain, subPath ) ).getInputStream();
+            return manager.getResource( new Identifier( domain, subPath ) ).getInputStream();
         }
         catch( IOException ignored )
         {
diff --git a/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java b/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java
index a817feae2f..48fd022bca 100644
--- a/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java
+++ b/src/main/java/dan200/computercraft/ComputerCraftAPIImpl.java
@@ -21,16 +21,16 @@
 import dan200.computercraft.core.apis.ApiFactories;
 import dan200.computercraft.core.filesystem.FileMount;
 import dan200.computercraft.shared.*;
+import dan200.computercraft.shared.peripheral.modem.wired.TileCable;
+import dan200.computercraft.shared.peripheral.modem.wired.TileWiredModemFull;
 import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork;
 import dan200.computercraft.shared.util.IDAssigner;
-import dan200.computercraft.shared.wired.CapabilityWiredElement;
 import dan200.computercraft.shared.wired.WiredNode;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockReader;
+import net.minecraft.util.math.Direction;
+import net.minecraft.world.BlockView;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.LazyOptional;
 
 import javax.annotation.Nonnull;
 import java.io.File;
@@ -52,7 +52,7 @@ public String getInstalledVersion()
     @Override
     public int createUniqueNumberedSaveDir( @Nonnull World world, @Nonnull String parentSubPath )
     {
-        return IDAssigner.getNextId( parentSubPath );
+        return IDAssigner.getNextId( world, parentSubPath );
     }
 
     @Override
@@ -60,7 +60,7 @@ public IWritableMount createSaveDirMount( @Nonnull World world, @Nonnull String
     {
         try
         {
-            return new FileMount( new File( IDAssigner.getDir(), subPath ), capacity );
+            return new FileMount( new File( IDAssigner.getDir( world ), subPath ), capacity );
         }
         catch( Exception e )
         {
@@ -93,7 +93,7 @@ public void registerBundledRedstoneProvider( @Nonnull IBundledRedstoneProvider p
     }
 
     @Override
-    public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
         return BundledRedstone.getDefaultOutput( world, pos, side );
     }
@@ -129,12 +129,17 @@ public IWiredNode createWiredNodeForElement( @Nonnull IWiredElement element )
     }
 
     @Override
-    public IWiredElement getWiredElementAt( @Nonnull IBlockReader world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public IWiredElement getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
-        TileEntity tile = world.getTileEntity( pos );
-        if( tile == null ) return null;
-
-        LazyOptional<IWiredElement> element = tile.getCapability( CapabilityWiredElement.CAPABILITY, side );
-        return CapabilityWiredElement.unwrap( element );
+        BlockEntity tile = world.getBlockEntity( pos );
+        if( tile instanceof TileCable )
+        {
+            return ((TileCable) tile).getElement( side );
+        }
+        else if( tile instanceof TileWiredModemFull )
+        {
+            return ((TileWiredModemFull) tile).getElement();
+        }
+        return null;
     }
 }
diff --git a/src/main/java/dan200/computercraft/api/AbstractTurtleUpgrade.java b/src/main/java/dan200/computercraft/api/AbstractTurtleUpgrade.java
index db6ce3474b..97b0ea343c 100644
--- a/src/main/java/dan200/computercraft/api/AbstractTurtleUpgrade.java
+++ b/src/main/java/dan200/computercraft/api/AbstractTurtleUpgrade.java
@@ -8,10 +8,10 @@
 
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import dan200.computercraft.api.turtle.TurtleUpgradeType;
+import net.minecraft.item.ItemProvider;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.IItemProvider;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.Util;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.SystemUtil;
 
 import javax.annotation.Nonnull;
 
@@ -22,12 +22,12 @@
  */
 public abstract class AbstractTurtleUpgrade implements ITurtleUpgrade
 {
-    private final ResourceLocation id;
+    private final Identifier id;
     private final TurtleUpgradeType type;
     private final String adjective;
     private final ItemStack stack;
 
-    protected AbstractTurtleUpgrade( ResourceLocation id, TurtleUpgradeType type, String adjective, ItemStack stack )
+    protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, ItemStack stack )
     {
         this.id = id;
         this.type = type;
@@ -35,24 +35,24 @@ protected AbstractTurtleUpgrade( ResourceLocation id, TurtleUpgradeType type, St
         this.stack = stack;
     }
 
-    protected AbstractTurtleUpgrade( ResourceLocation id, TurtleUpgradeType type, String adjective, IItemProvider item )
+    protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, String adjective, ItemProvider item )
     {
         this( id, type, adjective, new ItemStack( item ) );
     }
 
-    protected AbstractTurtleUpgrade( ResourceLocation id, TurtleUpgradeType type, ItemStack stack )
+    protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, ItemStack stack )
     {
-        this( id, type, Util.makeTranslationKey( "upgrade", id ) + ".adjective", stack );
+        this( id, type, SystemUtil.createTranslationKey( "upgrade", id ) + ".adjective", stack );
     }
 
-    protected AbstractTurtleUpgrade( ResourceLocation id, TurtleUpgradeType type, IItemProvider item )
+    protected AbstractTurtleUpgrade( Identifier id, TurtleUpgradeType type, ItemProvider item )
     {
         this( id, type, new ItemStack( item ) );
     }
 
     @Nonnull
     @Override
-    public final ResourceLocation getUpgradeID()
+    public final Identifier getUpgradeID()
     {
         return id;
     }
diff --git a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java
index b7495154ee..a54989c998 100644
--- a/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java
+++ b/src/main/java/dan200/computercraft/api/ComputerCraftAPI.java
@@ -20,9 +20,9 @@
 import dan200.computercraft.api.pocket.IPocketUpgrade;
 import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockReader;
+import net.minecraft.util.math.Direction;
+import net.minecraft.world.BlockView;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -182,7 +182,7 @@ public static void registerBundledRedstoneProvider( @Nonnull IBundledRedstonePro
      * If there is no block capable of emitting bundled redstone at the location, -1 will be returned.
      * @see IBundledRedstoneProvider
      */
-    public static int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public static int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
         return getInstance().getBundledRedstoneOutput( world, pos, side );
     }
@@ -241,7 +241,7 @@ public static IWiredNode createWiredNodeForElement( @Nonnull IWiredElement eleme
      * @see IWiredElement#getNode()
      */
     @Nullable
-    public static IWiredElement getWiredElementAt( @Nonnull IBlockReader world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public static IWiredElement getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
         return getInstance().getWiredElementAt( world, pos, side );
     }
@@ -280,7 +280,7 @@ public interface IComputerCraftAPI
 
         void registerBundledRedstoneProvider( @Nonnull IBundledRedstoneProvider provider );
 
-        int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side );
+        int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side );
 
         void registerMediaProvider( @Nonnull IMediaProvider provider );
 
@@ -292,6 +292,6 @@ public interface IComputerCraftAPI
 
         IWiredNode createWiredNodeForElement( @Nonnull IWiredElement element );
 
-        IWiredElement getWiredElementAt( @Nonnull IBlockReader world, @Nonnull BlockPos pos, @Nonnull EnumFacing side );
+        IWiredElement getWiredElementAt( @Nonnull BlockView world, @Nonnull BlockPos pos, @Nonnull Direction side );
     }
 }
diff --git a/src/main/java/dan200/computercraft/api/media/IMedia.java b/src/main/java/dan200/computercraft/api/media/IMedia.java
index efc752d7f2..afe574cee8 100644
--- a/src/main/java/dan200/computercraft/api/media/IMedia.java
+++ b/src/main/java/dan200/computercraft/api/media/IMedia.java
@@ -9,7 +9,7 @@
 import dan200.computercraft.api.filesystem.IMount;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.SoundEvent;
+import net.minecraft.sound.SoundEvent;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
diff --git a/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java b/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java
index 0c39e83cfe..bd97e1ed34 100644
--- a/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java
+++ b/src/main/java/dan200/computercraft/api/peripheral/IPeripheralProvider.java
@@ -6,9 +6,9 @@
 
 package dan200.computercraft.api.peripheral;
 
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -17,7 +17,8 @@
 /**
  * This interface is used to create peripheral implementations for blocks.
  *
- * If you have a {@link TileEntity} which acts as a peripheral, you may alternatively implement {@link IPeripheralTile}.
+ * If you have a {@link BlockEntity} which acts as a peripheral, you may alternatively implement
+ * {@link IPeripheralTile}.
  *
  * @see dan200.computercraft.api.ComputerCraftAPI#registerPeripheralProvider(IPeripheralProvider)
  */
@@ -34,5 +35,5 @@ public interface IPeripheralProvider
      * @see dan200.computercraft.api.ComputerCraftAPI#registerPeripheralProvider(IPeripheralProvider)
      */
     @Nullable
-    IPeripheral getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side );
+    IPeripheral getPeripheral( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side );
 }
diff --git a/src/main/java/dan200/computercraft/api/peripheral/IPeripheralTile.java b/src/main/java/dan200/computercraft/api/peripheral/IPeripheralTile.java
index 55e5a5c3f3..4675139c68 100644
--- a/src/main/java/dan200/computercraft/api/peripheral/IPeripheralTile.java
+++ b/src/main/java/dan200/computercraft/api/peripheral/IPeripheralTile.java
@@ -5,15 +5,15 @@
  */
 package dan200.computercraft.api.peripheral;
 
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 /**
- * A {@link net.minecraft.tileentity.TileEntity} which may act as a peripheral.
+ * A {@link net.minecraft.block.entity.BlockEntity} which may act as a peripheral.
  *
  * If you need more complex capabilities (such as handling TEs not belonging to your mod), you should use
  * {@link IPeripheralProvider}.
@@ -25,8 +25,8 @@ public interface IPeripheralTile
      *
      * @param side The side to get the peripheral from.
      * @return A peripheral, or {@code null} if there is not a peripheral here.
-     * @see IPeripheralProvider#getPeripheral(World, BlockPos, EnumFacing)
+     * @see IPeripheralProvider#getPeripheral(World, BlockPos, Direction)
      */
     @Nullable
-    IPeripheral getPeripheral( @Nonnull EnumFacing side );
+    IPeripheral getPeripheral( @Nonnull Direction side );
 }
diff --git a/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java b/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java
index 04fe6b6c9e..489897b301 100644
--- a/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java
+++ b/src/main/java/dan200/computercraft/api/pocket/AbstractPocketUpgrade.java
@@ -6,10 +6,10 @@
 
 package dan200.computercraft.api.pocket;
 
+import net.minecraft.item.ItemProvider;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.IItemProvider;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.Util;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.SystemUtil;
 
 import javax.annotation.Nonnull;
 
@@ -20,30 +20,30 @@
  */
 public abstract class AbstractPocketUpgrade implements IPocketUpgrade
 {
-    private final ResourceLocation id;
+    private final Identifier id;
     private final String adjective;
     private final ItemStack stack;
 
-    protected AbstractPocketUpgrade( ResourceLocation id, String adjective, ItemStack stack )
+    protected AbstractPocketUpgrade( Identifier id, String adjective, ItemStack stack )
     {
         this.id = id;
         this.adjective = adjective;
         this.stack = stack;
     }
 
-    protected AbstractPocketUpgrade( ResourceLocation id, String adjective, IItemProvider item )
+    protected AbstractPocketUpgrade( Identifier identifier, String adjective, ItemProvider item )
     {
-        this( id, adjective, new ItemStack( item ) );
+        this( identifier, adjective, new ItemStack( item ) );
     }
 
-    protected AbstractPocketUpgrade( ResourceLocation id, IItemProvider item )
+    protected AbstractPocketUpgrade( Identifier id, ItemProvider item )
     {
-        this( id, Util.makeTranslationKey( "upgrade", id ) + ".adjective", new ItemStack( item ) );
+        this( id, SystemUtil.createTranslationKey( "upgrade", id ) + ".adjective", new ItemStack( item ) );
     }
 
     @Nonnull
     @Override
-    public final ResourceLocation getUpgradeID()
+    public final Identifier getUpgradeID()
     {
         return id;
     }
diff --git a/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java b/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java
index beeda54fe8..e2e78dedff 100644
--- a/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java
+++ b/src/main/java/dan200/computercraft/api/pocket/IPocketAccess.java
@@ -8,8 +8,8 @@
 
 import dan200.computercraft.api.peripheral.IPeripheral;
 import net.minecraft.entity.Entity;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -75,7 +75,7 @@ public interface IPocketAccess
      * @see #updateUpgradeNBTData()
      */
     @Nonnull
-    NBTTagCompound getUpgradeNBTData();
+    CompoundTag getUpgradeNBTData();
 
     /**
      * Mark the upgrade-specific NBT as dirty.
@@ -95,5 +95,5 @@ public interface IPocketAccess
      * @return A collection of all upgrade names.
      */
     @Nonnull
-    Map<ResourceLocation, IPeripheral> getUpgrades();
+    Map<Identifier, IPeripheral> getUpgrades();
 }
diff --git a/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java b/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java
index 9c75b55171..495033db1d 100644
--- a/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java
+++ b/src/main/java/dan200/computercraft/api/pocket/IPocketUpgrade.java
@@ -10,7 +10,7 @@
 import dan200.computercraft.api.peripheral.IPeripheral;
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -36,7 +36,7 @@ public interface IPocketUpgrade
      * @see ComputerCraftAPI#registerPocketUpgrade(IPocketUpgrade)
      */
     @Nonnull
-    ResourceLocation getUpgradeID();
+    Identifier getUpgradeID();
 
     /**
      * Return an unlocalised string to describe the type of pocket computer this upgrade provides.
diff --git a/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java b/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java
index ee98f99589..1602a546c7 100644
--- a/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java
+++ b/src/main/java/dan200/computercraft/api/redstone/IBundledRedstoneProvider.java
@@ -6,8 +6,8 @@
 
 package dan200.computercraft.api.redstone;
 
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -30,5 +30,5 @@ public interface IBundledRedstoneProvider
      * handle this block.
      * @see dan200.computercraft.api.ComputerCraftAPI#registerBundledRedstoneProvider(IBundledRedstoneProvider)
      */
-    int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side );
+    int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side );
 }
diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java
index 1c29fd097c..e3ac2922b2 100644
--- a/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java
+++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleAccess.java
@@ -10,13 +10,12 @@
 import dan200.computercraft.api.lua.ILuaContext;
 import dan200.computercraft.api.lua.LuaException;
 import dan200.computercraft.api.peripheral.IPeripheral;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.nbt.CompoundTag;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.items.IItemHandlerModifiable;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -83,10 +82,10 @@ public interface ITurtleAccess
      * Returns the world direction the turtle is currently facing.
      *
      * @return The world direction the turtle is currently facing.
-     * @see #setDirection(EnumFacing)
+     * @see #setDirection(Direction)
      */
     @Nonnull
-    EnumFacing getDirection();
+    Direction getDirection();
 
     /**
      * Set the direction the turtle is facing. Note that this will not play a rotation animation, you will also need to
@@ -95,7 +94,7 @@ public interface ITurtleAccess
      * @param dir The new direction to set. This should be on either the x or z axis (so north, south, east or west).
      * @see #getDirection()
      */
-    void setDirection( @Nonnull EnumFacing dir );
+    void setDirection( @Nonnull Direction dir );
 
     /**
      * Get the currently selected slot in the turtle's inventory.
@@ -147,21 +146,9 @@ public interface ITurtleAccess
      * Get the inventory of this turtle
      *
      * @return This turtle's inventory
-     * @see #getItemHandler()
      */
     @Nonnull
-    IInventory getInventory();
-
-    /**
-     * Get the inventory of this turtle as an {@link IItemHandlerModifiable}.
-     *
-     * @return This turtle's inventory
-     * @see #getInventory()
-     * @see IItemHandlerModifiable
-     * @see net.minecraftforge.items.CapabilityItemHandler#ITEM_HANDLER_CAPABILITY
-     */
-    @Nonnull
-    IItemHandlerModifiable getItemHandler();
+    Inventory getInventory();
 
     /**
      * Determine whether this turtle will require fuel when performing actions.
@@ -290,7 +277,7 @@ public interface ITurtleAccess
      * @see #updateUpgradeNBTData(TurtleSide)
      */
     @Nonnull
-    NBTTagCompound getUpgradeNBTData( @Nullable TurtleSide side );
+    CompoundTag getUpgradeNBTData( @Nullable TurtleSide side );
 
     /**
      * Mark the upgrade-specific data as dirty on a specific side. This is required for the data to be synced to the
diff --git a/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java b/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java
index bfbe36a45b..2c020902f6 100644
--- a/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java
+++ b/src/main/java/dan200/computercraft/api/turtle/ITurtleUpgrade.java
@@ -10,15 +10,13 @@
 import dan200.computercraft.api.peripheral.IPeripheral;
 import dan200.computercraft.api.turtle.event.TurtleAttackEvent;
 import dan200.computercraft.api.turtle.event.TurtleBlockEvent;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ModelResourceLocation;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.util.ModelIdentifier;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
-import net.minecraftforge.event.entity.player.AttackEntityEvent;
-import net.minecraftforge.event.world.BlockEvent;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.Direction;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -42,7 +40,7 @@ public interface ITurtleUpgrade
      * @see ComputerCraftAPI#registerTurtleUpgrade(ITurtleUpgrade)
      */
     @Nonnull
-    ResourceLocation getUpgradeID();
+    Identifier getUpgradeID();
 
     /**
      * Return an unlocalised string to describe this type of turtle in turtle item names.
@@ -98,8 +96,8 @@ default IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull Tu
      * Will only be called for Tool turtle. Called when turtle.dig() or turtle.attack() is called
      * by the turtle, and the tool is required to do some work.
      *
-     * Conforming implementations should fire {@link BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig}for digging,
-     * {@link AttackEntityEvent} and {@link TurtleAttackEvent} for attacking.
+     * Conforming implementations should fire {@code BlockEvent.BreakEvent} and {@link TurtleBlockEvent.Dig}for digging,
+     * {@code AttackEntityEvent} and {@link TurtleAttackEvent} for attacking.
      *
      * @param turtle    Access to the turtle that the tool resides on.
      * @param side      Which side of the turtle (left or right) the tool resides on.
@@ -113,7 +111,7 @@ default IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull Tu
      * to be called.
      */
     @Nonnull
-    default TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull EnumFacing direction )
+    default TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction )
     {
         return TurtleCommandResult.failure();
     }
@@ -121,8 +119,8 @@ default TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull Tur
     /**
      * Called to obtain the model to be used when rendering a turtle peripheral.
      *
-     * This can be obtained from {@link net.minecraft.client.renderer.ItemModelMesher#getItemModel(ItemStack)},
-     * {@link net.minecraft.client.renderer.model.ModelManager#getModel(ModelResourceLocation)} or any other
+     * This can be obtained from {@link net.minecraft.client.render.item.ItemModels#getModel(ItemStack)},
+     * {@link net.minecraft.client.render.model.BakedModelManager#getModel(ModelIdentifier)} or any other
      * source.
      *
      * @param turtle Access to the turtle that the upgrade resides on. This will be null when getting item models!
@@ -131,8 +129,8 @@ default TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull Tur
      * a transformation of {@code null} has the same effect as the identify matrix.
      */
     @Nonnull
-    @OnlyIn( Dist.CLIENT )
-    Pair<IBakedModel, Matrix4f> getModel( @Nullable ITurtleAccess turtle, @Nonnull TurtleSide side );
+    @Environment( EnvType.CLIENT )
+    Pair<BakedModel, Matrix4f> getModel( @Nullable ITurtleAccess turtle, @Nonnull TurtleSide side );
 
     /**
      * Called once per tick for each turtle which has the upgrade equipped.
diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java b/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java
index 795513cc90..2debebabaa 100644
--- a/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java
+++ b/src/main/java/dan200/computercraft/api/turtle/TurtleCommandResult.java
@@ -6,7 +6,7 @@
 
 package dan200.computercraft.api.turtle;
 
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -15,7 +15,7 @@
  * Used to indicate the result of executing a turtle command.
  *
  * @see ITurtleCommand#execute(ITurtleAccess)
- * @see ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, EnumFacing)
+ * @see ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction)
  */
 public final class TurtleCommandResult
 {
diff --git a/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java b/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java
index cf8e6b0861..af69e60a3a 100644
--- a/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java
+++ b/src/main/java/dan200/computercraft/api/turtle/TurtleVerb.java
@@ -6,14 +6,14 @@
 
 package dan200.computercraft.api.turtle;
 
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 /**
  * An enum representing the different actions that an {@link ITurtleUpgrade} of type Tool may be called on to perform by
  * a turtle.
  *
  * @see ITurtleUpgrade#getType()
- * @see ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, EnumFacing)
+ * @see ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction)
  */
 public enum TurtleVerb
 {
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/FakePlayer.java b/src/main/java/dan200/computercraft/api/turtle/event/FakePlayer.java
new file mode 100644
index 0000000000..4d59a84042
--- /dev/null
+++ b/src/main/java/dan200/computercraft/api/turtle/event/FakePlayer.java
@@ -0,0 +1,26 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2018. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.api.turtle.event;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.network.ServerPlayerInteractionManager;
+import net.minecraft.server.world.ServerWorld;
+
+/**
+ * A wrapper for {@link ServerPlayerEntity} which denotes a "fake" player.
+ *
+ * Please note that this does not implement any of the traditional fake player behaviour. It simply exists to prevent
+ * me passing in normal players.
+ */
+public class FakePlayer extends ServerPlayerEntity
+{
+    public FakePlayer( ServerWorld world, GameProfile gameProfile )
+    {
+        super( world.getServer(), world, gameProfile, new ServerPlayerInteractionManager( world ) );
+    }
+}
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java
index 76e69eebeb..a87272be34 100644
--- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java
+++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleActionEvent.java
@@ -8,7 +8,6 @@
 
 import dan200.computercraft.api.turtle.ITurtleAccess;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
-import net.minecraftforge.eventbus.api.Cancelable;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -17,11 +16,11 @@
 /**
  * An event fired when a turtle is performing a known action.
  */
-@Cancelable
 public class TurtleActionEvent extends TurtleEvent
 {
     private final TurtleAction action;
     private String failureMessage;
+    private boolean cancelled = false;
 
     public TurtleActionEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action )
     {
@@ -45,7 +44,6 @@ public TurtleAction getAction()
      * @see TurtleCommandResult#failure()
      * @deprecated Use {@link #setCanceled(boolean, String)} instead.
      */
-    @Override
     @Deprecated
     public void setCanceled( boolean cancel )
     {
@@ -63,7 +61,7 @@ public void setCanceled( boolean cancel )
      */
     public void setCanceled( boolean cancel, @Nullable String failureMessage )
     {
-        super.setCanceled( cancel );
+        this.cancelled = true;
         this.failureMessage = cancel ? failureMessage : null;
     }
 
@@ -79,4 +77,15 @@ public String getFailureMessage()
     {
         return failureMessage;
     }
+
+    /**
+     * Determine if this event is cancelled
+     *
+     * @return If this event is cancelled
+     * @see #setCanceled(boolean, String)
+     */
+    public boolean isCancelled()
+    {
+        return cancelled;
+    }
 }
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java
index de95a7d8f0..32fdca3b4a 100644
--- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java
+++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleAttackEvent.java
@@ -11,9 +11,7 @@
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.api.turtle.TurtleVerb;
 import net.minecraft.entity.Entity;
-import net.minecraft.util.EnumFacing;
-import net.minecraftforge.common.util.FakePlayer;
-import net.minecraftforge.event.entity.player.AttackEntityEvent;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import java.util.Objects;
@@ -21,10 +19,11 @@
 /**
  * Fired when a turtle attempts to attack an entity.
  *
- * This must be fired by {@link ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, EnumFacing)},
+ * This must be fired by {@link ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction)},
  * as the base {@code turtle.attack()} command does not fire it.
  *
- * Note that such commands should also fire {@link AttackEntityEvent}, so you do not need to listen to both.
+ * Note that such commands should also fire {@link net.fabricmc.fabric.api.event.player.AttackEntityCallback}, so you do
+ * not need to listen to both.
  *
  * @see TurtleAction#ATTACK
  */
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java
index 0360757950..963ec42f1b 100644
--- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java
+++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleBlockEvent.java
@@ -12,13 +12,11 @@
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.api.turtle.TurtleVerb;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.FakePlayer;
-import net.minecraftforge.event.world.BlockEvent;
 
 import javax.annotation.Nonnull;
 import java.util.Map;
@@ -75,20 +73,21 @@ public BlockPos getPos()
     /**
      * Fired when a turtle attempts to dig a block.
      *
-     * This must be fired by {@link ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, EnumFacing)},
+     * This must be fired by {@link ITurtleUpgrade#useTool(ITurtleAccess, TurtleSide, TurtleVerb, Direction)},
      * as the base {@code turtle.dig()} command does not fire it.
      *
-     * Note that such commands should also fire {@link BlockEvent.BreakEvent}, so you do not need to listen to both.
+     * Note that such commands should also fire {@link net.fabricmc.fabric.api.event.player.AttackBlockCallback}, so you
+     * do not need to listen to both.
      *
      * @see TurtleAction#DIG
      */
     public static class Dig extends TurtleBlockEvent
     {
-        private final IBlockState block;
+        private final BlockState block;
         private final ITurtleUpgrade upgrade;
         private final TurtleSide side;
 
-        public Dig( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull IBlockState block, @Nonnull ITurtleUpgrade upgrade, @Nonnull TurtleSide side )
+        public Dig( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState block, @Nonnull ITurtleUpgrade upgrade, @Nonnull TurtleSide side )
         {
             super( turtle, TurtleAction.DIG, player, world, pos );
 
@@ -106,7 +105,7 @@ public Dig( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull
          * @return The block which is going to be broken.
          */
         @Nonnull
-        public IBlockState getBlock()
+        public BlockState getBlock()
         {
             return block;
         }
@@ -185,10 +184,10 @@ public ItemStack getStack()
      */
     public static class Inspect extends TurtleBlockEvent
     {
-        private final IBlockState state;
+        private final BlockState state;
         private final Map<String, Object> data;
 
-        public Inspect( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull IBlockState state, @Nonnull Map<String, Object> data )
+        public Inspect( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nonnull BlockState state, @Nonnull Map<String, Object> data )
         {
             super( turtle, TurtleAction.INSPECT, player, world, pos );
 
@@ -204,7 +203,7 @@ public Inspect( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonn
          * @return The inspected block state.
          */
         @Nonnull
-        public IBlockState getState()
+        public BlockState getState()
         {
             return state;
         }
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java
index ec47e308a6..142a3a45a7 100644
--- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java
+++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleEvent.java
@@ -6,8 +6,8 @@
 
 package dan200.computercraft.api.turtle.event;
 
+import com.google.common.eventbus.EventBus;
 import dan200.computercraft.api.turtle.ITurtleAccess;
-import net.minecraftforge.eventbus.api.Event;
 
 import javax.annotation.Nonnull;
 import java.util.Objects;
@@ -20,8 +20,10 @@
  *
  * @see TurtleActionEvent
  */
-public abstract class TurtleEvent extends Event
+public abstract class TurtleEvent
 {
+    public static final EventBus EVENT_BUS = new EventBus();
+
     private final ITurtleAccess turtle;
 
     protected TurtleEvent( @Nonnull ITurtleAccess turtle )
@@ -40,4 +42,10 @@ public ITurtleAccess getTurtle()
     {
         return turtle;
     }
+
+    public static boolean post( TurtleActionEvent event )
+    {
+        EVENT_BUS.post( event );
+        return event.isCancelled();
+    }
 }
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java
index 9e1ecf693c..a7a99be258 100644
--- a/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java
+++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtleInventoryEvent.java
@@ -7,11 +7,10 @@
 package dan200.computercraft.api.turtle.event;
 
 import dan200.computercraft.api.turtle.ITurtleAccess;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.FakePlayer;
-import net.minecraftforge.items.IItemHandler;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -22,9 +21,9 @@
  */
 public abstract class TurtleInventoryEvent extends TurtleBlockEvent
 {
-    private final IItemHandler handler;
+    private final Inventory handler;
 
-    protected TurtleInventoryEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable IItemHandler handler )
+    protected TurtleInventoryEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAction action, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable Inventory handler )
     {
         super( turtle, action, player, world, pos );
         this.handler = handler;
@@ -36,7 +35,7 @@ protected TurtleInventoryEvent( @Nonnull ITurtleAccess turtle, @Nonnull TurtleAc
      * @return The inventory being interacted with, {@code null} if the item will be dropped to/sucked from the world.
      */
     @Nullable
-    public IItemHandler getItemHandler()
+    public Inventory getItemHandler()
     {
         return handler;
     }
@@ -48,7 +47,7 @@ public IItemHandler getItemHandler()
      */
     public static class Suck extends TurtleInventoryEvent
     {
-        public Suck( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable IItemHandler handler )
+        public Suck( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable Inventory handler )
         {
             super( turtle, TurtleAction.SUCK, player, world, pos, handler );
         }
@@ -63,7 +62,7 @@ public static class Drop extends TurtleInventoryEvent
     {
         private final ItemStack stack;
 
-        public Drop( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable IItemHandler handler, @Nonnull ItemStack stack )
+        public Drop( @Nonnull ITurtleAccess turtle, @Nonnull FakePlayer player, @Nonnull World world, @Nonnull BlockPos pos, @Nullable Inventory handler, @Nonnull ItemStack stack )
         {
             super( turtle, TurtleAction.DROP, player, world, pos, handler );
 
diff --git a/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java b/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java
index 2ab9e8b3e6..3d7534dcd0 100644
--- a/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java
+++ b/src/main/java/dan200/computercraft/api/turtle/event/TurtlePlayerEvent.java
@@ -7,7 +7,6 @@
 package dan200.computercraft.api.turtle.event;
 
 import dan200.computercraft.api.turtle.ITurtleAccess;
-import net.minecraftforge.common.util.FakePlayer;
 
 import javax.annotation.Nonnull;
 import java.util.Objects;
diff --git a/src/main/java/dan200/computercraft/client/ClientRegistry.java b/src/main/java/dan200/computercraft/client/ClientRegistry.java
index ff4bcfd666..6ebb1bb4dc 100644
--- a/src/main/java/dan200/computercraft/client/ClientRegistry.java
+++ b/src/main/java/dan200/computercraft/client/ClientRegistry.java
@@ -7,36 +7,23 @@
 package dan200.computercraft.client;
 
 import dan200.computercraft.ComputerCraft;
-import dan200.computercraft.client.render.TurtleModelLoader;
 import dan200.computercraft.shared.common.IColouredItem;
 import dan200.computercraft.shared.media.items.ItemDisk;
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
 import dan200.computercraft.shared.util.Colour;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.IUnbakedModel;
-import net.minecraft.client.renderer.model.ModelResourceLocation;
-import net.minecraft.client.renderer.model.ModelRotation;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.resources.IResourceManager;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.client.event.ColorHandlerEvent;
-import net.minecraftforge.client.event.ModelBakeEvent;
-import net.minecraftforge.client.event.ModelRegistryEvent;
-import net.minecraftforge.client.event.TextureStitchEvent;
-import net.minecraftforge.client.model.ModelLoader;
-import net.minecraftforge.client.model.ModelLoaderRegistry;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
+import net.fabricmc.fabric.api.client.render.ColorProviderRegistry;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.ModelRotation;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.SpriteAtlasTexture;
 
 import java.util.HashSet;
-import java.util.Map;
 
 /**
  * Registers textures and models for items.
  */
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
 public final class ClientRegistry
 {
     private static final String[] EXTRA_MODELS = new String[] {
@@ -70,37 +57,38 @@ public final class ClientRegistry
 
     private ClientRegistry() {}
 
-    @SubscribeEvent
+    /*
+    TODO: @SubscribeEvent
     public static void registerModels( ModelRegistryEvent event )
     {
         ModelLoaderRegistry.registerLoader( TurtleModelLoader.INSTANCE );
     }
 
-    @SubscribeEvent
+    TODO: @SubscribeEvent
     public static void onTextureStitchEvent( TextureStitchEvent.Pre event )
     {
-        IResourceManager manager = Minecraft.getInstance().getResourceManager();
+        ResourceManager manager = MinecraftClient.getInstance().getResourceManager();
         for( String extra : EXTRA_TEXTURES )
         {
-            event.getMap().registerSprite( manager, new ResourceLocation( ComputerCraft.MOD_ID, extra ) );
+            event.getMap().registerSprite( manager, new Identifier( ComputerCraft.MOD_ID, extra ) );
         }
     }
 
-    @SubscribeEvent
+    TODO: @SubscribeEvent
     public static void onModelBakeEvent( ModelBakeEvent event )
     {
         // Load all extra models
         ModelLoader loader = event.getModelLoader();
-        Map<ModelResourceLocation, IBakedModel> registry = event.getModelRegistry();
+        Map<ModelIdentifier, BakedModel> registry = event.getModelRegistry();
 
         for( String model : EXTRA_MODELS )
         {
-            IBakedModel bakedModel = bake( loader, loader.getUnbakedModel( new ResourceLocation( ComputerCraft.MOD_ID, "item/" + model ) ) );
+            BakedModel bakedModel = bake( loader, loader.getOrLoadModel( new Identifier( ComputerCraft.MOD_ID, "item/" + model ) ) );
 
             if( bakedModel != null )
             {
                 registry.put(
-                    new ModelResourceLocation( new ResourceLocation( ComputerCraft.MOD_ID, model ), "inventory" ),
+                    new ModelIdentifier( new Identifier( ComputerCraft.MOD_ID, model ), "inventory" ),
                     bakedModel
                 );
             }
@@ -108,25 +96,25 @@ public static void onModelBakeEvent( ModelBakeEvent event )
 
         // And load the custom turtle models in too.
         registry.put(
-            new ModelResourceLocation( new ResourceLocation( ComputerCraft.MOD_ID, "turtle_normal" ), "inventory" ),
-            bake( loader, TurtleModelLoader.INSTANCE.loadModel( new ResourceLocation( ComputerCraft.MOD_ID, "item/turtle_normal" ) ) )
+            new ModelIdentifier( new Identifier( ComputerCraft.MOD_ID, "turtle_normal" ), "inventory" ),
+            bake( loader, TurtleModelLoader.INSTANCE.loadModel( new Identifier( ComputerCraft.MOD_ID, "item/turtle_normal" ) ) )
         );
 
         registry.put(
-            new ModelResourceLocation( new ResourceLocation( ComputerCraft.MOD_ID, "turtle_advanced" ), "inventory" ),
-            bake( loader, TurtleModelLoader.INSTANCE.loadModel( new ResourceLocation( ComputerCraft.MOD_ID, "item/turtle_advanced" ) ) )
+            new ModelIdentifier( new Identifier( ComputerCraft.MOD_ID, "turtle_advanced" ), "inventory" ),
+            bake( loader, TurtleModelLoader.INSTANCE.loadModel( new Identifier( ComputerCraft.MOD_ID, "item/turtle_advanced" ) ) )
         );
     }
+    */
 
-    @SubscribeEvent
-    public static void onItemColours( ColorHandlerEvent.Item event )
+    public static void onItemColours()
     {
-        event.getItemColors().register(
+        ColorProviderRegistry.ITEM.register(
             ( stack, layer ) -> layer == 1 ? ((ItemDisk) stack.getItem()).getColour( stack ) : 0xFFFFFF,
             ComputerCraft.Items.disk
         );
 
-        event.getItemColors().register( ( stack, layer ) -> {
+        ColorProviderRegistry.ITEM.register( ( stack, layer ) -> {
             switch( layer )
             {
                 case 0:
@@ -143,20 +131,16 @@ public static void onItemColours( ColorHandlerEvent.Item event )
         }, ComputerCraft.Items.pocketComputerNormal, ComputerCraft.Items.pocketComputerAdvanced );
 
         // Setup turtle colours
-        event.getItemColors().register(
+        ColorProviderRegistry.ITEM.register(
             ( stack, tintIndex ) -> tintIndex == 0 ? ((IColouredItem) stack.getItem()).getColour( stack ) : 0xFFFFFF,
             ComputerCraft.Blocks.turtleNormal, ComputerCraft.Blocks.turtleAdvanced
         );
     }
 
-    private static IBakedModel bake( ModelLoader loader, IUnbakedModel model )
+    private static BakedModel bake( ModelLoader loader, UnbakedModel model )
     {
-        model.getTextures( loader::getUnbakedModel, new HashSet<>() );
-
-        return model.bake(
-            loader::getUnbakedModel,
-            ModelLoader.defaultTextureGetter(),
-            ModelRotation.X0_Y0, false, DefaultVertexFormats.BLOCK
-        );
+        model.getTextureDependencies( loader::getOrLoadModel, new HashSet<>() );
+        SpriteAtlasTexture sprite = MinecraftClient.getInstance().getSpriteAtlas();
+        return model.bake( loader, sprite::getSprite, ModelRotation.X0_Y0 );
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/ClientTableFormatter.java b/src/main/java/dan200/computercraft/client/ClientTableFormatter.java
index 1445cd87dc..581e430222 100644
--- a/src/main/java/dan200/computercraft/client/ClientTableFormatter.java
+++ b/src/main/java/dan200/computercraft/client/ClientTableFormatter.java
@@ -10,13 +10,13 @@
 import dan200.computercraft.shared.command.text.TableBuilder;
 import dan200.computercraft.shared.command.text.TableFormatter;
 import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.FontRenderer;
-import net.minecraft.client.gui.GuiNewChat;
-import net.minecraft.client.gui.GuiUtilRenderComponents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.hud.ChatHud;
+import net.minecraft.client.util.TextComponentUtil;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TextFormat;
 import net.minecraft.util.math.MathHelper;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextFormatting;
 import org.apache.commons.lang3.StringUtils;
 
 import javax.annotation.Nullable;
@@ -28,25 +28,25 @@ public class ClientTableFormatter implements TableFormatter
 
     private static Int2IntOpenHashMap lastHeights = new Int2IntOpenHashMap();
 
-    private static FontRenderer renderer()
+    private static TextRenderer renderer()
     {
-        return Minecraft.getInstance().fontRenderer;
+        return MinecraftClient.getInstance().textRenderer;
     }
 
     @Override
     @Nullable
-    public ITextComponent getPadding( ITextComponent component, int width )
+    public TextComponent getPadding( TextComponent component, int width )
     {
         int extraWidth = width - getWidth( component );
         if( extraWidth <= 0 ) return null;
 
-        FontRenderer renderer = renderer();
+        TextRenderer renderer = renderer();
 
-        float spaceWidth = renderer.getStringWidth( " " );
+        float spaceWidth = renderer.getCharWidth( ' ' );
         int spaces = MathHelper.floor( extraWidth / spaceWidth );
         int extra = extraWidth - (int) (spaces * spaceWidth);
 
-        return ChatHelpers.coloured( StringUtils.repeat( ' ', spaces ) + StringUtils.repeat( (char) 712, extra ), TextFormatting.GRAY );
+        return ChatHelpers.coloured( StringUtils.repeat( ' ', spaces ) + StringUtils.repeat( (char) 712, extra ), TextFormat.GRAY );
     }
 
     @Override
@@ -56,34 +56,34 @@ public int getColumnPadding()
     }
 
     @Override
-    public int getWidth( ITextComponent component )
+    public int getWidth( TextComponent component )
     {
         return renderer().getStringWidth( component.getFormattedText() );
     }
 
     @Override
-    public void writeLine( int id, ITextComponent component )
+    public void writeLine( int id, TextComponent component )
     {
-        Minecraft mc = Minecraft.getInstance();
-        GuiNewChat chat = mc.ingameGUI.getChatGUI();
+        MinecraftClient mc = MinecraftClient.getInstance();
+        ChatHud chat = mc.inGameHud.getChatHud();
 
         // Trim the text if it goes over the allowed length
-        int maxWidth = MathHelper.floor( chat.getChatWidth() / chat.getScale() );
-        List<ITextComponent> list = GuiUtilRenderComponents.splitText( component, maxWidth, mc.fontRenderer, false, false );
-        if( !list.isEmpty() ) chat.printChatMessageWithOptionalDeletion( list.get( 0 ), id );
+        int maxWidth = MathHelper.floor( chat.getWidth() / chat.getScale() );
+        List<TextComponent> list = TextComponentUtil.wrapLines( component, maxWidth, mc.textRenderer, false, false );
+        if( !list.isEmpty() ) chat.addMessage( list.get( 0 ), id );
     }
 
     @Override
     public int display( TableBuilder table )
     {
-        GuiNewChat chat = Minecraft.getInstance().ingameGUI.getChatGUI();
+        ChatHud chat = MinecraftClient.getInstance().inGameHud.getChatHud();
 
         int lastHeight = lastHeights.get( table.getId() );
 
         int height = TableFormatter.super.display( table );
         lastHeights.put( table.getId(), height );
 
-        for( int i = height; i < lastHeight; i++ ) chat.deleteChatLine( i + table.getId() );
+        for( int i = height; i < lastHeight; i++ ) chat.removeMessage( i + table.getId() );
         return height;
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/FrameInfo.java b/src/main/java/dan200/computercraft/client/FrameInfo.java
index 0466b86848..a5b5d5b4ee 100644
--- a/src/main/java/dan200/computercraft/client/FrameInfo.java
+++ b/src/main/java/dan200/computercraft/client/FrameInfo.java
@@ -6,13 +6,6 @@
 
 package dan200.computercraft.client;
 
-import dan200.computercraft.ComputerCraft;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.common.gameevent.TickEvent;
-
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
 public final class FrameInfo
 {
     private static int tick;
@@ -32,15 +25,13 @@ public static long getRenderFrame()
         return renderFrame;
     }
 
-    @SubscribeEvent
-    public static void onTick( TickEvent.ClientTickEvent event )
+    public static void onTick()
     {
-        if( event.phase == TickEvent.Phase.START ) tick++;
+        tick++;
     }
 
-    @SubscribeEvent
-    public static void onRenderTick( TickEvent.RenderTickEvent event )
+    public static void onRenderFrame()
     {
-        if( event.phase == TickEvent.Phase.START ) renderFrame++;
+        renderFrame++;
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java b/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java
index b2e22db059..8b7a53a6a9 100644
--- a/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java
+++ b/src/main/java/dan200/computercraft/client/gui/FixedWidthFontRenderer.java
@@ -6,23 +6,23 @@
 
 package dan200.computercraft.client.gui;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.core.terminal.TextBuffer;
 import dan200.computercraft.shared.util.Palette;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.texture.TextureManager;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.texture.TextureManager;
+import net.minecraft.util.Identifier;
 import org.lwjgl.opengl.GL11;
 
 import java.util.Arrays;
 
 public final class FixedWidthFontRenderer
 {
-    private static final ResourceLocation FONT = new ResourceLocation( "computercraft", "textures/gui/term_font.png" );
-    public static final ResourceLocation BACKGROUND = new ResourceLocation( "computercraft", "textures/gui/term_background.png" );
+    private static final Identifier FONT = new Identifier( "computercraft", "textures/gui/term_font.png" );
+    public static final Identifier BACKGROUND = new Identifier( "computercraft", "textures/gui/term_background.png" );
 
     public static final int FONT_HEIGHT = 9;
     public static final int FONT_WIDTH = 6;
@@ -39,7 +39,7 @@ public static FixedWidthFontRenderer instance()
 
     private FixedWidthFontRenderer()
     {
-        m_textureManager = Minecraft.getInstance().getTextureManager();
+        m_textureManager = MinecraftClient.getInstance().getTextureManager();
     }
 
     private static void greyscaleify( double[] rgb )
@@ -64,12 +64,12 @@ private void drawChar( BufferBuilder renderer, double x, double y, int index, in
         int xStart = 1 + column * (FONT_WIDTH + 2);
         int yStart = 1 + row * (FONT_HEIGHT + 2);
 
-        renderer.pos( x, y, 0.0 ).tex( xStart / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x, y + FONT_HEIGHT, 0.0 ).tex( xStart / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x + FONT_WIDTH, y, 0.0 ).tex( (xStart + FONT_WIDTH) / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x + FONT_WIDTH, y, 0.0 ).tex( (xStart + FONT_WIDTH) / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x, y + FONT_HEIGHT, 0.0 ).tex( xStart / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x + FONT_WIDTH, y + FONT_HEIGHT, 0.0 ).tex( (xStart + FONT_WIDTH) / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).endVertex();
+        renderer.vertex( x, y, 0.0 ).texture( xStart / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x, y + FONT_HEIGHT, 0.0 ).texture( xStart / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x + FONT_WIDTH, y, 0.0 ).texture( (xStart + FONT_WIDTH) / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x + FONT_WIDTH, y, 0.0 ).texture( (xStart + FONT_WIDTH) / 256.0, yStart / 256.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x, y + FONT_HEIGHT, 0.0 ).texture( xStart / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x + FONT_WIDTH, y + FONT_HEIGHT, 0.0 ).texture( (xStart + FONT_WIDTH) / 256.0, (yStart + FONT_HEIGHT) / 256.0 ).color( r, g, b, 1.0f ).next();
     }
 
     private void drawQuad( BufferBuilder renderer, double x, double y, int color, double width, Palette p, boolean greyscale )
@@ -83,12 +83,12 @@ private void drawQuad( BufferBuilder renderer, double x, double y, int color, do
         float g = (float) colour[1];
         float b = (float) colour[2];
 
-        renderer.pos( x, y, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x + width, y, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x + width, y, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-        renderer.pos( x + width, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).endVertex();
+        renderer.vertex( x, y, 0.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x + width, y, 0.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x + width, y, 0.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).next();
+        renderer.vertex( x + width, y + FONT_HEIGHT, 0.0 ).color( r, g, b, 1.0f ).next();
     }
 
     private boolean isGreyScale( int colour )
@@ -100,8 +100,8 @@ public void drawStringBackgroundPart( int x, int y, TextBuffer backgroundColour,
     {
         // Draw the quads
         Tessellator tessellator = Tessellator.getInstance();
-        BufferBuilder renderer = tessellator.getBuffer();
-        renderer.begin( GL11.GL_TRIANGLES, DefaultVertexFormats.POSITION_COLOR );
+        BufferBuilder renderer = tessellator.getBufferBuilder();
+        renderer.begin( GL11.GL_TRIANGLES, VertexFormats.POSITION_COLOR );
         if( leftMarginSize > 0.0 )
         {
             int colour1 = "0123456789abcdef".indexOf( backgroundColour.charAt( 0 ) );
@@ -129,17 +129,17 @@ public void drawStringBackgroundPart( int x, int y, TextBuffer backgroundColour,
             }
             drawQuad( renderer, x + i * FONT_WIDTH, y, colour, FONT_WIDTH, p, greyScale );
         }
-        GlStateManager.disableTexture2D();
+        GlStateManager.disableTexture();
         tessellator.draw();
-        GlStateManager.enableTexture2D();
+        GlStateManager.enableTexture();
     }
 
     public void drawStringTextPart( int x, int y, TextBuffer s, TextBuffer textColour, boolean greyScale, Palette p )
     {
         // Draw the quads
         Tessellator tessellator = Tessellator.getInstance();
-        BufferBuilder renderer = tessellator.getBuffer();
-        renderer.begin( GL11.GL_TRIANGLES, DefaultVertexFormats.POSITION_TEX_COLOR );
+        BufferBuilder renderer = tessellator.getBufferBuilder();
+        renderer.begin( GL11.GL_TRIANGLES, VertexFormats.POSITION_UV_COLOR );
         for( int i = 0; i < s.length(); i++ )
         {
             // Switch colour
@@ -195,6 +195,6 @@ public int getStringWidth( String s )
     public void bindFont()
     {
         m_textureManager.bindTexture( FONT );
-        GlStateManager.texParameteri( GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP );
+        GlStateManager.texParameter( GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP );
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/gui/GuiComputer.java b/src/main/java/dan200/computercraft/client/gui/GuiComputer.java
index e0e7ee6fe6..44183e2e69 100644
--- a/src/main/java/dan200/computercraft/client/gui/GuiComputer.java
+++ b/src/main/java/dan200/computercraft/client/gui/GuiComputer.java
@@ -6,6 +6,7 @@
 
 package dan200.computercraft.client.gui;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.client.gui.widgets.WidgetTerminal;
 import dan200.computercraft.client.gui.widgets.WidgetWrapper;
@@ -13,16 +14,17 @@
 import dan200.computercraft.shared.computer.core.ClientComputer;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.inventory.ContainerComputer;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.inventory.Container;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.client.gui.ContainerScreen;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.util.Identifier;
 
-public class GuiComputer extends GuiContainer
+public class GuiComputer<T extends Container> extends ContainerScreen<T>
 {
-    private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation( "computercraft", "textures/gui/corners_normal.png" );
-    private static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation( "computercraft", "textures/gui/corners_advanced.png" );
-    private static final ResourceLocation BACKGROUND_COMMAND = new ResourceLocation( "computercraft", "textures/gui/corners_command.png" );
+    private static final Identifier BACKGROUND_NORMAL = new Identifier( "computercraft", "textures/gui/corners_normal.png" );
+    private static final Identifier BACKGROUND_ADVANCED = new Identifier( "computercraft", "textures/gui/corners_advanced.png" );
+    private static final Identifier BACKGROUND_COMMAND = new Identifier( "computercraft", "textures/gui/corners_command.png" );
 
     private final ComputerFamily m_family;
     private final ClientComputer m_computer;
@@ -32,9 +34,11 @@ public class GuiComputer extends GuiContainer
     private WidgetTerminal terminal;
     private WidgetWrapper terminalWrapper;
 
-    public GuiComputer( Container container, ComputerFamily family, ClientComputer computer, int termWidth, int termHeight )
+
+    public GuiComputer( T container, PlayerInventory player, ComputerFamily family, ClientComputer computer, int termWidth, int termHeight )
     {
-        super( container );
+        super( container, player, new StringTextComponent( "" ) );
+
         m_family = family;
         m_computer = computer;
         m_termWidth = termWidth;
@@ -42,10 +46,10 @@ public GuiComputer( Container container, ComputerFamily family, ClientComputer c
         terminal = null;
     }
 
-    public GuiComputer( TileComputer computer )
+    public static GuiComputer<ContainerComputer> create( int id, TileComputer computer, PlayerInventory player )
     {
-        this(
-            new ContainerComputer( computer ),
+        return new GuiComputer<>(
+            new ContainerComputer( id, computer ), player,
             computer.getFamily(),
             computer.createClientComputer(),
             ComputerCraft.terminalWidth_computer,
@@ -54,32 +58,32 @@ public GuiComputer( TileComputer computer )
     }
 
     @Override
-    protected void initGui()
+    protected void init()
     {
-        mc.keyboardListener.enableRepeatEvents( true );
+        minecraft.keyboard.enableRepeatEvents( true );
 
         int termPxWidth = m_termWidth * FixedWidthFontRenderer.FONT_WIDTH;
         int termPxHeight = m_termHeight * FixedWidthFontRenderer.FONT_HEIGHT;
 
-        xSize = termPxWidth + 4 + 24;
-        ySize = termPxHeight + 4 + 24;
+        containerWidth = termPxWidth + 4 + 24;
+        containerHeight = termPxHeight + 4 + 24;
 
-        super.initGui();
+        super.init();
 
-        terminal = new WidgetTerminal( mc, () -> m_computer, m_termWidth, m_termHeight, 2, 2, 2, 2 );
-        terminalWrapper = new WidgetWrapper( terminal, 2 + 12 + guiLeft, 2 + 12 + guiTop, termPxWidth, termPxHeight );
+        terminal = new WidgetTerminal( minecraft, () -> m_computer, m_termWidth, m_termHeight, 2, 2, 2, 2 );
+        terminalWrapper = new WidgetWrapper( terminal, 2 + 12 + left, 2 + 12 + top, termPxWidth, termPxHeight );
 
         children.add( terminalWrapper );
         setFocused( terminalWrapper );
     }
 
     @Override
-    public void onGuiClosed()
+    public void removed()
     {
-        super.onGuiClosed();
+        super.removed();
         children.remove( terminal );
         terminal = null;
-        mc.keyboardListener.enableRepeatEvents( false );
+        minecraft.keyboard.enableRepeatEvents( false );
     }
 
     @Override
@@ -90,7 +94,7 @@ public void tick()
     }
 
     @Override
-    public void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX, int mouseY )
+    public void drawBackground( float partialTicks, int mouseX, int mouseY )
     {
         // Work out where to draw
         int startX = terminalWrapper.getX() - 2;
@@ -107,34 +111,34 @@ public void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX, int
         {
             case Normal:
             default:
-                mc.getTextureManager().bindTexture( BACKGROUND_NORMAL );
+                minecraft.getTextureManager().bindTexture( BACKGROUND_NORMAL );
                 break;
             case Advanced:
-                mc.getTextureManager().bindTexture( BACKGROUND_ADVANCED );
+                minecraft.getTextureManager().bindTexture( BACKGROUND_ADVANCED );
                 break;
             case Command:
-                mc.getTextureManager().bindTexture( BACKGROUND_COMMAND );
+                minecraft.getTextureManager().bindTexture( BACKGROUND_COMMAND );
                 break;
         }
 
-        drawTexturedModalRect( startX - 12, startY - 12, 12, 28, 12, 12 );
-        drawTexturedModalRect( startX - 12, endY, 12, 40, 12, 16 );
-        drawTexturedModalRect( endX, startY - 12, 24, 28, 12, 12 );
-        drawTexturedModalRect( endX, endY, 24, 40, 12, 16 );
+        blit( startX - 12, startY - 12, 12, 28, 12, 12 );
+        blit( startX - 12, endY, 12, 40, 12, 16 );
+        blit( endX, startY - 12, 24, 28, 12, 12 );
+        blit( endX, endY, 24, 40, 12, 16 );
 
-        drawTexturedModalRect( startX, startY - 12, 0, 0, endX - startX, 12 );
-        drawTexturedModalRect( startX, endY, 0, 12, endX - startX, 16 );
+        blit( startX, startY - 12, 0, 0, endX - startX, 12 );
+        blit( startX, endY, 0, 12, endX - startX, 16 );
 
-        drawTexturedModalRect( startX - 12, startY, 0, 28, 12, endY - startY );
-        drawTexturedModalRect( endX, startY, 36, 28, 12, endY - startY );
+        blit( startX - 12, startY, 0, 28, 12, endY - startY );
+        blit( endX, startY, 36, 28, 12, endY - startY );
     }
 
     @Override
     public void render( int mouseX, int mouseY, float partialTicks )
     {
-        drawDefaultBackground();
+        renderBackground( 0 );
         super.render( mouseX, mouseY, partialTicks );
-        renderHoveredToolTip( mouseX, mouseY );
+        drawMouseoverTooltip( mouseX, mouseY );
     }
 
     @Override
diff --git a/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java b/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java
index ab42848390..6a5f03c814 100644
--- a/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java
+++ b/src/main/java/dan200/computercraft/client/gui/GuiDiskDrive.java
@@ -6,45 +6,44 @@
 
 package dan200.computercraft.client.gui;
 
+import com.mojang.blaze3d.platform.GlStateManager;
+import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.peripheral.diskdrive.ContainerDiskDrive;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.resources.I18n;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.client.gui.ContainerScreen;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.util.Identifier;
 
-public class GuiDiskDrive extends GuiContainer
+public class GuiDiskDrive extends ContainerScreen<ContainerDiskDrive>
 {
-    private static final ResourceLocation BACKGROUND = new ResourceLocation( "computercraft", "textures/gui/disk_drive.png" );
+    private static final Identifier BACKGROUND = new Identifier( "computercraft", "textures/gui/disk_drive.png" );
 
-    private final ContainerDiskDrive m_container;
-
-    public GuiDiskDrive( ContainerDiskDrive container )
+    public GuiDiskDrive( ContainerDiskDrive container, PlayerInventory inventory )
     {
-        super( container );
-        m_container = container;
+        super( container, inventory, ComputerCraft.Blocks.diskDrive.getTextComponent() );
     }
 
     @Override
-    protected void drawGuiContainerForegroundLayer( int mouseX, int mouseY )
+    protected void drawForeground( int par1, int par2 )
     {
-        String title = m_container.getDiskDrive().getDisplayName().getString();
-        fontRenderer.drawString( title, (xSize - fontRenderer.getStringWidth( title )) / 2.0f, 6, 0x404040 );
-        fontRenderer.drawString( I18n.format( "container.inventory" ), 8, ySize - 96 + 2, 0x404040 );
+        String title = getTitle().getFormattedText();
+        font.draw( title, (containerWidth - font.getStringWidth( title )) / 2.0f, 6, 0x404040 );
+        font.draw( I18n.translate( "container.inventory" ), 8, (containerHeight - 96) + 2, 0x404040 );
     }
 
     @Override
-    protected void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX, int mouseY )
+    protected void drawBackground( float partialTicks, int mouseX, int mouseY )
     {
         GlStateManager.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
-        mc.getTextureManager().bindTexture( BACKGROUND );
-        drawTexturedModalRect( guiLeft, guiTop, 0, 0, xSize, ySize );
+        minecraft.getTextureManager().bindTexture( BACKGROUND );
+        blit( left, top, 0, 0, containerWidth, containerHeight );
     }
 
     @Override
     public void render( int mouseX, int mouseY, float partialTicks )
     {
-        drawDefaultBackground();
+        renderBackground();
         super.render( mouseX, mouseY, partialTicks );
-        renderHoveredToolTip( mouseX, mouseY );
+        drawMouseoverTooltip( mouseX, mouseY );
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/gui/GuiPocketComputer.java b/src/main/java/dan200/computercraft/client/gui/GuiPocketComputer.java
index e3781ae964..8220a80b3d 100644
--- a/src/main/java/dan200/computercraft/client/gui/GuiPocketComputer.java
+++ b/src/main/java/dan200/computercraft/client/gui/GuiPocketComputer.java
@@ -10,15 +10,16 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer;
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
+import net.minecraft.entity.player.PlayerInventory;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
 
-public class GuiPocketComputer extends GuiComputer
+public class GuiPocketComputer extends GuiComputer<ContainerPocketComputer>
 {
-    public GuiPocketComputer( ContainerPocketComputer container )
+    public GuiPocketComputer( ContainerPocketComputer container, PlayerInventory player )
     {
         super(
-            container,
+            container, player,
             getFamily( container.getStack() ),
             ItemPocketComputer.createClientComputer( container.getStack() ),
             ComputerCraft.terminalWidth_pocketComputer,
diff --git a/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java b/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java
index 80e146f522..46834ea703 100644
--- a/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java
+++ b/src/main/java/dan200/computercraft/client/gui/GuiPrinter.java
@@ -6,47 +6,46 @@
 
 package dan200.computercraft.client.gui;
 
+import com.mojang.blaze3d.platform.GlStateManager;
+import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.peripheral.printer.ContainerPrinter;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.resources.I18n;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.client.gui.ContainerScreen;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.util.Identifier;
 
-public class GuiPrinter extends GuiContainer
+public class GuiPrinter extends ContainerScreen<ContainerPrinter>
 {
-    private static final ResourceLocation BACKGROUND = new ResourceLocation( "computercraft", "textures/gui/printer.png" );
+    private static final Identifier BACKGROUND = new Identifier( "computercraft", "textures/gui/printer.png" );
 
-    private final ContainerPrinter container;
-
-    public GuiPrinter( ContainerPrinter container )
+    public GuiPrinter( ContainerPrinter container, PlayerInventory player )
     {
-        super( container );
-        this.container = container;
+        super( container, player, ComputerCraft.Blocks.printer.getTextComponent() );
     }
 
     @Override
-    protected void drawGuiContainerForegroundLayer( int mouseX, int mouseY )
+    protected void drawForeground( int mouseX, int mouseY )
     {
-        String title = container.getPrinter().getDisplayName().getString();
-        fontRenderer.drawString( title, (xSize - fontRenderer.getStringWidth( title )) / 2.0f, 6, 0x404040 );
-        fontRenderer.drawString( I18n.format( "container.inventory" ), 8, ySize - 96 + 2, 0x404040 );
+        String title = getTitle().getFormattedText();
+        font.draw( title, (containerWidth - font.getStringWidth( title )) / 2.0f, 6, 0x404040 );
+        font.draw( I18n.translate( "container.inventory" ), 8, containerHeight - 96 + 2, 0x404040 );
     }
 
     @Override
-    protected void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX, int mouseY )
+    protected void drawBackground( float f, int i, int j )
     {
         GlStateManager.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
-        mc.getTextureManager().bindTexture( BACKGROUND );
-        drawTexturedModalRect( guiLeft, guiTop, 0, 0, xSize, ySize );
+        minecraft.getTextureManager().bindTexture( BACKGROUND );
+        blit( left, top, 0, 0, containerWidth, containerHeight );
 
-        if( container.isPrinting() ) drawTexturedModalRect( guiLeft + 34, guiTop + 21, 176, 0, 25, 45 );
+        if( container.isPrinting() ) blit( left + 34, top + 21, 176, 0, 25, 45 );
     }
 
     @Override
     public void render( int mouseX, int mouseY, float partialTicks )
     {
-        drawDefaultBackground();
+        renderBackground();
         super.render( mouseX, mouseY, partialTicks );
-        renderHoveredToolTip( mouseX, mouseY );
+        drawMouseoverTooltip( mouseX, mouseY );
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java b/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java
index f1349bc74c..c2deaf52dc 100644
--- a/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java
+++ b/src/main/java/dan200/computercraft/client/gui/GuiPrintout.java
@@ -6,16 +6,17 @@
 
 package dan200.computercraft.client.gui;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.core.terminal.TextBuffer;
 import dan200.computercraft.shared.common.ContainerHeldItem;
 import dan200.computercraft.shared.media.items.ItemPrintout;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.gui.ContainerScreen;
+import net.minecraft.entity.player.PlayerInventory;
 import org.lwjgl.glfw.GLFW;
 
 import static dan200.computercraft.client.render.PrintoutRenderer.*;
 
-public class GuiPrintout extends GuiContainer
+public class GuiPrintout extends ContainerScreen<ContainerHeldItem>
 {
     private final boolean m_book;
     private final int m_pages;
@@ -23,11 +24,11 @@ public class GuiPrintout extends GuiContainer
     private final TextBuffer[] m_colours;
     private int m_page;
 
-    public GuiPrintout( ContainerHeldItem container )
+    public GuiPrintout( ContainerHeldItem container, PlayerInventory player )
     {
-        super( container );
+        super( container, player, container.getStack().getDisplayName() );
 
-        ySize = Y_SIZE;
+        containerHeight = Y_SIZE;
 
         String[] text = ItemPrintout.getText( container.getStack() );
         m_text = new TextBuffer[text.length];
@@ -63,9 +64,9 @@ public boolean keyPressed( int key, int scancode, int modifiers )
     }
 
     @Override
-    public boolean mouseScrolled( double delta )
+    public boolean mouseScrolled( double x, double y, double delta )
     {
-        if( super.mouseScrolled( delta ) ) return true;
+        if( super.mouseScrolled( x, y, delta ) ) return true;
         if( delta < 0 )
         {
             // Scroll up goes to the next page
@@ -84,25 +85,25 @@ public boolean mouseScrolled( double delta )
     }
 
     @Override
-    public void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX, int mouseY )
+    public void drawBackground( float partialTicks, int mouseX, int mouseY )
     {
         // Draw the printout
         GlStateManager.color4f( 1.0f, 1.0f, 1.0f, 1.0f );
         GlStateManager.enableDepthTest();
 
-        drawBorder( guiLeft, guiTop, zLevel, m_page, m_pages, m_book );
-        drawText( guiLeft + X_TEXT_MARGIN, guiTop + Y_TEXT_MARGIN, ItemPrintout.LINES_PER_PAGE * m_page, m_text, m_colours );
+        drawBorder( left, top, blitOffset, m_page, m_pages, m_book );
+        drawText( left + X_TEXT_MARGIN, top + Y_TEXT_MARGIN, ItemPrintout.LINES_PER_PAGE * m_page, m_text, m_colours );
     }
 
     @Override
     public void render( int mouseX, int mouseY, float partialTicks )
     {
         // We must take the background further back in order to not overlap with our printed pages.
-        zLevel--;
-        drawDefaultBackground();
-        zLevel++;
+        blitOffset--;
+        renderBackground();
+        blitOffset++;
 
         super.render( mouseX, mouseY, partialTicks );
-        renderHoveredToolTip( mouseX, mouseY );
+        drawMouseoverTooltip( mouseX, mouseY );
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java b/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java
index 9274fd26d4..018220c312 100644
--- a/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java
+++ b/src/main/java/dan200/computercraft/client/gui/GuiTurtle.java
@@ -6,6 +6,7 @@
 
 package dan200.computercraft.client.gui;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.client.gui.widgets.WidgetTerminal;
 import dan200.computercraft.client.gui.widgets.WidgetWrapper;
@@ -13,14 +14,14 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.turtle.blocks.TileTurtle;
 import dan200.computercraft.shared.turtle.inventory.ContainerTurtle;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.client.gui.ContainerScreen;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.util.Identifier;
 
-public class GuiTurtle extends GuiContainer
+public class GuiTurtle extends ContainerScreen<ContainerTurtle>
 {
-    private static final ResourceLocation BACKGROUND_NORMAL = new ResourceLocation( "computercraft", "textures/gui/turtle_normal.png" );
-    private static final ResourceLocation BACKGROUND_ADVANCED = new ResourceLocation( "computercraft", "textures/gui/turtle_advanced.png" );
+    private static final Identifier BACKGROUND_NORMAL = new Identifier( "computercraft", "textures/gui/turtle_normal.png" );
+    private static final Identifier BACKGROUND_ADVANCED = new Identifier( "computercraft", "textures/gui/turtle_advanced.png" );
 
     private ContainerTurtle m_container;
 
@@ -30,45 +31,46 @@ public class GuiTurtle extends GuiContainer
     private WidgetTerminal terminal;
     private WidgetWrapper terminalWrapper;
 
-    public GuiTurtle( TileTurtle turtle, ContainerTurtle container )
+    public GuiTurtle( TileTurtle turtle, ContainerTurtle container, PlayerInventory player )
     {
-        super( container );
+        super( container, player, turtle.getDisplayName() );
 
         m_container = container;
         m_family = turtle.getFamily();
         m_computer = turtle.getClientComputer();
 
-        xSize = 254;
-        ySize = 217;
+        containerWidth = 254;
+        containerHeight = 217;
     }
 
     @Override
-    protected void initGui()
+    protected void init()
     {
-        super.initGui();
-        mc.keyboardListener.enableRepeatEvents( true );
+        super.init();
+        minecraft.keyboard.enableRepeatEvents( true );
 
         int termPxWidth = ComputerCraft.terminalWidth_turtle * FixedWidthFontRenderer.FONT_WIDTH;
         int termPxHeight = ComputerCraft.terminalHeight_turtle * FixedWidthFontRenderer.FONT_HEIGHT;
 
         terminal = new WidgetTerminal(
-            mc, () -> m_computer,
+            minecraft, () -> m_computer,
             ComputerCraft.terminalWidth_turtle,
             ComputerCraft.terminalHeight_turtle,
             2, 2, 2, 2
         );
-        terminalWrapper = new WidgetWrapper( terminal, 2 + 8 + guiLeft, 2 + 8 + guiTop, termPxWidth, termPxHeight );
+        terminalWrapper = new WidgetWrapper( terminal, 2 + 8 + left, 2 + 8 + top, termPxWidth, termPxHeight );
 
         children.add( terminalWrapper );
         setFocused( terminalWrapper );
     }
 
     @Override
-    public void onGuiClosed()
+    public void removed()
     {
+        super.removed();
         children.remove( terminal );
         terminal = null;
-        mc.keyboardListener.enableRepeatEvents( false );
+        minecraft.keyboard.enableRepeatEvents( false );
     }
 
     @Override
@@ -87,13 +89,13 @@ private void drawSelectionSlot( boolean advanced )
             GlStateManager.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
             int slotX = slot % 4;
             int slotY = slot / 4;
-            mc.getTextureManager().bindTexture( advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL );
-            drawTexturedModalRect( guiLeft + m_container.m_turtleInvStartX - 2 + slotX * 18, guiTop + m_container.m_playerInvStartY - 2 + slotY * 18, 0, 217, 24, 24 );
+            minecraft.getTextureManager().bindTexture( advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL );
+            blit( left + m_container.m_turtleInvStartX - 2 + slotX * 18, top + m_container.m_playerInvStartY - 2 + slotY * 18, 0, 217, 24, 24 );
         }
     }
 
     @Override
-    protected void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX, int mouseY )
+    protected void drawBackground( float partialTicks, int mouseX, int mouseY )
     {
         // Draw term
         boolean advanced = m_family == ComputerFamily.Advanced;
@@ -101,8 +103,8 @@ protected void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX,
 
         // Draw border/inventory
         GlStateManager.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
-        mc.getTextureManager().bindTexture( advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL );
-        drawTexturedModalRect( guiLeft, guiTop, 0, 0, xSize, ySize );
+        minecraft.getTextureManager().bindTexture( advanced ? BACKGROUND_ADVANCED : BACKGROUND_NORMAL );
+        blit( left, top, 0, 0, containerWidth, containerHeight );
 
         drawSelectionSlot( advanced );
     }
@@ -110,8 +112,8 @@ protected void drawGuiContainerBackgroundLayer( float partialTicks, int mouseX,
     @Override
     public void render( int mouseX, int mouseY, float partialTicks )
     {
-        drawDefaultBackground();
+        renderBackground();
         super.render( mouseX, mouseY, partialTicks );
-        renderHoveredToolTip( mouseX, mouseY );
+        drawMouseoverTooltip( mouseX, mouseY );
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java
index 3f75ccbe3c..c01610cee1 100644
--- a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java
+++ b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetTerminal.java
@@ -6,6 +6,7 @@
 
 package dan200.computercraft.client.gui.widgets;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.client.FrameInfo;
 import dan200.computercraft.client.gui.FixedWidthFontRenderer;
 import dan200.computercraft.core.terminal.Terminal;
@@ -14,13 +15,12 @@
 import dan200.computercraft.shared.computer.core.IComputer;
 import dan200.computercraft.shared.util.Colour;
 import dan200.computercraft.shared.util.Palette;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.IGuiEventListener;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.SharedConstants;
+import net.minecraft.SharedConstants;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
 import org.lwjgl.glfw.GLFW;
 import org.lwjgl.opengl.GL11;
 
@@ -29,11 +29,11 @@
 
 import static dan200.computercraft.client.gui.FixedWidthFontRenderer.BACKGROUND;
 
-public class WidgetTerminal implements IGuiEventListener
+public class WidgetTerminal implements Element
 {
     private static final float TERMINATE_TIME = 0.5f;
 
-    private final Minecraft client;
+    private final MinecraftClient minecraft;
 
     private final Supplier<ClientComputer> computer;
     private final int termWidth;
@@ -54,9 +54,9 @@ public class WidgetTerminal implements IGuiEventListener
 
     private final BitSet keysDown = new BitSet( 256 );
 
-    public WidgetTerminal( Minecraft client, Supplier<ClientComputer> computer, int termWidth, int termHeight, int leftMargin, int rightMargin, int topMargin, int bottomMargin )
+    public WidgetTerminal( MinecraftClient minecraft, Supplier<ClientComputer> computer, int termWidth, int termHeight, int leftMargin, int rightMargin, int topMargin, int bottomMargin )
     {
-        this.client = client;
+        this.minecraft = minecraft;
         this.computer = computer;
         this.termWidth = termWidth;
         this.termHeight = termHeight;
@@ -98,7 +98,7 @@ public boolean keyPressed( int key, int scancode, int modifiers )
 
                 case GLFW.GLFW_KEY_V:
                     // Ctrl+V for paste
-                    String clipboard = client.keyboardListener.getClipboardString();
+                    String clipboard = minecraft.keyboard.getClipboard();
                     if( clipboard != null )
                     {
                         // Clip to the first occurrence of \r or \n
@@ -118,7 +118,7 @@ else if( newLineIndex2 >= 0 )
                         }
 
                         // Filter the string
-                        clipboard = SharedConstants.filterAllowedCharacters( clipboard );
+                        clipboard = SharedConstants.stripInvalidChars( clipboard );
                         if( !clipboard.isEmpty() )
                         {
                             // Clip to 512 characters and queue the event
@@ -250,14 +250,23 @@ public boolean mouseDragged( double mouseX, double mouseY, int button, double v2
     }
 
     @Override
-    public boolean mouseScrolled( double delta )
+    public boolean mouseScrolled( double mouseX, double mouseY, double delta )
     {
         ClientComputer computer = this.computer.get();
-        if( computer == null || !computer.isColour() ) return false;
+        if( computer == null || !computer.isColour() || delta == 0 ) return false;
 
-        if( lastMouseX >= 0 && lastMouseY >= 0 && delta != 0 )
+        Terminal term = computer.getTerminal();
+        if( term != null )
         {
-            queueEvent( "mouse_scroll", delta < 0 ? 1 : -1, lastMouseX + 1, lastMouseY + 1 );
+            int charX = (int) (mouseX / FixedWidthFontRenderer.FONT_WIDTH);
+            int charY = (int) (mouseY / FixedWidthFontRenderer.FONT_HEIGHT);
+            charX = Math.min( Math.max( charX, 0 ), term.getWidth() - 1 );
+            charY = Math.min( Math.max( charY, 0 ), term.getHeight() - 1 );
+
+            computer.mouseScroll( delta < 0 ? 1 : -1, charX + 1, charY + 1 );
+
+            lastMouseX = charX;
+            lastMouseY = charY;
         }
 
         return true;
@@ -284,7 +293,7 @@ public void update()
     }
 
     @Override
-    public void focusChanged( boolean focused )
+    public void onFocusChanged( boolean noClue, boolean focused )
     {
         if( !focused )
         {
@@ -384,15 +393,15 @@ public void draw( int originX, int originY )
                     int width = termWidth * FixedWidthFontRenderer.FONT_WIDTH + leftMargin + rightMargin;
                     int height = termHeight * FixedWidthFontRenderer.FONT_HEIGHT + topMargin + bottomMargin;
 
-                    client.getTextureManager().bindTexture( BACKGROUND );
+                    minecraft.getTextureManager().bindTexture( BACKGROUND );
 
                     Tessellator tesslector = Tessellator.getInstance();
-                    BufferBuilder buffer = tesslector.getBuffer();
-                    buffer.begin( GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX );
-                    buffer.pos( x, y + height, 0 ).tex( 0 / 256.0, height / 256.0 ).endVertex();
-                    buffer.pos( x + width, y + height, 0 ).tex( width / 256.0, height / 256.0 ).endVertex();
-                    buffer.pos( x + width, y, 0 ).tex( width / 256.0, 0 / 256.0 ).endVertex();
-                    buffer.pos( x, y, 0 ).tex( 0 / 256.0, 0 / 256.0 ).endVertex();
+                    BufferBuilder buffer = tesslector.getBufferBuilder();
+                    buffer.begin( GL11.GL_QUADS, VertexFormats.POSITION_UV );
+                    buffer.vertex( x, y + height, 0 ).texture( 0 / 256.0, height / 256.0 ).next();
+                    buffer.vertex( x + width, y + height, 0 ).texture( width / 256.0, height / 256.0 ).next();
+                    buffer.vertex( x + width, y, 0 ).texture( width / 256.0, 0 / 256.0 ).next();
+                    buffer.vertex( x, y, 0 ).texture( 0 / 256.0, 0 / 256.0 ).next();
                     tesslector.draw();
                 }
                 finally
diff --git a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java
index b657983a0b..828b03fe65 100644
--- a/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java
+++ b/src/main/java/dan200/computercraft/client/gui/widgets/WidgetWrapper.java
@@ -6,17 +6,17 @@
 
 package dan200.computercraft.client.gui.widgets;
 
-import net.minecraft.client.gui.IGuiEventListener;
+import net.minecraft.client.gui.Element;
 
-public class WidgetWrapper implements IGuiEventListener
+public class WidgetWrapper implements Element
 {
-    private final IGuiEventListener listener;
+    private final Element listener;
     private final int x;
     private final int y;
     private final int width;
     private final int height;
 
-    public WidgetWrapper( IGuiEventListener listener, int x, int y, int width, int height )
+    public WidgetWrapper( Element listener, int x, int y, int width, int height )
     {
         this.listener = listener;
         this.x = x;
@@ -26,15 +26,16 @@ public WidgetWrapper( IGuiEventListener listener, int x, int y, int width, int h
     }
 
     @Override
-    public void focusChanged( boolean b )
+    public void mouseMoved( double x, double y )
     {
-        listener.focusChanged( b );
+        double dx = x - this.x, dy = y - this.y;
+        if( dx >= 0 && dx < width && dy >= 0 && dy < height ) listener.mouseMoved( dx, dy );
     }
 
     @Override
-    public boolean canFocus()
+    public void onFocusChanged( boolean a, boolean b )
     {
-        return listener.canFocus();
+        listener.onFocusChanged( a, b );
     }
 
     @Override
@@ -59,9 +60,10 @@ public boolean mouseDragged( double x, double y, int button, double deltaX, doub
     }
 
     @Override
-    public boolean mouseScrolled( double delta )
+    public boolean mouseScrolled( double x, double y, double delta )
     {
-        return listener.mouseScrolled( delta );
+        double dx = x - this.x, dy = y - this.y;
+        return dx >= 0 && dx < width && dy >= 0 && dy < height && listener.mouseScrolled( dx, dy, delta );
     }
 
     @Override
@@ -101,4 +103,11 @@ public int getHeight()
     {
         return height;
     }
+
+    @Override
+    public boolean isMouseOver( double x, double y )
+    {
+        double dx = x - this.x, dy = y - this.y;
+        return dx >= 0 && dx < width && dy >= 0 && dy < height;
+    }
 }
diff --git a/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java b/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java
index 841ad57da0..7c676fb71c 100644
--- a/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java
+++ b/src/main/java/dan200/computercraft/client/proxy/ComputerCraftProxyClient.java
@@ -7,6 +7,7 @@
 package dan200.computercraft.client.proxy;
 
 import dan200.computercraft.ComputerCraft;
+import dan200.computercraft.client.ClientRegistry;
 import dan200.computercraft.client.gui.*;
 import dan200.computercraft.client.render.TileEntityCableRenderer;
 import dan200.computercraft.client.render.TileEntityMonitorRenderer;
@@ -16,70 +17,51 @@
 import dan200.computercraft.shared.computer.inventory.ContainerViewComputer;
 import dan200.computercraft.shared.network.container.*;
 import dan200.computercraft.shared.peripheral.modem.wired.TileCable;
-import dan200.computercraft.shared.peripheral.monitor.ClientMonitor;
 import dan200.computercraft.shared.peripheral.monitor.TileMonitor;
 import dan200.computercraft.shared.turtle.blocks.TileTurtle;
 import dan200.computercraft.shared.turtle.inventory.ContainerTurtle;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.event.world.WorldEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.ExtensionPoint;
-import net.minecraftforge.fml.ModLoadingContext;
-import net.minecraftforge.fml.client.registry.ClientRegistry;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
+import net.fabricmc.fabric.api.client.render.BlockEntityRendererRegistry;
 
-import java.util.function.BiFunction;
-
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD )
 public final class ComputerCraftProxyClient
 {
-    @SubscribeEvent
-    public static void setupClient( FMLClientSetupEvent event )
+    public static void setup()
     {
         registerContainers();
 
         // Setup TESRs
-        ClientRegistry.bindTileEntitySpecialRenderer( TileMonitor.class, new TileEntityMonitorRenderer() );
-        ClientRegistry.bindTileEntitySpecialRenderer( TileCable.class, new TileEntityCableRenderer() );
-        ClientRegistry.bindTileEntitySpecialRenderer( TileTurtle.class, new TileEntityTurtleRenderer() );
+        BlockEntityRendererRegistry.INSTANCE.register( TileMonitor.class, new TileEntityMonitorRenderer() );
+        BlockEntityRendererRegistry.INSTANCE.register( TileCable.class, new TileEntityCableRenderer() );
+        BlockEntityRendererRegistry.INSTANCE.register( TileTurtle.class, new TileEntityTurtleRenderer() );
+
+        ClientRegistry.onItemColours();
     }
 
     private static void registerContainers()
     {
-        ContainerType.registerGui( TileEntityContainerType::computer, ( packet, player ) ->
-            new GuiComputer( (TileComputer) packet.getTileEntity( player ) ) );
+        ContainerType.registerGui( TileEntityContainerType::computer, ( id, packet, player ) ->
+            GuiComputer.create( id, (TileComputer) packet.getTileEntity( player ), player.inventory ) );
         ContainerType.registerGui( TileEntityContainerType::diskDrive, GuiDiskDrive::new );
         ContainerType.registerGui( TileEntityContainerType::printer, GuiPrinter::new );
-        ContainerType.registerGui( TileEntityContainerType::turtle, ( packet, player ) -> {
+        ContainerType.registerGui( TileEntityContainerType::turtle, ( id, packet, player ) -> {
             TileTurtle turtle = (TileTurtle) packet.getTileEntity( player );
-            return new GuiTurtle( turtle, new ContainerTurtle( player.inventory, turtle.getAccess(), turtle.getClientComputer() ) );
+            return new GuiTurtle( turtle, new ContainerTurtle( id, player.inventory, turtle.getAccess(), turtle.getClientComputer() ), player.inventory );
         } );
 
         ContainerType.registerGui( PocketComputerContainerType::new, GuiPocketComputer::new );
         ContainerType.registerGui( PrintoutContainerType::new, GuiPrintout::new );
-        ContainerType.registerGui( ViewComputerContainerType::new, ( packet, player ) -> {
+        ContainerType.registerGui( ViewComputerContainerType::new, ( id, packet, player ) -> {
             ClientComputer computer = ComputerCraft.clientComputerRegistry.get( packet.instanceId );
             if( computer == null )
             {
                 ComputerCraft.clientComputerRegistry.add( packet.instanceId, computer = new ClientComputer( packet.instanceId ) );
             }
 
-            ContainerViewComputer container = new ContainerViewComputer( computer );
-            return new GuiComputer( container, packet.family, computer, packet.width, packet.height );
-        } );
-
-        ModLoadingContext.get().registerExtensionPoint( ExtensionPoint.GUIFACTORY, () -> packet -> {
-            ContainerType<?> type = ContainerType.factories.get( packet.getId() ).get();
-            if( packet.getAdditionalData() != null ) type.fromBytes( packet.getAdditionalData() );
-            return ((BiFunction<ContainerType<?>, EntityPlayer, GuiContainer>) ContainerType.guiFactories.get( packet.getId() ))
-                .apply( type, Minecraft.getInstance().player );
+            ContainerViewComputer container = new ContainerViewComputer( id, computer );
+            return new GuiComputer<>( container, player.inventory, packet.family, computer, packet.width, packet.height );
         } );
     }
 
+    /*
     @Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
     public static final class ForgeHandlers
     {
@@ -92,4 +74,5 @@ public static void onWorldUnload( WorldEvent.Unload event )
             }
         }
     }
+    */
 }
diff --git a/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java b/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java
index a9226a71ed..537fd3354d 100644
--- a/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/ItemMapLikeRenderer.java
@@ -6,13 +6,14 @@
 
 package dan200.computercraft.client.render;
 
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.FirstPersonRenderer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.entity.player.EntityPlayer;
+import com.mojang.blaze3d.platform.GlStateManager;
+import dan200.computercraft.shared.mixed.MixedFirstPersonRenderer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.FirstPersonRenderer;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.EnumHandSide;
+import net.minecraft.util.AbsoluteHand;
+import net.minecraft.util.Hand;
 import net.minecraft.util.math.MathHelper;
 
 public abstract class ItemMapLikeRenderer
@@ -21,23 +22,23 @@ public abstract class ItemMapLikeRenderer
      * The main rendering method for the item
      *
      * @param stack The stack to render
-     * @see FirstPersonRenderer#renderMapFirstPerson(ItemStack)
+     * @see FirstPersonRenderer#renderFirstPersonMap(ItemStack)
      */
     protected abstract void renderItem( ItemStack stack );
 
-    protected void renderItemFirstPerson( EnumHand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack )
+    public void renderItemFirstPerson( Hand hand, float pitch, float equipProgress, float swingProgress, ItemStack stack )
     {
-        EntityPlayer player = Minecraft.getInstance().player;
+        PlayerEntity player = MinecraftClient.getInstance().player;
 
         GlStateManager.pushMatrix();
-        if( hand == EnumHand.MAIN_HAND && player.getHeldItemOffhand().isEmpty() )
+        if( hand == Hand.MAIN && player.getOffHandStack().isEmpty() )
         {
             renderItemFirstPersonCenter( pitch, equipProgress, swingProgress, stack );
         }
         else
         {
             renderItemFirstPersonSide(
-                hand == EnumHand.MAIN_HAND ? player.getPrimaryHand() : player.getPrimaryHand().opposite(),
+                hand == Hand.MAIN ? player.getMainHand() : player.getMainHand().getOpposite(),
                 equipProgress, swingProgress, stack
             );
         }
@@ -51,12 +52,12 @@ protected void renderItemFirstPerson( EnumHand hand, float pitch, float equipPro
      * @param equipProgress The equip progress of this item
      * @param swingProgress The swing progress of this item
      * @param stack         The stack to render
-     * @see FirstPersonRenderer#renderMapFirstPersonSide(float, EnumHandSide, float, ItemStack)
+     * @see FirstPersonRenderer#method_3222(float, AbsoluteHand, float, ItemStack) // renderMapFirstPersonSide
      */
-    private void renderItemFirstPersonSide( EnumHandSide side, float equipProgress, float swingProgress, ItemStack stack )
+    private void renderItemFirstPersonSide( AbsoluteHand side, float equipProgress, float swingProgress, ItemStack stack )
     {
-        Minecraft minecraft = Minecraft.getInstance();
-        float offset = side == EnumHandSide.RIGHT ? 1f : -1f;
+        MinecraftClient minecraft = MinecraftClient.getInstance();
+        float offset = side == AbsoluteHand.RIGHT ? 1f : -1f;
         GlStateManager.translatef( offset * 0.125f, -0.125f, 0f );
 
         // If the player is not invisible then render a single arm
@@ -64,7 +65,7 @@ private void renderItemFirstPersonSide( EnumHandSide side, float equipProgress,
         {
             GlStateManager.pushMatrix();
             GlStateManager.rotatef( offset * 10f, 0f, 0f, 1f );
-            minecraft.getFirstPersonRenderer().renderArmFirstPerson( equipProgress, swingProgress, side );
+            ((MixedFirstPersonRenderer) minecraft.getFirstPersonRenderer()).renderArmFirstPerson_CC( equipProgress, swingProgress, side );
             GlStateManager.popMatrix();
         }
 
@@ -93,11 +94,11 @@ private void renderItemFirstPersonSide( EnumHandSide side, float equipProgress,
      * @param equipProgress The equip progress of this item
      * @param swingProgress The swing progress of this item
      * @param stack         The stack to render
-     * @see FirstPersonRenderer#renderMapFirstPerson(float, float, float)
+     * @see FirstPersonRenderer#renderFirstPersonMap(float, float, float)
      */
     private void renderItemFirstPersonCenter( float pitch, float equipProgress, float swingProgress, ItemStack stack )
     {
-        FirstPersonRenderer renderer = Minecraft.getInstance().getFirstPersonRenderer();
+        MixedFirstPersonRenderer renderer = (MixedFirstPersonRenderer) MinecraftClient.getInstance().getFirstPersonRenderer();
 
         // Setup the appropriate transformations. This is just copied from the
         // corresponding method in ItemRenderer.
@@ -105,10 +106,10 @@ private void renderItemFirstPersonCenter( float pitch, float equipProgress, floa
         float tX = -0.2f * MathHelper.sin( swingProgress * (float) Math.PI );
         float tZ = -0.4f * MathHelper.sin( swingRt * (float) Math.PI );
         GlStateManager.translatef( 0f, -tX / 2f, tZ );
-        float pitchAngle = renderer.getMapAngleFromPitch( pitch );
+        float pitchAngle = renderer.getMapAngleFromPitch_CC( pitch );
         GlStateManager.translatef( 0f, 0.04f + equipProgress * -1.2f + pitchAngle * -0.5f, -0.72f );
         GlStateManager.rotatef( pitchAngle * -85f, 1f, 0f, 0f );
-        renderer.renderArms();
+        renderer.renderArms_CC();
         float rX = MathHelper.sin( swingRt * (float) Math.PI );
         GlStateManager.rotatef( rX * 20f, 1f, 0f, 0f );
         GlStateManager.scalef( 2f, 2f, 2f );
diff --git a/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java b/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java
index a44cb7b164..52d95fd2a0 100644
--- a/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/ItemPocketRenderer.java
@@ -6,7 +6,7 @@
 
 package dan200.computercraft.client.render;
 
-import dan200.computercraft.ComputerCraft;
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.client.FrameInfo;
 import dan200.computercraft.client.gui.FixedWidthFontRenderer;
 import dan200.computercraft.core.terminal.Terminal;
@@ -14,19 +14,15 @@
 import dan200.computercraft.shared.computer.core.ClientComputer;
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
 import dan200.computercraft.shared.util.Palette;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.ItemRenderer;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ItemCameraTransforms.TransformType;
-import net.minecraft.client.renderer.texture.TextureManager;
-import net.minecraft.client.renderer.texture.TextureMap;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.item.ItemRenderer;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.json.ModelTransformation;
+import net.minecraft.client.texture.SpriteAtlasTexture;
+import net.minecraft.client.texture.TextureManager;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.client.ForgeHooksClient;
-import net.minecraftforge.client.event.RenderSpecificHandEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
 import org.lwjgl.opengl.GL11;
 
 import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT;
@@ -35,15 +31,16 @@
 /**
  * Emulates map rendering for pocket computers
  */
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
+@Environment( EnvType.CLIENT )
 public final class ItemPocketRenderer extends ItemMapLikeRenderer
 {
-    private static final ItemPocketRenderer INSTANCE = new ItemPocketRenderer();
+    public static final ItemPocketRenderer INSTANCE = new ItemPocketRenderer();
 
     private ItemPocketRenderer()
     {
     }
 
+    /*
     @SubscribeEvent
     public static void renderItem( RenderSpecificHandEvent event )
     {
@@ -53,6 +50,7 @@ public static void renderItem( RenderSpecificHandEvent event )
         event.setCanceled( true );
         INSTANCE.renderItemFirstPerson( event.getHand(), event.getInterpolatedPitch(), event.getEquipProgress(), event.getSwingProgress(), event.getItemStack() );
     }
+    */
 
     @Override
     protected void renderItem( ItemStack stack )
@@ -74,13 +72,13 @@ protected void renderItem( ItemStack stack )
 
             GlStateManager.scalef( 1.0f, -1.0f, 1.0f );
 
-            Minecraft minecraft = Minecraft.getInstance();
+            MinecraftClient minecraft = MinecraftClient.getInstance();
             TextureManager textureManager = minecraft.getTextureManager();
             ItemRenderer renderItem = minecraft.getItemRenderer();
 
             // Copy of RenderItem#renderItemModelIntoGUI but without the translation or scaling
-            textureManager.bindTexture( TextureMap.LOCATION_BLOCKS_TEXTURE );
-            textureManager.getTexture( TextureMap.LOCATION_BLOCKS_TEXTURE ).setBlurMipmap( false, false );
+            textureManager.bindTexture( SpriteAtlasTexture.BLOCK_ATLAS_TEX );
+            textureManager.getTexture( SpriteAtlasTexture.BLOCK_ATLAS_TEX ).pushFilter( false, false );
 
             GlStateManager.enableRescaleNormal();
             GlStateManager.enableAlphaTest();
@@ -89,9 +87,9 @@ protected void renderItem( ItemStack stack )
             GlStateManager.blendFunc( GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA );
             GlStateManager.color4f( 1.0F, 1.0F, 1.0F, 1.0F );
 
-            IBakedModel baked = renderItem.getItemModelWithOverrides( stack, null, null );
-            baked = ForgeHooksClient.handleCameraTransforms( baked, TransformType.GUI, false );
-            renderItem.renderItem( stack, baked );
+            BakedModel baked = renderItem.getModel( stack, null, null );
+            baked.getTransformation().applyGl( ModelTransformation.Type.GUI );
+            renderItem.renderItemAndGlow( stack, baked );
 
             GlStateManager.disableAlphaTest();
             GlStateManager.disableRescaleNormal();
diff --git a/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java b/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java
index ab8eb26706..ed0aa96ca1 100644
--- a/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/ItemPrintoutRenderer.java
@@ -6,15 +6,12 @@
 
 package dan200.computercraft.client.render;
 
-import dan200.computercraft.ComputerCraft;
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.shared.media.items.ItemPrintout;
-import net.minecraft.client.renderer.GlStateManager;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.entity.decoration.ItemFrameEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.client.event.RenderItemInFrameEvent;
-import net.minecraftforge.client.event.RenderSpecificHandEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
 
 import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT;
 import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_WIDTH;
@@ -25,15 +22,16 @@
 /**
  * Emulates map and item-frame rendering for printouts
  */
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
+@Environment( EnvType.CLIENT )
 public final class ItemPrintoutRenderer extends ItemMapLikeRenderer
 {
-    private static final ItemPrintoutRenderer INSTANCE = new ItemPrintoutRenderer();
+    public static final ItemPrintoutRenderer INSTANCE = new ItemPrintoutRenderer();
 
     private ItemPrintoutRenderer()
     {
     }
 
+    /*
     @SubscribeEvent
     public static void onRenderInHand( RenderSpecificHandEvent event )
     {
@@ -43,6 +41,7 @@ public static void onRenderInHand( RenderSpecificHandEvent event )
         event.setCanceled( true );
         INSTANCE.renderItemFirstPerson( event.getHand(), event.getInterpolatedPitch(), event.getEquipProgress(), event.getSwingProgress(), event.getItemStack() );
     }
+    */
 
     @Override
     protected void renderItem( ItemStack stack )
@@ -61,16 +60,13 @@ protected void renderItem( ItemStack stack )
         GlStateManager.enableLighting();
     }
 
-    @SubscribeEvent
-    public static void onRenderInFrame( RenderItemInFrameEvent event )
+    public void renderInFrame( ItemFrameEntity entity, ItemStack stack )
     {
-        ItemStack stack = event.getItem();
-        if( !(stack.getItem() instanceof ItemPrintout) ) return;
-
-        event.setCanceled( true );
-
         GlStateManager.disableLighting();
 
+        int rotation = entity.getRotation();
+        GlStateManager.rotatef( (float) rotation * 360.0F / 8.0F, 0.0F, 0.0F, 1.0F );
+
         // Move a little bit forward to ensure we're not clipping with the frame
         GlStateManager.translatef( 0.0f, 0.0f, -0.001f );
         GlStateManager.rotatef( 180f, 0f, 0f, 1f );
diff --git a/src/main/java/dan200/computercraft/client/render/ModelTransformer.java b/src/main/java/dan200/computercraft/client/render/ModelTransformer.java
index 5c3ec90132..653e890fa5 100644
--- a/src/main/java/dan200/computercraft/client/render/ModelTransformer.java
+++ b/src/main/java/dan200/computercraft/client/render/ModelTransformer.java
@@ -6,19 +6,13 @@
 
 package dan200.computercraft.client.render;
 
-import net.minecraft.client.renderer.model.BakedQuad;
-import net.minecraft.client.renderer.texture.TextureAtlasSprite;
-import net.minecraft.client.renderer.vertex.VertexFormat;
-import net.minecraft.util.EnumFacing;
-import net.minecraftforge.client.model.pipeline.IVertexConsumer;
-import net.minecraftforge.client.model.pipeline.LightUtil;
-import net.minecraftforge.client.model.pipeline.VertexTransformer;
-import net.minecraftforge.common.model.TRSRTransformation;
+import net.minecraft.client.render.VertexFormat;
+import net.minecraft.client.render.VertexFormatElement;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.render.model.BakedQuad;
 
-import javax.annotation.Nonnull;
 import javax.vecmath.Matrix4f;
-import javax.vecmath.Point3f;
-import javax.vecmath.Vector3f;
+import javax.vecmath.Vector4f;
 import java.util.List;
 
 /**
@@ -40,6 +34,11 @@ private ModelTransformer()
     }
 
     public static void transformQuadsTo( List<BakedQuad> output, List<BakedQuad> input, Matrix4f transform )
+    {
+        transformQuadsTo( VertexFormats.POSITION_COLOR_UV_NORMAL, output, input, transform );
+    }
+
+    public static void transformQuadsTo( VertexFormat format, List<BakedQuad> output, List<BakedQuad> input, Matrix4f transform )
     {
         if( transform == null || transform.equals( identity ) )
         {
@@ -47,224 +46,55 @@ public static void transformQuadsTo( List<BakedQuad> output, List<BakedQuad> inp
         }
         else
         {
-            Matrix4f normalMatrix = new Matrix4f( transform );
-            normalMatrix.invert();
-            normalMatrix.transpose();
-
-            for( BakedQuad quad : input ) output.add( doTransformQuad( quad, transform, normalMatrix ) );
+            for( BakedQuad quad : input ) output.add( doTransformQuad( format, quad, transform ) );
         }
     }
 
-    public static BakedQuad transformQuad( BakedQuad input, Matrix4f transform )
+    public static BakedQuad transformQuad( VertexFormat format, BakedQuad input, Matrix4f transform )
     {
         if( transform == null || transform.equals( identity ) ) return input;
-
-        Matrix4f normalMatrix = new Matrix4f( transform );
-        normalMatrix.invert();
-        normalMatrix.transpose();
-        return doTransformQuad( input, transform, normalMatrix );
-    }
-
-    private static BakedQuad doTransformQuad( BakedQuad input, Matrix4f positionMatrix, Matrix4f normalMatrix )
-    {
-
-        BakedQuadBuilder builder = new BakedQuadBuilder( input.getFormat() );
-        NormalAwareTransformer transformer = new NormalAwareTransformer( builder, positionMatrix, normalMatrix );
-        input.pipe( transformer );
-
-        if( transformer.areNormalsInverted() )
-        {
-            builder.swap( 1, 3 );
-            transformer.areNormalsInverted();
-        }
-
-        return builder.build();
+        return doTransformQuad( format, input, transform );
     }
 
-    /**
-     * A vertex transformer that tracks whether the normals have been inverted and so the vertices
-     * should be reordered so backface culling works as expected.
-     */
-    private static class NormalAwareTransformer extends VertexTransformer
+    private static BakedQuad doTransformQuad( VertexFormat format, BakedQuad quad, Matrix4f transform )
     {
-        private final Matrix4f positionMatrix;
-        private final Matrix4f normalMatrix;
-
-        private int vertexIndex = 0, elementIndex = 0;
-        private final Point3f[] before = new Point3f[4];
-        private final Point3f[] after = new Point3f[4];
-
-        public NormalAwareTransformer( IVertexConsumer parent, Matrix4f positionMatrix, Matrix4f normalMatrix )
-        {
-            super( parent );
-            this.positionMatrix = positionMatrix;
-            this.normalMatrix = normalMatrix;
-        }
-
-        @Override
-        public void setQuadOrientation( @Nonnull EnumFacing orientation )
-        {
-            super.setQuadOrientation( orientation == null ? orientation : TRSRTransformation.rotate( positionMatrix, orientation ) );
-        }
-
-        @Override
-        public void put( int element, @Nonnull float... data )
-        {
-            switch( getVertexFormat().getElement( element ).getUsage() )
+        int[] vertexData = quad.getVertexData().clone();
+        int offset = 0;
+        BakedQuad copy = new BakedQuad( vertexData, -1, quad.getFace(), quad.getSprite() );
+        for( int i = 0; i < format.getElementCount(); ++i ) // For each vertex element
+        {
+            VertexFormatElement element = format.getElement( i );
+            if( element.isPosition() &&
+                element.getFormat() == VertexFormatElement.Format.FLOAT &&
+                element.getCount() == 3 ) // When we find a position element
             {
-                case POSITION:
-                {
-                    Point3f vec = new Point3f( data );
-                    Point3f newVec = new Point3f();
-                    positionMatrix.transform( vec, newVec );
-
-                    float[] newData = new float[4];
-                    newVec.get( newData );
-                    super.put( element, newData );
-
-
-                    before[vertexIndex] = vec;
-                    after[vertexIndex] = newVec;
-                    break;
-                }
-                case NORMAL:
+                for( int j = 0; j < 4; ++j ) // For each corner of the quad
                 {
-                    Vector3f vec = new Vector3f( data );
-                    normalMatrix.transform( vec );
-
-                    float[] newData = new float[4];
-                    vec.get( newData );
-                    super.put( element, newData );
-                    break;
+                    int start = offset + j * format.getVertexSize();
+                    if( (start % 4) == 0 )
+                    {
+                        start = start / 4;
+
+                        // Extract the position
+                        Vector4f pos = new Vector4f(
+                            Float.intBitsToFloat( vertexData[start] ),
+                            Float.intBitsToFloat( vertexData[start + 1] ),
+                            Float.intBitsToFloat( vertexData[start + 2] ),
+                            1
+                        );
+
+                        // Transform the position
+                        transform.transform( pos );
+
+                        // Insert the position
+                        vertexData[start] = Float.floatToRawIntBits( pos.x );
+                        vertexData[start + 1] = Float.floatToRawIntBits( pos.y );
+                        vertexData[start + 2] = Float.floatToRawIntBits( pos.z );
+                    }
                 }
-                default:
-                    super.put( element, data );
-                    break;
-            }
-
-            elementIndex++;
-            if( elementIndex == getVertexFormat().getElementCount() )
-            {
-                vertexIndex++;
-                elementIndex = 0;
             }
+            offset += element.getSize();
         }
-
-        public boolean areNormalsInverted()
-        {
-            Vector3f temp1 = new Vector3f(), temp2 = new Vector3f();
-            Vector3f crossBefore = new Vector3f(), crossAfter = new Vector3f();
-
-            // Determine what cross product we expect to have
-            temp1.sub( before[1], before[0] );
-            temp2.sub( before[1], before[2] );
-            crossBefore.cross( temp1, temp2 );
-            normalMatrix.transform( crossBefore );
-
-            // And determine what cross product we actually have
-            temp1.sub( after[1], after[0] );
-            temp2.sub( after[1], after[2] );
-            crossAfter.cross( temp1, temp2 );
-
-            // If the angle between expected and actual cross product is greater than
-            // pi/2 radians then we will need to reorder our quads.
-            return Math.abs( crossBefore.angle( crossAfter ) ) >= Math.PI / 2;
-        }
-    }
-
-    /**
-     * A vertex consumer which is capable of building {@link BakedQuad}s.
-     *
-     * Equivalent to {@link net.minecraftforge.client.model.pipeline.UnpackedBakedQuad.Builder} but more memory
-     * efficient.
-     *
-     * This also provides the ability to swap vertices through {@link #swap(int, int)} to allow reordering.
-     */
-    private static final class BakedQuadBuilder implements IVertexConsumer
-    {
-        private final VertexFormat format;
-
-        private final int[] vertexData;
-        private int vertexIndex = 0, elementIndex = 0;
-
-        private EnumFacing orientation;
-        private int quadTint;
-        private boolean diffuse;
-        private TextureAtlasSprite texture;
-
-        private BakedQuadBuilder( VertexFormat format )
-        {
-            this.format = format;
-            vertexData = new int[format.getSize()];
-        }
-
-        @Nonnull
-        @Override
-        public VertexFormat getVertexFormat()
-        {
-            return format;
-        }
-
-        @Override
-        public void setQuadTint( int tint )
-        {
-            quadTint = tint;
-        }
-
-        @Override
-        public void setQuadOrientation( @Nonnull EnumFacing orientation )
-        {
-            this.orientation = orientation;
-        }
-
-        @Override
-        public void setApplyDiffuseLighting( boolean diffuse )
-        {
-            this.diffuse = diffuse;
-        }
-
-        @Override
-        public void setTexture( @Nonnull TextureAtlasSprite texture )
-        {
-            this.texture = texture;
-        }
-
-        @Override
-        public void put( int element, @Nonnull float... data )
-        {
-            LightUtil.pack( data, vertexData, format, vertexIndex, element );
-
-            elementIndex++;
-            if( elementIndex == getVertexFormat().getElementCount() )
-            {
-                vertexIndex++;
-                elementIndex = 0;
-            }
-        }
-
-        public void swap( int a, int b )
-        {
-            int length = vertexData.length / 4;
-            for( int i = 0; i < length; i++ )
-            {
-                int temp = vertexData[a * length + i];
-                vertexData[a * length + i] = vertexData[b * length + i];
-                vertexData[b * length + i] = temp;
-            }
-        }
-
-        public BakedQuad build()
-        {
-            if( elementIndex != 0 || vertexIndex != 4 )
-            {
-                throw new IllegalStateException( "Got an unexpected number of elements/vertices" );
-            }
-            if( texture == null )
-            {
-                throw new IllegalStateException( "Texture has not been set" );
-            }
-
-            return new BakedQuad( vertexData, quadTint, orientation, texture, diffuse, format );
-        }
+        return copy;
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java b/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java
index 07e9af3102..bfae3c80ef 100644
--- a/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/PrintoutRenderer.java
@@ -6,17 +6,17 @@
 
 package dan200.computercraft.client.render;
 
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.platform.GlStateManager.DestFactor;
+import com.mojang.blaze3d.platform.GlStateManager.SourceFactor;
 import dan200.computercraft.client.gui.FixedWidthFontRenderer;
 import dan200.computercraft.core.terminal.TextBuffer;
 import dan200.computercraft.shared.util.Palette;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.GlStateManager.DestFactor;
-import net.minecraft.client.renderer.GlStateManager.SourceFactor;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.Identifier;
 import org.lwjgl.opengl.GL11;
 
 import static dan200.computercraft.client.gui.FixedWidthFontRenderer.FONT_HEIGHT;
@@ -24,7 +24,7 @@
 
 public final class PrintoutRenderer
 {
-    private static final ResourceLocation BG = new ResourceLocation( "computercraft", "textures/gui/printout.png" );
+    private static final Identifier BG = new Identifier( "computercraft", "textures/gui/printout.png" );
     private static final double BG_SIZE = 256.0;
 
     /**
@@ -76,7 +76,7 @@ public static void drawText( int x, int y, int start, String[] text, String[] co
     {
         GlStateManager.color4f( 1.0f, 1.0f, 1.0f, 1.0f );
         GlStateManager.enableBlend();
-        GlStateManager.enableTexture2D();
+        GlStateManager.enableTexture();
         GlStateManager.blendFuncSeparate( SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO );
 
         FixedWidthFontRenderer fontRenderer = FixedWidthFontRenderer.instance();
@@ -91,14 +91,14 @@ public static void drawBorder( double x, double y, double z, int page, int pages
     {
         GlStateManager.color4f( 1.0f, 1.0f, 1.0f, 1.0f );
         GlStateManager.enableBlend();
-        GlStateManager.enableTexture2D();
+        GlStateManager.enableTexture();
         GlStateManager.blendFuncSeparate( SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.ONE, DestFactor.ZERO );
 
-        Minecraft.getInstance().getTextureManager().bindTexture( BG );
+        MinecraftClient.getInstance().getTextureManager().bindTexture( BG );
 
         Tessellator tessellator = Tessellator.getInstance();
-        BufferBuilder buffer = tessellator.getBuffer();
-        buffer.begin( GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX );
+        BufferBuilder buffer = tessellator.getBufferBuilder();
+        buffer.begin( GL11.GL_QUADS, VertexFormats.POSITION_UV );
 
         int leftPages = page;
         int rightPages = pages - page - 1;
@@ -159,18 +159,18 @@ public static void drawBorder( double x, double y, double z, int page, int pages
 
     private static void drawTexture( BufferBuilder buffer, double x, double y, double z, double u, double v, double width, double height )
     {
-        buffer.pos( x, y + height, z ).tex( u / BG_SIZE, (v + height) / BG_SIZE ).endVertex();
-        buffer.pos( x + width, y + height, z ).tex( (u + width) / BG_SIZE, (v + height) / BG_SIZE ).endVertex();
-        buffer.pos( x + width, y, z ).tex( (u + width) / BG_SIZE, v / BG_SIZE ).endVertex();
-        buffer.pos( x, y, z ).tex( u / BG_SIZE, v / BG_SIZE ).endVertex();
+        buffer.vertex( x, y + height, z ).texture( u / BG_SIZE, (v + height) / BG_SIZE ).next();
+        buffer.vertex( x + width, y + height, z ).texture( (u + width) / BG_SIZE, (v + height) / BG_SIZE ).next();
+        buffer.vertex( x + width, y, z ).texture( (u + width) / BG_SIZE, v / BG_SIZE ).next();
+        buffer.vertex( x, y, z ).texture( u / BG_SIZE, v / BG_SIZE ).next();
     }
 
     private static void drawTexture( BufferBuilder buffer, double x, double y, double z, double width, double height, double u, double v, double tWidth, double tHeight )
     {
-        buffer.pos( x, y + height, z ).tex( u / BG_SIZE, (v + tHeight) / BG_SIZE ).endVertex();
-        buffer.pos( x + width, y + height, z ).tex( (u + tWidth) / BG_SIZE, (v + tHeight) / BG_SIZE ).endVertex();
-        buffer.pos( x + width, y, z ).tex( (u + tWidth) / BG_SIZE, v / BG_SIZE ).endVertex();
-        buffer.pos( x, y, z ).tex( u / BG_SIZE, v / BG_SIZE ).endVertex();
+        buffer.vertex( x, y + height, z ).texture( u / BG_SIZE, (v + tHeight) / BG_SIZE ).next();
+        buffer.vertex( x + width, y + height, z ).texture( (u + tWidth) / BG_SIZE, (v + tHeight) / BG_SIZE ).next();
+        buffer.vertex( x + width, y, z ).texture( (u + tWidth) / BG_SIZE, v / BG_SIZE ).next();
+        buffer.vertex( x, y, z ).texture( u / BG_SIZE, v / BG_SIZE ).next();
     }
 
     public static double offsetAt( int page )
diff --git a/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java b/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java
index e3af18ce3e..2f2de841e3 100644
--- a/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java
+++ b/src/main/java/dan200/computercraft/client/render/RenderOverlayCable.java
@@ -6,26 +6,23 @@
 
 package dan200.computercraft.client.render;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.peripheral.modem.wired.BlockCable;
 import dan200.computercraft.shared.peripheral.modem.wired.CableShapes;
 import dan200.computercraft.shared.util.WorldUtil;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.WorldRenderer;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
-import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.shape.VoxelShape;
 import net.minecraft.world.World;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.client.event.DrawBlockHighlightEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
 import org.lwjgl.opengl.GL11;
 
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
 public final class RenderOverlayCable
 {
     private RenderOverlayCable()
@@ -35,18 +32,18 @@ private RenderOverlayCable()
     /**
      * Draw an outline for a specific part of a cable "Multipart".
      *
-     * @param event The event to observe
-     * @see WorldRenderer#drawSelectionBox(EntityPlayer, RayTraceResult, int, float)
+     * @see WorldRenderer#drawHighlightedBlockOutline(Entity, HitResult, int, float)
      */
-    @SubscribeEvent
-    public static void drawHighlight( DrawBlockHighlightEvent event )
+    // TODO @SubscribeEvent
+    public static void drawHighlight()
     {
-        if( event.getTarget().type != RayTraceResult.Type.BLOCK ) return;
+        MinecraftClient mc = MinecraftClient.getInstance();
+        if( mc.hitResult == null || mc.hitResult.getType() != HitResult.Type.BLOCK ) return;
 
-        BlockPos pos = event.getTarget().getBlockPos();
-        World world = event.getPlayer().getEntityWorld();
+        BlockPos pos = ((BlockHitResult) mc.hitResult).getBlockPos();
+        World world = mc.world;
 
-        IBlockState state = world.getBlockState( pos );
+        BlockState state = world.getBlockState( pos );
 
         // We only care about instances with both cable and modem.
         if( state.getBlock() != ComputerCraft.Blocks.cable || state.get( BlockCable.MODEM ).getFacing() == null || !state.get( BlockCable.CABLE ) )
@@ -54,34 +51,31 @@ public static void drawHighlight( DrawBlockHighlightEvent event )
             return;
         }
 
-        event.setCanceled( true );
-
-        EntityPlayer player = event.getPlayer();
-        Minecraft mc = Minecraft.getInstance();
-        float partialTicks = event.getPartialTicks();
+        PlayerEntity player = mc.player;
+        float partialTicks = mc.getTickDelta();
 
         GlStateManager.enableBlend();
         GlStateManager.blendFuncSeparate( GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO );
-        GlStateManager.lineWidth( Math.max( 2.5F, mc.mainWindow.getFramebufferWidth() / 1920.0F * 2.5F ) );
-        GlStateManager.disableTexture2D();
+        GlStateManager.lineWidth( Math.max( 2.5F, mc.window.getFramebufferWidth() / 1920.0F * 2.5F ) );
+        GlStateManager.disableTexture();
         GlStateManager.depthMask( false );
         GlStateManager.matrixMode( GL11.GL_PROJECTION );
         GlStateManager.pushMatrix();
         GlStateManager.scalef( 1.0F, 1.0F, 0.999F );
 
-        double x = player.lastTickPosX + (player.posX - player.lastTickPosX) * partialTicks;
-        double y = player.lastTickPosY + (player.posY - player.lastTickPosY) * partialTicks;
-        double z = player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * partialTicks;
+        double x = player.prevX + (player.x - player.prevX) * partialTicks;
+        double y = player.prevY + (player.y - player.prevY) * partialTicks;
+        double z = player.prevZ + (player.z - player.prevZ) * partialTicks;
 
-        VoxelShape shape = WorldUtil.isVecInside( CableShapes.getModemShape( state ), event.getTarget().hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) )
+        VoxelShape shape = WorldUtil.isVecInside( CableShapes.getModemShape( state ), mc.hitResult.getPos().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
             ? CableShapes.getModemShape( state ) : CableShapes.getCableShape( state );
 
-        WorldRenderer.drawShape( shape, pos.getX() - x, pos.getY() - y, pos.getZ() - z, 0.0F, 0.0F, 0.0F, 0.4F );
+        WorldRenderer.drawShapeOutline( shape, pos.getX() - x, pos.getY() - y, pos.getZ() - z, 0.0F, 0.0F, 0.0F, 0.4F );
 
         GlStateManager.popMatrix();
         GlStateManager.matrixMode( GL11.GL_MODELVIEW );
         GlStateManager.depthMask( true );
-        GlStateManager.enableTexture2D();
+        GlStateManager.enableTexture();
         GlStateManager.disableBlend();
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityCableRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityCableRenderer.java
index 983de24b19..2cde012c65 100644
--- a/src/main/java/dan200/computercraft/client/render/TileEntityCableRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/TileEntityCableRenderer.java
@@ -6,29 +6,27 @@
 
 package dan200.computercraft.client.render;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.ComputerCraft;
 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.peripheral.modem.wired.TileCable;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.WorldRenderer;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.texture.TextureAtlasSprite;
-import net.minecraft.client.renderer.tileentity.TileEntityRenderer;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.BlockRenderLayer;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.client.render.block.entity.BlockEntityRenderer;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
-import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.shape.VoxelShape;
 import net.minecraft.world.World;
-import net.minecraftforge.client.ForgeHooksClient;
-import net.minecraftforge.client.MinecraftForgeClient;
 import org.lwjgl.opengl.GL11;
 
 import javax.annotation.Nonnull;
@@ -37,7 +35,7 @@
 /**
  * Render breaking animation only over part of a {@link TileCable}.
  */
-public class TileEntityCableRenderer extends TileEntityRenderer<TileCable>
+public class TileEntityCableRenderer extends BlockEntityRenderer<TileCable>
 {
     private static final Random random = new Random();
 
@@ -48,45 +46,35 @@ public void render( @Nonnull TileCable te, double x, double y, double z, float p
 
         BlockPos pos = te.getPos();
 
-        Minecraft mc = Minecraft.getInstance();
+        MinecraftClient mc = MinecraftClient.getInstance();
 
-        RayTraceResult hit = mc.objectMouseOver;
-        if( hit == null || !hit.getBlockPos().equals( pos ) ) return;
-
-        if( MinecraftForgeClient.getRenderPass() != 0 ) return;
+        HitResult hit = mc.hitResult;
+        if( !(hit instanceof BlockHitResult) || !((BlockHitResult) hit).getBlockPos().equals( pos ) ) return;
 
         World world = te.getWorld();
-        IBlockState state = world.getBlockState( pos );
+        BlockState state = te.getCachedState();
         Block block = state.getBlock();
         if( block != ComputerCraft.Blocks.cable ) return;
 
         VoxelShape shape = CableShapes.getModemShape( state );
-        state = te.hasModem() && shape.getBoundingBox().grow( 0.02, 0.02, 0.02 ).contains( hit.hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) )
+        state = te.hasModem() && shape.getBoundingBox().expand( 0.02, 0.02, 0.02 ).contains( hit.getPos().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
             ? block.getDefaultState().with( BlockCable.MODEM, state.get( BlockCable.MODEM ) )
             : state.with( BlockCable.MODEM, CableModemVariant.None );
 
-        IBakedModel model = mc.getBlockRendererDispatcher().getModelForState( state );
+        BakedModel model = mc.getBlockRenderManager().getModel( state );
 
         preRenderDamagedBlocks();
 
-        BufferBuilder buffer = Tessellator.getInstance().getBuffer();
-        buffer.begin( GL11.GL_QUADS, DefaultVertexFormats.BLOCK );
-        buffer.setTranslation( x - pos.getX(), y - pos.getY(), z - pos.getZ() );
-        buffer.noColor();
-
-        ForgeHooksClient.setRenderLayer( block.getRenderLayer() );
+        BufferBuilder buffer = Tessellator.getInstance().getBufferBuilder();
+        buffer.begin( GL11.GL_QUADS, VertexFormats.POSITION_COLOR_UV_LMAP );
+        buffer.setOffset( x - pos.getX(), y - pos.getY(), z - pos.getZ() );
+        buffer.disableColor();
 
         // See BlockRendererDispatcher#renderBlockDamage
-        TextureAtlasSprite breakingTexture = mc.getTextureMap().getSprite( DESTROY_STAGES[destroyStage] );
-        mc.getBlockRendererDispatcher().getBlockModelRenderer().renderModel(
-            world,
-            ForgeHooksClient.getDamageModel( model, breakingTexture, state, world, pos ),
-            state, pos, buffer, true, random, state.getPositionRandom( pos )
-        );
-
-        ForgeHooksClient.setRenderLayer( BlockRenderLayer.SOLID );
+        Sprite breakingTexture = mc.getSpriteAtlas().getSprite( DESTROY_STAGE_TEXTURES[destroyStage] );
+        mc.getBlockRenderManager().tesselateDamage( state, pos, breakingTexture, world );
 
-        buffer.setTranslation( 0, 0, 0 );
+        buffer.setOffset( 0, 0, 0 );
         Tessellator.getInstance().draw();
 
         postRenderDamagedBlocks();
diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java
index 3ac9ce6fae..f13de4bac8 100644
--- a/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/TileEntityMonitorRenderer.java
@@ -6,6 +6,8 @@
 
 package dan200.computercraft.client.render;
 
+import com.mojang.blaze3d.platform.GLX;
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.client.FrameInfo;
 import dan200.computercraft.client.gui.FixedWidthFontRenderer;
 import dan200.computercraft.core.terminal.Terminal;
@@ -15,18 +17,16 @@
 import dan200.computercraft.shared.util.Colour;
 import dan200.computercraft.shared.util.DirectionUtil;
 import dan200.computercraft.shared.util.Palette;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.OpenGlHelper;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.tileentity.TileEntityRenderer;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.render.block.entity.BlockEntityRenderer;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import org.lwjgl.opengl.GL11;
 
-public class TileEntityMonitorRenderer extends TileEntityRenderer<TileMonitor>
+public class TileEntityMonitorRenderer extends BlockEntityRenderer<TileMonitor>
 {
     @Override
     public void render( TileMonitor tileEntity, double posX, double posY, double posZ, float f, int i )
@@ -64,9 +64,9 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
         posZ += originPos.getZ() - monitorPos.getZ();
 
         // Determine orientation
-        EnumFacing dir = origin.getDirection();
-        EnumFacing front = origin.getFront();
-        float yaw = dir.getHorizontalAngle();
+        Direction dir = origin.getDirection();
+        Direction front = origin.getFront();
+        float yaw = dir.asRotation();
         float pitch = DirectionUtil.toPitchAngle( front );
 
         GlStateManager.pushMatrix();
@@ -85,16 +85,16 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
             double ySize = origin.getHeight() - 2.0 * (TileMonitor.RENDER_MARGIN + TileMonitor.RENDER_BORDER);
 
             // Get renderers
-            Minecraft mc = Minecraft.getInstance();
+            MinecraftClient mc = MinecraftClient.getInstance();
             Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder renderer = tessellator.getBuffer();
+            BufferBuilder renderer = tessellator.getBufferBuilder();
 
             // Get terminal
             boolean redraw = originTerminal.pollTerminalChanged();
 
             // Draw the contents
             GlStateManager.depthMask( false );
-            OpenGlHelper.glMultiTexCoord2f( OpenGlHelper.GL_TEXTURE1, 0xFFFF, 0xFFFF );
+            GLX.glMultiTexCoord2f( GLX.GL_TEXTURE1, 0xFFFF, 0xFFFF );
             GlStateManager.disableLighting();
             mc.gameRenderer.disableLightmap();
             try
@@ -171,7 +171,7 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
                             }
                         }
                         GlStateManager.callList( originTerminal.renderDisplayLists[0] );
-                        GlStateManager.resetColor();
+                        GlStateManager.clearCurrentColor();
 
                         // Draw text
                         fontRenderer.bindFont();
@@ -199,7 +199,7 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
                             }
                         }
                         GlStateManager.callList( originTerminal.renderDisplayLists[1] );
-                        GlStateManager.resetColor();
+                        GlStateManager.clearCurrentColor();
 
                         // Draw cursor
                         fontRenderer.bindFont();
@@ -233,7 +233,7 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
                         if( FrameInfo.getGlobalCursorBlink() )
                         {
                             GlStateManager.callList( originTerminal.renderDisplayLists[2] );
-                            GlStateManager.resetColor();
+                            GlStateManager.clearCurrentColor();
                         }
                     }
                     finally
@@ -251,11 +251,11 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
                     final float g = colour.getG();
                     final float b = colour.getB();
 
-                    renderer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR );
-                    renderer.pos( -TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0D ).tex( 0.0, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-                    renderer.pos( -TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).tex( 0.0, 1.0 ).color( r, g, b, 1.0f ).endVertex();
-                    renderer.pos( xSize + TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0D ).tex( 1.0, 0.0 ).color( r, g, b, 1.0f ).endVertex();
-                    renderer.pos( xSize + TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).tex( 1.0, 1.0 ).color( r, g, b, 1.0f ).endVertex();
+                    renderer.begin( GL11.GL_TRIANGLE_STRIP, VertexFormats.POSITION_UV_COLOR );
+                    renderer.vertex( -TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0D ).texture( 0.0, 0.0 ).color( r, g, b, 1.0f ).next();
+                    renderer.vertex( -TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).texture( 0.0, 1.0 ).color( r, g, b, 1.0f ).next();
+                    renderer.vertex( xSize + TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0D ).texture( 1.0, 0.0 ).color( r, g, b, 1.0f ).next();
+                    renderer.vertex( xSize + TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).texture( 1.0, 1.0 ).color( r, g, b, 1.0f ).next();
                     tessellator.draw();
                 }
             }
@@ -271,11 +271,11 @@ private static void renderMonitorAt( TileMonitor monitor, double posX, double po
             try
             {
                 mc.getTextureManager().bindTexture( FixedWidthFontRenderer.BACKGROUND );
-                renderer.begin( GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION );
-                renderer.pos( -TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0 ).endVertex();
-                renderer.pos( -TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).endVertex();
-                renderer.pos( xSize + TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0 ).endVertex();
-                renderer.pos( xSize + TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).endVertex();
+                renderer.begin( GL11.GL_TRIANGLE_STRIP, VertexFormats.POSITION );
+                renderer.vertex( -TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0 ).next();
+                renderer.vertex( -TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).next();
+                renderer.vertex( xSize + TileMonitor.RENDER_MARGIN, TileMonitor.RENDER_MARGIN, 0.0 ).next();
+                renderer.vertex( xSize + TileMonitor.RENDER_MARGIN, -ySize - TileMonitor.RENDER_MARGIN, 0.0 ).next();
                 tessellator.draw();
             }
             finally
diff --git a/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java b/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java
index a4d35a7f0f..a17bf0d8d1 100644
--- a/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java
+++ b/src/main/java/dan200/computercraft/client/render/TileEntityTurtleRenderer.java
@@ -6,6 +6,7 @@
 
 package dan200.computercraft.client.render;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
@@ -13,38 +14,37 @@
 import dan200.computercraft.shared.util.DirectionUtil;
 import dan200.computercraft.shared.util.Holiday;
 import dan200.computercraft.shared.util.HolidayUtil;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GameRenderer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.model.BakedQuad;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ModelManager;
-import net.minecraft.client.renderer.model.ModelResourceLocation;
-import net.minecraft.client.renderer.texture.TextureMap;
-import net.minecraft.client.renderer.tileentity.TileEntityRenderer;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.client.renderer.vertex.VertexFormat;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.*;
+import net.minecraft.client.render.block.entity.BlockEntityRenderer;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.BakedQuad;
+import net.minecraft.client.texture.SpriteAtlasTexture;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraftforge.client.ForgeHooksClient;
-import net.minecraftforge.client.model.pipeline.LightUtil;
+import net.minecraft.util.math.Vec3i;
 import org.apache.commons.lang3.tuple.Pair;
+import org.lwjgl.BufferUtils;
 import org.lwjgl.opengl.GL11;
 
 import javax.vecmath.Matrix4f;
+import java.nio.FloatBuffer;
 import java.util.List;
 import java.util.Random;
 
-public class TileEntityTurtleRenderer extends TileEntityRenderer<TileTurtle>
+public class TileEntityTurtleRenderer extends BlockEntityRenderer<TileTurtle>
 {
-    private static final ModelResourceLocation NORMAL_TURTLE_MODEL = new ModelResourceLocation( "computercraft:turtle_normal", "inventory" );
-    private static final ModelResourceLocation ADVANCED_TURTLE_MODEL = new ModelResourceLocation( "computercraft:turtle_advanced", "inventory" );
-    private static final ModelResourceLocation COLOUR_TURTLE_MODEL = new ModelResourceLocation( "computercraft:turtle_colour", "inventory" );
-    private static final ModelResourceLocation ELF_OVERLAY_MODEL = new ModelResourceLocation( "computercraft:turtle_elf_overlay", "inventory" );
+    private static final ModelIdentifier NORMAL_TURTLE_MODEL = new ModelIdentifier( "computercraft:turtle_normal", "inventory" );
+    private static final ModelIdentifier ADVANCED_TURTLE_MODEL = new ModelIdentifier( "computercraft:turtle_advanced", "inventory" );
+    private static final ModelIdentifier COLOUR_TURTLE_MODEL = new ModelIdentifier( "computercraft:turtle_colour", "inventory" );
+    private static final ModelIdentifier ELF_OVERLAY_MODEL = new ModelIdentifier( "computercraft:turtle_elf_overlay", "inventory" );
+
+    private static final FloatBuffer matrixBuf = BufferUtils.createFloatBuffer( 16 );
 
     @Override
     public void render( TileTurtle tileEntity, double posX, double posY, double posZ, float partialTicks, int breaking )
@@ -52,7 +52,7 @@ public void render( TileTurtle tileEntity, double posX, double posY, double posZ
         if( tileEntity != null ) renderTurtleAt( tileEntity, posX, posY, posZ, partialTicks );
     }
 
-    public static ModelResourceLocation getTurtleModel( ComputerFamily family, boolean coloured )
+    public static ModelIdentifier getTurtleModel( ComputerFamily family, boolean coloured )
     {
         switch( family )
         {
@@ -64,11 +64,11 @@ public static ModelResourceLocation getTurtleModel( ComputerFamily family, boole
         }
     }
 
-    public static ModelResourceLocation getTurtleOverlayModel( ResourceLocation overlay, boolean christmas )
+    public static ModelIdentifier getTurtleOverlayModel( Identifier overlay, boolean christmas )
     {
         if( overlay != null )
         {
-            return new ModelResourceLocation( overlay, "inventory" );
+            return new ModelIdentifier( overlay, "inventory" );
         }
         else if( christmas )
         {
@@ -84,21 +84,21 @@ private void renderTurtleAt( TileTurtle turtle, double posX, double posY, double
     {
         // Render the label
         String label = turtle.createProxy().getLabel();
-        if( label != null && rendererDispatcher.cameraHitResult != null && turtle.getPos().equals( rendererDispatcher.cameraHitResult.getBlockPos() ) )
+        if( label != null && renderManager.hitResult != null && renderManager.hitResult instanceof BlockHitResult && turtle.getPos().equals( ((BlockHitResult) renderManager.hitResult).getBlockPos() ) )
         {
-            setLightmapDisabled( true );
-            GameRenderer.drawNameplate(
+            method_3570( true );
+            GameRenderer.renderFloatingText(
                 getFontRenderer(), label,
                 (float) posX + 0.5F, (float) posY + 1.2F, (float) posZ + 0.5F, 0,
-                rendererDispatcher.entityYaw, rendererDispatcher.entityPitch, false, false
+                renderManager.cameraEntity.getYaw(), renderManager.cameraEntity.getPitch(), false
             );
-            setLightmapDisabled( false );
+            method_3570( false );
         }
 
         GlStateManager.pushMatrix();
         try
         {
-            IBlockState state = turtle.getBlockState();
+            BlockState state = turtle.getCachedState();
             // Setup the transform
             Vec3d offset = turtle.getRenderOffset( partialTicks );
             float yaw = turtle.getRenderYaw( partialTicks );
@@ -110,18 +110,18 @@ private void renderTurtleAt( TileTurtle turtle, double posX, double posY, double
             {
                 // Flip the model and swap the cull face as winding order will have changed.
                 GlStateManager.scalef( 1.0f, -1.0f, 1.0f );
-                GlStateManager.cullFace( GlStateManager.CullFace.FRONT );
+                GlStateManager.cullFace( GlStateManager.FaceSides.FRONT );
             }
             GlStateManager.translatef( -0.5f, -0.5f, -0.5f );
             // Render the turtle
             int colour = turtle.getColour();
             ComputerFamily family = turtle.getFamily();
-            ResourceLocation overlay = turtle.getOverlay();
+            Identifier overlay = turtle.getOverlay();
 
             renderModel( state, getTurtleModel( family, colour != -1 ), colour == -1 ? null : new int[] { colour } );
 
             // Render the overlay
-            ModelResourceLocation overlayModel = getTurtleOverlayModel(
+            ModelIdentifier overlayModel = getTurtleOverlayModel(
                 overlay,
                 HolidayUtil.getCurrentHoliday() == Holiday.Christmas
             );
@@ -148,11 +148,11 @@ private void renderTurtleAt( TileTurtle turtle, double posX, double posY, double
         finally
         {
             GlStateManager.popMatrix();
-            GlStateManager.cullFace( GlStateManager.CullFace.BACK );
+            GlStateManager.cullFace( GlStateManager.FaceSides.BACK );
         }
     }
 
-    private void renderUpgrade( IBlockState state, TileTurtle turtle, TurtleSide side, float f )
+    private void renderUpgrade( BlockState state, TileTurtle turtle, TurtleSide side, float f )
     {
         ITurtleUpgrade upgrade = turtle.getUpgrade( side );
         if( upgrade != null )
@@ -165,12 +165,21 @@ private void renderUpgrade( IBlockState state, TileTurtle turtle, TurtleSide sid
                 GlStateManager.rotatef( -toolAngle, 1.0f, 0.0f, 0.0f );
                 GlStateManager.translatef( 0.0f, -0.5f, -0.5f );
 
-                Pair<IBakedModel, Matrix4f> pair = upgrade.getModel( turtle.getAccess(), side );
+                Pair<BakedModel, Matrix4f> pair = upgrade.getModel( turtle.getAccess(), side );
                 if( pair != null )
                 {
                     if( pair.getRight() != null )
                     {
-                        ForgeHooksClient.multiplyCurrentGlMatrix( pair.getRight() );
+                        matrixBuf.clear();
+                        float[] t = new float[4];
+                        for( int i = 0; i < 4; i++ )
+                        {
+                            pair.getRight().getColumn( i, t );
+                            matrixBuf.put( t );
+                        }
+                        matrixBuf.flip();
+
+                        GlStateManager.multMatrix( matrixBuf );
                     }
                     if( pair.getLeft() != null )
                     {
@@ -185,20 +194,20 @@ private void renderUpgrade( IBlockState state, TileTurtle turtle, TurtleSide sid
         }
     }
 
-    private void renderModel( IBlockState state, ModelResourceLocation modelLocation, int[] tints )
+    private void renderModel( BlockState state, ModelIdentifier modelLocation, int[] tints )
     {
-        Minecraft mc = Minecraft.getInstance();
-        ModelManager modelManager = mc.getItemRenderer().getItemModelMesher().getModelManager();
+        MinecraftClient mc = MinecraftClient.getInstance();
+        BakedModelManager modelManager = mc.getItemRenderer().getModels().getModelManager();
         renderModel( state, modelManager.getModel( modelLocation ), tints );
     }
 
-    private void renderModel( IBlockState state, IBakedModel model, int[] tints )
+    private void renderModel( BlockState state, BakedModel model, int[] tints )
     {
         Random random = new Random( 0 );
         Tessellator tessellator = Tessellator.getInstance();
-        rendererDispatcher.textureManager.bindTexture( TextureMap.LOCATION_BLOCKS_TEXTURE );
+        renderManager.textureManager.bindTexture( SpriteAtlasTexture.BLOCK_ATLAS_TEX );
         renderQuads( tessellator, model.getQuads( state, null, random ), tints );
-        for( EnumFacing facing : DirectionUtil.FACINGS )
+        for( Direction facing : DirectionUtil.FACINGS )
         {
             renderQuads( tessellator, model.getQuads( state, facing, random ), tints );
         }
@@ -206,27 +215,22 @@ private void renderModel( IBlockState state, IBakedModel model, int[] tints )
 
     private static void renderQuads( Tessellator tessellator, List<BakedQuad> quads, int[] tints )
     {
-        BufferBuilder buffer = tessellator.getBuffer();
-        VertexFormat format = DefaultVertexFormats.ITEM;
+        BufferBuilder buffer = tessellator.getBufferBuilder();
+        VertexFormat format = VertexFormats.POSITION_COLOR_UV_NORMAL;
         buffer.begin( GL11.GL_QUADS, format );
         for( BakedQuad quad : quads )
         {
-            VertexFormat quadFormat = quad.getFormat();
-            if( quadFormat != format )
-            {
-                tessellator.draw();
-                format = quadFormat;
-                buffer.begin( GL11.GL_QUADS, format );
-            }
-
             int colour = 0xFFFFFFFF;
-            if( quad.hasTintIndex() && tints != null )
+            if( quad.hasColor() && tints != null )
             {
-                int index = quad.getTintIndex();
+                int index = quad.getColorIndex();
                 if( index >= 0 && index < tints.length ) colour = tints[index] | 0xFF000000;
             }
 
-            LightUtil.renderQuadColor( buffer, quad, colour );
+            buffer.putVertexData( quad.getVertexData() );
+            buffer.setQuadColor( colour );
+            Vec3i normal = quad.getFace().getVector();
+            buffer.postNormal( normal.getX(), normal.getY(), normal.getZ() );
         }
         tessellator.draw();
     }
diff --git a/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java b/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java
index c1198e9835..1e784acf9d 100644
--- a/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java
+++ b/src/main/java/dan200/computercraft/client/render/TurtleModelLoader.java
@@ -7,14 +7,12 @@
 package dan200.computercraft.client.render;
 
 import dan200.computercraft.ComputerCraft;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.IUnbakedModel;
-import net.minecraft.client.renderer.texture.TextureAtlasSprite;
-import net.minecraft.client.renderer.vertex.VertexFormat;
-import net.minecraft.resources.IResourceManager;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.client.model.ICustomModelLoader;
-import net.minecraftforge.common.model.IModelState;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.ModelRotationContainer;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -24,11 +22,11 @@
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-public final class TurtleModelLoader implements ICustomModelLoader
+public final class TurtleModelLoader
 {
-    private static final ResourceLocation NORMAL_TURTLE_MODEL = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_normal" );
-    private static final ResourceLocation ADVANCED_TURTLE_MODEL = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_advanced" );
-    private static final ResourceLocation COLOUR_TURTLE_MODEL = new ResourceLocation( ComputerCraft.MOD_ID, "block/turtle_colour" );
+    private static final Identifier NORMAL_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_normal" );
+    private static final Identifier ADVANCED_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_advanced" );
+    private static final Identifier COLOUR_TURTLE_MODEL = new Identifier( ComputerCraft.MOD_ID, "block/turtle_colour" );
 
     public static final TurtleModelLoader INSTANCE = new TurtleModelLoader();
 
@@ -36,21 +34,14 @@ private TurtleModelLoader()
     {
     }
 
-    @Override
-    public void onResourceManagerReload( @Nonnull IResourceManager manager )
-    {
-    }
-
-    @Override
-    public boolean accepts( @Nonnull ResourceLocation name )
+    public boolean accepts( @Nonnull Identifier name )
     {
         return name.getNamespace().equals( ComputerCraft.MOD_ID )
             && (name.getPath().equals( "item/turtle_normal" ) || name.getPath().equals( "item/turtle_advanced" ));
     }
 
     @Nonnull
-    @Override
-    public IUnbakedModel loadModel( @Nonnull ResourceLocation name )
+    public UnbakedModel loadModel( @Nonnull Identifier name )
     {
         if( name.getNamespace().equals( ComputerCraft.MOD_ID ) )
         {
@@ -66,35 +57,35 @@ public IUnbakedModel loadModel( @Nonnull ResourceLocation name )
         throw new IllegalStateException( "Loader does not accept " + name );
     }
 
-    private static final class TurtleModel implements IUnbakedModel
+    private static final class TurtleModel implements UnbakedModel
     {
-        private final ResourceLocation family;
+        private final Identifier family;
 
-        private TurtleModel( ResourceLocation family ) {this.family = family;}
+        private TurtleModel( Identifier family ) {this.family = family;}
 
         @Nonnull
         @Override
-        public Collection<ResourceLocation> getDependencies()
+        public Collection<Identifier> getModelDependencies()
         {
             return Arrays.asList( family, COLOUR_TURTLE_MODEL );
         }
 
         @Nonnull
         @Override
-        public Collection<ResourceLocation> getTextures( @Nonnull Function<ResourceLocation, IUnbakedModel> modelGetter, @Nonnull Set<String> missingTextureErrors )
+        public Collection<Identifier> getTextureDependencies( @Nonnull Function<Identifier, UnbakedModel> modelGetter, @Nonnull Set<String> missingTextureErrors )
         {
-            return getDependencies().stream()
-                .flatMap( x -> modelGetter.apply( x ).getTextures( modelGetter, missingTextureErrors ).stream() )
+            return getModelDependencies().stream()
+                .flatMap( x -> modelGetter.apply( x ).getTextureDependencies( modelGetter, missingTextureErrors ).stream() )
                 .collect( Collectors.toSet() );
         }
 
         @Nullable
         @Override
-        public IBakedModel bake( @Nonnull Function<ResourceLocation, IUnbakedModel> modelGetter, @Nonnull Function<ResourceLocation, TextureAtlasSprite> spriteGetter, @Nonnull IModelState state, boolean uvlock, @Nonnull VertexFormat format )
+        public BakedModel bake( @Nonnull ModelLoader loader, @Nonnull Function<Identifier, Sprite> spriteGetter, @Nonnull ModelRotationContainer state )
         {
             return new TurtleSmartItemModel(
-                modelGetter.apply( family ).bake( modelGetter, spriteGetter, state, uvlock, format ),
-                modelGetter.apply( COLOUR_TURTLE_MODEL ).bake( modelGetter, spriteGetter, state, uvlock, format )
+                loader.getOrLoadModel( family ).bake( loader, spriteGetter, state ),
+                loader.getOrLoadModel( COLOUR_TURTLE_MODEL ).bake( loader, spriteGetter, state )
             );
         }
     }
diff --git a/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java b/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java
index b29158056c..5e1e6834b6 100644
--- a/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java
+++ b/src/main/java/dan200/computercraft/client/render/TurtleMultiModel.java
@@ -6,31 +6,31 @@
 
 package dan200.computercraft.client.render;
 
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.client.renderer.model.BakedQuad;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ItemCameraTransforms;
-import net.minecraft.client.renderer.model.ItemOverrideList;
-import net.minecraft.client.renderer.texture.TextureAtlasSprite;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedQuad;
+import net.minecraft.client.render.model.json.ModelItemPropertyOverrideList;
+import net.minecraft.client.render.model.json.ModelTransformation;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.vecmath.Matrix4f;
 import java.util.*;
 
-public class TurtleMultiModel implements IBakedModel
+public class TurtleMultiModel implements BakedModel
 {
-    private final IBakedModel m_baseModel;
-    private final IBakedModel m_overlayModel;
+    private final BakedModel m_baseModel;
+    private final BakedModel m_overlayModel;
     private final Matrix4f m_generalTransform;
-    private final IBakedModel m_leftUpgradeModel;
+    private final BakedModel m_leftUpgradeModel;
     private final Matrix4f m_leftUpgradeTransform;
-    private final IBakedModel m_rightUpgradeModel;
+    private final BakedModel m_rightUpgradeModel;
     private final Matrix4f m_rightUpgradeTransform;
     private List<BakedQuad> m_generalQuads = null;
-    private Map<EnumFacing, List<BakedQuad>> m_faceQuads = new EnumMap<>( EnumFacing.class );
+    private Map<Direction, List<BakedQuad>> m_faceQuads = new EnumMap<>( Direction.class );
 
-    public TurtleMultiModel( IBakedModel baseModel, IBakedModel overlayModel, Matrix4f generalTransform, IBakedModel leftUpgradeModel, Matrix4f leftUpgradeTransform, IBakedModel rightUpgradeModel, Matrix4f rightUpgradeTransform )
+    public TurtleMultiModel( BakedModel baseModel, BakedModel overlayModel, Matrix4f generalTransform, BakedModel leftUpgradeModel, Matrix4f leftUpgradeTransform, BakedModel rightUpgradeModel, Matrix4f rightUpgradeTransform )
     {
         // Get the models
         m_baseModel = baseModel;
@@ -44,7 +44,7 @@ public TurtleMultiModel( IBakedModel baseModel, IBakedModel overlayModel, Matrix
 
     @Nonnull
     @Override
-    public List<BakedQuad> getQuads( IBlockState state, EnumFacing side, @Nonnull Random rand )
+    public List<BakedQuad> getQuads( BlockState state, Direction side, Random rand )
     {
         if( side != null )
         {
@@ -58,7 +58,7 @@ public List<BakedQuad> getQuads( IBlockState state, EnumFacing side, @Nonnull Ra
         }
     }
 
-    private List<BakedQuad> buildQuads( IBlockState state, EnumFacing side, Random rand )
+    private List<BakedQuad> buildQuads( BlockState state, Direction side, Random rand )
     {
         ArrayList<BakedQuad> quads = new ArrayList<>();
         ModelTransformer.transformQuadsTo( quads, m_baseModel.getQuads( state, side, rand ), m_generalTransform );
@@ -95,42 +95,38 @@ private List<BakedQuad> buildQuads( IBlockState state, EnumFacing side, Random r
     }
 
     @Override
-    public boolean isAmbientOcclusion()
+    public boolean useAmbientOcclusion()
     {
-        return m_baseModel.isAmbientOcclusion();
+        return m_baseModel.useAmbientOcclusion();
     }
 
     @Override
-    public boolean isGui3d()
+    public boolean hasDepthInGui()
     {
-        return m_baseModel.isGui3d();
+        return m_baseModel.hasDepthInGui();
     }
 
     @Override
-    public boolean isBuiltInRenderer()
+    public boolean isBuiltin()
     {
-        return m_baseModel.isBuiltInRenderer();
+        return m_baseModel.isBuiltin();
     }
 
-    @Nonnull
     @Override
-    public TextureAtlasSprite getParticleTexture()
+    public Sprite getSprite()
     {
-        return m_baseModel.getParticleTexture();
+        return m_baseModel.getSprite();
     }
 
-    @Nonnull
     @Override
-    @Deprecated
-    public ItemCameraTransforms getItemCameraTransforms()
+    public ModelTransformation getTransformation()
     {
-        return m_baseModel.getItemCameraTransforms();
+        return m_baseModel.getTransformation();
     }
 
-    @Nonnull
     @Override
-    public ItemOverrideList getOverrides()
+    public ModelItemPropertyOverrideList getItemPropertyOverrides()
     {
-        return ItemOverrideList.EMPTY;
+        return ModelItemPropertyOverrideList.EMPTY;
     }
 }
diff --git a/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java b/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java
index bbf5f34ff6..5409719b15 100644
--- a/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java
+++ b/src/main/java/dan200/computercraft/client/render/TurtleSmartItemModel.java
@@ -12,25 +12,31 @@
 import dan200.computercraft.shared.turtle.items.ItemTurtle;
 import dan200.computercraft.shared.util.Holiday;
 import dan200.computercraft.shared.util.HolidayUtil;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.model.*;
-import net.minecraft.client.renderer.texture.TextureAtlasSprite;
-import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.BakedQuad;
+import net.minecraft.client.render.model.json.ModelItemPropertyOverrideList;
+import net.minecraft.client.render.model.json.ModelTransformation;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.entity.LivingEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import javax.vecmath.Matrix4f;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Random;
 
-public class TurtleSmartItemModel implements IBakedModel
+public class TurtleSmartItemModel implements BakedModel
 {
     private static final Matrix4f s_identity, s_flip;
 
@@ -50,11 +56,11 @@ private static class TurtleModelCombination
         final boolean m_colour;
         final ITurtleUpgrade m_leftUpgrade;
         final ITurtleUpgrade m_rightUpgrade;
-        final ResourceLocation m_overlay;
+        final Identifier m_overlay;
         final boolean m_christmas;
         final boolean m_flip;
 
-        TurtleModelCombination( boolean colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, ResourceLocation overlay, boolean christmas, boolean flip )
+        TurtleModelCombination( boolean colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, Identifier overlay, boolean christmas, boolean flip )
         {
             m_colour = colour;
             m_leftUpgrade = leftUpgrade;
@@ -94,35 +100,35 @@ public int hashCode()
         }
     }
 
-    private final IBakedModel familyModel;
-    private final IBakedModel colourModel;
+    private final BakedModel familyModel;
+    private final BakedModel colourModel;
 
-    private HashMap<TurtleModelCombination, IBakedModel> m_cachedModels;
-    private ItemOverrideList m_overrides;
+    private HashMap<TurtleModelCombination, BakedModel> m_cachedModels;
+    private ModelItemPropertyOverrideList m_overrides;
 
-    public TurtleSmartItemModel( IBakedModel familyModel, IBakedModel colourModel )
+    public TurtleSmartItemModel( BakedModel familyModel, BakedModel colourModel )
     {
         this.familyModel = familyModel;
         this.colourModel = colourModel;
 
         m_cachedModels = new HashMap<>();
-        m_overrides = new ItemOverrideList()
+        m_overrides = new ModelItemPropertyOverrideList( null, null, null, Collections.emptyList() )
         {
             @Nonnull
             @Override
-            public IBakedModel getModelWithOverrides( @Nonnull IBakedModel originalModel, @Nonnull ItemStack stack, @Nullable World world, @Nullable EntityLivingBase entity )
+            public BakedModel apply( @Nonnull BakedModel originalModel, @Nonnull ItemStack stack, @Nullable World world, @Nullable LivingEntity entity )
             {
                 ItemTurtle turtle = (ItemTurtle) stack.getItem();
                 int colour = turtle.getColour( stack );
                 ITurtleUpgrade leftUpgrade = turtle.getUpgrade( stack, TurtleSide.Left );
                 ITurtleUpgrade rightUpgrade = turtle.getUpgrade( stack, TurtleSide.Right );
-                ResourceLocation overlay = turtle.getOverlay( stack );
+                Identifier overlay = turtle.getOverlay( stack );
                 boolean christmas = HolidayUtil.getCurrentHoliday() == Holiday.Christmas;
                 String label = turtle.getLabel( stack );
                 boolean flip = label != null && (label.equals( "Dinnerbone" ) || label.equals( "Grumm" ));
                 TurtleModelCombination combo = new TurtleModelCombination( colour != -1, leftUpgrade, rightUpgrade, overlay, christmas, flip );
 
-                IBakedModel model = m_cachedModels.get( combo );
+                BakedModel model = m_cachedModels.get( combo );
                 if( model == null ) m_cachedModels.put( combo, model = buildModel( combo ) );
                 return model;
             }
@@ -131,22 +137,22 @@ public IBakedModel getModelWithOverrides( @Nonnull IBakedModel originalModel, @N
 
     @Nonnull
     @Override
-    public ItemOverrideList getOverrides()
+    public ModelItemPropertyOverrideList getItemPropertyOverrides()
     {
         return m_overrides;
     }
 
-    private IBakedModel buildModel( TurtleModelCombination combo )
+    private BakedModel buildModel( TurtleModelCombination combo )
     {
-        Minecraft mc = Minecraft.getInstance();
-        ModelManager modelManager = mc.getItemRenderer().getItemModelMesher().getModelManager();
-        ModelResourceLocation overlayModelLocation = TileEntityTurtleRenderer.getTurtleOverlayModel( combo.m_overlay, combo.m_christmas );
+        MinecraftClient mc = MinecraftClient.getInstance();
+        BakedModelManager modelManager = mc.getItemRenderer().getModels().getModelManager();
+        ModelIdentifier overlayModelLocation = TileEntityTurtleRenderer.getTurtleOverlayModel( combo.m_overlay, combo.m_christmas );
 
-        IBakedModel baseModel = combo.m_colour ? colourModel : familyModel;
-        IBakedModel overlayModel = overlayModelLocation != null ? modelManager.getModel( overlayModelLocation ) : null;
+        BakedModel baseModel = combo.m_colour ? colourModel : familyModel;
+        BakedModel overlayModel = overlayModelLocation != null ? modelManager.getModel( overlayModelLocation ) : null;
         Matrix4f transform = combo.m_flip ? s_flip : s_identity;
-        Pair<IBakedModel, Matrix4f> leftModel = combo.m_leftUpgrade != null ? combo.m_leftUpgrade.getModel( null, TurtleSide.Left ) : null;
-        Pair<IBakedModel, Matrix4f> rightModel = combo.m_rightUpgrade != null ? combo.m_rightUpgrade.getModel( null, TurtleSide.Right ) : null;
+        Pair<BakedModel, Matrix4f> leftModel = combo.m_leftUpgrade != null ? combo.m_leftUpgrade.getModel( null, TurtleSide.Left ) : null;
+        Pair<BakedModel, Matrix4f> rightModel = combo.m_rightUpgrade != null ? combo.m_rightUpgrade.getModel( null, TurtleSide.Right ) : null;
         if( leftModel != null && rightModel != null )
         {
             return new TurtleMultiModel( baseModel, overlayModel, transform, leftModel.getLeft(), leftModel.getRight(), rightModel.getLeft(), rightModel.getRight() );
@@ -167,42 +173,38 @@ else if( rightModel != null )
 
     @Nonnull
     @Override
-    public List<BakedQuad> getQuads( IBlockState state, EnumFacing facing, @Nonnull Random rand )
+    public List<BakedQuad> getQuads( BlockState state, Direction facing, Random rand )
     {
         return familyModel.getQuads( state, facing, rand );
     }
 
     @Override
-    public boolean isAmbientOcclusion()
+    public boolean useAmbientOcclusion()
     {
-        return familyModel.isAmbientOcclusion();
+        return familyModel.useAmbientOcclusion();
     }
 
     @Override
-    public boolean isGui3d()
+    public boolean hasDepthInGui()
     {
-        return familyModel.isGui3d();
+        return familyModel.hasDepthInGui();
     }
 
     @Override
-    public boolean isBuiltInRenderer()
+    public boolean isBuiltin()
     {
-        return familyModel.isBuiltInRenderer();
+        return familyModel.isBuiltin();
     }
 
-    @Nonnull
     @Override
-    public TextureAtlasSprite getParticleTexture()
+    public Sprite getSprite()
     {
-        return familyModel.getParticleTexture();
+        return null;
     }
 
-    @Nonnull
     @Override
-    @Deprecated
-    public ItemCameraTransforms getItemCameraTransforms()
+    public ModelTransformation getTransformation()
     {
-        return familyModel.getItemCameraTransforms();
+        return familyModel.getTransformation();
     }
-
 }
diff --git a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java b/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java
index 8d12d6dbde..b0fd2933c9 100644
--- a/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java
+++ b/src/main/java/dan200/computercraft/core/computer/MainThreadExecutor.java
@@ -10,7 +10,7 @@
 import dan200.computercraft.api.peripheral.IWorkMonitor;
 import dan200.computercraft.core.tracking.Tracking;
 import dan200.computercraft.shared.turtle.core.TurtleBrain;
-import net.minecraft.tileentity.TileEntity;
+import net.minecraft.block.entity.BlockEntity;
 
 import javax.annotation.Nonnull;
 import java.util.ArrayDeque;
@@ -29,7 +29,7 @@
  * this tick. At the beginning of the tick, we execute as many {@link MainThread} tasks as possible, until our
  * time-frame or the global time frame has expired.
  *
- * Then, when other objects (such as {@link TileEntity}) are ticked, we update how much time we've used using
+ * Then, when other objects (such as {@link BlockEntity}) are ticked, we update how much time we've used using
  * {@link IWorkMonitor#trackWork(long, TimeUnit)}.
  *
  * Now, if anywhere during this period, we use more than our allocated time slice, the executor is marked as
diff --git a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java
index caf97ce71b..9953560a01 100644
--- a/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java
+++ b/src/main/java/dan200/computercraft/core/filesystem/ResourceMount.java
@@ -11,13 +11,9 @@
 import com.google.common.io.ByteStreams;
 import dan200.computercraft.api.filesystem.IMount;
 import dan200.computercraft.core.apis.handles.ArrayByteChannel;
-import net.minecraft.resources.IReloadableResourceManager;
-import net.minecraft.resources.IResource;
-import net.minecraft.resources.IResourceManager;
-import net.minecraft.resources.IResourceManagerReloadListener;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.resource.IResourceType;
-import net.minecraftforge.resource.ISelectiveResourceReloadListener;
+import net.minecraft.resource.*;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.profiler.Profiler;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -28,7 +24,6 @@
 import java.nio.channels.ReadableByteChannel;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
-import java.util.function.Predicate;
 
 public class ResourceMount implements IMount
 {
@@ -58,12 +53,12 @@ public class ResourceMount implements IMount
 
     private final String namespace;
     private final String subPath;
-    private final IReloadableResourceManager manager;
+    private final ReloadableResourceManager manager;
 
     @Nullable
     private FileEntry root;
 
-    public ResourceMount( String namespace, String subPath, IReloadableResourceManager manager )
+    public ResourceMount( String namespace, String subPath, ReloadableResourceManager manager )
     {
         this.namespace = namespace;
         this.subPath = subPath;
@@ -76,8 +71,8 @@ public ResourceMount( String namespace, String subPath, IReloadableResourceManag
     private void load()
     {
         boolean hasAny = false;
-        FileEntry newRoot = new FileEntry( new ResourceLocation( namespace, subPath ) );
-        for( ResourceLocation file : manager.getAllResourceLocations( subPath, s -> true ) )
+        FileEntry newRoot = new FileEntry( new Identifier( namespace, subPath ) );
+        for( Identifier file : manager.findResources( subPath, s -> true ) )
         {
             if( !file.getNamespace().equals( namespace ) ) continue;
 
@@ -120,7 +115,7 @@ private void create( FileEntry lastEntry, String path )
             FileEntry nextEntry = lastEntry.children.get( part );
             if( nextEntry == null )
             {
-                lastEntry.children.put( part, nextEntry = new FileEntry( new ResourceLocation( namespace, subPath + "/" + path ) ) );
+                lastEntry.children.put( part, nextEntry = new FileEntry( new Identifier( namespace, subPath + "/" + path ) ) );
             }
 
             lastEntry = nextEntry;
@@ -164,7 +159,7 @@ public long getSize( @Nonnull String path ) throws IOException
 
             try
             {
-                IResource resource = manager.getResource( file.identifier );
+                Resource resource = manager.getResource( file.identifier );
                 InputStream s = resource.getInputStream();
                 int total = 0, read = 0;
                 do
@@ -220,11 +215,11 @@ public ReadableByteChannel openChannelForRead( @Nonnull String path ) throws IOE
 
     private static class FileEntry
     {
-        final ResourceLocation identifier;
+        final Identifier identifier;
         Map<String, FileEntry> children;
         long size = -1;
 
-        FileEntry( ResourceLocation identifier )
+        FileEntry( Identifier identifier )
         {
             this.identifier = identifier;
         }
@@ -241,34 +236,41 @@ void list( List<String> contents )
     }
 
     /**
-     * A {@link IResourceManagerReloadListener} which reloads any associated mounts.
+     * A {@link ResourceReloadListener} which reloads any associated mounts.
      *
      * While people should really be keeping a permanent reference to this, some people construct it every
      * method call, so let's make this as small as possible.
      */
-    static class Listener implements ISelectiveResourceReloadListener
+    static class Listener extends SupplyingResourceReloadListener<Void>
     {
         private static final Listener INSTANCE = new Listener();
 
         private final Set<ResourceMount> mounts = Collections.newSetFromMap( new WeakHashMap<>() );
-        private final Set<IReloadableResourceManager> managers = Collections.newSetFromMap( new WeakHashMap<>() );
+        private final Set<ReloadableResourceManager> managers = Collections.newSetFromMap( new WeakHashMap<>() );
 
         @Override
-        public void onResourceManagerReload( @Nonnull IResourceManager manager )
+        protected synchronized Void load( ResourceManager manager, Profiler profiler )
         {
-            // FIXME: Remove this. We need this patch in order to prevent trying to load ReloadRequirements.
-            onResourceManagerReload( manager, x -> true );
+            profiler.push( "Mount reloading" );
+            try
+            {
+                for( ResourceMount mount : mounts ) mount.load();
+            }
+            finally
+            {
+                profiler.pop();
+            }
+            return null;
         }
 
         @Override
-        public synchronized void onResourceManagerReload( @Nonnull IResourceManager manager, @Nonnull Predicate<IResourceType> predicate )
+        protected void apply( Void res, ResourceManager manager, Profiler profiler )
         {
-            for( ResourceMount mount : mounts ) mount.load();
         }
 
-        synchronized void add( IReloadableResourceManager manager, ResourceMount mount )
+        synchronized void add( ReloadableResourceManager manager, ResourceMount mount )
         {
-            if( managers.add( manager ) ) manager.addReloadListener( this );
+            if( managers.add( manager ) ) manager.registerListener( this );
             mounts.add( mount );
         }
     }
diff --git a/src/main/java/dan200/computercraft/core/terminal/Terminal.java b/src/main/java/dan200/computercraft/core/terminal/Terminal.java
index a0229f53a8..be5870b8eb 100644
--- a/src/main/java/dan200/computercraft/core/terminal/Terminal.java
+++ b/src/main/java/dan200/computercraft/core/terminal/Terminal.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.core.terminal;
 
 import dan200.computercraft.shared.util.Palette;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 public class Terminal
 {
@@ -334,7 +334,7 @@ public final void clearChanged()
         m_changed = false;
     }
 
-    public synchronized NBTTagCompound writeToNBT( NBTTagCompound nbt )
+    public synchronized CompoundTag writeToNBT( CompoundTag nbt )
     {
         nbt.putInt( "term_cursorX", m_cursorX );
         nbt.putInt( "term_cursorY", m_cursorY );
@@ -354,7 +354,7 @@ public synchronized NBTTagCompound writeToNBT( NBTTagCompound nbt )
         return nbt;
     }
 
-    public synchronized void readFromNBT( NBTTagCompound nbt )
+    public synchronized void readFromNBT( CompoundTag nbt )
     {
         m_cursorX = nbt.getInt( "term_cursorX" );
         m_cursorY = nbt.getInt( "term_cursorY" );
@@ -365,17 +365,17 @@ public synchronized void readFromNBT( NBTTagCompound nbt )
         for( int n = 0; n < m_height; n++ )
         {
             m_text[n].fill( ' ' );
-            if( nbt.contains( "term_text_" + n ) )
+            if( nbt.containsKey( "term_text_" + n ) )
             {
                 m_text[n].write( nbt.getString( "term_text_" + n ) );
             }
             m_textColour[n].fill( base16.charAt( m_cursorColour ) );
-            if( nbt.contains( "term_textColour_" + n ) )
+            if( nbt.containsKey( "term_textColour_" + n ) )
             {
                 m_textColour[n].write( nbt.getString( "term_textColour_" + n ) );
             }
             m_backgroundColour[n].fill( base16.charAt( m_cursorBackgroundColour ) );
-            if( nbt.contains( "term_textBgColour_" + n ) )
+            if( nbt.containsKey( "term_textBgColour_" + n ) )
             {
                 m_backgroundColour[n].write( nbt.getString( "term_textBgColour_" + n ) );
             }
diff --git a/src/main/java/dan200/computercraft/shared/BundledRedstone.java b/src/main/java/dan200/computercraft/shared/BundledRedstone.java
index 78ee64eac8..84ae84ae4c 100644
--- a/src/main/java/dan200/computercraft/shared/BundledRedstone.java
+++ b/src/main/java/dan200/computercraft/shared/BundledRedstone.java
@@ -10,8 +10,8 @@
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
 import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -24,18 +24,18 @@ public final class BundledRedstone
 
     private BundledRedstone() {}
 
-    public static synchronized void register( @Nonnull IBundledRedstoneProvider provider )
+    public static void register( @Nonnull IBundledRedstoneProvider provider )
     {
         Preconditions.checkNotNull( provider, "provider cannot be null" );
         providers.add( provider );
     }
 
-    public static int getDefaultOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public static int getDefaultOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
         return World.isValid( pos ) ? DefaultBundledRedstoneProvider.getDefaultBundledRedstoneOutput( world, pos, side ) : -1;
     }
 
-    private static int getUnmaskedOutput( World world, BlockPos pos, EnumFacing side )
+    private static int getUnmaskedOutput( World world, BlockPos pos, Direction side )
     {
         if( !World.isValid( pos ) ) return -1;
 
@@ -60,7 +60,7 @@ private static int getUnmaskedOutput( World world, BlockPos pos, EnumFacing side
         return combinedSignal;
     }
 
-    public static int getOutput( World world, BlockPos pos, EnumFacing side )
+    public static int getOutput( World world, BlockPos pos, Direction side )
     {
         int signal = getUnmaskedOutput( world, pos, side );
         return signal >= 0 ? signal : 0;
diff --git a/src/main/java/dan200/computercraft/shared/Config.java b/src/main/java/dan200/computercraft/shared/Config.java
deleted file mode 100644
index 5d3b60f6cf..0000000000
--- a/src/main/java/dan200/computercraft/shared/Config.java
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * This file is part of ComputerCraft - http://www.computercraft.info
- * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
- * Send enquiries to dratcliffe@gmail.com
- */
-
-package dan200.computercraft.shared;
-
-import com.google.common.base.CaseFormat;
-import com.google.common.base.Converter;
-import dan200.computercraft.ComputerCraft;
-import dan200.computercraft.api.turtle.event.TurtleAction;
-import dan200.computercraft.core.apis.AddressPredicate;
-import dan200.computercraft.core.apis.http.websocket.Websocket;
-import net.minecraftforge.common.ForgeConfigSpec;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.ModLoadingContext;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.config.ModConfig;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import static dan200.computercraft.ComputerCraft.DEFAULT_HTTP_BLACKLIST;
-import static dan200.computercraft.ComputerCraft.DEFAULT_HTTP_WHITELIST;
-import static net.minecraftforge.common.ForgeConfigSpec.Builder;
-import static net.minecraftforge.common.ForgeConfigSpec.ConfigValue;
-
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD )
-public final class Config
-{
-    private static final int MODEM_MAX_RANGE = 100000;
-
-    private static final String TRANSLATION_PREFIX = "gui.computercraft.config.";
-
-    private static ConfigValue<Integer> computerSpaceLimit;
-    private static ConfigValue<Integer> floppySpaceLimit;
-    private static ConfigValue<Integer> maximumFilesOpen;
-    private static ConfigValue<Boolean> disableLua51Features;
-    private static ConfigValue<String> defaultComputerSettings;
-    private static ConfigValue<Boolean> debugEnabled;
-    private static ConfigValue<Boolean> logComputerErrors;
-
-    private static ConfigValue<Integer> computerThreads;
-    private static ConfigValue<Long> maxMainGlobalTime;
-    private static ConfigValue<Long> maxMainComputerTime;
-
-    private static ConfigValue<Boolean> httpEnabled;
-    private static ConfigValue<Boolean> httpWebsocketEnabled;
-    private static ConfigValue<List<? extends String>> httpWhitelist;
-    private static ConfigValue<List<? extends String>> httpBlacklist;
-
-    private static ConfigValue<Integer> httpTimeout;
-    private static ConfigValue<Integer> httpMaxRequests;
-    private static ConfigValue<Long> httpMaxDownload;
-    private static ConfigValue<Long> httpMaxUpload;
-    private static ConfigValue<Integer> httpMaxWebsockets;
-    private static ConfigValue<Integer> httpMaxWebsocketMessage;
-
-    private static ConfigValue<Boolean> commandBlockEnabled;
-    private static ConfigValue<Integer> modemRange;
-    private static ConfigValue<Integer> modemHighAltitudeRange;
-    private static ConfigValue<Integer> modemRangeDuringStorm;
-    private static ConfigValue<Integer> modemHighAltitudeRangeDuringStorm;
-    private static ConfigValue<Integer> maxNotesPerTick;
-
-    private static ConfigValue<Boolean> turtlesNeedFuel;
-    private static ConfigValue<Integer> turtleFuelLimit;
-    private static ConfigValue<Integer> advancedTurtleFuelLimit;
-    private static ConfigValue<Boolean> turtlesObeyBlockProtection;
-    private static ConfigValue<Boolean> turtlesCanPush;
-    private static ConfigValue<List<? extends String>> turtleDisabledActions;
-
-    private static final ForgeConfigSpec spec;
-
-    private Config() {}
-
-    static
-    {
-        Builder builder = new Builder();
-
-        { // General computers
-            computerSpaceLimit = builder
-                .comment( "The disk space limit for computers and turtles, in bytes" )
-                .translation( TRANSLATION_PREFIX + "computer_space_limit" )
-                .define( "computer_space_limit", ComputerCraft.computerSpaceLimit );
-
-            floppySpaceLimit = builder
-                .comment( "The disk space limit for floppy disks, in bytes" )
-                .translation( TRANSLATION_PREFIX + "floppy_space_limit" )
-                .define( "floppy_space_limit", ComputerCraft.floppySpaceLimit );
-
-            maximumFilesOpen = builder
-                .comment( "Set how many files a computer can have open at the same time. Set to 0 for unlimited." )
-                .translation( TRANSLATION_PREFIX + "maximum_open_files" )
-                .defineInRange( "maximum_open_files", ComputerCraft.maximumFilesOpen, 0, Integer.MAX_VALUE );
-
-            disableLua51Features = builder
-                .comment( "Set this to true to disable Lua 5.1 functions that will be removed in a future update. " +
-                    "Useful for ensuring forward compatibility of your programs now." )
-                .define( "disable_lua51_features", ComputerCraft.disable_lua51_features );
-
-            defaultComputerSettings = builder
-                .comment( "A comma separated list of default system settings to set on new computers. Example: " +
-                    "\"shell.autocomplete=false,lua.autocomplete=false,edit.autocomplete=false\" will disable all " +
-                    "autocompletion" )
-                .define( "default_computer_settings", ComputerCraft.default_computer_settings );
-
-            debugEnabled = builder
-                .comment( "Enable Lua's debug library. This is sandboxed to each computer, so is generally safe to be used by players." )
-                .define( "debug_enabled", ComputerCraft.debug_enable );
-
-            logComputerErrors = builder
-                .comment( "Log exceptions thrown by peripherals and other Lua objects.\n" +
-                    "This makes it easier for mod authors to debug problems, but may result in log spam should people use buggy methods." )
-                .define( "log_computer_errors", ComputerCraft.logPeripheralErrors );
-        }
-
-        {
-            builder.comment( "Controls execution behaviour of computers. This is largely intended for fine-tuning " +
-                "servers, and generally shouldn't need to be touched" );
-            builder.push( "execution" );
-
-            computerThreads = builder
-                .comment( "Set the number of threads computers can run on. A higher number means more computers can run " +
-                    "at once, but may induce lag.\n" +
-                    "Please note that some mods may not work with a thread count higher than 1. Use with caution." )
-                .worldRestart()
-                .defineInRange( "computer_threads", ComputerCraft.computer_threads, 1, Integer.MAX_VALUE );
-
-            maxMainGlobalTime = builder
-                .comment( "The maximum time that can be spent executing tasks in a single tick, in milliseconds.\n" +
-                    "Note, we will quite possibly go over this limit, as there's no way to tell how long a will take " +
-                    "- this aims to be the upper bound of the average time." )
-                .defineInRange( "max_main_global_time", TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainGlobalTime ), 1, Long.MAX_VALUE );
-
-            maxMainComputerTime = builder
-                .comment( "The ideal maximum time a computer can execute for in a tick, in milliseconds.\n" +
-                    "Note, we will quite possibly go over this limit, as there's no way to tell how long a will take " +
-                    "- this aims to be the upper bound of the average time." )
-                .defineInRange( "max_main_computer_time", TimeUnit.NANOSECONDS.toMillis( ComputerCraft.maxMainComputerTime ), 1, Long.MAX_VALUE );
-
-            builder.pop();
-        }
-
-        { // HTTP
-            builder.comment( "Controls the HTTP API" );
-            builder.push( "http" );
-
-            httpEnabled = builder
-                .comment( "Enable the \"http\" API on Computers (see \"http_whitelist\" and \"http_blacklist\" for more " +
-                    "fine grained control than this)" )
-                .define( "enabled", ComputerCraft.http_enable );
-
-            httpWebsocketEnabled = builder
-                .comment( "Enable use of http websockets. This requires the \"http_enable\" option to also be true." )
-                .define( "websocket_enabled", ComputerCraft.http_websocket_enable );
-
-            httpWhitelist = builder
-                .comment( "A list of wildcards for domains or IP ranges that can be accessed through the \"http\" API on Computers.\n" +
-                    "Set this to \"*\" to access to the entire internet. Example: \"*.pastebin.com\" will restrict access to just subdomains of pastebin.com.\n" +
-                    "You can use domain names (\"pastebin.com\"), wilcards (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\")." )
-                .defineList( "whitelist", Arrays.asList( DEFAULT_HTTP_WHITELIST ), x -> true );
-
-            httpBlacklist = builder
-                .comment( "A list of wildcards for domains or IP ranges that cannot be accessed through the \"http\" API on Computers.\n" +
-                    "If this is empty then all whitelisted domains will be accessible. Example: \"*.github.com\" will block access to all subdomains of github.com.\n" +
-                    "You can use domain names (\"pastebin.com\"), wilcards (\"*.pastebin.com\") or CIDR notation (\"127.0.0.0/8\")." )
-                .defineList( "blacklist", Arrays.asList( DEFAULT_HTTP_BLACKLIST ), x -> true );
-
-            httpTimeout = builder
-                .comment( "The period of time (in milliseconds) to wait before a HTTP request times out. Set to 0 for unlimited." )
-                .defineInRange( "timeout", ComputerCraft.httpTimeout, 0, Integer.MAX_VALUE );
-
-            httpMaxRequests = builder
-                .comment( "The number of http requests a computer can make at one time. Additional requests will be queued, and sent when the running requests have finished. Set to 0 for unlimited." )
-                .defineInRange( "max_requests", ComputerCraft.httpMaxRequests, 0, Integer.MAX_VALUE );
-
-            httpMaxDownload = builder
-                .comment( "The maximum size (in bytes) that a computer can download in a single request. Note that responses may receive more data than allowed, but this data will not be returned to the client." )
-                .defineInRange( "max_download", ComputerCraft.httpMaxDownload, 0, Long.MAX_VALUE );
-
-            httpMaxUpload = builder
-                .comment( "The maximum size (in bytes) that a computer can upload in a single request. This includes headers and POST text." )
-                .defineInRange( "max_upload", ComputerCraft.httpMaxUpload, 0, Long.MAX_VALUE );
-
-            httpMaxWebsockets = builder
-                .comment( "The number of websockets a computer can have open at one time. Set to 0 for unlimited." )
-                .defineInRange( "max_websockets", ComputerCraft.httpMaxWebsockets, 1, Integer.MAX_VALUE );
-
-            httpMaxWebsocketMessage = builder
-                .comment( "The maximum size (in bytes) that a computer can send or receive in one websocket packet." )
-                .defineInRange( "max_websocket_message", ComputerCraft.httpMaxWebsocketMessage, 0, Websocket.MAX_MESSAGE_SIZE );
-
-            builder.pop();
-        }
-
-        { // Peripherals
-            builder.comment( "Various options relating to peripherals." );
-            builder.push( "peripheral" );
-
-            commandBlockEnabled = builder
-                .comment( "Enable Command Block peripheral support" )
-                .define( "command_block_enabled", ComputerCraft.enableCommandBlock );
-
-            modemRange = builder
-                .comment( "The range of Wireless Modems at low altitude in clear weather, in meters" )
-                .defineInRange( "modem_range", ComputerCraft.modem_range, 0, MODEM_MAX_RANGE );
-
-            modemHighAltitudeRange = builder
-                .comment( "The range of Wireless Modems at maximum altitude in clear weather, in meters" )
-                .defineInRange( "modem_high_altitude_range", ComputerCraft.modem_highAltitudeRange, 0, MODEM_MAX_RANGE );
-
-            modemRangeDuringStorm = builder
-                .comment( "The range of Wireless Modems at low altitude in stormy weather, in meters" )
-                .defineInRange( "modem_range_during_storm", ComputerCraft.modem_rangeDuringStorm, 0, MODEM_MAX_RANGE );
-
-            modemHighAltitudeRangeDuringStorm = builder
-                .comment( "The range of Wireless Modems at maximum altitude in stormy weather, in meters" )
-                .defineInRange( "modem_high_altitude_range_during_storm", ComputerCraft.modem_highAltitudeRangeDuringStorm, 0, MODEM_MAX_RANGE );
-
-            maxNotesPerTick = builder
-                .comment( "Maximum amount of notes a speaker can play at once" )
-                .defineInRange( "max_notes_per_tick", ComputerCraft.maxNotesPerTick, 1, Integer.MAX_VALUE );
-
-            builder.pop();
-        }
-
-        { // Turtles
-            builder.comment( "Various options relating to turtles." );
-            builder.push( "turtle" );
-
-            turtlesNeedFuel = builder
-                .comment( "Set whether Turtles require fuel to move" )
-                .define( "need_fuel", ComputerCraft.turtlesNeedFuel );
-
-            turtleFuelLimit = builder
-                .comment( "The fuel limit for Turtles" )
-                .defineInRange( "normal_fuel_limit", ComputerCraft.turtleFuelLimit, 0, Integer.MAX_VALUE );
-
-            advancedTurtleFuelLimit = builder
-                .comment( "The fuel limit for Advanced Turtles" )
-                .defineInRange( "advanced_fuel_limit", ComputerCraft.advancedTurtleFuelLimit, 0, Integer.MAX_VALUE );
-
-            turtlesObeyBlockProtection = builder
-                .comment( "If set to true, Turtles will be unable to build, dig, or enter protected areas (such as near the server spawn point)" )
-                .define( "obey_block_protection", ComputerCraft.turtlesObeyBlockProtection );
-
-            turtlesCanPush = builder
-                .comment( "If set to true, Turtles will push entities out of the way instead of stopping if there is space to do so" )
-                .define( "can_push", ComputerCraft.turtlesCanPush );
-
-            turtleDisabledActions = builder
-                .comment( "A list of turtle actions which are disabled." )
-                .defineList( "disabled_actions", Collections.emptyList(), x -> x instanceof String && getAction( (String) x ) != null );
-
-            builder.pop();
-        }
-
-        spec = builder.build();
-    }
-
-    public static void load()
-    {
-        ModLoadingContext.get().registerConfig( ModConfig.Type.COMMON, spec );
-    }
-
-    public static void sync()
-    {
-        // General
-        ComputerCraft.computerSpaceLimit = computerSpaceLimit.get();
-        ComputerCraft.floppySpaceLimit = floppySpaceLimit.get();
-        ComputerCraft.maximumFilesOpen = maximumFilesOpen.get();
-        ComputerCraft.disable_lua51_features = disableLua51Features.get();
-        ComputerCraft.default_computer_settings = defaultComputerSettings.get();
-        ComputerCraft.debug_enable = debugEnabled.get();
-        ComputerCraft.computer_threads = computerThreads.get();
-        ComputerCraft.logPeripheralErrors = logComputerErrors.get();
-
-        // Execution
-        ComputerCraft.computer_threads = computerThreads.get();
-        ComputerCraft.maxMainGlobalTime = TimeUnit.MILLISECONDS.toNanos( maxMainGlobalTime.get() );
-        ComputerCraft.maxMainComputerTime = TimeUnit.MILLISECONDS.toNanos( maxMainComputerTime.get() );
-
-        // HTTP
-        ComputerCraft.http_enable = httpEnabled.get();
-        ComputerCraft.http_websocket_enable = httpWebsocketEnabled.get();
-        ComputerCraft.http_whitelist = new AddressPredicate( httpWhitelist.get() );
-        ComputerCraft.http_blacklist = new AddressPredicate( httpBlacklist.get() );
-
-        ComputerCraft.httpTimeout = httpTimeout.get();
-        ComputerCraft.httpMaxRequests = httpMaxRequests.get();
-        ComputerCraft.httpMaxDownload = httpMaxDownload.get();
-        ComputerCraft.httpMaxUpload = httpMaxUpload.get();
-        ComputerCraft.httpMaxWebsockets = httpMaxWebsockets.get();
-        ComputerCraft.httpMaxWebsocketMessage = httpMaxWebsocketMessage.get();
-
-        // Peripheral
-        ComputerCraft.enableCommandBlock = commandBlockEnabled.get();
-        ComputerCraft.maxNotesPerTick = maxNotesPerTick.get();
-        ComputerCraft.modem_range = modemRange.get();
-        ComputerCraft.modem_highAltitudeRange = modemHighAltitudeRange.get();
-        ComputerCraft.modem_rangeDuringStorm = modemRangeDuringStorm.get();
-        ComputerCraft.modem_highAltitudeRangeDuringStorm = modemHighAltitudeRangeDuringStorm.get();
-
-        // Turtles
-        ComputerCraft.turtlesNeedFuel = turtlesNeedFuel.get();
-        ComputerCraft.turtleFuelLimit = turtleFuelLimit.get();
-        ComputerCraft.advancedTurtleFuelLimit = advancedTurtleFuelLimit.get();
-        ComputerCraft.turtlesObeyBlockProtection = turtlesObeyBlockProtection.get();
-        ComputerCraft.turtlesCanPush = turtlesCanPush.get();
-
-        ComputerCraft.turtleDisabledActions.clear();
-        for( String value : turtleDisabledActions.get() ) ComputerCraft.turtleDisabledActions.add( getAction( value ) );
-    }
-
-    @SubscribeEvent
-    public static void sync( ModConfig.Loading event )
-    {
-        sync();
-    }
-
-    @SubscribeEvent
-    public static void sync( ModConfig.ConfigReloading event )
-    {
-        sync();
-    }
-
-    private static final Converter<String, String> converter = CaseFormat.LOWER_CAMEL.converterTo( CaseFormat.UPPER_UNDERSCORE );
-
-    private static TurtleAction getAction( String value )
-    {
-        try
-        {
-            return TurtleAction.valueOf( converter.convert( value ) );
-        }
-        catch( IllegalArgumentException e )
-        {
-            return null;
-        }
-    }
-}
diff --git a/src/main/java/dan200/computercraft/shared/Peripherals.java b/src/main/java/dan200/computercraft/shared/Peripherals.java
index 960ae0c0a0..4a37c6f523 100644
--- a/src/main/java/dan200/computercraft/shared/Peripherals.java
+++ b/src/main/java/dan200/computercraft/shared/Peripherals.java
@@ -9,8 +9,8 @@
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.peripheral.IPeripheral;
 import dan200.computercraft.api.peripheral.IPeripheralProvider;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -30,12 +30,12 @@ public static synchronized void register( @Nonnull IPeripheralProvider provider
         providers.add( provider );
     }
 
-    public static IPeripheral getPeripheral( World world, BlockPos pos, EnumFacing side )
+    public static IPeripheral getPeripheral( World world, BlockPos pos, Direction side )
     {
-        return World.isValid( pos ) && !world.isRemote ? getPeripheralAt( world, pos, side ) : null;
+        return World.isValid( pos ) && !world.isClient ? getPeripheralAt( world, pos, side ) : null;
     }
 
-    private static IPeripheral getPeripheralAt( World world, BlockPos pos, EnumFacing side )
+    private static IPeripheral getPeripheralAt( World world, BlockPos pos, Direction side )
     {
         // Try the handlers in order:
         for( IPeripheralProvider peripheralProvider : providers )
diff --git a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java
index 9a665e9812..1d8050cd06 100644
--- a/src/main/java/dan200/computercraft/shared/PocketUpgrades.java
+++ b/src/main/java/dan200/computercraft/shared/PocketUpgrades.java
@@ -10,17 +10,13 @@
 import dan200.computercraft.api.pocket.IPocketUpgrade;
 import dan200.computercraft.shared.util.InventoryUtil;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.fml.ModContainer;
-import net.minecraftforge.fml.ModLoadingContext;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.util.*;
 
 public final class PocketUpgrades
 {
     private static final Map<String, IPocketUpgrade> upgrades = new HashMap<>();
-    private static final IdentityHashMap<IPocketUpgrade, String> upgradeOwners = new IdentityHashMap<>();
 
     private PocketUpgrades() {}
 
@@ -36,9 +32,6 @@ public static synchronized void register( @Nonnull IPocketUpgrade upgrade )
         }
 
         upgrades.put( id, upgrade );
-
-        ModContainer mc = ModLoadingContext.get().getActiveContainer();
-        if( mc != null && mc.getModId() != null ) upgradeOwners.put( upgrade, mc.getModId() );
     }
 
     public static IPocketUpgrade get( String id )
@@ -65,12 +58,6 @@ public static IPocketUpgrade get( @Nonnull ItemStack stack )
         return null;
     }
 
-    @Nullable
-    public static String getOwner( IPocketUpgrade upgrade )
-    {
-        return upgradeOwners.get( upgrade );
-    }
-
     public static Iterable<IPocketUpgrade> getVanillaUpgrades()
     {
         List<IPocketUpgrade> vanilla = new ArrayList<>();
diff --git a/src/main/java/dan200/computercraft/shared/Registry.java b/src/main/java/dan200/computercraft/shared/Registry.java
index 5c36d45e40..91fe6d840b 100644
--- a/src/main/java/dan200/computercraft/shared/Registry.java
+++ b/src/main/java/dan200/computercraft/shared/Registry.java
@@ -8,14 +8,18 @@
 
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.ComputerCraftAPI;
+import dan200.computercraft.shared.common.ColourableRecipe;
 import dan200.computercraft.shared.computer.blocks.BlockComputer;
 import dan200.computercraft.shared.computer.blocks.TileCommandComputer;
 import dan200.computercraft.shared.computer.blocks.TileComputer;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.items.ItemComputer;
+import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
 import dan200.computercraft.shared.media.items.ItemDisk;
 import dan200.computercraft.shared.media.items.ItemPrintout;
 import dan200.computercraft.shared.media.items.ItemTreasureDisk;
+import dan200.computercraft.shared.media.recipes.DiskRecipe;
+import dan200.computercraft.shared.media.recipes.PrintoutRecipe;
 import dan200.computercraft.shared.peripheral.diskdrive.BlockDiskDrive;
 import dan200.computercraft.shared.peripheral.diskdrive.TileDiskDrive;
 import dan200.computercraft.shared.peripheral.modem.wired.*;
@@ -30,239 +34,219 @@
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
 import dan200.computercraft.shared.pocket.peripherals.PocketModem;
 import dan200.computercraft.shared.pocket.peripherals.PocketSpeaker;
+import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
 import dan200.computercraft.shared.turtle.blocks.BlockTurtle;
 import dan200.computercraft.shared.turtle.blocks.TileTurtle;
-import dan200.computercraft.shared.turtle.core.TurtlePlayer;
 import dan200.computercraft.shared.turtle.items.ItemTurtle;
+import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
+import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
 import dan200.computercraft.shared.turtle.upgrades.*;
-import dan200.computercraft.shared.util.CreativeTabMain;
+import dan200.computercraft.shared.util.ImpostorRecipe;
+import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
+import net.fabricmc.fabric.api.block.FabricBlockSettings;
+import net.fabricmc.fabric.api.client.itemgroup.FabricItemGroupBuilder;
 import net.minecraft.block.Block;
-import net.minecraft.block.material.Material;
-import net.minecraft.entity.EntityType;
-import net.minecraft.init.Items;
+import net.minecraft.block.Material;
+import net.minecraft.block.entity.BlockEntityType;
 import net.minecraft.item.Item;
-import net.minecraft.item.ItemBlock;
 import net.minecraft.item.ItemGroup;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.event.RegistryEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.registries.IForgeRegistry;
-
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD )
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.item.block.BlockItem;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.MutableRegistry;
+
 public final class Registry
 {
-    private static final ItemGroup mainItemGroup = new CreativeTabMain();
+    private static final ItemGroup mainItemGroup = FabricItemGroupBuilder
+        .create( new Identifier( ComputerCraft.MOD_ID, "main" ) )
+        .icon( () -> new ItemStack( ComputerCraft.Items.computerNormal ) )
+        .build();
 
     private Registry()
     {
     }
 
-    @SubscribeEvent
-    public static void registerBlocks( RegistryEvent.Register<Block> event )
+    public static void registerBlocks( MutableRegistry<Block> registry )
     {
-        IForgeRegistry<Block> registry = event.getRegistry();
-
         // Computers
         ComputerCraft.Blocks.computerNormal = new BlockComputer(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2.0f ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2.0f ).build(),
             ComputerFamily.Normal, TileComputer.FACTORY_NORMAL
         );
 
         ComputerCraft.Blocks.computerAdvanced = new BlockComputer(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2.0f ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2.0f ).build(),
             ComputerFamily.Advanced, TileComputer.FACTORY_ADVANCED
         );
 
         ComputerCraft.Blocks.computerCommand = new BlockComputer(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( -1, 6000000.0F ),
+            FabricBlockSettings.of( Material.STONE ).strength( -1, 6000000.0F ).build(),
             ComputerFamily.Command, TileCommandComputer.FACTORY
         );
 
-        registry.registerAll(
-            ComputerCraft.Blocks.computerNormal.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "computer_normal" ) ),
-            ComputerCraft.Blocks.computerAdvanced.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "computer_advanced" ) ),
-            ComputerCraft.Blocks.computerCommand.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "computer_command" ) )
-        );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "computer_normal" ), ComputerCraft.Blocks.computerNormal );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "computer_advanced" ), ComputerCraft.Blocks.computerAdvanced );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "computer_command" ), ComputerCraft.Blocks.computerCommand );
 
         // Turtles
         ComputerCraft.Blocks.turtleNormal = new BlockTurtle(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2.5f ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2.5f ).build(),
             ComputerFamily.Normal, TileTurtle.FACTORY_NORMAL
         );
 
         ComputerCraft.Blocks.turtleAdvanced = new BlockTurtle(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2.5f ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2.5f ).build(),
             ComputerFamily.Advanced, TileTurtle.FACTORY_ADVANCED
         );
 
-        registry.registerAll(
-            ComputerCraft.Blocks.turtleNormal.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "turtle_normal" ) ),
-            ComputerCraft.Blocks.turtleAdvanced.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "turtle_advanced" ) )
-        );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "turtle_normal" ), ComputerCraft.Blocks.turtleNormal );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "turtle_advanced" ), ComputerCraft.Blocks.turtleAdvanced );
 
         // Peripherals
         ComputerCraft.Blocks.speaker = new BlockSpeaker(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 )
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build()
         );
 
         ComputerCraft.Blocks.diskDrive = new BlockDiskDrive(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 )
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build()
         );
 
         ComputerCraft.Blocks.monitorNormal = new BlockMonitor(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build(),
             TileMonitor.FACTORY_NORMAL
         );
 
         ComputerCraft.Blocks.monitorAdvanced = new BlockMonitor(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build(),
             TileMonitor.FACTORY_ADVANCED
         );
 
         ComputerCraft.Blocks.printer = new BlockPrinter(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 )
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build()
         );
 
         ComputerCraft.Blocks.wirelessModemNormal = new BlockWirelessModem(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build(),
             TileWirelessModem.FACTORY_NORMAL
         );
 
         ComputerCraft.Blocks.wirelessModemAdvanced = new BlockWirelessModem(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 2 ),
+            FabricBlockSettings.of( Material.STONE ).hardness( 2 ).build(),
             TileWirelessModem.FACTORY_ADVANCED
         );
 
         ComputerCraft.Blocks.wiredModemFull = new BlockWiredModemFull(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 1.5f )
+            FabricBlockSettings.of( Material.STONE ).hardness( 1.5f ).build()
         );
 
         ComputerCraft.Blocks.cable = new BlockCable(
-            Block.Properties.create( Material.ROCK ).hardnessAndResistance( 1.5f )
+            FabricBlockSettings.of( Material.STONE ).hardness( 1.5f ).build()
         );
 
-        registry.registerAll(
-            ComputerCraft.Blocks.speaker.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "speaker" ) ),
-            ComputerCraft.Blocks.diskDrive.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "disk_drive" ) ),
-            ComputerCraft.Blocks.monitorNormal.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "monitor_normal" ) ),
-            ComputerCraft.Blocks.monitorAdvanced.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "monitor_advanced" ) ),
-            ComputerCraft.Blocks.printer.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "printer" ) ),
-            ComputerCraft.Blocks.wirelessModemNormal.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "wireless_modem_normal" ) ),
-            ComputerCraft.Blocks.wirelessModemAdvanced.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "wireless_modem_advanced" ) ),
-            ComputerCraft.Blocks.wiredModemFull.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "wired_modem_full" ) ),
-            ComputerCraft.Blocks.cable.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "cable" ) )
-        );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "speaker" ), ComputerCraft.Blocks.speaker );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "disk_drive" ), ComputerCraft.Blocks.diskDrive );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "monitor_normal" ), ComputerCraft.Blocks.monitorNormal );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "monitor_advanced" ), ComputerCraft.Blocks.monitorAdvanced );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "printer" ), ComputerCraft.Blocks.printer );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "wireless_modem_normal" ), ComputerCraft.Blocks.wirelessModemNormal );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "wireless_modem_advanced" ), ComputerCraft.Blocks.wirelessModemAdvanced );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "wired_modem_full" ), ComputerCraft.Blocks.wiredModemFull );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "cable" ), ComputerCraft.Blocks.cable );
     }
 
-    @SubscribeEvent
-    public static void registerTileEntities( RegistryEvent.Register<TileEntityType<?>> event )
+    public static void registerTileEntities( MutableRegistry<BlockEntityType<?>> registry )
     {
-        IForgeRegistry<TileEntityType<?>> registry = event.getRegistry();
-
         // Computers
-        registry.registerAll( TileComputer.FACTORY_NORMAL, TileComputer.FACTORY_ADVANCED, TileCommandComputer.FACTORY );
+        registry.add( TileComputer.FACTORY_NORMAL.getId(), TileComputer.FACTORY_NORMAL );
+        registry.add( TileComputer.FACTORY_ADVANCED.getId(), TileComputer.FACTORY_ADVANCED );
+        registry.add( TileCommandComputer.FACTORY.getId(), TileCommandComputer.FACTORY );
 
         // Turtles
-        registry.registerAll( TileTurtle.FACTORY_NORMAL, TileTurtle.FACTORY_ADVANCED );
+        registry.add( TileTurtle.FACTORY_NORMAL.getId(), TileTurtle.FACTORY_NORMAL );
+        registry.add( TileTurtle.FACTORY_ADVANCED.getId(), TileTurtle.FACTORY_ADVANCED );
 
         // Peripherals
-        registry.registerAll(
-            TileSpeaker.FACTORY,
-            TileDiskDrive.FACTORY,
-            TileMonitor.FACTORY_NORMAL,
-            TileMonitor.FACTORY_ADVANCED,
-            TilePrinter.FACTORY,
-            TileWirelessModem.FACTORY_NORMAL,
-            TileWirelessModem.FACTORY_ADVANCED,
-            TileWiredModemFull.FACTORY,
-            TileCable.FACTORY
-        );
+        registry.add( TileSpeaker.FACTORY.getId(), TileSpeaker.FACTORY );
+        registry.add( TileDiskDrive.FACTORY.getId(), TileDiskDrive.FACTORY );
+        registry.add( TilePrinter.FACTORY.getId(), TilePrinter.FACTORY );
+
+        registry.add( TileMonitor.FACTORY_NORMAL.getId(), TileMonitor.FACTORY_NORMAL );
+        registry.add( TileMonitor.FACTORY_ADVANCED.getId(), TileMonitor.FACTORY_ADVANCED );
+
+        registry.add( TileWirelessModem.FACTORY_NORMAL.getId(), TileWirelessModem.FACTORY_NORMAL );
+        registry.add( TileWirelessModem.FACTORY_ADVANCED.getId(), TileWirelessModem.FACTORY_ADVANCED );
+        registry.add( TileCable.FACTORY.getId(), TileCable.FACTORY );
+        registry.add( TileWiredModemFull.FACTORY.getId(), TileWiredModemFull.FACTORY );
     }
 
-    private static <T extends ItemBlock> T setupItemBlock( T item )
+    private static void registerItemBlock( MutableRegistry<Item> registry, BlockItem item )
     {
-        item.setRegistryName( item.getBlock().getRegistryName() );
-        return item;
+        registry.add( net.minecraft.util.registry.Registry.BLOCK.getId( item.getBlock() ), item );
     }
 
-    private static Item.Properties defaultItem()
+    private static Item.Settings defaultItem()
     {
-        return new Item.Properties().group( mainItemGroup );
+        return new Item.Settings().itemGroup( mainItemGroup );
     }
 
-    @SubscribeEvent
-    public static void registerItems( RegistryEvent.Register<Item> event )
+    public static void registerItems( MutableRegistry<Item> registry )
     {
-        IForgeRegistry<Item> registry = event.getRegistry();
-
         // Computer
         ComputerCraft.Items.computerNormal = new ItemComputer( ComputerCraft.Blocks.computerNormal, defaultItem() );
         ComputerCraft.Items.computerAdvanced = new ItemComputer( ComputerCraft.Blocks.computerAdvanced, defaultItem() );
         ComputerCraft.Items.computerCommand = new ItemComputer( ComputerCraft.Blocks.computerCommand, defaultItem() );
 
-        registry.registerAll(
-            setupItemBlock( ComputerCraft.Items.computerNormal ),
-            setupItemBlock( ComputerCraft.Items.computerAdvanced ),
-            setupItemBlock( ComputerCraft.Items.computerCommand )
-        );
+        registerItemBlock( registry, ComputerCraft.Items.computerNormal );
+        registerItemBlock( registry, ComputerCraft.Items.computerAdvanced );
+        registerItemBlock( registry, ComputerCraft.Items.computerCommand );
 
         // Turtle
         ComputerCraft.Items.turtleNormal = new ItemTurtle( ComputerCraft.Blocks.turtleNormal, defaultItem() );
         ComputerCraft.Items.turtleAdvanced = new ItemTurtle( ComputerCraft.Blocks.turtleAdvanced, defaultItem() );
-        registry.registerAll(
-            setupItemBlock( ComputerCraft.Items.turtleNormal ),
-            setupItemBlock( ComputerCraft.Items.turtleAdvanced )
-        );
+
+        registerItemBlock( registry, ComputerCraft.Items.turtleNormal );
+        registerItemBlock( registry, ComputerCraft.Items.turtleAdvanced );
 
         // Pocket computer
-        ComputerCraft.Items.pocketComputerNormal = new ItemPocketComputer( defaultItem().maxStackSize( 1 ), ComputerFamily.Normal );
-        ComputerCraft.Items.pocketComputerAdvanced = new ItemPocketComputer( defaultItem().maxStackSize( 1 ), ComputerFamily.Advanced );
+        ComputerCraft.Items.pocketComputerNormal = new ItemPocketComputer( defaultItem().stackSize( 1 ), ComputerFamily.Normal );
+        ComputerCraft.Items.pocketComputerAdvanced = new ItemPocketComputer( defaultItem().stackSize( 1 ), ComputerFamily.Advanced );
 
-        registry.registerAll(
-            ComputerCraft.Items.pocketComputerNormal.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "pocket_computer_normal" ) ),
-            ComputerCraft.Items.pocketComputerAdvanced.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "pocket_computer_advanced" ) )
-        );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "pocket_computer_normal" ), ComputerCraft.Items.pocketComputerNormal );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "pocket_computer_advanced" ), ComputerCraft.Items.pocketComputerAdvanced );
 
         // Floppy disk
-        ComputerCraft.Items.disk = new ItemDisk( defaultItem().maxStackSize( 1 ) );
-        ComputerCraft.Items.treasureDisk = new ItemTreasureDisk( defaultItem().maxStackSize( 1 ) );
+        ComputerCraft.Items.disk = new ItemDisk( defaultItem().stackSize( 1 ) );
+        ComputerCraft.Items.treasureDisk = new ItemTreasureDisk( defaultItem().stackSize( 1 ) );
 
-        registry.registerAll(
-            ComputerCraft.Items.disk.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "disk" ) ),
-            ComputerCraft.Items.treasureDisk.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "treasure_disk" ) )
-        );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "disk" ), ComputerCraft.Items.disk );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "treasure_disk" ), ComputerCraft.Items.treasureDisk );
 
         // Printouts
-        ComputerCraft.Items.printedPage = new ItemPrintout( defaultItem().maxStackSize( 1 ), ItemPrintout.Type.PAGE );
-        ComputerCraft.Items.printedPages = new ItemPrintout( defaultItem().maxStackSize( 1 ), ItemPrintout.Type.PAGES );
-        ComputerCraft.Items.printedBook = new ItemPrintout( defaultItem().maxStackSize( 1 ), ItemPrintout.Type.BOOK );
-
-        registry.registerAll(
-            ComputerCraft.Items.printedPage.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "printed_page" ) ),
-            ComputerCraft.Items.printedPages.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "printed_pages" ) ),
-            ComputerCraft.Items.printedBook.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "printed_book" ) )
-        );
+        ComputerCraft.Items.printedPage = new ItemPrintout( defaultItem().stackSize( 1 ), ItemPrintout.Type.PAGE );
+        ComputerCraft.Items.printedPages = new ItemPrintout( defaultItem().stackSize( 1 ), ItemPrintout.Type.PAGES );
+        ComputerCraft.Items.printedBook = new ItemPrintout( defaultItem().stackSize( 1 ), ItemPrintout.Type.BOOK );
+
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "printed_page" ), ComputerCraft.Items.printedPage );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "printed_pages" ), ComputerCraft.Items.printedPages );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "printed_book" ), ComputerCraft.Items.printedBook );
 
         // Peripherals
-        registry.registerAll(
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.speaker, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.diskDrive, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.printer, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.monitorNormal, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.monitorAdvanced, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.wirelessModemNormal, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.wirelessModemAdvanced, defaultItem() ) ),
-            setupItemBlock( new ItemBlock( ComputerCraft.Blocks.wiredModemFull, defaultItem() ) )
-        );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.speaker, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.diskDrive, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.printer, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.monitorNormal, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.monitorAdvanced, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.wirelessModemNormal, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.wirelessModemAdvanced, defaultItem() ) );
+        registerItemBlock( registry, new BlockItem( ComputerCraft.Blocks.wiredModemFull, defaultItem() ) );
 
         ComputerCraft.Items.cable = new ItemBlockCable.Cable( ComputerCraft.Blocks.cable, defaultItem() );
         ComputerCraft.Items.wiredModem = new ItemBlockCable.WiredModem( ComputerCraft.Blocks.cable, defaultItem() );
-        registry.registerAll(
-            ComputerCraft.Items.cable.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "cable" ) ),
-            ComputerCraft.Items.wiredModem.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "wired_modem" ) )
-        );
+
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "cable" ), ComputerCraft.Items.cable );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "wired_modem" ), ComputerCraft.Items.wiredModem );
 
         registerTurtleUpgrades();
         registerPocketUpgrades();
@@ -271,31 +255,31 @@ public static void registerItems( RegistryEvent.Register<Item> event )
     private static void registerTurtleUpgrades()
     {
         // Upgrades
-        ComputerCraft.TurtleUpgrades.wirelessModemNormal = new TurtleModem( false, new ResourceLocation( ComputerCraft.MOD_ID, "wireless_modem_normal" ) );
-        TurtleUpgrades.register( ComputerCraft.TurtleUpgrades.wirelessModemNormal );
+        ComputerCraft.TurtleUpgrades.wirelessModemNormal = new TurtleModem( false, new Identifier( ComputerCraft.MOD_ID, "wireless_modem_normal" ) );
+        ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.wirelessModemNormal );
 
-        ComputerCraft.TurtleUpgrades.wirelessModemAdvanced = new TurtleModem( true, new ResourceLocation( ComputerCraft.MOD_ID, "wireless_modem_advanced" ) );
+        ComputerCraft.TurtleUpgrades.wirelessModemAdvanced = new TurtleModem( true, new Identifier( ComputerCraft.MOD_ID, "wireless_modem_advanced" ) );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.wirelessModemAdvanced );
 
-        ComputerCraft.TurtleUpgrades.speaker = new TurtleSpeaker( new ResourceLocation( ComputerCraft.MOD_ID, "speaker" ) );
+        ComputerCraft.TurtleUpgrades.speaker = new TurtleSpeaker( new Identifier( ComputerCraft.MOD_ID, "speaker" ) );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.speaker );
 
-        ComputerCraft.TurtleUpgrades.craftingTable = new TurtleCraftingTable( new ResourceLocation( "minecraft", "crafting_table" ) );
+        ComputerCraft.TurtleUpgrades.craftingTable = new TurtleCraftingTable( new Identifier( "minecraft", "crafting_table" ) );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.craftingTable );
 
-        ComputerCraft.TurtleUpgrades.diamondSword = new TurtleSword( new ResourceLocation( "minecraft", "diamond_sword" ), Items.DIAMOND_SWORD );
+        ComputerCraft.TurtleUpgrades.diamondSword = new TurtleSword( new Identifier( "minecraft", "diamond_sword" ), Items.DIAMOND_SWORD );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondSword );
 
-        ComputerCraft.TurtleUpgrades.diamondShovel = new TurtleShovel( new ResourceLocation( "minecraft", "diamond_shovel" ), Items.DIAMOND_SHOVEL );
+        ComputerCraft.TurtleUpgrades.diamondShovel = new TurtleShovel( new Identifier( "minecraft", "diamond_shovel" ), Items.DIAMOND_SHOVEL );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondShovel );
 
-        ComputerCraft.TurtleUpgrades.diamondPickaxe = new TurtleTool( new ResourceLocation( "minecraft", "diamond_pickaxe" ), Items.DIAMOND_PICKAXE );
+        ComputerCraft.TurtleUpgrades.diamondPickaxe = new TurtleTool( new Identifier( "minecraft", "diamond_pickaxe" ), Items.DIAMOND_PICKAXE );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondPickaxe );
 
-        ComputerCraft.TurtleUpgrades.diamondAxe = new TurtleAxe( new ResourceLocation( "minecraft", "diamond_axe" ), Items.DIAMOND_AXE );
+        ComputerCraft.TurtleUpgrades.diamondAxe = new TurtleAxe( new Identifier( "minecraft", "diamond_axe" ), Items.DIAMOND_AXE );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondAxe );
 
-        ComputerCraft.TurtleUpgrades.diamondHoe = new TurtleHoe( new ResourceLocation( "minecraft", "diamond_hoe" ), Items.DIAMOND_HOE );
+        ComputerCraft.TurtleUpgrades.diamondHoe = new TurtleHoe( new Identifier( "minecraft", "diamond_hoe" ), Items.DIAMOND_HOE );
         ComputerCraftAPI.registerTurtleUpgrade( ComputerCraft.TurtleUpgrades.diamondHoe );
     }
 
@@ -306,9 +290,16 @@ private static void registerPocketUpgrades()
         ComputerCraftAPI.registerPocketUpgrade( ComputerCraft.PocketUpgrades.speaker = new PocketSpeaker() );
     }
 
-    @SubscribeEvent
-    public static void registerEntities( RegistryEvent.Register<EntityType<?>> registry )
+    public static void registerRecipes( MutableRegistry<RecipeSerializer<?>> registry )
     {
-        registry.getRegistry().register( TurtlePlayer.TYPE.setRegistryName( new ResourceLocation( ComputerCraft.MOD_ID, "turtle_player" ) ) );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "colour" ), ColourableRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "computer_upgrade" ), ComputerUpgradeRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "pocket_computer_upgrade" ), PocketComputerUpgradeRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "disk" ), DiskRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "printout" ), PrintoutRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "turtle" ), TurtleRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "turtle_upgrade" ), TurtleUpgradeRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "impostor_shaped" ), ImpostorRecipe.SERIALIZER );
+        registry.add( new Identifier( ComputerCraft.MOD_ID, "impostor_shapeless" ), ImpostorShapelessRecipe.SERIALIZER );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/TurtlePermissions.java b/src/main/java/dan200/computercraft/shared/TurtlePermissions.java
index f28c421cf1..4c2cc36d83 100644
--- a/src/main/java/dan200/computercraft/shared/TurtlePermissions.java
+++ b/src/main/java/dan200/computercraft/shared/TurtlePermissions.java
@@ -6,32 +6,30 @@
 
 package dan200.computercraft.shared;
 
+import com.google.common.eventbus.Subscribe;
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.turtle.event.TurtleActionEvent;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
 
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID )
 public final class TurtlePermissions
 {
-    public static boolean isBlockEnterable( World world, BlockPos pos, EntityPlayer player )
+    public static boolean isBlockEnterable( World world, BlockPos pos, PlayerEntity player )
     {
         MinecraftServer server = world.getServer();
-        return server == null || world.isRemote || !server.isBlockProtected( world, pos, player );
+        return server == null || world.isClient || !server.isSpawnProtected( world, pos, player );
     }
 
-    public static boolean isBlockEditable( World world, BlockPos pos, EntityPlayer player )
+    public static boolean isBlockEditable( World world, BlockPos pos, PlayerEntity player )
     {
         MinecraftServer server = world.getServer();
-        return server == null || world.isRemote || !server.isBlockProtected( world, pos, player );
+        return server == null || world.isClient || !server.isSpawnProtected( world, pos, player );
     }
 
-    @SubscribeEvent
-    public static void onTurtleAction( TurtleActionEvent event )
+    @Subscribe
+    public void onTurtleAction( TurtleActionEvent event )
     {
         if( ComputerCraft.turtleDisabledActions.contains( event.getAction() ) )
         {
diff --git a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java
index 63fb3f4789..0bf0cb6373 100644
--- a/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java
+++ b/src/main/java/dan200/computercraft/shared/TurtleUpgrades.java
@@ -11,17 +11,13 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.util.InventoryUtil;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.fml.ModContainer;
-import net.minecraftforge.fml.ModLoadingContext;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.util.*;
 
 public final class TurtleUpgrades
 {
     private static final Map<String, ITurtleUpgrade> upgrades = new HashMap<>();
-    private static final IdentityHashMap<ITurtleUpgrade, String> upgradeOwners = new IdentityHashMap<>();
 
     private TurtleUpgrades() {}
 
@@ -37,9 +33,6 @@ public static void register( @Nonnull ITurtleUpgrade upgrade )
         }
 
         upgrades.put( id, upgrade );
-
-        ModContainer mc = ModLoadingContext.get().getActiveContainer();
-        if( mc != null && mc.getModId() != null ) upgradeOwners.put( upgrade, mc.getModId() );
     }
 
 
@@ -84,12 +77,6 @@ public static Iterable<ITurtleUpgrade> getVanillaUpgrades()
         return vanilla;
     }
 
-    @Nullable
-    public static String getOwner( @Nonnull ITurtleUpgrade upgrade )
-    {
-        return upgradeOwners.get( upgrade );
-    }
-
     public static Iterable<ITurtleUpgrade> getUpgrades()
     {
         return Collections.unmodifiableCollection( upgrades.values() );
diff --git a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
index db1c6f3a39..7356ee1965 100644
--- a/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
+++ b/src/main/java/dan200/computercraft/shared/command/CommandComputerCraft.java
@@ -21,16 +21,15 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.network.Containers;
-import net.minecraft.command.CommandSource;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.network.play.server.SPacketPlayerPosLook;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
 import net.minecraft.world.World;
-import net.minecraft.world.WorldServer;
 
 import javax.annotation.Nonnull;
 import java.util.*;
@@ -45,7 +44,7 @@
 import static dan200.computercraft.shared.command.builder.CommandBuilder.command;
 import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.choice;
 import static dan200.computercraft.shared.command.text.ChatHelpers.*;
-import static net.minecraft.command.Commands.literal;
+import static net.minecraft.server.command.ServerCommandManager.literal;
 
 public final class CommandComputerCraft
 {
@@ -59,7 +58,7 @@ private CommandComputerCraft()
     {
     }
 
-    public static void register( CommandDispatcher<CommandSource> dispatcher )
+    public static void register( CommandDispatcher<ServerCommandSource> dispatcher )
     {
         dispatcher.register( choice( "computercraft" )
             .then( literal( "dump" )
@@ -67,17 +66,17 @@ public static void register( CommandDispatcher<CommandSource> dispatcher )
                 .executes( context -> {
                     TableBuilder table = new TableBuilder( DUMP_LIST_ID, "Computer", "On", "Position" );
 
-                    CommandSource source = context.getSource();
+                    ServerCommandSource source = context.getSource();
                     List<ServerComputer> computers = new ArrayList<>( ComputerCraft.serverComputerRegistry.getComputers() );
 
                     // Unless we're on a server, limit the number of rows we can send.
                     World world = source.getWorld();
-                    BlockPos pos = new BlockPos( source.getPos() );
+                    BlockPos pos = new BlockPos( source.getPosition() );
 
                     computers.sort( ( a, b ) -> {
                         if( a.getWorld() == b.getWorld() && a.getWorld() == world )
                         {
-                            return Double.compare( a.getPosition().distanceSq( pos ), b.getPosition().distanceSq( pos ) );
+                            return Double.compare( a.getPosition().getSquaredDistance( pos ), b.getPosition().getSquaredDistance( pos ) );
                         }
                         else if( a.getWorld() == world )
                         {
@@ -169,17 +168,17 @@ else if( b.getWorld() == world )
 
                     if( world == null || pos == null ) throw TP_NOT_THERE.create();
 
-                    Entity entity = context.getSource().assertIsEntity();
-                    if( !(entity instanceof EntityPlayerMP) ) throw TP_NOT_PLAYER.create();
+                    Entity entity = context.getSource().getEntityOrThrow();
+                    if( !(entity instanceof ServerPlayerEntity) ) throw TP_NOT_PLAYER.create();
 
-                    EntityPlayerMP player = (EntityPlayerMP) entity;
+                    ServerPlayerEntity player = (ServerPlayerEntity) entity;
                     if( player.getEntityWorld() == world )
                     {
-                        player.connection.setPlayerLocation( pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0, EnumSet.noneOf( SPacketPlayerPosLook.EnumFlags.class ) );
+                        player.networkHandler.teleportRequest( pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0, Collections.emptySet() );
                     }
                     else
                     {
-                        player.teleport( (WorldServer) world, pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0 );
+                        player.method_14251( (ServerWorld) world, pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 0, 0 );
                     }
 
                     return 1;
@@ -210,7 +209,7 @@ else if( b.getWorld() == world )
                 .requires( UserLevel.OP )
                 .arg( "computer", oneComputer() )
                 .executes( context -> {
-                    EntityPlayerMP player = context.getSource().asPlayer();
+                    ServerPlayerEntity player = context.getSource().getPlayer();
                     ServerComputer computer = getComputerArgument( context, "computer" );
                     Containers.openComputerGUI( player, computer );
                     return 1;
@@ -257,18 +256,18 @@ else if( b.getWorld() == world )
         );
     }
 
-    private static ITextComponent linkComputer( CommandSource source, ServerComputer serverComputer, int computerId )
+    private static TextComponent linkComputer( ServerCommandSource source, ServerComputer serverComputer, int computerId )
     {
-        ITextComponent out = new TextComponentString( "" );
+        TextComponent out = new StringTextComponent( "" );
 
         // Append the computer instance
         if( serverComputer == null )
         {
-            out.appendSibling( text( "?" ) );
+            out.append( text( "?" ) );
         }
         else
         {
-            out.appendSibling( link(
+            out.append( link(
                 text( Integer.toString( serverComputer.getInstanceID() ) ),
                 "/computercraft dump " + serverComputer.getInstanceID(),
                 translate( "commands.computercraft.dump.action" )
@@ -276,20 +275,20 @@ private static ITextComponent linkComputer( CommandSource source, ServerComputer
         }
 
         // And ID
-        out.appendText( " (id " + computerId + ")" );
+        out.append( " (id " + computerId + ")" );
 
         // And, if we're a player, some useful links
         if( serverComputer != null && UserLevel.OP.test( source ) && isPlayer( source ) )
         {
             out
-                .appendText( " " )
-                .appendSibling( link(
+                .append( " " )
+                .append( link(
                     text( "\u261b" ),
                     "/computercraft tp " + serverComputer.getInstanceID(),
                     translate( "commands.computercraft.tp.action" )
                 ) )
-                .appendText( " " )
-                .appendSibling( link(
+                .append( " " )
+                .append( link(
                     text( "\u20e2" ),
                     "/computercraft view " + serverComputer.getInstanceID(),
                     translate( "commands.computercraft.view.action" )
@@ -299,7 +298,7 @@ private static ITextComponent linkComputer( CommandSource source, ServerComputer
         return out;
     }
 
-    private static ITextComponent linkPosition( CommandSource context, ServerComputer computer )
+    private static TextComponent linkPosition( ServerCommandSource context, ServerComputer computer )
     {
         if( UserLevel.OP.test( context ) )
         {
@@ -316,20 +315,20 @@ private static ITextComponent linkPosition( CommandSource context, ServerCompute
     }
 
     @Nonnull
-    private static TrackingContext getTimingContext( CommandSource source )
+    private static TrackingContext getTimingContext( ServerCommandSource source )
     {
         Entity entity = source.getEntity();
-        return entity instanceof EntityPlayer ? Tracking.getContext( entity.getUniqueID() ) : Tracking.getContext( SYSTEM_UUID );
+        return entity instanceof PlayerEntity ? Tracking.getContext( entity.getUuid() ) : Tracking.getContext( SYSTEM_UUID );
     }
 
     private static final List<TrackingField> DEFAULT_FIELDS = Arrays.asList( TrackingField.TASKS, TrackingField.TOTAL_TIME, TrackingField.AVERAGE_TIME, TrackingField.MAX_TIME );
 
-    private static int displayTimings( CommandSource source, TrackingField sortField, List<TrackingField> fields ) throws CommandSyntaxException
+    private static int displayTimings( ServerCommandSource source, TrackingField sortField, List<TrackingField> fields ) throws CommandSyntaxException
     {
         return displayTimings( source, getTimingContext( source ).getTimings(), sortField, fields );
     }
 
-    private static int displayTimings( CommandSource source, @Nonnull List<ComputerTracker> timings, @Nonnull TrackingField sortField, @Nonnull List<TrackingField> fields ) throws CommandSyntaxException
+    private static int displayTimings( ServerCommandSource source, @Nonnull List<ComputerTracker> timings, @Nonnull TrackingField sortField, @Nonnull List<TrackingField> fields ) throws CommandSyntaxException
     {
         if( timings.isEmpty() ) throw NO_TIMINGS_EXCEPTION.create();
 
@@ -345,7 +344,7 @@ private static int displayTimings( CommandSource source, @Nonnull List<ComputerT
 
         timings.sort( Comparator.<ComputerTracker, Long>comparing( x -> x.get( sortField ) ).reversed() );
 
-        ITextComponent[] headers = new ITextComponent[1 + fields.size()];
+        TextComponent[] headers = new TextComponent[1 + fields.size()];
         headers[0] = translate( "commands.computercraft.track.dump.computer" );
         for( int i = 0; i < fields.size(); i++ ) headers[i + 1] = translate( fields.get( i ).translationKey() );
         TableBuilder table = new TableBuilder( TRACK_ID, headers );
@@ -355,9 +354,9 @@ private static int displayTimings( CommandSource source, @Nonnull List<ComputerT
             Computer computer = entry.getComputer();
             ServerComputer serverComputer = computer == null ? null : lookup.get( computer );
 
-            ITextComponent computerComponent = linkComputer( source, serverComputer, entry.getComputerId() );
+            TextComponent computerComponent = linkComputer( source, serverComputer, entry.getComputerId() );
 
-            ITextComponent[] row = new ITextComponent[1 + fields.size()];
+            TextComponent[] row = new TextComponent[1 + fields.size()];
             row[0] = computerComponent;
             for( int i = 0; i < fields.size(); i++ ) row[i + 1] = text( entry.getFormatted( fields.get( i ) ) );
             table.row( row );
diff --git a/src/main/java/dan200/computercraft/shared/command/CommandCopy.java b/src/main/java/dan200/computercraft/shared/command/CommandCopy.java
index 47ede7da2d..7c554544f9 100644
--- a/src/main/java/dan200/computercraft/shared/command/CommandCopy.java
+++ b/src/main/java/dan200/computercraft/shared/command/CommandCopy.java
@@ -8,23 +8,17 @@
 
 import com.mojang.brigadier.CommandDispatcher;
 import com.mojang.brigadier.arguments.StringArgumentType;
-import dan200.computercraft.ComputerCraft;
-import net.minecraft.client.Minecraft;
-import net.minecraft.command.CommandSource;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
-import net.minecraft.util.text.event.ClickEvent;
-import net.minecraft.util.text.event.HoverEvent;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.client.event.ClientChatEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.text.event.ClickEvent;
+import net.minecraft.text.event.HoverEvent;
 
-import static net.minecraft.command.Commands.argument;
-import static net.minecraft.command.Commands.literal;
+import static net.minecraft.server.command.ServerCommandManager.argument;
+import static net.minecraft.server.command.ServerCommandManager.literal;
 
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, value = Dist.CLIENT )
 public final class CommandCopy
 {
     private static final String PREFIX = "/computercraft copy ";
@@ -33,35 +27,36 @@ private CommandCopy()
     {
     }
 
-    public static void register( CommandDispatcher<CommandSource> registry )
+    public static void register( CommandDispatcher<ServerCommandSource> registry )
     {
         registry.register( literal( "computercraft" )
             .then( literal( "copy" ) )
             .then( argument( "message", StringArgumentType.greedyString() ) )
             .executes( context -> {
-                Minecraft.getInstance().keyboardListener.setClipboardString( context.getArgument( "message", String.class ) );
+                MinecraftClient.getInstance().keyboard.setClipboard( context.getArgument( "message", String.class ) );
                 return 1;
             } )
         );
     }
 
-    @SubscribeEvent
-    public static void onClientSendMessage( ClientChatEvent event )
+    public static boolean onClientSendMessage( String message )
     {
         // Emulate the command on the client side
-        if( event.getMessage().startsWith( PREFIX ) )
+        if( message.startsWith( PREFIX ) )
         {
-            Minecraft.getInstance().keyboardListener.setClipboardString( event.getMessage().substring( PREFIX.length() ) );
-            event.setCanceled( true );
+            MinecraftClient.getInstance().keyboard.setClipboard( message.substring( PREFIX.length() ) );
+            return true;
         }
+
+        return false;
     }
 
-    public static ITextComponent createCopyText( String text )
+    public static TextComponent createCopyText( String text )
     {
-        TextComponentString name = new TextComponentString( text );
+        StringTextComponent name = new StringTextComponent( text );
         name.getStyle()
             .setClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, PREFIX + text ) )
-            .setHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, new TextComponentTranslation( "gui.computercraft.tooltip.copy" ) ) );
+            .setHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, new TranslatableTextComponent( "gui.computercraft.tooltip.copy" ) ) );
         return name;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java
index 378c4d623b..f09bf4198b 100644
--- a/src/main/java/dan200/computercraft/shared/command/CommandUtils.java
+++ b/src/main/java/dan200/computercraft/shared/command/CommandUtils.java
@@ -9,11 +9,11 @@
 import com.mojang.brigadier.context.CommandContext;
 import com.mojang.brigadier.suggestion.Suggestions;
 import com.mojang.brigadier.suggestion.SuggestionsBuilder;
-import net.minecraft.command.CommandSource;
-import net.minecraft.command.ISuggestionProvider;
+import dan200.computercraft.api.turtle.event.FakePlayer;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraftforge.common.util.FakePlayer;
+import net.minecraft.server.command.CommandSource;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.network.ServerPlayerEntity;
 
 import java.util.Arrays;
 import java.util.Locale;
@@ -24,29 +24,29 @@ public final class CommandUtils
 {
     private CommandUtils() {}
 
-    public static boolean isPlayer( CommandSource output )
+    public static boolean isPlayer( ServerCommandSource output )
     {
         Entity sender = output.getEntity();
-        return sender instanceof EntityPlayerMP
+        return sender instanceof ServerPlayerEntity
             && !(sender instanceof FakePlayer)
-            && ((EntityPlayerMP) sender).connection != null;
+            && ((ServerPlayerEntity) sender).networkHandler != null;
     }
 
     @SuppressWarnings( "unchecked" )
-    public static CompletableFuture<Suggestions> suggestOnServer( CommandContext<?> context, SuggestionsBuilder builder, Function<CommandContext<CommandSource>, CompletableFuture<Suggestions>> supplier )
+    public static CompletableFuture<Suggestions> suggestOnServer( CommandContext<?> context, SuggestionsBuilder builder, Function<CommandContext<ServerCommandSource>, CompletableFuture<Suggestions>> supplier )
     {
         Object source = context.getSource();
-        if( !(source instanceof ISuggestionProvider) )
+        if( !(source instanceof CommandSource) )
         {
             return Suggestions.empty();
         }
-        else if( source instanceof CommandSource )
+        else if( source instanceof ServerCommandSource )
         {
-            return supplier.apply( (CommandContext<CommandSource>) context );
+            return supplier.apply( (CommandContext<ServerCommandSource>) context );
         }
         else
         {
-            return ((ISuggestionProvider) source).getSuggestionsFromServer( (CommandContext<ISuggestionProvider>) context, builder );
+            return ((CommandSource) source).getCompletions( (CommandContext<CommandSource>) context, builder );
         }
     }
 
diff --git a/src/main/java/dan200/computercraft/shared/command/Exceptions.java b/src/main/java/dan200/computercraft/shared/command/Exceptions.java
index a1a45b8021..eb48f73453 100644
--- a/src/main/java/dan200/computercraft/shared/command/Exceptions.java
+++ b/src/main/java/dan200/computercraft/shared/command/Exceptions.java
@@ -9,7 +9,7 @@
 import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType;
 import com.mojang.brigadier.exceptions.DynamicCommandExceptionType;
 import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
-import net.minecraft.util.text.TextComponentTranslation;
+import net.minecraft.text.TranslatableTextComponent;
 
 public final class Exceptions
 {
@@ -28,16 +28,16 @@ public final class Exceptions
 
     private static SimpleCommandExceptionType translated( String key )
     {
-        return new SimpleCommandExceptionType( new TextComponentTranslation( key ) );
+        return new SimpleCommandExceptionType( new TranslatableTextComponent( key ) );
     }
 
     private static DynamicCommandExceptionType translated1( String key )
     {
-        return new DynamicCommandExceptionType( x -> new TextComponentTranslation( key, x ) );
+        return new DynamicCommandExceptionType( x -> new TranslatableTextComponent( key, x ) );
     }
 
     private static Dynamic2CommandExceptionType translated2( String key )
     {
-        return new Dynamic2CommandExceptionType( ( x, y ) -> new TextComponentTranslation( key, x, y ) );
+        return new Dynamic2CommandExceptionType( ( x, y ) -> new TranslatableTextComponent( key, x, y ) );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/command/UserLevel.java b/src/main/java/dan200/computercraft/shared/command/UserLevel.java
index 7a24bc1fea..55ff7f118c 100644
--- a/src/main/java/dan200/computercraft/shared/command/UserLevel.java
+++ b/src/main/java/dan200/computercraft/shared/command/UserLevel.java
@@ -6,17 +6,17 @@
 
 package dan200.computercraft.shared.command;
 
-import net.minecraft.command.CommandSource;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.command.ServerCommandSource;
 
 import java.util.function.Predicate;
 
 /**
  * The level a user must be at in order to execute a command.
  */
-public enum UserLevel implements Predicate<CommandSource>
+public enum UserLevel implements Predicate<ServerCommandSource>
 {
     /**
      * Only can be used by the owner of the server: namely the server console or the player in SSP.
@@ -54,16 +54,16 @@ public int toLevel()
     }
 
     @Override
-    public boolean test( CommandSource source )
+    public boolean test( ServerCommandSource source )
     {
         if( this == ANYONE ) return true;
 
         // We *always* allow level 0 stuff, even if the
-        MinecraftServer server = source.getServer();
+        MinecraftServer server = source.getMinecraftServer();
         Entity sender = source.getEntity();
 
-        if( server.isSinglePlayer() && sender instanceof EntityPlayer &&
-            ((EntityPlayer) sender).getGameProfile().getName().equalsIgnoreCase( server.getServerModName() ) )
+        if( server.isSinglePlayer() && sender instanceof PlayerEntity &&
+            ((PlayerEntity) sender).getGameProfile().getName().equalsIgnoreCase( server.getUserName() ) )
         {
             if( this == OWNER || this == OWNER_OP ) return true;
         }
diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java b/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java
index ad8837024f..57505ca0f3 100644
--- a/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java
+++ b/src/main/java/dan200/computercraft/shared/command/arguments/ArgumentSerializers.java
@@ -8,34 +8,34 @@
 
 import com.mojang.brigadier.arguments.ArgumentType;
 import dan200.computercraft.ComputerCraft;
-import net.minecraft.command.arguments.ArgumentSerializer;
 import net.minecraft.command.arguments.ArgumentTypes;
-import net.minecraft.command.arguments.IArgumentSerializer;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.command.arguments.serialize.ArgumentSerializer;
+import net.minecraft.command.arguments.serialize.ConstantArgumentSerializer;
+import net.minecraft.util.Identifier;
 
 public final class ArgumentSerializers
 {
     @SuppressWarnings( "unchecked" )
-    private static <T extends ArgumentType<?>> void registerUnsafe( ResourceLocation id, Class<T> type, IArgumentSerializer<?> serializer )
+    private static <T extends ArgumentType<?>> void registerUnsafe( Identifier id, Class<T> type, ArgumentSerializer<?> serializer )
     {
-        ArgumentTypes.register( id, type, (IArgumentSerializer<T>) serializer );
+        ArgumentTypes.register( id.toString(), type, (ArgumentSerializer<T>) serializer );
     }
 
-    private static <T extends ArgumentType<?>> void register( ResourceLocation id, Class<T> type, IArgumentSerializer<T> serializer )
+    private static <T extends ArgumentType<?>> void register( Identifier id, Class<T> type, ArgumentSerializer<T> serializer )
     {
-        ArgumentTypes.register( id, type, serializer );
+        ArgumentTypes.register( id.toString(), type, serializer );
     }
 
-    private static <T extends ArgumentType<?>> void register( ResourceLocation id, T instance )
+    private static <T extends ArgumentType<?>> void register( Identifier id, T instance )
     {
-        registerUnsafe( id, instance.getClass(), new ArgumentSerializer<>( () -> instance ) );
+        registerUnsafe( id, instance.getClass(), new ConstantArgumentSerializer<>( () -> instance ) );
     }
 
     public static void register()
     {
-        register( new ResourceLocation( ComputerCraft.MOD_ID, "tracking_field" ), TrackingFieldArgumentType.trackingField() );
-        register( new ResourceLocation( ComputerCraft.MOD_ID, "computer" ), ComputerArgumentType.oneComputer() );
-        register( new ResourceLocation( ComputerCraft.MOD_ID, "computers" ), ComputersArgumentType.class, new ComputersArgumentType.Serializer() );
-        registerUnsafe( new ResourceLocation( ComputerCraft.MOD_ID, "repeat" ), RepeatArgumentType.class, new RepeatArgumentType.Serializer() );
+        register( new Identifier( ComputerCraft.MOD_ID, "tracking_field" ), TrackingFieldArgumentType.trackingField() );
+        register( new Identifier( ComputerCraft.MOD_ID, "computer" ), ComputerArgumentType.oneComputer() );
+        register( new Identifier( ComputerCraft.MOD_ID, "computers" ), ComputersArgumentType.class, new ComputersArgumentType.Serializer() );
+        registerUnsafe( new Identifier( ComputerCraft.MOD_ID, "repeat" ), RepeatArgumentType.class, new RepeatArgumentType.Serializer() );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java
index cd07d381b6..24d3eb1e53 100644
--- a/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java
+++ b/src/main/java/dan200/computercraft/shared/command/arguments/ComputerArgumentType.java
@@ -14,7 +14,7 @@
 import com.mojang.brigadier.suggestion.SuggestionsBuilder;
 import dan200.computercraft.shared.command.arguments.ComputersArgumentType.ComputersSupplier;
 import dan200.computercraft.shared.computer.core.ServerComputer;
-import net.minecraft.command.CommandSource;
+import net.minecraft.server.command.ServerCommandSource;
 
 import java.util.Collection;
 import java.util.concurrent.CompletableFuture;
@@ -30,7 +30,7 @@ public static ComputerArgumentType oneComputer()
         return INSTANCE;
     }
 
-    public static ServerComputer getComputerArgument( CommandContext<CommandSource> context, String name ) throws CommandSyntaxException
+    public static ServerComputer getComputerArgument( CommandContext<ServerCommandSource> context, String name ) throws CommandSyntaxException
     {
         return context.getArgument( name, ComputerSupplier.class ).unwrap( context.getSource() );
     }
@@ -89,6 +89,6 @@ public Collection<String> getExamples()
     @FunctionalInterface
     public interface ComputerSupplier
     {
-        ServerComputer unwrap( CommandSource source ) throws CommandSyntaxException;
+        ServerComputer unwrap( ServerCommandSource source ) throws CommandSyntaxException;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java
index a89f7a74c3..e72618d192 100644
--- a/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java
+++ b/src/main/java/dan200/computercraft/shared/command/arguments/ComputersArgumentType.java
@@ -16,9 +16,9 @@
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.core.ServerComputer;
-import net.minecraft.command.CommandSource;
-import net.minecraft.command.arguments.IArgumentSerializer;
-import net.minecraft.network.PacketBuffer;
+import net.minecraft.command.arguments.serialize.ArgumentSerializer;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 import java.util.*;
@@ -50,7 +50,7 @@ public static ComputersArgumentType someComputers()
         return SOME;
     }
 
-    public static Collection<ServerComputer> getComputersArgument( CommandContext<CommandSource> context, String name ) throws CommandSyntaxException
+    public static Collection<ServerComputer> getComputersArgument( CommandContext<ServerCommandSource> context, String name ) throws CommandSyntaxException
     {
         return context.getArgument( name, ComputersSupplier.class ).unwrap( context.getSource() );
     }
@@ -172,24 +172,24 @@ private static ComputersSupplier getComputers( Predicate<ServerComputer> predica
         );
     }
 
-    public static class Serializer implements IArgumentSerializer<ComputersArgumentType>
+    public static class Serializer implements ArgumentSerializer<ComputersArgumentType>
     {
 
         @Override
-        public void write( @Nonnull ComputersArgumentType arg, @Nonnull PacketBuffer buf )
+        public void toPacket( @Nonnull ComputersArgumentType arg, @Nonnull PacketByteBuf buf )
         {
             buf.writeBoolean( arg.requireSome );
         }
 
         @Nonnull
         @Override
-        public ComputersArgumentType read( @Nonnull PacketBuffer buf )
+        public ComputersArgumentType fromPacket( @Nonnull PacketByteBuf buf )
         {
             return buf.readBoolean() ? SOME : MANY;
         }
 
         @Override
-        public void write( @Nonnull ComputersArgumentType arg, @Nonnull JsonObject json )
+        public void toJson( @Nonnull ComputersArgumentType arg, @Nonnull JsonObject json )
         {
             json.addProperty( "requireSome", arg.requireSome );
         }
@@ -198,10 +198,10 @@ public void write( @Nonnull ComputersArgumentType arg, @Nonnull JsonObject json
     @FunctionalInterface
     public interface ComputersSupplier
     {
-        Collection<ServerComputer> unwrap( CommandSource source ) throws CommandSyntaxException;
+        Collection<ServerComputer> unwrap( ServerCommandSource source ) throws CommandSyntaxException;
     }
 
-    public static Set<ServerComputer> unwrap( CommandSource source, Collection<ComputersSupplier> suppliers ) throws CommandSyntaxException
+    public static Set<ServerComputer> unwrap( ServerCommandSource source, Collection<ComputersSupplier> suppliers ) throws CommandSyntaxException
     {
         Set<ServerComputer> computers = new HashSet<>();
         for( ComputersSupplier supplier : suppliers ) computers.addAll( supplier.unwrap( source ) );
diff --git a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java
index 6311ffd076..14e0495b53 100644
--- a/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java
+++ b/src/main/java/dan200/computercraft/shared/command/arguments/RepeatArgumentType.java
@@ -16,10 +16,10 @@
 import com.mojang.brigadier.suggestion.Suggestions;
 import com.mojang.brigadier.suggestion.SuggestionsBuilder;
 import net.minecraft.command.arguments.ArgumentTypes;
-import net.minecraft.command.arguments.IArgumentSerializer;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
+import net.minecraft.command.arguments.serialize.ArgumentSerializer;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 import java.util.ArrayList;
@@ -126,41 +126,41 @@ public Collection<String> getExamples()
         return child.getExamples();
     }
 
-    public static class Serializer implements IArgumentSerializer<RepeatArgumentType<?, ?>>
+    public static class Serializer implements ArgumentSerializer<RepeatArgumentType<?, ?>>
     {
         @Override
-        public void write( @Nonnull RepeatArgumentType<?, ?> arg, @Nonnull PacketBuffer buf )
+        public void toPacket( RepeatArgumentType<?, ?> arg, PacketByteBuf buf )
         {
             buf.writeBoolean( arg.flatten );
-            ArgumentTypes.serialize( buf, arg.child );
+            ArgumentTypes.toPacket( buf, arg.child );
             buf.writeTextComponent( getMessage( arg ) );
         }
 
         @Nonnull
         @Override
         @SuppressWarnings( { "unchecked", "rawtypes" } )
-        public RepeatArgumentType<?, ?> read( @Nonnull PacketBuffer buf )
+        public RepeatArgumentType<?, ?> fromPacket( @Nonnull PacketByteBuf buf )
         {
             boolean isList = buf.readBoolean();
-            ArgumentType<?> child = ArgumentTypes.deserialize( buf );
-            ITextComponent message = buf.readTextComponent();
+            ArgumentType<?> child = ArgumentTypes.fromPacket( buf );
+            TextComponent message = buf.readTextComponent();
             BiConsumer<List<Object>, ?> appender = isList ? ( list, x ) -> list.addAll( (Collection) x ) : List::add;
             return new RepeatArgumentType( child, appender, isList, new SimpleCommandExceptionType( message ) );
         }
 
         @Override
-        public void write( @Nonnull RepeatArgumentType<?, ?> arg, @Nonnull JsonObject json )
+        public void toJson( @Nonnull RepeatArgumentType<?, ?> arg, @Nonnull JsonObject json )
         {
             json.addProperty( "flatten", arg.flatten );
             json.addProperty( "child", "<<cannot serialize>>" ); // TODO: Potentially serialize this using reflection.
-            json.addProperty( "error", ITextComponent.Serializer.toJson( getMessage( arg ) ) );
+            json.addProperty( "error", TextComponent.Serializer.toJsonString( getMessage( arg ) ) );
         }
 
-        private static ITextComponent getMessage( RepeatArgumentType<?, ?> arg )
+        private static TextComponent getMessage( RepeatArgumentType<?, ?> arg )
         {
             Message message = arg.some.create().getRawMessage();
-            if( message instanceof ITextComponent ) return (ITextComponent) message;
-            return new TextComponentString( message.getString() );
+            if( message instanceof TextComponent ) return (TextComponent) message;
+            return new StringTextComponent( message.getString() );
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java b/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java
index bb1e607ab8..9cdcfc4bbb 100644
--- a/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java
+++ b/src/main/java/dan200/computercraft/shared/command/builder/CommandBuilder.java
@@ -13,7 +13,7 @@
 import com.mojang.brigadier.context.CommandContext;
 import com.mojang.brigadier.tree.CommandNode;
 import dan200.computercraft.shared.command.arguments.RepeatArgumentType;
-import net.minecraft.command.CommandSource;
+import net.minecraft.server.command.ServerCommandSource;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -33,14 +33,14 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>>
     private List<ArgumentBuilder<S, ?>> args = new ArrayList<>();
     private Predicate<S> requires;
 
-    public static CommandBuilder<CommandSource> args()
+    public static CommandBuilder<ServerCommandSource> args()
     {
         return new CommandBuilder<>();
     }
 
-    public static CommandBuilder<CommandSource> command( String literal )
+    public static CommandBuilder<ServerCommandSource> command( String literal )
     {
-        CommandBuilder<CommandSource> builder = new CommandBuilder<>();
+        CommandBuilder<ServerCommandSource> builder = new CommandBuilder<>();
         builder.args.add( literal( literal ) );
         return builder;
     }
diff --git a/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java b/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java
index 6fa6854439..6b8b4f22db 100644
--- a/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java
+++ b/src/main/java/dan200/computercraft/shared/command/builder/HelpingArgumentBuilder.java
@@ -13,11 +13,11 @@
 import com.mojang.brigadier.context.CommandContext;
 import com.mojang.brigadier.tree.CommandNode;
 import com.mojang.brigadier.tree.LiteralCommandNode;
-import net.minecraft.command.CommandSource;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextFormatting;
-import net.minecraft.util.text.event.ClickEvent;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TextFormat;
+import net.minecraft.text.event.ClickEvent;
 
 import javax.annotation.Nonnull;
 import java.util.ArrayList;
@@ -30,7 +30,7 @@
  * An alternative to {@link LiteralArgumentBuilder} which also provides a {@code /... help} command, and defaults
  * to that command when no arguments are given.
  */
-public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<CommandSource>
+public final class HelpingArgumentBuilder extends LiteralArgumentBuilder<ServerCommandSource>
 {
     private final Collection<HelpingArgumentBuilder> children = new ArrayList<>();
 
@@ -45,13 +45,13 @@ public static HelpingArgumentBuilder choice( String literal )
     }
 
     @Override
-    public LiteralArgumentBuilder<CommandSource> executes( final Command<CommandSource> command )
+    public LiteralArgumentBuilder<ServerCommandSource> executes( final Command<ServerCommandSource> command )
     {
         throw new IllegalStateException( "Cannot use executes on a HelpingArgumentBuilder" );
     }
 
     @Override
-    public LiteralArgumentBuilder<CommandSource> then( final ArgumentBuilder<CommandSource, ?> argument )
+    public LiteralArgumentBuilder<ServerCommandSource> then( final ArgumentBuilder<ServerCommandSource, ?> argument )
     {
         if( getRedirect() != null ) throw new IllegalStateException( "Cannot add children to a redirected node" );
 
@@ -72,7 +72,7 @@ else if( argument instanceof LiteralArgumentBuilder )
     }
 
     @Override
-    public LiteralArgumentBuilder<CommandSource> then( CommandNode<CommandSource> argument )
+    public LiteralArgumentBuilder<ServerCommandSource> then( CommandNode<ServerCommandSource> argument )
     {
         if( !(argument instanceof LiteralCommandNode) )
         {
@@ -82,33 +82,33 @@ public LiteralArgumentBuilder<CommandSource> then( CommandNode<CommandSource> ar
     }
 
     @Override
-    public LiteralCommandNode<CommandSource> build()
+    public LiteralCommandNode<ServerCommandSource> build()
     {
         return buildImpl( getLiteral().replace( '-', '_' ), getLiteral() );
     }
 
-    private LiteralCommandNode<CommandSource> build( @Nonnull String id, @Nonnull String command )
+    private LiteralCommandNode<ServerCommandSource> build( @Nonnull String id, @Nonnull String command )
     {
         return buildImpl( id + "." + getLiteral().replace( '-', '_' ), command + " " + getLiteral() );
     }
 
-    private LiteralCommandNode<CommandSource> buildImpl( String id, String command )
+    private LiteralCommandNode<ServerCommandSource> buildImpl( String id, String command )
     {
         HelpCommand helpCommand = new HelpCommand( id, command );
-        LiteralCommandNode<CommandSource> node = new LiteralCommandNode<>( getLiteral(), helpCommand, getRequirement(), getRedirect(), getRedirectModifier(), isFork() );
+        LiteralCommandNode<ServerCommandSource> node = new LiteralCommandNode<>( getLiteral(), helpCommand, getRequirement(), getRedirect(), getRedirectModifier(), isFork() );
         helpCommand.node = node;
 
         // Set up a /... help command
-        LiteralArgumentBuilder<CommandSource> helpNode = LiteralArgumentBuilder.<CommandSource>literal( "help" )
+        LiteralArgumentBuilder<ServerCommandSource> helpNode = LiteralArgumentBuilder.<ServerCommandSource>literal( "help" )
             .requires( x -> getArguments().stream().anyMatch( y -> y.getRequirement().test( x ) ) )
             .executes( helpCommand );
 
         // Add all normal command children to this and the help node
-        for( CommandNode<CommandSource> child : getArguments() )
+        for( CommandNode<ServerCommandSource> child : getArguments() )
         {
             node.addChild( child );
 
-            helpNode.then( LiteralArgumentBuilder.<CommandSource>literal( child.getName() )
+            helpNode.then( LiteralArgumentBuilder.<ServerCommandSource>literal( child.getName() )
                 .requires( child.getRequirement() )
                 .executes( helpForChild( child, id, command ) )
                 .build()
@@ -118,9 +118,9 @@ private LiteralCommandNode<CommandSource> buildImpl( String id, String command )
         // And add alternative versions of which forward instead
         for( HelpingArgumentBuilder childBuilder : children )
         {
-            LiteralCommandNode<CommandSource> child = childBuilder.build( id, command );
+            LiteralCommandNode<ServerCommandSource> child = childBuilder.build( id, command );
             node.addChild( child );
-            helpNode.then( LiteralArgumentBuilder.<CommandSource>literal( child.getName() )
+            helpNode.then( LiteralArgumentBuilder.<ServerCommandSource>literal( child.getName() )
                 .requires( child.getRequirement() )
                 .executes( helpForChild( child, id, command ) )
                 .redirect( child.getChild( "help" ) )
@@ -133,15 +133,15 @@ private LiteralCommandNode<CommandSource> buildImpl( String id, String command )
         return node;
     }
 
-    private static final TextFormatting HEADER = TextFormatting.LIGHT_PURPLE;
-    private static final TextFormatting SYNOPSIS = TextFormatting.AQUA;
-    private static final TextFormatting NAME = TextFormatting.GREEN;
+    private static final TextFormat HEADER = TextFormat.LIGHT_PURPLE;
+    private static final TextFormat SYNOPSIS = TextFormat.AQUA;
+    private static final TextFormat NAME = TextFormat.GREEN;
 
-    private static final class HelpCommand implements Command<CommandSource>
+    private static final class HelpCommand implements Command<ServerCommandSource>
     {
         private final String id;
         private final String command;
-        LiteralCommandNode<CommandSource> node;
+        LiteralCommandNode<ServerCommandSource> node;
 
         private HelpCommand( String id, String command )
         {
@@ -150,14 +150,14 @@ private HelpCommand( String id, String command )
         }
 
         @Override
-        public int run( CommandContext<CommandSource> context )
+        public int run( CommandContext<ServerCommandSource> context )
         {
             context.getSource().sendFeedback( getHelp( context, node, id, command ), false );
             return 0;
         }
     }
 
-    private static Command<CommandSource> helpForChild( CommandNode<CommandSource> node, String id, String command )
+    private static Command<ServerCommandSource> helpForChild( CommandNode<ServerCommandSource> node, String id, String command )
     {
         return context -> {
             context.getSource().sendFeedback( getHelp( context, node, id + "." + node.getName().replace( '-', '_' ), command + " " + node.getName() ), false );
@@ -165,39 +165,39 @@ private static Command<CommandSource> helpForChild( CommandNode<CommandSource> n
         };
     }
 
-    private static ITextComponent getHelp( CommandContext<CommandSource> context, CommandNode<CommandSource> node, String id, String command )
+    private static TextComponent getHelp( CommandContext<ServerCommandSource> context, CommandNode<ServerCommandSource> node, String id, String command )
     {
         // An ugly hack to extract usage information from the dispatcher. We generate a temporary node, generate
         // the shorthand usage, and emit that.
-        CommandDispatcher<CommandSource> dispatcher = context.getSource().getServer().getCommandManager().getDispatcher();
-        CommandNode<CommandSource> temp = new LiteralCommandNode<>( "_", null, x -> true, null, null, false );
+        CommandDispatcher<ServerCommandSource> dispatcher = context.getSource().getMinecraftServer().getCommandManager().getDispatcher();
+        CommandNode<ServerCommandSource> temp = new LiteralCommandNode<>( "_", null, x -> true, null, null, false );
         temp.addChild( node );
         String usage = dispatcher.getSmartUsage( temp, context.getSource() ).get( node ).substring( node.getName().length() );
 
-        ITextComponent output = new TextComponentString( "" )
-            .appendSibling( coloured( "/" + command + usage, HEADER ) )
-            .appendText( " " )
-            .appendSibling( coloured( translate( "commands." + id + ".synopsis" ), SYNOPSIS ) )
-            .appendText( "\n" )
-            .appendSibling( translate( "commands." + id + ".desc" ) );
+        TextComponent output = new StringTextComponent( "" )
+            .append( coloured( "/" + command + usage, HEADER ) )
+            .append( " " )
+            .append( coloured( translate( "commands." + id + ".synopsis" ), SYNOPSIS ) )
+            .append( "\n" )
+            .append( translate( "commands." + id + ".desc" ) );
 
-        for( CommandNode<CommandSource> child : node.getChildren() )
+        for( CommandNode<ServerCommandSource> child : node.getChildren() )
         {
             if( !child.getRequirement().test( context.getSource() ) || !(child instanceof LiteralCommandNode) )
             {
                 continue;
             }
 
-            output.appendText( "\n" );
+            output.append( "\n" );
 
-            ITextComponent component = coloured( child.getName(), NAME );
+            TextComponent component = coloured( child.getName(), NAME );
             component.getStyle().setClickEvent( new ClickEvent(
                 ClickEvent.Action.SUGGEST_COMMAND,
                 "/" + command + " " + child.getName()
             ) );
-            output.appendSibling( component );
+            output.append( component );
 
-            output.appendText( " - " ).appendSibling( translate( "commands." + id + "." + child.getName() + ".synopsis" ) );
+            output.append( " - " ).append( translate( "commands." + id + "." + child.getName() + ".synopsis" ) );
         }
 
         return output;
diff --git a/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java b/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java
index a3119a9921..eb06ff3c5c 100644
--- a/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java
+++ b/src/main/java/dan200/computercraft/shared/command/text/ChatHelpers.java
@@ -6,83 +6,83 @@
 
 package dan200.computercraft.shared.command.text;
 
+import net.minecraft.text.*;
+import net.minecraft.text.event.ClickEvent;
+import net.minecraft.text.event.HoverEvent;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.*;
-import net.minecraft.util.text.event.ClickEvent;
-import net.minecraft.util.text.event.HoverEvent;
 
 /**
  * Various helpers for building chat messages
  */
 public final class ChatHelpers
 {
-    private static final TextFormatting HEADER = TextFormatting.LIGHT_PURPLE;
+    private static final TextFormat HEADER = TextFormat.LIGHT_PURPLE;
 
     private ChatHelpers() {}
 
-    public static ITextComponent coloured( String text, TextFormatting colour )
+    public static TextComponent coloured( String text, TextFormat colour )
     {
-        ITextComponent component = new TextComponentString( text == null ? "" : text );
+        TextComponent component = new StringTextComponent( text == null ? "" : text );
         component.getStyle().setColor( colour );
         return component;
     }
 
-    public static <T extends ITextComponent> T coloured( T component, TextFormatting colour )
+    public static <T extends TextComponent> T coloured( T component, TextFormat colour )
     {
         component.getStyle().setColor( colour );
         return component;
     }
 
-    public static ITextComponent text( String text )
+    public static TextComponent text( String text )
     {
-        return new TextComponentString( text == null ? "" : text );
+        return new StringTextComponent( text == null ? "" : text );
     }
 
-    public static ITextComponent translate( String text )
+    public static TextComponent translate( String text )
     {
-        return new TextComponentTranslation( text == null ? "" : text );
+        return new TranslatableTextComponent( text == null ? "" : text );
     }
 
-    public static ITextComponent translate( String text, Object... args )
+    public static TextComponent translate( String text, Object... args )
     {
-        return new TextComponentTranslation( text == null ? "" : text, args );
+        return new TranslatableTextComponent( text == null ? "" : text, args );
     }
 
-    public static ITextComponent list( ITextComponent... children )
+    public static TextComponent list( TextComponent... children )
     {
-        ITextComponent component = new TextComponentString( "" );
-        for( ITextComponent child : children )
+        TextComponent component = new StringTextComponent( "" );
+        for( TextComponent child : children )
         {
-            component.appendSibling( child );
+            component.append( child );
         }
         return component;
     }
 
-    public static ITextComponent position( BlockPos pos )
+    public static TextComponent position( BlockPos pos )
     {
         if( pos == null ) return translate( "commands.computercraft.generic.no_position" );
         return translate( "commands.computercraft.generic.position", pos.getX(), pos.getY(), pos.getZ() );
     }
 
-    public static ITextComponent bool( boolean value )
+    public static TextComponent bool( boolean value )
     {
         return value
-            ? coloured( translate( "commands.computercraft.generic.yes" ), TextFormatting.GREEN )
-            : coloured( translate( "commands.computercraft.generic.no" ), TextFormatting.RED );
+            ? coloured( translate( "commands.computercraft.generic.yes" ), TextFormat.GREEN )
+            : coloured( translate( "commands.computercraft.generic.no" ), TextFormat.RED );
     }
 
-    public static ITextComponent link( ITextComponent component, String command, ITextComponent toolTip )
+    public static TextComponent link( TextComponent component, String command, TextComponent toolTip )
     {
         Style style = component.getStyle();
 
-        if( style.getColor() == null ) style.setColor( TextFormatting.YELLOW );
+        if( style.getColor() == null ) style.setColor( TextFormat.YELLOW );
         style.setClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, command ) );
         style.setHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_TEXT, toolTip ) );
 
         return component;
     }
 
-    public static ITextComponent header( String text )
+    public static TextComponent header( String text )
     {
         return coloured( text, HEADER );
     }
diff --git a/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java b/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java
index 41eeb60297..ed2fa61b17 100644
--- a/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java
+++ b/src/main/java/dan200/computercraft/shared/command/text/ServerTableFormatter.java
@@ -6,29 +6,29 @@
 
 package dan200.computercraft.shared.command.text;
 
-import net.minecraft.command.CommandSource;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
 import org.apache.commons.lang3.StringUtils;
 
 import javax.annotation.Nullable;
 
 public class ServerTableFormatter implements TableFormatter
 {
-    private final CommandSource source;
+    private final ServerCommandSource source;
 
-    public ServerTableFormatter( CommandSource source )
+    public ServerTableFormatter( ServerCommandSource source )
     {
         this.source = source;
     }
 
     @Override
-    @Nullable
-    public ITextComponent getPadding( ITextComponent component, int width )
+    public @Nullable
+    TextComponent getPadding( TextComponent component, int width )
     {
         int extraWidth = width - getWidth( component );
         if( extraWidth <= 0 ) return null;
-        return new TextComponentString( StringUtils.repeat( ' ', extraWidth ) );
+        return new StringTextComponent( StringUtils.repeat( ' ', extraWidth ) );
     }
 
     @Override
@@ -38,13 +38,13 @@ public int getColumnPadding()
     }
 
     @Override
-    public int getWidth( ITextComponent component )
+    public int getWidth( TextComponent component )
     {
-        return component.getString().length();
+        return component.getText().length();
     }
 
     @Override
-    public void writeLine( int id, ITextComponent component )
+    public void writeLine( int id, TextComponent component )
     {
         source.sendFeedback( component, false );
     }
diff --git a/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java b/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java
index 13e08d9c25..7cf2ea170a 100644
--- a/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java
+++ b/src/main/java/dan200/computercraft/shared/command/text/TableBuilder.java
@@ -9,9 +9,9 @@
 import dan200.computercraft.shared.command.CommandUtils;
 import dan200.computercraft.shared.network.NetworkHandler;
 import dan200.computercraft.shared.network.client.ChatTableClientMessage;
-import net.minecraft.command.CommandSource;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.util.text.ITextComponent;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.text.TextComponent;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -22,11 +22,11 @@ public class TableBuilder
 {
     private final int id;
     private int columns = -1;
-    private final ITextComponent[] headers;
-    private final ArrayList<ITextComponent[]> rows = new ArrayList<>();
+    private final TextComponent[] headers;
+    private final ArrayList<TextComponent[]> rows = new ArrayList<>();
     private int additional;
 
-    public TableBuilder( int id, @Nonnull ITextComponent... headers )
+    public TableBuilder( int id, @Nonnull TextComponent... headers )
     {
         if( id < 0 ) throw new IllegalArgumentException( "ID must be positive" );
         this.id = id;
@@ -45,13 +45,13 @@ public TableBuilder( int id, @Nonnull String... headers )
     {
         if( id < 0 ) throw new IllegalArgumentException( "ID must be positive" );
         this.id = id;
-        this.headers = new ITextComponent[headers.length];
+        this.headers = new TextComponent[headers.length];
         columns = headers.length;
 
         for( int i = 0; i < headers.length; i++ ) this.headers[i] = ChatHelpers.header( headers[i] );
     }
 
-    public void row( @Nonnull ITextComponent... row )
+    public void row( @Nonnull TextComponent... row )
     {
         if( columns == -1 ) columns = row.length;
         if( row.length != columns ) throw new IllegalArgumentException( "Row is the incorrect length" );
@@ -85,13 +85,13 @@ public int getColumns()
     }
 
     @Nullable
-    public ITextComponent[] getHeaders()
+    public TextComponent[] getHeaders()
     {
         return headers;
     }
 
     @Nonnull
-    public List<ITextComponent[]> getRows()
+    public List<TextComponent[]> getRows()
     {
         return rows;
     }
@@ -120,12 +120,12 @@ public void trim( int height )
         }
     }
 
-    public void display( CommandSource source )
+    public void display( ServerCommandSource source )
     {
         if( CommandUtils.isPlayer( source ) )
         {
             trim( 18 );
-            NetworkHandler.sendToPlayer( (EntityPlayerMP) source.getEntity(), new ChatTableClientMessage( this ) );
+            NetworkHandler.sendToPlayer( (ServerPlayerEntity) source.getEntity(), new ChatTableClientMessage( this ) );
         }
         else
         {
diff --git a/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java b/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java
index 0b3ec2322c..408d0ce6ec 100644
--- a/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java
+++ b/src/main/java/dan200/computercraft/shared/command/text/TableFormatter.java
@@ -6,9 +6,9 @@
 
 package dan200.computercraft.shared.command.text;
 
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextFormatting;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TextFormat;
 import org.apache.commons.lang3.StringUtils;
 
 import javax.annotation.Nullable;
@@ -18,8 +18,8 @@
 
 public interface TableFormatter
 {
-    ITextComponent SEPARATOR = coloured( "| ", TextFormatting.GRAY );
-    ITextComponent HEADER = coloured( "=", TextFormatting.GRAY );
+    TextComponent SEPARATOR = coloured( "| ", TextFormat.GRAY );
+    TextComponent HEADER = coloured( "=", TextFormat.GRAY );
 
     /**
      * Get additional padding for the component
@@ -29,7 +29,7 @@ public interface TableFormatter
      * @return The padding for this component, or {@code null} if none is needed.
      */
     @Nullable
-    ITextComponent getPadding( ITextComponent component, int width );
+    TextComponent getPadding( TextComponent component, int width );
 
     /**
      * Get the minimum padding between each column
@@ -38,9 +38,9 @@ public interface TableFormatter
      */
     int getColumnPadding();
 
-    int getWidth( ITextComponent component );
+    int getWidth( TextComponent component );
 
-    void writeLine( int id, ITextComponent component );
+    void writeLine( int id, TextComponent component );
 
     default int display( TableBuilder table )
     {
@@ -50,13 +50,13 @@ default int display( TableBuilder table )
         int columns = table.getColumns();
         int[] maxWidths = new int[columns];
 
-        ITextComponent[] headers = table.getHeaders();
+        TextComponent[] headers = table.getHeaders();
         if( headers != null )
         {
             for( int i = 0; i < columns; i++ ) maxWidths[i] = getWidth( headers[i] );
         }
 
-        for( ITextComponent[] row : table.getRows() )
+        for( TextComponent[] row : table.getRows() )
         {
             for( int i = 0; i < row.length; i++ )
             {
@@ -77,15 +77,15 @@ default int display( TableBuilder table )
 
         if( headers != null )
         {
-            TextComponentString line = new TextComponentString( "" );
+            StringTextComponent line = new StringTextComponent( "" );
             for( int i = 0; i < columns - 1; i++ )
             {
-                line.appendSibling( headers[i] );
-                ITextComponent padding = getPadding( headers[i], maxWidths[i] );
-                if( padding != null ) line.appendSibling( padding );
-                line.appendSibling( SEPARATOR );
+                line.append( headers[i] );
+                TextComponent padding = getPadding( headers[i], maxWidths[i] );
+                if( padding != null ) line.append( padding );
+                line.append( SEPARATOR );
             }
-            line.appendSibling( headers[columns - 1] );
+            line.append( headers[columns - 1] );
 
             writeLine( rowId++, line );
 
@@ -93,26 +93,26 @@ default int display( TableBuilder table )
             // it a tad prettier.
             int rowCharWidth = getWidth( HEADER );
             int rowWidth = totalWidth / rowCharWidth + (totalWidth % rowCharWidth == 0 ? 0 : 1);
-            writeLine( rowId++, coloured( StringUtils.repeat( HEADER.getString(), rowWidth ), TextFormatting.GRAY ) );
+            writeLine( rowId++, coloured( StringUtils.repeat( HEADER.getText(), rowWidth ), TextFormat.GRAY ) );
         }
 
-        for( ITextComponent[] row : table.getRows() )
+        for( TextComponent[] row : table.getRows() )
         {
-            TextComponentString line = new TextComponentString( "" );
+            StringTextComponent line = new StringTextComponent( "" );
             for( int i = 0; i < columns - 1; i++ )
             {
-                line.appendSibling( row[i] );
-                ITextComponent padding = getPadding( row[i], maxWidths[i] );
-                if( padding != null ) line.appendSibling( padding );
-                line.appendSibling( SEPARATOR );
+                line.append( row[i] );
+                TextComponent padding = getPadding( row[i], maxWidths[i] );
+                if( padding != null ) line.append( padding );
+                line.append( SEPARATOR );
             }
-            line.appendSibling( row[columns - 1] );
+            line.append( row[columns - 1] );
             writeLine( rowId++, line );
         }
 
         if( table.getAdditional() > 0 )
         {
-            writeLine( rowId++, coloured( translate( "commands.computercraft.generic.additional_rows", table.getAdditional() ), TextFormatting.AQUA ) );
+            writeLine( rowId++, coloured( translate( "commands.computercraft.generic.additional_rows", table.getAdditional() ), TextFormat.AQUA ) );
         }
 
         return rowId - table.getId();
diff --git a/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java b/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java
index a7a9420dfd..fc02a69e40 100644
--- a/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java
+++ b/src/main/java/dan200/computercraft/shared/common/BlockGeneric.java
@@ -7,27 +7,26 @@
 package dan200.computercraft.shared.common;
 
 import net.minecraft.block.Block;
-import net.minecraft.block.ITileEntityProvider;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
+import net.minecraft.block.BlockEntityProvider;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockReader;
-import net.minecraft.world.IWorldReader;
+import net.minecraft.world.BlockView;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.util.Random;
 
-public abstract class BlockGeneric extends Block implements ITileEntityProvider
+public abstract class BlockGeneric extends Block implements BlockEntityProvider
 {
-    private final TileEntityType<? extends TileGeneric> type;
+    private final BlockEntityType<? extends TileGeneric> type;
 
-    public BlockGeneric( Properties settings, TileEntityType<? extends TileGeneric> type )
+    public BlockGeneric( Settings settings, BlockEntityType<? extends TileGeneric> type )
     {
         super( settings );
         this.type = type;
@@ -35,52 +34,45 @@ public BlockGeneric( Properties settings, TileEntityType<? extends TileGeneric>
 
     @Override
     @Deprecated
-    public final void onReplaced( @Nonnull IBlockState block, @Nonnull World world, @Nonnull BlockPos pos, IBlockState replace, boolean bool )
+    public final void onBlockRemoved( @Nonnull BlockState block, @Nonnull World world, @Nonnull BlockPos pos, BlockState replace, boolean bool )
     {
         if( block.getBlock() == replace.getBlock() ) return;
 
-        TileEntity tile = world.getTileEntity( pos );
-        super.onReplaced( block, world, pos, replace, bool );
-        world.removeTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
+        super.onBlockRemoved( block, world, pos, replace, bool );
+        world.removeBlockEntity( pos );
         if( tile instanceof TileGeneric ) ((TileGeneric) tile).destroy();
     }
 
     @Override
     @Deprecated
-    public final boolean onBlockActivated( IBlockState state, World world, BlockPos pos, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public final boolean activate( BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit )
     {
-        TileEntity tile = world.getTileEntity( pos );
-        return tile instanceof TileGeneric && ((TileGeneric) tile).onActivate( player, hand, side, hitX, hitY, hitZ );
+        BlockEntity tile = world.getBlockEntity( pos );
+        return tile instanceof TileGeneric && ((TileGeneric) tile).onActivate( player, hand, hit );
     }
 
     @Override
     @Deprecated
-    @SuppressWarnings( "deprecation" )
-    public final void neighborChanged( IBlockState state, World world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos )
+    public final void neighborUpdate( BlockState state, World world, BlockPos pos, Block neighbourBlock, BlockPos neighbourPos, boolean flag )
     {
-        TileEntity tile = world.getTileEntity( pos );
+        super.neighborUpdate( state, world, pos, neighbourBlock, neighbourPos, flag );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( tile instanceof TileGeneric ) ((TileGeneric) tile).onNeighbourChange( neighbourPos );
     }
 
-    @Override
-    public final void onNeighborChange( IBlockState state, IWorldReader world, BlockPos pos, BlockPos neighbour )
-    {
-        TileEntity tile = world.getTileEntity( pos );
-        if( tile instanceof TileGeneric ) ((TileGeneric) tile).onNeighbourTileEntityChange( neighbour );
-    }
-
     @Override
     @Deprecated
-    public void tick( IBlockState state, World world, BlockPos pos, Random rand )
+    public void onScheduledTick( BlockState state, World world, BlockPos pos, Random rand )
     {
-        TileEntity te = world.getTileEntity( pos );
+        BlockEntity te = world.getBlockEntity( pos );
         if( te instanceof TileGeneric ) ((TileGeneric) te).blockTick();
     }
 
     @Nullable
     @Override
-    public TileEntity createNewTileEntity( @Nonnull IBlockReader world )
+    public BlockEntity createBlockEntity( BlockView blockView )
     {
-        return type.create();
+        return type.instantiate();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java b/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java
index 4e7ad37919..fd13493b53 100644
--- a/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java
+++ b/src/main/java/dan200/computercraft/shared/common/ClientTerminal.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.common;
 
 import dan200.computercraft.core.terminal.Terminal;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 public class ClientTerminal implements ITerminal
 {
@@ -47,12 +47,12 @@ public boolean isColour()
         return m_colour;
     }
 
-    public void readDescription( NBTTagCompound nbt )
+    public void readDescription( CompoundTag nbt )
     {
         m_colour = nbt.getBoolean( "colour" );
-        if( nbt.contains( "terminal" ) )
+        if( nbt.containsKey( "terminal" ) )
         {
-            NBTTagCompound terminal = nbt.getCompound( "terminal" );
+            CompoundTag terminal = nbt.getCompound( "terminal" );
             resizeTerminal( terminal.getInt( "term_width" ), terminal.getInt( "term_height" ) );
             m_terminal.readFromNBT( terminal );
         }
diff --git a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java
index d17d95e251..473ec6e247 100644
--- a/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/common/ColourableRecipe.java
@@ -6,36 +6,35 @@
 
 package dan200.computercraft.shared.common;
 
-import dan200.computercraft.ComputerCraft;
-import dan200.computercraft.shared.util.AbstractRecipe;
 import dan200.computercraft.shared.util.Colour;
 import dan200.computercraft.shared.util.ColourTracker;
 import dan200.computercraft.shared.util.ColourUtils;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.item.EnumDyeColor;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.SpecialRecipeSerializer;
+import net.minecraft.recipe.crafting.SpecialCraftingRecipe;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
-public class ColourableRecipe extends AbstractRecipe
+public class ColourableRecipe extends SpecialCraftingRecipe
 {
-    public ColourableRecipe( ResourceLocation id )
+    public ColourableRecipe( Identifier id )
     {
         super( id );
     }
 
     @Override
-    public boolean matches( @Nonnull IInventory inv, @Nonnull World world )
+    public boolean matches( @Nonnull CraftingInventory inv, @Nonnull World world )
     {
         boolean hasColourable = false;
         boolean hasDye = false;
-        for( int i = 0; i < inv.getSizeInventory(); i++ )
+        for( int i = 0; i < inv.getInvSize(); i++ )
         {
-            ItemStack stack = inv.getStackInSlot( i );
+            ItemStack stack = inv.getInvStack( i );
             if( stack.isEmpty() ) continue;
 
             if( stack.getItem() instanceof IColouredItem )
@@ -58,15 +57,15 @@ else if( ColourUtils.getStackColour( stack ) != null )
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inv )
+    public ItemStack craft( @Nonnull CraftingInventory inv )
     {
         ItemStack colourable = ItemStack.EMPTY;
 
         ColourTracker tracker = new ColourTracker();
 
-        for( int i = 0; i < inv.getSizeInventory(); i++ )
+        for( int i = 0; i < inv.getInvSize(); i++ )
         {
-            ItemStack stack = inv.getStackInSlot( i );
+            ItemStack stack = inv.getInvStack( i );
 
             if( stack.isEmpty() ) continue;
 
@@ -76,7 +75,7 @@ public ItemStack getCraftingResult( @Nonnull IInventory inv )
             }
             else
             {
-                EnumDyeColor dye = ColourUtils.getStackColour( stack );
+                DyeColor dye = ColourUtils.getStackColour( stack );
                 if( dye == null ) continue;
 
                 Colour colour = Colour.fromInt( 15 - dye.getId() );
@@ -89,19 +88,17 @@ public ItemStack getCraftingResult( @Nonnull IInventory inv )
     }
 
     @Override
-    public boolean canFit( int x, int y )
+    public boolean fits( int x, int y )
     {
         return x >= 2 && y >= 2;
     }
 
     @Override
     @Nonnull
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    public static final IRecipeSerializer<?> SERIALIZER = new RecipeSerializers.SimpleSerializer<>(
-        ComputerCraft.MOD_ID + ":colour", ColourableRecipe::new
-    );
+    public static final RecipeSerializer<?> SERIALIZER = new SpecialRecipeSerializer<>( ColourableRecipe::new );
 }
diff --git a/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java b/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java
index af4f469831..4e85344657 100644
--- a/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java
+++ b/src/main/java/dan200/computercraft/shared/common/ContainerHeldItem.java
@@ -7,22 +7,23 @@
 package dan200.computercraft.shared.common;
 
 import dan200.computercraft.shared.util.InventoryUtil;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumHand;
+import net.minecraft.util.Hand;
 
 import javax.annotation.Nonnull;
 
 public class ContainerHeldItem extends Container
 {
     private final ItemStack m_stack;
-    private final EnumHand m_hand;
+    private final Hand m_hand;
 
-    public ContainerHeldItem( EntityPlayer player, EnumHand hand )
+    public ContainerHeldItem( int id, PlayerEntity player, Hand hand )
     {
+        super( null, id );
         m_hand = hand;
-        m_stack = InventoryUtil.copyItem( player.getHeldItem( hand ) );
+        m_stack = InventoryUtil.copyItem( player.getStackInHand( hand ) );
     }
 
     @Nonnull
@@ -32,11 +33,11 @@ public ItemStack getStack()
     }
 
     @Override
-    public boolean canInteractWith( @Nonnull EntityPlayer player )
+    public boolean canUse( @Nonnull PlayerEntity player )
     {
         if( !player.isAlive() ) return false;
 
-        ItemStack stack = player.getHeldItem( m_hand );
+        ItemStack stack = player.getStackInHand( m_hand );
         return stack == m_stack || !stack.isEmpty() && !m_stack.isEmpty() && stack.getItem() == m_stack.getItem();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java b/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java
index 5b0a261482..bae001cb87 100644
--- a/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java
+++ b/src/main/java/dan200/computercraft/shared/common/DefaultBundledRedstoneProvider.java
@@ -8,8 +8,8 @@
 
 import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
 import net.minecraft.block.Block;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -17,12 +17,12 @@
 public class DefaultBundledRedstoneProvider implements IBundledRedstoneProvider
 {
     @Override
-    public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
         return getDefaultBundledRedstoneOutput( world, pos, side );
     }
 
-    public static int getDefaultBundledRedstoneOutput( World world, BlockPos pos, EnumFacing side )
+    public static int getDefaultBundledRedstoneOutput( World world, BlockPos pos, Direction side )
     {
         Block block = world.getBlockState( pos ).getBlock();
         if( block instanceof IBundledRedstoneBlock )
diff --git a/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java b/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java
index d6212dbeda..d227436fc4 100644
--- a/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java
+++ b/src/main/java/dan200/computercraft/shared/common/IBundledRedstoneBlock.java
@@ -6,13 +6,13 @@
 
 package dan200.computercraft.shared.common;
 
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 public interface IBundledRedstoneBlock
 {
-    boolean getBundledRedstoneConnectivity( World world, BlockPos pos, EnumFacing side );
+    boolean getBundledRedstoneConnectivity( World world, BlockPos pos, Direction side );
 
-    int getBundledRedstoneOutput( World world, BlockPos pos, EnumFacing side );
+    int getBundledRedstoneOutput( World world, BlockPos pos, Direction side );
 }
diff --git a/src/main/java/dan200/computercraft/shared/common/IColouredItem.java b/src/main/java/dan200/computercraft/shared/common/IColouredItem.java
index 9fe5ca6cfe..aa3b998b2c 100644
--- a/src/main/java/dan200/computercraft/shared/common/IColouredItem.java
+++ b/src/main/java/dan200/computercraft/shared/common/IColouredItem.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.common;
 
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 public interface IColouredItem
 {
@@ -27,15 +27,15 @@ default ItemStack withColour( ItemStack stack, int colour )
 
     static int getColourBasic( ItemStack stack )
     {
-        NBTTagCompound tag = stack.getTag();
-        return tag != null && tag.contains( NBT_COLOUR ) ? tag.getInt( NBT_COLOUR ) : -1;
+        CompoundTag tag = stack.getTag();
+        return tag != null && tag.containsKey( NBT_COLOUR ) ? tag.getInt( NBT_COLOUR ) : -1;
     }
 
     static void setColourBasic( ItemStack stack, int colour )
     {
         if( colour == -1 )
         {
-            NBTTagCompound tag = stack.getTag();
+            CompoundTag tag = stack.getTag();
             if( tag != null ) tag.remove( NBT_COLOUR );
         }
         else
diff --git a/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java b/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java
index 0898a38446..c40bbbc75f 100644
--- a/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java
+++ b/src/main/java/dan200/computercraft/shared/common/ServerTerminal.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.common;
 
 import dan200.computercraft.core.terminal.Terminal;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -86,12 +86,12 @@ public boolean isColour()
 
     // Networking stuff
 
-    public void writeDescription( NBTTagCompound nbt )
+    public void writeDescription( CompoundTag nbt )
     {
         nbt.putBoolean( "colour", m_colour );
         if( m_terminal != null )
         {
-            NBTTagCompound terminal = new NBTTagCompound();
+            CompoundTag terminal = new CompoundTag();
             terminal.putInt( "term_width", m_terminal.getWidth() );
             terminal.putInt( "term_height", m_terminal.getHeight() );
             m_terminal.writeToNBT( terminal );
diff --git a/src/main/java/dan200/computercraft/shared/common/TileGeneric.java b/src/main/java/dan200/computercraft/shared/common/TileGeneric.java
index 2b01a2be1d..c5d7adac28 100644
--- a/src/main/java/dan200/computercraft/shared/common/TileGeneric.java
+++ b/src/main/java/dan200/computercraft/shared/common/TileGeneric.java
@@ -6,22 +6,21 @@
 
 package dan200.computercraft.shared.common;
 
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.network.NetworkManager;
-import net.minecraft.network.play.server.SPacketUpdateTileEntity;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
+import net.fabricmc.fabric.api.block.entity.BlockEntityClientSerializable;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.Hand;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
 
 import javax.annotation.Nonnull;
 
-public abstract class TileGeneric extends TileEntity
+public abstract class TileGeneric extends BlockEntity implements BlockEntityClientSerializable
 {
-    public TileGeneric( TileEntityType<? extends TileGeneric> type )
+    public TileGeneric( BlockEntityType<? extends TileGeneric> type )
     {
         super( type );
     }
@@ -34,12 +33,12 @@ public final void updateBlock()
     {
         markDirty();
         BlockPos pos = getPos();
-        IBlockState state = getBlockState();
-        getWorld().markBlockRangeForRenderUpdate( pos, pos );
-        getWorld().notifyBlockUpdate( pos, state, state, 3 );
+        BlockState state = getCachedState();
+        getWorld().scheduleBlockRender( pos );
+        getWorld().updateListeners( pos, state, state, 3 );
     }
 
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
         return false;
     }
@@ -56,58 +55,40 @@ protected void blockTick()
     {
     }
 
-    protected double getInteractRange( EntityPlayer player )
+    protected double getInteractRange( PlayerEntity player )
     {
         return 8.0;
     }
 
-    public boolean isUsable( EntityPlayer player, boolean ignoreRange )
+    public boolean isUsable( PlayerEntity player, boolean ignoreRange )
     {
-        if( player == null || !player.isAlive() || getWorld().getTileEntity( getPos() ) != this ) return false;
+        if( player == null || !player.isAlive() || getWorld().getBlockEntity( getPos() ) != this ) return false;
         if( ignoreRange ) return true;
 
         double range = getInteractRange( player );
         BlockPos pos = getPos();
         return player.getEntityWorld() == getWorld() &&
-            player.getDistanceSq( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ) <= range * range;
+            player.squaredDistanceTo( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ) <= range * range;
     }
 
-    protected void writeDescription( @Nonnull NBTTagCompound nbt )
+    protected void writeDescription( @Nonnull CompoundTag nbt )
     {
     }
 
-    protected void readDescription( @Nonnull NBTTagCompound nbt )
+    protected void readDescription( @Nonnull CompoundTag nbt )
     {
     }
 
-    @Nonnull
     @Override
-    public final SPacketUpdateTileEntity getUpdatePacket()
+    public final CompoundTag toClientTag( CompoundTag tag )
     {
-        NBTTagCompound nbt = new NBTTagCompound();
-        writeDescription( nbt );
-        return new SPacketUpdateTileEntity( pos, 0, nbt );
-    }
-
-    @Override
-    public final void onDataPacket( NetworkManager net, SPacketUpdateTileEntity packet )
-    {
-        if( packet.getTileEntityType() == 0 ) readDescription( packet.getNbtCompound() );
-    }
-
-    @Nonnull
-    @Override
-    public NBTTagCompound getUpdateTag()
-    {
-        NBTTagCompound tag = super.getUpdateTag();
         writeDescription( tag );
         return tag;
     }
 
     @Override
-    public void handleUpdateTag( @Nonnull NBTTagCompound tag )
+    public final void fromClientTag( CompoundTag tag )
     {
-        super.handleUpdateTag( tag );
         readDescription( tag );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java
index d14dc2bd44..e0700b0134 100644
--- a/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java
+++ b/src/main/java/dan200/computercraft/shared/computer/apis/CommandAPI.java
@@ -16,16 +16,16 @@
 import dan200.computercraft.shared.computer.blocks.TileCommandComputer;
 import dan200.computercraft.shared.util.NBTUtil;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.command.CommandSource;
-import net.minecraft.command.Commands;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.nbt.CompoundTag;
 import net.minecraft.server.MinecraftServer;
-import net.minecraft.state.IProperty;
-import net.minecraft.tileentity.TileEntity;
+import net.minecraft.server.command.ServerCommandManager;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.state.property.Property;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.registry.Registry;
 import net.minecraft.world.World;
-import net.minecraftforge.registries.ForgeRegistries;
 
 import javax.annotation.Nonnull;
 import java.util.Collections;
@@ -76,17 +76,17 @@ private static Map<Object, Object> createOutput( String output )
     private Object[] doCommand( String command )
     {
         MinecraftServer server = m_computer.getWorld().getServer();
-        if( server == null || !server.isCommandBlockEnabled() )
+        if( server == null || !server.areCommandBlocksEnabled() )
         {
             return new Object[] { false, createOutput( "Command blocks disabled by server" ) };
         }
 
-        Commands commandManager = server.getCommandManager();
+        ServerCommandManager commandManager = server.getCommandManager();
         TileCommandComputer.CommandReceiver receiver = m_computer.getReceiver();
         try
         {
             receiver.clearOutput();
-            int result = commandManager.handleCommand( m_computer.getSource(), command );
+            int result = commandManager.execute( m_computer.getSource(), command );
             return new Object[] { result > 0, receiver.copyOutput() };
         }
         catch( Throwable t )
@@ -99,31 +99,31 @@ private Object[] doCommand( String command )
     private static Object getBlockInfo( World world, BlockPos pos )
     {
         // Get the details of the block
-        IBlockState state = world.getBlockState( pos );
+        BlockState state = world.getBlockState( pos );
         Block block = state.getBlock();
 
         Map<Object, Object> table = new HashMap<>();
-        table.put( "name", ForgeRegistries.BLOCKS.getKey( block ).toString() );
+        table.put( "name", Registry.BLOCK.getId( block ).toString() );
 
         Map<Object, Object> stateTable = new HashMap<>();
-        for( ImmutableMap.Entry<IProperty<?>, Comparable<?>> entry : state.getValues().entrySet() )
+        for( ImmutableMap.Entry<Property<?>, Comparable<?>> entry : state.getEntries().entrySet() )
         {
-            IProperty<?> property = entry.getKey();
+            Property<?> property = entry.getKey();
             stateTable.put( property.getName(), getPropertyValue( property, entry.getValue() ) );
         }
         table.put( "state", stateTable );
 
-        TileEntity tile = world.getTileEntity( pos );
-        if( tile != null ) table.put( "nbt", NBTUtil.toLua( tile.write( new NBTTagCompound() ) ) );
+        BlockEntity tile = world.getBlockEntity( pos );
+        if( tile != null ) table.put( "nbt", NBTUtil.toLua( tile.toTag( new CompoundTag() ) ) );
 
         return table;
     }
 
     @SuppressWarnings( { "unchecked", "rawtypes" } )
-    private static Object getPropertyValue( IProperty property, Comparable value )
+    private static Object getPropertyValue( Property property, Comparable value )
     {
         if( value instanceof String || value instanceof Number || value instanceof Boolean ) return value;
-        return property.getName( value );
+        return property.getValueAsString( value );
     }
 
     @Override
@@ -149,7 +149,7 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
                     MinecraftServer server = m_computer.getWorld().getServer();
 
                     if( server == null ) return new Object[] { Collections.emptyMap() };
-                    CommandNode<CommandSource> node = server.getCommandManager().getDispatcher().getRoot();
+                    CommandNode<ServerCommandSource> node = server.getCommandManager().getDispatcher().getRoot();
                     for( int j = 0; j < arguments.length; j++ )
                     {
                         String name = getString( arguments, j );
diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.java
index 41b68cd8c3..4757f3e537 100644
--- a/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputer.java
@@ -10,15 +10,15 @@
 import dan200.computercraft.shared.computer.core.ComputerState;
 import dan200.computercraft.shared.computer.items.ComputerItemFactory;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.EnumProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.EnumProperty;
+import net.minecraft.state.property.Properties;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -26,28 +26,28 @@
 public class BlockComputer extends BlockComputerBase<TileComputer>
 {
     public static final EnumProperty<ComputerState> STATE = EnumProperty.create( "state", ComputerState.class );
-    public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
+    public static final DirectionProperty FACING = Properties.FACING_HORIZONTAL;
 
-    public BlockComputer( Properties settings, ComputerFamily family, TileEntityType<? extends TileComputer> type )
+    public BlockComputer( Settings settings, ComputerFamily family, BlockEntityType<? extends TileComputer> type )
     {
         super( settings, family, type );
         setDefaultState( getDefaultState()
-            .with( FACING, EnumFacing.NORTH )
+            .with( FACING, Direction.NORTH )
             .with( STATE, ComputerState.OFF )
         );
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> builder )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> builder )
     {
-        builder.add( FACING, STATE );
+        builder.with( FACING, STATE );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext placement )
+    public BlockState getPlacementState( ItemPlacementContext placement )
     {
-        return getDefaultState().with( FACING, placement.getPlacementHorizontalFacing().getOpposite() );
+        return getDefaultState().with( FACING, placement.getPlayerHorizontalFacing().getOpposite() );
     }
 
     @Nonnull
diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java
index ab7e2f48a0..7c215dd062 100644
--- a/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java
+++ b/src/main/java/dan200/computercraft/shared/computer/blocks/BlockComputerBase.java
@@ -6,32 +6,32 @@
 
 package dan200.computercraft.shared.computer.blocks;
 
+import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.common.BlockGeneric;
 import dan200.computercraft.shared.common.IBundledRedstoneBlock;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.computer.items.IComputerItem;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.fluid.IFluidState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.LivingEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.NonNullList;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
-import net.minecraft.world.IBlockReader;
+import net.minecraft.util.math.Direction;
+import net.minecraft.world.BlockView;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
 public abstract class BlockComputerBase<T extends TileComputerBase> extends BlockGeneric implements IBundledRedstoneBlock
 {
+    public static final Identifier COMPUTER_DROP = new Identifier( ComputerCraft.MOD_ID, "computer" );
+
     private final ComputerFamily family;
 
-    protected BlockComputerBase( Properties settings, ComputerFamily family, TileEntityType<? extends T> type )
+    protected BlockComputerBase( Settings settings, ComputerFamily family, BlockEntityType<? extends T> type )
     {
         super( settings, type );
         this.family = family;
@@ -39,35 +39,35 @@ protected BlockComputerBase( Properties settings, ComputerFamily family, TileEnt
 
     @Override
     @Deprecated
-    public void onBlockAdded( IBlockState state, World world, BlockPos pos, IBlockState oldState )
+    public void onBlockAdded( BlockState state, World world, BlockPos pos, BlockState oldState, boolean flag )
     {
-        super.onBlockAdded( state, world, pos, oldState );
+        super.onBlockAdded( state, world, pos, oldState, flag );
 
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( tile instanceof TileComputerBase ) ((TileComputerBase) tile).updateInput();
     }
 
     @Override
     @Deprecated
-    public boolean canProvidePower( IBlockState state )
+    public boolean emitsRedstonePower( BlockState state )
     {
         return true;
     }
 
     @Override
     @Deprecated
-    public int getStrongPower( IBlockState state, IBlockReader world, BlockPos pos, EnumFacing incomingSide )
+    public int getStrongRedstonePower( BlockState state, BlockView world, BlockPos pos, Direction incomingSide )
     {
-        TileEntity entity = world.getTileEntity( pos );
+        BlockEntity entity = world.getBlockEntity( pos );
         if( !(entity instanceof TileComputerBase) ) return 0;
 
         TileComputerBase computerEntity = (TileComputerBase) entity;
         ServerComputer computer = computerEntity.getServerComputer();
         if( computer == null ) return 0;
 
-        EnumFacing localSide = computerEntity.remapToLocalSide( incomingSide.getOpposite() );
+        Direction localSide = computerEntity.remapToLocalSide( incomingSide.getOpposite() );
         return computerEntity.isRedstoneBlockedOnSide( localSide ) ? 0 :
-            computer.getRedstoneOutput( localSide.getIndex() );
+            computer.getRedstoneOutput( localSide.getId() );
     }
 
     @Nonnull
@@ -80,15 +80,15 @@ public ComputerFamily getFamily()
 
     @Override
     @Deprecated
-    public int getWeakPower( IBlockState state, IBlockReader world, BlockPos pos, EnumFacing incomingSide )
+    public int getWeakRedstonePower( BlockState state, BlockView world, BlockPos pos, Direction incomingSide )
     {
-        return getStrongPower( state, world, pos, incomingSide );
+        return getStrongRedstonePower( state, world, pos, incomingSide );
     }
 
     @Override
-    public boolean getBundledRedstoneConnectivity( World world, BlockPos pos, EnumFacing side )
+    public boolean getBundledRedstoneConnectivity( World world, BlockPos pos, Direction side )
     {
-        TileEntity entity = world.getTileEntity( pos );
+        BlockEntity entity = world.getBlockEntity( pos );
         if( !(entity instanceof TileComputerBase) ) return false;
 
         TileComputerBase computerEntity = (TileComputerBase) entity;
@@ -96,44 +96,46 @@ public boolean getBundledRedstoneConnectivity( World world, BlockPos pos, EnumFa
     }
 
     @Override
-    public int getBundledRedstoneOutput( World world, BlockPos pos, EnumFacing side )
+    public int getBundledRedstoneOutput( World world, BlockPos pos, Direction side )
     {
-        TileEntity entity = world.getTileEntity( pos );
+        BlockEntity entity = world.getBlockEntity( pos );
         if( !(entity instanceof TileComputerBase) ) return 0;
 
         TileComputerBase computerEntity = (TileComputerBase) entity;
         ServerComputer computer = computerEntity.getServerComputer();
         if( computer == null ) return 0;
 
-        EnumFacing localSide = computerEntity.remapToLocalSide( side );
+        Direction localSide = computerEntity.remapToLocalSide( side );
         return computerEntity.isRedstoneBlockedOnSide( localSide ) ? 0 :
-            computer.getBundledRedstoneOutput( localSide.getIndex() );
+            computer.getBundledRedstoneOutput( localSide.getId() );
     }
 
     @Nonnull
     @Override
-    public ItemStack getPickBlock( IBlockState state, RayTraceResult target, IBlockReader world, BlockPos pos, EntityPlayer player )
+    public ItemStack getPickStack( BlockView world, BlockPos pos, BlockState state )
     {
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( tile instanceof TileComputerBase )
         {
             ItemStack result = getItem( (TileComputerBase) tile );
             if( !result.isEmpty() ) return result;
         }
 
-        return super.getPickBlock( state, target, world, pos, player );
+        return super.getPickStack( world, pos, state );
     }
 
+    /*
+    TODO: Find a way of doing creative block drops
     @Override
     @Deprecated
-    public final void dropBlockAsItemWithChance( @Nonnull IBlockState state, World world, @Nonnull BlockPos pos, float change, int fortune )
+    public final void dropBlockAsItemWithChance( @Nonnull BlockState state, World world, @Nonnull BlockPos pos, float change, int fortune )
     {
     }
 
     @Override
-    public final void getDrops( IBlockState state, NonNullList<ItemStack> drops, World world, BlockPos pos, int fortune )
+    public final void getDrops( BlockState state, DefaultedList<ItemStack> drops, World world, BlockPos pos, int fortune )
     {
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( tile instanceof TileComputerBase )
         {
             ItemStack stack = getItem( (TileComputerBase) tile );
@@ -142,33 +144,34 @@ public final void getDrops( IBlockState state, NonNullList<ItemStack> drops, Wor
     }
 
     @Override
-    public boolean removedByPlayer( IBlockState state, World world, BlockPos pos, EntityPlayer player, boolean willHarvest, IFluidState fluid )
+    public boolean removedByPlayer( BlockState state, World world, BlockPos pos, PlayerEntity player, boolean willHarvest, FluidState fluid )
     {
-        if( !world.isRemote )
+        if( !world.isClient )
         {
             // We drop the item here instead of doing it in the harvest method, as we
             // need to drop it for creative players too.
-            TileEntity tile = world.getTileEntity( pos );
+            BlockEntity tile = world.getBlockEntity( pos );
             if( tile instanceof TileComputerBase )
             {
                 TileComputerBase computer = (TileComputerBase) tile;
-                if( !player.abilities.isCreativeMode || computer.getLabel() != null )
+                if( !player.abilities.creativeMode || computer.getLabel() != null )
                 {
-                    spawnAsEntity( world, pos, getItem( computer ) );
+                    dropStack( world, pos, getItem( computer ) );
                 }
             }
         }
 
         return super.removedByPlayer( state, world, pos, player, willHarvest, fluid );
     }
+    */
 
     @Override
-    public void onBlockPlacedBy( World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack )
+    public void onPlaced( World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack )
     {
-        super.onBlockPlacedBy( world, pos, state, placer, stack );
+        super.onPlaced( world, pos, state, placer, stack );
 
-        TileEntity tile = world.getTileEntity( pos );
-        if( !world.isRemote && tile instanceof IComputerTile && stack.getItem() instanceof IComputerItem )
+        BlockEntity tile = world.getBlockEntity( pos );
+        if( !world.isClient && tile instanceof IComputerTile && stack.getItem() instanceof IComputerItem )
         {
             IComputerTile computer = (IComputerTile) tile;
             IComputerItem item = (IComputerItem) stack.getItem();
diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java
index 8a203ce457..3a86887844 100644
--- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileCommandComputer.java
@@ -11,31 +11,30 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
-import net.minecraft.command.CommandSource;
-import net.minecraft.command.ICommandSource;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.server.MinecraftServer;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.server.command.CommandOutput;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.Vec2f;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
-import net.minecraft.world.WorldServer;
 
-import javax.annotation.Nonnull;
 import java.util.HashMap;
 import java.util.Map;
 
 public class TileCommandComputer extends TileComputer
 {
     public static final NamedBlockEntityType<TileCommandComputer> FACTORY = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "command_computer" ),
+        new Identifier( ComputerCraft.MOD_ID, "command_computer" ),
         f -> new TileCommandComputer( ComputerFamily.Command, f )
     );
 
-    public class CommandReceiver implements ICommandSource
+    public class CommandReceiver implements CommandOutput
     {
         private final Map<Integer, String> output = new HashMap<>();
 
@@ -55,25 +54,25 @@ public Map<Integer, String> copyOutput()
         }
 
         @Override
-        public void sendMessage( @Nonnull ITextComponent textComponent )
+        public void appendCommandFeedback( TextComponent textComponent )
         {
-            output.put( output.size() + 1, textComponent.getString() );
+            output.put( output.size() + 1, textComponent.getText() );
         }
 
         @Override
-        public boolean shouldReceiveFeedback()
+        public boolean sendCommandFeedback()
         {
             return getWorld().getGameRules().getBoolean( "sendCommandFeedback" );
         }
 
         @Override
-        public boolean shouldReceiveErrors()
+        public boolean shouldTrackOutput()
         {
             return true;
         }
 
         @Override
-        public boolean allowLogging()
+        public boolean shouldBroadcastConsoleToOps()
         {
             return getWorld().getGameRules().getBoolean( "commandBlockOutput" );
         }
@@ -81,7 +80,7 @@ public boolean allowLogging()
 
     private final CommandReceiver receiver;
 
-    public TileCommandComputer( ComputerFamily family, TileEntityType<? extends TileCommandComputer> type )
+    public TileCommandComputer( ComputerFamily family, BlockEntityType<? extends TileCommandComputer> type )
     {
         super( family, type );
         receiver = new CommandReceiver();
@@ -92,7 +91,7 @@ public CommandReceiver getReceiver()
         return receiver;
     }
 
-    public CommandSource getSource()
+    public ServerCommandSource getSource()
     {
         ServerComputer computer = getServerComputer();
         String name = "@";
@@ -102,10 +101,10 @@ public CommandSource getSource()
             if( label != null ) name = label;
         }
 
-        return new CommandSource( receiver,
+        return new ServerCommandSource( receiver,
             new Vec3d( pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5 ), Vec2f.ZERO,
-            (WorldServer) getWorld(), 2,
-            name, new TextComponentString( name ),
+            (ServerWorld) getWorld(), 2,
+            name, new StringTextComponent( name ),
             getWorld().getServer(), null
         );
     }
@@ -119,17 +118,17 @@ protected ServerComputer createComputer( int instanceID, int id )
     }
 
     @Override
-    public boolean isUsable( EntityPlayer player, boolean ignoreRange )
+    public boolean isUsable( PlayerEntity player, boolean ignoreRange )
     {
         MinecraftServer server = player.getServer();
-        if( server == null || !server.isCommandBlockEnabled() )
+        if( server == null || !server.areCommandBlocksEnabled() )
         {
-            player.sendStatusMessage( new TextComponentTranslation( "advMode.notEnabled" ), true );
+            player.addChatMessage( new TranslatableTextComponent( "advMode.notEnabled" ), true );
             return false;
         }
-        else if( !player.canUseCommandBlock() )
+        else if( !player.isCreativeLevelTwoOp() )
         {
-            player.sendStatusMessage( new TextComponentTranslation( "advMode.notAllowed" ), true );
+            player.addChatMessage( new TranslatableTextComponent( "advMode.notAllowed" ), true );
             return false;
         }
         else
diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java
index 6f132cd983..c802ea8960 100644
--- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputer.java
@@ -12,27 +12,27 @@
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.network.Containers;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.Direction;
 
 public class TileComputer extends TileComputerBase
 {
     public static final NamedBlockEntityType<TileComputer> FACTORY_NORMAL = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "computer_normal" ),
+        new Identifier( ComputerCraft.MOD_ID, "computer_normal" ),
         f -> new TileComputer( ComputerFamily.Normal, f )
     );
 
     public static final NamedBlockEntityType<TileComputer> FACTORY_ADVANCED = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "computer_advanced" ),
+        new Identifier( ComputerCraft.MOD_ID, "computer_advanced" ),
         f -> new TileComputer( ComputerFamily.Advanced, f )
     );
 
     private ComputerProxy m_proxy;
 
-    public TileComputer( ComputerFamily family, TileEntityType<? extends TileComputer> type )
+    public TileComputer( ComputerFamily family, BlockEntityType<? extends TileComputer> type )
     {
         super( type, family );
     }
@@ -68,26 +68,26 @@ protected TileComputerBase getTile()
     }
 
     @Override
-    public void openGUI( EntityPlayer player )
+    public void openGUI( PlayerEntity player )
     {
         Containers.openComputerGUI( player, this );
     }
 
-    public boolean isUsableByPlayer( EntityPlayer player )
+    public boolean isUsableByPlayer( PlayerEntity player )
     {
         return isUsable( player, false );
     }
 
     @Override
-    public EnumFacing getDirection()
+    public Direction getDirection()
     {
-        return getBlockState().get( BlockComputer.FACING );
+        return getCachedState().get( BlockComputer.FACING );
     }
 
     @Override
     protected void updateBlockState( ComputerState newState )
     {
-        IBlockState existing = getBlockState();
+        BlockState existing = getCachedState();
         if( existing.get( BlockComputer.STATE ) != newState )
         {
             getWorld().setBlockState( getPos(), existing.with( BlockComputer.STATE, newState ), 3 );
@@ -95,8 +95,8 @@ protected void updateBlockState( ComputerState newState )
     }
 
     @Override
-    protected EnumFacing remapLocalSide( EnumFacing localSide )
+    protected Direction remapLocalSide( Direction localSide )
     {
-        return localSide.getAxis() == EnumFacing.Axis.X ? localSide.getOpposite() : localSide;
+        return localSide.getAxis() == Direction.Axis.X ? localSide.getOpposite() : localSide;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java
index e50c9aa9d0..753309cb24 100644
--- a/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java
+++ b/src/main/java/dan200/computercraft/shared/computer/blocks/TileComputerBase.java
@@ -19,24 +19,25 @@
 import dan200.computercraft.shared.util.DirectionUtil;
 import dan200.computercraft.shared.util.RedstoneUtil;
 import joptsimple.internal.Strings;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.init.Items;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.INameable;
-import net.minecraft.util.ITickable;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Nameable;
+import net.minecraft.util.Tickable;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.util.Objects;
 
-public abstract class TileComputerBase extends TileGeneric implements IComputerTile, ITickable, IPeripheralTile, INameable
+public abstract class TileComputerBase extends TileGeneric implements IComputerTile, Tickable, IPeripheralTile, Nameable
 {
     private static final String NBT_ID = "ComputerId";
     private static final String NBT_LABEL = "Label";
@@ -52,7 +53,7 @@ public abstract class TileComputerBase extends TileGeneric implements IComputerT
 
     private final ComputerFamily family;
 
-    public TileComputerBase( TileEntityType<? extends TileGeneric> type, ComputerFamily family )
+    public TileComputerBase( BlockEntityType<? extends TileGeneric> type, ComputerFamily family )
     {
         super( type );
         this.family = family;
@@ -62,7 +63,7 @@ protected void unload()
     {
         if( m_instanceID >= 0 )
         {
-            if( !getWorld().isRemote ) ComputerCraft.serverComputerRegistry.remove( m_instanceID );
+            if( !getWorld().isClient ) ComputerCraft.serverComputerRegistry.remove( m_instanceID );
             m_instanceID = -1;
         }
     }
@@ -71,50 +72,52 @@ protected void unload()
     public void destroy()
     {
         unload();
-        for( EnumFacing dir : DirectionUtil.FACINGS )
+        for( Direction dir : DirectionUtil.FACINGS )
         {
             RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir );
         }
     }
 
+    /*
     @Override
     public void onChunkUnloaded()
     {
         unload();
     }
+    */
 
     @Override
-    public void remove()
+    public void invalidate()
     {
         unload();
-        super.remove();
+        super.invalidate();
     }
 
-    public abstract void openGUI( EntityPlayer player );
+    public abstract void openGUI( PlayerEntity player );
 
-    protected boolean canNameWithTag( EntityPlayer player )
+    protected boolean canNameWithTag( PlayerEntity player )
     {
         return false;
     }
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
-        ItemStack currentItem = player.getHeldItem( hand );
+        ItemStack currentItem = player.getStackInHand( hand );
         if( !currentItem.isEmpty() && currentItem.getItem() == Items.NAME_TAG && canNameWithTag( player ) && currentItem.hasDisplayName() )
         {
             // Label to rename computer
-            if( !getWorld().isRemote )
+            if( !getWorld().isClient )
             {
-                setLabel( currentItem.getDisplayName().getString() );
-                currentItem.shrink( 1 );
+                setLabel( currentItem.getDisplayName().getText() );
+                currentItem.subtractAmount( 1 );
             }
             return true;
         }
         else if( !player.isSneaking() )
         {
             // Regular right click to activate computer
-            if( !getWorld().isRemote && isUsable( player, false ) )
+            if( !getWorld().isClient && isUsable( player, false ) )
             {
                 createServerComputer().turnOn();
                 openGUI( player );
@@ -139,7 +142,7 @@ public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour )
     @Override
     public void tick()
     {
-        if( !getWorld().isRemote )
+        if( !getWorld().isClient )
         {
             ServerComputer computer = createServerComputer();
             if( computer == null ) return;
@@ -177,74 +180,74 @@ public void tick()
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound nbt )
+    public CompoundTag toTag( CompoundTag nbt )
     {
         // Save ID, label and power state
         if( m_computerID >= 0 ) nbt.putInt( NBT_ID, m_computerID );
         if( m_label != null ) nbt.putString( NBT_LABEL, m_label );
         nbt.putBoolean( NBT_ON, m_on );
 
-        return super.write( nbt );
+        return super.toTag( nbt );
     }
 
     @Override
-    public void read( NBTTagCompound nbt )
+    public void fromTag( CompoundTag nbt )
     {
-        super.read( nbt );
+        super.fromTag( nbt );
 
         // Load ID, label and power state
-        m_computerID = nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
-        m_label = nbt.contains( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null;
+        m_computerID = nbt.containsKey( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
+        m_label = nbt.containsKey( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null;
         m_on = m_startOn = nbt.getBoolean( NBT_ON );
     }
 
-    protected boolean isPeripheralBlockedOnSide( EnumFacing localSide )
+    protected boolean isPeripheralBlockedOnSide( Direction localSide )
     {
         return false;
     }
 
-    protected boolean isRedstoneBlockedOnSide( EnumFacing localSide )
+    protected boolean isRedstoneBlockedOnSide( Direction localSide )
     {
         return false;
     }
 
-    protected abstract EnumFacing getDirection();
+    protected abstract Direction getDirection();
 
-    protected EnumFacing remapToLocalSide( EnumFacing globalSide )
+    protected Direction remapToLocalSide( Direction globalSide )
     {
         return remapLocalSide( DirectionUtil.toLocal( getDirection(), globalSide ) );
     }
 
-    protected EnumFacing remapLocalSide( EnumFacing localSide )
+    protected Direction remapLocalSide( Direction localSide )
     {
         return localSide;
     }
 
-    private void updateSideInput( ServerComputer computer, EnumFacing dir, BlockPos offset )
+    private void updateSideInput( ServerComputer computer, Direction dir, BlockPos offset )
     {
-        EnumFacing offsetSide = dir.getOpposite();
-        EnumFacing localDir = remapToLocalSide( dir );
+        Direction offsetSide = dir.getOpposite();
+        Direction localDir = remapToLocalSide( dir );
         if( !isRedstoneBlockedOnSide( localDir ) )
         {
-            computer.setRedstoneInput( localDir.getIndex(), getWorld().getRedstonePower( offset, dir ) );
-            computer.setBundledRedstoneInput( localDir.getIndex(), BundledRedstone.getOutput( getWorld(), offset, offsetSide ) );
+            computer.setRedstoneInput( localDir.getId(), getWorld().getEmittedRedstonePower( offset, dir ) );
+            computer.setBundledRedstoneInput( localDir.getId(), BundledRedstone.getOutput( getWorld(), offset, offsetSide ) );
         }
         if( !isPeripheralBlockedOnSide( localDir ) )
         {
-            computer.setPeripheral( localDir.getIndex(), Peripherals.getPeripheral( getWorld(), offset, offsetSide ) );
+            computer.setPeripheral( localDir.getId(), Peripherals.getPeripheral( getWorld(), offset, offsetSide ) );
         }
     }
 
     public void updateInput()
     {
-        if( getWorld() == null || getWorld().isRemote ) return;
+        if( getWorld() == null || getWorld().isClient ) return;
 
         // Update all sides
         ServerComputer computer = getServerComputer();
         if( computer == null ) return;
 
         BlockPos pos = computer.getPosition();
-        for( EnumFacing dir : DirectionUtil.FACINGS )
+        for( Direction dir : DirectionUtil.FACINGS )
         {
             updateSideInput( computer, dir, pos.offset( dir ) );
         }
@@ -252,14 +255,14 @@ public void updateInput()
 
     private void updateInput( BlockPos neighbour )
     {
-        if( getWorld() == null || getWorld().isRemote ) return;
+        if( getWorld() == null || getWorld().isClient ) return;
 
         ServerComputer computer = getServerComputer();
         if( computer == null ) return;
 
         // Find the appropriate side and update.
         BlockPos pos = computer.getPosition();
-        for( EnumFacing dir : DirectionUtil.FACINGS )
+        for( Direction dir : DirectionUtil.FACINGS )
         {
             BlockPos offset = pos.offset( dir );
             if( offset.equals( neighbour ) )
@@ -274,7 +277,7 @@ public void updateOutput()
     {
         // Update redstone
         updateBlock();
-        for( EnumFacing dir : DirectionUtil.FACINGS )
+        for( Direction dir : DirectionUtil.FACINGS )
         {
             RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir );
         }
@@ -299,7 +302,7 @@ public final String getLabel()
     @Override
     public final void setComputerID( int id )
     {
-        if( getWorld().isRemote || m_computerID == id ) return;
+        if( getWorld().isClient || m_computerID == id ) return;
 
         m_computerID = id;
         ServerComputer computer = getServerComputer();
@@ -310,7 +313,7 @@ public final void setComputerID( int id )
     @Override
     public final void setLabel( String label )
     {
-        if( getWorld().isRemote || Objects.equals( m_label, label ) ) return;
+        if( getWorld().isClient || Objects.equals( m_label, label ) ) return;
 
         m_label = label;
         ServerComputer computer = getServerComputer();
@@ -326,7 +329,7 @@ public ComputerFamily getFamily()
 
     public ServerComputer createServerComputer()
     {
-        if( getWorld().isRemote ) return null;
+        if( getWorld().isClient ) return null;
 
         boolean changed = false;
         if( m_instanceID < 0 )
@@ -351,12 +354,12 @@ public ServerComputer createServerComputer()
 
     public ServerComputer getServerComputer()
     {
-        return getWorld().isRemote ? null : ComputerCraft.serverComputerRegistry.get( m_instanceID );
+        return getWorld().isClient ? null : ComputerCraft.serverComputerRegistry.get( m_instanceID );
     }
 
     public ClientComputer createClientComputer()
     {
-        if( !getWorld().isRemote || m_instanceID < 0 ) return null;
+        if( !getWorld().isClient || m_instanceID < 0 ) return null;
 
         ClientComputer computer = ComputerCraft.clientComputerRegistry.get( m_instanceID );
         if( computer == null )
@@ -368,13 +371,13 @@ public ClientComputer createClientComputer()
 
     public ClientComputer getClientComputer()
     {
-        return getWorld().isRemote ? ComputerCraft.clientComputerRegistry.get( m_instanceID ) : null;
+        return getWorld().isClient ? ComputerCraft.clientComputerRegistry.get( m_instanceID ) : null;
     }
 
     // Networking stuff
 
     @Override
-    protected void writeDescription( @Nonnull NBTTagCompound nbt )
+    protected void writeDescription( @Nonnull CompoundTag nbt )
     {
         super.writeDescription( nbt );
 
@@ -384,12 +387,12 @@ protected void writeDescription( @Nonnull NBTTagCompound nbt )
     }
 
     @Override
-    protected void readDescription( @Nonnull NBTTagCompound nbt )
+    protected void readDescription( @Nonnull CompoundTag nbt )
     {
         super.readDescription( nbt );
-        m_instanceID = nbt.contains( NBT_INSTANCE ) ? nbt.getInt( NBT_INSTANCE ) : -1;
-        m_label = nbt.contains( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null;
-        m_computerID = nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
+        m_instanceID = nbt.containsKey( NBT_INSTANCE ) ? nbt.getInt( NBT_INSTANCE ) : -1;
+        m_label = nbt.containsKey( NBT_LABEL ) ? nbt.getString( NBT_LABEL ) : null;
+        m_computerID = nbt.containsKey( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
     }
 
     protected void transferStateFrom( TileComputerBase copy )
@@ -409,16 +412,16 @@ protected void transferStateFrom( TileComputerBase copy )
 
     @Nullable
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         return new ComputerPeripheral( "computer", createProxy() );
     }
 
     @Nonnull
     @Override
-    public ITextComponent getName()
+    public TextComponent getName()
     {
-        return hasCustomName() ? new TextComponentString( m_label ) : getBlockState().getBlock().getNameTextComponent();
+        return hasCustomName() ? new StringTextComponent( m_label ) : getCachedState().getBlock().getTextComponent();
     }
 
     @Override
@@ -429,8 +432,8 @@ public boolean hasCustomName()
 
     @Nullable
     @Override
-    public ITextComponent getCustomName()
+    public TextComponent getCustomName()
     {
-        return hasCustomName() ? new TextComponentString( m_label ) : null;
+        return hasCustomName() ? new StringTextComponent( m_label ) : null;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java
index 7c029380d6..e71d82c8dd 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ClientComputer.java
@@ -10,7 +10,7 @@
 import dan200.computercraft.shared.common.ClientTerminal;
 import dan200.computercraft.shared.network.NetworkHandler;
 import dan200.computercraft.shared.network.server.*;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 public class ClientComputer extends ClientTerminal implements IComputer
 {
@@ -19,7 +19,7 @@ public class ClientComputer extends ClientTerminal implements IComputer
     private boolean m_on = false;
     private boolean m_blinking = false;
     private boolean m_changed = true;
-    private NBTTagCompound m_userData = null;
+    private CompoundTag m_userData = null;
 
     private boolean m_changedLastFrame = false;
 
@@ -40,7 +40,7 @@ public boolean hasOutputChanged()
         return m_changedLastFrame;
     }
 
-    public NBTTagCompound getUserData()
+    public CompoundTag getUserData()
     {
         return m_userData;
     }
@@ -135,11 +135,11 @@ public void mouseScroll( int direction, int x, int y )
         NetworkHandler.sendToServer( new MouseEventServerMessage( m_instanceID, MouseEventServerMessage.TYPE_SCROLL, direction, x, y ) );
     }
 
-    public void setState( ComputerState state, NBTTagCompound userData )
+    public void setState( ComputerState state, CompoundTag userData )
     {
         boolean oldOn = m_on;
         boolean oldBlinking = m_blinking;
-        NBTTagCompound oldUserData = m_userData;
+        CompoundTag oldUserData = m_userData;
 
         m_on = state != ComputerState.OFF;
         m_blinking = state == ComputerState.BLINKING;
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java b/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java
index 88dc8e752e..85f818ebe3 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ComputerState.java
@@ -6,11 +6,11 @@
 
 package dan200.computercraft.shared.computer.core;
 
-import net.minecraft.util.IStringSerializable;
+import net.minecraft.util.StringRepresentable;
 
 import javax.annotation.Nonnull;
 
-public enum ComputerState implements IStringSerializable
+public enum ComputerState implements StringRepresentable
 {
     OFF( "off" ),
     ON( "on" ),
@@ -25,7 +25,7 @@ public enum ComputerState implements IStringSerializable
 
     @Nonnull
     @Override
-    public String getName()
+    public String asString()
     {
         return name;
     }
diff --git a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
index 59d6348234..9012ff7493 100644
--- a/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/core/ServerComputer.java
@@ -21,14 +21,13 @@
 import dan200.computercraft.shared.network.client.ComputerDataClientMessage;
 import dan200.computercraft.shared.network.client.ComputerDeletedClientMessage;
 import dan200.computercraft.shared.network.client.ComputerTerminalClientMessage;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.SharedConstants;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.nbt.CompoundTag;
 import net.minecraft.server.MinecraftServer;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
-import net.minecraftforge.fml.server.ServerLifecycleHooks;
-import net.minecraftforge.versions.mcp.MCPVersion;
 
 import javax.annotation.Nullable;
 import java.io.InputStream;
@@ -42,7 +41,7 @@ public class ServerComputer extends ServerTerminal implements IComputer, IComput
 
     private final ComputerFamily m_family;
     private final Computer m_computer;
-    private NBTTagCompound m_userData;
+    private CompoundTag m_userData;
     private boolean m_changed;
 
     private boolean m_changedLastFrame;
@@ -133,11 +132,11 @@ public void unload()
         m_computer.unload();
     }
 
-    public NBTTagCompound getUserData()
+    public CompoundTag getUserData()
     {
         if( m_userData == null )
         {
-            m_userData = new NBTTagCompound();
+            m_userData = new CompoundTag();
         }
         return m_userData;
     }
@@ -154,39 +153,41 @@ private NetworkMessage createComputerPacket()
 
     protected NetworkMessage createTerminalPacket()
     {
-        NBTTagCompound tagCompound = new NBTTagCompound();
+        CompoundTag tagCompound = new CompoundTag();
         writeDescription( tagCompound );
         return new ComputerTerminalClientMessage( getInstanceID(), tagCompound );
     }
 
     public void broadcastState( boolean force )
     {
+        MinecraftServer server = m_world == null ? null : m_world.getServer();
+        if( server == null ) return;
+
         if( hasOutputChanged() || force )
         {
             // Send computer state to all clients
-            NetworkHandler.sendToAllPlayers( createComputerPacket() );
+            NetworkHandler.sendToAllPlayers( server, createComputerPacket() );
         }
 
         if( hasTerminalChanged() || force )
         {
             // Send terminal state to clients who are currently interacting with the computer.
-            MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
 
             NetworkMessage packet = createTerminalPacket();
-            for( EntityPlayer player : server.getPlayerList().getPlayers() )
+            for( PlayerEntity player : server.getPlayerManager().getPlayerList() )
             {
                 if( isInteracting( player ) ) NetworkHandler.sendToPlayer( player, packet );
             }
         }
     }
 
-    public void sendComputerState( EntityPlayer player )
+    public void sendComputerState( PlayerEntity player )
     {
         // Send state to client
         NetworkHandler.sendToPlayer( player, createComputerPacket() );
     }
 
-    public void sendTerminalState( EntityPlayer player )
+    public void sendTerminalState( PlayerEntity player )
     {
         // Send terminal state to client
         NetworkHandler.sendToPlayer( player, createTerminalPacket() );
@@ -195,7 +196,9 @@ public void sendTerminalState( EntityPlayer player )
     public void broadcastDelete()
     {
         // Send deletion to client
-        NetworkHandler.sendToAllPlayers( new ComputerDeletedClientMessage( getInstanceID() ) );
+        MinecraftServer server = m_world == null ? null : m_world.getServer();
+        if( server == null ) return;
+        NetworkHandler.sendToAllPlayers( server, new ComputerDeletedClientMessage( getInstanceID() ) );
     }
 
     public void setID( int id )
@@ -306,13 +309,13 @@ public void setLabel( String label )
     @Override
     public double getTimeOfDay()
     {
-        return (m_world.getGameTime() + 6000) % 24000 / 1000.0;
+        return (m_world.getTime() + 6000) % 24000 / 1000.0;
     }
 
     @Override
     public int getDay()
     {
-        return (int) ((m_world.getGameTime() + 6000) / 24000) + 1;
+        return (int) ((m_world.getTime() + 6000) / 24000) + 1;
     }
 
     @Override
@@ -342,7 +345,7 @@ public long getComputerSpaceLimit()
     @Override
     public String getHostString()
     {
-        return "ComputerCraft ${version} (Minecraft " + MCPVersion.getMCVersion() + ")";
+        return "ComputerCraft ${version} (Minecraft " + SharedConstants.getGameVersion() + ")";
     }
 
     @Override
@@ -352,18 +355,18 @@ public int assignNewID()
     }
 
     @Nullable
-    public IContainerComputer getContainer( EntityPlayer player )
+    public IContainerComputer getContainer( PlayerEntity player )
     {
         if( player == null ) return null;
 
-        Container container = player.openContainer;
+        Container container = player.container;
         if( !(container instanceof IContainerComputer) ) return null;
 
         IContainerComputer computerContainer = (IContainerComputer) container;
         return computerContainer.getComputer() != this ? null : computerContainer;
     }
 
-    protected boolean isInteracting( EntityPlayer player )
+    protected boolean isInteracting( PlayerEntity player )
     {
         return getContainer( player ) != null;
     }
diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java
index deb1f596f1..4e945857ae 100644
--- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerComputer.java
@@ -10,8 +10,8 @@
 import dan200.computercraft.shared.computer.core.IComputer;
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.InputState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerEntity;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -21,13 +21,14 @@ public class ContainerComputer extends Container implements IContainerComputer
     private final TileComputer computer;
     private final InputState input = new InputState( this );
 
-    public ContainerComputer( TileComputer computer )
+    public ContainerComputer( int id, TileComputer computer )
     {
+        super( null, id );
         this.computer = computer;
     }
 
     @Override
-    public boolean canInteractWith( @Nonnull EntityPlayer player )
+    public boolean canUse( @Nonnull PlayerEntity player )
     {
         return computer.isUsableByPlayer( player );
     }
@@ -47,9 +48,9 @@ public InputState getInput()
     }
 
     @Override
-    public void onContainerClosed( EntityPlayer player )
+    public void close( PlayerEntity player )
     {
-        super.onContainerClosed( player );
+        super.close( player );
         input.close();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java
index 098fc29597..0f714dc0f3 100644
--- a/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/inventory/ContainerViewComputer.java
@@ -8,10 +8,10 @@
 
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.computer.core.*;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.server.MinecraftServer;
-import net.minecraft.util.text.TextComponentTranslation;
+import net.minecraft.text.TranslatableTextComponent;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -21,8 +21,9 @@ public class ContainerViewComputer extends Container implements IContainerComput
     private final IComputer computer;
     private final InputState input = new InputState( this );
 
-    public ContainerViewComputer( IComputer computer )
+    public ContainerViewComputer( int id, IComputer computer )
     {
+        super( null, id );
         this.computer = computer;
     }
 
@@ -34,7 +35,7 @@ public IComputer getComputer()
     }
 
     @Override
-    public boolean canInteractWith( @Nonnull EntityPlayer player )
+    public boolean canUse( @Nonnull PlayerEntity player )
     {
         if( computer instanceof ServerComputer )
         {
@@ -50,14 +51,14 @@ public boolean canInteractWith( @Nonnull EntityPlayer player )
             if( serverComputer.getFamily() == ComputerFamily.Command )
             {
                 MinecraftServer server = player.getServer();
-                if( server == null || !server.isCommandBlockEnabled() )
+                if( server == null || !server.areCommandBlocksEnabled() )
                 {
-                    player.sendStatusMessage( new TextComponentTranslation( "advMode.notEnabled" ), false );
+                    player.addChatMessage( new TranslatableTextComponent( "advMode.notEnabled" ), false );
                     return false;
                 }
-                else if( !player.canUseCommandBlock() )
+                else if( !player.isCreativeLevelTwoOp() )
                 {
-                    player.sendStatusMessage( new TextComponentTranslation( "advMode.notAllowed" ), false );
+                    player.addChatMessage( new TranslatableTextComponent( "advMode.notAllowed" ), false );
                     return false;
                 }
             }
@@ -74,9 +75,9 @@ public InputState getInput()
     }
 
     @Override
-    public void onContainerClosed( EntityPlayer player )
+    public void close( PlayerEntity player )
     {
-        super.onContainerClosed( player );
+        super.close( player );
         input.close();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java b/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java
index d243dc50a8..71e18b294a 100644
--- a/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java
+++ b/src/main/java/dan200/computercraft/shared/computer/items/IComputerItem.java
@@ -8,7 +8,7 @@
 
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 import javax.annotation.Nonnull;
 
@@ -18,8 +18,8 @@ public interface IComputerItem
 
     default int getComputerID( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
     }
 
     default String getLabel( @Nonnull ItemStack stack )
diff --git a/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java
index 6c36c8ae5b..11af62c11b 100644
--- a/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java
+++ b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputer.java
@@ -9,13 +9,13 @@
 import dan200.computercraft.shared.computer.blocks.BlockComputer;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.text.TextComponentString;
+import net.minecraft.text.StringTextComponent;
 
 import javax.annotation.Nonnull;
 
 public class ItemComputer extends ItemComputerBase
 {
-    public ItemComputer( BlockComputer block, Properties settings )
+    public ItemComputer( BlockComputer block, Settings settings )
     {
         super( block, settings );
     }
@@ -24,7 +24,7 @@ public ItemStack create( int id, String label )
     {
         ItemStack result = new ItemStack( this );
         if( id >= 0 ) result.getOrCreateTag().putInt( NBT_ID, id );
-        if( label != null ) result.setDisplayName( new TextComponentString( label ) );
+        if( label != null ) result.setDisplayName( new StringTextComponent( label ) );
         return result;
     }
 
diff --git a/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java
index 377e41959f..30479a89f3 100644
--- a/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java
+++ b/src/main/java/dan200/computercraft/shared/computer/items/ItemComputerBase.java
@@ -12,39 +12,39 @@
 import dan200.computercraft.api.media.IMedia;
 import dan200.computercraft.shared.computer.blocks.BlockComputerBase;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
-import net.minecraft.client.util.ITooltipFlag;
-import net.minecraft.item.ItemBlock;
+import net.minecraft.client.item.TooltipContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
-import net.minecraft.util.text.TextFormatting;
+import net.minecraft.item.block.BlockItem;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TextFormat;
+import net.minecraft.text.TranslatableTextComponent;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.util.List;
 
-public abstract class ItemComputerBase extends ItemBlock implements IComputerItem, IMedia
+public abstract class ItemComputerBase extends BlockItem implements IComputerItem, IMedia
 {
     private final ComputerFamily family;
 
-    public ItemComputerBase( BlockComputerBase<?> block, Properties settings )
+    public ItemComputerBase( BlockComputerBase<?> block, Settings settings )
     {
         super( block, settings );
         family = block.getFamily();
     }
 
     @Override
-    public void addInformation( @Nonnull ItemStack stack, @Nullable World world, @Nonnull List<ITextComponent> list, @Nonnull ITooltipFlag options )
+    public void buildTooltip( @Nonnull ItemStack stack, @Nullable World world, @Nonnull List<TextComponent> list, @Nonnull TooltipContext options )
     {
         if( options.isAdvanced() )
         {
             int id = getComputerID( stack );
             if( id >= 0 )
             {
-                list.add( new TextComponentTranslation( "gui.computercraft.tooltip.computer_id", id )
-                    .applyTextStyle( TextFormatting.GRAY ) );
+                list.add( new TranslatableTextComponent( "gui.computercraft.tooltip.computer_id", id )
+                    .applyFormat( TextFormat.GRAY ) );
             }
         }
     }
@@ -52,7 +52,7 @@ public void addInformation( @Nonnull ItemStack stack, @Nullable World world, @No
     @Override
     public String getLabel( @Nonnull ItemStack stack )
     {
-        return IComputerItem.super.getLabel( stack );
+        return stack.hasDisplayName() ? stack.getDisplayName().getString() : null;
     }
 
     @Override
@@ -68,11 +68,11 @@ public boolean setLabel( @Nonnull ItemStack stack, String label )
     {
         if( label != null )
         {
-            stack.setDisplayName( new TextComponentString( label ) );
+            stack.setDisplayName( new StringTextComponent( label ) );
         }
         else
         {
-            stack.clearCustomName();
+            stack.removeDisplayName();
         }
         return true;
     }
diff --git a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java
index e93f772602..778a33d464 100644
--- a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerConvertRecipe.java
@@ -7,12 +7,12 @@
 package dan200.computercraft.shared.computer.recipe;
 
 import dan200.computercraft.shared.computer.items.IComputerItem;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.item.crafting.ShapedRecipe;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.crafting.ShapedRecipe;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -24,7 +24,7 @@ public abstract class ComputerConvertRecipe extends ShapedRecipe
 {
     private final String group;
 
-    public ComputerConvertRecipe( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result )
+    public ComputerConvertRecipe( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result )
     {
         super( identifier, group, width, height, ingredients, result );
         this.group = group;
@@ -34,13 +34,13 @@ public ComputerConvertRecipe( ResourceLocation identifier, String group, int wid
     protected abstract ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack stack );
 
     @Override
-    public boolean matches( @Nonnull IInventory inventory, @Nonnull World world )
+    public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world )
     {
-        if( !super.matches( inventory, world ) ) return false;
+        if( !method_17728( inventory, world ) ) return false;
 
-        for( int i = 0; i < inventory.getSizeInventory(); i++ )
+        for( int i = 0; i < inventory.getInvSize(); i++ )
         {
-            if( inventory.getStackInSlot( i ).getItem() instanceof IComputerItem ) return true;
+            if( inventory.getInvStack( i ).getItem() instanceof IComputerItem ) return true;
         }
 
         return false;
@@ -48,12 +48,12 @@ public boolean matches( @Nonnull IInventory inventory, @Nonnull World world )
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inventory )
+    public ItemStack craft( @Nonnull CraftingInventory inventory )
     {
         // Find our computer item and convert it.
-        for( int i = 0; i < inventory.getSizeInventory(); i++ )
+        for( int i = 0; i < inventory.getInvSize(); i++ )
         {
-            ItemStack stack = inventory.getStackInSlot( i );
+            ItemStack stack = inventory.getInvStack( i );
             if( stack.getItem() instanceof IComputerItem ) return convert( (IComputerItem) stack.getItem(), stack );
         }
 
diff --git a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java
index 3911c3534a..65dd2fb1fe 100644
--- a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerFamilyRecipe.java
@@ -10,12 +10,13 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.util.RecipeUtil;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.JsonUtils;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.crafting.ShapedRecipe;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.JsonHelper;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -23,7 +24,7 @@ public abstract class ComputerFamilyRecipe extends ComputerConvertRecipe
 {
     private final ComputerFamily family;
 
-    public ComputerFamilyRecipe( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
+    public ComputerFamilyRecipe( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
     {
         super( identifier, group, width, height, ingredients, result );
         this.family = family;
@@ -34,48 +35,48 @@ public ComputerFamily getFamily()
         return family;
     }
 
-    public abstract static class Serializer<T extends ComputerFamilyRecipe> implements IRecipeSerializer<T>
+    public abstract static class Serializer<T extends ComputerFamilyRecipe> implements RecipeSerializer<T>
     {
-        protected abstract T create( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family );
+        protected abstract T create( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, ComputerFamily family );
 
         @Nonnull
         @Override
-        public T read( @Nonnull ResourceLocation identifier, @Nonnull JsonObject json )
+        public T read( @Nonnull Identifier identifier, @Nonnull JsonObject json )
         {
-            String group = JsonUtils.getString( json, "group", "" );
+            String group = JsonHelper.getString( json, "group", "" );
             ComputerFamily family = RecipeUtil.getFamily( json, "family" );
 
             RecipeUtil.ShapedTemplate template = RecipeUtil.getTemplate( json );
-            ItemStack result = deserializeItem( JsonUtils.getJsonObject( json, "result" ) );
+            ItemStack result = ShapedRecipe.getItemStack( JsonHelper.getObject( json, "result" ) );
 
             return create( identifier, group, template.width, template.height, template.ingredients, result, family );
         }
 
         @Nonnull
         @Override
-        public T read( @Nonnull ResourceLocation identifier, @Nonnull PacketBuffer buf )
+        public T read( @Nonnull Identifier identifier, @Nonnull PacketByteBuf buf )
         {
             int width = buf.readVarInt();
             int height = buf.readVarInt();
             String group = buf.readString( Short.MAX_VALUE );
 
-            NonNullList<Ingredient> ingredients = NonNullList.withSize( width * height, Ingredient.EMPTY );
-            for( int i = 0; i < ingredients.size(); i++ ) ingredients.set( i, Ingredient.read( buf ) );
+            DefaultedList<Ingredient> ingredients = DefaultedList.create( width * height, Ingredient.EMPTY );
+            for( int i = 0; i < ingredients.size(); i++ ) ingredients.set( i, Ingredient.fromPacket( buf ) );
 
             ItemStack result = buf.readItemStack();
-            ComputerFamily family = buf.readEnumValue( ComputerFamily.class );
+            ComputerFamily family = buf.readEnumConstant( ComputerFamily.class );
             return create( identifier, group, width, height, ingredients, result, family );
         }
 
         @Override
-        public void write( @Nonnull PacketBuffer buf, @Nonnull T recipe )
+        public void write( @Nonnull PacketByteBuf buf, @Nonnull T recipe )
         {
             buf.writeVarInt( recipe.getWidth() );
             buf.writeVarInt( recipe.getHeight() );
             buf.writeString( recipe.getGroup() );
-            for( Ingredient ingredient : recipe.getIngredients() ) ingredient.write( buf );
-            buf.writeItemStack( recipe.getRecipeOutput() );
-            buf.writeEnumValue( recipe.getFamily() );
+            for( Ingredient ingredient : recipe.getPreviewInputs() ) ingredient.write( buf );
+            buf.writeItemStack( recipe.getOutput() );
+            buf.writeEnumConstant( recipe.getFamily() );
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java
index e7604f2dfb..f55c8cd313 100644
--- a/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/computer/recipe/ComputerUpgradeRecipe.java
@@ -6,20 +6,19 @@
 
 package dan200.computercraft.shared.computer.recipe;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.items.IComputerItem;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 
 public class ComputerUpgradeRecipe extends ComputerFamilyRecipe
 {
-    public ComputerUpgradeRecipe( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
+    public ComputerUpgradeRecipe( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
     {
         super( identifier, group, width, height, ingredients, result, family );
     }
@@ -33,25 +32,17 @@ protected ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack sta
 
     @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    private static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "computer_upgrade" );
-    public static final IRecipeSerializer<ComputerUpgradeRecipe> SERIALIZER = new Serializer<ComputerUpgradeRecipe>()
+    public static final RecipeSerializer<ComputerUpgradeRecipe> SERIALIZER = new Serializer<ComputerUpgradeRecipe>()
     {
         @Override
-        protected ComputerUpgradeRecipe create( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
+        protected ComputerUpgradeRecipe create( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
         {
             return new ComputerUpgradeRecipe( identifier, group, width, height, ingredients, result, family );
         }
-
-        @Nonnull
-        @Override
-        public ResourceLocation getName()
-        {
-            return ID;
-        }
     };
 }
diff --git a/src/main/java/dan200/computercraft/shared/integration/charset/BundledCapabilityProvider.java b/src/main/java/dan200/computercraft/shared/integration/charset/BundledCapabilityProvider.java
index e5ceb83749..9a2f8aa3ca 100644
--- a/src/main/java/dan200/computercraft/shared/integration/charset/BundledCapabilityProvider.java
+++ b/src/main/java/dan200/computercraft/shared/integration/charset/BundledCapabilityProvider.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.integration.charset;
 
 import dan200.computercraft.shared.common.TileGeneric;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Direction;
 import net.minecraftforge.common.capabilities.Capability;
 import net.minecraftforge.common.capabilities.ICapabilityProvider;
 import pl.asie.charset.api.wires.IBundledEmitter;
@@ -31,14 +31,14 @@ final class BundledCapabilityProvider implements ICapabilityProvider
     }
 
     @Override
-    public boolean hasCapability( @Nonnull Capability<?> capability, @Nullable EnumFacing side )
+    public boolean hasCapability( @Nonnull Capability<?> capability, @Nullable Direction side )
     {
         return capability == CAPABILITY_EMITTER || capability == CAPABILITY_RECEIVER;
     }
 
     @Nullable
     @Override
-    public <T> T getCapability( @Nonnull Capability<T> capability, @Nullable EnumFacing side )
+    public <T> T getCapability( @Nonnull Capability<T> capability, @Nullable Direction side )
     {
         if( capability == CAPABILITY_RECEIVER )
         {
@@ -63,7 +63,7 @@ else if( capability == CAPABILITY_EMITTER )
                 {
                     emitter = emitters[index] = () -> {
                         int flags = 0;
-                        for( EnumFacing facing : EnumFacing.VALUES ) flags |= tile.getBundledRedstoneOutput( facing );
+                        for( Direction facing : Direction.VALUES ) flags |= tile.getBundledRedstoneOutput( facing );
                         return toBytes( flags );
                     };
                 }
diff --git a/src/main/java/dan200/computercraft/shared/integration/charset/BundledRedstoneProvider.java b/src/main/java/dan200/computercraft/shared/integration/charset/BundledRedstoneProvider.java
index 1378357846..c64cc7f64c 100644
--- a/src/main/java/dan200/computercraft/shared/integration/charset/BundledRedstoneProvider.java
+++ b/src/main/java/dan200/computercraft/shared/integration/charset/BundledRedstoneProvider.java
@@ -8,7 +8,7 @@
 
 import dan200.computercraft.api.redstone.IBundledRedstoneProvider;
 import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Direction;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 
@@ -19,7 +19,7 @@
 public class BundledRedstoneProvider implements IBundledRedstoneProvider
 {
     @Override
-    public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull EnumFacing side )
+    public int getBundledRedstoneOutput( @Nonnull World world, @Nonnull BlockPos pos, @Nonnull Direction side )
     {
         TileEntity tile = world.getTileEntity( pos );
         if( tile == null || !tile.hasCapability( CAPABILITY_EMITTER, side ) ) return -1;
diff --git a/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPHooks.java b/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPHooks.java
index 35c0b47fea..5c4c810a18 100644
--- a/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPHooks.java
+++ b/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPHooks.java
@@ -9,11 +9,11 @@
 import mcmultipart.MCMultiPart;
 import mcmultipart.api.item.ItemBlockMultipart;
 import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemBlock;
 import net.minecraft.item.ItemStack;
+import net.minecraft.util.Direction;
 import net.minecraft.util.EnumActionResult;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.EnumHand;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
@@ -27,7 +27,7 @@ private MCMPHooks()
     {
     }
 
-    public static EnumActionResult onItemUse( ItemBlock itemBlock, EntityPlayer player, World world, @Nonnull BlockPos pos, @Nonnull EnumHand hand, @Nonnull EnumFacing facing, float hitX, float hitY, float hitZ )
+    public static EnumActionResult onItemUse( ItemBlock itemBlock, PlayerEntity player, World world, @Nonnull BlockPos pos, @Nonnull EnumHand hand, @Nonnull Direction facing, float hitX, float hitY, float hitZ )
     {
         if( !Loader.isModLoaded( MCMultiPart.MODID ) ) return EnumActionResult.PASS;
 
@@ -37,7 +37,7 @@ public static EnumActionResult onItemUse( ItemBlock itemBlock, EntityPlayer play
             MCMPIntegration.multipartMap.get( itemBlock.getBlock() ),
 
             (
-                ItemStack stack, EntityPlayer thisPlayer, World thisWorld, BlockPos thisPos, EnumFacing thisFacing,
+                ItemStack stack, PlayerEntity thisPlayer, World thisWorld, BlockPos thisPos, Direction thisFacing,
                 float thisX, float thisY, float thisZ, IBlockState thisState
             ) ->
                 thisPlayer.canPlayerEdit( thisPos, thisFacing, stack ) &&
diff --git a/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPIntegration.java b/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPIntegration.java
index 58bc39edd6..ec93e3aebd 100644
--- a/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPIntegration.java
+++ b/src/main/java/dan200/computercraft/shared/integration/mcmp/MCMPIntegration.java
@@ -22,7 +22,7 @@
 import mcmultipart.api.slot.EnumFaceSlot;
 import net.minecraft.block.Block;
 import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Direction;
 import net.minecraft.util.ResourceLocation;
 import net.minecraftforge.common.MinecraftForge;
 import net.minecraftforge.common.capabilities.Capability;
@@ -97,14 +97,14 @@ private static final class BasicMultipart implements ICapabilityProvider
         private BasicMultipart( TileEntity tile ) {this.tile = tile;}
 
         @Override
-        public boolean hasCapability( @Nonnull Capability<?> capability, @Nullable EnumFacing facing )
+        public boolean hasCapability( @Nonnull Capability<?> capability, @Nullable Direction facing )
         {
             return capability == MCMPCapabilities.MULTIPART_TILE;
         }
 
         @Nullable
         @Override
-        public <T> T getCapability( @Nonnull Capability<T> capability, @Nullable EnumFacing facing )
+        public <T> T getCapability( @Nonnull Capability<T> capability, @Nullable Direction facing )
         {
             if( capability == MCMPCapabilities.MULTIPART_TILE )
             {
diff --git a/src/main/java/dan200/computercraft/shared/integration/mcmp/PartAdvancedModem.java b/src/main/java/dan200/computercraft/shared/integration/mcmp/PartAdvancedModem.java
index 603ab9575d..5c6a7a4385 100644
--- a/src/main/java/dan200/computercraft/shared/integration/mcmp/PartAdvancedModem.java
+++ b/src/main/java/dan200/computercraft/shared/integration/mcmp/PartAdvancedModem.java
@@ -14,7 +14,7 @@
 import net.minecraft.block.Block;
 import net.minecraft.block.state.IBlockState;
 import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Direction;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.IBlockAccess;
 import net.minecraft.world.World;
@@ -22,7 +22,7 @@
 public class PartAdvancedModem implements IMultipart
 {
     @Override
-    public IPartSlot getSlotForPlacement( World world, BlockPos pos, IBlockState state, EnumFacing facing, float hitX, float hitY, float hitZ, EntityLivingBase placer )
+    public IPartSlot getSlotForPlacement( World world, BlockPos pos, IBlockState state, Direction facing, float hitX, float hitY, float hitZ, EntityLivingBase placer )
     {
         return EnumFaceSlot.fromFace( state.getValue( BlockAdvancedModem.FACING ) );
     }
diff --git a/src/main/java/dan200/computercraft/shared/integration/mcmp/PartNormalModem.java b/src/main/java/dan200/computercraft/shared/integration/mcmp/PartNormalModem.java
index 5e58111963..ffa5651a9c 100644
--- a/src/main/java/dan200/computercraft/shared/integration/mcmp/PartNormalModem.java
+++ b/src/main/java/dan200/computercraft/shared/integration/mcmp/PartNormalModem.java
@@ -15,7 +15,7 @@
 import net.minecraft.block.Block;
 import net.minecraft.block.state.IBlockState;
 import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Direction;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.IBlockAccess;
 import net.minecraft.world.World;
@@ -23,7 +23,7 @@
 public class PartNormalModem implements IMultipart
 {
     @Override
-    public IPartSlot getSlotForPlacement( World world, BlockPos pos, IBlockState state, EnumFacing facing, float hitX, float hitY, float hitZ, EntityLivingBase placer )
+    public IPartSlot getSlotForPlacement( World world, BlockPos pos, IBlockState state, Direction facing, float hitX, float hitY, float hitZ, EntityLivingBase placer )
     {
         return EnumFaceSlot.fromFace( getFacing( state ) );
     }
@@ -34,16 +34,16 @@ public IPartSlot getSlotFromWorld( IBlockAccess world, BlockPos pos, IBlockState
         return EnumFaceSlot.fromFace( getFacing( state ) );
     }
 
-    private EnumFacing getFacing( IBlockState state )
+    private Direction getFacing( IBlockState state )
     {
         BlockPeripheralVariant type = state.getValue( BlockPeripheral.VARIANT );
         if( type == BlockPeripheralVariant.WirelessModemUpOn || type == BlockPeripheralVariant.WirelessModemUpOff )
         {
-            return EnumFacing.UP;
+            return Direction.UP;
         }
         else if( type == BlockPeripheralVariant.WirelessModemDownOn || type == BlockPeripheralVariant.WirelessModemDownOff )
         {
-            return EnumFacing.UP;
+            return Direction.UP;
         }
         else
         {
diff --git a/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java b/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java
index 93ff17333f..521d6436a8 100644
--- a/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java
+++ b/src/main/java/dan200/computercraft/shared/media/items/ItemDisk.java
@@ -12,19 +12,16 @@
 import dan200.computercraft.api.media.IMedia;
 import dan200.computercraft.shared.common.IColouredItem;
 import dan200.computercraft.shared.util.Colour;
-import net.minecraft.client.util.ITooltipFlag;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.client.item.TooltipContext;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemGroup;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
-import net.minecraft.util.text.TextFormatting;
-import net.minecraft.world.IWorldReader;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TextFormat;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.util.DefaultedList;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -35,7 +32,7 @@ public class ItemDisk extends Item implements IMedia, IColouredItem
 {
     private static final String NBT_ID = "DiskId";
 
-    public ItemDisk( Properties settings )
+    public ItemDisk( Settings settings )
     {
         super( settings );
     }
@@ -51,9 +48,9 @@ public static ItemStack createFromIDAndColour( int id, String label, int colour
     }
 
     @Override
-    public void fillItemGroup( @Nonnull ItemGroup tabs, @Nonnull NonNullList<ItemStack> list )
+    public void appendItemsForGroup( @Nonnull ItemGroup tabs, @Nonnull DefaultedList<ItemStack> list )
     {
-        if( !isInGroup( tabs ) ) return;
+        if( !isInItemGroup( tabs ) ) return;
         for( int colour = 0; colour < 16; colour++ )
         {
             list.add( createFromIDAndColour( -1, null, Colour.VALUES[colour].getHex() ) );
@@ -61,25 +58,19 @@ public void fillItemGroup( @Nonnull ItemGroup tabs, @Nonnull NonNullList<ItemSta
     }
 
     @Override
-    public void addInformation( ItemStack stack, @Nullable World world, List<ITextComponent> list, ITooltipFlag options )
+    public void buildTooltip( ItemStack stack, @Nullable World world, List<TextComponent> list, TooltipContext options )
     {
         if( options.isAdvanced() )
         {
             int id = getDiskID( stack );
             if( id >= 0 )
             {
-                list.add( new TextComponentTranslation( "gui.computercraft.tooltip.disk_id", id )
-                    .applyTextStyle( TextFormatting.GRAY ) );
+                list.add( new TranslatableTextComponent( "gui.computercraft.tooltip.disk_id", id )
+                    .applyFormat( TextFormat.GRAY ) );
             }
         }
     }
 
-    @Override
-    public boolean doesSneakBypassUse( ItemStack stack, IWorldReader world, BlockPos pos, EntityPlayer player )
-    {
-        return true;
-    }
-
     @Override
     public String getLabel( @Nonnull ItemStack stack )
     {
@@ -91,11 +82,11 @@ public boolean setLabel( @Nonnull ItemStack stack, String label )
     {
         if( label != null )
         {
-            stack.setDisplayName( new TextComponentString( label ) );
+            stack.setDisplayName( new StringTextComponent( label ) );
         }
         else
         {
-            stack.clearCustomName();
+            stack.removeDisplayName();
         }
         return true;
     }
@@ -114,8 +105,8 @@ public IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world )
 
     public static int getDiskID( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_ID ) ? nbt.getInt( NBT_ID ) : -1;
     }
 
     private static void setDiskID( @Nonnull ItemStack stack, int id )
diff --git a/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java b/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java
index 5c12e84d22..a667925394 100644
--- a/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java
+++ b/src/main/java/dan200/computercraft/shared/media/items/ItemPrintout.java
@@ -8,16 +8,16 @@
 
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.network.Containers;
-import net.minecraft.client.util.ITooltipFlag;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.client.item.TooltipContext;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
 import net.minecraft.util.ActionResult;
-import net.minecraft.util.EnumActionResult;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
+import net.minecraft.util.Hand;
+import net.minecraft.util.TypedActionResult;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -43,25 +43,24 @@ public enum Type
 
     private final Type type;
 
-    public ItemPrintout( Properties settings, Type type )
+    public ItemPrintout( Settings settings, Type type )
     {
         super( settings );
         this.type = type;
     }
 
     @Override
-    public void addInformation( @Nonnull ItemStack stack, World world, List<ITextComponent> list, ITooltipFlag options )
+    public void buildTooltip( @Nonnull ItemStack stack, World world, List<TextComponent> list, TooltipContext options )
     {
         String title = getTitle( stack );
-        if( title != null && !title.isEmpty() ) list.add( new TextComponentString( title ) );
+        if( title != null && !title.isEmpty() ) list.add( new StringTextComponent( title ) );
     }
 
-    @Nonnull
     @Override
-    public ActionResult<ItemStack> onItemRightClick( World world, EntityPlayer player, @Nonnull EnumHand hand )
+    public TypedActionResult<ItemStack> use( World world, PlayerEntity player, Hand hand )
     {
-        if( !world.isRemote ) Containers.openPrintoutGUI( player, hand );
-        return new ActionResult<>( EnumActionResult.SUCCESS, player.getHeldItem( hand ) );
+        if( !world.isClient ) Containers.openPrintoutGUI( player, hand );
+        return new TypedActionResult<>( ActionResult.SUCCESS, player.getStackInHand( hand ) );
     }
 
     @Nonnull
@@ -73,7 +72,7 @@ private ItemStack createFromTitleAndText( String title, String[] text, String[]
         if( title != null ) stack.getOrCreateTag().putString( NBT_TITLE, title );
         if( text != null )
         {
-            NBTTagCompound tag = stack.getOrCreateTag();
+            CompoundTag tag = stack.getOrCreateTag();
             tag.putInt( NBT_PAGES, text.length / LINES_PER_PAGE );
             for( int i = 0; i < text.length; i++ )
             {
@@ -82,7 +81,7 @@ private ItemStack createFromTitleAndText( String title, String[] text, String[]
         }
         if( colours != null )
         {
-            NBTTagCompound tag = stack.getOrCreateTag();
+            CompoundTag tag = stack.getOrCreateTag();
             for( int i = 0; i < colours.length; i++ )
             {
                 if( colours[i] != null ) tag.putString( NBT_LINE_COLOUR + i, colours[i] );
@@ -118,14 +117,14 @@ public Type getType()
 
     public static String getTitle( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : null;
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : null;
     }
 
     public static int getPageCount( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_PAGES ) ? nbt.getInt( NBT_PAGES ) : 1;
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_PAGES ) ? nbt.getInt( NBT_PAGES ) : 1;
     }
 
     public static String[] getText( @Nonnull ItemStack stack )
@@ -140,7 +139,7 @@ public static String[] getColours( @Nonnull ItemStack stack )
 
     private static String[] getLines( @Nonnull ItemStack stack, String prefix )
     {
-        NBTTagCompound nbt = stack.getTag();
+        CompoundTag nbt = stack.getTag();
         int numLines = getPageCount( stack ) * LINES_PER_PAGE;
         String[] lines = new String[numLines];
         for( int i = 0; i < lines.length; i++ )
diff --git a/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java b/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java
index 106d8968e2..71d28f1bc6 100644
--- a/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java
+++ b/src/main/java/dan200/computercraft/shared/media/items/ItemTreasureDisk.java
@@ -12,17 +12,14 @@
 import dan200.computercraft.api.media.IMedia;
 import dan200.computercraft.core.filesystem.SubMount;
 import dan200.computercraft.shared.util.Colour;
-import net.minecraft.client.util.ITooltipFlag;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.client.item.TooltipContext;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemGroup;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.world.IWorldReader;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.DefaultedList;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -36,27 +33,21 @@ public class ItemTreasureDisk extends Item implements IMedia
     private static final String NBT_COLOUR = "Colour";
     private static final String NBT_SUB_PATH = "SubPath";
 
-    public ItemTreasureDisk( Properties settings )
+    public ItemTreasureDisk( Settings settings )
     {
         super( settings );
     }
 
     @Override
-    public void fillItemGroup( @Nonnull ItemGroup group, @Nonnull NonNullList<ItemStack> stacks )
+    public void appendItemsForGroup( @Nonnull ItemGroup group, @Nonnull DefaultedList<ItemStack> stacks )
     {
     }
 
     @Override
-    public void addInformation( ItemStack stack, @Nullable World world, List<ITextComponent> list, ITooltipFlag tooltipOptions )
+    public void buildTooltip( ItemStack stack, @Nullable World world, List<TextComponent> list, TooltipContext context )
     {
         String label = getTitle( stack );
-        if( !label.isEmpty() ) list.add( new TextComponentString( label ) );
-    }
-
-    @Override
-    public boolean doesSneakBypassUse( @Nonnull ItemStack stack, IWorldReader world, BlockPos pos, EntityPlayer player )
-    {
-        return true;
+        if( !label.isEmpty() ) list.add( new StringTextComponent( label ) );
     }
 
     @Override
@@ -94,7 +85,7 @@ else if( rootTreasure.exists( "deprecated/" + subPath ) )
     public static ItemStack create( String subPath, int colourIndex )
     {
         ItemStack result = new ItemStack( ComputerCraft.Items.treasureDisk );
-        NBTTagCompound nbt = result.getOrCreateTag();
+        CompoundTag nbt = result.getOrCreateTag();
         nbt.putString( NBT_SUB_PATH, subPath );
 
         int slash = subPath.indexOf( '/' );
@@ -121,20 +112,20 @@ private static IMount getTreasureMount()
     @Nonnull
     private static String getTitle( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : "'alongtimeago' by dan200";
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_TITLE ) ? nbt.getString( NBT_TITLE ) : "'alongtimeago' by dan200";
     }
 
     @Nonnull
     private static String getSubPath( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_SUB_PATH ) ? nbt.getString( NBT_SUB_PATH ) : "dan200/alongtimeago";
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_SUB_PATH ) ? nbt.getString( NBT_SUB_PATH ) : "dan200/alongtimeago";
     }
 
     public static int getColour( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : Colour.Blue.getHex();
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : Colour.Blue.getHex();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java b/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java
index c6341bc61f..17f139ea3e 100644
--- a/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java
+++ b/src/main/java/dan200/computercraft/shared/media/items/RecordMedia.java
@@ -8,9 +8,9 @@
 
 import dan200.computercraft.api.media.IMedia;
 import dan200.computercraft.shared.util.RecordUtil;
-import net.minecraft.item.ItemRecord;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.SoundEvent;
+import net.minecraft.item.MusicDiscItem;
+import net.minecraft.sound.SoundEvent;
 
 import javax.annotation.Nonnull;
 
@@ -40,6 +40,6 @@ public String getAudioTitle( @Nonnull ItemStack stack )
     @Override
     public SoundEvent getAudio( @Nonnull ItemStack stack )
     {
-        return ((ItemRecord) stack.getItem()).getSound();
+        return ((MusicDiscItem) stack.getItem()).getSound();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java
index f28bb8f2f5..9fad5439bc 100644
--- a/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/media/recipes/DiskRecipe.java
@@ -6,44 +6,42 @@
 
 package dan200.computercraft.shared.media.recipes;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.media.items.ItemDisk;
-import dan200.computercraft.shared.util.AbstractRecipe;
 import dan200.computercraft.shared.util.Colour;
 import dan200.computercraft.shared.util.ColourTracker;
 import dan200.computercraft.shared.util.ColourUtils;
-import net.minecraft.init.Items;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.item.EnumDyeColor;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.item.Items;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.SpecialRecipeSerializer;
+import net.minecraft.recipe.crafting.SpecialCraftingRecipe;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
-import net.minecraftforge.common.Tags;
 
 import javax.annotation.Nonnull;
 
-public class DiskRecipe extends AbstractRecipe
+public class DiskRecipe extends SpecialCraftingRecipe
 {
-    private final Ingredient paper = Ingredient.fromItems( Items.PAPER );
-    private final Ingredient redstone = Ingredient.fromTag( Tags.Items.DUSTS_REDSTONE );
+    private final Ingredient paper = Ingredient.ofItems( Items.PAPER );
+    private final Ingredient redstone = Ingredient.ofItems( Items.REDSTONE );
 
-    public DiskRecipe( ResourceLocation id )
+    public DiskRecipe( Identifier id )
     {
         super( id );
     }
 
     @Override
-    public boolean matches( @Nonnull IInventory inv, @Nonnull World world )
+    public boolean matches( @Nonnull CraftingInventory inv, @Nonnull World world )
     {
         boolean paperFound = false;
         boolean redstoneFound = false;
 
-        for( int i = 0; i < inv.getSizeInventory(); i++ )
+        for( int i = 0; i < inv.getInvSize(); i++ )
         {
-            ItemStack stack = inv.getStackInSlot( i );
+            ItemStack stack = inv.getInvStack( i );
 
             if( !stack.isEmpty() )
             {
@@ -69,19 +67,19 @@ else if( ColourUtils.getStackColour( stack ) != null )
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inv )
+    public ItemStack craft( @Nonnull CraftingInventory inv )
     {
         ColourTracker tracker = new ColourTracker();
 
-        for( int i = 0; i < inv.getSizeInventory(); i++ )
+        for( int i = 0; i < inv.getInvSize(); i++ )
         {
-            ItemStack stack = inv.getStackInSlot( i );
+            ItemStack stack = inv.getInvStack( i );
 
             if( stack.isEmpty() ) continue;
 
             if( !paper.test( stack ) && !redstone.test( stack ) )
             {
-                EnumDyeColor dye = ColourUtils.getStackColour( stack );
+                DyeColor dye = ColourUtils.getStackColour( stack );
                 if( dye == null ) continue;
 
                 Colour colour = Colour.VALUES[dye.getId()];
@@ -93,26 +91,24 @@ public ItemStack getCraftingResult( @Nonnull IInventory inv )
     }
 
     @Override
-    public boolean canFit( int x, int y )
+    public boolean fits( int x, int y )
     {
         return x >= 2 && y >= 2;
     }
 
     @Nonnull
     @Override
-    public ItemStack getRecipeOutput()
+    public ItemStack getOutput()
     {
         return ItemDisk.createFromIDAndColour( -1, null, Colour.Blue.getHex() );
     }
 
     @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    public static final IRecipeSerializer<DiskRecipe> SERIALIZER = new RecipeSerializers.SimpleSerializer<>(
-        ComputerCraft.MOD_ID + ":disk", DiskRecipe::new
-    );
+    public static final RecipeSerializer<DiskRecipe> SERIALIZER = new SpecialRecipeSerializer<>( DiskRecipe::new );
 }
diff --git a/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java b/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java
index 12c7c3a161..b9e502ff70 100644
--- a/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/media/recipes/PrintoutRecipe.java
@@ -6,53 +6,52 @@
 
 package dan200.computercraft.shared.media.recipes;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.media.items.ItemPrintout;
-import dan200.computercraft.shared.util.AbstractRecipe;
-import net.minecraft.init.Items;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.item.Items;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.SpecialRecipeSerializer;
+import net.minecraft.recipe.crafting.SpecialCraftingRecipe;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
-public final class PrintoutRecipe extends AbstractRecipe
+public final class PrintoutRecipe extends SpecialCraftingRecipe
 {
-    private final Ingredient paper = Ingredient.fromItems( Items.PAPER );
-    private final Ingredient leather = Ingredient.fromItems( Items.LEATHER );
-    private final Ingredient string = Ingredient.fromItems( Items.STRING );
+    private final Ingredient paper = Ingredient.ofItems( Items.PAPER );
+    private final Ingredient leather = Ingredient.ofItems( Items.LEATHER );
+    private final Ingredient string = Ingredient.ofItems( Items.STRING );
 
-    private PrintoutRecipe( ResourceLocation id )
+    public PrintoutRecipe( Identifier id )
     {
         super( id );
     }
 
     @Override
-    public boolean canFit( int x, int y )
+    public boolean fits( int x, int y )
     {
         return x >= 3 && y >= 3;
     }
 
     @Nonnull
     @Override
-    public ItemStack getRecipeOutput()
+    public ItemStack getOutput()
     {
         return ItemPrintout.createMultipleFromTitleAndText( null, null, null );
     }
 
     @Override
-    public boolean matches( @Nonnull IInventory inventory, @Nonnull World world )
+    public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world )
     {
-        return !getCraftingResult( inventory ).isEmpty();
+        return !craft( inventory ).isEmpty();
     }
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inventory )
+    public ItemStack craft( @Nonnull CraftingInventory inventory )
     {
         // See if we match the recipe, and extract the input disk ID and dye colour
         int numPages = 0;
@@ -65,7 +64,7 @@ public ItemStack getCraftingResult( @Nonnull IInventory inventory )
         {
             for( int x = 0; x < inventory.getWidth(); x++ )
             {
-                ItemStack stack = inventory.getStackInSlot( x + y * inventory.getWidth() );
+                ItemStack stack = inventory.getInvStack( x + y * inventory.getWidth() );
                 if( !stack.isEmpty() )
                 {
                     if( stack.getItem() instanceof ItemPrintout && ((ItemPrintout) stack.getItem()).getType() != ItemPrintout.Type.BOOK )
@@ -160,12 +159,10 @@ else if( leather.test( stack ) && !leatherFound )
 
     @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    public static final IRecipeSerializer<?> SERIALIZER = new RecipeSerializers.SimpleSerializer<>(
-        ComputerCraft.MOD_ID + ":printout", PrintoutRecipe::new
-    );
+    public static final RecipeSerializer<?> SERIALIZER = new SpecialRecipeSerializer<>( PrintoutRecipe::new );
 }
diff --git a/src/main/java/dan200/computercraft/shared/mixed/MixedFirstPersonRenderer.java b/src/main/java/dan200/computercraft/shared/mixed/MixedFirstPersonRenderer.java
new file mode 100644
index 0000000000..d5c065f27d
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/mixed/MixedFirstPersonRenderer.java
@@ -0,0 +1,18 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.mixed;
+
+import net.minecraft.util.AbsoluteHand;
+
+public interface MixedFirstPersonRenderer
+{
+    void renderArms_CC();
+
+    void renderArmFirstPerson_CC( float equip, float swing, AbsoluteHand hand );
+
+    float getMapAngleFromPitch_CC( float pitch );
+}
diff --git a/src/main/java/dan200/computercraft/shared/mixin/MixinFirstPersonRenderer.java b/src/main/java/dan200/computercraft/shared/mixin/MixinFirstPersonRenderer.java
new file mode 100644
index 0000000000..a490ff96cf
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/mixin/MixinFirstPersonRenderer.java
@@ -0,0 +1,80 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.mixin;
+
+import dan200.computercraft.client.render.ItemPocketRenderer;
+import dan200.computercraft.client.render.ItemPrintoutRenderer;
+import dan200.computercraft.shared.media.items.ItemPrintout;
+import dan200.computercraft.shared.mixed.MixedFirstPersonRenderer;
+import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.client.render.FirstPersonRenderer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.AbsoluteHand;
+import net.minecraft.util.Hand;
+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;
+
+@Mixin( FirstPersonRenderer.class )
+public class MixinFirstPersonRenderer implements MixedFirstPersonRenderer
+{
+    @Shadow
+    private float getMapAngle ( float pitch )
+    {
+        return 0;
+    }
+
+    @Shadow
+    private void renderArms()
+    {
+    }
+
+    @Shadow
+    private void renderArmHoldingItem( float equip, float swing, AbsoluteHand hand )
+    {
+    }
+
+    @Override
+    public void renderArms_CC()
+    {
+        renderArms();
+    }
+
+    @Override
+    public void renderArmFirstPerson_CC( float equip, float swing, AbsoluteHand hand )
+    {
+        renderArmHoldingItem( equip, swing, hand );
+    }
+
+    @Override
+    public float getMapAngleFromPitch_CC( float pitch )
+    {
+        return getMapAngle( pitch );
+    }
+
+    @Inject(
+        method = "renderFirstPersonItem(Lnet/minecraft/client/network/AbstractClientPlayerEntity;FFLnet/minecraft/util/Hand;FLnet/minecraft/item/ItemStack;F)V",
+        at = @At( "HEAD" ),
+        cancellable = true
+    )
+    public void renderFirstPersonItem_Injected( AbstractClientPlayerEntity player, float var2, float pitch, Hand hand, float swingProgress, ItemStack stack, float equipProgress, CallbackInfo callback )
+    {
+        if( stack.getItem() instanceof ItemPrintout )
+        {
+            ItemPrintoutRenderer.INSTANCE.renderItemFirstPerson( hand, pitch, equipProgress, swingProgress, stack );
+            callback.cancel();
+        }
+        else if( stack.getItem() instanceof ItemPocketComputer )
+        {
+            ItemPocketRenderer.INSTANCE.renderItemFirstPerson( hand, pitch, equipProgress, swingProgress, stack );
+            callback.cancel();
+        }
+    }
+}
diff --git a/src/main/java/dan200/computercraft/shared/mixin/MixinItemFrameEntityRenderer.java b/src/main/java/dan200/computercraft/shared/mixin/MixinItemFrameEntityRenderer.java
new file mode 100644
index 0000000000..7e3a284a3e
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/mixin/MixinItemFrameEntityRenderer.java
@@ -0,0 +1,32 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.mixin;
+
+import dan200.computercraft.client.render.ItemPrintoutRenderer;
+import dan200.computercraft.shared.media.items.ItemPrintout;
+import net.minecraft.client.render.entity.ItemFrameEntityRenderer;
+import net.minecraft.entity.decoration.ItemFrameEntity;
+import net.minecraft.item.ItemStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin( ItemFrameEntityRenderer.class )
+public class MixinItemFrameEntityRenderer
+{
+    @Inject( method = "method_3992", at = @At( "HEAD" ), cancellable = true )
+    private void method_3992_Injected( ItemFrameEntity entity, CallbackInfo info )
+    {
+        ItemStack stack = entity.getHeldItemStack();
+        if( stack.getItem() instanceof ItemPrintout )
+        {
+            ItemPrintoutRenderer.INSTANCE.renderInFrame( entity, stack );
+            info.cancel();
+        }
+    }
+}
diff --git a/src/main/java/dan200/computercraft/shared/mixin/MixinMinecraftGame.java b/src/main/java/dan200/computercraft/shared/mixin/MixinMinecraftGame.java
new file mode 100644
index 0000000000..c786a5b320
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/mixin/MixinMinecraftGame.java
@@ -0,0 +1,27 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.mixin;
+
+import dan200.computercraft.client.FrameInfo;
+import net.minecraft.client.MinecraftClient;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin( MinecraftClient.class )
+public abstract class MixinMinecraftGame
+{
+    /**
+     * @see MinecraftClient#render(boolean)
+     */
+    @Inject( method = "render", at = @At( "HEAD" ) )
+    private void onRender( CallbackInfo info )
+    {
+        FrameInfo.onRenderFrame();
+    }
+}
diff --git a/src/main/java/dan200/computercraft/shared/mixin/MixinScreen.java b/src/main/java/dan200/computercraft/shared/mixin/MixinScreen.java
new file mode 100644
index 0000000000..b2948bbae9
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/mixin/MixinScreen.java
@@ -0,0 +1,24 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.mixin;
+
+import dan200.computercraft.shared.command.CommandCopy;
+import net.minecraft.client.gui.Screen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin( Screen.class )
+public class MixinScreen
+{
+    @Inject( method = "sendMessage(Ljava/lang/String;Z)V", at = @At( "HEAD" ), cancellable = true )
+    public void sendClientCommand( String message, boolean add, CallbackInfo info )
+    {
+        if( CommandCopy.onClientSendMessage( message ) ) info.cancel();
+    }
+}
diff --git a/src/main/java/dan200/computercraft/shared/network/Containers.java b/src/main/java/dan200/computercraft/shared/network/Containers.java
index b59b45d16d..e08565055f 100644
--- a/src/main/java/dan200/computercraft/shared/network/Containers.java
+++ b/src/main/java/dan200/computercraft/shared/network/Containers.java
@@ -22,10 +22,10 @@
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
 import dan200.computercraft.shared.turtle.blocks.TileTurtle;
 import dan200.computercraft.shared.turtle.inventory.ContainerTurtle;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumHand;
+import net.minecraft.util.Hand;
 
 public final class Containers
 {
@@ -33,64 +33,70 @@ private Containers()
     {
     }
 
-    public static void openDiskDriveGUI( EntityPlayer player, TileDiskDrive drive )
+    public static void openDiskDriveGUI( PlayerEntity player, TileDiskDrive drive )
     {
         TileEntityContainerType.diskDrive( drive.getPos() ).open( player );
     }
 
-    public static void openComputerGUI( EntityPlayer player, TileComputer computer )
+    public static void openComputerGUI( PlayerEntity player, TileComputer computer )
     {
+        computer.createServerComputer().sendTerminalState( player );
         TileEntityContainerType.computer( computer.getPos() ).open( player );
     }
 
-    public static void openPrinterGUI( EntityPlayer player, TilePrinter printer )
+    public static void openPrinterGUI( PlayerEntity player, TilePrinter printer )
     {
         TileEntityContainerType.printer( printer.getPos() ).open( player );
     }
 
-    public static void openTurtleGUI( EntityPlayer player, TileTurtle turtle )
+    public static void openTurtleGUI( PlayerEntity player, TileTurtle turtle )
     {
+        turtle.createServerComputer().sendTerminalState( player );
         TileEntityContainerType.turtle( turtle.getPos() ).open( player );
     }
 
-    public static void openPrintoutGUI( EntityPlayer player, EnumHand hand )
+    public static void openPrintoutGUI( PlayerEntity player, Hand hand )
     {
-        ItemStack stack = player.getHeldItem( hand );
+        ItemStack stack = player.getStackInHand( hand );
         Item item = stack.getItem();
         if( !(item instanceof ItemPrintout) ) return;
 
         new PrintoutContainerType( hand ).open( player );
     }
 
-    public static void openPocketComputerGUI( EntityPlayer player, EnumHand hand )
+    public static void openPocketComputerGUI( PlayerEntity player, Hand hand )
     {
-        ItemStack stack = player.getHeldItem( hand );
+        ItemStack stack = player.getStackInHand( hand );
         Item item = stack.getItem();
         if( !(item instanceof ItemPocketComputer) ) return;
 
+        ServerComputer computer = ItemPocketComputer.getServerComputer( stack );
+        if( computer != null ) computer.sendTerminalState( player );
+
         new PocketComputerContainerType( hand ).open( player );
     }
 
-    public static void openComputerGUI( EntityPlayer player, ServerComputer computer )
+    public static void openComputerGUI( PlayerEntity player, ServerComputer computer )
     {
+        computer.sendTerminalState( player );
         new ViewComputerContainerType( computer ).open( player );
     }
 
     public static void setup()
     {
-        ContainerType.register( TileEntityContainerType::computer, ( packet, player ) ->
-            new ContainerComputer( (TileComputer) packet.getTileEntity( player ) ) );
-        ContainerType.register( TileEntityContainerType::turtle, ( packet, player ) -> {
+        ContainerType.register( TileEntityContainerType::computer, ( id, packet, player ) ->
+            new ContainerComputer( id, (TileComputer) packet.getTileEntity( player ) ) );
+        ContainerType.register( TileEntityContainerType::turtle, ( id, packet, player ) -> {
             TileTurtle turtle = (TileTurtle) packet.getTileEntity( player );
-            return new ContainerTurtle( player.inventory, turtle.getAccess(), turtle.getServerComputer() );
+            return new ContainerTurtle( id, player.inventory, turtle.getAccess(), turtle.getServerComputer() );
         } );
-        ContainerType.register( TileEntityContainerType::diskDrive, ( packet, player ) ->
-            new ContainerDiskDrive( player.inventory, (TileDiskDrive) packet.getTileEntity( player ) ) );
-        ContainerType.register( TileEntityContainerType::printer, ( packet, player ) ->
-            new ContainerPrinter( player.inventory, (TilePrinter) packet.getTileEntity( player ) ) );
+        ContainerType.register( TileEntityContainerType::diskDrive, ( id, packet, player ) ->
+            new ContainerDiskDrive( id, player.inventory, (TileDiskDrive) packet.getTileEntity( player ) ) );
+        ContainerType.register( TileEntityContainerType::printer, ( id, packet, player ) ->
+            new ContainerPrinter( id, player.inventory, (TilePrinter) packet.getTileEntity( player ) ) );
 
-        ContainerType.register( PocketComputerContainerType::new, ( packet, player ) -> new ContainerPocketComputer( player, packet.hand ) );
-        ContainerType.register( PrintoutContainerType::new, ( packet, player ) -> new ContainerHeldItem( player, packet.hand ) );
-        ContainerType.register( ViewComputerContainerType::new, ( packet, player ) -> new ContainerViewComputer( ComputerCraft.serverComputerRegistry.get( packet.instanceId ) ) );
+        ContainerType.register( PocketComputerContainerType::new, ( id, packet, player ) -> new ContainerPocketComputer( id, player, packet.hand ) );
+        ContainerType.register( PrintoutContainerType::new, ( id, packet, player ) -> new ContainerHeldItem( id, player, packet.hand ) );
+        ContainerType.register( ViewComputerContainerType::new, ( id, packet, player ) -> new ContainerViewComputer( id, ComputerCraft.serverComputerRegistry.get( packet.instanceId ) ) );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java
index 4afe60ce5d..a33a6764c3 100644
--- a/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java
+++ b/src/main/java/dan200/computercraft/shared/network/NetworkHandler.java
@@ -9,24 +9,35 @@
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.network.client.*;
 import dan200.computercraft.shared.network.server.*;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.ResourceLocation;
+import io.netty.buffer.Unpooled;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import net.fabricmc.fabric.api.network.ClientSidePacketRegistry;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.fabricmc.fabric.api.network.ServerSidePacketRegistry;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.packet.CustomPayloadS2CPacket;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.network.packet.CustomPayloadC2SPacket;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.fml.network.NetworkDirection;
-import net.minecraftforge.fml.network.NetworkEvent;
-import net.minecraftforge.fml.network.NetworkRegistry;
-import net.minecraftforge.fml.network.simple.SimpleChannel;
-import net.minecraftforge.fml.server.ServerLifecycleHooks;
 
+import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
 
 public final class NetworkHandler
 {
-    public static SimpleChannel network;
+    private static final Int2ObjectMap<BiConsumer<PacketContext, PacketByteBuf>> packetReaders = new Int2ObjectOpenHashMap<>();
+    private static final Object2IntMap<Class<?>> packetIds = new Object2IntOpenHashMap<>();
+
+    private static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "main" );
 
     private NetworkHandler()
     {
@@ -34,11 +45,8 @@ private NetworkHandler()
 
     public static void setup()
     {
-        String version = ComputerCraft.getVersion();
-        network = NetworkRegistry.ChannelBuilder.named( new ResourceLocation( ComputerCraft.MOD_ID, "network" ) )
-            .networkProtocolVersion( () -> version )
-            .clientAcceptedVersions( version::equals ).serverAcceptedVersions( version::equals )
-            .simpleChannel();
+        ClientSidePacketRegistry.INSTANCE.register( ID, NetworkHandler::receive );
+        ServerSidePacketRegistry.INSTANCE.register( ID, NetworkHandler::receive );
 
         // Server messages
         registerMainThread( 0, ComputerActionServerMessage::new );
@@ -55,35 +63,45 @@ public static void setup()
         registerMainThread( 14, PlayRecordClientMessage.class, PlayRecordClientMessage::new );
     }
 
-    public static void sendToPlayer( EntityPlayer player, NetworkMessage packet )
+    public static void sendToPlayer( PlayerEntity player, NetworkMessage packet )
     {
-        network.sendTo( packet, ((EntityPlayerMP) player).connection.netManager, NetworkDirection.PLAY_TO_CLIENT );
+        ((ServerPlayerEntity) player).networkHandler.sendPacket(
+            new CustomPayloadS2CPacket( ID, encode( packet ) )
+        );
     }
 
-    public static void sendToAllPlayers( NetworkMessage packet )
+    public static void sendToAllPlayers( MinecraftServer server, NetworkMessage packet )
     {
-        for( EntityPlayerMP player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers() )
-        {
-            sendToPlayer( player, packet );
-        }
+        server.getPlayerManager().sendToAll( new CustomPayloadS2CPacket( ID, encode( packet ) ) );
     }
 
     public static void sendToServer( NetworkMessage packet )
     {
-        network.sendToServer( packet );
+        MinecraftClient.getInstance().player.networkHandler.sendPacket(
+            new CustomPayloadC2SPacket( ID, encode( packet ) )
+        );
     }
 
     public static void sendToAllAround( NetworkMessage packet, World world, Vec3d pos, double range )
     {
-        for( EntityPlayerMP player : ServerLifecycleHooks.getCurrentServer().getPlayerList().getPlayers() )
-        {
-            if( player.getEntityWorld() != world ) continue;
-
-            double x = pos.x - player.posX;
-            double y = pos.y - player.posY;
-            double z = pos.z - player.posZ;
-            if( x * x + y * y + z * z < range * range ) sendToPlayer( player, packet );
-        }
+        world.getServer().getPlayerManager().sendToAround(
+            null, pos.x, pos.y, pos.z, range, world.getDimension().getType(),
+            new CustomPayloadS2CPacket( ID, encode( packet ) )
+        );
+    }
+
+    private static void receive( PacketContext context, PacketByteBuf buffer )
+    {
+        int type = buffer.readByte();
+        packetReaders.get( type ).accept( context, buffer );
+    }
+
+    private static PacketByteBuf encode( NetworkMessage message )
+    {
+        PacketByteBuf buf = new PacketByteBuf( Unpooled.buffer() );
+        buf.writeByte( packetIds.getInt( message.getClass() ) );
+        message.toBytes( buf );
+        return buf;
     }
 
     /**
@@ -109,17 +127,13 @@ private static <T extends NetworkMessage> void registerMainThread( int id, Suppl
      * @param id      The identifier for this packet type
      * @param decoder The factory for this type of packet.
      */
-    private static <T extends NetworkMessage> void registerMainThread( int id, Class<T> type, Function<PacketBuffer, T> decoder )
+    private static <T extends NetworkMessage> void registerMainThread( int id, Class<T> type, Function<PacketByteBuf, T> decoder )
     {
-        network.messageBuilder( type, id )
-            .encoder( NetworkMessage::toBytes )
-            .decoder( decoder )
-            .consumer( ( packet, contextSup ) -> {
-                NetworkEvent.Context context = contextSup.get();
-                context.enqueueWork( () -> packet.handle( context ) );
-                context.setPacketHandled( true );
-            } )
-            .add();
+        packetIds.put( type, id );
+        packetReaders.put( id, ( context, buf ) -> {
+            T result = decoder.apply( buf );
+            context.getTaskQueue().execute( () -> result.handle( context ) );
+        } );
     }
 
     @SuppressWarnings( "unchecked" )
diff --git a/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java b/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java
index e99b9ad73d..e35cbeee48 100644
--- a/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/NetworkMessage.java
@@ -6,8 +6,8 @@
 
 package dan200.computercraft.shared.network;
 
-import net.minecraft.network.PacketBuffer;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -16,6 +16,7 @@
  *
  * @see dan200.computercraft.shared.network.client
  * @see dan200.computercraft.shared.network.server
+ * @see net.fabricmc.fabric.api.network.PacketRegistry
  */
 public interface NetworkMessage
 {
@@ -26,7 +27,7 @@ public interface NetworkMessage
      *
      * @param buf The buffer to write data to.
      */
-    void toBytes( @Nonnull PacketBuffer buf );
+    void toBytes( @Nonnull PacketByteBuf buf );
 
     /**
      * Read this packet from a buffer.
@@ -35,7 +36,7 @@ public interface NetworkMessage
      *
      * @param buf The buffer to read data from.
      */
-    default void fromBytes( @Nonnull PacketBuffer buf )
+    default void fromBytes( @Nonnull PacketByteBuf buf )
     {
         throw new IllegalStateException( "Should have been registered using a \"from bytes\" method" );
     }
@@ -45,5 +46,5 @@ default void fromBytes( @Nonnull PacketBuffer buf )
      *
      * @param context The context with which to handle this message
      */
-    void handle( NetworkEvent.Context context );
+    void handle( PacketContext context );
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java
index a3731c7748..d99c7a0836 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/ChatTableClientMessage.java
@@ -9,11 +9,11 @@
 import dan200.computercraft.client.ClientTableFormatter;
 import dan200.computercraft.shared.command.text.TableBuilder;
 import dan200.computercraft.shared.network.NetworkMessage;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -32,34 +32,34 @@ public ChatTableClientMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         buf.writeVarInt( table.getId() );
         buf.writeVarInt( table.getColumns() );
         buf.writeBoolean( table.getHeaders() != null );
         if( table.getHeaders() != null )
         {
-            for( ITextComponent header : table.getHeaders() ) buf.writeTextComponent( header );
+            for( TextComponent header : table.getHeaders() ) buf.writeTextComponent( header );
         }
 
         buf.writeVarInt( table.getRows().size() );
-        for( ITextComponent[] row : table.getRows() )
+        for( TextComponent[] row : table.getRows() )
         {
-            for( ITextComponent column : row ) buf.writeTextComponent( column );
+            for( TextComponent column : row ) buf.writeTextComponent( column );
         }
 
         buf.writeVarInt( table.getAdditional() );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         int id = buf.readVarInt();
         int columns = buf.readVarInt();
         TableBuilder table;
         if( buf.readBoolean() )
         {
-            ITextComponent[] headers = new ITextComponent[columns];
+            TextComponent[] headers = new TextComponent[columns];
             for( int i = 0; i < columns; i++ ) headers[i] = buf.readTextComponent();
             table = new TableBuilder( id, headers );
         }
@@ -71,7 +71,7 @@ public void fromBytes( @Nonnull PacketBuffer buf )
         int rows = buf.readVarInt();
         for( int i = 0; i < rows; i++ )
         {
-            ITextComponent[] row = new ITextComponent[columns];
+            TextComponent[] row = new TextComponent[columns];
             for( int j = 0; j < columns; j++ ) row[j] = buf.readTextComponent();
             table.row( row );
         }
@@ -81,8 +81,8 @@ public void fromBytes( @Nonnull PacketBuffer buf )
     }
 
     @Override
-    @OnlyIn( Dist.CLIENT )
-    public void handle( NetworkEvent.Context context )
+    @Environment( EnvType.CLIENT )
+    public void handle( PacketContext context )
     {
         ClientTableFormatter.INSTANCE.display( table );
     }
diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java
index ac97c51f18..b2ddb693f5 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerClientMessage.java
@@ -9,7 +9,7 @@
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.computer.core.ClientComputer;
 import dan200.computercraft.shared.network.NetworkMessage;
-import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -35,13 +35,13 @@ public int getInstanceId()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         buf.writeVarInt( instanceId );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         instanceId = buf.readVarInt();
     }
diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java
index 77cf5605e9..9933af4bc8 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerDataClientMessage.java
@@ -8,9 +8,9 @@
 
 import dan200.computercraft.shared.computer.core.ComputerState;
 import dan200.computercraft.shared.computer.core.ServerComputer;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.network.PacketBuffer;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -20,7 +20,7 @@
 public class ComputerDataClientMessage extends ComputerClientMessage
 {
     private ComputerState state;
-    private NBTTagCompound userData;
+    private CompoundTag userData;
 
     public ComputerDataClientMessage( ServerComputer computer )
     {
@@ -34,23 +34,23 @@ public ComputerDataClientMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         super.toBytes( buf );
-        buf.writeEnumValue( state );
+        buf.writeEnumConstant( state );
         buf.writeCompoundTag( userData );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         super.fromBytes( buf );
-        state = buf.readEnumValue( ComputerState.class );
+        state = buf.readEnumConstant( ComputerState.class );
         userData = buf.readCompoundTag();
     }
 
     @Override
-    public void handle( NetworkEvent.Context context )
+    public void handle( PacketContext context )
     {
         getComputer().setState( state, userData );
     }
diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java
index b2dcb4285c..314bea7cf1 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerDeletedClientMessage.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.network.client;
 
 import dan200.computercraft.ComputerCraft;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.fabric.api.network.PacketContext;
 
 public class ComputerDeletedClientMessage extends ComputerClientMessage
 {
@@ -21,7 +21,7 @@ public ComputerDeletedClientMessage()
     }
 
     @Override
-    public void handle( NetworkEvent.Context context )
+    public void handle( PacketContext context )
     {
         ComputerCraft.clientComputerRegistry.remove( getInstanceId() );
     }
diff --git a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java
index 914101143a..bc1b6ec952 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/ComputerTerminalClientMessage.java
@@ -6,17 +6,17 @@
 
 package dan200.computercraft.shared.network.client;
 
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.network.PacketBuffer;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
 public class ComputerTerminalClientMessage extends ComputerClientMessage
 {
-    private NBTTagCompound tag;
+    private CompoundTag tag;
 
-    public ComputerTerminalClientMessage( int instanceId, NBTTagCompound tag )
+    public ComputerTerminalClientMessage( int instanceId, CompoundTag tag )
     {
         super( instanceId );
         this.tag = tag;
@@ -27,21 +27,21 @@ public ComputerTerminalClientMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         super.toBytes( buf );
         buf.writeCompoundTag( tag ); // TODO: Do we need to compress this?
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         super.fromBytes( buf );
         tag = buf.readCompoundTag();
     }
 
     @Override
-    public void handle( NetworkEvent.Context context )
+    public void handle( PacketContext context )
     {
         getComputer().readDescription( tag );
     }
diff --git a/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java b/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java
index 737468d092..d385f29ab3 100644
--- a/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/client/PlayRecordClientMessage.java
@@ -7,14 +7,14 @@
 package dan200.computercraft.shared.network.client;
 
 import dan200.computercraft.shared.network.NetworkMessage;
-import net.minecraft.client.Minecraft;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.SoundEvent;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.PacketByteBuf;
 import net.minecraft.util.math.BlockPos;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
-import net.minecraftforge.fml.network.NetworkEvent;
-import net.minecraftforge.registries.ForgeRegistries;
+import net.minecraft.util.registry.Registry;
 
 import javax.annotation.Nonnull;
 import java.util.Objects;
@@ -46,13 +46,13 @@ public PlayRecordClientMessage( BlockPos pos )
         soundEvent = null;
     }
 
-    public PlayRecordClientMessage( PacketBuffer buf )
+    public PlayRecordClientMessage( PacketByteBuf buf )
     {
         pos = buf.readBlockPos();
         if( buf.readBoolean() )
         {
             name = buf.readString( Short.MAX_VALUE );
-            soundEvent = ForgeRegistries.SOUND_EVENTS.getValue( buf.readResourceLocation() );
+            soundEvent = Registry.SOUND_EVENT.get( buf.readIdentifier() );
         }
         else
         {
@@ -62,7 +62,7 @@ public PlayRecordClientMessage( PacketBuffer buf )
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         buf.writeBlockPos( pos );
         if( soundEvent == null )
@@ -73,16 +73,16 @@ public void toBytes( @Nonnull PacketBuffer buf )
         {
             buf.writeBoolean( true );
             buf.writeString( name );
-            buf.writeResourceLocation( Objects.requireNonNull( soundEvent.getRegistryName(), "Sound is not registered" ) );
+            buf.writeIdentifier( Objects.requireNonNull( soundEvent.getId(), "Sound is not registered" ) );
         }
     }
 
     @Override
-    @OnlyIn( Dist.CLIENT )
-    public void handle( NetworkEvent.Context context )
+    @Environment( EnvType.CLIENT )
+    public void handle( PacketContext context )
     {
-        Minecraft mc = Minecraft.getInstance();
-        mc.world.playRecord( pos, soundEvent );
-        if( name != null ) mc.ingameGUI.setRecordPlayingMessage( name );
+        MinecraftClient mc = MinecraftClient.getInstance();
+        mc.worldRenderer.playSong( soundEvent, pos );
+        if( name != null ) mc.inGameHud.setRecordPlayingOverlay( name );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/container/ContainerType.java b/src/main/java/dan200/computercraft/shared/network/container/ContainerType.java
index 27ca786810..57e51c9777 100644
--- a/src/main/java/dan200/computercraft/shared/network/container/ContainerType.java
+++ b/src/main/java/dan200/computercraft/shared/network/container/ContainerType.java
@@ -6,99 +6,60 @@
 
 package dan200.computercraft.shared.network.container;
 
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.entity.player.InventoryPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.world.IInteractionObject;
-import net.minecraftforge.fml.network.NetworkHooks;
+import net.fabricmc.fabric.api.client.screen.ScreenProviderRegistry;
+import net.fabricmc.fabric.api.container.ContainerProviderRegistry;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.ContainerScreen;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.function.BiFunction;
-import java.util.function.Function;
 import java.util.function.Supplier;
 
-/**
- * A horrible hack to allow opening GUIs until Forge adds a built-in system.
- */
-public interface ContainerType<T extends Container> extends IInteractionObject
+public interface ContainerType<T extends Container>
 {
     @Nonnull
-    ResourceLocation getId();
-
-    void toBytes( PacketBuffer buf );
-
-    void fromBytes( PacketBuffer buf );
-
-    @Nonnull
-    @Override
-    @SuppressWarnings( "unchecked" )
-    default T createContainer( @Nonnull InventoryPlayer inventoryPlayer, @Nonnull EntityPlayer entityPlayer )
-    {
-        return ((BiFunction<ContainerType<T>, EntityPlayer, T>) containerFactories.get( getId() )).apply( this, entityPlayer );
-    }
-
-    @Nonnull
-    @Override
-    default String getGuiID()
-    {
-        return getId().toString();
-    }
+    Identifier getId();
 
-    @Nonnull
-    @Override
-    default ITextComponent getName()
-    {
-        return new TextComponentString( "" );
-    }
+    void toBytes( PacketByteBuf buf );
 
-    @Override
-    default boolean hasCustomName()
-    {
-        return false;
-    }
+    void fromBytes( PacketByteBuf buf );
 
-    @Nullable
-    @Override
-    default ITextComponent getCustomName()
+    default void open( PlayerEntity player )
     {
-        return null;
+        ContainerProviderRegistry.INSTANCE.openContainer( getId(), player, this::toBytes );
     }
 
-    default void open( EntityPlayer player )
+    static <C extends Container, T extends ContainerType<C>> void register( Supplier<T> containerType, ContainerFactory<T, C> factory )
     {
-        NetworkHooks.openGui( (EntityPlayerMP) player, this, this::toBytes );
+        ContainerProviderRegistry.INSTANCE.registerFactory( containerType.get().getId(), ( id, type, player, packet ) -> {
+            T container = containerType.get();
+            container.fromBytes( packet );
+            return factory.apply( id, container, player );
+        } );
     }
 
-    static <C extends Container, T extends ContainerType<C>> void register( Supplier<T> containerType, BiFunction<T, EntityPlayer, C> factory )
+    static <C extends Container, T extends ContainerType<C>> void registerGui( Supplier<T> containerType, ContainerFactory<T, ContainerScreen<?>> factory )
     {
-        factories.put( containerType.get().getId(), containerType );
-        containerFactories.put( containerType.get().getId(), factory );
+        ScreenProviderRegistry.INSTANCE.registerFactory( containerType.get().getId(), ( id, type, player, packet ) -> {
+            T container = containerType.get();
+            container.fromBytes( packet );
+            return factory.apply( id, container, player );
+        } );
     }
 
-    static <C extends Container, T extends ContainerType<C>> void registerGui( Supplier<T> containerType, BiFunction<T, EntityPlayer, GuiContainer> factory )
+    static <C extends Container, T extends ContainerType<C>> void registerGui( Supplier<T> containerType, BiFunction<C, PlayerInventory, ContainerScreen<?>> factory )
     {
-        guiFactories.put( containerType.get().getId(), factory );
+        ScreenProviderRegistry.INSTANCE.<C>registerFactory( containerType.get().getId(), container ->
+            factory.apply( container, MinecraftClient.getInstance().player.inventory ) );
     }
 
-    static <C extends Container, T extends ContainerType<C>> void registerGui( Supplier<T> containerType, Function<C, GuiContainer> factory )
+    interface ContainerFactory<T, R>
     {
-        registerGui( containerType, ( type, player ) -> {
-            @SuppressWarnings( "unchecked" )
-            C container = ((BiFunction<T, EntityPlayer, C>) containerFactories.get( type.getId() )).apply( type, player );
-            return container == null ? null : factory.apply( container );
-        } );
+        R apply( int id, T input, PlayerEntity player );
     }
-
-    Map<ResourceLocation, Supplier<? extends ContainerType<?>>> factories = new HashMap<>();
-    Map<ResourceLocation, BiFunction<? extends ContainerType<?>, EntityPlayer, GuiContainer>> guiFactories = new HashMap<>();
-    Map<ResourceLocation, BiFunction<? extends ContainerType<?>, EntityPlayer, ? extends Container>> containerFactories = new HashMap<>();
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/container/PocketComputerContainerType.java b/src/main/java/dan200/computercraft/shared/network/container/PocketComputerContainerType.java
index 859b369aa2..425843e606 100644
--- a/src/main/java/dan200/computercraft/shared/network/container/PocketComputerContainerType.java
+++ b/src/main/java/dan200/computercraft/shared/network/container/PocketComputerContainerType.java
@@ -8,9 +8,9 @@
 
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.pocket.inventory.ContainerPocketComputer;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -21,11 +21,11 @@
  */
 public class PocketComputerContainerType implements ContainerType<ContainerPocketComputer>
 {
-    public static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "pocket_computer_gui" );
+    public static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "pocket_computer_gui" );
 
-    public EnumHand hand;
+    public Hand hand;
 
-    public PocketComputerContainerType( EnumHand hand )
+    public PocketComputerContainerType( Hand hand )
     {
         this.hand = hand;
     }
@@ -36,20 +36,20 @@ public PocketComputerContainerType()
 
     @Nonnull
     @Override
-    public ResourceLocation getId()
+    public Identifier getId()
     {
         return ID;
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
-        buf.writeEnumValue( hand );
+        buf.writeEnumConstant( hand );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
-        hand = buf.readEnumValue( EnumHand.class );
+        hand = buf.readEnumConstant( Hand.class );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/container/PrintoutContainerType.java b/src/main/java/dan200/computercraft/shared/network/container/PrintoutContainerType.java
index b89244d758..1052f74657 100644
--- a/src/main/java/dan200/computercraft/shared/network/container/PrintoutContainerType.java
+++ b/src/main/java/dan200/computercraft/shared/network/container/PrintoutContainerType.java
@@ -8,9 +8,9 @@
 
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.common.ContainerHeldItem;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -21,11 +21,11 @@
  */
 public class PrintoutContainerType implements ContainerType<ContainerHeldItem>
 {
-    public static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "printout_gui" );
+    public static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "printout_gui" );
 
-    public EnumHand hand;
+    public Hand hand;
 
-    public PrintoutContainerType( EnumHand hand )
+    public PrintoutContainerType( Hand hand )
     {
         this.hand = hand;
     }
@@ -36,20 +36,20 @@ public PrintoutContainerType()
 
     @Nonnull
     @Override
-    public ResourceLocation getId()
+    public Identifier getId()
     {
         return ID;
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
-        buf.writeEnumValue( hand );
+        buf.writeEnumConstant( hand );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
-        hand = buf.readEnumValue( EnumHand.class );
+        hand = buf.readEnumConstant( Hand.class );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/container/TileEntityContainerType.java b/src/main/java/dan200/computercraft/shared/network/container/TileEntityContainerType.java
index 48185e1b5d..75689979e9 100644
--- a/src/main/java/dan200/computercraft/shared/network/container/TileEntityContainerType.java
+++ b/src/main/java/dan200/computercraft/shared/network/container/TileEntityContainerType.java
@@ -11,17 +11,17 @@
 import dan200.computercraft.shared.peripheral.diskdrive.ContainerDiskDrive;
 import dan200.computercraft.shared.peripheral.printer.ContainerPrinter;
 import dan200.computercraft.shared.turtle.inventory.ContainerTurtle;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.container.Container;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
 import net.minecraft.util.math.BlockPos;
 
 import javax.annotation.Nonnull;
 
 /**
- * Opens a GUI on a specific ComputerCraft TileEntity
+ * Opens a GUI on a specific ComputerCraft BlockEntity
  *
  * @see dan200.computercraft.shared.peripheral.diskdrive.TileDiskDrive
  * @see dan200.computercraft.shared.peripheral.printer.TilePrinter
@@ -29,47 +29,47 @@
  */
 public final class TileEntityContainerType<T extends Container> implements ContainerType<T>
 {
-    private static final ResourceLocation DISK_DRIVE = new ResourceLocation( ComputerCraft.MOD_ID, "disk_drive" );
-    private static final ResourceLocation PRINTER = new ResourceLocation( ComputerCraft.MOD_ID, "printer" );
-    private static final ResourceLocation COMPUTER = new ResourceLocation( ComputerCraft.MOD_ID, "computer" );
-    private static final ResourceLocation TURTLE = new ResourceLocation( ComputerCraft.MOD_ID, "turtle" );
+    private static final Identifier DISK_DRIVE = new Identifier( ComputerCraft.MOD_ID, "disk_drive" );
+    private static final Identifier PRINTER = new Identifier( ComputerCraft.MOD_ID, "printer" );
+    private static final Identifier COMPUTER = new Identifier( ComputerCraft.MOD_ID, "computer" );
+    private static final Identifier TURTLE = new Identifier( ComputerCraft.MOD_ID, "turtle" );
 
     public BlockPos pos;
-    private final ResourceLocation id;
+    private final Identifier id;
 
-    private TileEntityContainerType( ResourceLocation id, BlockPos pos )
+    private TileEntityContainerType( Identifier id, BlockPos pos )
     {
         this.id = id;
         this.pos = pos;
     }
 
-    private TileEntityContainerType( ResourceLocation id )
+    private TileEntityContainerType( Identifier id )
     {
         this.id = id;
     }
 
     @Nonnull
     @Override
-    public ResourceLocation getId()
+    public Identifier getId()
     {
         return id;
     }
 
     @Override
-    public void toBytes( PacketBuffer buf )
+    public void toBytes( PacketByteBuf buf )
     {
         buf.writeBlockPos( pos );
     }
 
     @Override
-    public void fromBytes( PacketBuffer buf )
+    public void fromBytes( PacketByteBuf buf )
     {
         pos = buf.readBlockPos();
     }
 
-    public TileEntity getTileEntity( EntityPlayer entity )
+    public BlockEntity getTileEntity( PlayerEntity entity )
     {
-        return entity.world.getTileEntity( pos );
+        return entity.world.getBlockEntity( pos );
     }
 
     public static TileEntityContainerType<ContainerDiskDrive> diskDrive()
diff --git a/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerType.java b/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerType.java
index ff541796aa..03898ce273 100644
--- a/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerType.java
+++ b/src/main/java/dan200/computercraft/shared/network/container/ViewComputerContainerType.java
@@ -11,8 +11,8 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.computer.inventory.ContainerViewComputer;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -23,7 +23,7 @@
  */
 public class ViewComputerContainerType implements ContainerType<ContainerViewComputer>
 {
-    public static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "view_computer_gui" );
+    public static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "view_computer_gui" );
 
     public int instanceId;
     public int width;
@@ -48,26 +48,26 @@ public ViewComputerContainerType()
 
     @Nonnull
     @Override
-    public ResourceLocation getId()
+    public Identifier getId()
     {
         return ID;
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         buf.writeVarInt( instanceId );
         buf.writeVarInt( width );
         buf.writeVarInt( height );
-        buf.writeEnumValue( family );
+        buf.writeEnumConstant( family );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         instanceId = buf.readVarInt();
         width = buf.readVarInt();
         height = buf.readVarInt();
-        family = buf.readEnumValue( ComputerFamily.class );
+        family = buf.readEnumConstant( ComputerFamily.class );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java
index f761bc044b..24c4e3dfba 100644
--- a/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/server/ComputerActionServerMessage.java
@@ -8,7 +8,7 @@
 
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.ServerComputer;
-import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -27,17 +27,17 @@ public ComputerActionServerMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         super.toBytes( buf );
-        buf.writeEnumValue( action );
+        buf.writeEnumConstant( action );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         super.fromBytes( buf );
-        action = buf.readEnumValue( Action.class );
+        action = buf.readEnumConstant( Action.class );
     }
 
     @Override
diff --git a/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java
index ab40f03b8f..1b1711e6d0 100644
--- a/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/server/ComputerServerMessage.java
@@ -10,8 +10,8 @@
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.network.NetworkMessage;
-import net.minecraft.network.PacketBuffer;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -35,24 +35,24 @@ public ComputerServerMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         buf.writeVarInt( instanceId );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         instanceId = buf.readVarInt();
     }
 
     @Override
-    public void handle( NetworkEvent.Context context )
+    public void handle( PacketContext context )
     {
         ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instanceId );
         if( computer == null ) return;
 
-        IContainerComputer container = computer.getContainer( context.getSender() );
+        IContainerComputer container = computer.getContainer( context.getPlayer() );
         if( container == null ) return;
 
         handle( computer, container );
diff --git a/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java
index 7472c4f326..b325251f92 100644
--- a/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/server/KeyEventServerMessage.java
@@ -9,7 +9,7 @@
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.InputState;
 import dan200.computercraft.shared.computer.core.ServerComputer;
-import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -34,7 +34,7 @@ public KeyEventServerMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         super.toBytes( buf );
         buf.writeByte( type );
@@ -42,7 +42,7 @@ public void toBytes( @Nonnull PacketBuffer buf )
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         super.fromBytes( buf );
         type = buf.readByte();
diff --git a/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java
index 385a801164..8289107b58 100644
--- a/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/server/MouseEventServerMessage.java
@@ -9,7 +9,7 @@
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.InputState;
 import dan200.computercraft.shared.computer.core.ServerComputer;
-import net.minecraft.network.PacketBuffer;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -39,7 +39,7 @@ public MouseEventServerMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         super.toBytes( buf );
         buf.writeByte( type );
@@ -49,7 +49,7 @@ public void toBytes( @Nonnull PacketBuffer buf )
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         super.fromBytes( buf );
         type = buf.readByte();
diff --git a/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java
index c2b57055ae..cedd1ba96d 100644
--- a/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/server/QueueEventServerMessage.java
@@ -9,8 +9,8 @@
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.util.NBTUtil;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.network.PacketBuffer;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -38,7 +38,7 @@ public QueueEventServerMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         super.toBytes( buf );
         buf.writeString( event );
@@ -46,12 +46,12 @@ public void toBytes( @Nonnull PacketBuffer buf )
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         super.fromBytes( buf );
         event = buf.readString( Short.MAX_VALUE );
 
-        NBTTagCompound args = buf.readCompoundTag();
+        CompoundTag args = buf.readCompoundTag();
         this.args = args == null ? null : NBTUtil.decodeObjects( args );
     }
 
diff --git a/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java b/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java
index 74282b7e82..9a246c43e4 100644
--- a/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java
+++ b/src/main/java/dan200/computercraft/shared/network/server/RequestComputerMessage.java
@@ -9,8 +9,8 @@
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.network.NetworkMessage;
-import net.minecraft.network.PacketBuffer;
-import net.minecraftforge.fml.network.NetworkEvent;
+import net.fabricmc.fabric.api.network.PacketContext;
+import net.minecraft.util.PacketByteBuf;
 
 import javax.annotation.Nonnull;
 
@@ -28,21 +28,21 @@ public RequestComputerMessage()
     }
 
     @Override
-    public void toBytes( @Nonnull PacketBuffer buf )
+    public void toBytes( @Nonnull PacketByteBuf buf )
     {
         buf.writeVarInt( instance );
     }
 
     @Override
-    public void fromBytes( @Nonnull PacketBuffer buf )
+    public void fromBytes( @Nonnull PacketByteBuf buf )
     {
         instance = buf.readVarInt();
     }
 
     @Override
-    public void handle( NetworkEvent.Context context )
+    public void handle( PacketContext context )
     {
         ServerComputer computer = ComputerCraft.serverComputerRegistry.get( instance );
-        if( computer != null ) computer.sendComputerState( context.getSender() );
+        if( computer != null ) computer.sendComputerState( context.getPlayer() );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java
index 58fedd28b0..c7980dfdc5 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/commandblock/CommandBlockPeripheral.java
@@ -10,7 +10,7 @@
 import dan200.computercraft.api.lua.LuaException;
 import dan200.computercraft.api.peripheral.IComputerAccess;
 import dan200.computercraft.api.peripheral.IPeripheral;
-import net.minecraft.tileentity.TileEntityCommandBlock;
+import net.minecraft.block.entity.CommandBlockBlockEntity;
 
 import javax.annotation.Nonnull;
 
@@ -18,9 +18,9 @@
 
 public class CommandBlockPeripheral implements IPeripheral
 {
-    private final TileEntityCommandBlock m_commandBlock;
+    private final CommandBlockBlockEntity m_commandBlock;
 
-    public CommandBlockPeripheral( TileEntityCommandBlock commandBlock )
+    public CommandBlockPeripheral( CommandBlockBlockEntity commandBlock )
     {
         m_commandBlock = commandBlock;
     }
@@ -52,7 +52,7 @@ public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaCont
         {
             case 0: // getCommand
                 return context.executeMainThreadTask( () -> new Object[] {
-                    m_commandBlock.getCommandBlockLogic().getCommand()
+                    m_commandBlock.getCommandExecutor().getCommand()
                 } );
             case 1:
             {
@@ -60,8 +60,8 @@ public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaCont
                 final String command = getString( arguments, 0 );
                 context.issueMainThreadTask( () ->
                 {
-                    m_commandBlock.getCommandBlockLogic().setCommand( command );
-                    m_commandBlock.getCommandBlockLogic().updateCommand();
+                    m_commandBlock.getCommandExecutor().setCommand( command );
+                    m_commandBlock.getCommandExecutor().method_8295();
                     return null;
                 } );
                 return null;
@@ -69,8 +69,8 @@ public Object[] callMethod( @Nonnull IComputerAccess computer, @Nonnull ILuaCont
             case 2: // runCommand
                 return context.executeMainThreadTask( () ->
                 {
-                    m_commandBlock.getCommandBlockLogic().trigger( m_commandBlock.getWorld() );
-                    int result = m_commandBlock.getCommandBlockLogic().getSuccessCount();
+                    m_commandBlock.getCommandExecutor().execute( m_commandBlock.getWorld() );
+                    int result = m_commandBlock.getCommandExecutor().getSuccessCount();
                     if( result > 0 )
                     {
                         return new Object[] { true };
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java
index 98d8936833..8825d51510 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/BlockDiskDrive.java
@@ -8,76 +8,53 @@
 
 import dan200.computercraft.shared.common.BlockGeneric;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.EnumProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.stats.StatList;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.INameable;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.EnumProperty;
+import net.minecraft.state.property.Properties;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
-import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public class BlockDiskDrive extends BlockGeneric
 {
-    static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
+    static final DirectionProperty FACING = Properties.FACING_HORIZONTAL;
     static final EnumProperty<DiskDriveState> STATE = EnumProperty.create( "state", DiskDriveState.class );
 
-    public BlockDiskDrive( Properties settings )
+    public BlockDiskDrive( Settings settings )
     {
         super( settings, TileDiskDrive.FACTORY );
-        setDefaultState( getStateContainer().getBaseState()
-            .with( FACING, EnumFacing.NORTH )
+        setDefaultState( getStateFactory().getDefaultState()
+            .with( FACING, Direction.NORTH )
             .with( STATE, DiskDriveState.EMPTY ) );
     }
 
-
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> properties )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> properties )
     {
-        properties.add( FACING, STATE );
+        properties.with( FACING, STATE );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext placement )
-    {
-        return getDefaultState().with( FACING, placement.getPlacementHorizontalFacing().getOpposite() );
-    }
-
-    @Override
-    public void harvestBlock( @Nonnull World world, EntityPlayer player, @Nonnull BlockPos pos, @Nonnull IBlockState state, @Nullable TileEntity te, ItemStack stack )
+    public BlockState getPlacementState( ItemPlacementContext placement )
     {
-        if( te instanceof INameable && ((INameable) te).hasCustomName() )
-        {
-            player.addStat( StatList.BLOCK_MINED.get( this ) );
-            player.addExhaustion( 0.005F );
-
-            ItemStack result = new ItemStack( this );
-            result.setDisplayName( ((INameable) te).getCustomName() );
-            spawnAsEntity( world, pos, result );
-        }
-        else
-        {
-            super.harvestBlock( world, player, pos, state, te, stack );
-        }
+        return getDefaultState().with( FACING, placement.getPlayerHorizontalFacing().getOpposite() );
     }
 
     @Override
-    public void onBlockPlacedBy( World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack )
+    public void onPlaced( World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack )
     {
         if( stack.hasDisplayName() )
         {
-            TileEntity tileentity = world.getTileEntity( pos );
+            BlockEntity tileentity = world.getBlockEntity( pos );
             if( tileentity instanceof TileDiskDrive ) ((TileDiskDrive) tileentity).customName = stack.getDisplayName();
         }
     }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java
index bde384e180..bd5c010711 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/ContainerDiskDrive.java
@@ -6,21 +6,31 @@
 
 package dan200.computercraft.shared.peripheral.diskdrive;
 
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.Slot;
+import net.minecraft.container.Container;
+import net.minecraft.container.Slot;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.inventory.BasicInventory;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nonnull;
 
 public class ContainerDiskDrive extends Container
 {
-    private final TileDiskDrive m_diskDrive;
+    private final Inventory m_diskDrive;
 
-    public ContainerDiskDrive( IInventory playerInventory, TileDiskDrive diskDrive )
+    public ContainerDiskDrive( int id, PlayerInventory player )
     {
+        this( id, player, new BasicInventory( TileDiskDrive.INVENTORY_SIZE ) );
+    }
+
+    public ContainerDiskDrive( int id, PlayerInventory playerInventory, Inventory diskDrive )
+    {
+        super( null, id );
+
         m_diskDrive = diskDrive;
+
         addSlot( new Slot( m_diskDrive, 0, 8 + 4 * 18, 35 ) );
 
         for( int y = 0; y < 3; y++ )
@@ -37,49 +47,43 @@ public ContainerDiskDrive( IInventory playerInventory, TileDiskDrive diskDrive )
         }
     }
 
-    public TileDiskDrive getDiskDrive()
-    {
-        return m_diskDrive;
-    }
-
     @Override
-    public boolean canInteractWith( @Nonnull EntityPlayer player )
+    public boolean canUse( @Nonnull PlayerEntity player )
     {
-        return m_diskDrive.isUsableByPlayer( player );
+        return m_diskDrive.canPlayerUseInv( player );
     }
 
-    @Nonnull
     @Override
-    public ItemStack transferStackInSlot( EntityPlayer player, int slotIndex )
+    public ItemStack transferSlot( PlayerEntity player, int slotIndex )
     {
-        Slot slot = inventorySlots.get( slotIndex );
-        if( slot == null || !slot.getHasStack() ) return ItemStack.EMPTY;
+        Slot slot = slotList.get( slotIndex );
+        if( slot == null || !slot.hasStack() ) return ItemStack.EMPTY;
 
         ItemStack existing = slot.getStack();
         ItemStack result = existing.copy();
         if( slotIndex == 0 )
         {
             // Insert into player inventory
-            if( !mergeItemStack( existing, 1, 37, true ) ) return ItemStack.EMPTY;
+            if( !insertItem( existing, 1, 37, true ) ) return ItemStack.EMPTY;
         }
         else
         {
             // Insert into drive inventory
-            if( !mergeItemStack( existing, 0, 1, false ) ) return ItemStack.EMPTY;
+            if( !insertItem( existing, 0, 1, false ) ) return ItemStack.EMPTY;
         }
 
         if( existing.isEmpty() )
         {
-            slot.putStack( ItemStack.EMPTY );
+            slot.setStack( ItemStack.EMPTY );
         }
         else
         {
-            slot.onSlotChanged();
+            slot.markDirty();
         }
 
-        if( existing.getCount() == result.getCount() ) return ItemStack.EMPTY;
+        if( existing.getAmount() == result.getAmount() ) return ItemStack.EMPTY;
 
-        slot.onTake( player, existing );
+        slot.onTakeItem( player, existing );
         return result;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java
index d15dd010d8..80c6a03289 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/DiskDriveState.java
@@ -6,11 +6,9 @@
 
 package dan200.computercraft.shared.peripheral.diskdrive;
 
-import net.minecraft.util.IStringSerializable;
+import net.minecraft.util.StringRepresentable;
 
-import javax.annotation.Nonnull;
-
-public enum DiskDriveState implements IStringSerializable
+public enum DiskDriveState implements StringRepresentable
 {
     EMPTY( "empty" ),
     FULL( "full" ),
@@ -23,9 +21,9 @@ public enum DiskDriveState implements IStringSerializable
         this.name = name;
     }
 
+
     @Override
-    @Nonnull
-    public String getName()
+    public String asString()
     {
         return name;
     }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java
index 313a26d111..be871eb75e 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/diskdrive/TileDiskDrive.java
@@ -20,18 +20,21 @@
 import dan200.computercraft.shared.util.InventoryUtil;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
 import dan200.computercraft.shared.util.RecordUtil;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.item.EntityItem;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.*;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Nameable;
+import net.minecraft.util.Tickable;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.items.IItemHandlerModifiable;
-import net.minecraftforge.items.wrapper.InvWrapper;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -39,30 +42,29 @@
 import java.util.Map;
 import java.util.Set;
 
-import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY;
-
-public final class TileDiskDrive extends TileGeneric implements DefaultInventory, ITickable, IPeripheralTile
+public final class TileDiskDrive extends TileGeneric implements DefaultInventory, Tickable, IPeripheralTile, Nameable
 {
     private static final String NBT_NAME = "CustomName";
     private static final String NBT_ITEM = "Item";
 
     public static final NamedBlockEntityType<TileDiskDrive> FACTORY = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "disk_drive" ),
+        new Identifier( ComputerCraft.MOD_ID, "disk_drive" ),
         TileDiskDrive::new
     );
 
+    public static final int INVENTORY_SIZE = 1;
+
     private static class MountInfo
     {
         String mountPath;
     }
 
-    ITextComponent customName;
+    TextComponent customName;
 
     private final Map<IComputerAccess, MountInfo> m_computers = new HashMap<>();
 
     @Nonnull
     private ItemStack m_diskStack = ItemStack.EMPTY;
-    private final LazyOptional<IItemHandlerModifiable> m_itemCap = LazyOptional.of( () -> new InvWrapper( this ) );
     private IMount m_diskMount = null;
 
     private boolean m_recordQueued = false;
@@ -70,72 +72,71 @@ private static class MountInfo
     private boolean m_restartRecord = false;
     private boolean m_ejectQueued;
 
-    private TileDiskDrive()
+    private TileDiskDrive( BlockEntityType<? extends TileDiskDrive> type )
     {
-        super( FACTORY );
+        super( type );
     }
 
     @Override
     public void destroy()
     {
-        ejectContents( true );
         if( m_recordPlaying ) stopRecord();
     }
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
         if( player.isSneaking() )
         {
             // Try to put a disk into the drive
-            ItemStack disk = player.getHeldItem( hand );
+            ItemStack disk = player.getStackInHand( hand );
             if( disk.isEmpty() ) return false;
-            if( !getWorld().isRemote && getStackInSlot( 0 ).isEmpty() && MediaProviders.get( disk ) != null )
+            if( !getWorld().isClient && getInvStack( 0 ).isEmpty() && MediaProviders.get( disk ) != null )
             {
                 setDiskStack( disk );
-                player.setHeldItem( hand, ItemStack.EMPTY );
+                player.setStackInHand( hand, ItemStack.EMPTY );
             }
             return true;
         }
         else
         {
             // Open the GUI
-            if( !getWorld().isRemote ) Containers.openDiskDriveGUI( player, this );
+            if( !getWorld().isClient ) Containers.openDiskDriveGUI( player, this );
             return true;
         }
     }
 
-    public EnumFacing getDirection()
+    public Direction getDirection()
     {
-        return getBlockState().get( BlockDiskDrive.FACING );
+        return getCachedState().get( BlockDiskDrive.FACING );
     }
 
     @Override
-    public void read( NBTTagCompound nbt )
+    public void fromTag( CompoundTag nbt )
     {
-        super.read( nbt );
-        customName = nbt.contains( NBT_NAME ) ? ITextComponent.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null;
-        if( nbt.contains( NBT_ITEM ) )
+        super.fromTag( nbt );
+        customName = nbt.containsKey( NBT_NAME ) ? TextComponent.Serializer.fromJsonString( nbt.getString( NBT_NAME ) ) : null;
+        if( nbt.containsKey( NBT_ITEM ) )
         {
-            NBTTagCompound item = nbt.getCompound( NBT_ITEM );
-            m_diskStack = ItemStack.read( item );
+            CompoundTag item = nbt.getCompound( NBT_ITEM );
+            m_diskStack = ItemStack.fromTag( item );
             m_diskMount = null;
         }
     }
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound nbt )
+    public CompoundTag toTag( CompoundTag nbt )
     {
-        if( customName != null ) nbt.putString( NBT_NAME, ITextComponent.Serializer.toJson( customName ) );
+        if( customName != null ) nbt.putString( NBT_NAME, TextComponent.Serializer.toJsonString( customName ) );
 
         if( !m_diskStack.isEmpty() )
         {
-            NBTTagCompound item = new NBTTagCompound();
-            m_diskStack.write( item );
+            CompoundTag item = new CompoundTag();
+            m_diskStack.toTag( item );
             nbt.put( NBT_ITEM, item );
         }
-        return super.write( nbt );
+        return super.toTag( nbt );
     }
 
     @Override
@@ -151,7 +152,7 @@ public void tick()
         // Music
         synchronized( this )
         {
-            if( !world.isRemote && m_recordPlaying != m_recordQueued || m_restartRecord )
+            if( !world.isClient && m_recordPlaying != m_recordQueued || m_restartRecord )
             {
                 m_restartRecord = false;
                 if( m_recordQueued )
@@ -177,30 +178,29 @@ public void tick()
         }
     }
 
-    // IInventory implementation
+    // Inventory implementation
 
     @Override
-    public int getSizeInventory()
+    public int getInvSize()
     {
-        return 1;
+        return INVENTORY_SIZE;
     }
 
     @Override
-    public boolean isEmpty()
+    public boolean isInvEmpty()
     {
         return m_diskStack.isEmpty();
     }
 
-    @Nonnull
     @Override
-    public ItemStack getStackInSlot( int slot )
+    public ItemStack getInvStack( int slot )
     {
         return m_diskStack;
     }
 
     @Nonnull
     @Override
-    public ItemStack removeStackFromSlot( int slot )
+    public ItemStack removeInvStack( int slot )
     {
         ItemStack result = m_diskStack;
         m_diskStack = ItemStack.EMPTY;
@@ -211,26 +211,26 @@ public ItemStack removeStackFromSlot( int slot )
 
     @Nonnull
     @Override
-    public ItemStack decrStackSize( int slot, int count )
+    public ItemStack takeInvStack( int slot, int count )
     {
         if( m_diskStack.isEmpty() ) return ItemStack.EMPTY;
 
-        if( m_diskStack.getCount() <= count )
+        if( m_diskStack.getAmount() <= count )
         {
             ItemStack disk = m_diskStack;
-            setInventorySlotContents( slot, ItemStack.EMPTY );
+            setInvStack( slot, ItemStack.EMPTY );
             return disk;
         }
 
         ItemStack part = m_diskStack.split( count );
-        setInventorySlotContents( slot, m_diskStack.isEmpty() ? ItemStack.EMPTY : m_diskStack );
+        setInvStack( slot, m_diskStack.isEmpty() ? ItemStack.EMPTY : m_diskStack );
         return part;
     }
 
     @Override
-    public void setInventorySlotContents( int slot, @Nonnull ItemStack stack )
+    public void setInvStack( int slot, @Nonnull ItemStack stack )
     {
-        if( getWorld().isRemote )
+        if( getWorld().isClient )
         {
             m_diskStack = stack;
             m_diskMount = null;
@@ -279,12 +279,12 @@ public void setInventorySlotContents( int slot, @Nonnull ItemStack stack )
     @Override
     public void markDirty()
     {
-        if( !world.isRemote ) updateBlockState();
+        if( !world.isClient ) updateBlockState();
         super.markDirty();
     }
 
     @Override
-    public boolean isUsableByPlayer( @Nonnull EntityPlayer player )
+    public boolean canPlayerUseInv( PlayerEntity player )
     {
         return isUsable( player, false );
     }
@@ -292,11 +292,11 @@ public boolean isUsableByPlayer( @Nonnull EntityPlayer player )
     @Override
     public void clear()
     {
-        setInventorySlotContents( 0, ItemStack.EMPTY );
+        setInvStack( 0, ItemStack.EMPTY );
     }
 
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         return new DiskDrivePeripheral( this );
     }
@@ -304,12 +304,12 @@ public IPeripheral getPeripheral( @Nonnull EnumFacing side )
     @Nonnull
     public ItemStack getDiskStack()
     {
-        return getStackInSlot( 0 );
+        return getInvStack( 0 );
     }
 
     public void setDiskStack( @Nonnull ItemStack stack )
     {
-        setInventorySlotContents( 0, stack );
+        setInvStack( 0, stack );
     }
 
     public IMedia getDiskMedia()
@@ -441,7 +441,7 @@ private synchronized void unmountDisk( IComputerAccess computer )
 
     private void updateBlockState()
     {
-        if( removed ) return;
+        if( invalid ) return;
 
         if( !m_diskStack.isEmpty() )
         {
@@ -456,7 +456,7 @@ private void updateBlockState()
 
     private void updateBlockState( DiskDriveState state )
     {
-        IBlockState blockState = getBlockState();
+        BlockState blockState = getCachedState();
         if( blockState.get( BlockDiskDrive.STATE ) == state ) return;
 
         getWorld().setBlockState( getPos(), blockState.with( BlockDiskDrive.STATE, state ) );
@@ -464,7 +464,7 @@ private void updateBlockState( DiskDriveState state )
 
     private synchronized void ejectContents( boolean destroyed )
     {
-        if( getWorld().isRemote || m_diskStack.isEmpty() ) return;
+        if( getWorld().isClient || m_diskStack.isEmpty() ) return;
 
         // Remove the disks from the inventory
         ItemStack disks = m_diskStack;
@@ -475,42 +475,40 @@ private synchronized void ejectContents( boolean destroyed )
         int zOff = 0;
         if( !destroyed )
         {
-            EnumFacing dir = getDirection();
-            xOff = dir.getXOffset();
-            zOff = dir.getZOffset();
+            Direction dir = getDirection();
+            xOff = dir.getOffsetX();
+            zOff = dir.getOffsetZ();
         }
 
         BlockPos pos = getPos();
         double x = pos.getX() + 0.5 + xOff * 0.5;
         double y = pos.getY() + 0.75;
         double z = pos.getZ() + 0.5 + zOff * 0.5;
-        EntityItem entityitem = new EntityItem( getWorld(), x, y, z, disks );
-        entityitem.motionX = xOff * 0.15;
-        entityitem.motionY = 0.0;
-        entityitem.motionZ = zOff * 0.15;
+        ItemEntity entityitem = new ItemEntity( getWorld(), x, y, z, disks );
+        entityitem.setVelocity( xOff * 0.15, 0.0, zOff * 0.15 );
 
         getWorld().spawnEntity( entityitem );
-        if( !destroyed ) getWorld().playBroadcastSound( 1000, getPos(), 0 );
+        if( !destroyed ) getWorld().playGlobalEvent( 1000, getPos(), 0 );
     }
 
     @Override
-    protected void readDescription( @Nonnull NBTTagCompound nbt )
+    protected void readDescription( @Nonnull CompoundTag nbt )
     {
         super.readDescription( nbt );
-        customName = nbt.contains( NBT_NAME ) ? ITextComponent.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null;
-        m_diskStack = nbt.contains( NBT_ITEM ) ? ItemStack.read( nbt.getCompound( NBT_ITEM ) ) : ItemStack.EMPTY;
+        customName = nbt.containsKey( NBT_NAME ) ? TextComponent.Serializer.fromJsonString( nbt.getString( NBT_NAME ) ) : null;
+        m_diskStack = nbt.containsKey( NBT_ITEM ) ? ItemStack.fromTag( nbt.getCompound( NBT_ITEM ) ) : ItemStack.EMPTY;
         updateBlock();
     }
 
     @Override
-    protected void writeDescription( @Nonnull NBTTagCompound nbt )
+    protected void writeDescription( @Nonnull CompoundTag nbt )
     {
         super.writeDescription( nbt );
-        if( customName != null ) nbt.putString( NBT_NAME, ITextComponent.Serializer.toJson( customName ) );
+        if( customName != null ) nbt.putString( NBT_NAME, TextComponent.Serializer.toJsonString( customName ) );
         if( !m_diskStack.isEmpty() )
         {
-            NBTTagCompound item = new NBTTagCompound();
-            m_diskStack.write( item );
+            CompoundTag item = new CompoundTag();
+            m_diskStack.toTag( item );
             nbt.put( NBT_ITEM, item );
         }
     }
@@ -536,14 +534,6 @@ private void stopRecord()
         RecordUtil.playRecord( null, null, getWorld(), getPos() );
     }
 
-    @Nonnull
-    @Override
-    public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> cap, @Nullable final EnumFacing side )
-    {
-        if( cap == ITEM_HANDLER_CAPABILITY ) return m_itemCap.cast();
-        return super.getCapability( cap, side );
-    }
-
     @Override
     public boolean hasCustomName()
     {
@@ -552,15 +542,15 @@ public boolean hasCustomName()
 
     @Nullable
     @Override
-    public ITextComponent getCustomName()
+    public TextComponent getCustomName()
     {
         return customName;
     }
 
     @Nonnull
     @Override
-    public ITextComponent getName()
+    public TextComponent getName()
     {
-        return customName != null ? customName : getBlockState().getBlock().getNameTextComponent();
+        return customName != null ? customName : getCachedState().getBlock().getTextComponent();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java
index 697ab044c5..68385ae929 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/ModemShapes.java
@@ -6,25 +6,25 @@
 
 package dan200.computercraft.shared.peripheral.modem;
 
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.util.math.shapes.VoxelShapes;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.util.shape.VoxelShapes;
 
 import javax.annotation.Nonnull;
 
 public final class ModemShapes
 {
     private static final VoxelShape[] BOXES = new VoxelShape[] {
-        VoxelShapes.create( 0.125, 0.0, 0.125, 0.875, 0.1875, 0.875 ), // Down
-        VoxelShapes.create( 0.125, 0.8125, 0.125, 0.875, 1.0, 0.875 ), // Up
-        VoxelShapes.create( 0.125, 0.125, 0.0, 0.875, 0.875, 0.1875 ), // North
-        VoxelShapes.create( 0.125, 0.125, 0.8125, 0.875, 0.875, 1.0 ), // South
-        VoxelShapes.create( 0.0, 0.125, 0.125, 0.1875, 0.875, 0.875 ), // West
-        VoxelShapes.create( 0.8125, 0.125, 0.125, 1.0, 0.875, 0.875 ), // East
+        VoxelShapes.cuboid( 0.125, 0.0, 0.125, 0.875, 0.1875, 0.875 ), // Down
+        VoxelShapes.cuboid( 0.125, 0.8125, 0.125, 0.875, 1.0, 0.875 ), // Up
+        VoxelShapes.cuboid( 0.125, 0.125, 0.0, 0.875, 0.875, 0.1875 ), // North
+        VoxelShapes.cuboid( 0.125, 0.125, 0.8125, 0.875, 0.875, 1.0 ), // South
+        VoxelShapes.cuboid( 0.0, 0.125, 0.125, 0.1875, 0.875, 0.875 ), // West
+        VoxelShapes.cuboid( 0.8125, 0.125, 0.125, 1.0, 0.875, 0.875 ), // East
     };
 
     @Nonnull
-    public static VoxelShape getBounds( EnumFacing facing )
+    public static VoxelShape getBounds( Direction facing )
     {
         int direction = facing.ordinal();
         return direction < BOXES.length ? BOXES[direction] : VoxelShapes.fullCube();
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java
index e39c96167a..bb7ef589cf 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockCable.java
@@ -11,27 +11,27 @@
 import dan200.computercraft.api.ComputerCraftAPI;
 import dan200.computercraft.shared.common.BlockGeneric;
 import dan200.computercraft.shared.util.WaterloggableBlock;
-import dan200.computercraft.shared.util.WorldUtil;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.BlockFaceShape;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.fluid.IFluidState;
-import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.VerticalEntityPosition;
+import net.minecraft.fluid.FluidState;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.EnumProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.NonNullList;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.BooleanProperty;
+import net.minecraft.state.property.EnumProperty;
+import net.minecraft.util.hit.HitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.world.IBlockReader;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.world.BlockView;
 import net.minecraft.world.IWorld;
-import net.minecraft.world.IWorldReaderBase;
+import net.minecraft.world.ViewableWorld;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -50,18 +50,18 @@ public class BlockCable extends BlockGeneric implements WaterloggableBlock
     private static final BooleanProperty UP = BooleanProperty.create( "up" );
     private static final BooleanProperty DOWN = BooleanProperty.create( "down" );
 
-    static final EnumMap<EnumFacing, BooleanProperty> CONNECTIONS =
-        new EnumMap<>( new ImmutableMap.Builder<EnumFacing, BooleanProperty>()
-            .put( EnumFacing.DOWN, DOWN ).put( EnumFacing.UP, UP )
-            .put( EnumFacing.NORTH, NORTH ).put( EnumFacing.SOUTH, SOUTH )
-            .put( EnumFacing.WEST, WEST ).put( EnumFacing.EAST, EAST )
+    static final EnumMap<Direction, BooleanProperty> CONNECTIONS =
+        new EnumMap<>( new ImmutableMap.Builder<Direction, BooleanProperty>()
+            .put( Direction.DOWN, DOWN ).put( Direction.UP, UP )
+            .put( Direction.NORTH, NORTH ).put( Direction.SOUTH, SOUTH )
+            .put( Direction.WEST, WEST ).put( Direction.EAST, EAST )
             .build() );
 
-    public BlockCable( Properties settings )
+    public BlockCable( Settings settings )
     {
         super( settings, TileCable.FACTORY );
 
-        setDefaultState( getStateContainer().getBaseState()
+        setDefaultState( getStateFactory().getDefaultState()
             .with( MODEM, CableModemVariant.None )
             .with( CABLE, false )
             .with( NORTH, false ).with( SOUTH, false )
@@ -72,61 +72,59 @@ public BlockCable( Properties settings )
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> builder )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> builder )
     {
-        builder.add( MODEM, CABLE, NORTH, SOUTH, EAST, WEST, UP, DOWN, WATERLOGGED );
+        builder.with( MODEM, CABLE, NORTH, SOUTH, EAST, WEST, UP, DOWN, WATERLOGGED );
     }
 
-    public static boolean canConnectIn( IBlockState state, EnumFacing direction )
+    public static boolean canConnectIn( BlockState state, Direction direction )
     {
         return state.get( BlockCable.CABLE ) && state.get( BlockCable.MODEM ).getFacing() != direction;
     }
 
-    public static boolean doesConnectVisually( IBlockState state, IBlockReader world, BlockPos pos, EnumFacing direction )
+    public static boolean doesConnectVisually( BlockState state, BlockView world, BlockPos pos, Direction direction )
     {
         if( !state.get( CABLE ) ) return false;
         if( state.get( MODEM ).getFacing() == direction ) return true;
         return ComputerCraftAPI.getWiredElementAt( world, pos.offset( direction ), direction.getOpposite() ) != null;
     }
 
-    @Nonnull
     @Override
     @Deprecated
-    public VoxelShape getShape( IBlockState state, IBlockReader world, BlockPos pos )
+    public VoxelShape getOutlineShape( BlockState state, BlockView world, BlockPos pos, VerticalEntityPosition position )
     {
         return CableShapes.getShape( state );
     }
 
+    /*
     @Override
-    public boolean removedByPlayer( IBlockState state, World world, BlockPos pos, EntityPlayer player, boolean willHarvest, IFluidState fluid )
+    public boolean removedByPlayer( @Nonnull BlockState state, World world, @Nonnull BlockPos pos, @Nonnull PlayerEntity player, boolean willHarvest )
     {
-        if( state.get( CABLE ) && state.get( MODEM ).getFacing() != null )
+        PeripheralType type = getPeripheralType( world, pos );
+        if( type == PeripheralType.WiredModemWithCable )
         {
-            RayTraceResult hit = Block.collisionRayTrace( state, world, pos, WorldUtil.getRayStart( player ), WorldUtil.getRayEnd( player ) );
+            RayTraceResult hit = state.collisionRayTrace( world, pos, WorldUtil.getRayStart( player ), WorldUtil.getRayEnd( player ) );
             if( hit != null )
             {
-                TileEntity tile = world.getTileEntity( pos );
-                if( tile instanceof TileCable && tile.hasWorld() )
+                BlockEntity tile = world.getTileEntity( pos );
+                if( tile != null && tile instanceof TileCable && tile.hasWorld() )
                 {
                     TileCable cable = (TileCable) tile;
 
                     ItemStack item;
-                    IBlockState newState;
 
-                    VoxelShape bb = CableShapes.getModemShape( state );
-                    if( WorldUtil.isVecInside( bb, hit.hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) ) )
+                    AxisAlignedBB bb = cable.getModemBounds();
+                    if( WorldUtil.isVecInsideInclusive( bb, hit.hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) ) )
                     {
                         newState = state.with( MODEM, CableModemVariant.None );
                         item = new ItemStack( ComputerCraft.Items.wiredModem );
                     }
                     else
                     {
-                        newState = state.with( CABLE, false );
-                        item = new ItemStack( ComputerCraft.Items.cable );
+                        world.setBlockState( pos, state.with( CABLE, BlockCableCableVariant.NONE ), 3 );
+                        item = PeripheralItemFactory.create( PeripheralType.Cable, null, 1 );
                     }
 
-                    world.setBlockState( pos, correctConnections( world, pos, newState ), 3 );
-
                     cable.modemChanged();
                     cable.connectionsChanged();
                     if( !world.isRemote && !player.abilities.isCreativeMode )
@@ -139,21 +137,16 @@ public boolean removedByPlayer( IBlockState state, World world, BlockPos pos, En
             }
         }
 
-        return super.removedByPlayer( state, world, pos, player, willHarvest, fluid );
-    }
-
-    @Override
-    public void getDrops( IBlockState state, NonNullList<ItemStack> drops, World world, BlockPos pos, int fortune )
-    {
-        if( state.get( CABLE ) ) drops.add( new ItemStack( ComputerCraft.Items.cable ) );
-        if( state.get( MODEM ) != CableModemVariant.None ) drops.add( new ItemStack( ComputerCraft.Items.cable ) );
+        return super.removedByPlayer( state, world, pos, player, willHarvest );
     }
+    */
 
     @Nonnull
     @Override
-    public ItemStack getPickBlock( IBlockState state, RayTraceResult hit, IBlockReader world, BlockPos pos, EntityPlayer player )
+    @Environment( EnvType.CLIENT )
+    public ItemStack getPickStack( BlockView world, BlockPos pos, BlockState state )
     {
-        EnumFacing modem = state.get( MODEM ).getFacing();
+        Direction modem = state.get( MODEM ).getFacing();
         boolean cable = state.get( CABLE );
 
         // If we've only got one, just use that.
@@ -161,31 +154,32 @@ public ItemStack getPickBlock( IBlockState state, RayTraceResult hit, IBlockRead
         if( modem == null ) return new ItemStack( ComputerCraft.Items.cable );
 
         // We've a modem and cable, so try to work out which one we're interacting with
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
+        HitResult hit = MinecraftClient.getInstance().hitResult;
         return tile instanceof TileCable && hit != null &&
-            CableShapes.getModemShape( state ).getBoundingBox().contains( hit.hitVec.subtract( pos.getX(), pos.getY(), pos.getZ() ) )
+            CableShapes.getModemShape( state ).getBoundingBox().contains( hit.getPos().subtract( pos.getX(), pos.getY(), pos.getZ() ) )
             ? new ItemStack( ComputerCraft.Items.wiredModem )
             : new ItemStack( ComputerCraft.Items.cable );
 
     }
 
     @Override
-    public void onBlockPlacedBy( World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack )
+    public void onPlaced( World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack )
     {
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( tile instanceof TileCable )
         {
             TileCable cable = (TileCable) tile;
             if( cable.hasCable() ) cable.connectionsChanged();
         }
 
-        super.onBlockPlacedBy( world, pos, state, placer, stack );
+        super.onPlaced( world, pos, state, placer, stack );
     }
 
     @Nonnull
     @Override
     @Deprecated
-    public IFluidState getFluidState( IBlockState state )
+    public FluidState getFluidState( BlockState state )
     {
         return getWaterloggedFluidState( state );
     }
@@ -193,75 +187,61 @@ public IFluidState getFluidState( IBlockState state )
     @Nonnull
     @Override
     @Deprecated
-    public IBlockState updatePostPlacement( @Nonnull IBlockState state, EnumFacing side, IBlockState otherState, IWorld world, BlockPos pos, BlockPos otherPos )
+    public BlockState getStateForNeighborUpdate( BlockState state, Direction side, BlockState otherState, IWorld world, BlockPos pos, BlockPos otherPos )
     {
         updateWaterloggedPostPlacement( state, world, pos );
+
         // Should never happen, but handle the case where we've no modem or cable.
         if( !state.get( CABLE ) && state.get( MODEM ) == CableModemVariant.None )
         {
             return getFluidState( state ).getBlockState();
         }
 
-        if( side == state.get( MODEM ).getFacing() && !state.isValidPosition( world, pos ) )
-        {
-            if( !state.get( CABLE ) ) return getFluidState( state ).getBlockState();
-
-            /* TODO:
-            TileEntity entity = world.getTileEntity( pos );
-            if( entity instanceof TileCable )
-            {
-                entity.modemChanged();
-                entity.connectionsChanged();
-            }
-            */
-            state = state.with( MODEM, CableModemVariant.None );
-        }
-
         return state.with( CONNECTIONS.get( side ), doesConnectVisually( state, world, pos, side ) );
     }
 
     @Override
     @Deprecated
-    public boolean isValidPosition( IBlockState state, IWorldReaderBase world, BlockPos pos )
+    public boolean canPlaceAt( BlockState state, ViewableWorld world, BlockPos pos )
     {
-        EnumFacing facing = state.get( MODEM ).getFacing();
+        Direction facing = state.get( MODEM ).getFacing();
         if( facing == null ) return true;
 
         BlockPos offsetPos = pos.offset( facing );
-        IBlockState offsetState = world.getBlockState( offsetPos );
-        return offsetState.getBlockFaceShape( world, offsetPos, facing.getOpposite() ) == BlockFaceShape.SOLID;
+        BlockState offsetState = world.getBlockState( offsetPos );
+        return Block.isFaceFullSquare( offsetState.getCollisionShape( world, offsetPos ), facing.getOpposite() ) && !method_9581( offsetState.getBlock() );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext context )
+    public BlockState getPlacementState( ItemPlacementContext context )
     {
-        IBlockState state = getDefaultState()
+        BlockState state = getDefaultState()
             .with( WATERLOGGED, getWaterloggedStateForPlacement( context ) );
 
-        if( context.getItem().getItem() instanceof ItemBlockCable.Cable )
+        if( context.getItemStack().getItem() instanceof ItemBlockCable.Cable )
         {
             World world = context.getWorld();
-            BlockPos pos = context.getPos();
+            BlockPos pos = context.getBlockPos();
             return correctConnections( world, pos, state.with( CABLE, true ) );
         }
         else
         {
-            return state.with( MODEM, CableModemVariant.from( context.getFace().getOpposite() ) );
+            return state.with( MODEM, CableModemVariant.from( context.getFacing().getOpposite() ) );
         }
     }
 
-    public static IBlockState correctConnections( World world, BlockPos pos, IBlockState state )
+    public static BlockState correctConnections( World world, BlockPos pos, BlockState state )
     {
         if( state.get( CABLE ) )
         {
             return state
-                .with( NORTH, doesConnectVisually( state, world, pos, EnumFacing.NORTH ) )
-                .with( SOUTH, doesConnectVisually( state, world, pos, EnumFacing.SOUTH ) )
-                .with( EAST, doesConnectVisually( state, world, pos, EnumFacing.EAST ) )
-                .with( WEST, doesConnectVisually( state, world, pos, EnumFacing.WEST ) )
-                .with( UP, doesConnectVisually( state, world, pos, EnumFacing.UP ) )
-                .with( DOWN, doesConnectVisually( state, world, pos, EnumFacing.DOWN ) );
+                .with( NORTH, doesConnectVisually( state, world, pos, Direction.NORTH ) )
+                .with( SOUTH, doesConnectVisually( state, world, pos, Direction.SOUTH ) )
+                .with( EAST, doesConnectVisually( state, world, pos, Direction.EAST ) )
+                .with( WEST, doesConnectVisually( state, world, pos, Direction.WEST ) )
+                .with( UP, doesConnectVisually( state, world, pos, Direction.UP ) )
+                .with( DOWN, doesConnectVisually( state, world, pos, Direction.DOWN ) );
         }
         else
         {
@@ -273,23 +253,7 @@ public static IBlockState correctConnections( World world, BlockPos pos, IBlockS
 
     @Override
     @Deprecated
-    public final boolean isFullCube( IBlockState state )
-    {
-        return false;
-    }
-
-
-    @Nonnull
-    @Override
-    @Deprecated
-    public BlockFaceShape getBlockFaceShape( IBlockReader worldIn, IBlockState state, BlockPos pos, EnumFacing face )
-    {
-        return BlockFaceShape.UNDEFINED;
-    }
-
-    @Override
-    @Deprecated
-    public boolean hasCustomBreakingProgress( IBlockState state )
+    public boolean hasBlockEntityBreakingRender( BlockState state )
     {
         return true;
     }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java
index 3cfc9e5b2a..85b6360931 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/BlockWiredModemFull.java
@@ -8,27 +8,27 @@
 
 import dan200.computercraft.shared.common.BlockGeneric;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.StateContainer;
+import net.minecraft.block.BlockState;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.BooleanProperty;
 
 public class BlockWiredModemFull extends BlockGeneric
 {
     public static final BooleanProperty MODEM_ON = BooleanProperty.create( "modem" );
     public static final BooleanProperty PERIPHERAL_ON = BooleanProperty.create( "peripheral" );
 
-    public BlockWiredModemFull( Properties settings )
+    public BlockWiredModemFull( Settings settings )
     {
         super( settings, TileWiredModemFull.FACTORY );
-        setDefaultState( getStateContainer().getBaseState()
+        setDefaultState( getStateFactory().getDefaultState()
             .with( MODEM_ON, false )
             .with( PERIPHERAL_ON, false )
         );
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> builder )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> builder )
     {
-        builder.add( MODEM_ON, PERIPHERAL_ON );
+        builder.with( MODEM_ON, PERIPHERAL_ON );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java
index de2ff52e71..d4539bd36a 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableModemVariant.java
@@ -6,71 +6,71 @@
 
 package dan200.computercraft.shared.peripheral.modem.wired;
 
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.IStringSerializable;
+import net.minecraft.util.StringRepresentable;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 
-public enum CableModemVariant implements IStringSerializable
+public enum CableModemVariant implements StringRepresentable
 {
     None( "none", null ),
-    DownOff( "down_off", EnumFacing.DOWN ),
-    UpOff( "up_off", EnumFacing.UP ),
-    NorthOff( "north_off", EnumFacing.NORTH ),
-    SouthOff( "south_off", EnumFacing.SOUTH ),
-    WestOff( "west_off", EnumFacing.WEST ),
-    EastOff( "east_off", EnumFacing.EAST ),
-    DownOn( "down_on", EnumFacing.DOWN ),
-    UpOn( "up_on", EnumFacing.UP ),
-    NorthOn( "north_on", EnumFacing.NORTH ),
-    SouthOn( "south_on", EnumFacing.SOUTH ),
-    WestOn( "west_on", EnumFacing.WEST ),
-    EastOn( "east_on", EnumFacing.EAST ),
-    DownOffPeripheral( "down_off_peripheral", EnumFacing.DOWN ),
-    UpOffPeripheral( "up_off_peripheral", EnumFacing.UP ),
-    NorthOffPeripheral( "north_off_peripheral", EnumFacing.NORTH ),
-    SouthOffPeripheral( "south_off_peripheral", EnumFacing.SOUTH ),
-    WestOffPeripheral( "west_off_peripheral", EnumFacing.WEST ),
-    EastOffPeripheral( "east_off_peripheral", EnumFacing.EAST ),
-    DownOnPeripheral( "down_on_peripheral", EnumFacing.DOWN ),
-    UpOnPeripheral( "up_on_peripheral", EnumFacing.UP ),
-    NorthOnPeripheral( "north_on_peripheral", EnumFacing.NORTH ),
-    SouthOnPeripheral( "south_on_peripheral", EnumFacing.SOUTH ),
-    WestOnPeripheral( "west_on_peripheral", EnumFacing.WEST ),
-    EastOnPeripheral( "east_on_peripheral", EnumFacing.EAST );
+    DownOff( "down_off", Direction.DOWN ),
+    UpOff( "up_off", Direction.UP ),
+    NorthOff( "north_off", Direction.NORTH ),
+    SouthOff( "south_off", Direction.SOUTH ),
+    WestOff( "west_off", Direction.WEST ),
+    EastOff( "east_off", Direction.EAST ),
+    DownOn( "down_on", Direction.DOWN ),
+    UpOn( "up_on", Direction.UP ),
+    NorthOn( "north_on", Direction.NORTH ),
+    SouthOn( "south_on", Direction.SOUTH ),
+    WestOn( "west_on", Direction.WEST ),
+    EastOn( "east_on", Direction.EAST ),
+    DownOffPeripheral( "down_off_peripheral", Direction.DOWN ),
+    UpOffPeripheral( "up_off_peripheral", Direction.UP ),
+    NorthOffPeripheral( "north_off_peripheral", Direction.NORTH ),
+    SouthOffPeripheral( "south_off_peripheral", Direction.SOUTH ),
+    WestOffPeripheral( "west_off_peripheral", Direction.WEST ),
+    EastOffPeripheral( "east_off_peripheral", Direction.EAST ),
+    DownOnPeripheral( "down_on_peripheral", Direction.DOWN ),
+    UpOnPeripheral( "up_on_peripheral", Direction.UP ),
+    NorthOnPeripheral( "north_on_peripheral", Direction.NORTH ),
+    SouthOnPeripheral( "south_on_peripheral", Direction.SOUTH ),
+    WestOnPeripheral( "west_on_peripheral", Direction.WEST ),
+    EastOnPeripheral( "east_on_peripheral", Direction.EAST );
 
     private static final CableModemVariant[] VALUES = values();
 
     private final String name;
-    private final EnumFacing facing;
+    private final Direction facing;
 
-    CableModemVariant( String name, EnumFacing facing )
+    CableModemVariant( String name, Direction facing )
     {
         this.name = name;
         this.facing = facing;
     }
 
     @Nonnull
-    public static CableModemVariant from( EnumFacing facing )
+    public static CableModemVariant from( Direction facing )
     {
-        return facing == null ? None : VALUES[1 + facing.getIndex()];
+        return facing == null ? None : VALUES[1 + facing.getId()];
     }
 
     @Nonnull
-    public static CableModemVariant from( EnumFacing facing, boolean modem, boolean peripheral )
+    public static CableModemVariant from( Direction facing, boolean modem, boolean peripheral )
     {
         int state = (modem ? 2 : 0) + (peripheral ? 1 : 0);
-        return facing == null ? None : VALUES[1 + 6 * state + facing.getIndex()];
+        return facing == null ? None : VALUES[1 + 6 * state + facing.getId()];
     }
 
     @Nonnull
     @Override
-    public String getName()
+    public String asString()
     {
         return name;
     }
 
-    public EnumFacing getFacing()
+    public Direction getFacing()
     {
         return facing;
     }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java
index 4f9810f44d..fc26e8694a 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/CableShapes.java
@@ -9,10 +9,10 @@
 import com.google.common.collect.ImmutableMap;
 import dan200.computercraft.shared.peripheral.modem.ModemShapes;
 import dan200.computercraft.shared.util.DirectionUtil;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.util.math.shapes.VoxelShapes;
+import net.minecraft.block.BlockState;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.util.shape.VoxelShapes;
 
 import java.util.EnumMap;
 
@@ -23,15 +23,15 @@ public final class CableShapes
     private static final double MIN = 0.375;
     private static final double MAX = 1 - MIN;
 
-    private static final VoxelShape SHAPE_CABLE_CORE = VoxelShapes.create( MIN, MIN, MIN, MAX, MAX, MAX );
-    private static final EnumMap<EnumFacing, VoxelShape> SHAPE_CABLE_ARM =
-        new EnumMap<>( new ImmutableMap.Builder<EnumFacing, VoxelShape>()
-            .put( EnumFacing.DOWN, VoxelShapes.create( MIN, 0, MIN, MAX, MIN, MAX ) )
-            .put( EnumFacing.UP, VoxelShapes.create( MIN, MAX, MIN, MAX, 1, MAX ) )
-            .put( EnumFacing.NORTH, VoxelShapes.create( MIN, MIN, 0, MAX, MAX, MIN ) )
-            .put( EnumFacing.SOUTH, VoxelShapes.create( MIN, MIN, MAX, MAX, MAX, 1 ) )
-            .put( EnumFacing.WEST, VoxelShapes.create( 0, MIN, MIN, MIN, MAX, MAX ) )
-            .put( EnumFacing.EAST, VoxelShapes.create( MAX, MIN, MIN, 1, MAX, MAX ) )
+    private static final VoxelShape SHAPE_CABLE_CORE = VoxelShapes.cuboid( MIN, MIN, MIN, MAX, MAX, MAX );
+    private static final EnumMap<Direction, VoxelShape> SHAPE_CABLE_ARM =
+        new EnumMap<>( new ImmutableMap.Builder<Direction, VoxelShape>()
+            .put( Direction.DOWN, VoxelShapes.cuboid( MIN, 0, MIN, MAX, MIN, MAX ) )
+            .put( Direction.UP, VoxelShapes.cuboid( MIN, MAX, MIN, MAX, 1, MAX ) )
+            .put( Direction.NORTH, VoxelShapes.cuboid( MIN, MIN, 0, MAX, MAX, MIN ) )
+            .put( Direction.SOUTH, VoxelShapes.cuboid( MIN, MIN, MAX, MAX, MAX, 1 ) )
+            .put( Direction.WEST, VoxelShapes.cuboid( 0, MIN, MIN, MIN, MAX, MAX ) )
+            .put( Direction.EAST, VoxelShapes.cuboid( MAX, MIN, MIN, 1, MAX, MAX ) )
             .build()
         );
 
@@ -42,10 +42,10 @@ private CableShapes()
     {
     }
 
-    private static int getCableIndex( IBlockState state )
+    private static int getCableIndex( BlockState state )
     {
         int index = 0;
-        for( EnumFacing facing : DirectionUtil.FACINGS )
+        for( Direction facing : DirectionUtil.FACINGS )
         {
             if( state.get( CONNECTIONS.get( facing ) ) ) index |= 1 << facing.ordinal();
         }
@@ -59,32 +59,32 @@ private static VoxelShape getCableShape( int index )
         if( shape != null ) return shape;
 
         shape = SHAPE_CABLE_CORE;
-        for( EnumFacing facing : DirectionUtil.FACINGS )
+        for( Direction facing : DirectionUtil.FACINGS )
         {
             if( (index & (1 << facing.ordinal())) != 0 )
             {
-                shape = VoxelShapes.or( shape, SHAPE_CABLE_ARM.get( facing ) );
+                shape = VoxelShapes.union( shape, SHAPE_CABLE_ARM.get( facing ) );
             }
         }
 
         return CABLE_SHAPES[index] = shape;
     }
 
-    public static VoxelShape getCableShape( IBlockState state )
+    public static VoxelShape getCableShape( BlockState state )
     {
         if( !state.get( CABLE ) ) return VoxelShapes.empty();
         return getCableShape( getCableIndex( state ) );
     }
 
-    public static VoxelShape getModemShape( IBlockState state )
+    public static VoxelShape getModemShape( BlockState state )
     {
-        EnumFacing facing = state.get( MODEM ).getFacing();
+        Direction facing = state.get( MODEM ).getFacing();
         return facing == null ? VoxelShapes.empty() : ModemShapes.getBounds( facing );
     }
 
-    public static VoxelShape getShape( IBlockState state )
+    public static VoxelShape getShape( BlockState state )
     {
-        EnumFacing facing = state.get( MODEM ).getFacing();
+        Direction facing = state.get( MODEM ).getFacing();
         if( !state.get( CABLE ) ) return getModemShape( state );
 
         int cableIndex = getCableIndex( state );
@@ -94,7 +94,7 @@ public static VoxelShape getShape( IBlockState state )
         if( shape != null ) return shape;
 
         shape = getCableShape( cableIndex );
-        if( facing != null ) shape = VoxelShapes.or( shape, ModemShapes.getBounds( facing ) );
+        if( facing != null ) shape = VoxelShapes.union( shape, ModemShapes.getBounds( facing ) );
         return SHAPES[index] = shape;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java
index d65ca60f02..2b2ecddf56 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/ItemBlockCable.java
@@ -7,42 +7,45 @@
 package dan200.computercraft.shared.peripheral.modem.wired;
 
 import dan200.computercraft.ComputerCraft;
-import net.minecraft.block.SoundType;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.item.ItemBlock;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.item.Item;
 import net.minecraft.item.ItemGroup;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.*;
+import net.minecraft.item.block.BlockItem;
+import net.minecraft.sound.BlockSoundGroup;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.util.ActionResult;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.SystemUtil;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.registry.Registry;
 import net.minecraft.world.World;
-import net.minecraftforge.registries.ForgeRegistries;
 
 import javax.annotation.Nonnull;
 
 import static dan200.computercraft.shared.peripheral.modem.wired.BlockCable.*;
 
-public abstract class ItemBlockCable extends ItemBlock
+public abstract class ItemBlockCable extends BlockItem
 {
     private String translationKey;
 
-    public ItemBlockCable( BlockCable block, Properties settings )
+    public ItemBlockCable( BlockCable block, Item.Settings settings )
     {
         super( block, settings );
     }
 
-    boolean placeAt( World world, BlockPos pos, IBlockState state, EntityPlayer player )
+    boolean placeAt( World world, BlockPos pos, BlockState state )
     {
-        // TODO: Check entity collision.
-        if( !state.isValidPosition( world, pos ) ) return false;
+        if( !state.canPlaceAt( world, pos ) ) return false;
 
         world.setBlockState( pos, state, 3 );
-        SoundType soundType = state.getBlock().getSoundType( state, world, pos, player );
-        world.playSound( null, pos, soundType.getPlaceSound(), SoundCategory.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F );
+        BlockSoundGroup soundType = state.getBlock().getSoundGroup( state );
+        world.playSound( null, pos, soundType.getPlaceSound(), SoundCategory.BLOCK, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F );
 
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( tile instanceof TileCable )
         {
             TileCable cable = (TileCable) tile;
@@ -53,101 +56,100 @@ boolean placeAt( World world, BlockPos pos, IBlockState state, EntityPlayer play
         return true;
     }
 
-    boolean placeAtCorrected( World world, BlockPos pos, IBlockState state )
+    boolean placeAtCorrected( World world, BlockPos pos, BlockState state )
     {
-        return placeAt( world, pos, correctConnections( world, pos, state ), null );
+        return placeAt( world, pos, correctConnections( world, pos, state ) );
     }
 
     @Override
-    public void fillItemGroup( @Nonnull ItemGroup group, @Nonnull NonNullList<ItemStack> list )
+    public void appendItemsForGroup( ItemGroup group, DefaultedList<ItemStack> list )
     {
-        if( isInGroup( group ) ) list.add( new ItemStack( this ) );
+        if( isInItemGroup( group ) ) list.add( new ItemStack( this ) );
     }
 
-    @Nonnull
     @Override
     public String getTranslationKey()
     {
         if( translationKey == null )
         {
-            translationKey = Util.makeTranslationKey( "block", ForgeRegistries.ITEMS.getKey( this ) );
+            translationKey = SystemUtil.createTranslationKey( "block", Registry.ITEM.getId( this ) );
         }
         return translationKey;
     }
 
     public static class WiredModem extends ItemBlockCable
     {
-        public WiredModem( BlockCable block, Properties settings )
+        public WiredModem( BlockCable block, Settings settings )
         {
             super( block, settings );
         }
 
         @Nonnull
         @Override
-        public EnumActionResult tryPlace( BlockItemUseContext context )
+        public ActionResult place( ItemPlacementContext context )
         {
-            ItemStack stack = context.getItem();
-            if( stack.isEmpty() ) return EnumActionResult.FAIL;
+            ItemStack stack = context.getItemStack();
+            if( stack.isEmpty() ) return ActionResult.FAIL;
 
             World world = context.getWorld();
-            BlockPos pos = context.getPos();
-            IBlockState existingState = world.getBlockState( pos );
+            BlockPos pos = context.getBlockPos();
+            BlockState existingState = world.getBlockState( pos );
 
             // Try to add a modem to a cable
             if( existingState.getBlock() == ComputerCraft.Blocks.cable && existingState.get( MODEM ) == CableModemVariant.None )
             {
-                EnumFacing side = context.getFace().getOpposite();
-                IBlockState newState = existingState
+                Direction side = context.getFacing().getOpposite();
+                BlockState newState = existingState
                     .with( MODEM, CableModemVariant.from( side ) )
                     .with( CONNECTIONS.get( side ), existingState.get( CABLE ) );
-                if( placeAt( world, pos, newState, context.getPlayer() ) )
+                if( placeAt( world, pos, newState ) )
                 {
-                    stack.shrink( 1 );
-                    return EnumActionResult.SUCCESS;
+                    stack.subtractAmount( 1 );
+                    return ActionResult.SUCCESS;
                 }
             }
 
-            return super.tryPlace( context );
+            return super.place( context );
         }
     }
 
     public static class Cable extends ItemBlockCable
     {
-        public Cable( BlockCable block, Properties settings )
+        public Cable( BlockCable block, Settings settings )
         {
             super( block, settings );
         }
 
         @Nonnull
         @Override
-        public EnumActionResult tryPlace( BlockItemUseContext context )
+        public ActionResult place( ItemPlacementContext context )
         {
-            ItemStack stack = context.getItem();
-            if( stack.isEmpty() ) return EnumActionResult.FAIL;
+            ItemStack stack = context.getItemStack();
+            if( stack.isEmpty() ) return ActionResult.FAIL;
 
             World world = context.getWorld();
-            BlockPos pos = context.getPos();
+            BlockPos pos = context.getBlockPos();
 
             // Try to add a cable to a modem inside the block we're clicking on.
-            BlockPos insidePos = pos.offset( context.getFace().getOpposite() );
-            IBlockState insideState = world.getBlockState( insidePos );
+            BlockPos insidePos = pos.offset( context.getFacing().getOpposite() );
+            BlockState insideState = world.getBlockState( insidePos );
             if( insideState.getBlock() == ComputerCraft.Blocks.cable && !insideState.get( BlockCable.CABLE )
                 && placeAtCorrected( world, insidePos, insideState.with( BlockCable.CABLE, true ) ) )
             {
-                stack.shrink( 1 );
-                return EnumActionResult.SUCCESS;
+                stack.subtractAmount( 1 );
+                return ActionResult.SUCCESS;
             }
 
             // Try to add a cable to a modem adjacent to this block
-            IBlockState existingState = world.getBlockState( pos );
+            BlockState existingState = world.getBlockState( pos );
             if( existingState.getBlock() == ComputerCraft.Blocks.cable && !existingState.get( BlockCable.CABLE )
                 && placeAtCorrected( world, pos, existingState.with( BlockCable.CABLE, true ) ) )
             {
-                stack.shrink( 1 );
-                return EnumActionResult.SUCCESS;
+                stack.subtractAmount( 1 );
+                return ActionResult.SUCCESS;
             }
 
-            return super.tryPlace( context );
+            return super.place( context );
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java
index 0ac5b5e7a2..8b3b9035c4 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileCable.java
@@ -19,32 +19,28 @@
 import dan200.computercraft.shared.util.DirectionUtil;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
 import dan200.computercraft.shared.util.TickScheduler;
-import dan200.computercraft.shared.wired.CapabilityWiredElement;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.BlockFaceShape;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.text.TextComponentTranslation;
 import net.minecraft.world.World;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.util.LazyOptional;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.util.Collections;
 import java.util.Map;
 
 public class TileCable extends TileGeneric implements IPeripheralTile
 {
     public static final NamedBlockEntityType<TileCable> FACTORY = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "cable" ),
+        new Identifier( ComputerCraft.MOD_ID, "cable" ),
         TileCable::new
     );
 
@@ -85,12 +81,11 @@ protected void detachPeripheral( String name )
 
     private boolean m_destroyed = false;
 
-    private EnumFacing modemDirection = EnumFacing.NORTH;
+    private Direction modemDirection = Direction.NORTH;
     private boolean hasModemDirection = false;
     private boolean m_connectionsFormed = false;
 
     private final WiredModemElement m_cable = new CableElement();
-    private LazyOptional<IWiredElement> m_cableCapability = LazyOptional.of( () -> m_cable );
     private final IWiredNode m_node = m_cable.getNode();
     private final WiredModemPeripheral m_modem = new WiredModemPeripheral(
         new ModemState( () -> TickScheduler.schedule( this ) ),
@@ -120,7 +115,7 @@ public TileCable()
 
     private void onRemove()
     {
-        if( world == null || !world.isRemote )
+        if( world == null || !world.isClient )
         {
             m_node.remove();
             m_connectionsFormed = false;
@@ -138,37 +133,35 @@ public void destroy()
         }
     }
 
+    /*
     @Override
-    public void onChunkUnloaded()
+    public void onChunkUnload()
     {
-        super.onChunkUnloaded();
-        onRemove();
+        super.onChunkUnload();
+        remove();
     }
+    */
 
     @Override
-    public void remove()
+    public void invalidate()
     {
-        super.remove();
+        super.invalidate();
         onRemove();
     }
 
     @Override
-    public void onLoad()
+    public void validate()
     {
-        super.onLoad();
-        if( !world.isRemote )
-        {
-            updateDirection();
-            world.getPendingBlockTicks().scheduleTick( pos, getBlockState().getBlock(), 0 );
-        }
+        super.validate();
+        TickScheduler.schedule( this );
     }
 
     @Override
-    public void updateContainingBlockInfo()
+    public void resetBlock()
     {
-        super.updateContainingBlockInfo();
+        super.resetBlock();
         hasModemDirection = false;
-        if( !world.isRemote ) world.getPendingBlockTicks().scheduleTick( pos, getBlockState().getBlock(), 0 );
+        if( !world.isClient ) world.getBlockTickScheduler().schedule( pos, getCachedState().getBlock(), 0 );
     }
 
     private void updateDirection()
@@ -180,34 +173,34 @@ private void updateDirection()
         }
     }
 
-    private EnumFacing getDirection()
+    public Direction getDirection()
     {
-        IBlockState state = getBlockState();
-        EnumFacing facing = state.get( BlockCable.MODEM ).getFacing();
-        return facing != null ? facing : EnumFacing.NORTH;
+        BlockState state = getCachedState();
+        Direction facing = state.get( BlockCable.MODEM ).getFacing();
+        return facing != null ? facing : Direction.NORTH;
     }
 
     @Override
     public void onNeighbourChange( @Nonnull BlockPos neighbour )
     {
-        EnumFacing dir = getDirection();
+        Direction dir = getDirection();
         if( neighbour.equals( getPos().offset( dir ) ) && hasModem()
-            && getWorld().getBlockState( neighbour ).getBlockFaceShape( world, neighbour, dir.getOpposite() ) != BlockFaceShape.SOLID
+            && !getCachedState().canPlaceAt( world, getPos() )
         )
         {
             if( hasCable() )
             {
                 // Drop the modem and convert to cable
-                Block.spawnAsEntity( getWorld(), getPos(), new ItemStack( ComputerCraft.Items.wiredModem ) );
-                getWorld().setBlockState( getPos(), getBlockState().with( BlockCable.MODEM, CableModemVariant.None ) );
+                Block.dropStack( getWorld(), getPos(), new ItemStack( ComputerCraft.Items.wiredModem ) );
+                getWorld().setBlockState( getPos(), getCachedState().with( BlockCable.MODEM, CableModemVariant.None ) );
                 modemChanged();
                 connectionsChanged();
             }
             else
             {
                 // Drop everything and remove block
-                Block.spawnAsEntity( getWorld(), getPos(), new ItemStack( ComputerCraft.Items.wiredModem ) );
-                getWorld().removeBlock( getPos() );
+                Block.dropStack( getWorld(), getPos(), new ItemStack( ComputerCraft.Items.wiredModem ) );
+                getWorld().clearBlockState( getPos(), false );
                 // This'll call #destroy(), so we don't need to reset the network here.
             }
 
@@ -221,9 +214,9 @@ && getWorld().getBlockState( neighbour ).getBlockFaceShape( world, neighbour, di
     public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour )
     {
         super.onNeighbourTileEntityChange( neighbour );
-        if( !world.isRemote && m_peripheralAccessAllowed )
+        if( !world.isClient && m_peripheralAccessAllowed )
         {
-            EnumFacing facing = getDirection();
+            Direction facing = getDirection();
             if( getPos().offset( facing ).equals( neighbour ) )
             {
                 if( m_peripheral.attach( world, getPos(), facing ) ) updateConnectedPeripherals();
@@ -232,11 +225,11 @@ public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour )
     }
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
         if( !canAttachPeripheral() || player.isSneaking() ) return false;
 
-        if( getWorld().isRemote ) return true;
+        if( getWorld().isClient ) return true;
 
         String oldName = m_peripheral.getConnectedName();
         togglePeripheralAccess();
@@ -245,12 +238,12 @@ public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side,
         {
             if( oldName != null )
             {
-                player.sendStatusMessage( new TextComponentTranslation( "chat.computercraft.wired_modem.peripheral_disconnected",
+                player.addChatMessage( new TranslatableTextComponent( "chat.computercraft.wired_modem.peripheral_disconnected",
                     CommandCopy.createCopyText( oldName ) ), false );
             }
             if( newName != null )
             {
-                player.sendStatusMessage( new TextComponentTranslation( "chat.computercraft.wired_modem.peripheral_connected",
+                player.addChatMessage( new TranslatableTextComponent( "chat.computercraft.wired_modem.peripheral_connected",
                     CommandCopy.createCopyText( newName ) ), false );
             }
         }
@@ -259,25 +252,25 @@ public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side,
     }
 
     @Override
-    public void read( NBTTagCompound nbt )
+    public void fromTag( CompoundTag nbt )
     {
-        super.read( nbt );
+        super.fromTag( nbt );
         m_peripheralAccessAllowed = nbt.getBoolean( NBT_PERIPHERAL_ENABLED );
-        m_peripheral.read( nbt, "" );
+        m_peripheral.fromTag( nbt, "" );
     }
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound nbt )
+    public CompoundTag toTag( CompoundTag nbt )
     {
         nbt.putBoolean( NBT_PERIPHERAL_ENABLED, m_peripheralAccessAllowed );
-        m_peripheral.write( nbt, "" );
-        return super.write( nbt );
+        m_peripheral.toTag( nbt, "" );
+        return super.toTag( nbt );
     }
 
     private void updateBlockState()
     {
-        IBlockState state = getBlockState();
+        BlockState state = getCachedState();
         CableModemVariant oldVariant = state.get( BlockCable.MODEM );
         CableModemVariant newVariant = CableModemVariant
             .from( oldVariant.getFacing(), m_modem.getModemState().isOpen(), m_peripheralAccessAllowed );
@@ -291,7 +284,7 @@ private void updateBlockState()
     @Override
     public void blockTick()
     {
-        if( getWorld().isRemote ) return;
+        if( getWorld().isClient ) return;
 
         updateDirection();
 
@@ -312,12 +305,12 @@ public void blockTick()
 
     void connectionsChanged()
     {
-        if( getWorld().isRemote ) return;
+        if( getWorld().isClient ) return;
 
-        IBlockState state = getBlockState();
+        BlockState state = getCachedState();
         World world = getWorld();
         BlockPos current = getPos();
-        for( EnumFacing facing : DirectionUtil.FACINGS )
+        for( Direction facing : DirectionUtil.FACINGS )
         {
             BlockPos offset = current.offset( facing );
             if( !world.isBlockLoaded( offset ) ) continue;
@@ -340,12 +333,7 @@ else if( m_node.getNetwork() == element.getNode().getNetwork() )
 
     void modemChanged()
     {
-        // Tell anyone who cares that the connection state has changed
-        // TODO: Be more restrictive about this.
-        m_cableCapability.invalidate();
-        m_cableCapability = LazyOptional.of( () -> m_cable );
-
-        if( getWorld().isRemote ) return;
+        if( getWorld().isClient ) return;
 
         // If we can no longer attach peripherals, then detach any
         // which may have existed
@@ -393,39 +381,33 @@ private void updateConnectedPeripherals()
         m_node.updatePeripherals( peripherals );
     }
 
+    /*
     @Override
     public boolean canRenderBreaking()
     {
         return true;
     }
+    */
 
-    @Nonnull
-    @Override
-    public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> capability, @Nullable EnumFacing facing )
+    public IWiredElement getElement( Direction facing )
     {
-        if( capability == CapabilityWiredElement.CAPABILITY )
-        {
-            return !m_destroyed && BlockCable.canConnectIn( getBlockState(), facing )
-                ? m_cableCapability.cast() : LazyOptional.empty();
-        }
-
-        return super.getCapability( capability, facing );
+        return BlockCable.canConnectIn( getCachedState(), facing ) ? m_cable : null;
     }
 
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         return !m_destroyed && hasModem() && side == getDirection() ? m_modem : null;
     }
 
     public boolean hasCable()
     {
-        return getBlockState().get( BlockCable.CABLE );
+        return getCachedState().get( BlockCable.CABLE );
     }
 
     public boolean hasModem()
     {
-        return getBlockState().get( BlockCable.MODEM ) != CableModemVariant.None;
+        return getCachedState().get( BlockCable.MODEM ) != CableModemVariant.None;
     }
 
     boolean canAttachPeripheral()
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java
index 56a1ff3ef8..4d188567a9 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/TileWiredModemFull.java
@@ -19,23 +19,20 @@
 import dan200.computercraft.shared.util.DirectionUtil;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
 import dan200.computercraft.shared.util.TickScheduler;
-import dan200.computercraft.shared.wired.CapabilityWiredElement;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
 import net.minecraft.world.World;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.util.LazyOptional;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.util.*;
 
 import static dan200.computercraft.shared.peripheral.modem.wired.BlockWiredModemFull.MODEM_ON;
@@ -44,7 +41,7 @@
 public class TileWiredModemFull extends TileGeneric implements IPeripheralTile
 {
     public static final NamedBlockEntityType<TileWiredModemFull> FACTORY = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "wired_modem_full" ),
+        new Identifier( ComputerCraft.MOD_ID, "wired_modem_full" ),
         TileWiredModemFull::new
     );
 
@@ -105,11 +102,8 @@ public Vec3d getPosition()
 
     private final ModemState m_modemState = new ModemState( () -> TickScheduler.schedule( this ) );
     private final WiredModemElement m_element = new FullElement( this );
-    private final LazyOptional<WiredModemElement> m_elementCap = LazyOptional.of( () -> m_element );
     private final IWiredNode m_node = m_element.getNode();
 
-    private int m_state = 0;
-
     public TileWiredModemFull()
     {
         super( FACTORY );
@@ -118,7 +112,7 @@ public TileWiredModemFull()
 
     private void doRemove()
     {
-        if( world == null || !world.isRemote )
+        if( world == null || !world.isClient )
         {
             m_node.remove();
             m_connectionsFormed = false;
@@ -136,17 +130,19 @@ public void destroy()
         super.destroy();
     }
 
+    /*
     @Override
     public void onChunkUnloaded()
     {
         super.onChunkUnloaded();
         doRemove();
     }
+    */
 
     @Override
-    public void remove()
+    public void invalidate()
     {
-        super.remove();
+        super.invalidate();
         doRemove();
     }
 
@@ -159,9 +155,9 @@ public void onNeighbourChange( @Nonnull BlockPos neighbour )
     @Override
     public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour )
     {
-        if( !world.isRemote && m_peripheralAccessAllowed )
+        if( !world.isClient && m_peripheralAccessAllowed )
         {
-            for( EnumFacing facing : DirectionUtil.FACINGS )
+            for( Direction facing : DirectionUtil.FACINGS )
             {
                 if( getPos().offset( facing ).equals( neighbour ) )
                 {
@@ -173,9 +169,9 @@ public void onNeighbourTileEntityChange( @Nonnull BlockPos neighbour )
     }
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
-        if( getWorld().isRemote ) return true;
+        if( getWorld().isClient ) return true;
 
         // On server, we interacted if a peripheral was found
         Set<String> oldPeriphNames = getConnectedPeripheralNames();
@@ -191,43 +187,43 @@ public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side,
         return true;
     }
 
-    private static void sendPeripheralChanges( EntityPlayer player, String kind, Collection<String> peripherals )
+    private static void sendPeripheralChanges( PlayerEntity player, String kind, Collection<String> peripherals )
     {
         if( peripherals.isEmpty() ) return;
 
         List<String> names = new ArrayList<>( peripherals );
         names.sort( Comparator.naturalOrder() );
 
-        TextComponentString base = new TextComponentString( "" );
+        StringTextComponent base = new StringTextComponent( "" );
         for( int i = 0; i < names.size(); i++ )
         {
-            if( i > 0 ) base.appendText( ", " );
-            base.appendSibling( CommandCopy.createCopyText( names.get( i ) ) );
+            if( i > 0 ) base.append( ", " );
+            base.append( CommandCopy.createCopyText( names.get( i ) ) );
         }
 
-        player.sendStatusMessage( new TextComponentTranslation( kind, base ), false );
+        player.addChatMessage( new TranslatableTextComponent( kind, base ), false );
     }
 
     @Override
-    public void read( NBTTagCompound nbt )
+    public void fromTag( CompoundTag nbt )
     {
-        super.read( nbt );
+        super.fromTag( nbt );
         m_peripheralAccessAllowed = nbt.getBoolean( NBT_PERIPHERAL_ENABLED );
-        for( int i = 0; i < m_peripherals.length; i++ ) m_peripherals[i].read( nbt, Integer.toString( i ) );
+        for( int i = 0; i < m_peripherals.length; i++ ) m_peripherals[i].fromTag( nbt, Integer.toString( i ) );
     }
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound nbt )
+    public CompoundTag toTag( CompoundTag nbt )
     {
         nbt.putBoolean( NBT_PERIPHERAL_ENABLED, m_peripheralAccessAllowed );
-        for( int i = 0; i < m_peripherals.length; i++ ) m_peripherals[i].write( nbt, Integer.toString( i ) );
-        return super.write( nbt );
+        for( int i = 0; i < m_peripherals.length; i++ ) m_peripherals[i].toTag( nbt, Integer.toString( i ) );
+        return super.toTag( nbt );
     }
 
     private void updateBlockState()
     {
-        IBlockState state = getBlockState();
+        BlockState state = getCachedState();
         boolean modemOn = m_modemState.isOpen(), peripheralOn = m_peripheralAccessAllowed;
         if( state.get( MODEM_ON ) == modemOn && state.get( PERIPHERAL_ON ) == peripheralOn ) return;
 
@@ -235,16 +231,16 @@ private void updateBlockState()
     }
 
     @Override
-    public void onLoad()
+    public void validate()
     {
-        super.onLoad();
-        if( !world.isRemote ) world.getPendingBlockTicks().scheduleTick( pos, getBlockState().getBlock(), 0 );
+        super.validate();
+        TickScheduler.schedule( this );
     }
 
     @Override
     public void blockTick()
     {
-        if( getWorld().isRemote ) return;
+        if( getWorld().isClient ) return;
 
         if( m_modemState.pollChanged() ) updateBlockState();
 
@@ -255,7 +251,7 @@ public void blockTick()
             connectionsChanged();
             if( m_peripheralAccessAllowed )
             {
-                for( EnumFacing facing : DirectionUtil.FACINGS )
+                for( Direction facing : DirectionUtil.FACINGS )
                 {
                     m_peripherals[facing.ordinal()].attach( world, getPos(), facing );
                 }
@@ -266,11 +262,11 @@ public void blockTick()
 
     private void connectionsChanged()
     {
-        if( getWorld().isRemote ) return;
+        if( getWorld().isClient ) return;
 
         World world = getWorld();
         BlockPos current = getPos();
-        for( EnumFacing facing : DirectionUtil.FACINGS )
+        for( Direction facing : DirectionUtil.FACINGS )
         {
             BlockPos offset = current.offset( facing );
             if( !world.isBlockLoaded( offset ) ) continue;
@@ -288,7 +284,7 @@ private void togglePeripheralAccess()
         if( !m_peripheralAccessAllowed )
         {
             boolean hasAny = false;
-            for( EnumFacing facing : DirectionUtil.FACINGS )
+            for( Direction facing : DirectionUtil.FACINGS )
             {
                 WiredModemLocalPeripheral peripheral = m_peripherals[facing.ordinal()];
                 peripheral.attach( world, getPos(), facing );
@@ -346,13 +342,15 @@ private void updateConnectedPeripherals()
         m_node.updatePeripherals( peripherals );
     }
 
+    /*
     @Nonnull
     @Override
-    public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> capability, @Nullable EnumFacing facing )
+    public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> capability, @Nullable Direction facing )
     {
         if( capability == CapabilityWiredElement.CAPABILITY ) return m_elementCap.cast();
         return super.getCapability( capability, facing );
     }
+    */
 
     public IWiredElement getElement()
     {
@@ -362,7 +360,7 @@ public IWiredElement getElement()
     // IPeripheralTile
 
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         if( m_destroyed ) return null;
 
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java
index 7159aa5d82..4f4b13833b 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wired/WiredModemLocalPeripheral.java
@@ -10,12 +10,12 @@
 import dan200.computercraft.api.peripheral.IPeripheral;
 import dan200.computercraft.shared.Peripherals;
 import dan200.computercraft.shared.util.IDAssigner;
+import dan200.computercraft.shared.util.NBTUtil;
 import net.minecraft.block.Block;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.nbt.CompoundTag;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.Constants;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -46,7 +46,7 @@ public final class WiredModemLocalPeripheral
      * @param direction The direction so search in
      * @return Whether the peripheral changed.
      */
-    public boolean attach( @Nonnull World world, @Nonnull BlockPos origin, @Nonnull EnumFacing direction )
+    public boolean attach( @Nonnull World world, @Nonnull BlockPos origin, @Nonnull Direction direction )
     {
         IPeripheral oldPeripheral = peripheral;
         IPeripheral peripheral = this.peripheral = getPeripheralFrom( world, origin, direction );
@@ -68,7 +68,7 @@ public boolean attach( @Nonnull World world, @Nonnull BlockPos origin, @Nonnull
             else if( id < 0 || !type.equals( this.type ) )
             {
                 this.type = type;
-                this.id = IDAssigner.getNextId( "peripheral." + type );
+                this.id = IDAssigner.getNextId( world, "peripheral." + type );
             }
 
             return oldPeripheral == null || !oldPeripheral.equals( peripheral );
@@ -116,22 +116,22 @@ public Map<String, IPeripheral> toMap()
             : Collections.singletonMap( type + "_" + id, peripheral );
     }
 
-    public void write( @Nonnull NBTTagCompound tag, @Nonnull String suffix )
+    public void toTag( @Nonnull CompoundTag tag, @Nonnull String suffix )
     {
         if( id >= 0 ) tag.putInt( NBT_PERIPHERAL_ID + suffix, id );
         if( type != null ) tag.putString( NBT_PERIPHERAL_TYPE + suffix, type );
     }
 
-    public void read( @Nonnull NBTTagCompound tag, @Nonnull String suffix )
+    public void fromTag( @Nonnull CompoundTag tag, @Nonnull String suffix )
     {
-        id = tag.contains( NBT_PERIPHERAL_ID + suffix, Constants.NBT.TAG_ANY_NUMERIC )
+        id = tag.containsKey( NBT_PERIPHERAL_ID + suffix, NBTUtil.TAG_ANY_NUMERIC )
             ? tag.getInt( NBT_PERIPHERAL_ID + suffix ) : -1;
 
-        type = tag.contains( NBT_PERIPHERAL_TYPE + suffix, Constants.NBT.TAG_STRING )
+        type = tag.containsKey( NBT_PERIPHERAL_TYPE + suffix, NBTUtil.TAG_STRING )
             ? tag.getString( NBT_PERIPHERAL_TYPE + suffix ) : null;
     }
 
-    private static IPeripheral getPeripheralFrom( World world, BlockPos pos, EnumFacing direction )
+    private static IPeripheral getPeripheralFrom( World world, BlockPos pos, Direction direction )
     {
         BlockPos offset = pos.offset( direction );
 
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java
index 208df3b0ec..5a3807617d 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/BlockWirelessModem.java
@@ -10,49 +10,49 @@
 import dan200.computercraft.shared.peripheral.modem.ModemShapes;
 import dan200.computercraft.shared.util.WaterloggableBlock;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.BlockFaceShape;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.fluid.IFluidState;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.VerticalEntityPosition;
+import net.minecraft.fluid.FluidState;
+import net.minecraft.item.ItemPlacementContext;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.BooleanProperty;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.Properties;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.world.IBlockReader;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.world.BlockView;
 import net.minecraft.world.IWorld;
-import net.minecraft.world.IWorldReaderBase;
+import net.minecraft.world.ViewableWorld;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public class BlockWirelessModem extends BlockGeneric implements WaterloggableBlock
 {
-    public static final DirectionProperty FACING = BlockStateProperties.FACING;
+    public static final DirectionProperty FACING = Properties.FACING;
     public static final BooleanProperty ON = BooleanProperty.create( "on" );
 
-    public BlockWirelessModem( Properties settings, TileEntityType<? extends TileWirelessModem> type )
+    public BlockWirelessModem( Settings settings, BlockEntityType<? extends TileWirelessModem> type )
     {
         super( settings, type );
-        setDefaultState( getStateContainer().getBaseState()
-            .with( FACING, EnumFacing.NORTH )
+        setDefaultState( getStateFactory().getDefaultState()
+            .with( FACING, Direction.NORTH )
             .with( ON, false )
             .with( WATERLOGGED, false ) );
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> builder )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> builder )
     {
-        builder.add( FACING, ON, WATERLOGGED );
+        builder.with( FACING, ON, WATERLOGGED );
     }
 
     @Nonnull
     @Override
     @Deprecated
-    public VoxelShape getShape( IBlockState blockState, IBlockReader blockView, BlockPos blockPos )
+    public VoxelShape getOutlineShape( BlockState blockState, BlockView world, BlockPos pos, VerticalEntityPosition position )
     {
         return ModemShapes.getBounds( blockState.get( FACING ) );
     }
@@ -60,7 +60,7 @@ public VoxelShape getShape( IBlockState blockState, IBlockReader blockView, Bloc
     @Nonnull
     @Override
     @Deprecated
-    public IFluidState getFluidState( IBlockState state )
+    public FluidState getFluidState( BlockState state )
     {
         return getWaterloggedFluidState( state );
     }
@@ -68,45 +68,30 @@ public IFluidState getFluidState( IBlockState state )
     @Nonnull
     @Override
     @Deprecated
-    public IBlockState updatePostPlacement( @Nonnull IBlockState state, EnumFacing side, IBlockState otherState, IWorld world, BlockPos pos, BlockPos otherPos )
+    public BlockState getStateForNeighborUpdate( BlockState state, Direction side, BlockState otherState, IWorld world, BlockPos pos, BlockPos otherPos )
     {
         updateWaterloggedPostPlacement( state, world, pos );
-        return side == state.get( FACING ) && !state.isValidPosition( world, pos )
+        return side == state.get( FACING ) && !state.canPlaceAt( world, pos )
             ? state.getFluidState().getBlockState()
             : state;
     }
 
     @Override
     @Deprecated
-    public boolean isValidPosition( IBlockState state, IWorldReaderBase world, BlockPos pos )
+    public boolean canPlaceAt( BlockState state, ViewableWorld world, BlockPos pos )
     {
-        EnumFacing facing = state.get( FACING );
+        Direction facing = state.get( FACING );
         BlockPos offsetPos = pos.offset( facing );
-        IBlockState offsetState = world.getBlockState( offsetPos );
-        return offsetState.getBlockFaceShape( world, offsetPos, facing.getOpposite() ) == BlockFaceShape.SOLID;
+        BlockState offsetState = world.getBlockState( offsetPos );
+        return Block.isFaceFullSquare( offsetState.getCollisionShape( world, offsetPos ), facing.getOpposite() ) && !method_9581( offsetState.getBlock() );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext placement )
+    public BlockState getPlacementState( ItemPlacementContext placement )
     {
         return getDefaultState()
-            .with( FACING, placement.getFace().getOpposite() )
+            .with( FACING, placement.getFacing().getOpposite() )
             .with( WATERLOGGED, getWaterloggedStateForPlacement( placement ) );
     }
-
-    @Override
-    @Deprecated
-    public final boolean isFullCube( IBlockState state )
-    {
-        return false;
-    }
-
-    @Nonnull
-    @Override
-    @Deprecated
-    public BlockFaceShape getBlockFaceShape( IBlockReader worldIn, IBlockState state, BlockPos pos, EnumFacing face )
-    {
-        return BlockFaceShape.UNDEFINED;
-    }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java
index 9637a225af..c1ad2ef9a7 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/TileWirelessModem.java
@@ -8,30 +8,32 @@
 
 import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.peripheral.IPeripheral;
+import dan200.computercraft.api.peripheral.IPeripheralTile;
 import dan200.computercraft.shared.common.TileGeneric;
 import dan200.computercraft.shared.peripheral.modem.ModemPeripheral;
 import dan200.computercraft.shared.peripheral.modem.ModemState;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
 import dan200.computercraft.shared.util.TickScheduler;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
-public class TileWirelessModem extends TileGeneric
+public class TileWirelessModem extends TileGeneric implements IPeripheralTile
 {
     public static final NamedBlockEntityType<TileWirelessModem> FACTORY_NORMAL = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "wireless_modem_normal" ),
+        new Identifier( ComputerCraft.MOD_ID, "wireless_modem_normal" ),
         f -> new TileWirelessModem( f, false )
     );
 
     public static final NamedBlockEntityType<TileWirelessModem> FACTORY_ADVANCED = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "wireless_modem_advanced" ),
+        new Identifier( ComputerCraft.MOD_ID, "wireless_modem_advanced" ),
         f -> new TileWirelessModem( f, true )
     );
 
@@ -70,11 +72,11 @@ public boolean equals( IPeripheral other )
     private final boolean advanced;
 
     private boolean hasModemDirection = false;
-    private EnumFacing modemDirection = EnumFacing.DOWN;
+    private Direction modemDirection = Direction.DOWN;
     private final ModemPeripheral modem;
     private boolean destroyed = false;
 
-    public TileWirelessModem( TileEntityType<? extends TileWirelessModem> type, boolean advanced )
+    public TileWirelessModem( BlockEntityType<? extends TileWirelessModem> type, boolean advanced )
     {
         super( type );
         this.advanced = advanced;
@@ -82,11 +84,10 @@ public TileWirelessModem( TileEntityType<? extends TileWirelessModem> type, bool
     }
 
     @Override
-    public void onLoad()
+    public void validate()
     {
-        super.onLoad();
-        updateDirection();
-        world.getPendingBlockTicks().scheduleTick( getPos(), getBlockState().getBlock(), 0 );
+        super.validate();
+        TickScheduler.schedule( this );
     }
 
     @Override
@@ -114,11 +115,11 @@ public void markDirty()
     }
 
     @Override
-    public void updateContainingBlockInfo()
+    public void resetBlock()
     {
-        super.updateContainingBlockInfo();
+        super.resetBlock();
         hasModemDirection = false;
-        world.getPendingBlockTicks().scheduleTick( getPos(), getBlockState().getBlock(), 0 );
+        world.getBlockTickScheduler().schedule( getPos(), getCachedState().getBlock(), 0 );
     }
 
     @Override
@@ -134,16 +135,24 @@ private void updateDirection()
         if( hasModemDirection ) return;
 
         hasModemDirection = true;
-        modemDirection = getBlockState().get( BlockWirelessModem.FACING );
+        modemDirection = getCachedState().get( BlockWirelessModem.FACING );
     }
 
     private void updateBlockState()
     {
         boolean on = modem.getModemState().isOpen();
-        IBlockState state = getBlockState();
+        BlockState state = getCachedState();
         if( state.get( BlockWirelessModem.ON ) != on )
         {
             getWorld().setBlockState( getPos(), state.with( BlockWirelessModem.ON, on ) );
         }
     }
+
+    @Nullable
+    @Override
+    public IPeripheral getPeripheral( @Nonnull Direction side )
+    {
+        updateDirection();
+        return side == modemDirection ? modem : null;
+    }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java
index 1f6ee792c6..db5ca7f613 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/modem/wireless/WirelessNetwork.java
@@ -71,7 +71,7 @@ private static void tryTransmit( IPacketReceiver receiver, Packet packet, double
         if( receiver.getWorld() == sender.getWorld() )
         {
             double receiveRange = Math.max( range, receiver.getRange() ); // Ensure range is symmetrical
-            double distanceSq = receiver.getPosition().squareDistanceTo( sender.getPosition() );
+            double distanceSq = receiver.getPosition().squaredDistanceTo( sender.getPosition() );
             if( interdimensional || receiver.isInterdimensional() || distanceSq <= receiveRange * receiveRange )
             {
                 receiver.receiveSameDimension( packet, Math.sqrt( distanceSq ) );
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java
index 40e389323c..83bd1c4671 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/BlockMonitor.java
@@ -9,43 +9,41 @@
 import dan200.computercraft.shared.common.BlockGeneric;
 import dan200.computercraft.shared.common.TileGeneric;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.block.BlockRenderLayer;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.EnumProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.BlockRenderLayer;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.EnumProperty;
+import net.minecraft.state.property.Properties;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
-import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public class BlockMonitor extends BlockGeneric
 {
     public static final DirectionProperty ORIENTATION = DirectionProperty.create( "orientation",
-        EnumFacing.UP, EnumFacing.DOWN, EnumFacing.NORTH );
+        Direction.UP, Direction.DOWN, Direction.NORTH );
 
-    public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
+    public static final DirectionProperty FACING = Properties.FACING_HORIZONTAL;
 
     static final EnumProperty<MonitorEdgeState> STATE = EnumProperty.create( "state", MonitorEdgeState.class );
 
-    public BlockMonitor( Properties settings, TileEntityType<? extends TileGeneric> type )
+    public BlockMonitor( Settings settings, BlockEntityType<? extends TileGeneric> type )
     {
         super( settings, type );
-        setDefaultState( getStateContainer().getBaseState()
-            .with( ORIENTATION, EnumFacing.NORTH )
-            .with( FACING, EnumFacing.NORTH )
+        setDefaultState( getStateFactory().getDefaultState()
+            .with( ORIENTATION, Direction.NORTH )
+            .with( FACING, Direction.NORTH )
             .with( STATE, MonitorEdgeState.NONE ) );
     }
 
-    @Nonnull
     @Override
     public BlockRenderLayer getRenderLayer()
     {
@@ -53,44 +51,44 @@ public BlockRenderLayer getRenderLayer()
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> builder )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> builder )
     {
-        builder.add( ORIENTATION, FACING, STATE );
+        builder.with( ORIENTATION, FACING, STATE );
     }
 
     @Override
     @Nullable
-    public IBlockState getStateForPlacement( BlockItemUseContext context )
+    public BlockState getPlacementState( ItemPlacementContext context )
     {
-        float pitch = context.getPlayer() == null ? 0 : context.getPlayer().rotationPitch;
-        EnumFacing orientation;
+        float pitch = context.getPlayer() == null ? 0 : context.getPlayer().pitch;
+        Direction orientation;
         if( pitch > 66.5f )
         {
             // If the player is looking down, place it facing upwards
-            orientation = EnumFacing.UP;
+            orientation = Direction.UP;
         }
         else if( pitch < -66.5f )
         {
             // If they're looking up, place it down.
-            orientation = EnumFacing.DOWN;
+            orientation = Direction.DOWN;
         }
         else
         {
-            orientation = EnumFacing.NORTH;
+            orientation = Direction.NORTH;
         }
 
         return getDefaultState()
-            .with( FACING, context.getPlacementHorizontalFacing().getOpposite() )
+            .with( FACING, context.getPlayerHorizontalFacing().getOpposite() )
             .with( ORIENTATION, orientation );
     }
 
     @Override
-    public void onBlockPlacedBy( World world, BlockPos pos, IBlockState blockState, @Nullable EntityLivingBase livingEntity, ItemStack itemStack )
+    public void onPlaced( World world, BlockPos pos, BlockState blockState, @Nullable LivingEntity livingEntity, ItemStack itemStack )
     {
-        super.onBlockPlacedBy( world, pos, blockState, livingEntity, itemStack );
+        super.onPlaced( world, pos, blockState, livingEntity, itemStack );
 
-        TileEntity entity = world.getTileEntity( pos );
-        if( entity instanceof TileMonitor && !world.isRemote )
+        BlockEntity entity = world.getBlockEntity( pos );
+        if( entity instanceof TileMonitor && !world.isClient )
         {
             TileMonitor monitor = (TileMonitor) entity;
             monitor.contractNeighbours();
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java
index d2fb796753..6821a3e599 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/ClientMonitor.java
@@ -6,11 +6,11 @@
 
 package dan200.computercraft.shared.peripheral.monitor;
 
+import com.mojang.blaze3d.platform.GlStateManager;
 import dan200.computercraft.shared.common.ClientTerminal;
-import net.minecraft.client.renderer.GlStateManager;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.util.math.BlockPos;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
 
 import java.util.HashSet;
 import java.util.Iterator;
@@ -37,7 +37,7 @@ public TileMonitor getOrigin()
         return origin;
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     public void createLists()
     {
         if( renderDisplayLists == null )
@@ -56,7 +56,7 @@ public void createLists()
         }
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     public void destroy()
     {
         if( renderDisplayLists != null )
@@ -75,7 +75,7 @@ public void destroy()
         }
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     public static void destroyAll()
     {
         synchronized( allMonitors )
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java
index 00b60f65cd..68a0071399 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/MonitorEdgeState.java
@@ -6,13 +6,13 @@
 
 package dan200.computercraft.shared.peripheral.monitor;
 
-import net.minecraft.util.IStringSerializable;
+import net.minecraft.util.StringRepresentable;
 
 import javax.annotation.Nonnull;
 
 import static dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState.Flags.*;
 
-public enum MonitorEdgeState implements IStringSerializable
+public enum MonitorEdgeState implements StringRepresentable
 {
     NONE( "none", 0 ),
 
@@ -60,7 +60,7 @@ public static MonitorEdgeState fromConnections( boolean up, boolean down, boolea
 
     @Nonnull
     @Override
-    public String getName()
+    public String asString()
     {
         return name;
     }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java
index 47b748f417..660f2825e8 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/TileMonitor.java
@@ -14,14 +14,16 @@
 import dan200.computercraft.shared.common.ServerTerminal;
 import dan200.computercraft.shared.common.TileGeneric;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.ResourceLocation;
+import dan200.computercraft.shared.util.TickScheduler;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.Hand;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -31,12 +33,12 @@
 public class TileMonitor extends TileGeneric implements IPeripheralTile
 {
     public static final NamedBlockEntityType<TileMonitor> FACTORY_NORMAL = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "monitor_normal" ),
+        new Identifier( ComputerCraft.MOD_ID, "monitor_normal" ),
         f -> new TileMonitor( f, false )
     );
 
     public static final NamedBlockEntityType<TileMonitor> FACTORY_ADVANCED = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "monitor_advanced" ),
+        new Identifier( ComputerCraft.MOD_ID, "monitor_advanced" ),
         f -> new TileMonitor( f, true )
     );
 
@@ -67,17 +69,17 @@ public class TileMonitor extends TileGeneric implements IPeripheralTile
     private int m_xIndex = 0;
     private int m_yIndex = 0;
 
-    public TileMonitor( TileEntityType<? extends TileMonitor> type, boolean advanced )
+    public TileMonitor( BlockEntityType<? extends TileMonitor> type, boolean advanced )
     {
         super( type );
         this.advanced = advanced;
     }
 
     @Override
-    public void onLoad()
+    public void validate()
     {
-        super.onLoad();
-        world.getPendingBlockTicks().scheduleTick( getPos(), getBlockState().getBlock(), 0 );
+        super.validate();
+        TickScheduler.schedule( this );
     }
 
     @Override
@@ -86,29 +88,34 @@ public void destroy()
         // TODO: Call this before using the block
         if( m_destroyed ) return;
         m_destroyed = true;
-        if( !getWorld().isRemote ) contractNeighbours();
+        if( !getWorld().isClient ) contractNeighbours();
     }
 
     @Override
-    public void remove()
+    public void invalidate()
     {
-        super.remove();
+        super.invalidate();
         if( m_clientMonitor != null && m_xIndex == 0 && m_yIndex == 0 ) m_clientMonitor.destroy();
     }
 
+    /*
     @Override
     public void onChunkUnloaded()
     {
         super.onChunkUnloaded();
         if( m_clientMonitor != null && m_xIndex == 0 && m_yIndex == 0 ) m_clientMonitor.destroy();
     }
+    */
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
-        if( !player.isSneaking() && getFront() == side )
+        if( !player.isSneaking() && getFront() == hit.getSide() )
         {
-            if( !getWorld().isRemote ) monitorTouched( hitX, hitY, hitZ );
+            if( !getWorld().isClient )
+            {
+                monitorTouched( (float) hit.getPos().x, (float) hit.getPos().y, (float) hit.getPos().z );
+            }
             return true;
         }
 
@@ -117,19 +124,19 @@ public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side,
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound tag )
+    public CompoundTag toTag( CompoundTag tag )
     {
         tag.putInt( NBT_X, m_xIndex );
         tag.putInt( NBT_Y, m_yIndex );
         tag.putInt( NBT_WIDTH, m_width );
         tag.putInt( NBT_HEIGHT, m_height );
-        return super.write( tag );
+        return super.toTag( tag );
     }
 
     @Override
-    public void read( NBTTagCompound tag )
+    public void fromTag( CompoundTag tag )
     {
-        super.read( tag );
+        super.fromTag( tag );
         m_xIndex = tag.getInt( NBT_X );
         m_yIndex = tag.getInt( NBT_Y );
         m_width = tag.getInt( NBT_WIDTH );
@@ -168,7 +175,7 @@ public void blockTick()
     // IPeripheralTile implementation
 
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         createServerMonitor(); // Ensure the monitor is created before doing anything else.
         if( m_peripheral == null ) m_peripheral = new MonitorPeripheral( this );
@@ -217,7 +224,7 @@ private ServerMonitor createServerMonitor()
             // Otherwise fetch the origin and attempt to get its monitor
             // Note this may load chunks, but we don't really have a choice here.
             BlockPos pos = getPos();
-            TileEntity te = world.getTileEntity( pos.offset( getRight(), -m_xIndex ).offset( getDown(), -m_yIndex ) );
+            BlockEntity te = world.getBlockEntity( pos.offset( getRight(), -m_xIndex ).offset( getDown(), -m_yIndex ) );
             if( !(te instanceof TileMonitor) ) return null;
 
             return m_serverMonitor = ((TileMonitor) te).createServerMonitor();
@@ -229,7 +236,7 @@ public ClientMonitor getClientMonitor()
         if( m_clientMonitor != null ) return m_clientMonitor;
 
         BlockPos pos = getPos();
-        TileEntity te = world.getTileEntity( pos.offset( getRight(), -m_xIndex ).offset( getDown(), -m_yIndex ) );
+        BlockEntity te = world.getBlockEntity( pos.offset( getRight(), -m_xIndex ).offset( getDown(), -m_yIndex ) );
         if( !(te instanceof TileMonitor) ) return null;
 
         return m_clientMonitor = ((TileMonitor) te).m_clientMonitor;
@@ -238,7 +245,7 @@ public ClientMonitor getClientMonitor()
     // Networking stuff
 
     @Override
-    protected void writeDescription( @Nonnull NBTTagCompound nbt )
+    protected void writeDescription( @Nonnull CompoundTag nbt )
     {
         super.writeDescription( nbt );
 
@@ -254,7 +261,7 @@ protected void writeDescription( @Nonnull NBTTagCompound nbt )
     }
 
     @Override
-    protected final void readDescription( @Nonnull NBTTagCompound nbt )
+    protected final void readDescription( @Nonnull CompoundTag nbt )
     {
         super.readDescription( nbt );
 
@@ -293,39 +300,39 @@ protected final void readDescription( @Nonnull NBTTagCompound nbt )
 
     private void updateBlockState()
     {
-        getWorld().setBlockState( getPos(), getBlockState()
+        getWorld().setBlockState( getPos(), getCachedState()
             .with( BlockMonitor.STATE, MonitorEdgeState.fromConnections(
                 m_yIndex < m_height - 1, m_yIndex > 0,
                 m_xIndex > 0, m_xIndex < m_width - 1 ) ), 2 );
     }
 
     // region Sizing and placement stuff
-    public EnumFacing getDirection()
+    public Direction getDirection()
     {
-        return getBlockState().get( BlockMonitor.FACING );
+        return getCachedState().get( BlockMonitor.FACING );
     }
 
-    public EnumFacing getOrientation()
+    public Direction getOrientation()
     {
-        return getBlockState().get( BlockMonitor.ORIENTATION );
+        return getCachedState().get( BlockMonitor.ORIENTATION );
     }
 
-    public EnumFacing getFront()
+    public Direction getFront()
     {
-        EnumFacing orientation = getOrientation();
-        return orientation == EnumFacing.NORTH ? getDirection() : orientation;
+        Direction orientation = getOrientation();
+        return orientation == Direction.NORTH ? getDirection() : orientation;
     }
 
-    public EnumFacing getRight()
+    public Direction getRight()
     {
-        return getDirection().rotateYCCW();
+        return getDirection().rotateYCounterclockwise();
     }
 
-    private EnumFacing getDown()
+    private Direction getDown()
     {
-        EnumFacing orientation = getOrientation();
-        if( orientation == EnumFacing.NORTH ) return EnumFacing.UP;
-        return orientation == EnumFacing.DOWN ? getDirection() : getDirection().getOpposite();
+        Direction orientation = getOrientation();
+        if( orientation == Direction.NORTH ) return Direction.UP;
+        return orientation == Direction.DOWN ? getDirection() : getDirection().getOpposite();
     }
 
     public int getWidth()
@@ -356,7 +363,7 @@ private TileMonitor getSimilarMonitorAt( BlockPos pos )
         World world = getWorld();
         if( world == null || !world.isBlockLoaded( pos ) ) return null;
 
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         if( !(tile instanceof TileMonitor) ) return null;
 
         TileMonitor monitor = (TileMonitor) tile;
@@ -368,8 +375,8 @@ && getDirection() == monitor.getDirection() && getOrientation() == monitor.getOr
     private TileMonitor getNeighbour( int x, int y )
     {
         BlockPos pos = getPos();
-        EnumFacing right = getRight();
-        EnumFacing down = getDown();
+        Direction right = getRight();
+        Direction down = getDown();
         int xOffset = -m_xIndex + x;
         int yOffset = -m_yIndex + y;
         return getSimilarMonitorAt( pos.offset( right, xOffset ).offset( down, yOffset ) );
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java b/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java
index 2fd92b1423..29d6a3279c 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/monitor/XYPair.java
@@ -6,7 +6,7 @@
 
 package dan200.computercraft.shared.peripheral.monitor;
 
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 public class XYPair
 {
@@ -24,7 +24,7 @@ public XYPair add( float x, float y )
         return new XYPair( this.x + x, this.y + y );
     }
 
-    public static XYPair of( float xPos, float yPos, float zPos, EnumFacing facing, EnumFacing orientation )
+    public static XYPair of( float xPos, float yPos, float zPos, Direction facing, Direction orientation )
     {
         switch( orientation )
         {
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java
index 47d538c97c..522828191f 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/BlockPrinter.java
@@ -8,77 +8,56 @@
 
 import dan200.computercraft.shared.common.BlockGeneric;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.stats.StatList;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.INameable;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.BooleanProperty;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.Properties;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
-import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public class BlockPrinter extends BlockGeneric
 {
-    private static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
+    private static final DirectionProperty FACING = Properties.FACING_HORIZONTAL;
     static final BooleanProperty TOP = BooleanProperty.create( "top" );
     static final BooleanProperty BOTTOM = BooleanProperty.create( "bottom" );
 
-    public BlockPrinter( Properties settings )
+    public BlockPrinter( Settings settings )
     {
         super( settings, TilePrinter.FACTORY );
-        setDefaultState( getStateContainer().getBaseState()
-            .with( FACING, EnumFacing.NORTH )
+        setDefaultState( getStateFactory().getDefaultState()
+            .with( FACING, Direction.NORTH )
             .with( TOP, false )
             .with( BOTTOM, false ) );
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> properties )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> properties )
     {
-        properties.add( FACING, TOP, BOTTOM );
+        super.appendProperties( properties );
+        properties.with( FACING, TOP, BOTTOM );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext placement )
+    public BlockState getPlacementState( ItemPlacementContext placement )
     {
-        return getDefaultState().with( FACING, placement.getPlacementHorizontalFacing().getOpposite() );
+        return getDefaultState().with( FACING, placement.getPlayerHorizontalFacing().getOpposite() );
     }
 
     @Override
-    public void harvestBlock( @Nonnull World world, EntityPlayer player, @Nonnull BlockPos pos, @Nonnull IBlockState state, @Nullable TileEntity te, ItemStack stack )
-    {
-        if( te instanceof INameable && ((INameable) te).hasCustomName() )
-        {
-            player.addStat( StatList.BLOCK_MINED.get( this ) );
-            player.addExhaustion( 0.005F );
-
-            ItemStack result = new ItemStack( this );
-            result.setDisplayName( ((INameable) te).getCustomName() );
-            spawnAsEntity( world, pos, result );
-        }
-        else
-        {
-            super.harvestBlock( world, player, pos, state, te, stack );
-        }
-    }
-
-    @Override
-    public void onBlockPlacedBy( World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack )
+    public void onPlaced( World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack )
     {
         if( stack.hasDisplayName() )
         {
-            TileEntity tileentity = world.getTileEntity( pos );
+            BlockEntity tileentity = world.getBlockEntity( pos );
             if( tileentity instanceof TilePrinter ) ((TilePrinter) tileentity).customName = stack.getDisplayName();
         }
     }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java
index 7d6eae5163..61530bf9e4 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/ContainerPrinter.java
@@ -6,27 +6,41 @@
 
 package dan200.computercraft.shared.peripheral.printer;
 
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.inventory.IContainerListener;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.Slot;
-import net.minecraft.item.ItemDye;
+import net.minecraft.container.ArrayPropertyDelegate;
+import net.minecraft.container.Container;
+import net.minecraft.container.PropertyDelegate;
+import net.minecraft.container.Slot;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.inventory.BasicInventory;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.DyeItem;
 import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nonnull;
 
+import static dan200.computercraft.shared.peripheral.printer.TilePrinter.PROPERTY_PRINTING;
+
 public class ContainerPrinter extends Container
 {
-    private static final int PROPERTY_PRINTING = 0;
+    private final Inventory m_printer;
+    private final PropertyDelegate properties;
+
+    public ContainerPrinter( int id, PlayerInventory player, TilePrinter printer )
+    {
+        this( id, player, printer, printer );
+    }
 
-    private final TilePrinter m_printer;
-    private boolean m_lastPrinting;
+    public ContainerPrinter( int id, PlayerInventory player )
+    {
+        this( id, player, new BasicInventory( TilePrinter.INVENTORY_SIZE ), new ArrayPropertyDelegate( TilePrinter.PROPERTY_SIZE ) );
+    }
 
-    public ContainerPrinter( IInventory playerInventory, TilePrinter printer )
+    public ContainerPrinter( int id, PlayerInventory playerInventory, Inventory printer, PropertyDelegate printerInfo )
     {
+        super( null, id );
         m_printer = printer;
-        m_lastPrinting = false;
+        properties = printerInfo;
 
         // Ink slot
         addSlot( new Slot( printer, 0, 13, 35 ) );
@@ -48,98 +62,59 @@ public ContainerPrinter( IInventory playerInventory, TilePrinter printer )
 
         // Player hotbar
         for( int x = 0; x < 9; x++ ) addSlot( new Slot( playerInventory, x, 8 + x * 18, 142 ) );
-    }
 
-    public boolean isPrinting()
-    {
-        return m_lastPrinting;
-    }
-
-    public TilePrinter getPrinter()
-    {
-        return m_printer;
+        addProperties( printerInfo );
     }
 
-    @Override
-    public void addListener( IContainerListener listener )
-    {
-        super.addListener( listener );
-        listener.sendWindowProperty( this, PROPERTY_PRINTING, m_printer.isPrinting() ? 1 : 0 );
-    }
-
-    @Override
-    public void detectAndSendChanges()
-    {
-        super.detectAndSendChanges();
-
-        if( !m_printer.getWorld().isRemote )
-        {
-            // Push the printing state to the client if needed.
-            boolean printing = m_printer.isPrinting();
-            if( printing != m_lastPrinting )
-            {
-                for( IContainerListener listener : listeners )
-                {
-                    listener.sendWindowProperty( this, PROPERTY_PRINTING, printing ? 1 : 0 );
-                }
-                m_lastPrinting = printing;
-            }
-        }
-    }
-
-    @Override
-    public void updateProgressBar( int property, int value )
+    public boolean isPrinting()
     {
-        if( m_printer.getWorld().isRemote )
-        {
-            if( property == PROPERTY_PRINTING ) m_lastPrinting = value != 0;
-        }
+        return properties.get( PROPERTY_PRINTING ) != 0;
     }
 
     @Override
-    public boolean canInteractWith( @Nonnull EntityPlayer player )
+    public boolean canUse( @Nonnull PlayerEntity player )
     {
-        return m_printer.isUsableByPlayer( player );
+        return m_printer.canPlayerUseInv( player );
     }
 
     @Nonnull
     @Override
-    public ItemStack transferStackInSlot( EntityPlayer player, int index )
+    public ItemStack transferSlot( PlayerEntity player, int index )
     {
-        Slot slot = inventorySlots.get( index );
-        if( slot == null || !slot.getHasStack() ) return ItemStack.EMPTY;
+        Slot slot = slotList.get( index );
+        if( slot == null || !slot.hasStack() ) return ItemStack.EMPTY;
         ItemStack stack = slot.getStack();
         ItemStack result = stack.copy();
         if( index < 13 )
         {
             // Transfer from printer to inventory
-            if( !mergeItemStack( stack, 13, 49, true ) ) return ItemStack.EMPTY;
+            if( !insertItem( stack, 13, 49, true ) ) return ItemStack.EMPTY;
         }
         else
         {
             // Transfer from inventory to printer
-            if( stack.getItem() instanceof ItemDye )
+            if( stack.getItem() instanceof DyeItem )
             {
-                if( !mergeItemStack( stack, 0, 1, false ) ) return ItemStack.EMPTY;
+                if( !insertItem( stack, 0, 1, false ) ) return ItemStack.EMPTY;
             }
             else //if is paper
             {
-                if( !mergeItemStack( stack, 1, 13, false ) ) return ItemStack.EMPTY;
+                if( !insertItem( stack, 1, 13, false ) ) return ItemStack.EMPTY;
             }
         }
 
         if( stack.isEmpty() )
         {
-            slot.putStack( ItemStack.EMPTY );
+            slot.setStack( ItemStack.EMPTY );
         }
         else
         {
-            slot.onSlotChanged();
+            slot.markDirty();
         }
 
-        if( stack.getCount() == result.getCount() ) return ItemStack.EMPTY;
+        if( stack.getAmount() == result.getAmount() ) return ItemStack.EMPTY;
 
-        slot.onTake( player, stack );
+        slot.onTakeItem( player, stack );
         return result;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java
index 6f32d3bef7..29ffa20385 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/printer/TilePrinter.java
@@ -13,40 +13,28 @@
 import dan200.computercraft.shared.common.TileGeneric;
 import dan200.computercraft.shared.media.items.ItemPrintout;
 import dan200.computercraft.shared.network.Containers;
-import dan200.computercraft.shared.util.ColourUtils;
-import dan200.computercraft.shared.util.DefaultSidedInventory;
-import dan200.computercraft.shared.util.NamedBlockEntityType;
-import dan200.computercraft.shared.util.WorldUtil;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.init.Items;
-import net.minecraft.inventory.ItemStackHelper;
-import net.minecraft.item.EnumDyeColor;
+import dan200.computercraft.shared.util.*;
+import net.minecraft.block.BlockState;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventories;
+import net.minecraft.item.DyeItem;
 import net.minecraft.item.Item;
-import net.minecraft.item.ItemDye;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.*;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.items.IItemHandlerModifiable;
-import net.minecraftforge.items.wrapper.InvWrapper;
-import net.minecraftforge.items.wrapper.SidedInvWrapper;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY;
-
-public final class TilePrinter extends TileGeneric implements DefaultSidedInventory, IPeripheralTile
+public final class TilePrinter extends TileGeneric implements DefaultSidedInventory, IPeripheralTile, DefaultPropertyDelegate, Nameable
 {
     public static final NamedBlockEntityType<TilePrinter> FACTORY = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "printer" ),
+        new Identifier( ComputerCraft.MOD_ID, "printer" ),
         TilePrinter::new
     );
 
@@ -54,15 +42,19 @@ public final class TilePrinter extends TileGeneric implements DefaultSidedInvent
     private static final String NBT_PRINTING = "Printing";
     private static final String NBT_PAGE_TITLE = "PageTitle";
 
+    public static final int PROPERTY_SIZE = 1;
+    public static final int PROPERTY_PRINTING = 0;
+
+    public static final int INVENTORY_SIZE = 13;
+
     private static final int[] BOTTOM_SLOTS = new int[] { 7, 8, 9, 10, 11, 12 };
     private static final int[] TOP_SLOTS = new int[] { 1, 2, 3, 4, 5, 6 };
     private static final int[] SIDE_SLOTS = new int[] { 0 };
 
-    ITextComponent customName;
+    TextComponent customName;
 
-    private final NonNullList<ItemStack> m_inventory = NonNullList.withSize( 13, ItemStack.EMPTY );
-    private IItemHandlerModifiable m_itemHandlerAll = new InvWrapper( this );
-    private LazyOptional<IItemHandlerModifiable>[] m_itemHandlerSides;
+    private final DefaultedList<ItemStack> m_inventory = DefaultedList.create( INVENTORY_SIZE, ItemStack.EMPTY );
+    private final ItemStorage m_itemHandlerAll = ItemStorage.wrap( this );
 
     private final Terminal m_page = new Terminal( ItemPrintout.LINE_MAX_LENGTH, ItemPrintout.LINES_PER_PAGE );
     private String m_pageTitle = "";
@@ -80,20 +72,20 @@ public void destroy()
     }
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
         if( player.isSneaking() ) return false;
 
-        if( !getWorld().isRemote ) Containers.openPrinterGUI( player, this );
+        if( !getWorld().isClient ) Containers.openPrinterGUI( player, this );
         return true;
     }
 
     @Override
-    public void read( NBTTagCompound nbt )
+    public void fromTag( CompoundTag nbt )
     {
-        super.read( nbt );
+        super.fromTag( nbt );
 
-        customName = nbt.contains( NBT_NAME ) ? ITextComponent.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null;
+        customName = nbt.containsKey( NBT_NAME ) ? TextComponent.Serializer.fromJsonString( nbt.getString( NBT_NAME ) ) : null;
 
         // Read page
         synchronized( m_page )
@@ -106,15 +98,15 @@ public void read( NBTTagCompound nbt )
         // Read inventory
         synchronized( m_inventory )
         {
-            ItemStackHelper.loadAllItems( nbt, m_inventory );
+            Inventories.fromTag( nbt, m_inventory );
         }
     }
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound nbt )
+    public CompoundTag toTag( CompoundTag nbt )
     {
-        if( customName != null ) nbt.putString( NBT_NAME, ITextComponent.Serializer.toJson( customName ) );
+        if( customName != null ) nbt.putString( NBT_NAME, TextComponent.Serializer.toJsonString( customName ) );
 
         // Write page
         synchronized( m_page )
@@ -127,24 +119,24 @@ public NBTTagCompound write( NBTTagCompound nbt )
         // Write inventory
         synchronized( m_inventory )
         {
-            ItemStackHelper.saveAllItems( nbt, m_inventory );
+            Inventories.toTag( nbt, m_inventory );
         }
 
-        return super.write( nbt );
+        return super.toTag( nbt );
     }
 
     @Override
-    protected void writeDescription( @Nonnull NBTTagCompound nbt )
+    protected void writeDescription( @Nonnull CompoundTag nbt )
     {
         super.writeDescription( nbt );
-        if( customName != null ) nbt.putString( NBT_NAME, ITextComponent.Serializer.toJson( customName ) );
+        if( customName != null ) nbt.putString( NBT_NAME, TextComponent.Serializer.toJsonString( customName ) );
     }
 
     @Override
-    public void readDescription( @Nonnull NBTTagCompound nbt )
+    public void readDescription( @Nonnull CompoundTag nbt )
     {
         super.readDescription( nbt );
-        customName = nbt.contains( NBT_NAME ) ? ITextComponent.Serializer.fromJson( nbt.getString( NBT_NAME ) ) : null;
+        customName = nbt.containsKey( NBT_NAME ) ? TextComponent.Serializer.fromJsonString( nbt.getString( NBT_NAME ) ) : null;
         updateBlock();
     }
 
@@ -153,15 +145,15 @@ public boolean isPrinting()
         return m_printing;
     }
 
-    // IInventory implementation
+    // Inventory implementation
     @Override
-    public int getSizeInventory()
+    public int getInvSize()
     {
         return m_inventory.size();
     }
 
     @Override
-    public boolean isEmpty()
+    public boolean isInvEmpty()
     {
         for( ItemStack stack : m_inventory )
         {
@@ -172,14 +164,14 @@ public boolean isEmpty()
 
     @Nonnull
     @Override
-    public ItemStack getStackInSlot( int i )
+    public ItemStack getInvStack( int i )
     {
         return m_inventory.get( i );
     }
 
     @Nonnull
     @Override
-    public ItemStack removeStackFromSlot( int i )
+    public ItemStack removeInvStack( int i )
     {
         synchronized( m_inventory )
         {
@@ -193,13 +185,13 @@ public ItemStack removeStackFromSlot( int i )
 
     @Nonnull
     @Override
-    public ItemStack decrStackSize( int i, int j )
+    public ItemStack takeInvStack( int i, int j )
     {
         synchronized( m_inventory )
         {
             if( m_inventory.get( i ).isEmpty() ) return ItemStack.EMPTY;
 
-            if( m_inventory.get( i ).getCount() <= j )
+            if( m_inventory.get( i ).getAmount() <= j )
             {
                 ItemStack itemstack = m_inventory.get( i );
                 m_inventory.set( i, ItemStack.EMPTY );
@@ -220,7 +212,7 @@ public ItemStack decrStackSize( int i, int j )
     }
 
     @Override
-    public void setInventorySlotContents( int i, @Nonnull ItemStack stack )
+    public void setInvStack( int i, @Nonnull ItemStack stack )
     {
         synchronized( m_inventory )
         {
@@ -242,7 +234,7 @@ public void clear()
     }
 
     @Override
-    public boolean isItemValidForSlot( int slot, @Nonnull ItemStack stack )
+    public boolean isValidInvStack( int slot, @Nonnull ItemStack stack )
     {
         if( slot == 0 )
         {
@@ -259,16 +251,15 @@ else if( slot >= TOP_SLOTS[0] && slot <= TOP_SLOTS[TOP_SLOTS.length - 1] )
     }
 
     @Override
-    public boolean isUsableByPlayer( @Nonnull EntityPlayer playerEntity )
+    public boolean canPlayerUseInv( PlayerEntity playerEntity )
     {
         return isUsable( playerEntity, false );
     }
 
     // ISidedInventory implementation
 
-    @Nonnull
     @Override
-    public int[] getSlotsForFace( @Nonnull EnumFacing side )
+    public int[] getInvAvailableSlots( @Nonnull Direction side )
     {
         switch( side )
         {
@@ -284,7 +275,7 @@ public int[] getSlotsForFace( @Nonnull EnumFacing side )
     // IPeripheralTile implementation
 
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         return new PrinterPeripheral( this );
     }
@@ -321,7 +312,7 @@ public int getInkLevel()
         synchronized( m_inventory )
         {
             ItemStack inkStack = m_inventory.get( 0 );
-            return isInk( inkStack ) ? inkStack.getCount() : 0;
+            return isInk( inkStack ) ? inkStack.getAmount() : 0;
         }
     }
 
@@ -335,7 +326,7 @@ public int getPaperLevel()
                 ItemStack paperStack = m_inventory.get( i );
                 if( !paperStack.isEmpty() && isPaper( paperStack ) )
                 {
-                    count += paperStack.getCount();
+                    count += paperStack.getAmount();
                 }
             }
         }
@@ -352,7 +343,7 @@ public void setPageTitle( String title )
 
     private static boolean isInk( @Nonnull ItemStack stack )
     {
-        return stack.getItem() instanceof ItemDye;
+        return stack.getItem() instanceof DyeItem;
     }
 
     private static boolean isPaper( @Nonnull ItemStack stack )
@@ -384,7 +375,7 @@ private boolean inputPage()
                 if( !paperStack.isEmpty() && isPaper( paperStack ) )
                 {
                     // Setup the new page
-                    EnumDyeColor dye = ColourUtils.getStackColour( inkStack );
+                    DyeColor dye = ColourUtils.getStackColour( inkStack );
                     m_page.setTextColour( dye != null ? dye.getId() : 15 );
 
                     m_page.clear();
@@ -405,11 +396,11 @@ private boolean inputPage()
                     m_page.setCursorPos( 0, 0 );
 
                     // Decrement ink
-                    inkStack.shrink( 1 );
+                    inkStack.subtractAmount( 1 );
                     if( inkStack.isEmpty() ) m_inventory.set( 0, ItemStack.EMPTY );
 
                     // Decrement paper
-                    paperStack.shrink( 1 );
+                    paperStack.subtractAmount( 1 );
                     if( paperStack.isEmpty() )
                     {
                         m_inventory.set( i, ItemStack.EMPTY );
@@ -445,7 +436,7 @@ private boolean outputPage()
                 {
                     if( m_inventory.get( slot ).isEmpty() )
                     {
-                        setInventorySlotContents( slot, stack );
+                        setInvStack( slot, stack );
                         m_printing = false;
                         return true;
                     }
@@ -465,7 +456,7 @@ private void ejectContents()
                 if( !stack.isEmpty() )
                 {
                     // Remove the stack from the inventory
-                    setInventorySlotContents( i, ItemStack.EMPTY );
+                    setInvStack( i, ItemStack.EMPTY );
 
                     // Spawn the item in the world
                     BlockPos pos = getPos();
@@ -508,50 +499,25 @@ private void updateBlockState()
 
     private void updateBlockState( boolean top, boolean bottom )
     {
-        if( removed ) return;
+        if( invalid ) return;
 
-        IBlockState state = getBlockState();
+        BlockState state = getCachedState();
         if( state.get( BlockPrinter.TOP ) == top & state.get( BlockPrinter.BOTTOM ) == bottom ) return;
 
         getWorld().setBlockState( getPos(), state.with( BlockPrinter.TOP, top ).with( BlockPrinter.BOTTOM, bottom ) );
     }
 
-
-    @SuppressWarnings( { "unchecked", "rawtypes" } )
-    @Nonnull
     @Override
-    public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> capability, @Nullable EnumFacing facing )
+    public int get( int property )
     {
-        if( capability == ITEM_HANDLER_CAPABILITY )
-        {
-            LazyOptional<IItemHandlerModifiable>[] handlers = m_itemHandlerSides;
-            if( handlers == null ) handlers = m_itemHandlerSides = new LazyOptional[6];
-
-            LazyOptional<IItemHandlerModifiable> handler;
-            if( facing == null )
-            {
-                int i = 6;
-                handler = handlers[i];
-                if( handler == null )
-                {
-                    handler = handlers[i] = LazyOptional.of( () -> m_itemHandlerAll );
-                }
-            }
-            else
-            {
-
-                int i = facing.ordinal();
-                handler = handlers[i];
-                if( handler == null )
-                {
-                    handler = handlers[i] = LazyOptional.of( () -> new SidedInvWrapper( this, facing ) );
-                }
-            }
-
-            return handler.cast();
-        }
+        if( property == PROPERTY_PRINTING ) return isPrinting() ? 1 : 0;
+        return 0;
+    }
 
-        return super.getCapability( capability, facing );
+    @Override
+    public int size()
+    {
+        return PROPERTY_SIZE;
     }
 
     @Override
@@ -562,15 +528,15 @@ public boolean hasCustomName()
 
     @Nullable
     @Override
-    public ITextComponent getCustomName()
+    public TextComponent getCustomName()
     {
         return customName;
     }
 
     @Nonnull
     @Override
-    public ITextComponent getName()
+    public TextComponent getName()
     {
-        return customName != null ? customName : getBlockState().getBlock().getNameTextComponent();
+        return customName != null ? customName : getCachedState().getBlock().getTextComponent();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java
index d31169fabe..9094596ef8 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/BlockSpeaker.java
@@ -8,36 +8,36 @@
 
 import dan200.computercraft.shared.common.BlockGeneric;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.BlockState;
+import net.minecraft.item.ItemPlacementContext;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.Properties;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nullable;
 
 public class BlockSpeaker extends BlockGeneric
 {
-    private static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
+    private static final DirectionProperty FACING = Properties.FACING_HORIZONTAL;
 
-    public BlockSpeaker( Properties settings )
+    public BlockSpeaker( Settings settings )
     {
         super( settings, TileSpeaker.FACTORY );
-        setDefaultState( getStateContainer().getBaseState()
-            .with( FACING, EnumFacing.NORTH ) );
+        setDefaultState( getStateFactory().getDefaultState()
+            .with( FACING, Direction.NORTH ) );
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> properties )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> properties )
     {
-        properties.add( FACING );
+        properties.with( FACING );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext placement )
+    public BlockState getPlacementState( ItemPlacementContext placement )
     {
-        return getDefaultState().with( FACING, placement.getPlacementHorizontalFacing().getOpposite() );
+        return getDefaultState().with( FACING, placement.getPlayerHorizontalFacing().getOpposite() );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java
index 5cc4da1a43..ba852eb936 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/SpeakerPeripheral.java
@@ -11,12 +11,12 @@
 import dan200.computercraft.api.lua.LuaException;
 import dan200.computercraft.api.peripheral.IComputerAccess;
 import dan200.computercraft.api.peripheral.IPeripheral;
-import net.minecraft.network.play.server.SPacketCustomSound;
+import net.minecraft.block.enums.Instrument;
+import net.minecraft.client.network.packet.PlaySoundIdS2CPacket;
 import net.minecraft.server.MinecraftServer;
-import net.minecraft.state.properties.NoteBlockInstrument;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.ResourceLocationException;
-import net.minecraft.util.SoundCategory;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.InvalidIdentifierException;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 
@@ -75,12 +75,12 @@ public Object[] callMethod( @Nonnull IComputerAccess computerAccess, @Nonnull IL
                 float volume = (float) optReal( args, 1, 1.0 );
                 float pitch = (float) optReal( args, 2, 1.0 );
 
-                ResourceLocation identifier;
+                Identifier identifier;
                 try
                 {
-                    identifier = new ResourceLocation( name );
+                    identifier = new Identifier( name );
                 }
-                catch( ResourceLocationException e )
+                catch( InvalidIdentifierException e )
                 {
                     throw new LuaException( "Malformed sound name '" + name + "' " );
                 }
@@ -103,10 +103,10 @@ private synchronized Object[] playNote( Object[] arguments, ILuaContext context
         float volume = (float) optReal( arguments, 1, 1.0 );
         float pitch = (float) optReal( arguments, 2, 1.0 );
 
-        NoteBlockInstrument instrument = null;
-        for( NoteBlockInstrument testInstrument : NoteBlockInstrument.values() )
+        Instrument instrument = null;
+        for( Instrument testInstrument : Instrument.values() )
         {
-            if( testInstrument.getName().equalsIgnoreCase( name ) )
+            if( testInstrument.asString().equalsIgnoreCase( name ) )
             {
                 instrument = testInstrument;
                 break;
@@ -120,13 +120,13 @@ private synchronized Object[] playNote( Object[] arguments, ILuaContext context
         }
 
         // If the resource location for note block notes changes, this method call will need to be updated
-        boolean success = playSound( context, instrument.getSound().getName(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true );
+        boolean success = playSound( context, instrument.getSound().getId(), volume, (float) Math.pow( 2.0, (pitch - 12.0) / 12.0 ), true );
 
         if( success ) m_notesThisTick.incrementAndGet();
         return new Object[] { success };
     }
 
-    private synchronized boolean playSound( ILuaContext context, ResourceLocation name, float volume, float pitch, boolean isNote ) throws LuaException
+    private synchronized boolean playSound( ILuaContext context, Identifier name, float volume, float pitch, boolean isNote ) throws LuaException
     {
         if( m_clock - m_lastPlayTime < TileSpeaker.MIN_TICKS_BETWEEN_SOUNDS &&
             (!isNote || m_clock - m_lastPlayTime != 0 || m_notesThisTick.get() >= ComputerCraft.maxNotesPerTick) )
@@ -144,9 +144,9 @@ private synchronized boolean playSound( ILuaContext context, ResourceLocation na
             if( server == null ) return null;
 
             float adjVolume = Math.min( volume, 3.0f );
-            server.getPlayerList().sendToAllNearExcept(
-                null, pos.x, pos.y, pos.z, adjVolume > 1.0f ? 16 * adjVolume : 16.0, world.dimension.getType(),
-                new SPacketCustomSound( name, SoundCategory.RECORDS, pos, adjVolume, pitch )
+            server.getPlayerManager().sendToAround(
+                null, pos.x, pos.y, pos.z, adjVolume > 1.0f ? 16 * adjVolume : 16.0, world.getDimension().getType(),
+                new PlaySoundIdS2CPacket( name, SoundCategory.RECORD, pos, adjVolume, pitch )
             );
             return null;
         } );
diff --git a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java
index 667af245de..b1c34de994 100644
--- a/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java
+++ b/src/main/java/dan200/computercraft/shared/peripheral/speaker/TileSpeaker.java
@@ -11,22 +11,22 @@
 import dan200.computercraft.api.peripheral.IPeripheralTile;
 import dan200.computercraft.shared.common.TileGeneric;
 import dan200.computercraft.shared.util.NamedBlockEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ITickable;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Tickable;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-public class TileSpeaker extends TileGeneric implements ITickable, IPeripheralTile
+public class TileSpeaker extends TileGeneric implements Tickable, IPeripheralTile
 {
     public static final int MIN_TICKS_BETWEEN_SOUNDS = 1;
 
     public static final NamedBlockEntityType<TileSpeaker> FACTORY = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "speaker" ),
+        new Identifier( ComputerCraft.MOD_ID, "speaker" ),
         TileSpeaker::new
     );
 
@@ -45,7 +45,7 @@ public void tick()
     }
 
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         return m_peripheral;
     }
diff --git a/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java b/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java
index 078a6ac8c8..53d4ed83d6 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/apis/PocketAPI.java
@@ -13,13 +13,13 @@
 import dan200.computercraft.shared.PocketUpgrades;
 import dan200.computercraft.shared.pocket.core.PocketServerComputer;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.InventoryPlayer;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.NonNullList;
-import net.minecraftforge.items.wrapper.PlayerMainInvWrapper;
+import net.minecraft.util.DefaultedList;
 
 import javax.annotation.Nonnull;
 
@@ -60,17 +60,17 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
                 return context.executeMainThreadTask( () ->
                 {
                     Entity entity = m_computer.getEntity();
-                    if( !(entity instanceof EntityPlayer) ) return new Object[] { false, "Cannot find player" };
-                    EntityPlayer player = (EntityPlayer) entity;
-                    InventoryPlayer inventory = player.inventory;
+                    if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" };
+                    PlayerEntity player = (PlayerEntity) entity;
+                    PlayerInventory inventory = player.inventory;
                     IPocketUpgrade previousUpgrade = m_computer.getUpgrade();
 
                     // Attempt to find the upgrade, starting in the main segment, and then looking in the opposite
                     // one. We start from the position the item is currently in and loop round to the start.
-                    IPocketUpgrade newUpgrade = findUpgrade( inventory.mainInventory, inventory.currentItem, previousUpgrade );
+                    IPocketUpgrade newUpgrade = findUpgrade( inventory.main, inventory.selectedSlot, previousUpgrade );
                     if( newUpgrade == null )
                     {
-                        newUpgrade = findUpgrade( inventory.offHandInventory, 0, previousUpgrade );
+                        newUpgrade = findUpgrade( inventory.offHand, 0, previousUpgrade );
                     }
                     if( newUpgrade == null ) return new Object[] { false, "Cannot find a valid upgrade" };
 
@@ -80,10 +80,10 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
                         ItemStack stack = previousUpgrade.getCraftingItem();
                         if( !stack.isEmpty() )
                         {
-                            stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.currentItem );
+                            stack = InventoryUtil.storeItems( stack, ItemStorage.wrap( inventory ).view( 0, 36 ), inventory.selectedSlot );
                             if( !stack.isEmpty() )
                             {
-                                WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.posX, player.posY, player.posZ );
+                                WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.x, player.y, player.z );
                             }
                         }
                     }
@@ -99,9 +99,9 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
                 return context.executeMainThreadTask( () ->
                 {
                     Entity entity = m_computer.getEntity();
-                    if( !(entity instanceof EntityPlayer) ) return new Object[] { false, "Cannot find player" };
-                    EntityPlayer player = (EntityPlayer) entity;
-                    InventoryPlayer inventory = player.inventory;
+                    if( !(entity instanceof PlayerEntity) ) return new Object[] { false, "Cannot find player" };
+                    PlayerEntity player = (PlayerEntity) entity;
+                    PlayerInventory inventory = player.inventory;
                     IPocketUpgrade previousUpgrade = m_computer.getUpgrade();
 
                     if( previousUpgrade == null ) return new Object[] { false, "Nothing to unequip" };
@@ -111,10 +111,10 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
                     ItemStack stack = previousUpgrade.getCraftingItem();
                     if( !stack.isEmpty() )
                     {
-                        stack = InventoryUtil.storeItems( stack, new PlayerMainInvWrapper( inventory ), inventory.currentItem );
+                        stack = InventoryUtil.storeItems( stack, ItemStorage.wrap( inventory ).view( 0, 36 ), inventory.selectedSlot );
                         if( stack.isEmpty() )
                         {
-                            WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.posX, player.posY, player.posZ );
+                            WorldUtil.dropItemStack( stack, player.getEntityWorld(), player.x, player.y, player.z );
                         }
                     }
 
@@ -125,7 +125,7 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
         }
     }
 
-    private static IPocketUpgrade findUpgrade( NonNullList<ItemStack> inv, int start, IPocketUpgrade previous )
+    private static IPocketUpgrade findUpgrade( DefaultedList<ItemStack> inv, int start, IPocketUpgrade previous )
     {
         for( int i = 0; i < inv.size(); i++ )
         {
@@ -138,7 +138,7 @@ private static IPocketUpgrade findUpgrade( NonNullList<ItemStack> inv, int start
                 {
                     // Consume an item from this stack and exit the loop
                     invStack = invStack.copy();
-                    invStack.shrink( 1 );
+                    invStack.subtractAmount( 1 );
                     inv.set( (i + start) % inv.size(), invStack.isEmpty() ? ItemStack.EMPTY : invStack );
 
                     return newUpgrade;
diff --git a/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java b/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java
index f805055472..a49956b6c5 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/core/PocketServerComputer.java
@@ -15,16 +15,16 @@
 import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.network.NetworkHandler;
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
+import dan200.computercraft.shared.util.NBTUtil;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.entity.player.InventoryPlayer;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.Constants;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -51,15 +51,15 @@ public Entity getEntity()
         Entity entity = m_entity;
         if( entity == null || m_stack == null || !entity.isAlive() ) return null;
 
-        if( entity instanceof EntityPlayer )
+        if( entity instanceof PlayerEntity )
         {
-            InventoryPlayer inventory = ((EntityPlayer) entity).inventory;
-            return inventory.mainInventory.contains( m_stack ) || inventory.offHandInventory.contains( m_stack ) ? entity : null;
+            PlayerInventory inventory = ((PlayerEntity) entity).inventory;
+            return inventory.main.contains( m_stack ) || inventory.offHand.contains( m_stack ) ? entity : null;
         }
-        else if( entity instanceof EntityLivingBase )
+        else if( entity instanceof LivingEntity )
         {
-            EntityLivingBase living = (EntityLivingBase) entity;
-            return living.getHeldItemMainhand() == m_stack || living.getHeldItemOffhand() == m_stack ? entity : null;
+            LivingEntity living = (LivingEntity) entity;
+            return living.getMainHandStack() == m_stack || living.getOffHandStack() == m_stack ? entity : null;
         }
         else
         {
@@ -83,23 +83,23 @@ public void setColour( int colour )
     @Override
     public int getLight()
     {
-        NBTTagCompound tag = getUserData();
-        return tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) ? tag.getInt( NBT_LIGHT ) : -1;
+        CompoundTag tag = getUserData();
+        return tag.containsKey( NBT_LIGHT, NBTUtil.TAG_ANY_NUMERIC ) ? tag.getInt( NBT_LIGHT ) : -1;
     }
 
     @Override
     public void setLight( int colour )
     {
-        NBTTagCompound tag = getUserData();
+        CompoundTag tag = getUserData();
         if( colour >= 0 && colour <= 0xFFFFFF )
         {
-            if( !tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) || tag.getInt( NBT_LIGHT ) != colour )
+            if( !tag.containsKey( NBT_LIGHT, NBTUtil.TAG_ANY_NUMERIC ) || tag.getInt( NBT_LIGHT ) != colour )
             {
                 tag.putInt( NBT_LIGHT, colour );
                 updateUserData();
             }
         }
-        else if( tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) )
+        else if( tag.containsKey( NBT_LIGHT, NBTUtil.TAG_ANY_NUMERIC ) )
         {
             tag.remove( NBT_LIGHT );
             updateUserData();
@@ -108,7 +108,7 @@ else if( tag.contains( NBT_LIGHT, Constants.NBT.TAG_ANY_NUMERIC ) )
 
     @Nonnull
     @Override
-    public NBTTagCompound getUpgradeNBTData()
+    public CompoundTag getUpgradeNBTData()
     {
         return ItemPocketComputer.getUpgradeInfo( m_stack );
     }
@@ -116,7 +116,7 @@ public NBTTagCompound getUpgradeNBTData()
     @Override
     public void updateUpgradeNBTData()
     {
-        if( m_entity instanceof EntityPlayer ) ((EntityPlayer) m_entity).inventory.markDirty();
+        if( m_entity instanceof PlayerEntity ) ((PlayerEntity) m_entity).inventory.markDirty();
     }
 
     @Override
@@ -128,7 +128,7 @@ public void invalidatePeripheral()
 
     @Nonnull
     @Override
-    public Map<ResourceLocation, IPeripheral> getUpgrades()
+    public Map<Identifier, IPeripheral> getUpgrades()
     {
         return m_upgrade == null ? Collections.emptyMap() : Collections.singletonMap( m_upgrade.getUpgradeID(), getPeripheral( 2 ) );
     }
@@ -163,11 +163,11 @@ public synchronized void updateValues( Entity entity, @Nonnull ItemStack stack,
         if( entity != null )
         {
             setWorld( entity.getEntityWorld() );
-            setPosition( entity.getPosition() );
+            setPosition( entity.getBlockPos() );
         }
 
         // If a new entity has picked it up then rebroadcast the terminal to them
-        if( entity != m_entity && entity instanceof EntityPlayerMP ) markTerminalChanged();
+        if( entity != m_entity && entity instanceof ServerPlayerEntity ) markTerminalChanged();
 
         m_entity = entity;
         m_stack = stack;
@@ -184,11 +184,11 @@ public void broadcastState( boolean force )
     {
         super.broadcastState( force );
 
-        if( (hasTerminalChanged() || force) && m_entity instanceof EntityPlayerMP )
+        if( (hasTerminalChanged() || force) && m_entity instanceof ServerPlayerEntity )
         {
             // Broadcast the state to the current entity if they're not already interacting with it.
-            EntityPlayerMP player = (EntityPlayerMP) m_entity;
-            if( player.connection != null && !isInteracting( player ) )
+            ServerPlayerEntity player = (ServerPlayerEntity) m_entity;
+            if( player.networkHandler != null && !isInteracting( player ) )
             {
                 NetworkHandler.sendToPlayer( player, createTerminalPacket() );
             }
diff --git a/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java
index 9619ba9e35..538172e119 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/inventory/ContainerPocketComputer.java
@@ -11,9 +11,9 @@
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.InputState;
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumHand;
+import net.minecraft.util.Hand;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -22,9 +22,9 @@ public class ContainerPocketComputer extends ContainerHeldItem implements IConta
 {
     private final InputState input = new InputState( this );
 
-    public ContainerPocketComputer( EntityPlayer player, EnumHand hand )
+    public ContainerPocketComputer( int id, PlayerEntity player, Hand hand )
     {
-        super( player, hand );
+        super( id, player, hand );
     }
 
     @Nullable
@@ -44,9 +44,9 @@ public InputState getInput()
     }
 
     @Override
-    public void onContainerClosed( EntityPlayer player )
+    public void close( PlayerEntity player )
     {
-        super.onContainerClosed( player );
+        super.close( player );
         input.close();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java
index 6cbf405f50..58d473a051 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/items/ItemPocketComputer.java
@@ -22,23 +22,23 @@
 import dan200.computercraft.shared.network.Containers;
 import dan200.computercraft.shared.pocket.apis.PocketAPI;
 import dan200.computercraft.shared.pocket.core.PocketServerComputer;
-import net.minecraft.client.util.ITooltipFlag;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.item.TooltipContext;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.item.IItemPropertyGetter;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemGroup;
+import net.minecraft.item.ItemPropertyGetter;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TextFormat;
+import net.minecraft.text.TranslatableTextComponent;
 import net.minecraft.util.*;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
-import net.minecraft.util.text.TextFormatting;
 import net.minecraft.world.World;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -55,28 +55,28 @@ public class ItemPocketComputer extends Item implements IComputerItem, IMedia, I
 
     private final ComputerFamily family;
 
-    public ItemPocketComputer( Properties settings, ComputerFamily family )
+    public ItemPocketComputer( Settings settings, ComputerFamily family )
     {
         super( settings );
         this.family = family;
-        addPropertyOverride( new ResourceLocation( ComputerCraft.MOD_ID, "state" ), COMPUTER_STATE );
-        addPropertyOverride( new ResourceLocation( ComputerCraft.MOD_ID, "coloured" ), COMPUTER_COLOURED );
+        addProperty( new Identifier( ComputerCraft.MOD_ID, "state" ), COMPUTER_STATE );
+        addProperty( new Identifier( ComputerCraft.MOD_ID, "coloured" ), COMPUTER_COLOURED );
     }
 
     public ItemStack create( int id, String label, int colour, IPocketUpgrade upgrade )
     {
         ItemStack result = new ItemStack( this );
         if( id >= 0 ) result.getOrCreateTag().putInt( NBT_ID, id );
-        if( label != null ) result.setDisplayName( new TextComponentString( label ) );
+        if( label != null ) result.setDisplayName( new StringTextComponent( label ) );
         if( upgrade != null ) result.getOrCreateTag().putString( NBT_UPGRADE, upgrade.getUpgradeID().toString() );
         if( colour != -1 ) result.getOrCreateTag().putInt( NBT_COLOUR, colour );
         return result;
     }
 
     @Override
-    public void fillItemGroup( @Nonnull ItemGroup group, @Nonnull NonNullList<ItemStack> stacks )
+    public void appendItemsForGroup( ItemGroup group, DefaultedList<ItemStack> stacks )
     {
-        if( !isInGroup( group ) ) return;
+        if( !isInItemGroup( group ) ) return;
         stacks.add( create( -1, null, -1, null ) );
         for( IPocketUpgrade upgrade : PocketUpgrades.getVanillaUpgrades() )
         {
@@ -85,12 +85,12 @@ public void fillItemGroup( @Nonnull ItemGroup group, @Nonnull NonNullList<ItemSt
     }
 
     @Override
-    public void inventoryTick( ItemStack stack, World world, Entity entity, int slotNum, boolean selected )
+    public void onEntityTick( ItemStack stack, World world, Entity entity, int slotNum, boolean selected )
     {
-        if( !world.isRemote )
+        if( !world.isClient )
         {
             // Server side
-            IInventory inventory = entity instanceof EntityPlayer ? ((EntityPlayer) entity).inventory : null;
+            Inventory inventory = entity instanceof PlayerEntity ? ((PlayerEntity) entity).inventory : null;
             PocketServerComputer computer = createServerComputer( world, inventory, entity, stack );
             if( computer != null )
             {
@@ -133,10 +133,10 @@ public void inventoryTick( ItemStack stack, World world, Entity entity, int slot
 
     @Nonnull
     @Override
-    public ActionResult<ItemStack> onItemRightClick( World world, EntityPlayer player, @Nonnull EnumHand hand )
+    public TypedActionResult<ItemStack> use( World world, PlayerEntity player, @Nonnull Hand hand )
     {
-        ItemStack stack = player.getHeldItem( hand );
-        if( !world.isRemote )
+        ItemStack stack = player.getStackInHand( hand );
+        if( !world.isClient )
         {
             PocketServerComputer computer = createServerComputer( world, player.inventory, player, stack );
 
@@ -155,61 +155,45 @@ public ActionResult<ItemStack> onItemRightClick( World world, EntityPlayer playe
 
             if( !stop ) Containers.openPocketComputerGUI( player, hand );
         }
-        return new ActionResult<>( EnumActionResult.SUCCESS, stack );
+        return new TypedActionResult<>( ActionResult.SUCCESS, stack );
     }
 
     @Nonnull
     @Override
-    public ITextComponent getDisplayName( @Nonnull ItemStack stack )
+    public TextComponent getTranslatedNameTrimmed( @Nonnull ItemStack stack )
     {
         String baseString = getTranslationKey( stack );
         IPocketUpgrade upgrade = getUpgrade( stack );
         if( upgrade != null )
         {
-            return new TextComponentTranslation( baseString + ".upgraded",
-                new TextComponentTranslation( upgrade.getUnlocalisedAdjective() )
+            return new TranslatableTextComponent( baseString + ".upgraded",
+                new TranslatableTextComponent( upgrade.getUnlocalisedAdjective() )
             );
         }
         else
         {
-            return super.getDisplayName( stack );
+            return super.getTranslatedNameTrimmed( stack );
         }
     }
 
 
     @Override
-    public void addInformation( @Nonnull ItemStack stack, @Nullable World world, List<ITextComponent> list, ITooltipFlag flag )
+    public void buildTooltip( ItemStack stack, @Nullable World world, List<TextComponent> list, TooltipContext flag )
     {
         if( flag.isAdvanced() )
         {
             int id = getComputerID( stack );
             if( id >= 0 )
             {
-                list.add( new TextComponentTranslation( "gui.computercraft.tooltip.computer_id", id )
-                    .applyTextStyle( TextFormatting.GRAY ) );
+                list.add( new TranslatableTextComponent( "gui.computercraft.tooltip.computer_id", id )
+                    .applyFormat( TextFormat.GRAY ) );
             }
         }
     }
 
-    @Nullable
-    @Override
-    public String getCreatorModId( ItemStack stack )
-    {
-        IPocketUpgrade upgrade = getUpgrade( stack );
-        if( upgrade != null )
-        {
-            // If we're a non-vanilla, non-CC upgrade then return whichever mod this upgrade
-            // belongs to.
-            String mod = PocketUpgrades.getOwner( upgrade );
-            if( mod != null && !mod.equals( ComputerCraft.MOD_ID ) ) return mod;
-        }
-
-        return super.getCreatorModId( stack );
-    }
-
-    private PocketServerComputer createServerComputer( final World world, IInventory inventory, Entity entity, @Nonnull ItemStack stack )
+    private PocketServerComputer createServerComputer( final World world, Inventory inventory, Entity entity, @Nonnull ItemStack stack )
     {
-        if( world.isRemote ) return null;
+        if( world.isClient ) return null;
 
         PocketServerComputer computer;
         int instanceID = getInstanceID( stack );
@@ -312,11 +296,11 @@ public boolean setLabel( @Nonnull ItemStack stack, String label )
     {
         if( label != null )
         {
-            stack.setDisplayName( new TextComponentString( label ) );
+            stack.setDisplayName( new StringTextComponent( label ) );
         }
         else
         {
-            stack.clearCustomName();
+            stack.removeDisplayName();
         }
         return true;
     }
@@ -334,8 +318,8 @@ public IMount createDataMount( @Nonnull ItemStack stack, @Nonnull World world )
 
     private static int getInstanceID( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_INSTANCE ) ? nbt.getInt( NBT_INSTANCE ) : -1;
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_INSTANCE ) ? nbt.getInt( NBT_INSTANCE ) : -1;
     }
 
     private static void setInstanceID( @Nonnull ItemStack stack, int instanceID )
@@ -345,8 +329,8 @@ private static void setInstanceID( @Nonnull ItemStack stack, int instanceID )
 
     private static int getSessionID( @Nonnull ItemStack stack )
     {
-        NBTTagCompound nbt = stack.getTag();
-        return nbt != null && nbt.contains( NBT_SESSION ) ? nbt.getInt( NBT_SESSION ) : -1;
+        CompoundTag nbt = stack.getTag();
+        return nbt != null && nbt.containsKey( NBT_SESSION ) ? nbt.getInt( NBT_SESSION ) : -1;
     }
 
     private static void setSessionID( @Nonnull ItemStack stack, int sessionID )
@@ -354,21 +338,21 @@ private static void setSessionID( @Nonnull ItemStack stack, int sessionID )
         stack.getOrCreateTag().putInt( NBT_SESSION, sessionID );
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     public static ComputerState getState( @Nonnull ItemStack stack )
     {
         ClientComputer computer = getClientComputer( stack );
         return computer == null ? ComputerState.OFF : computer.getState();
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     public static int getLightState( @Nonnull ItemStack stack )
     {
         ClientComputer computer = getClientComputer( stack );
         if( computer != null && computer.isOn() )
         {
-            NBTTagCompound computerNBT = computer.getUserData();
-            if( computerNBT != null && computerNBT.contains( NBT_LIGHT ) )
+            CompoundTag computerNBT = computer.getUserData();
+            if( computerNBT != null && computerNBT.containsKey( NBT_LIGHT ) )
             {
                 return computerNBT.getInt( NBT_LIGHT );
             }
@@ -378,15 +362,15 @@ public static int getLightState( @Nonnull ItemStack stack )
 
     public static IPocketUpgrade getUpgrade( @Nonnull ItemStack stack )
     {
-        NBTTagCompound compound = stack.getTag();
-        return compound != null && compound.contains( NBT_UPGRADE )
+        CompoundTag compound = stack.getTag();
+        return compound != null && compound.containsKey( NBT_UPGRADE )
             ? PocketUpgrades.get( compound.getString( NBT_UPGRADE ) ) : null;
 
     }
 
     public static void setUpgrade( @Nonnull ItemStack stack, IPocketUpgrade upgrade )
     {
-        NBTTagCompound compound = stack.getOrCreateTag();
+        CompoundTag compound = stack.getOrCreateTag();
 
         if( upgrade == null )
         {
@@ -400,11 +384,11 @@ public static void setUpgrade( @Nonnull ItemStack stack, IPocketUpgrade upgrade
         compound.remove( NBT_UPGRADE_INFO );
     }
 
-    public static NBTTagCompound getUpgradeInfo( @Nonnull ItemStack stack )
+    public static CompoundTag getUpgradeInfo( @Nonnull ItemStack stack )
     {
-        return stack.getOrCreateChildTag( NBT_UPGRADE_INFO );
+        return stack.getOrCreateSubCompoundTag( NBT_UPGRADE_INFO );
     }
 
-    private static final IItemPropertyGetter COMPUTER_STATE = ( stack, world, player ) -> getState( stack ).ordinal();
-    private static final IItemPropertyGetter COMPUTER_COLOURED = ( stack, world, player ) -> IColouredItem.getColourBasic( stack ) != -1 ? 1 : 0;
+    private static final ItemPropertyGetter COMPUTER_STATE = ( stack, world, player ) -> getState( stack ).ordinal();
+    private static final ItemPropertyGetter COMPUTER_COLOURED = ( stack, world, player ) -> IColouredItem.getColourBasic( stack ) != -1 ? 1 : 0;
 }
diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java
index 6fdd707cb7..4ddc42b7cb 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketModem.java
@@ -12,7 +12,7 @@
 import dan200.computercraft.api.pocket.IPocketAccess;
 import dan200.computercraft.shared.peripheral.modem.ModemState;
 import net.minecraft.entity.Entity;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -24,7 +24,7 @@ public class PocketModem extends AbstractPocketUpgrade
     public PocketModem( boolean advanced )
     {
         super(
-            new ResourceLocation( "computercraft", advanced ? "wireless_modem_advanced" : "wireless_modem_normal" ),
+            new Identifier( "computercraft", advanced ? "wireless_modem_advanced" : "wireless_modem_normal" ),
             advanced
                 ? ComputerCraft.Blocks.wirelessModemAdvanced
                 : ComputerCraft.Blocks.wirelessModemNormal
@@ -48,7 +48,7 @@ public void update( @Nonnull IPocketAccess access, @Nullable IPeripheral periphe
 
         PocketModemPeripheral modem = (PocketModemPeripheral) peripheral;
 
-        if( entity != null ) modem.setLocation( entity.getEntityWorld(), entity.getEyePosition( 1 ) );
+        if( entity != null ) modem.setLocation( entity.getEntityWorld(), entity.getCameraPosVec( 1 ) );
 
         ModemState state = modem.getModemState();
         if( state.pollChanged() ) access.setLight( state.isOpen() ? 0xBA0000 : -1 );
diff --git a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java
index 65a4e07fd7..c7deb0856e 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/peripherals/PocketSpeaker.java
@@ -11,7 +11,7 @@
 import dan200.computercraft.api.pocket.AbstractPocketUpgrade;
 import dan200.computercraft.api.pocket.IPocketAccess;
 import net.minecraft.entity.Entity;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -20,7 +20,7 @@ public class PocketSpeaker extends AbstractPocketUpgrade
 {
     public PocketSpeaker()
     {
-        super( new ResourceLocation( "computercraft", "speaker" ), ComputerCraft.Blocks.speaker );
+        super( new Identifier( "computercraft", "speaker" ), ComputerCraft.Blocks.speaker );
     }
 
     @Nullable
@@ -40,7 +40,7 @@ public void update( @Nonnull IPocketAccess access, @Nullable IPeripheral periphe
         Entity entity = access.getEntity();
         if( entity != null )
         {
-            speaker.setLocation( entity.getEntityWorld(), entity.getEyePosition( 1 ) );
+            speaker.setLocation( entity.getEntityWorld(), entity.getCameraPosVec( 1 ) );
         }
 
         speaker.update();
diff --git a/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java b/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java
index 76fbac517f..325d753028 100644
--- a/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/pocket/recipes/PocketComputerUpgradeRecipe.java
@@ -6,51 +6,50 @@
 
 package dan200.computercraft.shared.pocket.recipes;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.pocket.IPocketUpgrade;
 import dan200.computercraft.shared.PocketUpgrades;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.pocket.items.ItemPocketComputer;
 import dan200.computercraft.shared.pocket.items.PocketComputerItemFactory;
-import dan200.computercraft.shared.util.AbstractRecipe;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.SpecialRecipeSerializer;
+import net.minecraft.recipe.crafting.SpecialCraftingRecipe;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
-public final class PocketComputerUpgradeRecipe extends AbstractRecipe
+public final class PocketComputerUpgradeRecipe extends SpecialCraftingRecipe
 {
-    private PocketComputerUpgradeRecipe( ResourceLocation identifier )
+    private PocketComputerUpgradeRecipe( Identifier identifier )
     {
         super( identifier );
     }
 
     @Override
-    public boolean canFit( int x, int y )
+    public boolean fits( int x, int y )
     {
         return x >= 2 && y >= 2;
     }
 
     @Nonnull
     @Override
-    public ItemStack getRecipeOutput()
+    public ItemStack getOutput()
     {
         return PocketComputerItemFactory.create( -1, null, -1, ComputerFamily.Normal, null );
     }
 
     @Override
-    public boolean matches( @Nonnull IInventory inventory, @Nonnull World world )
+    public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world )
     {
-        return !getCraftingResult( inventory ).isEmpty();
+        return !craft( inventory ).isEmpty();
     }
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inventory )
+    public ItemStack craft( @Nonnull CraftingInventory inventory )
     {
         // Scan the grid for a pocket computer
         ItemStack computer = ItemStack.EMPTY;
@@ -61,7 +60,7 @@ public ItemStack getCraftingResult( @Nonnull IInventory inventory )
         {
             for( int x = 0; x < inventory.getWidth(); x++ )
             {
-                ItemStack item = inventory.getStackInSlot( x + y * inventory.getWidth() );
+                ItemStack item = inventory.getInvStack( x + y * inventory.getWidth() );
                 if( !item.isEmpty() && item.getItem() instanceof ItemPocketComputer )
                 {
                     computer = item;
@@ -83,7 +82,7 @@ public ItemStack getCraftingResult( @Nonnull IInventory inventory )
         {
             for( int x = 0; x < inventory.getWidth(); x++ )
             {
-                ItemStack item = inventory.getStackInSlot( x + y * inventory.getWidth() );
+                ItemStack item = inventory.getInvStack( x + y * inventory.getWidth() );
                 if( x == computerX && y == computerY ) continue;
 
                 if( x == computerX && y == computerY - 1 )
@@ -108,15 +107,11 @@ else if( !item.isEmpty() )
         return PocketComputerItemFactory.create( computerID, label, colour, family, upgrade );
     }
 
-    @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    public static final IRecipeSerializer<PocketComputerUpgradeRecipe> SERIALIZER = new RecipeSerializers.SimpleSerializer<>(
-        ComputerCraft.MOD_ID + ":pocket_computer_upgrade",
-        PocketComputerUpgradeRecipe::new
-    );
+    public static final RecipeSerializer<PocketComputerUpgradeRecipe> SERIALIZER = new SpecialRecipeSerializer<>( PocketComputerUpgradeRecipe::new );
 }
diff --git a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java
index 66e749df69..51bbf65d2c 100644
--- a/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java
+++ b/src/main/java/dan200/computercraft/shared/proxy/ComputerCraftProxyCommon.java
@@ -10,16 +10,16 @@
 import dan200.computercraft.api.ComputerCraftAPI;
 import dan200.computercraft.api.media.IMedia;
 import dan200.computercraft.api.peripheral.IPeripheralTile;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
+import dan200.computercraft.client.FrameInfo;
 import dan200.computercraft.core.computer.MainThread;
 import dan200.computercraft.core.tracking.Tracking;
-import dan200.computercraft.shared.Config;
+import dan200.computercraft.shared.Registry;
+import dan200.computercraft.shared.TurtlePermissions;
 import dan200.computercraft.shared.command.CommandComputerCraft;
 import dan200.computercraft.shared.command.arguments.ArgumentSerializers;
 import dan200.computercraft.shared.common.ColourableRecipe;
 import dan200.computercraft.shared.common.DefaultBundledRedstoneProvider;
-import dan200.computercraft.shared.computer.core.IComputer;
-import dan200.computercraft.shared.computer.core.IContainerComputer;
-import dan200.computercraft.shared.computer.core.ServerComputer;
 import dan200.computercraft.shared.computer.recipe.ComputerUpgradeRecipe;
 import dan200.computercraft.shared.media.items.RecordMedia;
 import dan200.computercraft.shared.media.recipes.DiskRecipe;
@@ -29,64 +29,68 @@
 import dan200.computercraft.shared.peripheral.commandblock.CommandBlockPeripheral;
 import dan200.computercraft.shared.peripheral.modem.wireless.WirelessNetwork;
 import dan200.computercraft.shared.pocket.recipes.PocketComputerUpgradeRecipe;
+import dan200.computercraft.shared.turtle.FurnaceRefuelHandler;
 import dan200.computercraft.shared.turtle.recipes.TurtleRecipe;
 import dan200.computercraft.shared.turtle.recipes.TurtleUpgradeRecipe;
 import dan200.computercraft.shared.util.ImpostorRecipe;
 import dan200.computercraft.shared.util.ImpostorShapelessRecipe;
-import dan200.computercraft.shared.wired.CapabilityWiredElement;
-import net.minecraft.inventory.Container;
+import dan200.computercraft.shared.util.TickScheduler;
+import net.fabricmc.fabric.api.event.client.ClientTickCallback;
+import net.fabricmc.fabric.api.event.server.ServerStartCallback;
+import net.fabricmc.fabric.api.event.server.ServerStopCallback;
+import net.fabricmc.fabric.api.event.server.ServerTickCallback;
+import net.fabricmc.fabric.api.registry.CommandRegistry;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.block.entity.CommandBlockBlockEntity;
 import net.minecraft.item.Item;
-import net.minecraft.item.ItemRecord;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityCommandBlock;
-import net.minecraftforge.event.entity.player.PlayerContainerEvent;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.client.event.ConfigChangedEvent;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.common.gameevent.TickEvent;
-import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
-import net.minecraftforge.fml.event.server.FMLServerStartedEvent;
-import net.minecraftforge.fml.event.server.FMLServerStartingEvent;
-import net.minecraftforge.fml.event.server.FMLServerStoppedEvent;
-
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD )
-public final class ComputerCraftProxyCommon
+import net.minecraft.item.MusicDiscItem;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.util.registry.MutableRegistry;
+
+public class ComputerCraftProxyCommon
 {
-    @SubscribeEvent
-    public static void init( FMLCommonSetupEvent event )
+    private static MinecraftServer server;
+
+    public static void setup()
     {
         NetworkHandler.setup();
+
+        Registry.registerBlocks( net.minecraft.util.registry.Registry.BLOCK );
+        Registry.registerTileEntities( (MutableRegistry<BlockEntityType<?>>) net.minecraft.util.registry.Registry.BLOCK_ENTITY );
+        Registry.registerItems( net.minecraft.util.registry.Registry.ITEM );
+        Registry.registerRecipes( (MutableRegistry<RecipeSerializer<?>>) net.minecraft.util.registry.Registry.RECIPE_SERIALIZER );
+
         Containers.setup();
 
         registerProviders();
-
-        RecipeSerializers.register( ColourableRecipe.SERIALIZER );
-        RecipeSerializers.register( ComputerUpgradeRecipe.SERIALIZER );
-        RecipeSerializers.register( PocketComputerUpgradeRecipe.SERIALIZER );
-        RecipeSerializers.register( DiskRecipe.SERIALIZER );
-        RecipeSerializers.register( PrintoutRecipe.SERIALIZER );
-        RecipeSerializers.register( TurtleRecipe.SERIALIZER );
-        RecipeSerializers.register( TurtleUpgradeRecipe.SERIALIZER );
-        RecipeSerializers.register( ImpostorShapelessRecipe.SERIALIZER );
-        RecipeSerializers.register( ImpostorRecipe.SERIALIZER );
+        registerHandlers();
+
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":colour", ColourableRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":computer_upgrade", ComputerUpgradeRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":pocket_computer_upgrade", PocketComputerUpgradeRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":disk", DiskRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":printout", PrintoutRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":turtle", TurtleRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":turtle_upgrade", TurtleUpgradeRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":impostor_shapeless", ImpostorShapelessRecipe.SERIALIZER );
+        RecipeSerializer.register( ComputerCraft.MOD_ID + ":impostor_shaped", ImpostorRecipe.SERIALIZER );
 
         ArgumentSerializers.register();
-
-        // if( Loader.isModLoaded( ModCharset.MODID ) ) IntegrationCharset.register();
     }
 
     private static void registerProviders()
     {
         // Register peripheral providers
         ComputerCraftAPI.registerPeripheralProvider( ( world, pos, side ) -> {
-            TileEntity tile = world.getTileEntity( pos );
+            BlockEntity tile = world.getBlockEntity( pos );
             return tile instanceof IPeripheralTile ? ((IPeripheralTile) tile).getPeripheral( side ) : null;
         } );
 
         ComputerCraftAPI.registerPeripheralProvider( ( world, pos, side ) -> {
-            TileEntity tile = world.getTileEntity( pos );
-            return ComputerCraft.enableCommandBlock && tile instanceof TileEntityCommandBlock ? new CommandBlockPeripheral( (TileEntityCommandBlock) tile ) : null;
+            BlockEntity tile = world.getBlockEntity( pos );
+            return ComputerCraft.enableCommandBlock && tile instanceof CommandBlockBlockEntity ? new CommandBlockPeripheral( (CommandBlockBlockEntity) tile ) : null;
         } );
 
         // Register bundled power providers
@@ -96,15 +100,51 @@ private static void registerProviders()
         ComputerCraftAPI.registerMediaProvider( stack -> {
             Item item = stack.getItem();
             if( item instanceof IMedia ) return (IMedia) item;
-            if( item instanceof ItemRecord ) return RecordMedia.INSTANCE;
+            if( item instanceof MusicDiscItem ) return RecordMedia.INSTANCE;
             return null;
         } );
+    }
+
+    private static void registerHandlers()
+    {
+        CommandRegistry.INSTANCE.register( false, CommandComputerCraft::register );
+
+        ClientTickCallback.EVENT.register( client -> {
+            FrameInfo.onTick();
+        } );
+
+        ServerTickCallback.EVENT.register( server -> {
+            MainThread.executePendingTasks();
+            ComputerCraft.serverComputerRegistry.update();
+            TickScheduler.tick();
+        } );
 
-        // Register network providers
-        CapabilityWiredElement.register();
+        ServerStartCallback.EVENT.register( server -> {
+            ComputerCraftProxyCommon.server = server;
+            ComputerCraft.serverComputerRegistry.reset();
+            WirelessNetwork.resetNetworks();
+            MainThread.reset();
+            Tracking.reset();
+        } );
+
+        ServerStopCallback.EVENT.register( server -> {
+            ComputerCraft.serverComputerRegistry.reset();
+            WirelessNetwork.resetNetworks();
+            MainThread.reset();
+            Tracking.reset();
+            ComputerCraftProxyCommon.server = null;
+        } );
+
+        TurtleEvent.EVENT_BUS.register( FurnaceRefuelHandler.INSTANCE );
+        TurtleEvent.EVENT_BUS.register( new TurtlePermissions() );
+    }
+
+    public static MinecraftServer getServer()
+    {
+        // Sorry asie
+        return server;
     }
 
-    @Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID )
     public static final class ForgeHandlers
     {
         private ForgeHandlers()
@@ -123,32 +163,6 @@ public static void onConnectionClosed( FMLNetworkEvent.ClientDisconnectionFromSe
         {
             ComputerCraft.clientComputerRegistry.reset();
         }
-        */
-
-        @SubscribeEvent
-        public static void onClientTick( TickEvent.ClientTickEvent event )
-        {
-            if( event.phase == TickEvent.Phase.START )
-            {
-                ComputerCraft.clientComputerRegistry.update();
-            }
-        }
-
-        @SubscribeEvent
-        public static void onServerTick( TickEvent.ServerTickEvent event )
-        {
-            if( event.phase == TickEvent.Phase.START )
-            {
-                MainThread.executePendingTasks();
-                ComputerCraft.serverComputerRegistry.update();
-            }
-        }
-
-        @SubscribeEvent
-        public static void onConfigChanged( ConfigChangedEvent.OnConfigChangedEvent event )
-        {
-            if( event.getModID().equals( ComputerCraft.MOD_ID ) ) Config.sync();
-        }
 
         @SubscribeEvent
         public static void onContainerOpen( PlayerContainerEvent.Open event )
@@ -164,27 +178,6 @@ public static void onContainerOpen( PlayerContainerEvent.Open event )
                 }
             }
         }
-
-        @SubscribeEvent
-        public static void onServerStarting( FMLServerStartingEvent event )
-        {
-            CommandComputerCraft.register( event.getCommandDispatcher() );
-        }
-
-        @SubscribeEvent
-        public static void onServerStarted( FMLServerStartedEvent event )
-        {
-            ComputerCraft.serverComputerRegistry.reset();
-            WirelessNetwork.resetNetworks();
-            Tracking.reset();
-        }
-
-        @SubscribeEvent
-        public static void onServerStopped( FMLServerStoppedEvent event )
-        {
-            ComputerCraft.serverComputerRegistry.reset();
-            WirelessNetwork.resetNetworks();
-            Tracking.reset();
-        }
+        */
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java b/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java
index 1e99819b14..bdf368c61b 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/FurnaceRefuelHandler.java
@@ -6,23 +6,21 @@
 
 package dan200.computercraft.shared.turtle;
 
-import dan200.computercraft.ComputerCraft;
+import com.google.common.eventbus.Subscribe;
 import dan200.computercraft.api.turtle.ITurtleAccess;
 import dan200.computercraft.api.turtle.event.TurtleRefuelEvent;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
+import net.minecraft.block.entity.FurnaceBlockEntity;
+import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntityFurnace;
-import net.minecraftforge.event.ForgeEventFactory;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
 
 import javax.annotation.Nonnull;
 
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID )
 public final class FurnaceRefuelHandler implements TurtleRefuelEvent.Handler
 {
-    private static final FurnaceRefuelHandler INSTANCE = new FurnaceRefuelHandler();
+    public static final FurnaceRefuelHandler INSTANCE = new FurnaceRefuelHandler();
 
     private FurnaceRefuelHandler()
     {
@@ -31,14 +29,15 @@ private FurnaceRefuelHandler()
     @Override
     public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStack, int slot, int limit )
     {
-        ItemStack stack = turtle.getItemHandler().extractItem( slot, limit, false );
-        int fuelToGive = getFuelPerItem( stack ) * stack.getCount();
+        ItemStorage storage = ItemStorage.wrap( turtle.getInventory() );
+        ItemStack stack = storage.take( slot, limit, ItemStack.EMPTY, false );
+        int fuelToGive = getFuelPerItem( stack ) * stack.getAmount();
 
         // Store the replacement item in the inventory
-        ItemStack replacementStack = stack.getItem().getContainerItem( stack );
-        if( !replacementStack.isEmpty() )
+        Item replacementStack = stack.getItem().getRecipeRemainder();
+        if( replacementStack != null )
         {
-            ItemStack remainder = InventoryUtil.storeItems( replacementStack, turtle.getItemHandler(), turtle.getSelectedSlot() );
+            ItemStack remainder = InventoryUtil.storeItems( new ItemStack( replacementStack ), storage, turtle.getSelectedSlot() );
             if( !remainder.isEmpty() )
             {
                 WorldUtil.dropItemStack( remainder, turtle.getWorld(), turtle.getPosition(), turtle.getDirection().getOpposite() );
@@ -51,16 +50,12 @@ public int refuel( @Nonnull ITurtleAccess turtle, @Nonnull ItemStack currentStac
 
     private static int getFuelPerItem( @Nonnull ItemStack stack )
     {
-        int basicBurnTime = stack.getBurnTime();
-        int burnTime = ForgeEventFactory.getItemBurnTime(
-            stack,
-            basicBurnTime == -1 ? TileEntityFurnace.getBurnTimes().getOrDefault( stack.getItem(), 0 ) : basicBurnTime
-        );
+        int burnTime = FurnaceBlockEntity.createFuelTimeMap().getOrDefault( stack.getItem(), 0 );
         return (burnTime * 5) / 100;
     }
 
-    @SubscribeEvent
-    public static void onTurtleRefuel( TurtleRefuelEvent event )
+    @Subscribe
+    public void onTurtleRefuel( TurtleRefuelEvent event )
     {
         if( event.getHandler() == null && getFuelPerItem( event.getStack() ) > 0 ) event.setHandler( INSTANCE );
     }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java
index 9b99279098..f972d53a73 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/apis/TurtleAPI.java
@@ -14,14 +14,14 @@
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.api.turtle.event.TurtleActionEvent;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.api.turtle.event.TurtleInspectItemEvent;
 import dan200.computercraft.core.apis.IAPIEnvironment;
 import dan200.computercraft.core.tracking.TrackingField;
 import dan200.computercraft.shared.turtle.core.*;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.registries.ForgeRegistries;
+import net.minecraft.util.registry.Registry;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -221,15 +221,15 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
             {
                 // getItemCount
                 int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() );
-                ItemStack stack = m_turtle.getInventory().getStackInSlot( slot );
-                return new Object[] { stack.getCount() };
+                ItemStack stack = m_turtle.getInventory().getInvStack( slot );
+                return new Object[] { stack.getAmount() };
             }
             case 15:
             {
                 // getItemSpace
                 int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() );
-                ItemStack stack = m_turtle.getInventory().getStackInSlot( slot );
-                return new Object[] { stack.isEmpty() ? 64 : Math.min( stack.getMaxStackSize(), 64 ) - stack.getCount() };
+                ItemStack stack = m_turtle.getInventory().getInvStack( slot );
+                return new Object[] { stack.isEmpty() ? 64 : Math.min( stack.getMaxAmount(), 64 ) - stack.getAmount() };
             }
             case 16: // detect
                 return tryCommand( context, new TurtleDetectCommand( InteractDirection.Forward ) );
@@ -340,19 +340,19 @@ public Object[] callMethod( @Nonnull ILuaContext context, int method, @Nonnull O
             {
                 // getItemDetail
                 int slot = parseOptionalSlotNumber( args, 0, m_turtle.getSelectedSlot() );
-                ItemStack stack = m_turtle.getInventory().getStackInSlot( slot );
+                ItemStack stack = m_turtle.getInventory().getInvStack( slot );
                 if( stack.isEmpty() ) return new Object[] { null };
 
                 Item item = stack.getItem();
-                String name = ForgeRegistries.ITEMS.getKey( item ).toString();
-                int count = stack.getCount();
+                String name = Registry.ITEM.getId( item ).toString();
+                int count = stack.getAmount();
 
                 Map<String, Object> table = new HashMap<>();
                 table.put( "name", name );
                 table.put( "count", count );
 
                 TurtleActionEvent event = new TurtleInspectItemEvent( m_turtle, stack, table );
-                if( MinecraftForge.EVENT_BUS.post( event ) ) return new Object[] { false, event.getFailureMessage() };
+                if( TurtleEvent.post( event ) ) return new Object[] { false, event.getFailureMessage() };
 
                 return new Object[] { table };
             }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java
index d16515e3ba..0daccefa1f 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/BlockTurtle.java
@@ -15,102 +15,87 @@
 import dan200.computercraft.shared.turtle.items.TurtleItemFactory;
 import dan200.computercraft.shared.util.WaterloggableBlock;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.BlockFaceShape;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.Entity;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.projectile.EntityFireball;
-import net.minecraft.fluid.IFluidState;
-import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.block.BlockRenderType;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.VerticalEntityPosition;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.fluid.FluidState;
+import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.item.ItemStack;
-import net.minecraft.state.DirectionProperty;
-import net.minecraft.state.StateContainer;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumBlockRenderType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.state.StateFactory;
+import net.minecraft.state.property.DirectionProperty;
+import net.minecraft.state.property.Properties;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.math.shapes.VoxelShape;
-import net.minecraft.util.math.shapes.VoxelShapes;
-import net.minecraft.world.*;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.util.shape.VoxelShapes;
+import net.minecraft.world.BlockView;
+import net.minecraft.world.IWorld;
+import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public class BlockTurtle extends BlockComputerBase<TileTurtle> implements WaterloggableBlock
 {
-    public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
+    public static final DirectionProperty FACING = Properties.FACING_HORIZONTAL;
 
-    private static final VoxelShape DEFAULT_SHAPE = VoxelShapes.create(
+    private static final VoxelShape DEFAULT_SHAPE = VoxelShapes.cuboid(
         0.125, 0.125, 0.125,
         0.875, 0.875, 0.875
     );
 
-    public BlockTurtle( Properties settings, ComputerFamily family, TileEntityType<TileTurtle> type )
+    public BlockTurtle( Settings settings, ComputerFamily family, BlockEntityType<TileTurtle> type )
     {
         super( settings, family, type );
-        setDefaultState( getStateContainer().getBaseState()
-            .with( FACING, EnumFacing.NORTH )
+        setDefaultState( getStateFactory().getDefaultState()
+            .with( FACING, Direction.NORTH )
             .with( WATERLOGGED, false )
         );
     }
 
     @Override
-    protected void fillStateContainer( StateContainer.Builder<Block, IBlockState> builder )
+    protected void appendProperties( StateFactory.Builder<Block, BlockState> builder )
     {
-        builder.add( FACING, WATERLOGGED );
+        builder.with( FACING, WATERLOGGED );
     }
 
     @Nonnull
     @Override
     @Deprecated
-    public EnumBlockRenderType getRenderType( IBlockState state )
+    public BlockRenderType getRenderType( BlockState state )
     {
-        return EnumBlockRenderType.INVISIBLE;
-    }
-
-    @Override
-    @Deprecated
-    public boolean isFullCube( IBlockState state )
-    {
-        return false;
-    }
-
-    @Nonnull
-    @Override
-    @Deprecated
-    public BlockFaceShape getBlockFaceShape( IBlockReader world, IBlockState state, BlockPos pos, EnumFacing side )
-    {
-        return BlockFaceShape.UNDEFINED;
+        return BlockRenderType.INVISIBLE;
     }
 
     @Nonnull
     @Override
     @Deprecated
-    public VoxelShape getShape( IBlockState state, IBlockReader world, BlockPos pos )
+    public VoxelShape getOutlineShape( BlockState state, BlockView world, BlockPos pos, VerticalEntityPosition position )
     {
-        TileEntity tile = world.getTileEntity( pos );
+        BlockEntity tile = world.getBlockEntity( pos );
         Vec3d offset = tile instanceof TileTurtle ? ((TileTurtle) tile).getRenderOffset( 1.0f ) : Vec3d.ZERO;
-        return offset.equals( Vec3d.ZERO ) ? DEFAULT_SHAPE : DEFAULT_SHAPE.withOffset( offset.x, offset.y, offset.z );
+        return offset.equals( Vec3d.ZERO ) ? DEFAULT_SHAPE : DEFAULT_SHAPE.offset( offset.x, offset.y, offset.z );
     }
 
     @Nullable
     @Override
-    public IBlockState getStateForPlacement( BlockItemUseContext placement )
+    public BlockState getPlacementState( ItemPlacementContext placement )
     {
         return getDefaultState()
-            .with( FACING, placement.getPlacementHorizontalFacing() )
+            .with( FACING, placement.getPlayerHorizontalFacing() )
             .with( WATERLOGGED, getWaterloggedStateForPlacement( placement ) );
     }
 
     @Nonnull
     @Override
     @Deprecated
-    public IFluidState getFluidState( IBlockState state )
+    public FluidState getFluidState( BlockState state )
     {
         return getWaterloggedFluidState( state );
     }
@@ -118,25 +103,25 @@ public IFluidState getFluidState( IBlockState state )
     @Nonnull
     @Override
     @Deprecated
-    public IBlockState updatePostPlacement( @Nonnull IBlockState state, EnumFacing side, IBlockState otherState, IWorld world, BlockPos pos, BlockPos otherPos )
+    public BlockState getStateForNeighborUpdate( @Nonnull BlockState state, Direction side, BlockState otherState, IWorld world, BlockPos pos, BlockPos otherPos )
     {
         updateWaterloggedPostPlacement( state, world, pos );
         return state;
     }
 
     @Override
-    public void onBlockPlacedBy( World world, BlockPos pos, IBlockState state, @Nullable EntityLivingBase player, @Nonnull ItemStack stack )
+    public void onPlaced( World world, BlockPos pos, BlockState state, @Nullable LivingEntity player, @Nonnull ItemStack stack )
     {
-        super.onBlockPlacedBy( world, pos, state, player, stack );
+        super.onPlaced( world, pos, state, player, stack );
 
-        TileEntity tile = world.getTileEntity( pos );
-        if( !world.isRemote && tile instanceof TileTurtle )
+        BlockEntity tile = world.getBlockEntity( pos );
+        if( !world.isClient && tile instanceof TileTurtle )
         {
             TileTurtle turtle = (TileTurtle) tile;
 
-            if( player instanceof EntityPlayer )
+            if( player instanceof PlayerEntity )
             {
-                ((TileTurtle) tile).setOwningPlayer( ((EntityPlayer) player).getGameProfile() );
+                ((TileTurtle) tile).setOwningPlayer( ((PlayerEntity) player).getGameProfile() );
             }
 
             if( stack.getItem() instanceof ITurtleItem )
@@ -156,23 +141,12 @@ public void onBlockPlacedBy( World world, BlockPos pos, IBlockState state, @Null
                 if( colour != -1 ) turtle.getAccess().setColour( colour );
 
                 // Set overlay
-                ResourceLocation overlay = item.getOverlay( stack );
+                Identifier overlay = item.getOverlay( stack );
                 if( overlay != null ) ((TurtleBrain) turtle.getAccess()).setOverlay( overlay );
             }
         }
     }
 
-    @Override
-    public float getExplosionResistance( IBlockState state, IWorldReader world, BlockPos pos, @Nullable Entity exploder, Explosion explosion )
-    {
-        if( getFamily() == ComputerFamily.Advanced && (exploder instanceof EntityLivingBase || exploder instanceof EntityFireball) )
-        {
-            return 2000;
-        }
-
-        return super.getExplosionResistance( state, world, pos, exploder, explosion );
-    }
-
     @Nonnull
     @Override
     protected ItemStack getItem( TileComputerBase tile )
diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java
index 9bb93086b8..fd13056eac 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/ITurtleTile.java
@@ -10,14 +10,14 @@
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.shared.computer.blocks.IComputerTile;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.Vec3d;
 
 public interface ITurtleTile extends IComputerTile
 {
     int getColour();
 
-    ResourceLocation getOverlay();
+    Identifier getOverlay();
 
     ITurtleUpgrade getUpgrade( TurtleSide side );
 
diff --git a/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java
index 50569ee1e8..5a570ab58c 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/blocks/TileTurtle.java
@@ -23,32 +23,23 @@
 import dan200.computercraft.shared.turtle.apis.TurtleAPI;
 import dan200.computercraft.shared.turtle.core.TurtleBrain;
 import dan200.computercraft.shared.util.*;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.init.Items;
-import net.minecraft.item.EnumDyeColor;
-import net.minecraft.item.ItemDye;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.DyeItem;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.nbt.NBTTagList;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.item.Items;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.util.*;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.util.Constants;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.items.IItemHandlerModifiable;
-import net.minecraftforge.items.wrapper.InvWrapper;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import static net.minecraftforge.items.CapabilityItemHandler.ITEM_HANDLER_CAPABILITY;
-
-public class TileTurtle extends TileComputerBase implements ITurtleTile, DefaultInventory
+public class TileTurtle extends TileComputerBase implements ITurtleTile, DefaultInventory, Nameable
 {
     // Statics
 
@@ -57,12 +48,12 @@ public class TileTurtle extends TileComputerBase implements ITurtleTile, Default
     public static final int INVENTORY_HEIGHT = 4;
 
     public static final NamedBlockEntityType<TileTurtle> FACTORY_NORMAL = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "turtle_normal" ),
+        new Identifier( ComputerCraft.MOD_ID, "turtle_normal" ),
         type -> new TileTurtle( type, ComputerFamily.Normal )
     );
 
     public static final NamedBlockEntityType<TileTurtle> FACTORY_ADVANCED = NamedBlockEntityType.create(
-        new ResourceLocation( ComputerCraft.MOD_ID, "turtle_advanced" ),
+        new Identifier( ComputerCraft.MOD_ID, "turtle_advanced" ),
         type -> new TileTurtle( type, ComputerFamily.Advanced )
     );
 
@@ -75,19 +66,17 @@ enum MoveState
         MOVED
     }
 
-    private NonNullList<ItemStack> m_inventory;
-    private NonNullList<ItemStack> m_previousInventory;
-    private final IItemHandlerModifiable m_itemHandler = new InvWrapper( this );
-    private final LazyOptional<IItemHandlerModifiable> m_itemHandlerCap = LazyOptional.of( () -> m_itemHandler );
+    private DefaultedList<ItemStack> m_inventory;
+    private DefaultedList<ItemStack> m_previousInventory;
     private boolean m_inventoryChanged;
     private TurtleBrain m_brain;
     private MoveState m_moveState;
 
-    public TileTurtle( TileEntityType<? extends TileGeneric> type, ComputerFamily family )
+    public TileTurtle( BlockEntityType<? extends TileGeneric> type, ComputerFamily family )
     {
         super( type, family );
-        m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
-        m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
+        m_inventory = DefaultedList.create( INVENTORY_SIZE, ItemStack.EMPTY );
+        m_previousInventory = DefaultedList.create( INVENTORY_SIZE, ItemStack.EMPTY );
         m_inventoryChanged = false;
         m_brain = new TurtleBrain( this );
         m_moveState = MoveState.NOT_MOVED;
@@ -126,12 +115,12 @@ public void destroy()
             super.destroy();
 
             // Drop contents
-            if( !getWorld().isRemote )
+            if( !getWorld().isClient )
             {
-                int size = getSizeInventory();
+                int size = getInvSize();
                 for( int i = 0; i < size; i++ )
                 {
-                    ItemStack stack = getStackInSlot( i );
+                    ItemStack stack = getInvStack( i );
                     if( !stack.isEmpty() )
                     {
                         WorldUtil.dropItemStack( stack, getWorld(), getPos() );
@@ -142,7 +131,7 @@ public void destroy()
         else
         {
             // Just turn off any redstone we had on
-            for( EnumFacing dir : DirectionUtil.FACINGS )
+            for( Direction dir : DirectionUtil.FACINGS )
             {
                 RedstoneUtil.propagateRedstoneOutput( getWorld(), getPos(), dir );
             }
@@ -159,27 +148,27 @@ protected void unload()
     }
 
     @Override
-    public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ )
+    public boolean onActivate( PlayerEntity player, Hand hand, BlockHitResult hit )
     {
         // Request description from server
         // requestTileEntityUpdate();
 
         // Apply dye
-        ItemStack currentItem = player.getHeldItem( hand );
+        ItemStack currentItem = player.getStackInHand( hand );
         if( !currentItem.isEmpty() )
         {
-            if( currentItem.getItem() instanceof ItemDye )
+            if( currentItem.getItem() instanceof DyeItem )
             {
                 // Dye to change turtle colour
-                if( !getWorld().isRemote )
+                if( !getWorld().isClient )
                 {
-                    EnumDyeColor dye = ((ItemDye) currentItem.getItem()).getDyeColor();
+                    DyeColor dye = ((DyeItem) currentItem.getItem()).getColor();
                     if( m_brain.getDyeColour() != dye )
                     {
                         m_brain.setDyeColour( dye );
                         if( !player.isCreative() )
                         {
-                            currentItem.shrink( 1 );
+                            currentItem.subtractAmount( 1 );
                         }
                     }
                 }
@@ -188,14 +177,14 @@ public boolean onActivate( EntityPlayer player, EnumHand hand, EnumFacing side,
             else if( currentItem.getItem() == Items.WATER_BUCKET && m_brain.getColour() != -1 )
             {
                 // Water to remove turtle colour
-                if( !getWorld().isRemote )
+                if( !getWorld().isClient )
                 {
                     if( m_brain.getColour() != -1 )
                     {
                         m_brain.setColour( -1 );
                         if( !player.isCreative() )
                         {
-                            player.setHeldItem( hand, new ItemStack( Items.BUCKET ) );
+                            player.setStackInHand( hand, new ItemStack( Items.BUCKET ) );
                             player.inventory.markDirty();
                         }
                     }
@@ -205,23 +194,23 @@ else if( currentItem.getItem() == Items.WATER_BUCKET && m_brain.getColour() != -
         }
 
         // Open GUI or whatever
-        return super.onActivate( player, hand, side, hitX, hitY, hitZ );
+        return super.onActivate( player, hand, hit );
     }
 
     @Override
-    protected boolean canNameWithTag( EntityPlayer player )
+    protected boolean canNameWithTag( PlayerEntity player )
     {
         return true;
     }
 
     @Override
-    public void openGUI( EntityPlayer player )
+    public void openGUI( PlayerEntity player )
     {
         Containers.openTurtleGUI( player, this );
     }
 
     @Override
-    protected double getInteractRange( EntityPlayer player )
+    protected double getInteractRange( PlayerEntity player )
     {
         return 12.0;
     }
@@ -233,15 +222,15 @@ public void tick()
         m_brain.update();
         synchronized( m_inventory )
         {
-            if( !getWorld().isRemote && m_inventoryChanged )
+            if( !getWorld().isClient && m_inventoryChanged )
             {
                 ServerComputer computer = getServerComputer();
                 if( computer != null ) computer.queueEvent( "turtle_inventory" );
 
                 m_inventoryChanged = false;
-                for( int n = 0; n < getSizeInventory(); n++ )
+                for( int n = 0; n < getInvSize(); n++ )
                 {
-                    m_previousInventory.set( n, InventoryUtil.copyItem( getStackInSlot( n ) ) );
+                    m_previousInventory.set( n, InventoryUtil.copyItem( getInvStack( n ) ) );
                 }
             }
         }
@@ -276,21 +265,21 @@ public void notifyMoveEnd()
     }
 
     @Override
-    public void read( NBTTagCompound nbt )
+    public void fromTag( CompoundTag nbt )
     {
-        super.read( nbt );
+        super.fromTag( nbt );
 
         // Read inventory
-        NBTTagList nbttaglist = nbt.getList( "Items", Constants.NBT.TAG_COMPOUND );
-        m_inventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
-        m_previousInventory = NonNullList.withSize( INVENTORY_SIZE, ItemStack.EMPTY );
+        ListTag nbttaglist = nbt.getList( "Items", NBTUtil.TAG_COMPOUND );
+        m_inventory = DefaultedList.create( INVENTORY_SIZE, ItemStack.EMPTY );
+        m_previousInventory = DefaultedList.create( INVENTORY_SIZE, ItemStack.EMPTY );
         for( int i = 0; i < nbttaglist.size(); i++ )
         {
-            NBTTagCompound tag = nbttaglist.getCompound( i );
+            CompoundTag tag = nbttaglist.getCompoundTag( i );
             int slot = tag.getByte( "Slot" ) & 0xff;
-            if( slot < getSizeInventory() )
+            if( slot < getInvSize() )
             {
-                m_inventory.set( slot, ItemStack.read( tag ) );
+                m_inventory.set( slot, ItemStack.fromTag( tag ) );
                 m_previousInventory.set( slot, InventoryUtil.copyItem( m_inventory.get( slot ) ) );
             }
         }
@@ -301,17 +290,17 @@ public void read( NBTTagCompound nbt )
 
     @Nonnull
     @Override
-    public NBTTagCompound write( NBTTagCompound nbt )
+    public CompoundTag toTag( CompoundTag nbt )
     {
         // Write inventory
-        NBTTagList nbttaglist = new NBTTagList();
+        ListTag nbttaglist = new ListTag();
         for( int i = 0; i < INVENTORY_SIZE; i++ )
         {
             if( !m_inventory.get( i ).isEmpty() )
             {
-                NBTTagCompound tag = new NBTTagCompound();
+                CompoundTag tag = new CompoundTag();
                 tag.putByte( "Slot", (byte) i );
-                m_inventory.get( i ).write( tag );
+                m_inventory.get( i ).toTag( tag );
                 nbttaglist.add( tag );
             }
         }
@@ -320,17 +309,17 @@ public NBTTagCompound write( NBTTagCompound nbt )
         // Write brain
         nbt = m_brain.writeToNBT( nbt );
 
-        return super.write( nbt );
+        return super.toTag( nbt );
     }
 
     @Override
-    protected boolean isPeripheralBlockedOnSide( EnumFacing localSide )
+    protected boolean isPeripheralBlockedOnSide( Direction localSide )
     {
         return hasPeripheralUpgradeOnSide( localSide );
     }
 
     @Override
-    protected boolean isRedstoneBlockedOnSide( EnumFacing localSide )
+    protected boolean isRedstoneBlockedOnSide( Direction localSide )
     {
         return false;
     }
@@ -338,15 +327,15 @@ protected boolean isRedstoneBlockedOnSide( EnumFacing localSide )
     // IDirectionalTile
 
     @Override
-    public EnumFacing getDirection()
+    public Direction getDirection()
     {
-        return getBlockState().get( BlockTurtle.FACING );
+        return getCachedState().get( BlockTurtle.FACING );
     }
 
-    public void setDirection( EnumFacing dir )
+    public void setDirection( Direction dir )
     {
-        if( dir.getAxis() == EnumFacing.Axis.Y ) dir = EnumFacing.NORTH;
-        world.setBlockState( pos, getBlockState().with( BlockTurtle.FACING, dir ) );
+        if( dir.getAxis() == Direction.Axis.Y ) dir = Direction.NORTH;
+        world.setBlockState( pos, getCachedState().with( BlockTurtle.FACING, dir ) );
         updateOutput();
         updateInput();
         onTileEntityChange();
@@ -367,7 +356,7 @@ public int getColour()
     }
 
     @Override
-    public ResourceLocation getOverlay()
+    public Identifier getOverlay()
     {
         return m_brain.getOverlay();
     }
@@ -405,13 +394,13 @@ public void setOwningPlayer( GameProfile player )
     // IInventory
 
     @Override
-    public int getSizeInventory()
+    public int getInvSize()
     {
         return INVENTORY_SIZE;
     }
 
     @Override
-    public boolean isEmpty()
+    public boolean isInvEmpty()
     {
         for( ItemStack stack : m_inventory )
         {
@@ -422,7 +411,7 @@ public boolean isEmpty()
 
     @Nonnull
     @Override
-    public ItemStack getStackInSlot( int slot )
+    public ItemStack getInvStack( int slot )
     {
         if( slot >= 0 && slot < INVENTORY_SIZE )
         {
@@ -436,19 +425,19 @@ public ItemStack getStackInSlot( int slot )
 
     @Nonnull
     @Override
-    public ItemStack removeStackFromSlot( int slot )
+    public ItemStack removeInvStack( int slot )
     {
         synchronized( m_inventory )
         {
-            ItemStack result = getStackInSlot( slot );
-            setInventorySlotContents( slot, ItemStack.EMPTY );
+            ItemStack result = getInvStack( slot );
+            setInvStack( slot, ItemStack.EMPTY );
             return result;
         }
     }
 
     @Nonnull
     @Override
-    public ItemStack decrStackSize( int slot, int count )
+    public ItemStack takeInvStack( int slot, int count )
     {
         if( count == 0 )
         {
@@ -457,15 +446,15 @@ public ItemStack decrStackSize( int slot, int count )
 
         synchronized( m_inventory )
         {
-            ItemStack stack = getStackInSlot( slot );
+            ItemStack stack = getInvStack( slot );
             if( stack.isEmpty() )
             {
                 return ItemStack.EMPTY;
             }
 
-            if( stack.getCount() <= count )
+            if( stack.getAmount() <= count )
             {
-                setInventorySlotContents( slot, ItemStack.EMPTY );
+                setInvStack( slot, ItemStack.EMPTY );
                 return stack;
             }
 
@@ -476,7 +465,7 @@ public ItemStack decrStackSize( int slot, int count )
     }
 
     @Override
-    public void setInventorySlotContents( int i, @Nonnull ItemStack stack )
+    public void setInvStack( int i, @Nonnull ItemStack stack )
     {
         if( i >= 0 && i < INVENTORY_SIZE )
         {
@@ -520,9 +509,9 @@ public void markDirty()
         {
             if( !m_inventoryChanged )
             {
-                for( int n = 0; n < getSizeInventory(); n++ )
+                for( int n = 0; n < getInvSize(); n++ )
                 {
-                    if( !ItemStack.areItemStacksEqual( getStackInSlot( n ), m_previousInventory.get( n ) ) )
+                    if( !ItemStack.areEqual( getInvStack( n ), m_previousInventory.get( n ) ) )
                     {
                         m_inventoryChanged = true;
                         break;
@@ -533,7 +522,7 @@ public void markDirty()
     }
 
     @Override
-    public boolean isUsableByPlayer( @Nonnull EntityPlayer player )
+    public boolean canPlayerUseInv( @Nonnull PlayerEntity player )
     {
         return isUsable( player, false );
     }
@@ -552,14 +541,14 @@ public void onTileEntityChange()
     // Networking stuff
 
     @Override
-    protected void writeDescription( @Nonnull NBTTagCompound nbt )
+    protected void writeDescription( @Nonnull CompoundTag nbt )
     {
         super.writeDescription( nbt );
         m_brain.writeDescription( nbt );
     }
 
     @Override
-    protected void readDescription( @Nonnull NBTTagCompound nbt )
+    protected void readDescription( @Nonnull CompoundTag nbt )
     {
         super.readDescription( nbt );
         m_brain.readDescription( nbt );
@@ -568,7 +557,7 @@ protected void readDescription( @Nonnull NBTTagCompound nbt )
 
     // Privates
 
-    private boolean hasPeripheralUpgradeOnSide( EnumFacing side )
+    private boolean hasPeripheralUpgradeOnSide( Direction side )
     {
         ITurtleUpgrade upgrade;
         switch( side )
@@ -598,21 +587,8 @@ public void transferStateFrom( TileTurtle copy )
 
     @Nullable
     @Override
-    public IPeripheral getPeripheral( @Nonnull EnumFacing side )
+    public IPeripheral getPeripheral( @Nonnull Direction side )
     {
         return hasMoved() ? null : new ComputerPeripheral( "turtle", createProxy() );
     }
-
-    public IItemHandlerModifiable getItemHandler()
-    {
-        return m_itemHandler;
-    }
-
-    @Nonnull
-    @Override
-    public <T> LazyOptional<T> getCapability( @Nonnull Capability<T> cap, @Nullable EnumFacing side )
-    {
-        if( cap == ITEM_HANDLER_CAPABILITY ) return m_itemHandlerCap.cast();
-        return super.getCapability( cap, side );
-    }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java b/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java
index 1e8a5175f1..7555b441ec 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/InteractDirection.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.turtle.core;
 
 import dan200.computercraft.api.turtle.ITurtleAccess;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 public enum InteractDirection
 {
@@ -15,7 +15,7 @@ public enum InteractDirection
     Up,
     Down;
 
-    public EnumFacing toWorldDir( ITurtleAccess turtle )
+    public Direction toWorldDir( ITurtleAccess turtle )
     {
         switch( this )
         {
@@ -23,9 +23,9 @@ public EnumFacing toWorldDir( ITurtleAccess turtle )
             default:
                 return turtle.getDirection();
             case Up:
-                return EnumFacing.UP;
+                return Direction.UP;
             case Down:
-                return EnumFacing.DOWN;
+                return Direction.DOWN;
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java b/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java
index 26c6d9681f..a663dbf536 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/MoveDirection.java
@@ -7,7 +7,7 @@
 package dan200.computercraft.shared.turtle.core;
 
 import dan200.computercraft.api.turtle.ITurtleAccess;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 public enum MoveDirection
 {
@@ -16,7 +16,7 @@ public enum MoveDirection
     Up,
     Down;
 
-    public EnumFacing toWorldDir( ITurtleAccess turtle )
+    public Direction toWorldDir( ITurtleAccess turtle )
     {
         switch( this )
         {
@@ -26,9 +26,9 @@ public EnumFacing toWorldDir( ITurtleAccess turtle )
             case Back:
                 return turtle.getDirection().getOpposite();
             case Up:
-                return EnumFacing.UP;
+                return Direction.UP;
             case Down:
-                return EnumFacing.DOWN;
+                return Direction.DOWN;
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java
index b590838ae5..9b64779a80 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleBrain.java
@@ -22,26 +22,25 @@
 import dan200.computercraft.shared.util.Colour;
 import dan200.computercraft.shared.util.Holiday;
 import dan200.computercraft.shared.util.HolidayUtil;
+import dan200.computercraft.shared.util.NBTUtil;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.MoverType;
-import net.minecraft.fluid.IFluidState;
-import net.minecraft.init.Particles;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.item.EnumDyeColor;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.tags.FluidTags;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EntitySelectors;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.entity.MovementType;
+import net.minecraft.fluid.FluidState;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.tag.FluidTags;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.BoundingBox;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.Constants;
-import net.minecraftforge.items.IItemHandlerModifiable;
 
 import javax.annotation.Nonnull;
 import java.util.*;
@@ -72,12 +71,12 @@ public class TurtleBrain implements ITurtleAccess
 
     private Map<TurtleSide, ITurtleUpgrade> m_upgrades = new EnumMap<>( TurtleSide.class );
     private Map<TurtleSide, IPeripheral> peripherals = new EnumMap<>( TurtleSide.class );
-    private Map<TurtleSide, NBTTagCompound> m_upgradeNBTData = new EnumMap<>( TurtleSide.class );
+    private Map<TurtleSide, CompoundTag> m_upgradeNBTData = new EnumMap<>( TurtleSide.class );
 
     private int m_selectedSlot = 0;
     private int m_fuelLevel = 0;
     private int m_colourHex = -1;
-    private ResourceLocation m_overlay = null;
+    private Identifier m_overlay = null;
 
     private TurtleAnimation m_animation = TurtleAnimation.None;
     private int m_animationProgress = 0;
@@ -129,7 +128,7 @@ public void setupComputer( ServerComputer computer )
     public void update()
     {
         World world = getWorld();
-        if( !world.isRemote )
+        if( !world.isClient )
         {
             // Advance movement
             updateCommands();
@@ -153,30 +152,30 @@ public void update()
      *
      * @param nbt The tag to read from
      */
-    private void readCommon( NBTTagCompound nbt )
+    private void readCommon( CompoundTag nbt )
     {
         // Read fields
-        m_colourHex = nbt.contains( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : -1;
-        m_fuelLevel = nbt.contains( NBT_FUEL ) ? nbt.getInt( NBT_FUEL ) : 0;
-        m_overlay = nbt.contains( NBT_OVERLAY ) ? new ResourceLocation( nbt.getString( NBT_OVERLAY ) ) : null;
+        m_colourHex = nbt.containsKey( NBT_COLOUR ) ? nbt.getInt( NBT_COLOUR ) : -1;
+        m_fuelLevel = nbt.containsKey( NBT_FUEL ) ? nbt.getInt( NBT_FUEL ) : 0;
+        m_overlay = nbt.containsKey( NBT_OVERLAY ) ? new Identifier( nbt.getString( NBT_OVERLAY ) ) : null;
 
         // Read upgrades
-        setUpgrade( TurtleSide.Left, nbt.contains( NBT_LEFT_UPGRADE ) ? TurtleUpgrades.get( nbt.getString( NBT_LEFT_UPGRADE ) ) : null );
-        setUpgrade( TurtleSide.Right, nbt.contains( NBT_RIGHT_UPGRADE ) ? TurtleUpgrades.get( nbt.getString( NBT_RIGHT_UPGRADE ) ) : null );
+        setUpgrade( TurtleSide.Left, nbt.containsKey( NBT_LEFT_UPGRADE ) ? TurtleUpgrades.get( nbt.getString( NBT_LEFT_UPGRADE ) ) : null );
+        setUpgrade( TurtleSide.Right, nbt.containsKey( NBT_RIGHT_UPGRADE ) ? TurtleUpgrades.get( nbt.getString( NBT_RIGHT_UPGRADE ) ) : null );
 
         // NBT
         m_upgradeNBTData.clear();
-        if( nbt.contains( NBT_LEFT_UPGRADE_DATA ) )
+        if( nbt.containsKey( NBT_LEFT_UPGRADE_DATA ) )
         {
-            m_upgradeNBTData.put( TurtleSide.Left, nbt.getCompound( NBT_LEFT_UPGRADE_DATA ).copy() );
+            m_upgradeNBTData.put( TurtleSide.Left, nbt.getCompound( NBT_LEFT_UPGRADE_DATA ).method_10553() );
         }
-        if( nbt.contains( NBT_RIGHT_UPGRADE_DATA ) )
+        if( nbt.containsKey( NBT_RIGHT_UPGRADE_DATA ) )
         {
-            m_upgradeNBTData.put( TurtleSide.Right, nbt.getCompound( NBT_RIGHT_UPGRADE_DATA ).copy() );
+            m_upgradeNBTData.put( TurtleSide.Right, nbt.getCompound( NBT_RIGHT_UPGRADE_DATA ).method_10553() );
         }
     }
 
-    private void writeCommon( NBTTagCompound nbt )
+    private void writeCommon( CompoundTag nbt )
     {
         nbt.putInt( NBT_FUEL, m_fuelLevel );
         if( m_colourHex != -1 ) nbt.putInt( NBT_COLOUR, m_colourHex );
@@ -199,7 +198,7 @@ private void writeCommon( NBTTagCompound nbt )
         }
     }
 
-    public void readFromNBT( NBTTagCompound nbt )
+    public void readFromNBT( CompoundTag nbt )
     {
         readCommon( nbt );
 
@@ -207,9 +206,9 @@ public void readFromNBT( NBTTagCompound nbt )
         m_selectedSlot = nbt.getInt( NBT_SLOT );
 
         // Read owner
-        if( nbt.contains( "Owner", Constants.NBT.TAG_COMPOUND ) )
+        if( nbt.containsKey( "Owner", NBTUtil.TAG_COMPOUND ) )
         {
-            NBTTagCompound owner = nbt.getCompound( "Owner" );
+            CompoundTag owner = nbt.getCompound( "Owner" );
             m_owningPlayer = new GameProfile(
                 new UUID( owner.getLong( "UpperId" ), owner.getLong( "LowerId" ) ),
                 owner.getString( "Name" )
@@ -221,7 +220,7 @@ public void readFromNBT( NBTTagCompound nbt )
         }
     }
 
-    public NBTTagCompound writeToNBT( NBTTagCompound nbt )
+    public CompoundTag writeToNBT( CompoundTag nbt )
     {
         writeCommon( nbt );
 
@@ -231,7 +230,7 @@ public NBTTagCompound writeToNBT( NBTTagCompound nbt )
         // Write owner
         if( m_owningPlayer != null )
         {
-            NBTTagCompound owner = new NBTTagCompound();
+            CompoundTag owner = new CompoundTag();
             nbt.put( "Owner", owner );
 
             owner.putLong( "UpperId", m_owningPlayer.getId().getMostSignificantBits() );
@@ -247,7 +246,7 @@ private static String getUpgradeId( ITurtleUpgrade upgrade )
         return upgrade != null ? upgrade.getUpgradeID().toString() : null;
     }
 
-    public void readDescription( NBTTagCompound nbt )
+    public void readDescription( CompoundTag nbt )
     {
         readCommon( nbt );
 
@@ -264,7 +263,7 @@ public void readDescription( NBTTagCompound nbt )
         }
     }
 
-    public void writeDescription( NBTTagCompound nbt )
+    public void writeDescription( CompoundTag nbt )
     {
         writeCommon( nbt );
         nbt.putInt( "Animation", m_animation.ordinal() );
@@ -287,7 +286,7 @@ public BlockPos getPosition()
     @Override
     public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos )
     {
-        if( world.isRemote || getWorld().isRemote )
+        if( world.isClient || getWorld().isClient )
         {
             throw new UnsupportedOperationException( "Cannot teleport on the client" );
         }
@@ -296,7 +295,7 @@ public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos )
         World oldWorld = getWorld();
         TileTurtle oldOwner = m_owner;
         BlockPos oldPos = m_owner.getPos();
-        IBlockState oldBlock = m_owner.getBlockState();
+        BlockState oldBlock = m_owner.getCachedState();
 
         if( oldWorld == world && oldPos.equals( pos ) )
         {
@@ -310,12 +309,12 @@ public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos )
         // Ensure we're inside the world border
         if( !world.getWorldBorder().contains( pos ) ) return false;
 
-        IFluidState existingFluid = world.getBlockState( pos ).getFluidState();
-        IBlockState newState = oldBlock
+        FluidState existingFluid = world.getBlockState( pos ).getFluidState();
+        BlockState newState = oldBlock
             // We only mark this as waterlogged when travelling into a source block. This prevents us from spreading
             // fluid by creating a new source when moving into a block, causing the next block to be almost full and
             // then moving into that.
-            .with( WATERLOGGED, existingFluid.isTagged( FluidTags.WATER ) && existingFluid.isSource() );
+            .with( WATERLOGGED, existingFluid.matches( FluidTags.WATER ) && existingFluid.isStill() );
 
         oldOwner.notifyMoveStart();
 
@@ -327,7 +326,7 @@ public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos )
                 Block block = world.getBlockState( pos ).getBlock();
                 if( block == oldBlock.getBlock() )
                 {
-                    TileEntity newTile = world.getTileEntity( pos );
+                    BlockEntity newTile = world.getBlockEntity( pos );
                     if( newTile instanceof TileTurtle )
                     {
                         // Copy the old turtle state into the new turtle
@@ -339,7 +338,7 @@ public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos )
                         newTurtle.createServerComputer().setPosition( pos );
 
                         // Remove the old turtle
-                        oldWorld.removeBlock( oldPos );
+                        oldWorld.clearBlockState( oldPos, false );
 
                         // Make sure everybody knows about it
                         newTurtle.updateBlock();
@@ -350,7 +349,7 @@ public boolean teleportTo( @Nonnull World world, @Nonnull BlockPos pos )
                 }
 
                 // Something went wrong, remove the newly created turtle
-                world.removeBlock( pos );
+                world.clearBlockState( pos, false );
             }
         }
         finally
@@ -378,7 +377,7 @@ public Vec3d getVisualPosition( float f )
     @Override
     public float getVisualYaw( float f )
     {
-        float yaw = getDirection().getHorizontalAngle();
+        float yaw = getDirection().asRotation();
         switch( m_animation )
         {
             case TurnLeft:
@@ -405,13 +404,13 @@ public float getVisualYaw( float f )
 
     @Nonnull
     @Override
-    public EnumFacing getDirection()
+    public Direction getDirection()
     {
         return m_owner.getDirection();
     }
 
     @Override
-    public void setDirection( @Nonnull EnumFacing dir )
+    public void setDirection( @Nonnull Direction dir )
     {
         m_owner.setDirection( dir );
     }
@@ -425,9 +424,9 @@ public int getSelectedSlot()
     @Override
     public void setSelectedSlot( int slot )
     {
-        if( getWorld().isRemote ) throw new UnsupportedOperationException( "Cannot set the slot on the client" );
+        if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot set the slot on the client" );
 
-        if( slot >= 0 && slot < m_owner.getSizeInventory() )
+        if( slot >= 0 && slot < m_owner.getInvSize() )
         {
             m_selectedSlot = slot;
             m_owner.onTileEntityChange();
@@ -436,17 +435,19 @@ public void setSelectedSlot( int slot )
 
     @Nonnull
     @Override
-    public IInventory getInventory()
+    public Inventory getInventory()
     {
         return m_owner;
     }
 
+    /*
     @Nonnull
     @Override
     public IItemHandlerModifiable getItemHandler()
     {
         return m_owner.getItemHandler();
     }
+    */
 
     @Override
     public boolean isFuelNeeded()
@@ -483,7 +484,7 @@ public int getFuelLimit()
     @Override
     public boolean consumeFuel( int fuel )
     {
-        if( getWorld().isRemote ) throw new UnsupportedOperationException( "Cannot consume fuel on the client" );
+        if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot consume fuel on the client" );
 
         if( !isFuelNeeded() ) return true;
 
@@ -499,8 +500,7 @@ public boolean consumeFuel( int fuel )
     @Override
     public void addFuel( int fuel )
     {
-        if( getWorld().isRemote ) throw new UnsupportedOperationException( "Cannot add fuel on the client" );
-
+        if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot add fuel on the client" );
         int addition = Math.max( fuel, 0 );
         setFuelLevel( getFuelLevel() + addition );
     }
@@ -515,7 +515,7 @@ private int issueCommand( ITurtleCommand command )
     @Override
     public Object[] executeCommand( @Nonnull ILuaContext context, @Nonnull ITurtleCommand command ) throws LuaException, InterruptedException
     {
-        if( getWorld().isRemote ) throw new UnsupportedOperationException( "Cannot run commands on the client" );
+        if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot run commands on the client" );
 
         // Issue command
         int commandID = issueCommand( command );
@@ -539,8 +539,7 @@ public Object[] executeCommand( @Nonnull ILuaContext context, @Nonnull ITurtleCo
     @Override
     public void playAnimation( @Nonnull TurtleAnimation animation )
     {
-        if( getWorld().isRemote ) throw new UnsupportedOperationException( "Cannot play animations on the client" );
-
+        if( getWorld().isClient ) throw new UnsupportedOperationException( "Cannot play animations on the client" );
         m_animation = animation;
         if( m_animation == TurtleAnimation.ShortWait )
         {
@@ -555,12 +554,12 @@ public void playAnimation( @Nonnull TurtleAnimation animation )
         m_owner.updateBlock();
     }
 
-    public ResourceLocation getOverlay()
+    public Identifier getOverlay()
     {
         return m_overlay;
     }
 
-    public void setOverlay( ResourceLocation overlay )
+    public void setOverlay( Identifier overlay )
     {
         if( !Objects.equal( m_overlay, overlay ) )
         {
@@ -569,14 +568,14 @@ public void setOverlay( ResourceLocation overlay )
         }
     }
 
-    public EnumDyeColor getDyeColour()
+    public DyeColor getDyeColour()
     {
         if( m_colourHex == -1 ) return null;
         Colour colour = Colour.fromHex( m_colourHex );
-        return colour == null ? null : EnumDyeColor.byId( 15 - colour.ordinal() );
+        return colour == null ? null : DyeColor.byId( 15 - colour.ordinal() );
     }
 
-    public void setDyeColour( EnumDyeColor dyeColour )
+    public void setDyeColour( DyeColor dyeColour )
     {
         int newColour = -1;
         if( dyeColour != null )
@@ -667,10 +666,10 @@ public IPeripheral getPeripheral( @Nonnull TurtleSide side )
 
     @Nonnull
     @Override
-    public NBTTagCompound getUpgradeNBTData( TurtleSide side )
+    public CompoundTag getUpgradeNBTData( TurtleSide side )
     {
-        NBTTagCompound nbt = m_upgradeNBTData.get( side );
-        if( nbt == null ) m_upgradeNBTData.put( side, nbt = new NBTTagCompound() );
+        CompoundTag nbt = m_upgradeNBTData.get( side );
+        if( nbt == null ) m_upgradeNBTData.put( side, nbt = new CompoundTag() );
         return nbt;
     }
 
@@ -690,7 +689,7 @@ public Vec3d getRenderOffset( float f )
             case MoveDown:
             {
                 // Get direction
-                EnumFacing dir;
+                Direction dir;
                 switch( m_animation )
                 {
                     case MoveForward:
@@ -701,18 +700,18 @@ public Vec3d getRenderOffset( float f )
                         dir = getDirection().getOpposite();
                         break;
                     case MoveUp:
-                        dir = EnumFacing.UP;
+                        dir = Direction.UP;
                         break;
                     case MoveDown:
-                        dir = EnumFacing.DOWN;
+                        dir = Direction.DOWN;
                         break;
                 }
 
                 double distance = -1.0 + getAnimationFraction( f );
                 return new Vec3d(
-                    distance * dir.getXOffset(),
-                    distance * dir.getYOffset(),
-                    distance * dir.getZOffset()
+                    distance * dir.getOffsetX(),
+                    distance * dir.getOffsetY(),
+                    distance * dir.getOffsetZ()
                 );
             }
             default:
@@ -837,7 +836,7 @@ private void updateAnimation()
                     m_animation == TurtleAnimation.MoveDown )
                 {
                     BlockPos pos = getPosition();
-                    EnumFacing moveDir;
+                    Direction moveDir;
                     switch( m_animation )
                     {
                         case MoveForward:
@@ -848,10 +847,10 @@ private void updateAnimation()
                             moveDir = getDirection().getOpposite();
                             break;
                         case MoveUp:
-                            moveDir = EnumFacing.UP;
+                            moveDir = Direction.UP;
                             break;
                         case MoveDown:
-                            moveDir = EnumFacing.DOWN;
+                            moveDir = Direction.DOWN;
                             break;
                     }
 
@@ -864,51 +863,51 @@ private void updateAnimation()
 
                     float pushFrac = 1.0f - (float) (m_animationProgress + 1) / ANIM_DURATION;
                     float push = Math.max( pushFrac + 0.0125f, 0.0f );
-                    if( moveDir.getXOffset() < 0 )
+                    if( moveDir.getOffsetX() < 0 )
                     {
-                        minX += moveDir.getXOffset() * push;
+                        minX += moveDir.getOffsetX() * push;
                     }
                     else
                     {
-                        maxX -= moveDir.getXOffset() * push;
+                        maxX -= moveDir.getOffsetX() * push;
                     }
 
-                    if( moveDir.getYOffset() < 0 )
+                    if( moveDir.getOffsetY() < 0 )
                     {
-                        minY += moveDir.getYOffset() * push;
+                        minY += moveDir.getOffsetY() * push;
                     }
                     else
                     {
-                        maxY -= moveDir.getYOffset() * push;
+                        maxY -= moveDir.getOffsetY() * push;
                     }
 
-                    if( moveDir.getZOffset() < 0 )
+                    if( moveDir.getOffsetZ() < 0 )
                     {
-                        minZ += moveDir.getZOffset() * push;
+                        minZ += moveDir.getOffsetZ() * push;
                     }
                     else
                     {
-                        maxZ -= moveDir.getZOffset() * push;
+                        maxZ -= moveDir.getOffsetZ() * push;
                     }
 
-                    AxisAlignedBB aabb = new AxisAlignedBB( minX, minY, minZ, maxX, maxY, maxZ );
-                    List<Entity> list = world.getEntitiesWithinAABB( Entity.class, aabb, EntitySelectors.NOT_SPECTATING );
+                    BoundingBox aabb = new BoundingBox( minX, minY, minZ, maxX, maxY, maxZ );
+                    List<Entity> list = world.getEntities( (Entity) null, aabb, EntityPredicates.EXCEPT_SPECTATOR );
                     if( !list.isEmpty() )
                     {
                         double pushStep = 1.0f / ANIM_DURATION;
-                        double pushStepX = moveDir.getXOffset() * pushStep;
-                        double pushStepY = moveDir.getYOffset() * pushStep;
-                        double pushStepZ = moveDir.getZOffset() * pushStep;
+                        double pushStepX = moveDir.getOffsetX() * pushStep;
+                        double pushStepY = moveDir.getOffsetY() * pushStep;
+                        double pushStepZ = moveDir.getOffsetZ() * pushStep;
                         for( Entity entity : list )
                         {
-                            entity.move( MoverType.PISTON, pushStepX, pushStepY, pushStepZ );
+                            entity.move( MovementType.PISTON, new Vec3d( pushStepX, pushStepY, pushStepZ ) );
                         }
                     }
                 }
             }
 
             // Advance valentines day easter egg
-            if( world.isRemote && m_animation == TurtleAnimation.MoveForward && m_animationProgress == 4 )
+            if( world.isClient && m_animation == TurtleAnimation.MoveForward && m_animationProgress == 4 )
             {
                 // Spawn love pfx if valentines day
                 Holiday currentHoliday = HolidayUtil.getCurrentHoliday();
@@ -917,14 +916,14 @@ private void updateAnimation()
                     Vec3d position = getVisualPosition( 1.0f );
                     if( position != null )
                     {
-                        double x = position.x + world.rand.nextGaussian() * 0.1;
-                        double y = position.y + 0.5 + world.rand.nextGaussian() * 0.1;
-                        double z = position.z + world.rand.nextGaussian() * 0.1;
+                        double x = position.x + world.random.nextGaussian() * 0.1;
+                        double y = position.y + 0.5 + world.random.nextGaussian() * 0.1;
+                        double z = position.z + world.random.nextGaussian() * 0.1;
                         world.addParticle(
-                            Particles.HEART, x, y, z,
-                            world.rand.nextGaussian() * 0.02,
-                            world.rand.nextGaussian() * 0.02,
-                            world.rand.nextGaussian() * 0.02
+                            ParticleTypes.HEART, x, y, z,
+                            world.random.nextGaussian() * 0.02,
+                            world.random.nextGaussian() * 0.02,
+                            world.random.nextGaussian() * 0.02
                         );
                     }
                 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java
index 5b5be12634..9f4dd41c0c 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareCommand.java
@@ -10,14 +10,15 @@
 import dan200.computercraft.api.turtle.ITurtleCommand;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.NonNullList;
+import net.minecraft.server.world.ServerWorld;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
+import java.util.List;
 
 public class TurtleCompareCommand implements ITurtleCommand
 {
@@ -33,10 +34,10 @@ public TurtleCompareCommand( InteractDirection direction )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         // Get world direction from direction
-        EnumFacing direction = m_direction.toWorldDir( turtle );
+        Direction direction = m_direction.toWorldDir( turtle );
 
         // Get currently selected stack
-        ItemStack selectedStack = turtle.getInventory().getStackInSlot( turtle.getSelectedSlot() );
+        ItemStack selectedStack = turtle.getInventory().getInvStack( turtle.getSelectedSlot() );
 
         // Get stack representing thing in front
         World world = turtle.getWorld();
@@ -44,42 +45,22 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         BlockPos newPosition = oldPosition.offset( direction );
 
         ItemStack lookAtStack = ItemStack.EMPTY;
-        if( !world.isAirBlock( newPosition ) )
+        if( !world.isAir( newPosition ) )
         {
-            IBlockState lookAtState = world.getBlockState( newPosition );
+            BlockState lookAtState = world.getBlockState( newPosition );
             Block lookAtBlock = lookAtState.getBlock();
-            if( !lookAtBlock.isAir( lookAtState, world, newPosition ) )
+            if( !lookAtState.isAir() )
             {
-                // Try createStackedBlock first
-                /*
-                if( !lookAtBlock.hasTileEntity( lookAtState ) )
-                {
-                    try
-                    {
-                        Method method = ReflectionHelper.findMethod(
-                            Block.class,
-                            "func_180643_i", "getSilkTouchDrop",
-                            IBlockState.class
-                        );
-                        lookAtStack = (ItemStack) method.invoke( lookAtBlock, lookAtState );
-                    }
-                    catch( ReflectiveOperationException ignored )
-                    {
-                    }
-                }
-                */
-
                 // See if the block drops anything with the same ID as itself
                 // (try 5 times to try and beat random number generators)
                 for( int i = 0; i < 5 && lookAtStack.isEmpty(); i++ )
                 {
-                    NonNullList<ItemStack> drops = NonNullList.create();
-                    lookAtState.getDrops( drops, world, newPosition, 0 );
+                    List<ItemStack> drops = Block.getDroppedStacks( lookAtState, (ServerWorld) world, newPosition, world.getBlockEntity( newPosition ) );
                     if( !drops.isEmpty() )
                     {
                         for( ItemStack drop : drops )
                         {
-                            if( drop.getItem() == lookAtBlock.asItem() )
+                            if( drop.getItem() == lookAtBlock.getItem() )
                             {
                                 lookAtStack = drop;
                                 break;
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java
index d00b0b24e5..fe029ca232 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCompareToCommand.java
@@ -27,8 +27,8 @@ public TurtleCompareToCommand( int slot )
     @Override
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
-        ItemStack selectedStack = turtle.getInventory().getStackInSlot( turtle.getSelectedSlot() );
-        ItemStack stack = turtle.getInventory().getStackInSlot( m_slot );
+        ItemStack selectedStack = turtle.getInventory().getInvStack( turtle.getSelectedSlot() );
+        ItemStack stack = turtle.getInventory().getInvStack( m_slot );
         if( InventoryUtil.areItemsStackable( selectedStack, stack ) )
         {
             return TurtleCommandResult.success();
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java
index ad1f08ac9c..d8472a8a9d 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleCraftCommand.java
@@ -12,6 +12,7 @@
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.shared.turtle.upgrades.TurtleInventoryCrafting;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
 import net.minecraft.item.ItemStack;
 
@@ -37,9 +38,10 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         if( results == null ) return TurtleCommandResult.failure( "No matching recipes" );
 
         // Store or drop any remainders
+        ItemStorage storage = ItemStorage.wrap( turtle.getInventory() );
         for( ItemStack stack : results )
         {
-            ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() );
+            ItemStack remainder = InventoryUtil.storeItems( stack, storage, turtle.getSelectedSlot() );
             if( !remainder.isEmpty() )
             {
                 WorldUtil.dropItemStack( remainder, turtle.getWorld(), turtle.getPosition(), turtle.getDirection() );
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java
index ab89ae6b05..47258d6f35 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDetectCommand.java
@@ -10,8 +10,8 @@
 import dan200.computercraft.api.turtle.ITurtleCommand;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.shared.util.WorldUtil;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -30,14 +30,14 @@ public TurtleDetectCommand( InteractDirection direction )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         // Get world direction from direction
-        EnumFacing direction = m_direction.toWorldDir( turtle );
+        Direction direction = m_direction.toWorldDir( turtle );
 
         // Check if thing in front is air or not
         World world = turtle.getWorld();
         BlockPos oldPosition = turtle.getPosition();
         BlockPos newPosition = oldPosition.offset( direction );
 
-        if( !WorldUtil.isLiquidBlock( world, newPosition ) && !world.isAirBlock( newPosition ) )
+        if( !WorldUtil.isLiquidBlock( world, newPosition ) && !world.isAir( newPosition ) )
         {
             return TurtleCommandResult.success();
         }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java
index 21de8bab16..3f0ecc0a70 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleDropCommand.java
@@ -10,15 +10,16 @@
 import dan200.computercraft.api.turtle.ITurtleCommand;
 import dan200.computercraft.api.turtle.TurtleAnimation;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.api.turtle.event.TurtleInventoryEvent;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.items.IItemHandler;
 
 import javax.annotation.Nonnull;
 
@@ -45,10 +46,11 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         // Get world direction from direction
-        EnumFacing direction = m_direction.toWorldDir( turtle );
+        Direction direction = m_direction.toWorldDir( turtle );
+        ItemStorage storage = ItemStorage.wrap( turtle.getInventory() );
 
         // Get things to drop
-        ItemStack stack = InventoryUtil.takeItems( m_quantity, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
+        ItemStack stack = InventoryUtil.takeItems( m_quantity, storage, turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
         if( stack.isEmpty() )
         {
             return TurtleCommandResult.failure( "No items to drop" );
@@ -58,27 +60,27 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         World world = turtle.getWorld();
         BlockPos oldPosition = turtle.getPosition();
         BlockPos newPosition = oldPosition.offset( direction );
-        EnumFacing side = direction.getOpposite();
+        Direction side = direction.getOpposite();
 
-        IItemHandler inventory = InventoryUtil.getInventory( world, newPosition, side );
+        Inventory inventory = InventoryUtil.getInventory( world, newPosition, side );
 
         // Fire the event, restoring the inventory and exiting if it is cancelled.
         TurtlePlayer player = TurtlePlaceCommand.createPlayer( turtle, oldPosition, direction );
         TurtleInventoryEvent.Drop event = new TurtleInventoryEvent.Drop( turtle, player, world, newPosition, inventory, stack );
-        if( MinecraftForge.EVENT_BUS.post( event ) )
+        if( TurtleEvent.post( event ) )
         {
-            InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() );
+            InventoryUtil.storeItems( stack, storage, turtle.getSelectedSlot() );
             return TurtleCommandResult.failure( event.getFailureMessage() );
         }
 
         if( inventory != null )
         {
             // Drop the item into the inventory
-            ItemStack remainder = InventoryUtil.storeItems( stack, inventory );
+            ItemStack remainder = InventoryUtil.storeItems( stack, ItemStorage.wrap( inventory, side ) );
             if( !remainder.isEmpty() )
             {
                 // Put the remainder back in the turtle
-                InventoryUtil.storeItems( remainder, turtle.getItemHandler(), turtle.getSelectedSlot() );
+                InventoryUtil.storeItems( remainder, storage, turtle.getSelectedSlot() );
             }
 
             // Return true if we stored anything
@@ -96,7 +98,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         {
             // Drop the item into the world
             WorldUtil.dropItemStack( stack, world, oldPosition, direction );
-            world.playBroadcastSound( 1000, newPosition, 0 );
+            world.playGlobalEvent( 1000, newPosition, 0 ); // BLOCK_DISPENSER_DISPENSE
             turtle.playAnimation( TurtleAnimation.Wait );
             return TurtleCommandResult.success();
         }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java
index 795e3bd98f..c56350a826 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleEquipCommand.java
@@ -9,13 +9,14 @@
 import dan200.computercraft.api.turtle.*;
 import dan200.computercraft.api.turtle.event.TurtleAction;
 import dan200.computercraft.api.turtle.event.TurtleActionEvent;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.shared.TurtleUpgrades;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.math.BlockPos;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.items.IItemHandler;
 
 import javax.annotation.Nonnull;
 
@@ -35,8 +36,9 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         // Determine the upgrade to equipLeft
         ITurtleUpgrade newUpgrade;
         ItemStack newUpgradeStack;
-        IItemHandler inventory = turtle.getItemHandler();
-        ItemStack selectedStack = inventory.getStackInSlot( turtle.getSelectedSlot() );
+        Inventory inventory = turtle.getInventory();
+        ItemStorage storage = ItemStorage.wrap( turtle.getInventory() );
+        ItemStack selectedStack = inventory.getInvStack( turtle.getSelectedSlot() );
         if( !selectedStack.isEmpty() )
         {
             newUpgradeStack = selectedStack.copy();
@@ -66,7 +68,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         TurtleActionEvent event = new TurtleActionEvent( turtle, TurtleAction.EQUIP );
-        if( MinecraftForge.EVENT_BUS.post( event ) )
+        if( TurtleEvent.post( event ) )
         {
             return TurtleCommandResult.failure( event.getFailureMessage() );
         }
@@ -75,12 +77,12 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         if( newUpgradeStack != null )
         {
             // Consume new upgrades item
-            InventoryUtil.takeItems( 1, inventory, turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
+            InventoryUtil.takeItems( 1, storage, turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
         }
         if( oldUpgradeStack != null )
         {
             // Store old upgrades item
-            ItemStack remainder = InventoryUtil.storeItems( oldUpgradeStack, inventory, turtle.getSelectedSlot() );
+            ItemStack remainder = InventoryUtil.storeItems( oldUpgradeStack, storage, turtle.getSelectedSlot() );
             if( !remainder.isEmpty() )
             {
                 // If there's no room for the items, drop them
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java
index d1f75e761a..a5fa4fc42f 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleInspectCommand.java
@@ -11,14 +11,14 @@
 import dan200.computercraft.api.turtle.ITurtleCommand;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.api.turtle.event.TurtleBlockEvent;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.state.IProperty;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.BlockState;
+import net.minecraft.state.property.Property;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.registry.Registry;
 import net.minecraft.world.World;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.registries.ForgeRegistries;
 
 import javax.annotation.Nonnull;
 import java.util.HashMap;
@@ -38,29 +38,29 @@ public TurtleInspectCommand( InteractDirection direction )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         // Get world direction from direction
-        EnumFacing direction = this.direction.toWorldDir( turtle );
+        Direction direction = this.direction.toWorldDir( turtle );
 
         // Check if thing in front is air or not
         World world = turtle.getWorld();
         BlockPos oldPosition = turtle.getPosition();
         BlockPos newPosition = oldPosition.offset( direction );
 
-        IBlockState state = world.getBlockState( newPosition );
-        if( state.getBlock().isAir( state, world, newPosition ) )
+        BlockState state = world.getBlockState( newPosition );
+        if( state.isAir() )
         {
             return TurtleCommandResult.failure( "No block to inspect" );
         }
 
         Block block = state.getBlock();
-        String name = ForgeRegistries.BLOCKS.getKey( block ).toString();
+        String name = Registry.BLOCK.getId( block ).toString();
 
         Map<String, Object> table = new HashMap<>();
         table.put( "name", name );
 
         Map<Object, Object> stateTable = new HashMap<>();
-        for( ImmutableMap.Entry<IProperty<?>, ? extends Comparable<?>> entry : state.getValues().entrySet() )
+        for( ImmutableMap.Entry<Property<?>, ? extends Comparable<?>> entry : state.getEntries().entrySet() )
         {
-            IProperty<?> property = entry.getKey();
+            Property<?> property = entry.getKey();
             stateTable.put( property.getName(), getPropertyValue( property, entry.getValue() ) );
         }
         table.put( "state", stateTable );
@@ -68,16 +68,16 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         // Fire the event, exiting if it is cancelled
         TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, oldPosition, direction );
         TurtleBlockEvent.Inspect event = new TurtleBlockEvent.Inspect( turtle, turtlePlayer, world, newPosition, state, table );
-        if( MinecraftForge.EVENT_BUS.post( event ) ) return TurtleCommandResult.failure( event.getFailureMessage() );
+        if( TurtleEvent.post( event ) ) return TurtleCommandResult.failure( event.getFailureMessage() );
 
         return TurtleCommandResult.success( new Object[] { table } );
 
     }
 
     @SuppressWarnings( { "unchecked", "rawtypes" } )
-    private static Object getPropertyValue( IProperty property, Comparable value )
+    private static Object getPropertyValue( Property property, Comparable value )
     {
         if( value instanceof String || value instanceof Number || value instanceof Boolean ) return value;
-        return property.getName( value );
+        return property.getValueAsString( value );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java
index dfe3dd5c1b..bfc361b562 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleMoveCommand.java
@@ -12,16 +12,18 @@
 import dan200.computercraft.api.turtle.TurtleAnimation;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.api.turtle.event.TurtleBlockEvent;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.shared.TurtlePermissions;
 import dan200.computercraft.shared.util.WorldUtil;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
 import net.minecraft.entity.Entity;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.predicate.entity.EntityPredicates;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.math.BoundingBox;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.shape.VoxelShape;
 import net.minecraft.world.World;
-import net.minecraftforge.common.MinecraftForge;
 
 import javax.annotation.Nonnull;
 import java.util.List;
@@ -40,7 +42,7 @@ public TurtleMoveCommand( MoveDirection direction )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         // Get world direction from direction
-        EnumFacing direction = m_direction.toWorldDir( turtle );
+        Direction direction = m_direction.toWorldDir( turtle );
 
         // Check if we can move
         World oldWorld = turtle.getWorld();
@@ -55,8 +57,10 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         // Check existing block is air or replaceable
-        IBlockState state = oldWorld.getBlockState( newPosition );
-        if( !oldWorld.isAirBlock( newPosition ) &&
+        BlockState state = oldWorld.getBlockState( newPosition );
+        Block block = state.getBlock();
+        if( block != null &&
+            !oldWorld.isAir( newPosition ) &&
             !WorldUtil.isLiquidBlock( oldWorld, newPosition ) &&
             !state.getMaterial().isReplaceable() )
         {
@@ -64,14 +68,13 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         // Check there isn't anything in the way
-        AxisAlignedBB aabb = getBox( state.getCollisionShape( oldWorld, oldPosition ) );
+        BoundingBox aabb = getBox( state.getCollisionShape( oldWorld, oldPosition ) );
         aabb = aabb.offset(
             newPosition.getX(),
             newPosition.getY(),
             newPosition.getZ()
         );
-
-        if( !oldWorld.checkNoEntityCollision( null, aabb ) )
+        if( !oldWorld.doesNotCollide( aabb ) )
         {
             if( !ComputerCraft.turtlesCanPush || m_direction == MoveDirection.Up || m_direction == MoveDirection.Down )
             {
@@ -79,19 +82,18 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
             }
 
             // Check there is space for all the pushable entities to be pushed
-            List<Entity> list = oldWorld.getEntitiesWithinAABB( Entity.class, aabb, x -> x != null && x.isAlive() && x.preventEntitySpawning );
+            List<Entity> list = oldWorld.getEntities( (Entity) null, aabb, EntityPredicates.VALID_ENTITY );
             for( Entity entity : list )
             {
-                AxisAlignedBB entityBB = entity.getBoundingBox();
-                if( entityBB == null ) entityBB = entity.getCollisionBoundingBox();
+                BoundingBox entityBB = entity.getBoundingBox();
                 if( entityBB == null ) continue;
 
-                AxisAlignedBB pushedBB = entityBB.offset(
-                    direction.getXOffset(),
-                    direction.getYOffset(),
-                    direction.getZOffset()
+                BoundingBox pushedBB = entityBB.offset(
+                    direction.getOffsetX(),
+                    direction.getOffsetY(),
+                    direction.getOffsetZ()
                 );
-                if( !oldWorld.checkNoEntityCollision( null, pushedBB ) )
+                if( oldWorld.doesNotCollide( pushedBB ) )
                 {
                     return TurtleCommandResult.failure( "Movement obstructed" );
                 }
@@ -99,7 +101,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         TurtleBlockEvent.Move moveEvent = new TurtleBlockEvent.Move( turtle, turtlePlayer, oldWorld, newPosition );
-        if( MinecraftForge.EVENT_BUS.post( moveEvent ) )
+        if( TurtleEvent.post( moveEvent ) )
         {
             return TurtleCommandResult.failure( moveEvent.getFailureMessage() );
         }
@@ -111,7 +113,10 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         // Move
-        if( !turtle.teleportTo( oldWorld, newPosition ) ) return TurtleCommandResult.failure( "Movement failed" );
+        if( !turtle.teleportTo( oldWorld, newPosition ) )
+        {
+            return TurtleCommandResult.failure( "Movement failed" );
+        }
 
         // Consume fuel
         turtle.consumeFuel( 1 );
@@ -138,7 +143,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
 
     private static TurtleCommandResult canEnter( TurtlePlayer turtlePlayer, World world, BlockPos position )
     {
-        if( World.isOutsideBuildHeight( position ) )
+        if( World.isHeightInvalid( position ) )
         {
             return TurtleCommandResult.failure( position.getY() < 0 ? "Too low to move" : "Too high to move" );
         }
@@ -159,10 +164,10 @@ private static TurtleCommandResult canEnter( TurtlePlayer turtlePlayer, World wo
         return TurtleCommandResult.success();
     }
 
-    private static AxisAlignedBB getBox( VoxelShape shape )
+    private static BoundingBox getBox( VoxelShape shape )
     {
         return shape.isEmpty() ? EMPTY_BOX : shape.getBoundingBox();
     }
 
-    private static final AxisAlignedBB EMPTY_BOX = new AxisAlignedBB( 0, 0, 0, 0, 0, 0 );
+    private static final BoundingBox EMPTY_BOX = new BoundingBox( 0, 0, 0, 0, 0, 0 );
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java
index 463f385d8d..5438200502 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlaceCommand.java
@@ -12,30 +12,28 @@
 import dan200.computercraft.api.turtle.TurtleAnimation;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.api.turtle.event.TurtleBlockEvent;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.shared.TurtlePermissions;
-import dan200.computercraft.shared.util.DirectionUtil;
-import dan200.computercraft.shared.util.DropConsumer;
-import dan200.computercraft.shared.util.InventoryUtil;
-import dan200.computercraft.shared.util.WorldUtil;
+import dan200.computercraft.shared.util.*;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.SignBlockEntity;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.entity.LivingEntity;
 import net.minecraft.item.*;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntitySign;
+import net.minecraft.item.block.BlockItem;
+import net.minecraft.item.block.LilyPadItem;
+import net.minecraft.item.block.SignItem;
+import net.minecraft.text.StringTextComponent;
 import net.minecraft.util.ActionResult;
-import net.minecraft.util.EnumActionResult;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
+import net.minecraft.util.Hand;
+import net.minecraft.util.TypedActionResult;
+import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.text.TextComponentString;
 import net.minecraft.world.World;
-import net.minecraftforge.common.ForgeHooks;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.event.entity.player.PlayerInteractEvent;
-import net.minecraftforge.eventbus.api.Event;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -57,14 +55,14 @@ public TurtlePlaceCommand( InteractDirection direction, Object[] arguments )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         // Get thing to place
-        ItemStack stack = turtle.getInventory().getStackInSlot( turtle.getSelectedSlot() );
+        ItemStack stack = turtle.getInventory().getInvStack( turtle.getSelectedSlot() );
         if( stack.isEmpty() )
         {
             return TurtleCommandResult.failure( "No items to place" );
         }
 
         // Remember old block
-        EnumFacing direction = m_direction.toWorldDir( turtle );
+        Direction direction = m_direction.toWorldDir( turtle );
         World world = turtle.getWorld();
         BlockPos coordinates = turtle.getPosition().offset( direction );
 
@@ -73,7 +71,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         TurtlePlayer turtlePlayer = createPlayer( turtle, playerPosition, direction );
 
         TurtleBlockEvent.Place place = new TurtleBlockEvent.Place( turtle, turtlePlayer, turtle.getWorld(), coordinates, stack );
-        if( MinecraftForge.EVENT_BUS.post( place ) )
+        if( TurtleEvent.post( place ) )
         {
             return TurtleCommandResult.failure( place.getFailureMessage() );
         }
@@ -84,7 +82,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         if( remainder != stack )
         {
             // Put the remaining items back
-            turtle.getInventory().setInventorySlotContents( turtle.getSelectedSlot(), remainder );
+            turtle.getInventory().setInvStack( turtle.getSelectedSlot(), remainder );
             turtle.getInventory().markDirty();
 
             // Animate and return success
@@ -97,7 +95,7 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
             {
                 return TurtleCommandResult.failure( errorMessage[0] );
             }
-            else if( stack.getItem() instanceof ItemBlock )
+            else if( stack.getItem() instanceof BlockItem )
             {
                 return TurtleCommandResult.failure( "Cannot place block here" );
             }
@@ -108,7 +106,7 @@ else if( stack.getItem() instanceof ItemBlock )
         }
     }
 
-    public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle, EnumFacing direction, Object[] extraArguments, String[] outErrorMessage )
+    public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle, Direction direction, Object[] extraArguments, String[] outErrorMessage )
     {
         // Create a fake player, and orient it appropriately
         BlockPos playerPosition = turtle.getPosition().offset( direction );
@@ -117,7 +115,7 @@ public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle,
         return deploy( stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage );
     }
 
-    public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, EnumFacing direction, Object[] extraArguments, String[] outErrorMessage )
+    public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction, Object[] extraArguments, String[] outErrorMessage )
     {
         // Deploy on an entity
         ItemStack remainder = deployOnEntity( stack, turtle, turtlePlayer, direction, extraArguments, outErrorMessage );
@@ -142,10 +140,10 @@ public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle,
             return remainder;
         }
 
-        if( direction.getAxis() != EnumFacing.Axis.Y )
+        if( direction.getAxis() != Direction.Axis.Y )
         {
             // Deploy down on the block in front
-            remainder = deployOnBlock( stack, turtle, turtlePlayer, newPosition.down(), EnumFacing.UP, extraArguments, false, outErrorMessage );
+            remainder = deployOnBlock( stack, turtle, turtlePlayer, newPosition.down(), Direction.UP, extraArguments, false, outErrorMessage );
             if( remainder != stack )
             {
                 return remainder;
@@ -163,56 +161,56 @@ public static ItemStack deploy( @Nonnull ItemStack stack, ITurtleAccess turtle,
         return stack;
     }
 
-    public static TurtlePlayer createPlayer( ITurtleAccess turtle, BlockPos position, EnumFacing direction )
+    public static TurtlePlayer createPlayer( ITurtleAccess turtle, BlockPos position, Direction direction )
     {
         TurtlePlayer turtlePlayer = TurtlePlayer.get( turtle );
         orientPlayer( turtle, turtlePlayer, position, direction );
         return turtlePlayer;
     }
 
-    private static void orientPlayer( ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, EnumFacing direction )
+    private static void orientPlayer( ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, Direction direction )
     {
-        turtlePlayer.posX = position.getX() + 0.5;
-        turtlePlayer.posY = position.getY() + 0.5;
-        turtlePlayer.posZ = position.getZ() + 0.5;
+        turtlePlayer.x = position.getX() + 0.5;
+        turtlePlayer.y = position.getY() + 0.5;
+        turtlePlayer.z = position.getZ() + 0.5;
 
         // Stop intersection with the turtle itself
         if( turtle.getPosition().equals( position ) )
         {
-            turtlePlayer.posX += 0.48 * direction.getXOffset();
-            turtlePlayer.posY += 0.48 * direction.getYOffset();
-            turtlePlayer.posZ += 0.48 * direction.getZOffset();
+            turtlePlayer.x += 0.48 * direction.getOffsetX();
+            turtlePlayer.y += 0.48 * direction.getOffsetY();
+            turtlePlayer.z += 0.48 * direction.getOffsetZ();
         }
 
-        if( direction.getAxis() != EnumFacing.Axis.Y )
+        if( direction.getAxis() != Direction.Axis.Y )
         {
-            turtlePlayer.rotationYaw = direction.getHorizontalAngle();
-            turtlePlayer.rotationPitch = 0.0f;
+            turtlePlayer.yaw = direction.asRotation();
+            turtlePlayer.pitch = 0.0f;
         }
         else
         {
-            turtlePlayer.rotationYaw = turtle.getDirection().getHorizontalAngle();
-            turtlePlayer.rotationPitch = DirectionUtil.toPitchAngle( direction );
+            turtlePlayer.yaw = turtle.getDirection().asRotation();
+            turtlePlayer.pitch = DirectionUtil.toPitchAngle( direction );
         }
 
-        turtlePlayer.prevPosX = turtlePlayer.posX;
-        turtlePlayer.prevPosY = turtlePlayer.posY;
-        turtlePlayer.prevPosZ = turtlePlayer.posZ;
-        turtlePlayer.prevRotationPitch = turtlePlayer.rotationPitch;
-        turtlePlayer.prevRotationYaw = turtlePlayer.rotationYaw;
+        turtlePlayer.prevX = turtlePlayer.x;
+        turtlePlayer.prevY = turtlePlayer.y;
+        turtlePlayer.prevZ = turtlePlayer.z;
+        turtlePlayer.prevPitch = turtlePlayer.pitch;
+        turtlePlayer.prevYaw = turtlePlayer.yaw;
 
-        turtlePlayer.rotationYawHead = turtlePlayer.rotationYaw;
-        turtlePlayer.prevRotationYawHead = turtlePlayer.rotationYawHead;
+        turtlePlayer.headYaw = turtlePlayer.yaw;
+        turtlePlayer.prevHeadYaw = turtlePlayer.yaw;
     }
 
     @Nonnull
-    private static ItemStack deployOnEntity( @Nonnull ItemStack stack, final ITurtleAccess turtle, TurtlePlayer turtlePlayer, EnumFacing direction, Object[] extraArguments, String[] outErrorMessage )
+    private static ItemStack deployOnEntity( @Nonnull ItemStack stack, final ITurtleAccess turtle, TurtlePlayer turtlePlayer, Direction direction, Object[] extraArguments, String[] outErrorMessage )
     {
         // See if there is an entity present
         final World world = turtle.getWorld();
         final BlockPos position = turtle.getPosition();
-        Vec3d turtlePos = new Vec3d( turtlePlayer.posX, turtlePlayer.posY, turtlePlayer.posZ );
-        Vec3d rayDir = turtlePlayer.getLook( 1.0f );
+        Vec3d turtlePos = new Vec3d( turtlePlayer.x, turtlePlayer.y, turtlePlayer.z );
+        Vec3d rayDir = turtlePlayer.getRotationVec( 1.0f );
         Pair<Entity, Vec3d> hit = WorldUtil.rayTraceEntities( world, turtlePos, rayDir, 1.5 );
         if( hit == null )
         {
@@ -228,38 +226,38 @@ private static ItemStack deployOnEntity( @Nonnull ItemStack stack, final ITurtle
         Vec3d hitPos = hit.getValue();
         DropConsumer.set(
             hitEntity,
-            drop -> InventoryUtil.storeItems( drop, turtle.getItemHandler(), turtle.getSelectedSlot() )
+            drop -> InventoryUtil.storeItems( drop, ItemStorage.wrap( turtle.getInventory() ), turtle.getSelectedSlot() )
         );
 
         // Place on the entity
         boolean placed = false;
-        EnumActionResult cancelResult = ForgeHooks.onInteractEntityAt( turtlePlayer, hitEntity, hitPos, EnumHand.MAIN_HAND );
+        ActionResult cancelResult = null; // ForgeHooks.onInteractEntityAt( turtlePlayer, hitEntity, hitPos, Hand.MAIN );
         if( cancelResult == null )
         {
-            cancelResult = hitEntity.applyPlayerInteraction( turtlePlayer, hitPos, EnumHand.MAIN_HAND );
+            cancelResult = hitEntity.interactAt( turtlePlayer, hitPos, Hand.MAIN );
         }
 
-        if( cancelResult == EnumActionResult.SUCCESS )
+        if( cancelResult == ActionResult.SUCCESS )
         {
             placed = true;
         }
         else
         {
-            // See EntityPlayer.interactOn
-            cancelResult = ForgeHooks.onInteractEntity( turtlePlayer, hitEntity, EnumHand.MAIN_HAND );
-            if( cancelResult == EnumActionResult.SUCCESS )
+            // See PlayerEntity.interactOn
+            // cancelResult = ForgeHooks.onInteractEntity( turtlePlayer, hitEntity, Hand.MAIN );
+            if( cancelResult == ActionResult.SUCCESS )
             {
                 placed = true;
             }
             else if( cancelResult == null )
             {
-                if( hitEntity.processInitialInteract( turtlePlayer, EnumHand.MAIN_HAND ) )
+                if( hitEntity.interact( turtlePlayer, Hand.MAIN ) )
                 {
                     placed = true;
                 }
-                else if( hitEntity instanceof EntityLivingBase )
+                else if( hitEntity instanceof LivingEntity )
                 {
-                    placed = stackCopy.interactWithEntity( turtlePlayer, (EntityLivingBase) hitEntity, EnumHand.MAIN_HAND );
+                    placed = stackCopy.interactWithEntity( turtlePlayer, (LivingEntity) hitEntity, Hand.MAIN );
                     if( placed ) turtlePlayer.loadInventory( stackCopy );
                 }
             }
@@ -274,7 +272,7 @@ else if( hitEntity instanceof EntityLivingBase )
 
         // Put everything we collected into the turtles inventory, then return
         ItemStack remainder = turtlePlayer.unloadInventory( turtle );
-        if( !placed && ItemStack.areItemStacksEqual( stack, remainder ) )
+        if( !placed && ItemStack.areEqual( stack, remainder ) )
         {
             return stack;
         }
@@ -288,20 +286,20 @@ else if( !remainder.isEmpty() )
         }
     }
 
-    private static boolean canDeployOnBlock( @Nonnull BlockItemUseContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, EnumFacing side, boolean allowReplaceable, String[] outErrorMessage )
+    private static boolean canDeployOnBlock( @Nonnull ItemPlacementContext context, ITurtleAccess turtle, TurtlePlayer player, BlockPos position, Direction side, boolean allowReplaceable, String[] outErrorMessage )
     {
         World world = turtle.getWorld();
         if( World.isValid( position ) &&
-            !world.isAirBlock( position ) &&
-            !(context.getItem().getItem() instanceof ItemBlock && WorldUtil.isLiquidBlock( world, position )) )
+            !world.isAir( position ) &&
+            !(context.getItemStack().getItem() instanceof BlockItem && WorldUtil.isLiquidBlock( world, position )) )
         {
             return false;
         }
 
-        IBlockState state = world.getBlockState( position );
+        BlockState state = world.getBlockState( position );
         Block block = state.getBlock();
 
-        boolean replaceable = state.isReplaceable( context );
+        boolean replaceable = state.method_11587( context );
         if( !allowReplaceable && replaceable ) return false;
 
         if( ComputerCraft.turtlesObeyBlockProtection )
@@ -318,14 +316,14 @@ private static boolean canDeployOnBlock( @Nonnull BlockItemUseContext context, I
         }
 
         // Check the block is solid or liquid
-        return block.isCollidable( state );
+        return !state.getCollisionShape( world, position ).isEmpty();
     }
 
     @Nonnull
-    private static ItemStack deployOnBlock( @Nonnull ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, EnumFacing side, Object[] extraArguments, boolean allowReplace, String[] outErrorMessage )
+    private static ItemStack deployOnBlock( @Nonnull ItemStack stack, ITurtleAccess turtle, TurtlePlayer turtlePlayer, BlockPos position, Direction side, Object[] extraArguments, boolean allowReplace, String[] outErrorMessage )
     {
         // Re-orient the fake player
-        EnumFacing playerDir = side.getOpposite();
+        Direction playerDir = side.getOpposite();
         BlockPos playerPosition = position.offset( side );
         orientPlayer( turtle, turtlePlayer, playerPosition, playerDir );
 
@@ -333,17 +331,17 @@ private static ItemStack deployOnBlock( @Nonnull ItemStack stack, ITurtleAccess
         turtlePlayer.loadInventory( stackCopy );
 
         // Calculate where the turtle would hit the block
-        float hitX = 0.5f + side.getXOffset() * 0.5f;
-        float hitY = 0.5f + side.getYOffset() * 0.5f;
-        float hitZ = 0.5f + side.getZOffset() * 0.5f;
+        float hitX = 0.5f + side.getOffsetX() * 0.5f;
+        float hitY = 0.5f + side.getOffsetY() * 0.5f;
+        float hitZ = 0.5f + side.getOffsetZ() * 0.5f;
         if( Math.abs( hitY - 0.5f ) < 0.01f )
         {
             hitY = 0.45f;
         }
 
         // Check if there's something suitable to place onto
-        ItemUseContext context = new ItemUseContext( turtlePlayer, stackCopy, position, side, hitX, hitY, hitZ );
-        if( !canDeployOnBlock( new BlockItemUseContext( context ), turtle, turtlePlayer, position, side, allowReplace, outErrorMessage ) )
+        ItemUsageContext context = new ItemUsageContext( turtlePlayer, Hand.MAIN, new BlockHitResult( new Vec3d( hitX, hitY, hitZ ), side, position, false ) );
+        if( !canDeployOnBlock( new ItemPlacementContext( context ), turtle, turtlePlayer, position, side, allowReplace, outErrorMessage ) )
         {
             return stack;
         }
@@ -353,88 +351,89 @@ private static ItemStack deployOnBlock( @Nonnull ItemStack stack, ITurtleAccess
 
         // Do the deploying (put everything in the players inventory)
         boolean placed = false;
-        TileEntity existingTile = turtle.getWorld().getTileEntity( position );
+        BlockEntity existingTile = turtle.getWorld().getBlockEntity( position );
 
         // See PlayerInteractionManager.processRightClickBlock
-        // TODO: ^ Check we're still consistent.
-        PlayerInteractEvent.RightClickBlock event = ForgeHooks.onRightClickBlock( turtlePlayer, EnumHand.MAIN_HAND, position, side, new Vec3d( hitX, hitY, hitZ ) );
-        if( !event.isCanceled() )
+        /*
+        PlayerInteractEvent.RightClickBlock event = ForgeHooks.onRightClickBlock( turtlePlayer, Hand.MAIN, position, side, new Vec3d( hitX, hitY, hitZ ) );
+        if( !event.isCanceled() ) */
         {
-            if( item.onItemUseFirst( stack, context ) == EnumActionResult.SUCCESS )
+            /* if( item.onItemUseFirst( turtlePlayer, turtle.getWorld(), position, side, hitX, hitY, hitZ, Hand.MAIN ) == ActionResult.SUCCESS )
             {
                 placed = true;
                 turtlePlayer.loadInventory( stackCopy );
             }
-            else if( event.getUseItem() != Event.Result.DENY &&
-                stackCopy.onItemUse( context ) == EnumActionResult.SUCCESS )
+            else*/
+            if( /* event.getUseItem() != Event.Result.DENY && */
+                stackCopy.useOnBlock( context ) == ActionResult.SUCCESS )
             {
                 placed = true;
                 turtlePlayer.loadInventory( stackCopy );
             }
         }
 
-        if( !placed && (item instanceof ItemBucket || item instanceof ItemBoat || item instanceof ItemLilyPad || item instanceof ItemGlassBottle) )
+        if( !placed && (item instanceof BucketItem || item instanceof BoatItem || item instanceof LilyPadItem || item instanceof GlassBottleItem) )
         {
-            EnumActionResult actionResult = ForgeHooks.onItemRightClick( turtlePlayer, EnumHand.MAIN_HAND );
-            if( actionResult == EnumActionResult.SUCCESS )
+            ActionResult actionResult = null; // ForgeHooks.onItemRightClick( turtlePlayer, Hand.MAIN );
+            if( actionResult == ActionResult.SUCCESS )
             {
                 placed = true;
             }
             else if( actionResult == null )
             {
-                ActionResult<ItemStack> result = stackCopy.useItemRightClick( turtle.getWorld(), turtlePlayer, EnumHand.MAIN_HAND );
-                if( result.getType() == EnumActionResult.SUCCESS && !ItemStack.areItemStacksEqual( stack, result.getResult() ) )
+                TypedActionResult<ItemStack> result = stackCopy.use( turtle.getWorld(), turtlePlayer, Hand.MAIN );
+                if( result.getResult() == ActionResult.SUCCESS && !ItemStack.areEqual( stack, result.getValue() ) )
                 {
                     placed = true;
-                    turtlePlayer.loadInventory( result.getResult() );
+                    turtlePlayer.loadInventory( result.getValue() );
                 }
             }
         }
 
         // Set text on signs
-        if( placed && item instanceof ItemSign )
+        if( placed && item instanceof SignItem )
         {
             if( extraArguments != null && extraArguments.length >= 1 && extraArguments[0] instanceof String )
             {
                 World world = turtle.getWorld();
-                TileEntity tile = world.getTileEntity( position );
+                BlockEntity tile = world.getBlockEntity( position );
                 if( tile == null || tile == existingTile )
                 {
-                    tile = world.getTileEntity( position.offset( side ) );
+                    tile = world.getBlockEntity( position.offset( side ) );
                 }
-                if( tile instanceof TileEntitySign )
+                if( tile instanceof SignBlockEntity )
                 {
-                    TileEntitySign signTile = (TileEntitySign) tile;
+                    SignBlockEntity signTile = (SignBlockEntity) tile;
                     String s = (String) extraArguments[0];
                     String[] split = s.split( "\n" );
                     int firstLine = split.length <= 2 ? 1 : 0;
-                    for( int i = 0; i < signTile.signText.length; i++ )
+                    for( int i = 0; i < signTile.text.length; i++ )
                     {
                         if( i >= firstLine && i < firstLine + split.length )
                         {
                             if( split[i - firstLine].length() > 15 )
                             {
-                                signTile.signText[i] = new TextComponentString( split[i - firstLine].substring( 0, 15 ) );
+                                signTile.text[i] = new StringTextComponent( split[i - firstLine].substring( 0, 15 ) );
                             }
                             else
                             {
-                                signTile.signText[i] = new TextComponentString( split[i - firstLine] );
+                                signTile.text[i] = new StringTextComponent( split[i - firstLine] );
                             }
                         }
                         else
                         {
-                            signTile.signText[i] = new TextComponentString( "" );
+                            signTile.text[i] = new StringTextComponent( "" );
                         }
                     }
                     signTile.markDirty();
-                    world.markBlockRangeForRenderUpdate( signTile.getPos(), signTile.getPos() );
+                    world.scheduleBlockRender( signTile.getPos() ); // TODO: This doesn't do anything!
                 }
             }
         }
 
         // Put everything we collected into the turtles inventory, then return
         ItemStack remainder = turtlePlayer.unloadInventory( turtle );
-        if( !placed && ItemStack.areItemStacksEqual( stack, remainder ) )
+        if( !placed && ItemStack.areEqual( stack, remainder ) )
         {
             return stack;
         }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java
index 5640330551..e47131d589 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtlePlayer.java
@@ -7,25 +7,27 @@
 package dan200.computercraft.shared.turtle.core;
 
 import com.mojang.authlib.GameProfile;
-import dan200.computercraft.ComputerCraft;
+import com.mojang.datafixers.util.Either;
 import dan200.computercraft.api.turtle.ITurtleAccess;
+import dan200.computercraft.api.turtle.event.FakePlayer;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
+import net.minecraft.command.arguments.EntityAnchorArgumentType;
+import net.minecraft.container.Container;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.EntityType;
-import net.minecraft.entity.IMerchant;
-import net.minecraft.entity.passive.AbstractHorse;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.entity.passive.HorseBaseEntity;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntitySign;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.text.TextComponent;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Void;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.world.IInteractionObject;
 import net.minecraft.world.World;
-import net.minecraft.world.WorldServer;
-import net.minecraftforge.common.util.FakePlayer;
+import net.minecraft.world.dimension.DimensionType;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -33,24 +35,19 @@
 
 public final class TurtlePlayer extends FakePlayer
 {
-    public static final GameProfile DEFAULT_PROFILE = new GameProfile(
+    private static final GameProfile DEFAULT_PROFILE = new GameProfile(
         UUID.fromString( "0d0c4ca0-4ff1-11e4-916c-0800200c9a66" ),
         "[ComputerCraft]"
     );
 
-    public static final EntityType<TurtlePlayer> TYPE = EntityType.Builder.create( TurtlePlayer.class, TurtlePlayer::new )
-        .disableSerialization()
-        .disableSummoning()
-        .build( ComputerCraft.MOD_ID + ":turtle_player" );
-
     private TurtlePlayer( World world )
     {
-        super( (WorldServer) world, DEFAULT_PROFILE );
+        super( (ServerWorld) world, DEFAULT_PROFILE );
     }
 
     private TurtlePlayer( ITurtleAccess turtle )
     {
-        super( (WorldServer) turtle.getWorld(), getProfile( turtle.getOwningPlayer() ) );
+        super( (ServerWorld) turtle.getWorld(), getProfile( turtle.getOwningPlayer() ) );
         setState( turtle );
     }
 
@@ -62,12 +59,12 @@ private static GameProfile getProfile( @Nullable GameProfile profile )
     private void setState( ITurtleAccess turtle )
     {
         BlockPos position = turtle.getPosition();
-        posX = position.getX() + 0.5;
-        posY = position.getY() + 0.5;
-        posZ = position.getZ() + 0.5;
+        x = position.getX() + 0.5;
+        y = position.getY() + 0.5;
+        z = position.getZ() + 0.5;
 
-        rotationYaw = turtle.getDirection().getHorizontalAngle();
-        rotationPitch = 0.0f;
+        yaw = turtle.getDirection().asRotation();
+        pitch = 0.0f;
 
         inventory.clear();
     }
@@ -94,30 +91,31 @@ public static TurtlePlayer get( ITurtleAccess access )
     public void loadInventory( @Nonnull ItemStack currentStack )
     {
         // Load up the fake inventory
-        inventory.currentItem = 0;
-        inventory.setInventorySlotContents( 0, currentStack );
+        inventory.selectedSlot = 0;
+        inventory.setInvStack( 0, currentStack );
     }
 
     public ItemStack unloadInventory( ITurtleAccess turtle )
     {
         // Get the item we placed with
-        ItemStack results = inventory.getStackInSlot( 0 );
-        inventory.setInventorySlotContents( 0, ItemStack.EMPTY );
+        ItemStack results = inventory.getInvStack( 0 );
+        inventory.setInvStack( 0, ItemStack.EMPTY );
 
         // Store (or drop) anything else we found
         BlockPos dropPosition = turtle.getPosition();
-        EnumFacing dropDirection = turtle.getDirection().getOpposite();
-        for( int i = 0; i < inventory.getSizeInventory(); i++ )
+        Direction dropDirection = turtle.getDirection().getOpposite();
+        ItemStorage storage = ItemStorage.wrap( turtle.getInventory() );
+        for( int i = 0; i < inventory.getInvSize(); ++i )
         {
-            ItemStack stack = inventory.getStackInSlot( i );
+            ItemStack stack = inventory.getInvStack( i );
             if( !stack.isEmpty() )
             {
-                ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() );
+                ItemStack remainder = InventoryUtil.storeItems( stack, storage, turtle.getSelectedSlot() );
                 if( !remainder.isEmpty() )
                 {
                     WorldUtil.dropItemStack( remainder, turtle.getWorld(), dropPosition, dropDirection );
                 }
-                inventory.setInventorySlotContents( i, ItemStack.EMPTY );
+                inventory.setInvStack( i, ItemStack.EMPTY );
             }
         }
         inventory.markDirty();
@@ -125,91 +123,86 @@ public ItemStack unloadInventory( ITurtleAccess turtle )
     }
 
     @Override
-    public Vec3d getPositionVector()
+    public void method_6000()
     {
-        return new Vec3d( posX, posY, posZ );
     }
 
     @Override
-    public float getEyeHeight()
+    public void method_6044()
     {
-        return 0.0f;
     }
 
     @Override
-    public float getDefaultEyeHeight()
+    @Nullable
+    public Entity changeDimension( DimensionType dimensionType )
     {
-        return 0.0f;
+        return null;
     }
 
-    @Override
-    public void sendEnterCombat()
-    {
-    }
 
+    @Nonnull
     @Override
-    public void sendEndCombat()
+    public Either<SleepResult, Void> trySleep( @Nonnull BlockPos bedLocation )
     {
+        return Either.left( SleepResult.INVALID_ATTEMPT );
     }
 
-    @Nonnull
     @Override
-    public SleepResult trySleep( @Nonnull BlockPos bedLocation )
+    public boolean startRiding( Entity entity, boolean bool )
     {
-        return SleepResult.OTHER_PROBLEM;
+        return false;
     }
 
-    // TODO: Flesh this out. Or just stub out the connection, like plethora?
-
     @Override
-    public void openSignEditor( TileEntitySign signTile )
+    public void stopRiding()
     {
     }
 
     @Override
-    public void displayGui( IInteractionObject guiOwner )
+    public void openHorseInventory( HorseBaseEntity entity, Inventory inventory )
     {
     }
 
     @Override
-    public void displayGUIChest( IInventory chestInventory )
+    public void onContainerRegistered( Container container, DefaultedList<ItemStack> defaultedList )
     {
     }
 
     @Override
-    public void displayVillagerTradeGui( IMerchant villager )
+    public void onContainerPropertyUpdate( Container container, int i, int j )
     {
     }
 
+
     @Override
-    public void openHorseInventory( AbstractHorse horse, IInventory inventoryIn )
+    public void closeGui()
     {
     }
 
     @Override
-    public void openBook( ItemStack stack, @Nonnull EnumHand hand )
+    public void onContainerSlotUpdate( Container container, int i, ItemStack itemStack )
     {
     }
 
     @Override
-    public void updateHeldItem()
+    public void method_14241()
     {
     }
 
     @Override
-    protected void onItemUseFinish()
+    public void addChatMessage( TextComponent textComponent, boolean bool )
     {
     }
 
-    /*
     @Override
-    public void mountEntityAndWakeUp()
+    protected void method_6040()
     {
     }
-    */
 
     @Override
-    public void dismountEntity( @Nonnull Entity entity )
+    public void lookAt( EntityAnchorArgumentType.EntityAnchor anchor, Vec3d pos )
     {
     }
+
+    // TODO: Finish this off.
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java
index 8e4ebce113..9d79a9e2f2 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleRefuelCommand.java
@@ -10,9 +10,9 @@
 import dan200.computercraft.api.turtle.ITurtleCommand;
 import dan200.computercraft.api.turtle.TurtleAnimation;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.api.turtle.event.TurtleRefuelEvent;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.common.MinecraftForge;
 
 import javax.annotation.Nonnull;
 
@@ -30,11 +30,11 @@ public TurtleRefuelCommand( int limit )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         int slot = turtle.getSelectedSlot();
-        ItemStack stack = turtle.getInventory().getStackInSlot( slot );
+        ItemStack stack = turtle.getInventory().getInvStack( slot );
         if( stack.isEmpty() ) return TurtleCommandResult.failure( "No items to combust" );
 
         TurtleRefuelEvent event = new TurtleRefuelEvent( turtle, stack );
-        if( MinecraftForge.EVENT_BUS.post( event ) ) return TurtleCommandResult.failure( event.getFailureMessage() );
+        if( TurtleEvent.post( event ) ) return TurtleCommandResult.failure( event.getFailureMessage() );
         if( event.getHandler() == null ) return TurtleCommandResult.failure( "Items not combustible" );
 
         if( limit != 0 )
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java
index 7fd9b598d1..9e1157690f 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleSuckCommand.java
@@ -10,17 +10,19 @@
 import dan200.computercraft.api.turtle.ITurtleCommand;
 import dan200.computercraft.api.turtle.TurtleAnimation;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.api.turtle.event.TurtleInventoryEvent;
 import dan200.computercraft.shared.util.InventoryUtil;
-import net.minecraft.entity.item.EntityItem;
+import dan200.computercraft.shared.util.ItemStorage;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EntitySelectors;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.predicate.entity.EntityPredicates;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.BoundingBox;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.items.IItemHandler;
 
 import javax.annotation.Nonnull;
 import java.util.List;
@@ -48,36 +50,37 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         // Get world direction from direction
-        EnumFacing direction = m_direction.toWorldDir( turtle );
+        Direction direction = m_direction.toWorldDir( turtle );
 
         // Get inventory for thing in front
         World world = turtle.getWorld();
-        BlockPos turtlePosition = turtle.getPosition();
-        BlockPos blockPosition = turtlePosition.offset( direction );
-        EnumFacing side = direction.getOpposite();
+        BlockPos oldPosition = turtle.getPosition();
+        BlockPos newPosition = oldPosition.offset( direction );
+        Direction side = direction.getOpposite();
 
-        IItemHandler inventory = InventoryUtil.getInventory( world, blockPosition, side );
+        Inventory inventory = InventoryUtil.getInventory( world, newPosition, side );
 
         // Fire the event, exiting if it is cancelled.
-        TurtlePlayer player = TurtlePlaceCommand.createPlayer( turtle, turtlePosition, direction );
-        TurtleInventoryEvent.Suck event = new TurtleInventoryEvent.Suck( turtle, player, world, blockPosition, inventory );
-        if( MinecraftForge.EVENT_BUS.post( event ) )
+        TurtlePlayer player = TurtlePlaceCommand.createPlayer( turtle, oldPosition, direction );
+        TurtleInventoryEvent.Suck event = new TurtleInventoryEvent.Suck( turtle, player, world, newPosition, inventory );
+        if( TurtleEvent.post( event ) )
         {
             return TurtleCommandResult.failure( event.getFailureMessage() );
         }
 
         if( inventory != null )
         {
+            ItemStorage storage = ItemStorage.wrap( inventory, side );
             // Take from inventory of thing in front
-            ItemStack stack = InventoryUtil.takeItems( m_quantity, inventory );
+            ItemStack stack = InventoryUtil.takeItems( m_quantity, storage );
             if( stack.isEmpty() ) return TurtleCommandResult.failure( "No items to take" );
 
             // Try to place into the turtle
-            ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), turtle.getSelectedSlot() );
+            ItemStack remainder = InventoryUtil.storeItems( stack, ItemStorage.wrap( turtle.getInventory() ), turtle.getSelectedSlot() );
             if( !remainder.isEmpty() )
             {
                 // Put the remainder back in the inventory
-                InventoryUtil.storeItems( remainder, inventory );
+                InventoryUtil.storeItems( remainder, storage );
             }
 
             // Return true if we consumed anything
@@ -94,62 +97,76 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         else
         {
             // Suck up loose items off the ground
-            AxisAlignedBB aabb = new AxisAlignedBB(
-                blockPosition.getX(), blockPosition.getY(), blockPosition.getZ(),
-                blockPosition.getX() + 1.0, blockPosition.getY() + 1.0, blockPosition.getZ() + 1.0
+            BoundingBox aabb = new BoundingBox(
+                newPosition.getX(), newPosition.getY(), newPosition.getZ(),
+                newPosition.getX() + 1.0, newPosition.getY() + 1.0, newPosition.getZ() + 1.0
             );
-            List<EntityItem> list = world.getEntitiesWithinAABB( EntityItem.class, aabb, EntitySelectors.IS_ALIVE );
+            List<Entity> list = world.getEntities( (Entity) null, aabb, EntityPredicates.VALID_ENTITY );
             if( list.isEmpty() ) return TurtleCommandResult.failure( "No items to take" );
 
-            for( EntityItem entity : list )
+            boolean foundItems = false;
+            boolean storedItems = false;
+            for( Entity entity : list )
             {
-                // Suck up the item
-                ItemStack stack = entity.getItem().copy();
-
-                ItemStack storeStack;
-                ItemStack leaveStack;
-                if( stack.getCount() > m_quantity )
-                {
-                    storeStack = stack.split( m_quantity );
-                    leaveStack = stack;
-                }
-                else
+                if( entity instanceof ItemEntity && entity.isAlive() )
                 {
-                    storeStack = stack;
-                    leaveStack = ItemStack.EMPTY;
-                }
-
-                ItemStack remainder = InventoryUtil.storeItems( storeStack, turtle.getItemHandler(), turtle.getSelectedSlot() );
-
-                if( remainder != storeStack )
-                {
-                    if( remainder.isEmpty() && leaveStack.isEmpty() )
+                    // Suck up the item
+                    foundItems = true;
+                    ItemEntity entityItem = (ItemEntity) entity;
+                    ItemStack stack = entityItem.getStack().copy();
+                    ItemStack storeStack;
+                    ItemStack leaveStack;
+                    if( stack.getAmount() > m_quantity )
                     {
-                        entity.remove();
+                        storeStack = stack.split( m_quantity );
+                        leaveStack = stack;
                     }
-                    else if( remainder.isEmpty() )
+                    else
                     {
-                        entity.setItem( leaveStack );
+                        storeStack = stack;
+                        leaveStack = ItemStack.EMPTY;
                     }
-                    else if( leaveStack.isEmpty() )
+                    ItemStack remainder = InventoryUtil.storeItems( storeStack, ItemStorage.wrap( turtle.getInventory() ), turtle.getSelectedSlot() );
+                    if( remainder != storeStack )
                     {
-                        entity.setItem( remainder );
-                    }
-                    else
-                    {
-                        leaveStack.grow( remainder.getCount() );
-                        entity.setItem( leaveStack );
+                        storedItems = true;
+                        if( remainder.isEmpty() && leaveStack.isEmpty() )
+                        {
+                            entityItem.remove();
+                        }
+                        else if( remainder.isEmpty() )
+                        {
+                            entityItem.setStack( leaveStack );
+                        }
+                        else if( leaveStack.isEmpty() )
+                        {
+                            entityItem.setStack( remainder );
+                        }
+                        else
+                        {
+                            leaveStack.addAmount( remainder.getAmount() );
+                            entityItem.setStack( leaveStack );
+                        }
+                        break;
                     }
+                }
+            }
 
+            if( foundItems )
+            {
+                if( storedItems )
+                {
                     // Play fx
-                    world.playBroadcastSound( 1000, turtlePosition, 0 ); // BLOCK_DISPENSER_DISPENSE
+                    world.playGlobalEvent( 1000, oldPosition, 0 ); // BLOCK_DISPENSER_DISPENSE
                     turtle.playAnimation( TurtleAnimation.Wait );
                     return TurtleCommandResult.success();
                 }
+                else
+                {
+                    return TurtleCommandResult.failure( "No space for items" );
+                }
             }
-
-
-            return TurtleCommandResult.failure( "No space for items" );
+            return TurtleCommandResult.failure( "No items to take" );
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java
index 574a0e05b3..6749f9ab40 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTransferToCommand.java
@@ -11,6 +11,7 @@
 import dan200.computercraft.api.turtle.TurtleAnimation;
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nonnull;
@@ -31,7 +32,8 @@ public TurtleTransferToCommand( int slot, int limit )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         // Take stack
-        ItemStack stack = InventoryUtil.takeItems( m_quantity, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
+        ItemStorage storage = ItemStorage.wrap( turtle.getInventory() );
+        ItemStack stack = InventoryUtil.takeItems( m_quantity, storage, turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
         if( stack.isEmpty() )
         {
             turtle.playAnimation( TurtleAnimation.Wait );
@@ -39,11 +41,11 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         }
 
         // Store stack
-        ItemStack remainder = InventoryUtil.storeItems( stack, turtle.getItemHandler(), m_slot, 1, m_slot );
+        ItemStack remainder = InventoryUtil.storeItems( stack, storage, m_slot, 1, m_slot );
         if( !remainder.isEmpty() )
         {
             // Put the remainder back
-            InventoryUtil.storeItems( remainder, turtle.getItemHandler(), turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
+            InventoryUtil.storeItems( remainder, storage, turtle.getSelectedSlot(), 1, turtle.getSelectedSlot() );
         }
 
         // Return true if we moved anything
diff --git a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java
index f129efc4b5..b7286d581e 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/core/TurtleTurnCommand.java
@@ -12,7 +12,7 @@
 import dan200.computercraft.api.turtle.TurtleCommandResult;
 import dan200.computercraft.api.turtle.event.TurtleAction;
 import dan200.computercraft.api.turtle.event.TurtleActionEvent;
-import net.minecraftforge.common.MinecraftForge;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 
 import javax.annotation.Nonnull;
 
@@ -30,7 +30,7 @@ public TurtleTurnCommand( TurnDirection direction )
     public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
     {
         TurtleActionEvent event = new TurtleActionEvent( turtle, TurtleAction.TURN );
-        if( MinecraftForge.EVENT_BUS.post( event ) )
+        if( TurtleEvent.post( event ) )
         {
             return TurtleCommandResult.failure( event.getFailureMessage() );
         }
@@ -39,13 +39,13 @@ public TurtleCommandResult execute( @Nonnull ITurtleAccess turtle )
         {
             case Left:
             {
-                turtle.setDirection( turtle.getDirection().rotateYCCW() );
+                turtle.setDirection( turtle.getDirection().rotateYCounterclockwise() );
                 turtle.playAnimation( TurtleAnimation.TurnLeft );
                 return TurtleCommandResult.success();
             }
             case Right:
             {
-                turtle.setDirection( turtle.getDirection().rotateY() );
+                turtle.setDirection( turtle.getDirection().rotateYClockwise() );
                 turtle.playAnimation( TurtleAnimation.TurnRight );
                 return TurtleCommandResult.success();
             }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java
index 66a4fefc64..ff606511b4 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/inventory/ContainerTurtle.java
@@ -10,13 +10,13 @@
 import dan200.computercraft.shared.computer.core.IComputer;
 import dan200.computercraft.shared.computer.core.IContainerComputer;
 import dan200.computercraft.shared.computer.core.InputState;
-import dan200.computercraft.shared.turtle.blocks.TileTurtle;
-import dan200.computercraft.shared.turtle.core.TurtleBrain;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.inventory.IContainerListener;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.Slot;
+import dan200.computercraft.shared.util.DefaultPropertyDelegate;
+import net.minecraft.container.Container;
+import net.minecraft.container.PropertyDelegate;
+import net.minecraft.container.Slot;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nonnull;
@@ -24,30 +24,34 @@
 
 public class ContainerTurtle extends Container implements IContainerComputer
 {
-    private static final int PROGRESS_ID_SELECTED_SLOT = 0;
+    private static final int PROPERTY_SLOT = 0;
 
     public final int m_playerInvStartY;
     public final int m_turtleInvStartX;
 
-    private final ITurtleAccess m_turtle;
-    private IComputer m_computer;
+    private final Inventory inventory;
+    private final PropertyDelegate properties;
+    private IComputer computer;
     private final InputState input = new InputState( this );
     private int m_selectedSlot;
 
-    protected ContainerTurtle( IInventory playerInventory, ITurtleAccess turtle, int playerInvStartY, int turtleInvStartX )
+    protected ContainerTurtle( int id, PlayerInventory playerInventory, Inventory inventory, PropertyDelegate properties, int playerInvStartY, int turtleInvStartX )
     {
+        super( null, id );
+
         m_playerInvStartY = playerInvStartY;
         m_turtleInvStartX = turtleInvStartX;
 
-        m_turtle = turtle;
-        m_selectedSlot = m_turtle.getWorld().isRemote ? 0 : m_turtle.getSelectedSlot();
+        this.inventory = inventory;
+        this.properties = properties;
+        addProperties( properties );
 
         // Turtle inventory
         for( int y = 0; y < 4; y++ )
         {
             for( int x = 0; x < 4; x++ )
             {
-                addSlot( new Slot( m_turtle.getInventory(), x + y * 4, turtleInvStartX + 1 + x * 18, playerInvStartY + 1 + y * 18 ) );
+                addSlot( new Slot( inventory, x + y * 4, turtleInvStartX + 1 + x * 18, playerInvStartY + 1 + y * 18 ) );
             }
         }
 
@@ -67,96 +71,63 @@ protected ContainerTurtle( IInventory playerInventory, ITurtleAccess turtle, int
         }
     }
 
-    public ContainerTurtle( IInventory playerInventory, ITurtleAccess turtle )
-    {
-        this( playerInventory, turtle, 134, 175 );
-    }
-
-    public ContainerTurtle( IInventory playerInventory, ITurtleAccess turtle, IComputer computer )
-    {
-        this( playerInventory, turtle );
-        m_computer = computer;
-    }
-
-    public int getSelectedSlot()
-    {
-        return m_selectedSlot;
-    }
-
-    private void sendStateToPlayer( IContainerListener listener )
-    {
-        int selectedSlot = m_turtle.getSelectedSlot();
-        listener.sendWindowProperty( this, PROGRESS_ID_SELECTED_SLOT, selectedSlot );
-    }
-
-    @Override
-    public void addListener( IContainerListener listener )
+    public ContainerTurtle( int id, PlayerInventory playerInventory, ITurtleAccess turtle, IComputer computer )
     {
-        super.addListener( listener );
-        sendStateToPlayer( listener );
-    }
-
-    @Override
-    public void detectAndSendChanges()
-    {
-        super.detectAndSendChanges();
-
-        int selectedSlot = m_turtle.getSelectedSlot();
-        for( IContainerListener listener : listeners )
+        this( id, playerInventory, turtle.getInventory(), new DefaultPropertyDelegate()
         {
-            if( m_selectedSlot != selectedSlot )
+            @Override
+            public int get( int id )
             {
-                listener.sendWindowProperty( this, PROGRESS_ID_SELECTED_SLOT, selectedSlot );
+                if( id == PROPERTY_SLOT ) return turtle.getSelectedSlot();
+                return 0;
             }
-        }
-        m_selectedSlot = selectedSlot;
+
+            @Override
+            public int size()
+            {
+                return 1;
+            }
+        }, 134, 175 );
+        this.computer = computer;
     }
 
-    @Override
-    public void updateProgressBar( int id, int value )
+    public int getSelectedSlot()
     {
-        super.updateProgressBar( id, value );
-        switch( id )
-        {
-            case PROGRESS_ID_SELECTED_SLOT:
-                m_selectedSlot = value;
-                break;
-        }
+        return properties.get( PROPERTY_SLOT );
     }
 
     @Override
-    public boolean canInteractWith( @Nonnull EntityPlayer player )
+    public boolean canUse( @Nonnull PlayerEntity player )
     {
-        TileTurtle turtle = ((TurtleBrain) m_turtle).getOwner();
-        return turtle != null && turtle.isUsableByPlayer( player );
+        return inventory.canPlayerUseInv( player );
     }
 
     @Nonnull
-    private ItemStack tryItemMerge( EntityPlayer player, int slotNum, int firstSlot, int lastSlot, boolean reverse )
+    private ItemStack tryItemMerge( PlayerEntity player, int slotNum, int firstSlot, int lastSlot, boolean reverse )
     {
-        Slot slot = inventorySlots.get( slotNum );
+        Slot slot = slotList.get( slotNum );
         ItemStack originalStack = ItemStack.EMPTY;
-        if( slot != null && slot.getHasStack() )
+        if( slot != null && slot.hasStack() )
         {
             ItemStack clickedStack = slot.getStack();
             originalStack = clickedStack.copy();
-            if( !mergeItemStack( clickedStack, firstSlot, lastSlot, reverse ) )
+            if( !insertItem( clickedStack, firstSlot, lastSlot, reverse ) )
             {
                 return ItemStack.EMPTY;
             }
 
             if( clickedStack.isEmpty() )
             {
-                slot.putStack( ItemStack.EMPTY );
+                slot.setStack( ItemStack.EMPTY );
             }
             else
             {
-                slot.onSlotChanged();
+                slot.markDirty();
             }
 
-            if( clickedStack.getCount() != originalStack.getCount() )
+            if( clickedStack.getAmount() != originalStack.getAmount() )
             {
-                slot.onTake( player, clickedStack );
+                slot.onTakeItem( player, clickedStack );
             }
             else
             {
@@ -168,7 +139,7 @@ private ItemStack tryItemMerge( EntityPlayer player, int slotNum, int firstSlot,
 
     @Nonnull
     @Override
-    public ItemStack transferStackInSlot( EntityPlayer player, int slotNum )
+    public ItemStack transferSlot( PlayerEntity player, int slotNum )
     {
         if( slotNum >= 0 && slotNum < 16 )
         {
@@ -185,7 +156,7 @@ else if( slotNum >= 16 )
     @Override
     public IComputer getComputer()
     {
-        return m_computer;
+        return computer;
     }
 
     @Nonnull
@@ -196,9 +167,9 @@ public InputState getInput()
     }
 
     @Override
-    public void onContainerClosed( EntityPlayer player )
+    public void close( PlayerEntity player )
     {
-        super.onContainerClosed( player );
+        super.close( player );
         input.close();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java b/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java
index 3fa25bda98..ec63edb889 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/items/ITurtleItem.java
@@ -11,7 +11,7 @@
 import dan200.computercraft.shared.common.IColouredItem;
 import dan200.computercraft.shared.computer.items.IComputerItem;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -24,5 +24,5 @@ public interface ITurtleItem extends IComputerItem, IColouredItem
     int getFuelLevel( @Nonnull ItemStack stack );
 
     @Nullable
-    ResourceLocation getOverlay( @Nonnull ItemStack stack );
+    Identifier getOverlay( @Nonnull ItemStack stack );
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java b/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java
index 33519d2d69..df517b8b05 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/items/ItemTurtle.java
@@ -6,7 +6,6 @@
 
 package dan200.computercraft.shared.turtle.items;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.shared.TurtleUpgrades;
@@ -16,30 +15,29 @@
 import dan200.computercraft.shared.turtle.blocks.BlockTurtle;
 import net.minecraft.item.ItemGroup;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
-import net.minecraft.util.text.TextComponentTranslation;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.text.TextComponent;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
 import static dan200.computercraft.shared.turtle.core.TurtleBrain.*;
 
 public class ItemTurtle extends ItemComputerBase implements ITurtleItem
 {
-    public ItemTurtle( BlockTurtle block, Properties settings )
+    public ItemTurtle( BlockTurtle block, Settings settings )
     {
         super( block, settings );
     }
 
-    public ItemStack create( int id, String label, int colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, int fuelLevel, ResourceLocation overlay )
+    public ItemStack create( int id, String label, int colour, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, int fuelLevel, Identifier overlay )
     {
         // Build the stack
         ItemStack stack = new ItemStack( this );
-        if( label != null ) stack.setDisplayName( new TextComponentString( label ) );
+        if( label != null ) stack.setDisplayName( new StringTextComponent( label ) );
         if( id >= 0 ) stack.getOrCreateTag().putInt( NBT_ID, id );
         IColouredItem.setColourBasic( stack, colour );
         if( fuelLevel > 0 ) stack.getOrCreateTag().putInt( NBT_FUEL, fuelLevel );
@@ -59,9 +57,9 @@ public ItemStack create( int id, String label, int colour, ITurtleUpgrade leftUp
     }
 
     @Override
-    public void fillItemGroup( @Nonnull ItemGroup group, @Nonnull NonNullList<ItemStack> list )
+    public void appendItemsForGroup( @Nonnull ItemGroup group, @Nonnull DefaultedList<ItemStack> list )
     {
-        if( !isInGroup( group ) ) return;
+        if( !isInItemGroup( group ) ) return;
 
         ComputerFamily family = getFamily();
 
@@ -76,60 +74,36 @@ public void fillItemGroup( @Nonnull ItemGroup group, @Nonnull NonNullList<ItemSt
 
     @Nonnull
     @Override
-    public ITextComponent getDisplayName( @Nonnull ItemStack stack )
+    public TextComponent getTranslatedNameTrimmed( @Nonnull ItemStack stack )
     {
         String baseString = getTranslationKey( stack );
         ITurtleUpgrade left = getUpgrade( stack, TurtleSide.Left );
         ITurtleUpgrade right = getUpgrade( stack, TurtleSide.Right );
         if( left != null && right != null )
         {
-            return new TextComponentTranslation( baseString + ".upgraded_twice",
-                new TextComponentTranslation( right.getUnlocalisedAdjective() ),
-                new TextComponentTranslation( left.getUnlocalisedAdjective() )
+            return new TranslatableTextComponent( baseString + ".upgraded_twice",
+                new TranslatableTextComponent( right.getUnlocalisedAdjective() ),
+                new TranslatableTextComponent( left.getUnlocalisedAdjective() )
             );
         }
         else if( left != null )
         {
-            return new TextComponentTranslation( baseString + ".upgraded",
-                new TextComponentTranslation( left.getUnlocalisedAdjective() )
+            return new TranslatableTextComponent( baseString + ".upgraded",
+                new TranslatableTextComponent( left.getUnlocalisedAdjective() )
             );
         }
         else if( right != null )
         {
-            return new TextComponentTranslation( baseString + ".upgraded",
-                new TextComponentTranslation( right.getUnlocalisedAdjective() )
+            return new TranslatableTextComponent( baseString + ".upgraded",
+                new TranslatableTextComponent( right.getUnlocalisedAdjective() )
             );
         }
         else
         {
-            return new TextComponentTranslation( baseString );
+            return new TranslatableTextComponent( baseString );
         }
     }
 
-    @Nullable
-    @Override
-    public String getCreatorModId( ItemStack stack )
-    {
-        // Determine our "creator mod" from the upgrades. We attempt to find the first non-vanilla/non-CC
-        // upgrade (starting from the left).
-
-        ITurtleUpgrade left = getUpgrade( stack, TurtleSide.Left );
-        if( left != null )
-        {
-            String mod = TurtleUpgrades.getOwner( left );
-            if( mod != null && !mod.equals( ComputerCraft.MOD_ID ) ) return mod;
-        }
-
-        ITurtleUpgrade right = getUpgrade( stack, TurtleSide.Right );
-        if( right != null )
-        {
-            String mod = TurtleUpgrades.getOwner( right );
-            if( mod != null && !mod.equals( ComputerCraft.MOD_ID ) ) return mod;
-        }
-
-        return super.getCreatorModId( stack );
-    }
-
     @Override
     public ItemStack withFamily( @Nonnull ItemStack stack, @Nonnull ComputerFamily family )
     {
@@ -144,24 +118,24 @@ public ItemStack withFamily( @Nonnull ItemStack stack, @Nonnull ComputerFamily f
     @Override
     public ITurtleUpgrade getUpgrade( @Nonnull ItemStack stack, @Nonnull TurtleSide side )
     {
-        NBTTagCompound tag = stack.getTag();
+        CompoundTag tag = stack.getTag();
         if( tag == null ) return null;
 
         String key = side == TurtleSide.Left ? NBT_LEFT_UPGRADE : NBT_RIGHT_UPGRADE;
-        return tag.contains( key ) ? TurtleUpgrades.get( tag.getString( key ) ) : null;
+        return tag.containsKey( key ) ? TurtleUpgrades.get( tag.getString( key ) ) : null;
     }
 
     @Override
-    public ResourceLocation getOverlay( @Nonnull ItemStack stack )
+    public Identifier getOverlay( @Nonnull ItemStack stack )
     {
-        NBTTagCompound tag = stack.getTag();
-        return tag != null && tag.contains( NBT_OVERLAY ) ? new ResourceLocation( tag.getString( NBT_OVERLAY ) ) : null;
+        CompoundTag tag = stack.getTag();
+        return tag != null && tag.containsKey( NBT_OVERLAY ) ? new Identifier( tag.getString( NBT_OVERLAY ) ) : null;
     }
 
     @Override
     public int getFuelLevel( @Nonnull ItemStack stack )
     {
-        NBTTagCompound tag = stack.getTag();
-        return tag != null && tag.contains( NBT_FUEL ) ? tag.getInt( NBT_FUEL ) : 0;
+        CompoundTag tag = stack.getTag();
+        return tag != null && tag.containsKey( NBT_FUEL ) ? tag.getInt( NBT_FUEL ) : 0;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java b/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java
index 2ff09685f0..1c6ad90372 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/items/TurtleItemFactory.java
@@ -12,7 +12,7 @@
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.turtle.blocks.ITurtleTile;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 
@@ -38,7 +38,7 @@ public static ItemStack create( ITurtleTile turtle )
     }
 
     @Nonnull
-    public static ItemStack create( int id, String label, int colour, ComputerFamily family, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, int fuelLevel, ResourceLocation overlay )
+    public static ItemStack create( int id, String label, int colour, ComputerFamily family, ITurtleUpgrade leftUpgrade, ITurtleUpgrade rightUpgrade, int fuelLevel, Identifier overlay )
     {
         switch( family )
         {
diff --git a/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java
index ff48d31dab..e33e253cc1 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleRecipe.java
@@ -6,29 +6,27 @@
 
 package dan200.computercraft.shared.turtle.recipes;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.computer.items.IComputerItem;
 import dan200.computercraft.shared.computer.recipe.ComputerFamilyRecipe;
 import dan200.computercraft.shared.turtle.items.TurtleItemFactory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
 
 import javax.annotation.Nonnull;
 
 public final class TurtleRecipe extends ComputerFamilyRecipe
 {
-    private TurtleRecipe( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
+    private TurtleRecipe( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
     {
         super( identifier, group, width, height, ingredients, result, family );
     }
 
-    @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
@@ -43,20 +41,12 @@ protected ItemStack convert( @Nonnull IComputerItem item, @Nonnull ItemStack sta
         return TurtleItemFactory.create( computerID, label, -1, getFamily(), null, null, 0, null );
     }
 
-    private static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "turtle" );
-    public static final IRecipeSerializer<TurtleRecipe> SERIALIZER = new Serializer<TurtleRecipe>()
+    public static final RecipeSerializer<TurtleRecipe> SERIALIZER = new Serializer<TurtleRecipe>()
     {
         @Override
-        protected TurtleRecipe create( ResourceLocation identifier, String group, int width, int height, NonNullList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
+        protected TurtleRecipe create( Identifier identifier, String group, int width, int height, DefaultedList<Ingredient> ingredients, ItemStack result, ComputerFamily family )
         {
             return new TurtleRecipe( identifier, group, width, height, ingredients, result, family );
         }
-
-        @Nonnull
-        @Override
-        public ResourceLocation getName()
-        {
-            return ID;
-        }
     };
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java
index c896666f6e..1dc69fa60d 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/recipes/TurtleUpgradeRecipe.java
@@ -6,52 +6,51 @@
 
 package dan200.computercraft.shared.turtle.recipes;
 
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.api.turtle.ITurtleUpgrade;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.shared.TurtleUpgrades;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
 import dan200.computercraft.shared.turtle.items.ITurtleItem;
 import dan200.computercraft.shared.turtle.items.TurtleItemFactory;
-import dan200.computercraft.shared.util.AbstractRecipe;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.SpecialRecipeSerializer;
+import net.minecraft.recipe.crafting.SpecialCraftingRecipe;
+import net.minecraft.util.Identifier;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
-public final class TurtleUpgradeRecipe extends AbstractRecipe
+public final class TurtleUpgradeRecipe extends SpecialCraftingRecipe
 {
-    private TurtleUpgradeRecipe( ResourceLocation id )
+    private TurtleUpgradeRecipe( Identifier id )
     {
         super( id );
     }
 
     @Override
-    public boolean canFit( int x, int y )
+    public boolean fits( int x, int y )
     {
         return x >= 3 && y >= 1;
     }
 
     @Nonnull
     @Override
-    public ItemStack getRecipeOutput()
+    public ItemStack getOutput()
     {
         return TurtleItemFactory.create( -1, null, -1, ComputerFamily.Normal, null, null, 0, null );
     }
 
     @Override
-    public boolean matches( @Nonnull IInventory inventory, @Nonnull World world )
+    public boolean matches( @Nonnull CraftingInventory inventory, @Nonnull World world )
     {
-        return !getCraftingResult( inventory ).isEmpty();
+        return !craft( inventory ).isEmpty();
     }
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inventory )
+    public ItemStack craft( @Nonnull CraftingInventory inventory )
     {
         // Scan the grid for a row containing a turtle and 1 or 2 items
         ItemStack leftItem = ItemStack.EMPTY;
@@ -66,7 +65,7 @@ public ItemStack getCraftingResult( @Nonnull IInventory inventory )
                 boolean finishedRow = false;
                 for( int x = 0; x < inventory.getWidth(); x++ )
                 {
-                    ItemStack item = inventory.getStackInSlot( x + y * inventory.getWidth() );
+                    ItemStack item = inventory.getInvStack( x + y * inventory.getWidth() );
                     if( !item.isEmpty() )
                     {
                         if( finishedRow )
@@ -124,7 +123,7 @@ else if( !turtle.isEmpty() && rightItem.isEmpty() )
                 // Turtle is already found, just check this row is empty
                 for( int x = 0; x < inventory.getWidth(); x++ )
                 {
-                    ItemStack item = inventory.getStackInSlot( x + y * inventory.getWidth() );
+                    ItemStack item = inventory.getInvStack( x + y * inventory.getWidth() );
                     if( !item.isEmpty() )
                     {
                         return ItemStack.EMPTY;
@@ -176,18 +175,16 @@ else if( !turtle.isEmpty() && rightItem.isEmpty() )
         String label = itemTurtle.getLabel( turtle );
         int fuelLevel = itemTurtle.getFuelLevel( turtle );
         int colour = itemTurtle.getColour( turtle );
-        ResourceLocation overlay = itemTurtle.getOverlay( turtle );
+        Identifier overlay = itemTurtle.getOverlay( turtle );
         return TurtleItemFactory.create( computerID, label, colour, family, upgrades[0], upgrades[1], fuelLevel, overlay );
     }
 
     @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    public static final IRecipeSerializer<TurtleUpgradeRecipe> SERIALIZER = new RecipeSerializers.SimpleSerializer<>(
-        ComputerCraft.MOD_ID + ":turtle_upgrade", TurtleUpgradeRecipe::new
-    );
+    public static final RecipeSerializer<TurtleUpgradeRecipe> SERIALIZER = new SpecialRecipeSerializer<>( TurtleUpgradeRecipe::new );
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java
index ff03200a64..67f8670b17 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleAxe.java
@@ -7,16 +7,16 @@
 package dan200.computercraft.shared.turtle.upgrades;
 
 import net.minecraft.item.Item;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 
 public class TurtleAxe extends TurtleTool
 {
-    public TurtleAxe( ResourceLocation id, String adjective, Item item )
+    public TurtleAxe( Identifier id, String adjective, Item item )
     {
         super( id, adjective, item );
     }
 
-    public TurtleAxe( ResourceLocation id, Item item )
+    public TurtleAxe( Identifier id, Item item )
     {
         super( id, item );
     }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java
index 884781afc0..5daa120bfe 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleCraftingTable.java
@@ -11,14 +11,14 @@
 import dan200.computercraft.api.turtle.ITurtleAccess;
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.api.turtle.TurtleUpgradeType;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ModelManager;
-import net.minecraft.client.renderer.model.ModelResourceLocation;
-import net.minecraft.init.Blocks;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -26,13 +26,13 @@
 
 public class TurtleCraftingTable extends AbstractTurtleUpgrade
 {
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_leftModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_leftModel;
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_rightModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_rightModel;
 
-    public TurtleCraftingTable( ResourceLocation id )
+    public TurtleCraftingTable( Identifier id )
     {
         super( id, TurtleUpgradeType.Peripheral, Blocks.CRAFTING_TABLE );
     }
@@ -43,25 +43,25 @@ public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull Tur
         return new CraftingTablePeripheral( turtle );
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     private void loadModelLocations()
     {
         if( m_leftModel == null )
         {
-            m_leftModel = new ModelResourceLocation( "computercraft:turtle_crafting_table_left", "inventory" );
-            m_rightModel = new ModelResourceLocation( "computercraft:turtle_crafting_table_right", "inventory" );
+            m_leftModel = new ModelIdentifier( "computercraft:turtle_crafting_table_left", "inventory" );
+            m_rightModel = new ModelIdentifier( "computercraft:turtle_crafting_table_right", "inventory" );
         }
     }
 
     @Nonnull
     @Override
-    @OnlyIn( Dist.CLIENT )
-    public Pair<IBakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
+    @Environment( EnvType.CLIENT )
+    public Pair<BakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
     {
         loadModelLocations();
 
         Matrix4f transform = null;
-        ModelManager modelManager = Minecraft.getInstance().getItemRenderer().getItemModelMesher().getModelManager();
+        BakedModelManager modelManager = MinecraftClient.getInstance().getItemRenderer().getModels().getModelManager();
         if( side == TurtleSide.Left )
         {
             return Pair.of( modelManager.getModel( m_leftModel ), transform );
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java
index 0a07975413..701bd74d13 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleHoe.java
@@ -12,46 +12,46 @@
 import dan200.computercraft.api.turtle.TurtleVerb;
 import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand;
 import dan200.computercraft.shared.turtle.core.TurtlePlayer;
-import net.minecraft.block.material.Material;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Material;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
 public class TurtleHoe extends TurtleTool
 {
-    public TurtleHoe( ResourceLocation id, String adjective, Item item )
+    public TurtleHoe( Identifier id, String adjective, Item item )
     {
         super( id, adjective, item );
     }
 
-    public TurtleHoe( ResourceLocation id, Item item )
+    public TurtleHoe( Identifier id, Item item )
     {
         super( id, item );
     }
 
     @Override
-    protected boolean canBreakBlock( IBlockState state, World world, BlockPos pos, TurtlePlayer player )
+    protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player )
     {
         if( !super.canBreakBlock( state, world, pos, player ) ) return false;
 
         Material material = state.getMaterial();
-        return material == Material.PLANTS ||
+        return material == Material.PLANT ||
             material == Material.CACTUS ||
-            material == Material.GOURD ||
+            material == Material.PUMPKIN ||
             material == Material.LEAVES ||
-            material == Material.OCEAN_PLANT ||
-            material == Material.VINE;
+            material == Material.UNDERWATER_PLANT;
+        // material == Material.VINE;
     }
 
     @Nonnull
     @Override
-    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull EnumFacing direction )
+    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction )
     {
         if( verb == TurtleVerb.Dig )
         {
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java
index 169dd98cc0..555833d4be 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleInventoryCrafting.java
@@ -9,18 +9,14 @@
 import dan200.computercraft.api.turtle.ITurtleAccess;
 import dan200.computercraft.shared.turtle.blocks.TileTurtle;
 import dan200.computercraft.shared.turtle.core.TurtlePlayer;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.InventoryCrafting;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipe;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.text.ITextComponent;
-import net.minecraft.util.text.TextComponentString;
+import net.minecraft.recipe.RecipeType;
+import net.minecraft.recipe.crafting.CraftingRecipe;
+import net.minecraft.server.world.ServerWorld;
+import net.minecraft.util.DefaultedList;
 import net.minecraft.world.World;
-import net.minecraft.world.WorldServer;
-import net.minecraftforge.common.ForgeHooks;
-import net.minecraftforge.common.crafting.VanillaRecipeTypes;
-import net.minecraftforge.fml.hooks.BasicEventHooks;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -28,7 +24,7 @@
 import java.util.Collections;
 import java.util.List;
 
-public class TurtleInventoryCrafting extends InventoryCrafting
+public class TurtleInventoryCrafting extends CraftingInventory
 {
     private ITurtleAccess m_turtle;
     private int m_xStart;
@@ -46,7 +42,7 @@ public TurtleInventoryCrafting( ITurtleAccess turtle )
     }
 
     @Nullable
-    private IRecipe tryCrafting( int xStart, int yStart )
+    private CraftingRecipe tryCrafting( int xStart, int yStart )
     {
         m_xStart = xStart;
         m_yStart = yStart;
@@ -59,7 +55,7 @@ private IRecipe tryCrafting( int xStart, int yStart )
                 if( x < m_xStart || x >= m_xStart + 3 ||
                     y < m_yStart || y >= m_yStart + 3 )
                 {
-                    if( !m_turtle.getInventory().getStackInSlot( x + y * TileTurtle.INVENTORY_WIDTH ).isEmpty() )
+                    if( !m_turtle.getInventory().getInvStack( x + y * TileTurtle.INVENTORY_WIDTH ).isEmpty() )
                     {
                         return null;
                     }
@@ -68,16 +64,16 @@ private IRecipe tryCrafting( int xStart, int yStart )
         }
 
         // Check the actual crafting
-        return m_turtle.getWorld().getRecipeManager().getRecipe( this, m_turtle.getWorld(), VanillaRecipeTypes.CRAFTING );
+        return m_turtle.getWorld().getRecipeManager().getFirstMatch( RecipeType.CRAFTING, this, m_turtle.getWorld() ).orElse( null );
     }
 
     @Nullable
     public List<ItemStack> doCrafting( World world, int maxCount )
     {
-        if( world.isRemote || !(world instanceof WorldServer) ) return null;
+        if( world.isClient || !(world instanceof ServerWorld) ) return null;
 
         // Find out what we can craft
-        IRecipe recipe = tryCrafting( 0, 0 );
+        CraftingRecipe recipe = tryCrafting( 0, 0 );
         if( recipe == null ) recipe = tryCrafting( 0, 1 );
         if( recipe == null ) recipe = tryCrafting( 1, 0 );
         if( recipe == null ) recipe = tryCrafting( 1, 1 );
@@ -91,26 +87,22 @@ public List<ItemStack> doCrafting( World world, int maxCount )
         ArrayList<ItemStack> results = new ArrayList<>();
         for( int i = 0; i < maxCount && recipe.matches( this, world ); i++ )
         {
-            ItemStack result = recipe.getCraftingResult( this );
+            ItemStack result = recipe.craft( this );
             if( result.isEmpty() ) break;
             results.add( result );
 
-            result.onCrafting( world, player, result.getCount() );
-            BasicEventHooks.firePlayerCraftingEvent( player, result, this );
-
-            ForgeHooks.setCraftingPlayer( player );
-            NonNullList<ItemStack> remainders = recipe.getRemainingItems( this );
-            ForgeHooks.setCraftingPlayer( null );
+            result.onCrafted( world, player, result.getAmount() );
+            DefaultedList<ItemStack> remainders = recipe.getRemainingStacks( this );
 
             for( int slot = 0; slot < remainders.size(); slot++ )
             {
-                ItemStack existing = getStackInSlot( slot );
+                ItemStack existing = getInvStack( slot );
                 ItemStack remainder = remainders.get( slot );
 
                 if( !existing.isEmpty() )
                 {
-                    decrStackSize( slot, 1 );
-                    existing = getStackInSlot( slot );
+                    takeInvStack( slot, 1 );
+                    existing = getInvStack( slot );
                 }
 
                 if( remainder.isEmpty() ) continue;
@@ -119,12 +111,12 @@ public List<ItemStack> doCrafting( World world, int maxCount )
                 // afterwards).
                 if( existing.isEmpty() )
                 {
-                    setInventorySlotContents( slot, remainder );
+                    setInvStack( slot, remainder );
                 }
-                else if( ItemStack.areItemsEqual( existing, remainder ) && ItemStack.areItemStackTagsEqual( existing, remainder ) )
+                else if( existing.getItem() == remainder.getItem() && ItemStack.areTagsEqual( existing, remainder ) )
                 {
-                    remainder.grow( existing.getCount() );
-                    setInventorySlotContents( slot, remainder );
+                    remainder.addAmount( existing.getAmount() );
+                    setInvStack( slot, remainder );
                 }
                 else
                 {
@@ -160,59 +152,46 @@ private int modifyIndex( int index )
     // IInventory implementation
 
     @Override
-    public int getSizeInventory()
+    public int getInvSize()
     {
         return getWidth() * getHeight();
     }
 
     @Nonnull
     @Override
-    public ItemStack getStackInSlot( int i )
+    public ItemStack getInvStack( int i )
     {
         i = modifyIndex( i );
-        return m_turtle.getInventory().getStackInSlot( i );
-    }
-
-    @Nonnull
-    @Override
-    public ITextComponent getName()
-    {
-        return new TextComponentString( "" );
-    }
-
-    @Override
-    public boolean hasCustomName()
-    {
-        return false;
+        return m_turtle.getInventory().getInvStack( i );
     }
 
     @Nonnull
     @Override
-    public ItemStack removeStackFromSlot( int i )
+    public ItemStack removeInvStack( int i )
     {
         i = modifyIndex( i );
-        return m_turtle.getInventory().removeStackFromSlot( i );
+        return m_turtle.getInventory().removeInvStack( i );
     }
 
     @Nonnull
     @Override
-    public ItemStack decrStackSize( int i, int size )
+    public ItemStack takeInvStack( int i, int size )
     {
         i = modifyIndex( i );
-        return m_turtle.getInventory().decrStackSize( i, size );
+        return m_turtle.getInventory().takeInvStack( i, size );
     }
 
     @Override
-    public void setInventorySlotContents( int i, @Nonnull ItemStack stack )
+    public void setInvStack( int i, @Nonnull ItemStack stack )
     {
         i = modifyIndex( i );
-        m_turtle.getInventory().setInventorySlotContents( i, stack );
+        m_turtle.getInventory().setInvStack( i, stack );
     }
 
     @Override
-    public int getInventoryStackLimit()
+    public int getInvMaxStackAmount()
     {
-        return m_turtle.getInventory().getInventoryStackLimit();
+        return m_turtle.getInventory().getInvMaxStackAmount();
     }
 
     @Override
@@ -222,25 +201,25 @@ public void markDirty()
     }
 
     @Override
-    public boolean isUsableByPlayer( EntityPlayer player )
+    public boolean canPlayerUseInv( PlayerEntity player )
     {
         return true;
     }
 
     @Override
-    public boolean isItemValidForSlot( int i, @Nonnull ItemStack stack )
+    public boolean isValidInvStack( int i, @Nonnull ItemStack stack )
     {
         i = modifyIndex( i );
-        return m_turtle.getInventory().isItemValidForSlot( i, stack );
+        return m_turtle.getInventory().isValidInvStack( i, stack );
     }
 
     @Override
     public void clear()
     {
-        for( int i = 0; i < getSizeInventory(); i++ )
+        for( int i = 0; i < getInvSize(); i++ )
         {
             int j = modifyIndex( i );
-            m_turtle.getInventory().setInventorySlotContents( j, ItemStack.EMPTY );
+            m_turtle.getInventory().setInvStack( j, ItemStack.EMPTY );
         }
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java
index 4fa8e9a627..41e6fd5b86 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleModem.java
@@ -12,18 +12,18 @@
 import dan200.computercraft.api.turtle.*;
 import dan200.computercraft.shared.peripheral.modem.ModemState;
 import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ModelManager;
-import net.minecraft.client.renderer.model.ModelResourceLocation;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -69,19 +69,19 @@ public boolean equals( IPeripheral other )
 
     private boolean advanced;
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_leftOffModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_leftOffModel;
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_rightOffModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_rightOffModel;
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_leftOnModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_leftOnModel;
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_rightOnModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_rightOnModel;
 
-    public TurtleModem( boolean advanced, ResourceLocation id )
+    public TurtleModem( boolean advanced, Identifier id )
     {
         super(
             id, TurtleUpgradeType.Peripheral,
@@ -100,52 +100,52 @@ public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull Tur
 
     @Nonnull
     @Override
-    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull EnumFacing dir )
+    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction dir )
     {
         return TurtleCommandResult.failure();
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     private void loadModelLocations()
     {
         if( m_leftOffModel == null )
         {
             if( advanced )
             {
-                m_leftOffModel = new ModelResourceLocation( "computercraft:turtle_modem_advanced_off_left", "inventory" );
-                m_rightOffModel = new ModelResourceLocation( "computercraft:turtle_modem_advanced_off_right", "inventory" );
-                m_leftOnModel = new ModelResourceLocation( "computercraft:turtle_modem_advanced_on_left", "inventory" );
-                m_rightOnModel = new ModelResourceLocation( "computercraft:turtle_modem_advanced_on_right", "inventory" );
+                m_leftOffModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_off_left", "inventory" );
+                m_rightOffModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_off_right", "inventory" );
+                m_leftOnModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_on_left", "inventory" );
+                m_rightOnModel = new ModelIdentifier( "computercraft:turtle_modem_advanced_on_right", "inventory" );
             }
             else
             {
-                m_leftOffModel = new ModelResourceLocation( "computercraft:turtle_modem_normal_off_left", "inventory" );
-                m_rightOffModel = new ModelResourceLocation( "computercraft:turtle_modem_normal_off_right", "inventory" );
-                m_leftOnModel = new ModelResourceLocation( "computercraft:turtle_modem_normal_on_left", "inventory" );
-                m_rightOnModel = new ModelResourceLocation( "computercraft:turtle_modem_normal_on_right", "inventory" );
+                m_leftOffModel = new ModelIdentifier( "computercraft:turtle_modem_normal_off_left", "inventory" );
+                m_rightOffModel = new ModelIdentifier( "computercraft:turtle_modem_normal_off_right", "inventory" );
+                m_leftOnModel = new ModelIdentifier( "computercraft:turtle_modem_normal_on_left", "inventory" );
+                m_rightOnModel = new ModelIdentifier( "computercraft:turtle_modem_normal_on_right", "inventory" );
             }
         }
     }
 
     @Nonnull
     @Override
-    @OnlyIn( Dist.CLIENT )
-    public Pair<IBakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
+    @Environment( EnvType.CLIENT )
+    public Pair<BakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
     {
         loadModelLocations();
 
         boolean active = false;
         if( turtle != null )
         {
-            NBTTagCompound turtleNBT = turtle.getUpgradeNBTData( side );
-            if( turtleNBT.contains( "active" ) )
+            CompoundTag turtleNBT = turtle.getUpgradeNBTData( side );
+            if( turtleNBT.containsKey( "active" ) )
             {
                 active = turtleNBT.getBoolean( "active" );
             }
         }
 
         Matrix4f transform = null;
-        ModelManager modelManager = Minecraft.getInstance().getItemRenderer().getItemModelMesher().getModelManager();
+        BakedModelManager modelManager = MinecraftClient.getInstance().getItemRenderer().getModels().getModelManager();
         if( side == TurtleSide.Left )
         {
             return Pair.of(
@@ -166,7 +166,7 @@ public Pair<IBakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull Turt
     public void update( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side )
     {
         // Advance the modem
-        if( !turtle.getWorld().isRemote )
+        if( !turtle.getWorld().isClient )
         {
             IPeripheral peripheral = turtle.getPeripheral( side );
             if( peripheral instanceof Peripheral )
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java
index cb5f66cd67..f7fc0972fc 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleShovel.java
@@ -12,51 +12,51 @@
 import dan200.computercraft.api.turtle.TurtleVerb;
 import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand;
 import dan200.computercraft.shared.turtle.core.TurtlePlayer;
-import net.minecraft.block.material.Material;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Material;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
 
 public class TurtleShovel extends TurtleTool
 {
-    public TurtleShovel( ResourceLocation id, String adjective, Item item )
+    public TurtleShovel( Identifier id, String adjective, Item item )
     {
         super( id, adjective, item );
     }
 
-    public TurtleShovel( ResourceLocation id, Item item )
+    public TurtleShovel( Identifier id, Item item )
     {
         super( id, item );
     }
 
     @Override
-    protected boolean canBreakBlock( IBlockState state, World world, BlockPos pos, TurtlePlayer player )
+    protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player )
     {
         if( !super.canBreakBlock( state, world, pos, player ) ) return false;
 
         Material material = state.getMaterial();
-        return material == Material.GROUND ||
+        return material == Material.EARTH ||
             material == Material.SAND ||
             material == Material.SNOW ||
             material == Material.CLAY ||
-            material == Material.CRAFTED_SNOW ||
-            material == Material.GRASS ||
-            material == Material.PLANTS ||
+            material == Material.SNOW_BLOCK ||
+            material == Material.REPLACEABLE_PLANT ||
+            material == Material.PLANT ||
             material == Material.CACTUS ||
-            material == Material.GOURD ||
-            material == Material.LEAVES ||
-            material == Material.VINE;
+            material == Material.PUMPKIN ||
+            material == Material.LEAVES;
+        // material == Material.VINE;
     }
 
     @Nonnull
     @Override
-    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull EnumFacing direction )
+    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction )
     {
         if( verb == TurtleVerb.Dig )
         {
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java
index c82a0663d6..cd29979c68 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSpeaker.java
@@ -14,16 +14,16 @@
 import dan200.computercraft.api.turtle.TurtleSide;
 import dan200.computercraft.api.turtle.TurtleUpgradeType;
 import dan200.computercraft.shared.peripheral.speaker.SpeakerPeripheral;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.model.IBakedModel;
-import net.minecraft.client.renderer.model.ModelManager;
-import net.minecraft.client.renderer.model.ModelResourceLocation;
-import net.minecraft.util.ResourceLocation;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -60,13 +60,13 @@ public boolean equals( IPeripheral other )
         }
     }
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_leftModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_leftModel;
 
-    @OnlyIn( Dist.CLIENT )
-    private ModelResourceLocation m_rightModel;
+    @Environment( EnvType.CLIENT )
+    private ModelIdentifier m_rightModel;
 
-    public TurtleSpeaker( ResourceLocation id )
+    public TurtleSpeaker( Identifier id )
     {
         super( id, TurtleUpgradeType.Peripheral, ComputerCraft.Blocks.speaker );
     }
@@ -77,23 +77,23 @@ public IPeripheral createPeripheral( @Nonnull ITurtleAccess turtle, @Nonnull Tur
         return new TurtleSpeaker.Peripheral( turtle );
     }
 
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     private void loadModelLocations()
     {
         if( m_leftModel == null )
         {
-            m_leftModel = new ModelResourceLocation( "computercraft:turtle_speaker_upgrade_left", "inventory" );
-            m_rightModel = new ModelResourceLocation( "computercraft:turtle_speaker_upgrade_right", "inventory" );
+            m_leftModel = new ModelIdentifier( "computercraft:turtle_speaker_upgrade_left", "inventory" );
+            m_rightModel = new ModelIdentifier( "computercraft:turtle_speaker_upgrade_right", "inventory" );
         }
     }
 
     @Nonnull
     @Override
-    @OnlyIn( Dist.CLIENT )
-    public Pair<IBakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
+    @Environment( EnvType.CLIENT )
+    public Pair<BakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
     {
         loadModelLocations();
-        ModelManager modelManager = Minecraft.getInstance().getItemRenderer().getItemModelMesher().getModelManager();
+        BakedModelManager modelManager = MinecraftClient.getInstance().getItemRenderer().getModels().getModelManager();
 
         if( side == TurtleSide.Left )
         {
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java
index 31b4450811..afe221d66b 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleSword.java
@@ -7,36 +7,36 @@
 package dan200.computercraft.shared.turtle.upgrades;
 
 import dan200.computercraft.shared.turtle.core.TurtlePlayer;
-import net.minecraft.block.material.Material;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Material;
 import net.minecraft.item.Item;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 
 public class TurtleSword extends TurtleTool
 {
-    public TurtleSword( ResourceLocation id, String adjective, Item item )
+    public TurtleSword( Identifier id, String adjective, Item item )
     {
         super( id, adjective, item );
     }
 
-    public TurtleSword( ResourceLocation id, Item item )
+    public TurtleSword( Identifier id, Item item )
     {
         super( id, item );
     }
 
     @Override
-    protected boolean canBreakBlock( IBlockState state, World world, BlockPos pos, TurtlePlayer player )
+    protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player )
     {
         if( !super.canBreakBlock( state, world, pos, player ) ) return false;
 
         Material material = state.getMaterial();
-        return material == Material.PLANTS ||
+        return material == Material.PLANT ||
             material == Material.LEAVES ||
-            material == Material.VINE ||
-            material == Material.CLOTH ||
-            material == Material.WEB;
+            // material == Material.VINE ||
+            material == Material.WOOL ||
+            material == Material.COBWEB;
     }
 
     @Override
diff --git a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java
index c272787770..9cde1e43aa 100644
--- a/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java
+++ b/src/main/java/dan200/computercraft/shared/turtle/upgrades/TurtleTool.java
@@ -11,35 +11,33 @@
 import dan200.computercraft.api.turtle.*;
 import dan200.computercraft.api.turtle.event.TurtleAttackEvent;
 import dan200.computercraft.api.turtle.event.TurtleBlockEvent;
+import dan200.computercraft.api.turtle.event.TurtleEvent;
 import dan200.computercraft.shared.TurtlePermissions;
 import dan200.computercraft.shared.turtle.core.TurtlePlaceCommand;
 import dan200.computercraft.shared.turtle.core.TurtlePlayer;
 import dan200.computercraft.shared.util.DropConsumer;
 import dan200.computercraft.shared.util.InventoryUtil;
+import dan200.computercraft.shared.util.ItemStorage;
 import dan200.computercraft.shared.util.WorldUtil;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.block.Block;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.model.IBakedModel;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Blocks;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.BakedModel;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.SharedMonsterAttributes;
-import net.minecraft.entity.item.EntityArmorStand;
-import net.minecraft.fluid.IFluidState;
-import net.minecraft.init.Blocks;
+import net.minecraft.entity.attribute.EntityAttributes;
+import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.entity.decoration.ArmorStandEntity;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.DamageSource;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.event.entity.player.AttackEntityEvent;
-import net.minecraftforge.event.world.BlockEvent;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -51,13 +49,13 @@ public class TurtleTool extends AbstractTurtleUpgrade
 {
     protected ItemStack m_item;
 
-    public TurtleTool( ResourceLocation id, String adjective, Item item )
+    public TurtleTool( Identifier id, String adjective, Item item )
     {
         super( id, TurtleUpgradeType.Tool, adjective, item );
         m_item = new ItemStack( item );
     }
 
-    public TurtleTool( ResourceLocation id, Item item )
+    public TurtleTool( Identifier id, Item item )
     {
         super( id, TurtleUpgradeType.Tool, item );
         m_item = new ItemStack( item );
@@ -65,26 +63,25 @@ public TurtleTool( ResourceLocation id, Item item )
 
     @Nonnull
     @Override
-    @OnlyIn( Dist.CLIENT )
-    public Pair<IBakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
+    @Environment( EnvType.CLIENT )
+    public Pair<BakedModel, Matrix4f> getModel( ITurtleAccess turtle, @Nonnull TurtleSide side )
     {
         float xOffset = side == TurtleSide.Left ? -0.40625f : 0.40625f;
-        Matrix4f transform = new Matrix4f(
+        Matrix4f transform = new Matrix4f( new float[] {
             0.0f, 0.0f, -1.0f, 1.0f + xOffset,
             1.0f, 0.0f, 0.0f, 0.0f,
             0.0f, -1.0f, 0.0f, 1.0f,
             0.0f, 0.0f, 0.0f, 1.0f
-        );
-        Minecraft mc = Minecraft.getInstance();
+        } );
         return Pair.of(
-            mc.getItemRenderer().getItemModelMesher().getItemModel( m_item ),
+            MinecraftClient.getInstance().getItemRenderer().getModels().getModel( m_item ),
             transform
         );
     }
 
     @Nonnull
     @Override
-    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull EnumFacing direction )
+    public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull TurtleSide side, @Nonnull TurtleVerb verb, @Nonnull Direction direction )
     {
         switch( verb )
         {
@@ -97,13 +94,13 @@ public TurtleCommandResult useTool( @Nonnull ITurtleAccess turtle, @Nonnull Turt
         }
     }
 
-    protected boolean canBreakBlock( IBlockState state, World world, BlockPos pos, TurtlePlayer player )
+    protected boolean canBreakBlock( BlockState state, World world, BlockPos pos, TurtlePlayer player )
     {
         Block block = state.getBlock();
-        return !state.isAir( world, pos )
+        return !state.isAir()
             && block != Blocks.BEDROCK
-            && state.getPlayerRelativeBlockHardness( player, world, pos ) > 0
-            && block.canEntityDestroy( state, world, pos, player );
+            && state.getHardness( world, pos ) > 0
+            /*&& block.canEntityDestroy( state, world, pos, player )*/;
     }
 
     protected float getDamageMultiplier()
@@ -111,7 +108,7 @@ protected float getDamageMultiplier()
         return 3.0f;
     }
 
-    private TurtleCommandResult attack( final ITurtleAccess turtle, EnumFacing direction, TurtleSide side )
+    private TurtleCommandResult attack( final ITurtleAccess turtle, Direction direction, TurtleSide side )
     {
         // Create a fake player, and orient it appropriately
         final World world = turtle.getWorld();
@@ -119,8 +116,8 @@ private TurtleCommandResult attack( final ITurtleAccess turtle, EnumFacing direc
         final TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, position, direction );
 
         // See if there is an entity present
-        Vec3d turtlePos = new Vec3d( turtlePlayer.posX, turtlePlayer.posY, turtlePlayer.posZ );
-        Vec3d rayDir = turtlePlayer.getLook( 1.0f );
+        Vec3d turtlePos = turtlePlayer.getPosVector();
+        Vec3d rayDir = turtlePlayer.getRotationVec( 1.0f );
         Pair<Entity, Vec3d> hit = WorldUtil.rayTraceEntities( world, turtlePos, rayDir, 1.5 );
         if( hit != null )
         {
@@ -131,13 +128,15 @@ private TurtleCommandResult attack( final ITurtleAccess turtle, EnumFacing direc
             Entity hitEntity = hit.getKey();
 
             // Fire several events to ensure we have permissions.
+            /*
             if( MinecraftForge.EVENT_BUS.post( new AttackEntityEvent( turtlePlayer, hitEntity ) ) || !hitEntity.canBeAttackedWithItem() )
             {
                 return TurtleCommandResult.failure( "Nothing to attack here" );
             }
+            */
 
             TurtleAttackEvent attackEvent = new TurtleAttackEvent( turtle, turtlePlayer, hitEntity, this, side );
-            if( MinecraftForge.EVENT_BUS.post( attackEvent ) )
+            if( TurtleEvent.post( attackEvent ) )
             {
                 return TurtleCommandResult.failure( attackEvent.getFailureMessage() );
             }
@@ -147,26 +146,24 @@ private TurtleCommandResult attack( final ITurtleAccess turtle, EnumFacing direc
 
             // Attack the entity
             boolean attacked = false;
-            if( !hitEntity.hitByEntity( turtlePlayer ) )
+            if( !hitEntity.method_5698( turtlePlayer ) ) // hitByEntity
             {
-                float damage = (float) turtlePlayer.getAttribute( SharedMonsterAttributes.ATTACK_DAMAGE ).getValue();
+                float damage = (float) turtlePlayer.getAttributeInstance( EntityAttributes.ATTACK_DAMAGE ).getValue();
                 damage *= getDamageMultiplier();
                 if( damage > 0.0f )
                 {
-                    DamageSource source = DamageSource.causePlayerDamage( turtlePlayer );
-                    if( hitEntity instanceof EntityArmorStand )
+                    // TODO: Is this sufficient? I feel we need to do velocity updates here now.
+                    DamageSource source = DamageSource.player( turtlePlayer );
+                    if( hitEntity instanceof ArmorStandEntity )
                     {
                         // Special case for armor stands: attack twice to guarantee destroy
-                        hitEntity.attackEntityFrom( source, damage );
-                        if( hitEntity.isAlive() )
-                        {
-                            hitEntity.attackEntityFrom( source, damage );
-                        }
+                        hitEntity.damage( source, damage );
+                        if( hitEntity.isAlive() ) hitEntity.damage( source, damage );
                         attacked = true;
                     }
                     else
                     {
-                        if( hitEntity.attackEntityFrom( source, damage ) )
+                        if( hitEntity.damage( source, damage ) )
                         {
                             attacked = true;
                         }
@@ -188,20 +185,19 @@ private TurtleCommandResult attack( final ITurtleAccess turtle, EnumFacing direc
         return TurtleCommandResult.failure( "Nothing to attack here" );
     }
 
-    private TurtleCommandResult dig( ITurtleAccess turtle, EnumFacing direction, TurtleSide side )
+    private TurtleCommandResult dig( ITurtleAccess turtle, Direction direction, TurtleSide side )
     {
         // Get ready to dig
         World world = turtle.getWorld();
         BlockPos turtlePosition = turtle.getPosition();
         BlockPos blockPosition = turtlePosition.offset( direction );
 
-        if( world.isAirBlock( blockPosition ) || WorldUtil.isLiquidBlock( world, blockPosition ) )
+        if( world.isAir( blockPosition ) || WorldUtil.isLiquidBlock( world, blockPosition ) )
         {
             return TurtleCommandResult.failure( "Nothing to dig here" );
         }
 
-        IBlockState state = world.getBlockState( blockPosition );
-        IFluidState fluidState = world.getFluidState( blockPosition );
+        BlockState state = world.getBlockState( blockPosition );
 
         TurtlePlayer turtlePlayer = TurtlePlaceCommand.createPlayer( turtle, turtlePosition, direction );
         turtlePlayer.loadInventory( m_item.copy() );
@@ -209,10 +205,12 @@ private TurtleCommandResult dig( ITurtleAccess turtle, EnumFacing direction, Tur
         if( ComputerCraft.turtlesObeyBlockProtection )
         {
             // Check spawn protection
+            /*
             if( MinecraftForge.EVENT_BUS.post( new BlockEvent.BreakEvent( world, blockPosition, state, turtlePlayer ) ) )
             {
                 return TurtleCommandResult.failure( "Cannot break protected block" );
             }
+            */
 
             if( !TurtlePermissions.isBlockEditable( world, blockPosition, turtlePlayer ) )
             {
@@ -228,7 +226,7 @@ private TurtleCommandResult dig( ITurtleAccess turtle, EnumFacing direction, Tur
 
         // Fire the dig event, checking whether it was cancelled.
         TurtleBlockEvent.Dig digEvent = new TurtleBlockEvent.Dig( turtle, turtlePlayer, world, blockPosition, state, this, side );
-        if( MinecraftForge.EVENT_BUS.post( digEvent ) )
+        if( TurtleEvent.post( digEvent ) )
         {
             return TurtleCommandResult.failure( digEvent.getFailureMessage() );
         }
@@ -236,21 +234,23 @@ private TurtleCommandResult dig( ITurtleAccess turtle, EnumFacing direction, Tur
         // Consume the items the block drops
         DropConsumer.set( world, blockPosition, turtleDropConsumer( turtle ) );
 
-        TileEntity tile = world.getTileEntity( blockPosition );
+        BlockEntity tile = world.getBlockEntity( blockPosition );
 
         // Much of this logic comes from PlayerInteractionManager#tryHarvestBlock, so it's a good idea
         // to consult there before making any changes.
 
         // Play the destruction sound and particles
-        world.playEvent( 2001, blockPosition, Block.getStateId( state ) );
+        world.playGlobalEvent( 2001, blockPosition, Block.getRawIdFromState( state ) );
 
         // Destroy the block
-        boolean canHarvest = state.canHarvestBlock( world, blockPosition, turtlePlayer );
-        boolean canBreak = state.removedByPlayer( world, blockPosition, turtlePlayer, canHarvest, fluidState );
-        if( canBreak ) state.getBlock().onPlayerDestroy( world, blockPosition, state );
-        if( canHarvest )
+        state.getBlock().onBreak( world, blockPosition, state, turtlePlayer );
+        if( world.clearBlockState( blockPosition, false ) )
         {
-            state.getBlock().harvestBlock( world, turtlePlayer, blockPosition, state, tile, turtlePlayer.getHeldItemMainhand() );
+            state.getBlock().onBroken( world, blockPosition, state );
+            if( turtlePlayer.isUsingEffectiveTool( state ) )
+            {
+                state.getBlock().afterBreak( world, turtlePlayer, blockPosition, state, tile, m_item.copy() );
+            }
         }
 
         stopConsuming( turtle );
@@ -261,7 +261,7 @@ private TurtleCommandResult dig( ITurtleAccess turtle, EnumFacing direction, Tur
 
     private static Function<ItemStack, ItemStack> turtleDropConsumer( ITurtleAccess turtle )
     {
-        return drop -> InventoryUtil.storeItems( drop, turtle.getItemHandler(), turtle.getSelectedSlot() );
+        return drop -> InventoryUtil.storeItems( drop, ItemStorage.wrap( turtle.getInventory() ), turtle.getSelectedSlot() );
     }
 
     private static void stopConsuming( ITurtleAccess turtle )
diff --git a/src/main/java/dan200/computercraft/shared/util/AbstractRecipe.java b/src/main/java/dan200/computercraft/shared/util/AbstractRecipe.java
deleted file mode 100644
index a6908bbee2..0000000000
--- a/src/main/java/dan200/computercraft/shared/util/AbstractRecipe.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * This file is part of ComputerCraft - http://www.computercraft.info
- * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
- * Send enquiries to dratcliffe@gmail.com
- */
-
-package dan200.computercraft.shared.util;
-
-import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipe;
-import net.minecraft.util.ResourceLocation;
-
-import javax.annotation.Nonnull;
-
-public abstract class AbstractRecipe implements IRecipe
-{
-    private final ResourceLocation id;
-
-    public AbstractRecipe( ResourceLocation id )
-    {
-        this.id = id;
-    }
-
-    @Nonnull
-    @Override
-    public ItemStack getRecipeOutput()
-    {
-        return ItemStack.EMPTY;
-    }
-
-    @Nonnull
-    @Override
-    public ResourceLocation getId()
-    {
-        return id;
-    }
-
-    @Override
-    public boolean isDynamic()
-    {
-        return true;
-    }
-}
diff --git a/src/main/java/dan200/computercraft/shared/util/ColourUtils.java b/src/main/java/dan200/computercraft/shared/util/ColourUtils.java
index 22d47f8272..b41f0d38f1 100644
--- a/src/main/java/dan200/computercraft/shared/util/ColourUtils.java
+++ b/src/main/java/dan200/computercraft/shared/util/ColourUtils.java
@@ -6,47 +6,18 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.item.EnumDyeColor;
+import net.minecraft.item.DyeItem;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.tags.Tag;
-import net.minecraftforge.common.Tags;
-
-import javax.annotation.Nullable;
+import net.minecraft.util.DyeColor;
 
 public final class ColourUtils
 {
-    @SuppressWarnings( { "unchecked", "rawtypes" } )
-    private static final Tag<Item>[] DYES = new Tag[] {
-        Tags.Items.DYES_WHITE,
-        Tags.Items.DYES_ORANGE,
-        Tags.Items.DYES_MAGENTA,
-        Tags.Items.DYES_LIGHT_BLUE,
-        Tags.Items.DYES_YELLOW,
-        Tags.Items.DYES_LIME,
-        Tags.Items.DYES_PINK,
-        Tags.Items.DYES_GRAY,
-        Tags.Items.DYES_LIGHT_GRAY,
-        Tags.Items.DYES_CYAN,
-        Tags.Items.DYES_PURPLE,
-        Tags.Items.DYES_BLUE,
-        Tags.Items.DYES_BROWN,
-        Tags.Items.DYES_GREEN,
-        Tags.Items.DYES_RED,
-        Tags.Items.DYES_BLACK,
-    };
-
-    @Nullable
     private ColourUtils() {}
 
-    public static EnumDyeColor getStackColour( ItemStack stack )
+    public static DyeColor getStackColour( ItemStack stack )
     {
-        for( int i = 0; i < DYES.length; i++ )
-        {
-            Tag<Item> dye = DYES[i];
-            if( dye.contains( stack.getItem() ) ) return EnumDyeColor.byId( i );
-        }
-
-        return null;
+        Item item = stack.getItem();
+        return item instanceof DyeItem ? ((DyeItem) item).getColor() : null;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java b/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java
index 28905442fd..a73a0fb066 100644
--- a/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java
+++ b/src/main/java/dan200/computercraft/shared/util/CreativeTabMain.java
@@ -7,23 +7,23 @@
 package dan200.computercraft.shared.util;
 
 import dan200.computercraft.ComputerCraft;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.item.ItemGroup;
 import net.minecraft.item.ItemStack;
-import net.minecraftforge.api.distmarker.Dist;
-import net.minecraftforge.api.distmarker.OnlyIn;
 
 import javax.annotation.Nonnull;
 
 public class CreativeTabMain extends ItemGroup
 {
-    public CreativeTabMain()
+    public CreativeTabMain( int i )
     {
-        super( ComputerCraft.MOD_ID );
+        super( i, ComputerCraft.MOD_ID );
     }
 
     @Nonnull
     @Override
-    @OnlyIn( Dist.CLIENT )
+    @Environment( EnvType.CLIENT )
     public ItemStack createIcon()
     {
         return new ItemStack( ComputerCraft.Blocks.computerNormal );
diff --git a/src/main/java/dan200/computercraft/shared/util/DefaultInteractionObject.java b/src/main/java/dan200/computercraft/shared/util/DefaultInteractionObject.java
deleted file mode 100644
index 878f2951a8..0000000000
--- a/src/main/java/dan200/computercraft/shared/util/DefaultInteractionObject.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * This file is part of ComputerCraft - http://www.computercraft.info
- * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
- * Send enquiries to dratcliffe@gmail.com
- */
-
-package dan200.computercraft.shared.util;
-
-import dan200.computercraft.shared.network.container.ContainerType;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.InventoryPlayer;
-import net.minecraft.inventory.Container;
-import net.minecraft.world.IInteractionObject;
-
-import javax.annotation.Nonnull;
-
-public interface DefaultInteractionObject<T extends Container> extends IInteractionObject
-{
-    @Nonnull
-    @Override
-    T createContainer( @Nonnull InventoryPlayer inventory, @Nonnull EntityPlayer player );
-
-    @Nonnull
-    ContainerType<T> getContainerType();
-
-    @Nonnull
-    @Override
-    default String getGuiID()
-    {
-        return getContainerType().getId().toString();
-    }
-}
diff --git a/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java b/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java
index dbaf268256..59e223083e 100644
--- a/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java
+++ b/src/main/java/dan200/computercraft/shared/util/DefaultInventory.java
@@ -6,65 +6,8 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.text.ITextComponent;
+import net.minecraft.inventory.Inventory;
 
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-public interface DefaultInventory extends IInventory
+public interface DefaultInventory extends Inventory
 {
-    @Override
-    default int getInventoryStackLimit()
-    {
-        return 64;
-    }
-
-    @Override
-    default void openInventory( @Nonnull EntityPlayer player )
-    {
-    }
-
-    @Override
-    default void closeInventory( @Nonnull EntityPlayer player )
-    {
-    }
-
-    @Override
-    default boolean isItemValidForSlot( int slot, @Nonnull ItemStack stack )
-    {
-        return true;
-    }
-
-    @Override
-    default int getField( int field )
-    {
-        return 0;
-    }
-
-    @Override
-    default void setField( int field, int value )
-    {
-    }
-
-    @Override
-    default int getFieldCount()
-    {
-        return 0;
-    }
-
-    @Override
-    default boolean hasCustomName()
-    {
-        return getCustomName() != null;
-    }
-
-    @Nullable
-    @Override
-    default ITextComponent getCustomName()
-    {
-        return null;
-    }
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/DefaultPropertyDelegate.java b/src/main/java/dan200/computercraft/shared/util/DefaultPropertyDelegate.java
new file mode 100644
index 0000000000..543392a466
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/util/DefaultPropertyDelegate.java
@@ -0,0 +1,17 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.util;
+
+import net.minecraft.container.PropertyDelegate;
+
+public interface DefaultPropertyDelegate extends PropertyDelegate
+{
+    @Override
+    default void set( int property, int value )
+    {
+    }
+}
diff --git a/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java b/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java
index 2c1f0bc249..b7f22f9744 100644
--- a/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java
+++ b/src/main/java/dan200/computercraft/shared/util/DefaultSidedInventory.java
@@ -6,23 +6,23 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.inventory.ISidedInventory;
+import net.minecraft.inventory.SidedInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-public interface DefaultSidedInventory extends DefaultInventory, ISidedInventory
+public interface DefaultSidedInventory extends DefaultInventory, SidedInventory
 {
     @Override
-    default boolean canInsertItem( int slot, @Nonnull ItemStack stack, @Nullable EnumFacing side )
+    default boolean canInsertInvStack( int slot, @Nonnull ItemStack stack, @Nullable Direction side )
     {
-        return isItemValidForSlot( slot, stack );
+        return isValidInvStack( slot, stack );
     }
 
     @Override
-    default boolean canExtractItem( int slot, @Nonnull ItemStack stack, @Nonnull EnumFacing side )
+    default boolean canExtractInvStack( int slot, @Nonnull ItemStack stack, @Nonnull Direction side )
     {
         return true;
     }
diff --git a/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java b/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java
index a4d654fb49..bbe46af546 100644
--- a/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/DirectionUtil.java
@@ -6,39 +6,39 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.Direction;
 
 public final class DirectionUtil
 {
     private DirectionUtil() {}
 
-    public static final EnumFacing[] FACINGS = EnumFacing.values();
+    public static final Direction[] FACINGS = Direction.values();
 
-    public static EnumFacing toLocal( EnumFacing front, EnumFacing relative )
+    public static Direction toLocal( Direction front, Direction relative )
     {
-        if( relative.getAxis() == EnumFacing.Axis.Y ) return relative;
+        if( relative.getAxis() == Direction.Axis.Y ) return relative;
 
-        if( front.getAxis() == EnumFacing.Axis.Y ) front = EnumFacing.NORTH;
+        if( front.getAxis() == Direction.Axis.Y ) front = Direction.NORTH;
 
         if( relative == front )
         {
-            return EnumFacing.SOUTH;
+            return Direction.SOUTH;
         }
         else if( relative == front.getOpposite() )
         {
-            return EnumFacing.NORTH;
+            return Direction.NORTH;
         }
-        else if( relative == front.rotateYCCW() )
+        else if( relative == front.rotateYCounterclockwise() )
         {
-            return EnumFacing.EAST;
+            return Direction.EAST;
         }
         else
         {
-            return EnumFacing.WEST;
+            return Direction.WEST;
         }
     }
 
-    public static float toPitchAngle( EnumFacing dir )
+    public static float toPitchAngle( Direction dir )
     {
         switch( dir )
         {
diff --git a/src/main/java/dan200/computercraft/shared/util/DropConsumer.java b/src/main/java/dan200/computercraft/shared/util/DropConsumer.java
index 701dc8dbda..11d682c144 100644
--- a/src/main/java/dan200/computercraft/shared/util/DropConsumer.java
+++ b/src/main/java/dan200/computercraft/shared/util/DropConsumer.java
@@ -6,27 +6,17 @@
 
 package dan200.computercraft.shared.util;
 
-import dan200.computercraft.ComputerCraft;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.item.EntityItem;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.math.AxisAlignedBB;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.BoundingBox;
 import net.minecraft.world.World;
-import net.minecraftforge.event.entity.EntityJoinWorldEvent;
-import net.minecraftforge.event.entity.living.LivingDropsEvent;
-import net.minecraftforge.event.world.BlockEvent;
-import net.minecraftforge.eventbus.api.EventPriority;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 import java.util.function.Function;
 
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID )
 public final class DropConsumer
 {
     private DropConsumer()
@@ -37,7 +27,7 @@ private DropConsumer()
     private static List<ItemStack> remainingDrops;
     private static WeakReference<World> dropWorld;
     private static BlockPos dropPos;
-    private static AxisAlignedBB dropBounds;
+    private static BoundingBox dropBounds;
     private static WeakReference<Entity> dropEntity;
 
     public static void set( Entity entity, Function<ItemStack, ItemStack> consumer )
@@ -47,9 +37,9 @@ public static void set( Entity entity, Function<ItemStack, ItemStack> consumer )
         dropEntity = new WeakReference<>( entity );
         dropWorld = new WeakReference<>( entity.world );
         dropPos = null;
-        dropBounds = new AxisAlignedBB( entity.getPosition() ).grow( 2, 2, 2 );
+        dropBounds = new BoundingBox( entity.getBlockPos() ).expand( 2, 2, 2 );
 
-        entity.captureDrops( new ArrayList<>() );
+        // entity.captureDrops( new ArrayList<>() );
     }
 
     public static void set( World world, BlockPos pos, Function<ItemStack, ItemStack> consumer )
@@ -59,23 +49,25 @@ public static void set( World world, BlockPos pos, Function<ItemStack, ItemStack
         dropEntity = null;
         dropWorld = new WeakReference<>( world );
         dropPos = pos;
-        dropBounds = new AxisAlignedBB( pos ).grow( 2, 2, 2 );
+        dropBounds = new BoundingBox( pos ).expand( 2, 2, 2 );
     }
 
     public static List<ItemStack> clear()
     {
+        /*
         if( dropEntity != null )
         {
             Entity entity = dropEntity.get();
             if( entity != null )
             {
-                Collection<EntityItem> dropped = entity.captureDrops( null );
+                Collection<ItemEntity> dropped = entity.captureDrops( null );
                 if( dropped != null )
                 {
                     for( EntityItem entityItem : dropped ) handleDrops( entityItem.getItem() );
                 }
             }
         }
+        */
 
         List<ItemStack> remainingStacks = remainingDrops;
 
@@ -95,6 +87,7 @@ private static void handleDrops( ItemStack stack )
         if( !remaining.isEmpty() ) remainingDrops.add( remaining );
     }
 
+    /*
     @SubscribeEvent( priority = EventPriority.LOWEST )
     public static void onEntityLivingDrops( LivingDropsEvent event )
     {
@@ -133,4 +126,5 @@ public static void onEntitySpawn( EntityJoinWorldEvent event )
             event.setCanceled( true );
         }
     }
+    */
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/IDAssigner.java b/src/main/java/dan200/computercraft/shared/util/IDAssigner.java
index 68d6c361f7..b56dccf8e7 100644
--- a/src/main/java/dan200/computercraft/shared/util/IDAssigner.java
+++ b/src/main/java/dan200/computercraft/shared/util/IDAssigner.java
@@ -11,8 +11,8 @@
 import com.google.gson.reflect.TypeToken;
 import dan200.computercraft.ComputerCraft;
 import net.minecraft.server.MinecraftServer;
+import net.minecraft.world.World;
 import net.minecraft.world.dimension.DimensionType;
-import net.minecraftforge.fml.server.ServerLifecycleHooks;
 
 import java.io.File;
 import java.io.Reader;
@@ -38,33 +38,33 @@ private IDAssigner()
     private static WeakReference<MinecraftServer> server;
     private static Path idFile;
 
-    public static File getDir()
+    public static File getDir( World world )
     {
-        MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
-        File worldDirectory = server.getWorld( DimensionType.OVERWORLD ).getSaveHandler().getWorldDirectory();
+        MinecraftServer server = world.getServer();
+        File worldDirectory = server.getWorld( DimensionType.OVERWORLD ).getSaveHandler().getWorldDir();
         return new File( worldDirectory, ComputerCraft.MOD_ID );
     }
 
-    private static MinecraftServer getCachedServer()
+    private static MinecraftServer getCachedServer( World world )
     {
         if( server == null ) return null;
 
         MinecraftServer currentServer = server.get();
         if( currentServer == null ) return null;
 
-        if( currentServer != ServerLifecycleHooks.getCurrentServer() ) return null;
+        if( currentServer != world.getServer() ) return null;
         return currentServer;
     }
 
-    public static synchronized int getNextId( String kind )
+    public static synchronized int getNextId( World world, String kind )
     {
-        MinecraftServer currentServer = getCachedServer();
+        MinecraftServer currentServer = getCachedServer( world );
         if( currentServer == null )
         {
             // The server has changed, refetch our ID map
-            server = new WeakReference<>( ServerLifecycleHooks.getCurrentServer() );
+            server = new WeakReference<>( world.getServer() );
 
-            File dir = getDir();
+            File dir = getDir( world );
             dir.mkdirs();
 
             // Load our ID file from disk
diff --git a/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java b/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java
index dd4d0d86ea..fa0217c8af 100644
--- a/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/util/ImpostorRecipe.java
@@ -8,18 +8,16 @@
 
 import com.google.gson.JsonObject;
 import dan200.computercraft.ComputerCraft;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.item.crafting.RecipeSerializers;
-import net.minecraft.item.crafting.ShapedRecipe;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.JsonUtils;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.crafting.ShapedRecipe;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.JsonHelper;
+import net.minecraft.util.PacketByteBuf;
 import net.minecraft.world.World;
-import net.minecraftforge.common.crafting.CraftingHelper;
 
 import javax.annotation.Nonnull;
 
@@ -27,7 +25,7 @@ public final class ImpostorRecipe extends ShapedRecipe
 {
     private final String group;
 
-    private ImpostorRecipe( @Nonnull ResourceLocation id, @Nonnull String group, int width, int height, NonNullList<Ingredient> ingredients, @Nonnull ItemStack result )
+    private ImpostorRecipe( @Nonnull Identifier id, @Nonnull String group, int width, int height, DefaultedList<Ingredient> ingredients, @Nonnull ItemStack result )
     {
         super( id, group, width, height, ingredients, result );
         this.group = group;
@@ -41,64 +39,57 @@ public String getGroup()
     }
 
     @Override
-    public boolean matches( @Nonnull IInventory inv, World world )
+    public boolean matches( @Nonnull CraftingInventory inv, World world )
     {
         return false;
     }
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( @Nonnull IInventory inventory )
+    public ItemStack craft( @Nonnull CraftingInventory inventory )
     {
         return ItemStack.EMPTY;
     }
 
     @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    private static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "impostor_shaped" );
-    public static final IRecipeSerializer<ImpostorRecipe> SERIALIZER = new IRecipeSerializer<ImpostorRecipe>()
+    private static final Identifier ID = new Identifier( ComputerCraft.MOD_ID, "impostor_shaped" );
+    public static final RecipeSerializer<ImpostorRecipe> SERIALIZER = new RecipeSerializer<ImpostorRecipe>()
     {
         @Override
-        public ImpostorRecipe read( @Nonnull ResourceLocation identifier, @Nonnull JsonObject json )
+        public ImpostorRecipe read( Identifier identifier, JsonObject json )
         {
-            String group = JsonUtils.getString( json, "group", "" );
-            ShapedRecipe recipe = RecipeSerializers.CRAFTING_SHAPED.read( identifier, json );
-            ItemStack result = CraftingHelper.getItemStack( JsonUtils.getJsonObject( json, "result" ), true );
-            return new ImpostorRecipe( identifier, group, recipe.getWidth(), recipe.getHeight(), recipe.getIngredients(), result );
+            String group = JsonHelper.getString( json, "group", "" );
+            ShapedRecipe recipe = RecipeSerializer.SHAPED.read( identifier, json );
+            ItemStack result = ShapedRecipe.getItemStack( JsonHelper.getObject( json, "result" ) );
+            return new ImpostorRecipe( identifier, group, recipe.getWidth(), recipe.getHeight(), recipe.getPreviewInputs(), result );
         }
 
         @Override
-        public ImpostorRecipe read( @Nonnull ResourceLocation identifier, @Nonnull PacketBuffer buf )
+        public ImpostorRecipe read( Identifier identifier, PacketByteBuf buf )
         {
             int width = buf.readVarInt();
             int height = buf.readVarInt();
             String group = buf.readString( Short.MAX_VALUE );
-            NonNullList<Ingredient> items = NonNullList.withSize( width * height, Ingredient.EMPTY );
-            for( int k = 0; k < items.size(); ++k ) items.set( k, Ingredient.read( buf ) );
+            DefaultedList<Ingredient> items = DefaultedList.create( width * height, Ingredient.EMPTY );
+            for( int k = 0; k < items.size(); ++k ) items.set( k, Ingredient.fromPacket( buf ) );
             ItemStack result = buf.readItemStack();
             return new ImpostorRecipe( identifier, group, width, height, items, result );
         }
 
         @Override
-        public void write( @Nonnull PacketBuffer buf, @Nonnull ImpostorRecipe recipe )
+        public void write( @Nonnull PacketByteBuf buf, @Nonnull ImpostorRecipe recipe )
         {
-            buf.writeVarInt( recipe.getRecipeWidth() );
-            buf.writeVarInt( recipe.getRecipeHeight() );
+            buf.writeVarInt( recipe.getWidth() );
+            buf.writeVarInt( recipe.getHeight() );
             buf.writeString( recipe.getGroup() );
-            for( Ingredient ingredient : recipe.getIngredients() ) ingredient.write( buf );
-            buf.writeItemStack( recipe.getRecipeOutput() );
-        }
-
-        @Nonnull
-        @Override
-        public ResourceLocation getName()
-        {
-            return ID;
+            for( Ingredient ingredient : recipe.getPreviewInputs() ) ingredient.write( buf );
+            buf.writeItemStack( recipe.getOutput() );
         }
     };
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java b/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java
index 889ed2869c..ed5f80e7e3 100644
--- a/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java
+++ b/src/main/java/dan200/computercraft/shared/util/ImpostorShapelessRecipe.java
@@ -9,18 +9,17 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
-import dan200.computercraft.ComputerCraft;
-import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.CraftingInventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.crafting.IRecipeSerializer;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.item.crafting.ShapelessRecipe;
-import net.minecraft.network.PacketBuffer;
-import net.minecraft.util.JsonUtils;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.recipe.RecipeSerializer;
+import net.minecraft.recipe.crafting.ShapedRecipe;
+import net.minecraft.recipe.crafting.ShapelessRecipe;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.JsonHelper;
+import net.minecraft.util.PacketByteBuf;
 import net.minecraft.world.World;
-import net.minecraftforge.common.crafting.CraftingHelper;
 
 import javax.annotation.Nonnull;
 
@@ -28,7 +27,7 @@ public final class ImpostorShapelessRecipe extends ShapelessRecipe
 {
     private final String group;
 
-    private ImpostorShapelessRecipe( @Nonnull ResourceLocation id, @Nonnull String group, @Nonnull ItemStack result, NonNullList<Ingredient> ingredients )
+    private ImpostorShapelessRecipe( @Nonnull Identifier id, @Nonnull String group, @Nonnull ItemStack result, DefaultedList<Ingredient> ingredients )
     {
         super( id, group, result, ingredients );
         this.group = group;
@@ -42,34 +41,32 @@ public String getGroup()
     }
 
     @Override
-    public boolean matches( IInventory inv, World world )
+    public boolean matches( CraftingInventory inv, World world )
     {
         return false;
     }
 
     @Nonnull
     @Override
-    public ItemStack getCraftingResult( IInventory inventory )
+    public ItemStack craft( CraftingInventory inventory )
     {
         return ItemStack.EMPTY;
     }
 
     @Nonnull
     @Override
-    public IRecipeSerializer<?> getSerializer()
+    public RecipeSerializer<?> getSerializer()
     {
         return SERIALIZER;
     }
 
-    private static final ResourceLocation ID = new ResourceLocation( ComputerCraft.MOD_ID, "impostor_shapeless" );
-
-    public static final IRecipeSerializer<ImpostorShapelessRecipe> SERIALIZER = new IRecipeSerializer<ImpostorShapelessRecipe>()
+    public static final RecipeSerializer<ImpostorShapelessRecipe> SERIALIZER = new RecipeSerializer<ImpostorShapelessRecipe>()
     {
         @Override
-        public ImpostorShapelessRecipe read( @Nonnull ResourceLocation id, @Nonnull JsonObject json )
+        public ImpostorShapelessRecipe read( @Nonnull Identifier id, @Nonnull JsonObject json )
         {
-            String s = JsonUtils.getString( json, "group", "" );
-            NonNullList<Ingredient> ingredients = readIngredients( JsonUtils.getJsonArray( json, "ingredients" ) );
+            String s = JsonHelper.getString( json, "group", "" );
+            DefaultedList<Ingredient> ingredients = readIngredients( JsonHelper.getArray( json, "ingredients" ) );
 
             if( ingredients.isEmpty() ) throw new JsonParseException( "No ingredients for shapeless recipe" );
             if( ingredients.size() > 9 )
@@ -77,50 +74,43 @@ public ImpostorShapelessRecipe read( @Nonnull ResourceLocation id, @Nonnull Json
                 throw new JsonParseException( "Too many ingredients for shapeless recipe the max is 9" );
             }
 
-            ItemStack itemstack = CraftingHelper.getItemStack( JsonUtils.getJsonObject( json, "result" ), true );
+            ItemStack itemstack = ShapedRecipe.getItemStack( JsonHelper.getObject( json, "result" ) );
             return new ImpostorShapelessRecipe( id, s, itemstack, ingredients );
         }
 
-        private NonNullList<Ingredient> readIngredients( JsonArray arrays )
+        private DefaultedList<Ingredient> readIngredients( JsonArray arrays )
         {
-            NonNullList<Ingredient> items = NonNullList.create();
+            DefaultedList<Ingredient> items = DefaultedList.create();
             for( int i = 0; i < arrays.size(); ++i )
             {
-                Ingredient ingredient = Ingredient.deserialize( arrays.get( i ) );
-                if( !ingredient.hasNoMatchingItems() ) items.add( ingredient );
+                Ingredient ingredient = Ingredient.fromJson( arrays.get( i ) );
+                if( !ingredient.isEmpty() ) items.add( ingredient );
             }
 
             return items;
         }
 
         @Override
-        public ImpostorShapelessRecipe read( @Nonnull ResourceLocation id, PacketBuffer buffer )
+        public ImpostorShapelessRecipe read( @Nonnull Identifier id, PacketByteBuf buffer )
         {
             String s = buffer.readString( 32767 );
             int i = buffer.readVarInt();
-            NonNullList<Ingredient> items = NonNullList.withSize( i, Ingredient.EMPTY );
+            DefaultedList<Ingredient> items = DefaultedList.create( i, Ingredient.EMPTY );
 
-            for( int j = 0; j < items.size(); j++ ) items.set( j, Ingredient.read( buffer ) );
+            for( int j = 0; j < items.size(); j++ ) items.set( j, Ingredient.fromPacket( buffer ) );
             ItemStack result = buffer.readItemStack();
 
             return new ImpostorShapelessRecipe( id, s, result, items );
         }
 
         @Override
-        public void write( @Nonnull PacketBuffer buffer, @Nonnull ImpostorShapelessRecipe recipe )
+        public void write( @Nonnull PacketByteBuf buffer, @Nonnull ImpostorShapelessRecipe recipe )
         {
             buffer.writeString( recipe.getGroup() );
-            buffer.writeVarInt( recipe.getIngredients().size() );
-
-            for( Ingredient ingredient : recipe.getIngredients() ) ingredient.write( buffer );
-            buffer.writeItemStack( recipe.getRecipeOutput() );
-        }
+            buffer.writeVarInt( recipe.getPreviewInputs().size() );
 
-        @Nonnull
-        @Override
-        public ResourceLocation getName()
-        {
-            return ID;
+            for( Ingredient ingredient : recipe.getPreviewInputs() ) ingredient.write( buffer );
+            buffer.writeItemStack( recipe.getOutput() );
         }
     };
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java b/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java
index d67223e244..3e13a9eb15 100644
--- a/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/InventoryUtil.java
@@ -6,22 +6,15 @@
 
 package dan200.computercraft.shared.util;
 
+import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.entity.Entity;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.ISidedInventory;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.NBTTagCompound;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.nbt.CompoundTag;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
-import net.minecraftforge.common.util.LazyOptional;
-import net.minecraftforge.items.CapabilityItemHandler;
-import net.minecraftforge.items.IItemHandler;
-import net.minecraftforge.items.ItemHandlerHelper;
-import net.minecraftforge.items.wrapper.InvWrapper;
-import net.minecraftforge.items.wrapper.SidedInvWrapper;
 import org.apache.commons.lang3.tuple.Pair;
 
 import javax.annotation.Nonnull;
@@ -33,12 +26,12 @@ private InventoryUtil() {}
 
     public static boolean areItemsEqual( @Nonnull ItemStack a, @Nonnull ItemStack b )
     {
-        return a == b || ItemStack.areItemStacksEqual( a, b );
+        return a == b || ItemStack.areEqual( a, b );
     }
 
     public static boolean areItemsStackable( @Nonnull ItemStack a, @Nonnull ItemStack b )
     {
-        return a == b || ItemHandlerHelper.canItemStacksStack( a, b );
+        return a == b || (a.getItem() == b.getItem() && ItemStack.areTagsEqual( a, b ));
     }
 
     /**
@@ -62,8 +55,8 @@ public static boolean areItemsSimilar( @Nonnull ItemStack a, @Nonnull ItemStack
 
         // A more expanded form of ItemStack.areShareTagsEqual, but allowing an empty tag to be equal to a
         // null one.
-        NBTTagCompound shareTagA = a.getItem().getShareTag( a );
-        NBTTagCompound shareTagB = b.getItem().getShareTag( b );
+        CompoundTag shareTagA = a.getTag();
+        CompoundTag shareTagB = b.getTag();
         if( shareTagA == shareTagB ) return true;
         if( shareTagA == null ) return shareTagB.isEmpty();
         if( shareTagB == null ) return shareTagA.isEmpty();
@@ -78,65 +71,63 @@ public static ItemStack copyItem( @Nonnull ItemStack a )
 
     // Methods for finding inventories:
 
-    public static IItemHandler getInventory( World world, BlockPos pos, EnumFacing side )
+    public static Inventory getInventory( World world, BlockPos pos, Direction side )
     {
         // Look for tile with inventory
-        TileEntity tileEntity = world.getTileEntity( pos );
-        if( tileEntity != null )
+        int y = pos.getY();
+        if( y >= 0 && y < world.getHeight() )
         {
-            LazyOptional<IItemHandler> itemHandler = tileEntity.getCapability( CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, side );
-            if( itemHandler.isPresent() )
+            BlockEntity tileEntity = world.getBlockEntity( pos );
+            if( tileEntity != null )
             {
-                return itemHandler.orElseThrow( NullPointerException::new );
-            }
-            else if( side != null && tileEntity instanceof ISidedInventory )
-            {
-                return new SidedInvWrapper( (ISidedInventory) tileEntity, side );
-            }
-            else if( tileEntity instanceof IInventory )
-            {
-                return new InvWrapper( (IInventory) tileEntity );
+                return (Inventory) tileEntity;
             }
         }
 
         // Look for entity with inventory
         Vec3d vecStart = new Vec3d(
-            pos.getX() + 0.5 + 0.6 * side.getXOffset(),
-            pos.getY() + 0.5 + 0.6 * side.getYOffset(),
-            pos.getZ() + 0.5 + 0.6 * side.getZOffset()
+            pos.getX() + 0.5 + 0.6 * side.getOffsetX(),
+            pos.getY() + 0.5 + 0.6 * side.getOffsetY(),
+            pos.getZ() + 0.5 + 0.6 * side.getOffsetZ()
         );
-        EnumFacing dir = side.getOpposite();
+        Direction dir = side.getOpposite();
         Vec3d vecDir = new Vec3d(
-            dir.getXOffset(), dir.getYOffset(), dir.getZOffset()
+            dir.getOffsetX(), dir.getOffsetY(), dir.getOffsetZ()
         );
         Pair<Entity, Vec3d> hit = WorldUtil.rayTraceEntities( world, vecStart, vecDir, 1.1 );
         if( hit != null )
         {
             Entity entity = hit.getKey();
-            if( entity instanceof IInventory )
+            if( entity instanceof Inventory )
             {
-                return new InvWrapper( (IInventory) entity );
+                return (Inventory) entity;
             }
         }
         return null;
     }
 
+    public static ItemStorage getStorage( World world, BlockPos pos, Direction side )
+    {
+        Inventory inventory = getInventory( world, pos, side );
+        return inventory == null ? null : ItemStorage.wrap( inventory, side );
+    }
+
     // Methods for placing into inventories:
 
     @Nonnull
-    public static ItemStack storeItems( @Nonnull ItemStack itemstack, IItemHandler inventory, int begin )
+    public static ItemStack storeItems( @Nonnull ItemStack itemstack, ItemStorage inventory, int begin )
     {
-        return storeItems( itemstack, inventory, 0, inventory.getSlots(), begin );
+        return storeItems( itemstack, inventory, 0, inventory.size(), begin );
     }
 
     @Nonnull
-    public static ItemStack storeItems( @Nonnull ItemStack itemstack, IItemHandler inventory )
+    public static ItemStack storeItems( @Nonnull ItemStack itemstack, ItemStorage inventory )
     {
-        return storeItems( itemstack, inventory, 0, inventory.getSlots(), 0 );
+        return storeItems( itemstack, inventory, 0, inventory.size(), 0 );
     }
 
     @Nonnull
-    public static ItemStack storeItems( @Nonnull ItemStack stack, IItemHandler inventory, int start, int range, int begin )
+    public static ItemStack storeItems( @Nonnull ItemStack stack, ItemStorage inventory, int start, int range, int begin )
     {
         if( stack.isEmpty() ) return ItemStack.EMPTY;
 
@@ -146,7 +137,7 @@ public static ItemStack storeItems( @Nonnull ItemStack stack, IItemHandler inven
         {
             int slot = start + (i + begin - start) % range;
             if( remainder.isEmpty() ) break;
-            remainder = inventory.insertItem( slot, remainder, false );
+            remainder = inventory.store( slot, remainder, false );
         }
         return areItemsEqual( stack, remainder ) ? stack : remainder;
     }
@@ -154,19 +145,19 @@ public static ItemStack storeItems( @Nonnull ItemStack stack, IItemHandler inven
     // Methods for taking out of inventories
 
     @Nonnull
-    public static ItemStack takeItems( int count, IItemHandler inventory, int begin )
+    public static ItemStack takeItems( int count, ItemStorage inventory, int begin )
     {
-        return takeItems( count, inventory, 0, inventory.getSlots(), begin );
+        return takeItems( count, inventory, 0, inventory.size(), begin );
     }
 
     @Nonnull
-    public static ItemStack takeItems( int count, IItemHandler inventory )
+    public static ItemStack takeItems( int count, ItemStorage inventory )
     {
-        return takeItems( count, inventory, 0, inventory.getSlots(), 0 );
+        return takeItems( count, inventory, 0, inventory.size(), 0 );
     }
 
     @Nonnull
-    public static ItemStack takeItems( int count, IItemHandler inventory, int start, int range, int begin )
+    public static ItemStack takeItems( int count, ItemStorage inventory, int start, int range, int begin )
     {
         // Combine multiple stacks from inventory into one if necessary
         ItemStack partialStack = ItemStack.EMPTY;
@@ -174,31 +165,23 @@ public static ItemStack takeItems( int count, IItemHandler inventory, int start,
         {
             int slot = start + (i + begin - start) % range;
 
-            // If we've extracted all items, return
             if( count <= 0 ) break;
 
             // If this doesn't slot, abort.
-            ItemStack stack = inventory.getStackInSlot( slot );
-            if( !stack.isEmpty() && (partialStack.isEmpty() || areItemsStackable( stack, partialStack )) )
+            ItemStack extracted = inventory.take( slot, count, partialStack, false );
+            if( extracted.isEmpty() ) continue;
+
+            count -= extracted.getAmount();
+            if( partialStack.isEmpty() )
             {
-                ItemStack extracted = inventory.extractItem( slot, count, false );
-                if( !extracted.isEmpty() )
-                {
-                    if( partialStack.isEmpty() )
-                    {
-                        // If we've extracted for this first time, then limit the count to the maximum stack size.
-                        partialStack = extracted;
-                        count = Math.min( count, extracted.getMaxStackSize() );
-                    }
-                    else
-                    {
-                        partialStack.grow( extracted.getCount() );
-                    }
-
-                    count -= extracted.getCount();
-                }
+                // If we've extracted for this first time, then limit the count to the maximum stack size.
+                partialStack = extracted;
+                count = Math.min( count, extracted.getMaxAmount() );
+            }
+            else
+            {
+                partialStack.addAmount( extracted.getAmount() );
             }
-
         }
 
         return partialStack;
diff --git a/src/main/java/dan200/computercraft/shared/util/ItemStorage.java b/src/main/java/dan200/computercraft/shared/util/ItemStorage.java
new file mode 100644
index 0000000000..4400147d58
--- /dev/null
+++ b/src/main/java/dan200/computercraft/shared/util/ItemStorage.java
@@ -0,0 +1,257 @@
+/*
+ * This file is part of ComputerCraft - http://www.computercraft.info
+ * Copyright Daniel Ratcliffe, 2011-2019. Do not distribute without permission.
+ * Send enquiries to dratcliffe@gmail.com
+ */
+
+package dan200.computercraft.shared.util;
+
+import net.minecraft.inventory.Inventory;
+import net.minecraft.inventory.SidedInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.math.Direction;
+
+import javax.annotation.Nonnull;
+
+/**
+ * The most cutesy alternative of {@code IItemHandler} the world has ever seen.
+ */
+public interface ItemStorage
+{
+    int size();
+
+    @Nonnull
+    ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate );
+
+    @Nonnull
+    ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate );
+
+    default ItemStorage view( int start, int size )
+    {
+        return new View( this, start, size );
+    }
+
+    class InventoryWrapper implements ItemStorage
+    {
+        private final Inventory inventory;
+
+        InventoryWrapper( Inventory inventory )
+        {
+            this.inventory = inventory;
+        }
+
+        private void setAndDirty( int slot, @Nonnull ItemStack stack )
+        {
+            inventory.setInvStack( slot, stack );
+            inventory.markDirty();
+        }
+
+        protected boolean canExtract( int slot, ItemStack stack )
+        {
+            return true;
+        }
+
+        @Override
+        public int size()
+        {
+            return inventory.getInvSize();
+        }
+
+        @Override
+        public @Nonnull
+        ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate )
+        {
+            ItemStack existing = inventory.getInvStack( slot );
+            if( existing.isEmpty() || !canExtract( slot, existing )
+                || (!filter.isEmpty() && !areStackable( existing, filter )) )
+            {
+                return ItemStack.EMPTY;
+            }
+
+            if( simulate )
+            {
+                existing = existing.copy();
+                if( existing.getAmount() > limit ) existing.setAmount( limit );
+                return existing;
+            }
+            else if( existing.getAmount() < limit )
+            {
+                setAndDirty( slot, ItemStack.EMPTY );
+                return existing;
+            }
+            else
+            {
+                ItemStack result = existing.split( limit );
+                setAndDirty( slot, existing );
+                return result;
+            }
+        }
+
+        @Override
+        public @Nonnull
+        ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate )
+        {
+            if( stack.isEmpty() || !inventory.isValidInvStack( slot, stack ) ) return stack;
+
+            ItemStack existing = inventory.getInvStack( slot );
+            if( existing.isEmpty() )
+            {
+                int limit = Math.min( stack.getMaxAmount(), inventory.getInvMaxStackAmount() );
+                if( limit <= 0 ) return ItemStack.EMPTY;
+
+                if( stack.getAmount() < limit )
+                {
+                    if( !simulate ) setAndDirty( slot, stack );
+                    return ItemStack.EMPTY;
+                }
+                else
+                {
+                    stack = stack.copy();
+                    ItemStack insert = stack.split( limit );
+                    if( !simulate ) setAndDirty( slot, insert );
+                    return stack;
+                }
+            }
+            else if( areStackable( stack, existing ) )
+            {
+                int limit = Math.min( existing.getMaxAmount(), inventory.getInvMaxStackAmount() ) - stack.getAmount();
+                if( limit <= 0 ) return ItemStack.EMPTY;
+
+                if( stack.getAmount() < limit )
+                {
+                    if( !simulate )
+                    {
+                        existing.addAmount( stack.getAmount() );
+                        setAndDirty( slot, existing );
+                    }
+                    return ItemStack.EMPTY;
+                }
+                else
+                {
+                    stack = stack.copy();
+                    stack.subtractAmount( limit );
+                    if( !simulate )
+                    {
+                        existing.addAmount( limit );
+                        setAndDirty( slot, existing );
+                    }
+                    return stack;
+                }
+            }
+            else
+            {
+                return ItemStack.EMPTY;
+            }
+        }
+    }
+
+    class SidedInventoryWrapper extends InventoryWrapper
+    {
+        private final SidedInventory inventory;
+        private final Direction facing;
+
+        SidedInventoryWrapper( SidedInventory inventory, Direction facing )
+        {
+            super( inventory );
+            this.inventory = inventory;
+            this.facing = facing;
+        }
+
+        @Override
+        public int size()
+        {
+            return inventory.getInvAvailableSlots( facing ).length;
+        }
+
+        @Override
+        protected boolean canExtract( int slot, ItemStack stack )
+        {
+            return super.canExtract( slot, stack ) && inventory.canExtractInvStack( slot, stack, facing );
+        }
+
+        @Override
+        public @Nonnull
+        ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate )
+        {
+            int[] slots = inventory.getInvAvailableSlots( facing );
+            return slot >= 0 && slot < slots.length ? super.take( slots[slot], limit, filter, simulate ) : ItemStack.EMPTY;
+
+        }
+
+        @Override
+        public @Nonnull
+        ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate )
+        {
+            int[] slots = inventory.getInvAvailableSlots( facing );
+            if( slot < 0 || slot >= slots.length ) return ItemStack.EMPTY;
+
+            int mappedSlot = slots[slot];
+            if( !inventory.canInsertInvStack( slot, stack, facing ) ) return ItemStack.EMPTY;
+            return super.store( mappedSlot, stack, simulate );
+        }
+    }
+
+    class View implements ItemStorage
+    {
+        private final ItemStorage parent;
+        private final int start;
+        private final int size;
+
+        public View( ItemStorage parent, int start, int size )
+        {
+            this.parent = parent;
+            this.start = start;
+            this.size = size;
+        }
+
+        @Override
+        public int size()
+        {
+            return size;
+        }
+
+        @Override
+        public @Nonnull
+        ItemStack take( int slot, int limit, @Nonnull ItemStack filter, boolean simulate )
+        {
+            if( slot < start || slot >= start + size ) return ItemStack.EMPTY;
+            return parent.take( slot - start, limit, filter, simulate );
+        }
+
+        @Override
+        public @Nonnull
+        ItemStack store( int slot, @Nonnull ItemStack stack, boolean simulate )
+        {
+            if( slot < start || slot >= start + size ) return ItemStack.EMPTY;
+            return parent.store( slot - start, stack, simulate );
+        }
+
+        @Override
+        public ItemStorage view( int start, int size )
+        {
+            return new View( this.parent, this.start + start, size );
+        }
+    }
+
+    static ItemStorage wrap( Inventory inventory )
+    {
+        return new InventoryWrapper( inventory );
+    }
+
+    static ItemStorage wrap( @Nonnull SidedInventory inventory, @Nonnull Direction facing )
+    {
+        return new SidedInventoryWrapper( inventory, facing );
+    }
+
+    static ItemStorage wrap( @Nonnull Inventory inventory, @Nonnull Direction facing )
+    {
+        return inventory instanceof SidedInventory
+            ? new SidedInventoryWrapper( (SidedInventory) inventory, facing )
+            : new InventoryWrapper( inventory );
+    }
+
+    static boolean areStackable( @Nonnull ItemStack a, @Nonnull ItemStack b )
+    {
+        return a == b || (a.getItem() == b.getItem() && ItemStack.areTagsEqual( a, b ));
+    }
+}
diff --git a/src/main/java/dan200/computercraft/shared/util/NBTUtil.java b/src/main/java/dan200/computercraft/shared/util/NBTUtil.java
index 9a115002b8..556c34dcd4 100644
--- a/src/main/java/dan200/computercraft/shared/util/NBTUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/NBTUtil.java
@@ -7,32 +7,44 @@
 package dan200.computercraft.shared.util;
 
 import net.minecraft.nbt.*;
-import net.minecraftforge.common.util.Constants;
 
 import java.util.HashMap;
 import java.util.Map;
 
-import static net.minecraftforge.common.util.Constants.NBT.*;
-
 public final class NBTUtil
 {
+    public static final int TAG_END = 0;
+    public static final int TAG_BYTE = 1;
+    public static final int TAG_SHORT = 2;
+    public static final int TAG_INT = 3;
+    public static final int TAG_LONG = 4;
+    public static final int TAG_FLOAT = 5;
+    public static final int TAG_DOUBLE = 6;
+    public static final int TAG_BYTE_ARRAY = 7;
+    public static final int TAG_STRING = 8;
+    public static final int TAG_LIST = 9;
+    public static final int TAG_COMPOUND = 10;
+    public static final int TAG_INT_ARRAY = 11;
+    public static final int TAG_LONG_ARRAY = 12;
+    public static final int TAG_ANY_NUMERIC = 99;
+
     private NBTUtil() {}
 
-    private static INBTBase toNBTTag( Object object )
+    private static Tag toNBTTag( Object object )
     {
         if( object == null ) return null;
-        if( object instanceof Boolean ) return new NBTTagByte( (byte) ((boolean) (Boolean) object ? 1 : 0) );
-        if( object instanceof Number ) return new NBTTagDouble( ((Number) object).doubleValue() );
-        if( object instanceof String ) return new NBTTagString( object.toString() );
+        if( object instanceof Boolean ) return new ByteTag( (byte) ((boolean) (Boolean) object ? 1 : 0) );
+        if( object instanceof Number ) return new DoubleTag( ((Number) object).doubleValue() );
+        if( object instanceof String ) return new StringTag( object.toString() );
         if( object instanceof Map )
         {
             Map<?, ?> m = (Map<?, ?>) object;
-            NBTTagCompound nbt = new NBTTagCompound();
+            CompoundTag nbt = new CompoundTag();
             int i = 0;
             for( Map.Entry<?, ?> entry : m.entrySet() )
             {
-                INBTBase key = toNBTTag( entry.getKey() );
-                INBTBase value = toNBTTag( entry.getKey() );
+                Tag key = toNBTTag( entry.getKey() );
+                Tag value = toNBTTag( entry.getKey() );
                 if( key != null && value != null )
                 {
                     nbt.put( "k" + i, key );
@@ -47,41 +59,41 @@ private static INBTBase toNBTTag( Object object )
         return null;
     }
 
-    public static NBTTagCompound encodeObjects( Object[] objects )
+    public static CompoundTag encodeObjects( Object[] objects )
     {
         if( objects == null || objects.length <= 0 ) return null;
 
-        NBTTagCompound nbt = new NBTTagCompound();
+        CompoundTag nbt = new CompoundTag();
         nbt.putInt( "len", objects.length );
         for( int i = 0; i < objects.length; i++ )
         {
-            INBTBase child = toNBTTag( objects[i] );
+            Tag child = toNBTTag( objects[i] );
             if( child != null ) nbt.put( Integer.toString( i ), child );
         }
         return nbt;
     }
 
-    private static Object fromNBTTag( INBTBase tag )
+    private static Object fromNBTTag( Tag tag )
     {
         if( tag == null ) return null;
-        switch( tag.getId() )
+        switch( tag.getType() )
         {
             case TAG_BYTE:
-                return ((NBTTagByte) tag).getByte() > 0;
+                return ((ByteTag) tag).getByte() > 0;
             case TAG_DOUBLE:
-                return ((NBTTagDouble) tag).getDouble();
+                return ((DoubleTag) tag).getDouble();
             default:
             case TAG_STRING:
-                return tag.getString();
+                return tag.asString();
             case TAG_COMPOUND:
             {
-                NBTTagCompound c = (NBTTagCompound) tag;
+                CompoundTag c = (CompoundTag) tag;
                 int len = c.getInt( "len" );
                 Map<Object, Object> map = new HashMap<>( len );
                 for( int i = 0; i < len; i++ )
                 {
-                    Object key = fromNBTTag( c.get( "k" + i ) );
-                    Object value = fromNBTTag( c.get( "v" + i ) );
+                    Object key = fromNBTTag( c.getTag( "k" + i ) );
+                    Object value = fromNBTTag( c.getTag( "v" + i ) );
                     if( key != null && value != null ) map.put( key, value );
                 }
                 return map;
@@ -89,51 +101,51 @@ private static Object fromNBTTag( INBTBase tag )
         }
     }
 
-    public static Object toLua( INBTBase tag )
+    public static Object toLua( Tag tag )
     {
         if( tag == null ) return null;
 
-        byte typeID = tag.getId();
+        byte typeID = tag.getType();
         switch( typeID )
         {
-            case Constants.NBT.TAG_BYTE:
-            case Constants.NBT.TAG_SHORT:
-            case Constants.NBT.TAG_INT:
-            case Constants.NBT.TAG_LONG:
-                return ((NBTPrimitive) tag).getLong();
-            case Constants.NBT.TAG_FLOAT:
-            case Constants.NBT.TAG_DOUBLE:
-                return ((NBTPrimitive) tag).getDouble();
-            case Constants.NBT.TAG_STRING: // String
-                return tag.getString();
-            case Constants.NBT.TAG_COMPOUND: // Compound
+            case TAG_BYTE:
+            case TAG_SHORT:
+            case TAG_INT:
+            case TAG_LONG:
+                return ((AbstractNumberTag) tag).getLong();
+            case TAG_FLOAT:
+            case TAG_DOUBLE:
+                return ((AbstractNumberTag) tag).getDouble();
+            case TAG_STRING: // String
+                return tag.asString();
+            case TAG_COMPOUND: // Compound
             {
-                NBTTagCompound compound = (NBTTagCompound) tag;
-                Map<String, Object> map = new HashMap<>( compound.size() );
-                for( String key : compound.keySet() )
+                CompoundTag compound = (CompoundTag) tag;
+                Map<String, Object> map = new HashMap<>( compound.getSize() );
+                for( String key : compound.getKeys() )
                 {
-                    Object value = toLua( compound.get( key ) );
+                    Object value = toLua( compound.getTag( key ) );
                     if( value != null ) map.put( key, value );
                 }
                 return map;
             }
-            case Constants.NBT.TAG_LIST:
+            case TAG_LIST:
             {
-                NBTTagList list = (NBTTagList) tag;
+                ListTag list = (ListTag) tag;
                 Map<Integer, Object> map = new HashMap<>( list.size() );
                 for( int i = 0; i < list.size(); i++ ) map.put( i, toLua( list.get( i ) ) );
                 return map;
             }
-            case Constants.NBT.TAG_BYTE_ARRAY:
+            case TAG_BYTE_ARRAY:
             {
-                byte[] array = ((NBTTagByteArray) tag).getByteArray();
+                byte[] array = ((ByteArrayTag) tag).getByteArray();
                 Map<Integer, Byte> map = new HashMap<>( array.length );
                 for( int i = 0; i < array.length; i++ ) map.put( i + 1, array[i] );
                 return map;
             }
-            case Constants.NBT.TAG_INT_ARRAY:
+            case TAG_INT_ARRAY:
             {
-                int[] array = ((NBTTagIntArray) tag).getIntArray();
+                int[] array = ((IntArrayTag) tag).getIntArray();
                 Map<Integer, Integer> map = new HashMap<>( array.length );
                 for( int i = 0; i < array.length; i++ ) map.put( i + 1, array[i] );
                 return map;
@@ -144,7 +156,7 @@ public static Object toLua( INBTBase tag )
         }
     }
 
-    public static Object[] decodeObjects( NBTTagCompound tag )
+    public static Object[] decodeObjects( CompoundTag tag )
     {
         int len = tag.getInt( "len" );
         if( len <= 0 ) return null;
@@ -153,9 +165,9 @@ public static Object[] decodeObjects( NBTTagCompound tag )
         for( int i = 0; i < len; i++ )
         {
             String key = Integer.toString( i );
-            if( tag.contains( key ) )
+            if( tag.containsKey( key ) )
             {
-                objects[i] = fromNBTTag( tag.get( key ) );
+                objects[i] = fromNBTTag( tag.getTag( key ) );
             }
         }
         return objects;
diff --git a/src/main/java/dan200/computercraft/shared/util/NamedBlockEntityType.java b/src/main/java/dan200/computercraft/shared/util/NamedBlockEntityType.java
index db4c0caad1..95979406a2 100644
--- a/src/main/java/dan200/computercraft/shared/util/NamedBlockEntityType.java
+++ b/src/main/java/dan200/computercraft/shared/util/NamedBlockEntityType.java
@@ -9,66 +9,71 @@
 import com.mojang.datafixers.DataFixUtils;
 import com.mojang.datafixers.types.Type;
 import dan200.computercraft.ComputerCraft;
-import net.minecraft.tileentity.TileEntity;
-import net.minecraft.tileentity.TileEntityType;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.SharedConstants;
-import net.minecraft.util.datafix.DataFixesManager;
-import net.minecraft.util.datafix.TypeReferences;
+import net.minecraft.SharedConstants;
+import net.minecraft.block.entity.BlockEntity;
+import net.minecraft.block.entity.BlockEntityType;
+import net.minecraft.datafixers.Schemas;
+import net.minecraft.datafixers.TypeReferences;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.MutableRegistry;
 
 import java.util.function.Function;
 import java.util.function.Supplier;
 
-public final class NamedBlockEntityType<T extends TileEntity> extends TileEntityType<T>
+public class NamedBlockEntityType<T extends BlockEntity> extends BlockEntityType<T>
 {
-    private final ResourceLocation identifier;
+    private final Identifier identifier;
 
-    private NamedBlockEntityType( ResourceLocation identifier, Supplier<? extends T> supplier )
+    private NamedBlockEntityType( Identifier identifier, Supplier<? extends T> supplier )
     {
         super( supplier, getDatafixer( identifier ) );
         this.identifier = identifier;
-        setRegistryName( identifier );
     }
 
-    public static <T extends TileEntity> NamedBlockEntityType<T> create( ResourceLocation identifier, Supplier<? extends T> supplier )
+    public static <T extends BlockEntity> NamedBlockEntityType<T> create( Identifier identifier, Supplier<? extends T> supplier )
     {
         return new NamedBlockEntityType<>( identifier, supplier );
     }
 
-    public static <T extends TileEntity> NamedBlockEntityType<T> create( ResourceLocation identifier, Function<NamedBlockEntityType<T>, ? extends T> builder )
+    public static <T extends BlockEntity> NamedBlockEntityType<T> create( Identifier identifier, Function<NamedBlockEntityType<T>, ? extends T> builder )
     {
-        return new FixedPointSupplier<>( identifier, builder ).factory;
+        return new FixedPointSupplier<T>( identifier, builder ).factory;
     }
 
-    public ResourceLocation getId()
+    public Identifier getId()
     {
         return identifier;
     }
 
-    public static Type<?> getDatafixer( ResourceLocation id )
+    public void register( MutableRegistry<BlockEntityType<?>> registry )
+    {
+        registry.add( getId(), this );
+    }
+
+    public static Type<?> getDatafixer( Identifier id )
     {
         try
         {
-            return DataFixesManager.getDataFixer()
+            return Schemas.getFixer()
                 .getSchema( DataFixUtils.makeKey( ComputerCraft.DATAFIXER_VERSION ) )
                 .getChoiceType( TypeReferences.BLOCK_ENTITY, id.toString() );
         }
         catch( IllegalArgumentException e )
         {
-            if( SharedConstants.developmentMode ) throw e;
+            if( SharedConstants.isDevelopment ) throw e;
             ComputerCraft.log.warn( "No data fixer registered for block entity " + id );
             return null;
         }
     }
 
-    private static final class FixedPointSupplier<T extends TileEntity> implements Supplier<T>
+    private static class FixedPointSupplier<T extends BlockEntity> implements Supplier<T>
     {
         final NamedBlockEntityType<T> factory;
         private final Function<NamedBlockEntityType<T>, ? extends T> builder;
 
-        private FixedPointSupplier( ResourceLocation identifier, Function<NamedBlockEntityType<T>, ? extends T> builder )
+        private FixedPointSupplier( Identifier identifier, Function<NamedBlockEntityType<T>, ? extends T> builder )
         {
-            factory = create( identifier, this );
+            this.factory = create( identifier, this );
             this.builder = builder;
         }
 
diff --git a/src/main/java/dan200/computercraft/shared/util/Palette.java b/src/main/java/dan200/computercraft/shared/util/Palette.java
index 81d47f50bd..856df6aa64 100644
--- a/src/main/java/dan200/computercraft/shared/util/Palette.java
+++ b/src/main/java/dan200/computercraft/shared/util/Palette.java
@@ -6,7 +6,7 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.CompoundTag;
 
 public class Palette
 {
@@ -80,7 +80,7 @@ public static double[] decodeRGB8( int rgb )
             };
     }
 
-    public NBTTagCompound writeToNBT( NBTTagCompound nbt )
+    public CompoundTag writeToNBT( CompoundTag nbt )
     {
         int[] rgb8 = new int[colours.length];
 
@@ -93,9 +93,9 @@ public NBTTagCompound writeToNBT( NBTTagCompound nbt )
         return nbt;
     }
 
-    public void readFromNBT( NBTTagCompound nbt )
+    public void readFromNBT( CompoundTag nbt )
     {
-        if( !nbt.contains( "term_palette" ) ) return;
+        if( !nbt.containsKey( "term_palette" ) ) return;
         int[] rgb8 = nbt.getIntArray( "term_palette" );
 
         if( rgb8.length != colours.length ) return;
diff --git a/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java b/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java
index 332c9e1619..77319a31c8 100644
--- a/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/RecipeUtil.java
@@ -10,15 +10,13 @@
 import com.google.common.collect.Sets;
 import com.google.gson.*;
 import dan200.computercraft.shared.computer.core.ComputerFamily;
-import net.minecraft.item.crafting.Ingredient;
-import net.minecraft.util.JsonUtils;
-import net.minecraft.util.NonNullList;
+import net.minecraft.recipe.Ingredient;
+import net.minecraft.util.DefaultedList;
+import net.minecraft.util.JsonHelper;
 
 import java.util.Map;
 import java.util.Set;
 
-// TODO: Replace some things with Forge??
-
 public final class RecipeUtil
 {
     private RecipeUtil() {}
@@ -27,9 +25,9 @@ public static class ShapedTemplate
     {
         public final int width;
         public final int height;
-        public final NonNullList<Ingredient> ingredients;
+        public final DefaultedList<Ingredient> ingredients;
 
-        public ShapedTemplate( int width, int height, NonNullList<Ingredient> ingredients )
+        public ShapedTemplate( int width, int height, DefaultedList<Ingredient> ingredients )
         {
             this.width = width;
             this.height = height;
@@ -40,7 +38,7 @@ public ShapedTemplate( int width, int height, NonNullList<Ingredient> ingredient
     public static ShapedTemplate getTemplate( JsonObject json )
     {
         Map<Character, Ingredient> ingMap = Maps.newHashMap();
-        for( Map.Entry<String, JsonElement> entry : JsonUtils.getJsonObject( json, "key" ).entrySet() )
+        for( Map.Entry<String, JsonElement> entry : JsonHelper.getObject( json, "key" ).entrySet() )
         {
             if( entry.getKey().length() != 1 )
             {
@@ -51,12 +49,12 @@ public static ShapedTemplate getTemplate( JsonObject json )
                 throw new JsonSyntaxException( "Invalid key entry: ' ' is a reserved symbol." );
             }
 
-            ingMap.put( entry.getKey().charAt( 0 ), Ingredient.deserialize( entry.getValue() ) );
+            ingMap.put( entry.getKey().charAt( 0 ), Ingredient.fromJson( entry.getValue() ) );
         }
 
         ingMap.put( ' ', Ingredient.EMPTY );
 
-        JsonArray patternJ = JsonUtils.getJsonArray( json, "pattern" );
+        JsonArray patternJ = JsonHelper.getArray( json, "pattern" );
 
         if( patternJ.size() == 0 )
         {
@@ -66,7 +64,7 @@ public static ShapedTemplate getTemplate( JsonObject json )
         String[] pattern = new String[patternJ.size()];
         for( int x = 0; x < pattern.length; x++ )
         {
-            String line = JsonUtils.getString( patternJ.get( x ), "pattern[" + x + "]" );
+            String line = JsonHelper.asString( patternJ.get( x ), "pattern[" + x + "]" );
             if( x > 0 && pattern[0].length() != line.length() )
             {
                 throw new JsonSyntaxException( "Invalid pattern: each row must  be the same width" );
@@ -76,7 +74,7 @@ public static ShapedTemplate getTemplate( JsonObject json )
 
         int width = pattern[0].length();
         int height = pattern.length;
-        NonNullList<Ingredient> ingredients = NonNullList.withSize( width * height, Ingredient.EMPTY );
+        DefaultedList<Ingredient> ingredients = DefaultedList.create( width * height, Ingredient.EMPTY );
 
         Set<Character> missingKeys = Sets.newHashSet( ingMap.keySet() );
         missingKeys.remove( ' ' );
@@ -104,12 +102,12 @@ public static ShapedTemplate getTemplate( JsonObject json )
         return new ShapedTemplate( width, height, ingredients );
     }
 
-    public static NonNullList<Ingredient> getIngredients( JsonObject json )
+    public static DefaultedList<Ingredient> getIngredients( JsonObject json )
     {
-        NonNullList<Ingredient> ingredients = NonNullList.create();
-        for( JsonElement ele : JsonUtils.getJsonArray( json, "ingredients" ) )
+        DefaultedList<Ingredient> ingredients = DefaultedList.create();
+        for( JsonElement ele : JsonHelper.getArray( json, "ingredients" ) )
         {
-            ingredients.add( Ingredient.deserialize( ele ) );
+            ingredients.add( Ingredient.fromJson( ele ) );
         }
 
         if( ingredients.isEmpty() ) throw new JsonParseException( "No ingredients for recipe" );
@@ -118,7 +116,7 @@ public static NonNullList<Ingredient> getIngredients( JsonObject json )
 
     public static ComputerFamily getFamily( JsonObject json, String name )
     {
-        String familyName = JsonUtils.getString( json, name );
+        String familyName = JsonHelper.getString( json, name );
         try
         {
             return ComputerFamily.valueOf( familyName );
diff --git a/src/main/java/dan200/computercraft/shared/util/RecordUtil.java b/src/main/java/dan200/computercraft/shared/util/RecordUtil.java
index f5ff089e97..0e36d2fb84 100644
--- a/src/main/java/dan200/computercraft/shared/util/RecordUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/RecordUtil.java
@@ -10,12 +10,12 @@
 import dan200.computercraft.shared.network.NetworkMessage;
 import dan200.computercraft.shared.network.client.PlayRecordClientMessage;
 import net.minecraft.item.Item;
-import net.minecraft.item.ItemRecord;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.SoundEvent;
+import net.minecraft.item.MusicDiscItem;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.text.TranslatableTextComponent;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.text.TextComponentTranslation;
 import net.minecraft.world.World;
 
 import javax.annotation.Nonnull;
@@ -33,8 +33,8 @@ public static void playRecord( SoundEvent record, String recordInfo, World world
     public static String getRecordInfo( @Nonnull ItemStack recordStack )
     {
         Item item = recordStack.getItem();
-        if( !(item instanceof ItemRecord) ) return null;
+        if( !(item instanceof MusicDiscItem) ) return null;
 
-        return new TextComponentTranslation( item.getTranslationKey() + ".desc" ).getString();
+        return new TranslatableTextComponent( item.getTranslationKey() + ".desc" ).getString();
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java b/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java
index 90ea40e912..36b8547460 100644
--- a/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/RedstoneUtil.java
@@ -6,25 +6,25 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.BlockState;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.World;
 
 public final class RedstoneUtil
 {
-    public static void propagateRedstoneOutput( World world, BlockPos pos, EnumFacing side )
+    public static void propagateRedstoneOutput( World world, BlockPos pos, Direction side )
     {
         // Propagate ordinary output
-        IBlockState block = world.getBlockState( pos );
+        BlockState block = world.getBlockState( pos );
         BlockPos neighbourPos = pos.offset( side );
-        IBlockState neighbour = world.getBlockState( neighbourPos );
-        if( !neighbour.isAir( world, pos ) )
+        BlockState neighbour = world.getBlockState( neighbourPos );
+        if( !neighbour.isAir() )
         {
-            world.neighborChanged( neighbourPos, block.getBlock(), pos );
-            if( neighbour.getBlock().isNormalCube( neighbour, world, neighbourPos ) )
+            world.updateNeighbor( neighbourPos, block.getBlock(), pos );
+            if( neighbour.isSimpleFullBlock( world, neighbourPos ) )
             {
-                world.notifyNeighborsOfStateExcept( neighbourPos, block.getBlock(), side.getOpposite() );
+                world.updateNeighborsExcept( neighbourPos, block.getBlock(), side.getOpposite() );
             }
         }
     }
diff --git a/src/main/java/dan200/computercraft/shared/util/TickScheduler.java b/src/main/java/dan200/computercraft/shared/util/TickScheduler.java
index 6a367da987..4db62d631f 100644
--- a/src/main/java/dan200/computercraft/shared/util/TickScheduler.java
+++ b/src/main/java/dan200/computercraft/shared/util/TickScheduler.java
@@ -7,33 +7,27 @@
 package dan200.computercraft.shared.util;
 
 import com.google.common.collect.MapMaker;
-import dan200.computercraft.ComputerCraft;
 import dan200.computercraft.shared.common.TileGeneric;
-import net.minecraft.tileentity.TileEntity;
+import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.ITickList;
 import net.minecraft.world.World;
-import net.minecraftforge.eventbus.api.SubscribeEvent;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.common.gameevent.TickEvent;
 
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.Set;
 
 /**
- * A thread-safe version of {@link ITickList#scheduleTick(BlockPos, Object, int)}.
+ * A thread-safe version of {@link net.minecraft.world.TickScheduler#schedule(BlockPos, Object, int)}.
  *
  * We use this when modems and other peripherals change a block in a different thread.
  */
-@Mod.EventBusSubscriber( modid = ComputerCraft.MOD_ID )
 public final class TickScheduler
 {
     private TickScheduler()
     {
     }
 
-    private static final Set<TileEntity> toTick = Collections.newSetFromMap(
+    private static final Set<BlockEntity> toTick = Collections.newSetFromMap(
         new MapMaker()
             .weakKeys()
             .makeMap()
@@ -42,26 +36,23 @@ private TickScheduler()
     public static void schedule( TileGeneric tile )
     {
         World world = tile.getWorld();
-        if( world != null && !world.isRemote ) toTick.add( tile );
+        if( world != null && !world.isClient ) toTick.add( tile );
     }
 
-    @SubscribeEvent
-    public static void tick( TickEvent.ServerTickEvent event )
+    public static void tick()
     {
-        if( event.phase != TickEvent.Phase.START ) return;
-
-        Iterator<TileEntity> iterator = toTick.iterator();
+        Iterator<BlockEntity> iterator = toTick.iterator();
         while( iterator.hasNext() )
         {
-            TileEntity tile = iterator.next();
+            BlockEntity tile = iterator.next();
             iterator.remove();
 
             World world = tile.getWorld();
             BlockPos pos = tile.getPos();
 
-            if( world != null && pos != null && world.isBlockLoaded( pos ) && world.getTileEntity( pos ) == tile )
+            if( world != null && pos != null && world.isBlockLoaded( pos ) && world.getBlockEntity( pos ) == tile )
             {
-                world.getPendingBlockTicks().scheduleTick( pos, tile.getBlockState().getBlock(), 0 );
+                world.getBlockTickScheduler().schedule( pos, tile.getCachedState().getBlock(), 0 );
             }
         }
     }
diff --git a/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java b/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java
index eaed69114a..b2cc46918b 100644
--- a/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java
+++ b/src/main/java/dan200/computercraft/shared/util/ValidatingSlot.java
@@ -6,20 +6,23 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.Slot;
+import net.minecraft.container.Slot;
+import net.minecraft.inventory.Inventory;
 import net.minecraft.item.ItemStack;
 
 public class ValidatingSlot extends Slot
 {
-    public ValidatingSlot( IInventory inventoryIn, int index, int xPosition, int yPosition )
+    private final int invSlot;
+
+    public ValidatingSlot( Inventory inventoryIn, int index, int xPosition, int yPosition )
     {
         super( inventoryIn, index, xPosition, yPosition );
+        this.invSlot = index;
     }
 
     @Override
-    public boolean isItemValid( ItemStack stack )
+    public boolean canInsert( ItemStack stack )
     {
-        return true; // inventory.isItemValidForSlot( slotNumber, stack );
+        return inventory.isValidInvStack( invSlot, stack );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/WaterloggableBlock.java b/src/main/java/dan200/computercraft/shared/util/WaterloggableBlock.java
index 4c0f593c56..796b7195ff 100644
--- a/src/main/java/dan200/computercraft/shared/util/WaterloggableBlock.java
+++ b/src/main/java/dan200/computercraft/shared/util/WaterloggableBlock.java
@@ -6,94 +6,54 @@
 
 package dan200.computercraft.shared.util;
 
-import net.minecraft.block.IBucketPickupHandler;
-import net.minecraft.block.ILiquidContainer;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.fluid.Fluid;
-import net.minecraft.fluid.IFluidState;
-import net.minecraft.init.Fluids;
-import net.minecraft.item.BlockItemUseContext;
-import net.minecraft.state.BooleanProperty;
-import net.minecraft.state.properties.BlockStateProperties;
-import net.minecraft.util.EnumFacing;
+import net.minecraft.block.BlockState;
+import net.minecraft.block.Waterloggable;
+import net.minecraft.fluid.FluidState;
+import net.minecraft.fluid.Fluids;
+import net.minecraft.item.ItemPlacementContext;
+import net.minecraft.state.property.BooleanProperty;
+import net.minecraft.state.property.Properties;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockReader;
+import net.minecraft.util.math.Direction;
 import net.minecraft.world.IWorld;
 
-import javax.annotation.Nonnull;
-
 /**
  * Represents a block which can be filled with water
  *
  * I'm fairly sure this exists on 1.14, but it's a useful convenience wrapper to have on 1.13.
  */
-public interface WaterloggableBlock extends IBucketPickupHandler, ILiquidContainer
+public interface WaterloggableBlock extends Waterloggable
 {
-    BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED;
+    BooleanProperty WATERLOGGED = Properties.WATERLOGGED;
 
     /**
-     * Call from {@link net.minecraft.block.Block#getFluidState(IBlockState)}
+     * Call from {@link net.minecraft.block.Block#getFluidState(BlockState)}
      *
      * @param state The current state
      * @return This waterlogged block's current fluid
      */
-    default IFluidState getWaterloggedFluidState( IBlockState state )
+    default FluidState getWaterloggedFluidState( BlockState state )
     {
-        return state.get( WATERLOGGED ) ? Fluids.WATER.getStillFluidState( false ) : Fluids.EMPTY.getDefaultState();
-    }
-
-    @Nonnull
-    @Override
-    default Fluid pickupFluid( @Nonnull IWorld world, @Nonnull BlockPos pos, @Nonnull IBlockState state )
-    {
-        if( state.get( WATERLOGGED ) )
-        {
-            world.setBlockState( pos, state.with( WATERLOGGED, false ), 3 );
-            return Fluids.WATER;
-        }
-        else
-        {
-            return Fluids.EMPTY;
-        }
-    }
-
-    @Override
-    default boolean canContainFluid( @Nonnull IBlockReader world, @Nonnull BlockPos pos, @Nonnull IBlockState state, @Nonnull Fluid fluid )
-    {
-        return !state.get( WATERLOGGED ) && fluid == Fluids.WATER;
-    }
-
-    @Override
-    default boolean receiveFluid( @Nonnull IWorld world, @Nonnull BlockPos pos, @Nonnull IBlockState state, @Nonnull IFluidState fluid )
-    {
-        if( !canContainFluid( world, pos, state, fluid.getFluid() ) ) return false;
-
-        if( !world.isRemote() )
-        {
-            world.setBlockState( pos, state.with( WATERLOGGED, true ), 3 );
-            world.getPendingFluidTicks().scheduleTick( pos, fluid.getFluid(), fluid.getFluid().getTickRate( world ) );
-        }
-
-        return true;
+        return state.get( WATERLOGGED ) ? Fluids.WATER.getState( false ) : Fluids.EMPTY.getDefaultState();
     }
 
     /**
-     * Call from {@link net.minecraft.block.Block#updatePostPlacement(IBlockState, EnumFacing, IBlockState, IWorld, BlockPos, BlockPos)}
+     * Call from {@link net.minecraft.block.Block#getStateForNeighborUpdate(BlockState, Direction, BlockState, IWorld, BlockPos, BlockPos)}
      *
      * @param state The current state
      * @param world The position of this block
      * @param pos   The world this block exists in
      */
-    default void updateWaterloggedPostPlacement( IBlockState state, IWorld world, BlockPos pos )
+    default void updateWaterloggedPostPlacement( BlockState state, IWorld world, BlockPos pos )
     {
         if( state.get( WATERLOGGED ) )
         {
-            world.getPendingFluidTicks().scheduleTick( pos, Fluids.WATER, Fluids.WATER.getTickRate( world ) );
+            world.getFluidTickScheduler().schedule( pos, Fluids.WATER, Fluids.WATER.getTickRate( world ) );
         }
     }
 
-    default boolean getWaterloggedStateForPlacement( BlockItemUseContext context )
+    default boolean getWaterloggedStateForPlacement( ItemPlacementContext context )
     {
-        return context.getWorld().getFluidState( context.getPos() ).getFluid() == Fluids.WATER;
+        return context.getWorld().getFluidState( context.getBlockPos() ).getFluid() == Fluids.WATER;
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java
index ecf7da47e0..49d8a098f0 100644
--- a/src/main/java/dan200/computercraft/shared/util/WorldUtil.java
+++ b/src/main/java/dan200/computercraft/shared/util/WorldUtil.java
@@ -7,18 +7,19 @@
 package dan200.computercraft.shared.util;
 
 import com.google.common.base.Predicate;
-import net.minecraft.block.state.IBlockState;
+import net.minecraft.block.BlockState;
 import net.minecraft.entity.Entity;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.item.EntityItem;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.item.ItemStack;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.util.hit.HitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
+import net.minecraft.util.math.BoundingBox;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
-import net.minecraft.util.math.shapes.VoxelShape;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.world.RayTraceContext;
 import net.minecraft.world.World;
 import org.apache.commons.lang3.tuple.Pair;
 
@@ -28,12 +29,12 @@
 public final class WorldUtil
 {
     @SuppressWarnings( "Guava" )
-    private static final Predicate<Entity> CAN_COLLIDE = x -> x != null && x.isAlive() && x.canBeCollidedWith();
+    private static final Predicate<Entity> CAN_COLLIDE = x -> x != null && x.isAlive() && x.doesCollide();
 
     public static boolean isLiquidBlock( World world, BlockPos pos )
     {
         if( !World.isValid( pos ) ) return false;
-        IBlockState state = world.getBlockState( pos );
+        BlockState state = world.getBlockState( pos );
         return !state.getFluidState().isEmpty();
     }
 
@@ -41,7 +42,7 @@ public static boolean isVecInside( VoxelShape shape, Vec3d vec )
     {
         if( shape.isEmpty() ) return false;
         // return shape.contains( pos.x, pos.y, pos.z );
-        AxisAlignedBB bb = shape.getBoundingBox();
+        BoundingBox bb = shape.getBoundingBox();
         return vec.x >= bb.minX && vec.x <= bb.maxX && vec.y >= bb.minY && vec.y <= bb.maxY && vec.z >= bb.minZ && vec.z <= bb.maxZ;
     }
 
@@ -50,10 +51,10 @@ public static Pair<Entity, Vec3d> rayTraceEntities( World world, Vec3d vecStart,
         Vec3d vecEnd = vecStart.add( vecDir.x * distance, vecDir.y * distance, vecDir.z * distance );
 
         // Raycast for blocks
-        RayTraceResult result = world.rayTraceBlocks( vecStart, vecEnd );
-        if( result != null && result.type == RayTraceResult.Type.BLOCK )
+        HitResult result = world.rayTrace( new RayTraceContext( vecStart, vecEnd, RayTraceContext.ShapeType.OUTLINE, RayTraceContext.FluidHandling.NONE, null ) );
+        if( result != null && result.getType() == HitResult.Type.BLOCK )
         {
-            distance = vecStart.distanceTo( result.hitVec );
+            distance = vecStart.distanceTo( result.getPos() );
             vecEnd = vecStart.add( vecDir.x * distance, vecDir.y * distance, vecDir.z * distance );
         }
 
@@ -61,7 +62,7 @@ public static Pair<Entity, Vec3d> rayTraceEntities( World world, Vec3d vecStart,
         float xStretch = Math.abs( vecDir.x ) > 0.25f ? 0.0f : 1.0f;
         float yStretch = Math.abs( vecDir.y ) > 0.25f ? 0.0f : 1.0f;
         float zStretch = Math.abs( vecDir.z ) > 0.25f ? 0.0f : 1.0f;
-        AxisAlignedBB bigBox = new AxisAlignedBB(
+        BoundingBox bigBox = new BoundingBox(
             Math.min( vecStart.x, vecEnd.x ) - 0.375f * xStretch,
             Math.min( vecStart.y, vecEnd.y ) - 0.375f * yStretch,
             Math.min( vecStart.z, vecEnd.z ) - 0.375f * zStretch,
@@ -72,18 +73,10 @@ public static Pair<Entity, Vec3d> rayTraceEntities( World world, Vec3d vecStart,
 
         Entity closest = null;
         double closestDist = 99.0;
-        List<Entity> list = world.getEntitiesWithinAABB( Entity.class, bigBox, CAN_COLLIDE );
+        List<Entity> list = world.getEntitiesInBox( Entity.class, bigBox, CAN_COLLIDE );
         for( Entity entity : list )
         {
-            AxisAlignedBB littleBox = entity.getBoundingBox();
-            if( littleBox == null )
-            {
-                littleBox = entity.getCollisionBoundingBox();
-                if( littleBox == null )
-                {
-                    continue;
-                }
-            }
+            BoundingBox littleBox = entity.getBoundingBox();
 
             if( littleBox.contains( vecStart ) )
             {
@@ -92,10 +85,10 @@ public static Pair<Entity, Vec3d> rayTraceEntities( World world, Vec3d vecStart,
                 continue;
             }
 
-            RayTraceResult littleBoxResult = littleBox.calculateIntercept( vecStart, vecEnd );
+            Vec3d littleBoxResult = littleBox.rayTrace( vecStart, vecEnd ).orElse( null );
             if( littleBoxResult != null )
             {
-                double dist = vecStart.distanceTo( littleBoxResult.hitVec );
+                double dist = vecStart.distanceTo( littleBoxResult );
                 if( closest == null || dist <= closestDist )
                 {
                     closest = entity;
@@ -119,15 +112,15 @@ else if( littleBox.intersects( bigBox ) )
         return null;
     }
 
-    public static Vec3d getRayStart( EntityLivingBase entity )
+    public static Vec3d getRayStart( LivingEntity entity )
     {
-        return entity.getEyePosition( 1 );
+        return entity.getCameraPosVec( 1 );
     }
 
-    public static Vec3d getRayEnd( EntityPlayer player )
+    public static Vec3d getRayEnd( PlayerEntity player )
     {
-        double reach = player.getAttribute( EntityPlayer.REACH_DISTANCE ).getValue();
-        Vec3d look = player.getLookVec();
+        double reach = 5; // TODO: player.getAttributeInstance( PlayerEntity.REACH_DISTANCE ).getAttributeValue();
+        Vec3d look = player.getRotationVec( 1 );
         return getRayStart( player ).add( look.x * reach, look.y * reach, look.z * reach );
     }
 
@@ -136,16 +129,16 @@ public static void dropItemStack( @Nonnull ItemStack stack, World world, BlockPo
         dropItemStack( stack, world, pos, null );
     }
 
-    public static void dropItemStack( @Nonnull ItemStack stack, World world, BlockPos pos, EnumFacing direction )
+    public static void dropItemStack( @Nonnull ItemStack stack, World world, BlockPos pos, Direction direction )
     {
         double xDir;
         double yDir;
         double zDir;
         if( direction != null )
         {
-            xDir = direction.getXOffset();
-            yDir = direction.getYOffset();
-            zDir = direction.getZOffset();
+            xDir = direction.getOffsetX();
+            yDir = direction.getOffsetY();
+            zDir = direction.getOffsetZ();
         }
         else
         {
@@ -167,11 +160,13 @@ public static void dropItemStack( @Nonnull ItemStack stack, World world, double
 
     public static void dropItemStack( @Nonnull ItemStack stack, World world, double xPos, double yPos, double zPos, double xDir, double yDir, double zDir )
     {
-        EntityItem item = new EntityItem( world, xPos, yPos, zPos, stack.copy() );
-        item.motionX = xDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1;
-        item.motionY = yDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1;
-        item.motionZ = zDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1;
-        item.setDefaultPickupDelay();
+        ItemEntity item = new ItemEntity( world, xPos, yPos, zPos, stack.copy() );
+        item.setVelocity(
+            xDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1,
+            yDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1,
+            zDir * 0.7 + world.getRandom().nextFloat() * 0.2 - 0.1
+        );
+        item.resetPickupDelay();
         world.spawnEntity( item );
     }
 }
diff --git a/src/main/java/dan200/computercraft/shared/wired/CapabilityWiredElement.java b/src/main/java/dan200/computercraft/shared/wired/CapabilityWiredElement.java
index d484f94bb1..78041f683d 100644
--- a/src/main/java/dan200/computercraft/shared/wired/CapabilityWiredElement.java
+++ b/src/main/java/dan200/computercraft/shared/wired/CapabilityWiredElement.java
@@ -6,22 +6,9 @@
 
 package dan200.computercraft.shared.wired;
 
-import dan200.computercraft.api.network.wired.IWiredElement;
-import dan200.computercraft.api.network.wired.IWiredNode;
-import net.minecraft.nbt.INBTBase;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.math.Vec3d;
-import net.minecraft.world.World;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.capabilities.CapabilityInject;
-import net.minecraftforge.common.capabilities.CapabilityManager;
-import net.minecraftforge.common.util.LazyOptional;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
 public final class CapabilityWiredElement
 {
+    /*
     @CapabilityInject( IWiredElement.class )
     public static Capability<IWiredElement> CAPABILITY = null;
 
@@ -66,13 +53,13 @@ public String getSenderID()
     private static class NullStorage implements Capability.IStorage<IWiredElement>
     {
         @Override
-        public INBTBase writeNBT( Capability<IWiredElement> capability, IWiredElement instance, EnumFacing side )
+        public INBTBase writeNBT( Capability<IWiredElement> capability, IWiredElement instance, Direction side )
         {
             return null;
         }
 
         @Override
-        public void readNBT( Capability<IWiredElement> capability, IWiredElement instance, EnumFacing side, INBTBase base )
+        public void readNBT( Capability<IWiredElement> capability, IWiredElement instance, Direction side, INBTBase base )
         {
         }
     }
@@ -85,4 +72,5 @@ public static IWiredElement unwrap( LazyOptional<IWiredElement> capability )
         IWiredElement element = capability.orElse( NULL_ELEMENT );
         return element == NULL_ELEMENT ? null : element;
     }
+    */
 }
diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg
deleted file mode 100644
index 11a40d2cb2..0000000000
--- a/src/main/resources/META-INF/accesstransformer.cfg
+++ /dev/null
@@ -1,23 +0,0 @@
-# ItemPocketRenderer/ItemPrintoutRenderer
-public net.minecraft.client.renderer.FirstPersonRenderer func_187466_c()V # renderArms
-public net.minecraft.client.renderer.FirstPersonRenderer func_178100_c(F)F # getMapAngleFromPitch
-public net.minecraft.client.renderer.FirstPersonRenderer func_187456_a(FFLnet/minecraft/util/EnumHandSide;)V # renderArmFirstPerson
-
-# JEI ATs
-public net.minecraft.client.renderer.ItemRenderer func_191965_a(Lnet/minecraft/client/renderer/model/IBakedModel;I)V # renderModel
-public net.minecraft.client.renderer.ItemRenderer func_191961_a(Lnet/minecraft/client/renderer/model/IBakedModel;Lnet/minecraft/item/ItemStack;)V # renderModel
-protected net.minecraft.client.renderer.texture.TextureMap field_94252_e # mapUploadedSprites
-protected net.minecraft.client.renderer.texture.TextureMap field_94258_i # listAnimatedSprites
-protected net.minecraft.client.renderer.texture.TextureMap field_94249_f # missingImage
-protected net.minecraft.client.renderer.texture.TextureMap field_94254_c # basePath
-protected net.minecraft.client.renderer.texture.TextureMap field_195427_i # sprites
-protected net.minecraft.client.renderer.texture.TextureMap func_195422_a(Lnet/minecraft/resources/IResourceManager;Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;)Z # loadSprite
-public net.minecraft.client.renderer.texture.TextureAtlasSprite <init>(Lnet/minecraft/util/ResourceLocation;Lnet/minecraft/client/renderer/texture/PngSizeInfo;Lnet/minecraft/client/resources/data/AnimationMetadataSection;)V # constructor
-public net.minecraft.client.renderer.texture.TextureAtlasSprite field_110975_c # x
-public net.minecraft.client.renderer.texture.TextureAtlasSprite field_110974_d # y
-public net.minecraft.client.KeyboardListener field_197973_b # repeatEventsEnabled
-public net.minecraft.client.renderer.texture.TextureAtlasSprite field_195670_c # frames
-public net.minecraft.client.gui.recipebook.GuiRecipeBook field_191904_o # width
-public net.minecraft.client.gui.recipebook.GuiRecipeBook field_191905_p # height
-public net.minecraft.client.gui.recipebook.GuiRecipeBook field_191903_n # xOffset
-public net.minecraft.client.gui.recipebook.GuiRecipeBook field_193018_j # recipeTabs
diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml
deleted file mode 100644
index 5d5bffd89d..0000000000
--- a/src/main/resources/META-INF/mods.toml
+++ /dev/null
@@ -1,25 +0,0 @@
-modLoader="javafml"
-loaderVersion="[25,)"
-
-# updateJSONURL="http://myurl.me/" #optional
-issueTrackerURL="https://github.com/SquidDev-CC/CC-Tweaked/issues"
-displayURL="https://github.com/SquidDev-CC/CC-Tweaked"
-logoFile="pack.png"
-
-credits="Created by Daniel Ratcliffe (@DanTwoHundred)"
-authors="Daniel Ratcliffe, Aaron Mills, SquidDev"
-
-[[mods]]
-modId="computercraft"
-version="${version}"
-displayName="CC: Tweaked"
-description='''
-CC: Tweaked is a fork of ComputerCraft, adding programmable computers, turtles and more to Minecraft.
-'''
-
-[[dependencies.computercraft]]
-    modId="forge"
-    mandatory=true
-    versionRange="[25,)"
-    ordering="NONE"
-    side="BOTH"
diff --git a/src/main/resources/assets/computercraft/lang/en_us.json b/src/main/resources/assets/computercraft/lang/en_us.json
index 4c90acf02c..049ad63755 100644
--- a/src/main/resources/assets/computercraft/lang/en_us.json
+++ b/src/main/resources/assets/computercraft/lang/en_us.json
@@ -1,5 +1,5 @@
 {
-    "itemGroup.computercraft": "ComputerCraft",
+    "itemGroup.computercraft.main": "ComputerCraft",
 
     "block.computercraft.computer_normal": "Computer",
     "block.computercraft.computer_advanced": "Advanced Computer",
diff --git a/src/main/resources/computercraft.client.json b/src/main/resources/computercraft.client.json
new file mode 100644
index 0000000000..f0679bc6fb
--- /dev/null
+++ b/src/main/resources/computercraft.client.json
@@ -0,0 +1,14 @@
+{
+    "required": true,
+    "package": "dan200.computercraft.shared.mixin",
+    "compatibilityLevel": "JAVA_8",
+    "mixins": [
+        "MixinFirstPersonRenderer",
+        "MixinItemFrameEntityRenderer",
+        "MixinMinecraftGame",
+        "MixinScreen"
+    ],
+    "injectors": {
+        "defaultRequire": 1
+    }
+}
diff --git a/src/main/resources/data/computercraft/advancements/recipes/advanced_computer.json b/src/main/resources/data/computercraft/advancements/recipes/advanced_computer.json
index fa3783c51f..b4e8147309 100644
--- a/src/main/resources/data/computercraft/advancements/recipes/advanced_computer.json
+++ b/src/main/resources/data/computercraft/advancements/recipes/advanced_computer.json
@@ -7,7 +7,7 @@
         "has_redstone": {
             "trigger": "minecraft:inventory_changed",
             "conditions": {
-                "items": [ { "tag": "forge:dusts/redstone" } ]
+                "items": [ { "tag": "minecraft:redstone" } ]
             }
         },
         "has_the_recipe": {
diff --git a/src/main/resources/data/computercraft/advancements/recipes/normal_computer.json b/src/main/resources/data/computercraft/advancements/recipes/normal_computer.json
index 1ad34028d1..b8ef77055c 100644
--- a/src/main/resources/data/computercraft/advancements/recipes/normal_computer.json
+++ b/src/main/resources/data/computercraft/advancements/recipes/normal_computer.json
@@ -7,7 +7,7 @@
         "has_redstone": {
             "trigger": "minecraft:inventory_changed",
             "conditions": {
-                "items": [ { "tag": "forge:dusts/redstone" } ]
+                "items": [ { "item": "minecraft:redstone" } ]
             }
         },
         "has_the_recipe": {
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/cable.json b/src/main/resources/data/computercraft/loot_tables/blocks/cable.json
new file mode 100644
index 0000000000..464c823b50
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/cable.json
@@ -0,0 +1,36 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:cable" }
+            ],
+            "conditions": [
+                { "condition": "minecraft:survives_explosion" },
+                {
+                    "condition": "minecraft:block_state_property",
+                    "block": "computercraft:cable",
+                    "properties": { "cable": "true" }
+                }
+            ]
+        },
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:wired_modem" }
+            ],
+            "conditions": [
+                { "condition": "minecraft:survives_explosion" },
+                {
+                    "condition": "minecraft:inverted",
+                    "term": {
+                        "condition": "minecraft:block_state_property",
+                        "block": "computercraft:cable",
+                        "properties": { "modem": "none" }
+                    }
+                }
+            ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/computer_advanced.json b/src/main/resources/data/computercraft/loot_tables/blocks/computer_advanced.json
new file mode 100644
index 0000000000..7e55bfbab8
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/computer_advanced.json
@@ -0,0 +1,11 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:dynamic", "name": "computercraft:computer" }
+            ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json b/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json
new file mode 100644
index 0000000000..7e55bfbab8
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/computer_command.json
@@ -0,0 +1,11 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:dynamic", "name": "computercraft:computer" }
+            ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/computer_normal.json b/src/main/resources/data/computercraft/loot_tables/blocks/computer_normal.json
new file mode 100644
index 0000000000..888d1a5516
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/computer_normal.json
@@ -0,0 +1,9 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:dynamic", "name": "computercraft:computer" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/disk_drive.json b/src/main/resources/data/computercraft/loot_tables/blocks/disk_drive.json
new file mode 100644
index 0000000000..0cdc589b63
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/disk_drive.json
@@ -0,0 +1,15 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:item", "name": "computercraft:disk_drive" } ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        },
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:dynamic", "name": "computercraft:contents" } ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/monitor_advanced.json b/src/main/resources/data/computercraft/loot_tables/blocks/monitor_advanced.json
new file mode 100644
index 0000000000..0862a05911
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/monitor_advanced.json
@@ -0,0 +1,12 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:monitor_advanced" }
+            ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/monitor_normal.json b/src/main/resources/data/computercraft/loot_tables/blocks/monitor_normal.json
new file mode 100644
index 0000000000..f487890e4b
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/monitor_normal.json
@@ -0,0 +1,12 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:monitor_normal" }
+            ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/printer.json b/src/main/resources/data/computercraft/loot_tables/blocks/printer.json
new file mode 100644
index 0000000000..00e2d6be4b
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/printer.json
@@ -0,0 +1,15 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:item", "name": "computercraft:printer" } ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        },
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:dynamic", "name": "computercraft:contents" } ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/speaker.json b/src/main/resources/data/computercraft/loot_tables/blocks/speaker.json
new file mode 100644
index 0000000000..6beed7b85c
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/speaker.json
@@ -0,0 +1,12 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:speaker" }
+            ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/turtle_advanced.json b/src/main/resources/data/computercraft/loot_tables/blocks/turtle_advanced.json
new file mode 100644
index 0000000000..53be1b4002
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/turtle_advanced.json
@@ -0,0 +1,16 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:dynamic", "name": "computercraft:computer" }
+            ]
+        },
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:dynamic", "name": "computercraft:contents" } ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/turtle_normal.json b/src/main/resources/data/computercraft/loot_tables/blocks/turtle_normal.json
new file mode 100644
index 0000000000..e7c25eb99d
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/turtle_normal.json
@@ -0,0 +1,14 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:dynamic", "name": "computercraft:computer" } ]
+        },
+        {
+            "rolls": 1,
+            "entries": [ { "type": "minecraft:dynamic", "name": "computercraft:contents" } ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/wired_modem_full.json b/src/main/resources/data/computercraft/loot_tables/blocks/wired_modem_full.json
new file mode 100644
index 0000000000..fa6b9de17e
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/wired_modem_full.json
@@ -0,0 +1,12 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:wired_modem_full" }
+            ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/wireless_modem_advanced.json b/src/main/resources/data/computercraft/loot_tables/blocks/wireless_modem_advanced.json
new file mode 100644
index 0000000000..0672f78cfb
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/wireless_modem_advanced.json
@@ -0,0 +1,12 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:wireless_modem_advanced" }
+            ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/loot_tables/blocks/wireless_modem_normal.json b/src/main/resources/data/computercraft/loot_tables/blocks/wireless_modem_normal.json
new file mode 100644
index 0000000000..dad3722ecd
--- /dev/null
+++ b/src/main/resources/data/computercraft/loot_tables/blocks/wireless_modem_normal.json
@@ -0,0 +1,12 @@
+{
+    "type": "minecraft:block",
+    "pools": [
+        {
+            "rolls": 1,
+            "entries": [
+                { "type": "minecraft:item", "name": "computercraft:wireless_modem_normal" }
+            ],
+            "conditions": [ { "condition": "minecraft:survives_explosion" } ]
+        }
+    ]
+}
diff --git a/src/main/resources/data/computercraft/recipes/cable.json b/src/main/resources/data/computercraft/recipes/cable.json
index 09115ec12a..04291f7d5a 100644
--- a/src/main/resources/data/computercraft/recipes/cable.json
+++ b/src/main/resources/data/computercraft/recipes/cable.json
@@ -6,8 +6,8 @@
         " # "
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
-        "R": { "tag": "forge:dusts/redstone" }
+        "#": { "item": "minecraft:stone" },
+        "R": { "item": "minecraft:redstone" }
     },
     "result": { "item": "computercraft:cable", "count": 6 }
 }
diff --git a/src/main/resources/data/computercraft/recipes/computer_advanced.json b/src/main/resources/data/computercraft/recipes/computer_advanced.json
index 81afaad433..2e03c417ac 100644
--- a/src/main/resources/data/computercraft/recipes/computer_advanced.json
+++ b/src/main/resources/data/computercraft/recipes/computer_advanced.json
@@ -6,8 +6,8 @@
         "#G#"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
-        "R": { "tag": "forge:dusts/redstone" },
+        "#": { "item": "minecraft:gold_ingot" },
+        "R": { "item": "minecraft:redstone" },
         "G": { "item": "minecraft:glass_pane" }
     },
     "result": { "item": "computercraft:computer_advanced" }
diff --git a/src/main/resources/data/computercraft/recipes/computer_advanced_upgrade.json b/src/main/resources/data/computercraft/recipes/computer_advanced_upgrade.json
index e6de6b37e2..afa918c2a4 100644
--- a/src/main/resources/data/computercraft/recipes/computer_advanced_upgrade.json
+++ b/src/main/resources/data/computercraft/recipes/computer_advanced_upgrade.json
@@ -6,7 +6,7 @@
         "# #"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
         "C": { "item": "computercraft:computer_normal" }
     },
     "family": "Advanced",
diff --git a/src/main/resources/data/computercraft/recipes/computer_command.json b/src/main/resources/data/computercraft/recipes/computer_command.json
index c89aa7f6db..17b3f60b28 100644
--- a/src/main/resources/data/computercraft/recipes/computer_command.json
+++ b/src/main/resources/data/computercraft/recipes/computer_command.json
@@ -6,7 +6,7 @@
         "#G#"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
+        "#": { "item": "minecraft:stone" },
         "R": { "item": "minecraft:command_block" },
         "G": { "item": "minecraft:glass_pane" }
     },
diff --git a/src/main/resources/data/computercraft/recipes/computer_normal.json b/src/main/resources/data/computercraft/recipes/computer_normal.json
index 1761701bca..744cb14908 100644
--- a/src/main/resources/data/computercraft/recipes/computer_normal.json
+++ b/src/main/resources/data/computercraft/recipes/computer_normal.json
@@ -6,8 +6,8 @@
         "#G#"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
-        "R": { "tag": "forge:dusts/redstone" },
+        "#": { "item": "minecraft:stone" },
+        "R": { "item": "minecraft:redstone" },
         "G": { "item": "minecraft:glass_pane" }
     },
     "result": { "item": "computercraft:computer_normal" }
diff --git a/src/main/resources/data/computercraft/recipes/disk_drive.json b/src/main/resources/data/computercraft/recipes/disk_drive.json
index 59c78b50f5..f0ce1227f4 100644
--- a/src/main/resources/data/computercraft/recipes/disk_drive.json
+++ b/src/main/resources/data/computercraft/recipes/disk_drive.json
@@ -6,8 +6,8 @@
         "#R#"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
-        "R": { "tag": "forge:dusts/redstone" }
+        "#": { "item": "minecraft:stone" },
+        "R": { "item": "minecraft:redstone" }
     },
     "result": { "item": "computercraft:disk_drive" }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_1.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_1.json
index 8b01925bbb..4688fe0fdf 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_1.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_1.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:ink_sac" }
+        { "item": "minecraft:black_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 1118481 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_10.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_10.json
index 26b83fe6bd..7fda63f5cd 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_10.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_10.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:pink_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_11.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_11.json
index 40d6157c0b..f986dfbf82 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_11.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_11.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:lime_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_12.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_12.json
index 7920fc6dd7..e1d6e7546e 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_12.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_12.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:dandelion_yellow" }
+        { "item": "minecraft:yellow_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 14605932 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_13.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_13.json
index e270135ef7..1651d29157 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_13.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_13.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:light_blue_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_14.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_14.json
index 73794dac11..771dda91ca 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_14.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_14.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:magenta_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_15.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_15.json
index 13ff4dd712..ded6079afa 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_15.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_15.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:orange_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_16.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_16.json
index c3c6f193dd..571f6e3f7d 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_16.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_16.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:bone_meal" }
+        { "item": "minecraft:white_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 15790320 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_2.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_2.json
index 53f78a99a1..5abb3dd15e 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_2.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_2.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:rose_red" }
+        { "item": "minecraft:red_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 13388876 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_3.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_3.json
index 452e4c348d..14c89aecf6 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_3.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_3.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:cactus_green" }
+        { "item": "minecraft:green_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 5744206 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_4.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_4.json
index 457bba1edf..08ec7369af 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_4.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_4.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:cocoa_beans" }
+        { "item": "minecraft:brown_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 8349260 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_5.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_5.json
index 94520e1856..c3b669458b 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_5.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_5.json
@@ -2,9 +2,9 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
-        { "item": "minecraft:lapis_lazuli" }
+        { "item": "minecraft:blue_dye" }
     ],
     "result": { "item": "computercraft:disk", "nbt": { "color": 3368652 } }
 }
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_6.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_6.json
index 6a9c1ed5b4..f00ca7ce33 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_6.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_6.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:purple_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_7.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_7.json
index 6f6a442f72..2ee04d2221 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_7.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_7.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:cyan_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_8.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_8.json
index bd8bc8f420..a2ecd595e6 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_8.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_8.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:light_gray_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/generated/disk/disk_9.json b/src/main/resources/data/computercraft/recipes/generated/disk/disk_9.json
index 67ad7f58e3..c988cb88b1 100644
--- a/src/main/resources/data/computercraft/recipes/generated/disk/disk_9.json
+++ b/src/main/resources/data/computercraft/recipes/generated/disk/disk_9.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "minecraft:gray_dye" }
     ],
diff --git a/src/main/resources/data/computercraft/recipes/monitor_advanced.json b/src/main/resources/data/computercraft/recipes/monitor_advanced.json
index 6dc27ecff1..99221dc083 100644
--- a/src/main/resources/data/computercraft/recipes/monitor_advanced.json
+++ b/src/main/resources/data/computercraft/recipes/monitor_advanced.json
@@ -6,7 +6,7 @@
         "###"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
         "G": { "item": "minecraft:glass_pane" }
     },
     "result": { "item": "computercraft:monitor_advanced", "count": 4 }
diff --git a/src/main/resources/data/computercraft/recipes/monitor_normal.json b/src/main/resources/data/computercraft/recipes/monitor_normal.json
index 8ee7866864..503996a142 100644
--- a/src/main/resources/data/computercraft/recipes/monitor_normal.json
+++ b/src/main/resources/data/computercraft/recipes/monitor_normal.json
@@ -6,7 +6,7 @@
         "###"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
+        "#": { "item": "minecraft:stone" },
         "G": { "item": "minecraft:glass_pane" }
     },
     "result": { "item": "computercraft:monitor_normal" }
diff --git a/src/main/resources/data/computercraft/recipes/pocket_computer_advanced.json b/src/main/resources/data/computercraft/recipes/pocket_computer_advanced.json
index f4119b18bc..ec2739cb9d 100644
--- a/src/main/resources/data/computercraft/recipes/pocket_computer_advanced.json
+++ b/src/main/resources/data/computercraft/recipes/pocket_computer_advanced.json
@@ -6,7 +6,7 @@
         "#G#"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
         "G": { "item": "minecraft:glass_pane" },
         "A": { "item": "minecraft:golden_apple" }
     },
diff --git a/src/main/resources/data/computercraft/recipes/pocket_computer_advanced_upgrade.json b/src/main/resources/data/computercraft/recipes/pocket_computer_advanced_upgrade.json
index 79ee81b414..4625f8b1f3 100644
--- a/src/main/resources/data/computercraft/recipes/pocket_computer_advanced_upgrade.json
+++ b/src/main/resources/data/computercraft/recipes/pocket_computer_advanced_upgrade.json
@@ -6,7 +6,7 @@
         "# #"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
         "C": { "item": "computercraft:pocket_computer_normal" }
     },
     "family": "Advanced",
diff --git a/src/main/resources/data/computercraft/recipes/pocket_computer_normal.json b/src/main/resources/data/computercraft/recipes/pocket_computer_normal.json
index f01612779b..5e5c0eb4f2 100644
--- a/src/main/resources/data/computercraft/recipes/pocket_computer_normal.json
+++ b/src/main/resources/data/computercraft/recipes/pocket_computer_normal.json
@@ -6,7 +6,7 @@
         "#G#"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
+        "#": { "item": "minecraft:stone" },
         "G": { "item": "minecraft:glass_pane" },
         "A": { "item": "minecraft:golden_apple" }
     },
diff --git a/src/main/resources/data/computercraft/recipes/printer.json b/src/main/resources/data/computercraft/recipes/printer.json
index 4d120a5410..eb3f244e6c 100644
--- a/src/main/resources/data/computercraft/recipes/printer.json
+++ b/src/main/resources/data/computercraft/recipes/printer.json
@@ -6,9 +6,10 @@
         "#D#"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
+        "#": { "item": "minecraft:stone" },
         "R": { "item": "minecraft:glass_pane" },
-        "D": { "tag": "forge:dyes" }
+        "D": { "item": "minecraft:black_dye" }
     },
+    "_comment": "TODO: All dyes",
     "result": { "item": "computercraft:printer" }
 }
diff --git a/src/main/resources/data/computercraft/recipes/speaker.json b/src/main/resources/data/computercraft/recipes/speaker.json
index 6c0cf7595b..d6039f3471 100644
--- a/src/main/resources/data/computercraft/recipes/speaker.json
+++ b/src/main/resources/data/computercraft/recipes/speaker.json
@@ -6,8 +6,8 @@
         "#R#"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
-        "R": { "tag": "forge:dusts/redstone" },
+        "#": { "item": "minecraft:stone" },
+        "R": { "item": "minecraft:redstone" },
         "N": { "item": "minecraft:note_block" }
     },
     "result": { "item": "computercraft:speaker" }
diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced.json b/src/main/resources/data/computercraft/recipes/turtle_advanced.json
index 7da7f90171..63a5e41ecb 100644
--- a/src/main/resources/data/computercraft/recipes/turtle_advanced.json
+++ b/src/main/resources/data/computercraft/recipes/turtle_advanced.json
@@ -6,9 +6,9 @@
         "#I#"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
         "C": { "item": "computercraft:computer_advanced" },
-        "I": { "tag": "forge:chests/wooden" }
+        "I": { "item": "minecraft:chest" }
     },
     "family": "Advanced",
     "result": { "item": "computercraft:turtle_advanced" }
diff --git a/src/main/resources/data/computercraft/recipes/turtle_advanced_upgrade.json b/src/main/resources/data/computercraft/recipes/turtle_advanced_upgrade.json
index 79f4444ea2..5410e9f599 100644
--- a/src/main/resources/data/computercraft/recipes/turtle_advanced_upgrade.json
+++ b/src/main/resources/data/computercraft/recipes/turtle_advanced_upgrade.json
@@ -6,8 +6,8 @@
         " B "
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
-        "B": { "tag": "forge:storage_blocks/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
+        "B": { "item": "minecraft:gold_block" },
         "C": { "item": "computercraft:turtle_normal" }
     },
     "family": "Advanced",
diff --git a/src/main/resources/data/computercraft/recipes/turtle_normal.json b/src/main/resources/data/computercraft/recipes/turtle_normal.json
index f432bdf3c3..8d83a25506 100644
--- a/src/main/resources/data/computercraft/recipes/turtle_normal.json
+++ b/src/main/resources/data/computercraft/recipes/turtle_normal.json
@@ -6,9 +6,9 @@
         "#I#"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/iron" },
+        "#": { "item": "minecraft:iron_ingot" },
         "C": { "item": "computercraft:computer_normal" },
-        "I": { "tag": "forge:chests/wooden" }
+        "I": { "item": "minecraft:chest" }
     },
     "family": "Normal",
     "result": { "item": "computercraft:turtle_normal" }
diff --git a/src/main/resources/data/computercraft/recipes/wired_modem.json b/src/main/resources/data/computercraft/recipes/wired_modem.json
index fa9e47af27..e5a1d6b2b0 100644
--- a/src/main/resources/data/computercraft/recipes/wired_modem.json
+++ b/src/main/resources/data/computercraft/recipes/wired_modem.json
@@ -6,8 +6,8 @@
         "###"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
-        "R": { "tag": "forge:dusts/redstone" }
+        "#": { "item": "minecraft:stone" },
+        "R": { "item": "minecraft:redstone" }
     },
     "result": { "item": "computercraft:wired_modem" }
 }
diff --git a/src/main/resources/data/computercraft/recipes/wireless_modem_advanced.json b/src/main/resources/data/computercraft/recipes/wireless_modem_advanced.json
index 3d3297d6cc..6ace336b3e 100644
--- a/src/main/resources/data/computercraft/recipes/wireless_modem_advanced.json
+++ b/src/main/resources/data/computercraft/recipes/wireless_modem_advanced.json
@@ -6,7 +6,7 @@
         "###"
     ],
     "key": {
-        "#": { "tag": "forge:ingots/gold" },
+        "#": { "item": "minecraft:gold_ingot" },
         "E": { "item": "minecraft:ender_eye" }
     },
     "result": { "item": "computercraft:wireless_modem_advanced" }
diff --git a/src/main/resources/data/computercraft/recipes/wireless_modem_normal.json b/src/main/resources/data/computercraft/recipes/wireless_modem_normal.json
index 3005edd7df..004c7509c8 100644
--- a/src/main/resources/data/computercraft/recipes/wireless_modem_normal.json
+++ b/src/main/resources/data/computercraft/recipes/wireless_modem_normal.json
@@ -6,7 +6,7 @@
         "###"
     ],
     "key": {
-        "#": { "tag": "forge:stone" },
+        "#": { "item": "minecraft:stone" },
         "E": { "item": "minecraft:ender_pearl" }
     },
     "result": { "item": "computercraft:wireless_modem_normal" }
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
new file mode 100644
index 0000000000..fb9e39d298
--- /dev/null
+++ b/src/main/resources/fabric.mod.json
@@ -0,0 +1,10 @@
+{
+    "id": "computercraft",
+    "name": "CC: Tweaked",
+    "version": "${version}",
+    "side": "universal",
+    "initializer": "dan200.computercraft.ComputerCraft",
+    "mixins": {
+        "client": "computercraft.client.json"
+    }
+}
diff --git a/src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java b/src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java
index 30cc449bef..78af4f31db 100644
--- a/src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java
+++ b/src/test/java/dan200/computercraft/core/filesystem/ResourceMountTest.java
@@ -7,9 +7,9 @@
 package dan200.computercraft.core.filesystem;
 
 import dan200.computercraft.api.filesystem.IMount;
-import net.minecraft.resources.FolderPack;
-import net.minecraft.resources.ResourcePackType;
-import net.minecraft.resources.SimpleReloadableResourceManager;
+import net.minecraft.resource.DirectoryResourcePack;
+import net.minecraft.resource.ReloadableResourceManagerImpl;
+import net.minecraft.resource.ResourceType;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -29,8 +29,8 @@ public class ResourceMountTest
     @BeforeEach
     public void before()
     {
-        SimpleReloadableResourceManager manager = new SimpleReloadableResourceManager( ResourcePackType.SERVER_DATA );
-        manager.addResourcePack( new FolderPack( new File( "src/main/resources" ) ) );
+        ReloadableResourceManagerImpl manager = new ReloadableResourceManagerImpl( ResourceType.DATA, null );
+        manager.addPack( new DirectoryResourcePack( new File( "src/main/resources" ) ) );
 
         mount = new ResourceMount( "computercraft", "lua/rom", manager );
     }
diff --git a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java
index 6d5329b697..e5e92b3145 100644
--- a/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java
+++ b/src/test/java/dan200/computercraft/shared/wired/NetworkTest.java
@@ -18,8 +18,8 @@
 import dan200.computercraft.api.peripheral.IComputerAccess;
 import dan200.computercraft.api.peripheral.IPeripheral;
 import dan200.computercraft.shared.util.DirectionUtil;
-import net.minecraft.util.EnumFacing;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 import org.junit.jupiter.api.Disabled;
@@ -261,7 +261,7 @@ public void testLarge()
             long start = System.nanoTime();
 
             grid.forEach( ( existing, pos ) -> {
-                for( EnumFacing facing : DirectionUtil.FACINGS )
+                for( Direction facing : DirectionUtil.FACINGS )
                 {
                     BlockPos offset = pos.offset( facing );
                     if( offset.getX() > BRUTE_SIZE / 2 == pos.getX() > BRUTE_SIZE / 2 )
diff --git a/tools/disk_recipe.json b/tools/disk_recipe.json
index c6a67ab7ad..c4303f5f3e 100644
--- a/tools/disk_recipe.json
+++ b/tools/disk_recipe.json
@@ -2,7 +2,7 @@
     "type": "computercraft:impostor_shapeless",
     "group": "computercraft:disk",
     "ingredients": [
-        { "tag": "forge:dusts/redstone" },
+        { "item": "minecraft:redstone" },
         { "item": "minecraft:paper" },
         { "item": "${dye}" }
     ],
diff --git a/tools/recipes.lua b/tools/recipes.lua
index 53c619f450..d2c0075235 100644
--- a/tools/recipes.lua
+++ b/tools/recipes.lua
@@ -30,22 +30,22 @@ local pocket_upgrades = {
 
 --- All dye/disk colours
 local colours = {
-    { 0x111111, "minecraft:ink_sac" },
-    { 0xcc4c4c, "minecraft:rose_red" },
-    { 0x57A64E, "minecraft:cactus_green" },
-    { 0x7f664c, "minecraft:cocoa_beans" },
-    { 0x3366cc, "minecraft:lapis_lazuli" },
+    { 0x111111, "minecraft:black_dye" },
+    { 0xcc4c4c, "minecraft:red_dye" },
+    { 0x57A64E, "minecraft:green_dye" },
+    { 0x7f664c, "minecraft:brown_dye" },
+    { 0x3366cc, "minecraft:blue_dye" },
     { 0xb266e5, "minecraft:purple_dye" },
     { 0x4c99b2, "minecraft:cyan_dye" },
     { 0x999999, "minecraft:light_gray_dye" },
     { 0x4c4c4c, "minecraft:gray_dye" },
     { 0xf2b2cc, "minecraft:pink_dye" },
     { 0x7fcc19, "minecraft:lime_dye" },
-    { 0xdede6c, "minecraft:dandelion_yellow" },
+    { 0xdede6c, "minecraft:yellow_dye" },
     { 0x99b2f2, "minecraft:light_blue_dye" },
     { 0xe57fd8, "minecraft:magenta_dye" },
     { 0xf2b233, "minecraft:orange_dye" },
-    { 0xf0f0f0, "minecraft:bone_meal" },
+    { 0xf0f0f0, "minecraft:white_dye" },
 }
 
 --- Read the provided file into a string, exiting the program if not found.