diff --git a/game/client/src/main/java/net/minecraft/client/Minecraft.java b/game/client/src/main/java/net/minecraft/client/Minecraft.java
index 79ccb7266..6d099f075 100644
--- a/game/client/src/main/java/net/minecraft/client/Minecraft.java
+++ b/game/client/src/main/java/net/minecraft/client/Minecraft.java
@@ -35,6 +35,7 @@ import net.minecraft.client.net.handler.PacketHandlerClient;
 import net.minecraft.client.render.block.model.BlockModelGrass;
 import net.minecraft.client.render.font.FontRenderer;
 import net.minecraft.client.render.font.FontRendererDefault;
+import net.minecraft.client.render.terrain.TerrainRendererMultiDraw;
 import net.minecraft.client.util.helper.UrlHelper;
 import net.minecraft.client.world.WorldClient;
 import net.minecraft.client.world.chunk.provider.ChunkProviderStatic;
@@ -512,18 +513,18 @@ public class Minecraft
 		
         OpenGLHelper.testCapabilities(this);
         
-//        if(gameSettings.enableMultiDrawRendering.value) {
-//            if(OpenGLHelper.gl20) {
-//            	System.out.println("Using MultiDraw terrain renderer");
-//            	terrainRenderer = new TerrainRendererMultiDraw(this);
-//            }else {
-//            	System.out.println("MultiDraw not supported, using legacy terrain renderer");
-//            	terrainRenderer = new TerrainRendererLegacy(this);
-//            }
-//        }else {
-//        	System.out.println("MultiDraw disabled, using legacy terrain renderer");
+        if(gameSettings.enableMultiDrawRendering.value) {
+            if(OpenGLHelper.gl20) {
+            	System.out.println("Using MultiDraw terrain renderer");
+            	terrainRenderer = new TerrainRendererMultiDraw(this);
+            }else {
+            	System.out.println("MultiDraw not supported, using legacy terrain renderer");
+            	terrainRenderer = new TerrainRendererLegacy(this);
+            }
+        }else {
+        	System.out.println("MultiDraw disabled, using legacy terrain renderer");
         	terrainRenderer = new TerrainRendererLegacy(this);
-//        }
+        }
         
         if(Shaders.enableShaders) {
             setRenderer(new ShadersRenderer(this));
diff --git a/game/client/src/main/java/net/minecraft/client/gui/ScreenSignEditor.java b/game/client/src/main/java/net/minecraft/client/gui/ScreenSignEditor.java
index b7d7c0ca6..0c2126d44 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/ScreenSignEditor.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/ScreenSignEditor.java
@@ -44,13 +44,15 @@ public class ScreenSignEditor extends Screen
         buttons.add(new ButtonElement(1, width / 2 - 20, 120 + 50 + yOffset, 20, 20, "<"));
         buttons.add(new ButtonElement(2, width / 2, 120 + 50 + yOffset, 20, 20, ">"));
 
-        add(new ListenerButtonElement(30, width/2 - 50, 12 * 0 + 67 + ((entitySign.getBlockId() == Blocks.SIGN_WALL_PLANKS_OAK.id()) ? 31 : 0), 100, 12, "")
+        boolean wallSign = !((BlockLogicSign) block.getLogic()).isFreeStanding;
+
+        add(new ListenerButtonElement(30, width/2 - 50, 12 * 0 + 67 + (wallSign ? 31 : 0), 100, 12, "")
             .setActionListener(() -> {editLine = 0; inKeyboard = true;})).mute().hide();
-        add(new ListenerButtonElement(30, width/2 - 50, 12 * 1 + 67 + ((entitySign.getBlockId() == Blocks.SIGN_WALL_PLANKS_OAK.id()) ? 31 : 0), 100, 12, "")
+        add(new ListenerButtonElement(30, width/2 - 50, 12 * 1 + 67 + (wallSign ? 31 : 0), 100, 12, "")
             .setActionListener(() -> {editLine = 1;inKeyboard = true;})).mute().hide();
-        add(new ListenerButtonElement(30, width/2 - 50, 12 * 2 + 67 + ((entitySign.getBlockId() == Blocks.SIGN_WALL_PLANKS_OAK.id()) ? 31 : 0), 100, 12, "")
+        add(new ListenerButtonElement(30, width/2 - 50, 12 * 2 + 67 + (wallSign ? 31 : 0), 100, 12, "")
             .setActionListener(() -> {editLine = 2;inKeyboard = true;})).mute().hide();
-        add(new ListenerButtonElement(30, width/2 - 50, 12 * 3 + 67 + ((entitySign.getBlockId() == Blocks.SIGN_WALL_PLANKS_OAK.id()) ? 31 : 0), 100, 12, "")
+        add(new ListenerButtonElement(30, width/2 - 50, 12 * 3 + 67 + (wallSign ? 31 : 0), 100, 12, "")
             .setActionListener(() -> {editLine = 3;inKeyboard = true;})).mute().hide();
 
         inKeyboard = (mc.gameSettings.forceOnscreenKeyboard.value || mc.inputType == InputType.CONTROLLER) && !(mc.currentScreen instanceof ScreenKeyboard);
@@ -159,7 +161,7 @@ public class ScreenSignEditor extends Screen
         GL11.glScalef(-scale, -scale, -scale);
         GL11.glRotatef(180F, 0.0F, 1.0F, 0.0F);
         Block<?> block = entitySign.getBlock();
-        if(block == Blocks.SIGN_POST_PLANKS_OAK)
+        if(((BlockLogicSign) block.getLogic()).isFreeStanding)
         {
             float signAngle = (float)((entitySign.getBlockMeta() & BlockLogicSign.MASK_SIDE) * 360) / 16F;
             GL11.glRotatef(signAngle, 0.0F, 1.0F, 0.0F);
diff --git a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java
index ee3951f92..26deddd45 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryBlock.java
@@ -302,7 +302,7 @@ public class ViewerCategoryBlock extends ModelViewerCategory<ViewerCategoryBlock
         mc.currentWorld.markBlockDirty(DisplayChunk.displayBlockPosX, DisplayChunk.displayBlockPosY, DisplayChunk.displayBlockPosZ);
     }
 
-    public static class BlockRenderInfo extends Info {
+    public static class BlockRenderInfo extends ModelViewerCategory.Info {
         final @NotNull File outputDir;
         final int blockId;
         final int blockMeta;
diff --git a/game/client/src/main/java/net/minecraft/client/gui/options/ScreenOptions.java b/game/client/src/main/java/net/minecraft/client/gui/options/ScreenOptions.java
index 6784fb64e..83c3b7998 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/options/ScreenOptions.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/options/ScreenOptions.java
@@ -33,6 +33,8 @@ public class ScreenOptions
     public static KeyBinding pickingKeyBinding = null;
     public static KeyBindingComponent pickingKeyBindingComponent = null;
 
+    public static boolean queueReload = false;
+
     private float optionsScrollAmount = 0.0f;
     private float pagesListScrollAmount = 0.0f;
     private int top, bottom, optionsScrollRegionHeight, pagesListScrollRegionHeight, pageWidth, pagesListWidth;
@@ -57,6 +59,7 @@ public class ScreenOptions
     {
         super(parent);
         this.selectedPage = selectedPage;
+        queueReload = false;
     }
 
     @Override
@@ -95,6 +98,7 @@ public class ScreenOptions
         if (eventKey == Keyboard.KEY_ESCAPE)
         {
             mc.texturePackList.refreshIfReady(); // Sets the texturepacks on menu close
+            reloadIfReady();
             mc.displayScreen(getParentScreen());
         }
         else
@@ -149,6 +153,7 @@ public class ScreenOptions
         {
             mc.displayScreen(getParentScreen());
             mc.texturePackList.refreshIfReady(); // Sets the texturepacks on menu close
+            reloadIfReady();
         }
     }
 
@@ -160,6 +165,7 @@ public class ScreenOptions
         super.removed();
         mc.gameSettings.saveOptions();
         mc.texturePackList.refreshIfReady();
+        reloadIfReady();
     }
 
     private void scrollOptions(float amount) {
@@ -449,6 +455,7 @@ public class ScreenOptions
                     this.selectedPage.initComponents(mc);
                     mc.sndManager.playSound("random.click", SoundCategory.GUI_SOUNDS, 1.0F, 1.0F);
                     mc.texturePackList.refreshIfReady();
+                    reloadIfReady();
                     break;
                 }
                 pagesListY += PAGE_BUTTON_HEIGHT;
@@ -559,4 +566,11 @@ public class ScreenOptions
             mc.displayScreen(getParentScreen());
         }
     }
+
+    private void reloadIfReady() {
+        if (queueReload) {
+            mc.renderGlobal.loadRenderers();
+            queueReload = false;
+        }
+    }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java b/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java
index a9a46b332..b8bd2f426 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java
@@ -348,7 +348,7 @@ public abstract class OptionsPages
                 .withComponent(new BooleanOptionComponent(mc.gameSettings.downloadSoundfiles))
                 .withComponent(new BooleanOptionComponent(mc.gameSettings.enableDevResourceLoading))
                 .withComponent(new BooleanOptionComponent(mc.gameSettings.devModels))
-//                .withComponent(new BooleanOptionComponent(mc.gameSettings.enableMultiDrawRendering))
+                .withComponent(new BooleanOptionComponent(mc.gameSettings.enableMultiDrawRendering))
             )
             .withComponent(new OptionsCategory("gui.options.page.debug.category.utility")
                 .withComponent(new ShortcutComponent("gui.options.page.debug.button.open.model_viewer", () -> {
@@ -416,6 +416,7 @@ public abstract class OptionsPages
                     Minecraft.getMinecraft().hasCrashed = true;
                     throw new Error();
                 }))
+                .withComponent(new ShortcutComponent("gui.options.page.debug.button.garbage_collect", System::gc))
             );
     }
 }
\ No newline at end of file
diff --git a/game/client/src/main/java/net/minecraft/client/option/GameSettings.java b/game/client/src/main/java/net/minecraft/client/option/GameSettings.java
index 52ce4d5c1..edb5f4137 100644
--- a/game/client/src/main/java/net/minecraft/client/option/GameSettings.java
+++ b/game/client/src/main/java/net/minecraft/client/option/GameSettings.java
@@ -16,6 +16,7 @@ import net.minecraft.client.Minecraft;
 import net.minecraft.client.discord.RichPresenceHandlerThread;
 import net.minecraft.client.gui.TooltipElement;
 import net.minecraft.client.gui.hud.component.HudComponents;
+import net.minecraft.client.gui.options.ScreenOptions;
 import net.minecraft.client.input.InputDevice;
 import net.minecraft.client.option.enums.Colorblindness;
 import net.minecraft.client.render.font.Fonts;
@@ -133,8 +134,7 @@ public class GameSettings {
 	public OptionBoolean alwaysShowDescriptions = new OptionBoolean(this, "alwaysShowDescriptions", false);
 	public OptionBoolean turnWithVehicle = new OptionBoolean(this, "turnWithVehicle", true);
 	public OptionBoolean disableCrashHandler = new OptionBoolean(this, "disableCrashHandler", false);
-	// Permanently disabled for 7.3 due to issues with render distance
-//	public OptionBoolean enableMultiDrawRendering = new OptionBoolean(this, "enableMultiDrawRendering", false);
+	public OptionBoolean enableMultiDrawRendering = new OptionBoolean(this, "enableMultiDrawRendering", false);
 
 	public OptionBoolean enableDevResourceLoading = new OptionBoolean(this, "enableDevResourceLoading", false);
 	public OptionBoolean downloadSoundfiles = new OptionBoolean(this, "downloadSoundfiles", true);
@@ -300,7 +300,7 @@ public class GameSettings {
 		loadOptions();
 		saveOptions();
 	}
-	
+
 	public String getDisplayString(Option<?> option) {
 		if (option == null) return "";
 		I18n trans = I18n.getInstance();
@@ -455,6 +455,13 @@ public class GameSettings {
 				mc.currentWorld.setDifficulty(difficulty.value, false);
 			}
 		}
+		if(option == renderDistance) {
+			if (mc.currentScreen instanceof ScreenOptions) {
+				ScreenOptions.queueReload = true;
+			} else {
+				mc.renderGlobal.loadRenderers();
+			}
+		}
 
 		if(option == playerLeftHanded && mc.thePlayer != null) {mc.thePlayer.playerConfig.setLeftHand(playerLeftHanded.value);}
 		if(option == playerCape && mc.thePlayer != null) {mc.thePlayer.playerConfig.setCape(playerCape.value);}
diff --git a/game/client/src/main/java/net/minecraft/client/render/FogManager.java b/game/client/src/main/java/net/minecraft/client/render/FogManager.java
index da3ac9efc..24587ee2b 100644
--- a/game/client/src/main/java/net/minecraft/client/render/FogManager.java
+++ b/game/client/src/main/java/net/minecraft/client/render/FogManager.java
@@ -58,6 +58,17 @@ public class FogManager {
         glFogfv(GL_FOG_COLOR, buffer(fogRed, fogGreen, fogBlue, 0.5F));
         glNormal3f(0.0F, -1F, 0.0F);
         glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
+        if (!mc.gameSettings.fog.value && fogMode != FOG_MODE_SKY) {
+            glFogi(GL_FOG_MODE, GL_EXP);
+            glFogf(GL_FOG_DENSITY, 0.0F);
+            float maxFogDistance = mc.gameSettings.renderDistance.highest * 16;
+            glFogf(GL_FOG_START, maxFogDistance * 0.25F);
+            glFogf(GL_FOG_END, maxFogDistance);
+            glEnable(GL_COLOR_MATERIAL);
+            glColorMaterial(GL_FRONT, GL_AMBIENT);
+            return;
+        }
+
         if(CameraUtil.isUnderLiquid(mc.activeCamera, mc.currentWorld, Material.water, partialTick))
         {
             glFogi(GL_FOG_MODE, GL_EXP);
@@ -70,8 +81,8 @@ public class FogManager {
         }
         else
         {
-            float maxFogDistance = RenderDistance.EXTREME.chunks * 16;
             float fogDistance = farPlaneDistance;
+            float maxFogDistance = mc.gameSettings.renderDistance.highest * 16;
 
             float fogModifier = mc.currentWorld.getCurrentWeather() != null ? mc.currentWorld.getCurrentWeather().getFogDistanceMultiplier() : 1.0f;
             fogModifier = (1.0f - ((1.0f - fogModifier) * mc.currentWorld.weatherManager.getWeatherIntensity() * mc.currentWorld.weatherManager.getWeatherPower()));
diff --git a/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java b/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java
index c8c24a439..08720bee3 100644
--- a/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java
+++ b/game/client/src/main/java/net/minecraft/client/render/RenderGlobal.java
@@ -6,6 +6,8 @@ import net.minecraft.client.entity.particle.Particle;
 import net.minecraft.client.entity.particle.ParticleDispatcher;
 import net.minecraft.client.gui.ScreenPhotoMode;
 import net.minecraft.client.gui.modelviewer.ScreenModelViewer;
+import net.minecraft.client.gui.options.ScreenOptions;
+import net.minecraft.client.render.block.model.BlockModel;
 import net.minecraft.client.render.block.model.BlockModelDispatcher;
 import net.minecraft.client.render.block.model.BlockModelLeaves;
 import net.minecraft.client.render.camera.EntityCamera;
@@ -259,6 +261,7 @@ public class RenderGlobal
     }
     public void loadRenderers()
     {
+        ScreenOptions.queueReload = false;
         updateStars();
         // Delete all display lists - this is a (hopefully temporary) workaround to AMD's OpenGL driver leaking memory
         // See https://community.amd.com/t5/opengl-vulkan/high-vram-consumption-in-minecraft/m-p/607299
@@ -1492,7 +1495,7 @@ public class RenderGlobal
         tessellator.draw();
         GL11.glEnable(GL11.GL_TEXTURE_2D);
     }
-    
+
     public void drawInterpolatedEntityBoundingBox(Entity entity, AABB boundingBox, ICamera camera, float partialTicks) {
         double cameraX = camera.getX(partialTicks);
         double cameraY = camera.getY(partialTicks);
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java
index e9807a55e..3080645cf 100644
--- a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelDispatcher.java
@@ -670,8 +670,8 @@ public final class BlockModelDispatcher
         addDispatch(new BlockModelEmpty<>(Blocks.SIGN_POST_PLANKS_OAK).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
         addDispatch(new BlockModelEmpty<>(Blocks.SIGN_WALL_PLANKS_OAK).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
         addDispatch(new BlockModelEmpty<>(Blocks.FLAG).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
-        addDispatch(new BlockModelEmpty<>(Blocks.SIGN_POST_PLANKS_OAK_PAINTED).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
-        addDispatch(new BlockModelEmpty<>(Blocks.SIGN_WALL_PLANKS_OAK_PAINTED).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
+        addDispatch(new BlockModelSignPainted<>(Blocks.SIGN_POST_PLANKS_OAK_PAINTED).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
+        addDispatch(new BlockModelSignPainted<>(Blocks.SIGN_WALL_PLANKS_OAK_PAINTED).setAllTextures(BLOCK_TEXTURES, "minecraft:block/planks/oak"));
 
         addDispatch(new BlockModelLayer<>(Blocks.LAYER_SNOW).setAllTextures(BLOCK_TEXTURES, "minecraft:block/block_snow"));
         addDispatch(new BlockModelLayer<>(Blocks.LAYER_LEAVES_OAK).setAllTextures(BLOCK_TEXTURES, "minecraft:block/leaves/oak_fancy"));
diff --git a/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelSignPainted.java b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelSignPainted.java
new file mode 100644
index 000000000..a6687f240
--- /dev/null
+++ b/game/client/src/main/java/net/minecraft/client/render/block/model/BlockModelSignPainted.java
@@ -0,0 +1,19 @@
+package net.minecraft.client.render.block.model;
+
+import net.minecraft.client.render.texture.stitcher.IconCoordinate;
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.BlockLogicSignPainted;
+import net.minecraft.core.util.helper.Side;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class BlockModelSignPainted<T extends BlockLogicSignPainted> extends BlockModelEmpty<T>{
+    public BlockModelSignPainted(Block<T> block) {
+        super(block);
+    }
+
+    @Override
+    public @Nullable IconCoordinate getParticleTexture(@NotNull final Side side, final int meta) {
+        return BlockModelPlanksPainted.texCoords[this.block.getLogic().fromMetadata(meta).blockMeta];
+    }
+}
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRenderer.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRenderer.java
index 97fe0b14f..f9337ba3f 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRenderer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRenderer.java
@@ -33,15 +33,25 @@ public abstract class EntityRenderer<T extends Entity>
     protected EntityRenderDispatcher renderDispatcher = null;
     protected Class<? extends T> appliedClass = null;
 
-    protected float shadowSize;
+    private float shadowSize;
     protected float shadowOpacity;
 
-    public EntityRenderer()
+    public EntityRenderer(final float shadowSize)
     {
-        this.shadowSize = 0.0F;
+        this.shadowSize = shadowSize;
         this.shadowOpacity = 1.0F;
     }
 
+    public EntityRenderer()
+    {
+        this(0.0f);
+    }
+
+    public float getShadowSize(final @NotNull T entity)
+    {
+        return this.shadowSize;
+    }
+
     public abstract void render(@NotNull Tessellator tessellator, @NotNull T entity, double x, double y, double z,
                                 float yaw, float partialTick);
 
@@ -145,12 +155,15 @@ public abstract class EntityRenderer<T extends Entity>
         final double lerpX = entity.xo + (entity.x - entity.xo) * (double)partialTick;
         final double lerpY = entity.yo + (entity.y - entity.yo) * (double)partialTick + (double)entity.getShadowHeightOffs();
         final double lerpZ = entity.zo + (entity.z - entity.zo) * (double)partialTick;
-        final int minX = MathHelper.floor(lerpX - (double) this.shadowSize);
-        final int maxX = MathHelper.floor(lerpX + (double) this.shadowSize);
-        final int minY = MathHelper.floor(lerpY - (double) this.shadowSize);
+
+        final float shadowSize = getShadowSize(entity);
+
+        final int minX = MathHelper.floor(lerpX - (double) shadowSize);
+        final int maxX = MathHelper.floor(lerpX + (double) shadowSize);
+        final int minY = MathHelper.floor(lerpY - (double) shadowSize);
         final int maxY = MathHelper.floor(lerpY);
-        final int minZ = MathHelper.floor(lerpZ - (double) this.shadowSize);
-        final int maxZ = MathHelper.floor(lerpZ + (double) this.shadowSize);
+        final int minZ = MathHelper.floor(lerpZ - (double) shadowSize);
+        final int maxZ = MathHelper.floor(lerpZ + (double) shadowSize);
         final double dxLerp = posX - lerpX;
         final double dyLerp = posY - lerpY;
         final double dzLerp = posZ - lerpZ;
@@ -165,7 +178,7 @@ public abstract class EntityRenderer<T extends Entity>
                     final int blockId = world.getBlockId(blockX, blockY - 1, blockZ);
                     if(blockId > 0 && world.getBlockLightValue(blockX, blockY, blockZ) > 3)
                     {
-                        renderShadowOnBlock(tessellator, entity, Blocks.blocksList[blockId], posX, posY + (double)entity.getShadowHeightOffs(), posZ, blockX, blockY, blockZ, opacity, this.shadowSize, dxLerp, dyLerp + (double)entity.getShadowHeightOffs(), dzLerp);
+                        renderShadowOnBlock(tessellator, entity, Blocks.blocksList[blockId], posX, posY + (double)entity.getShadowHeightOffs(), posZ, blockX, blockY, blockZ, opacity, shadowSize, dxLerp, dyLerp + (double)entity.getShadowHeightOffs(), dzLerp);
                     }
                 }
 
@@ -297,7 +310,7 @@ public abstract class EntityRenderer<T extends Entity>
 
     public void postRender(final @NotNull Tessellator tessellator, final @NotNull T entity, final double x, final double y, final double z, final float yaw, final float partialTick)
     {
-        if(renderShadows && this.renderDispatcher.settings.fancyGraphics.value == 1 && this.shadowSize > 0.0F)
+        if(renderShadows && this.renderDispatcher.settings.fancyGraphics.value == 1 && getShadowSize(entity) > 0.0F)
         {
             final double distance = this.renderDispatcher.distanceToLerpSquared(entity.x, entity.y, entity.z);
             final float opacity = (float)((1.0D - distance / 256D) * (double) this.shadowOpacity);
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererBoat.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererBoat.java
index 5ecdc0d13..b7349c67c 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererBoat.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererBoat.java
@@ -21,8 +21,7 @@ public class EntityRendererBoat extends EntityRenderer<EntityBoat>
     private final Minecraft mc = Minecraft.getMinecraft();
 
     public EntityRendererBoat() {
-        super();
-        this.shadowSize = 0.5F;
+        super(0.5f);
         this.flagRenderer = (TileEntityRendererFlag) (Object) TileEntityRenderDispatcher.instance.getRenderer(TileEntityFlag.class); // Definitely safe casting :)
     }
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererFallingBlock.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererFallingBlock.java
index 47ef9d12c..06df18f9e 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererFallingBlock.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererFallingBlock.java
@@ -23,8 +23,8 @@ public class EntityRendererFallingBlock extends EntityRenderer<EntityFallingBloc
 
     public EntityRendererFallingBlock()
     {
+        super(0.5f);
         mc = Minecraft.getMinecraft();
-        shadowSize = 0.5F;
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererGeneric.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererGeneric.java
index c64d1ed24..62aa77466 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererGeneric.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererGeneric.java
@@ -9,8 +9,7 @@ import org.useless.dragonfly.models.entity.StaticEntityModel;
 public class EntityRendererGeneric<T extends Entity> extends EntityRenderer<T>{
     public final @NotNull StaticEntityModel entityModel;
     public EntityRendererGeneric(final @NotNull StaticEntityModel entityModel, final float shadowSize) {
-        super();
-        this.shadowSize = shadowSize;
+        super(shadowSize);
         this.entityModel = entityModel;
     }
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererItem.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererItem.java
index a4d01df79..c9861e379 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererItem.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererItem.java
@@ -19,7 +19,7 @@ public class EntityRendererItem extends EntityRenderer<EntityItem>
 
 
 	public EntityRendererItem() {
-		shadowSize = 0.15F;
+		super(0.15f);
 		shadowOpacity = 0.75F;
 	}
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererMinecart.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererMinecart.java
index 5c0f563c8..a4aa7d283 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererMinecart.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererMinecart.java
@@ -16,8 +16,7 @@ import org.lwjgl.opengl.GL12;
 
 public class EntityRendererMinecart extends EntityRenderer<EntityMinecart> {
     public EntityRendererMinecart() {
-        super();
-        this.shadowSize = 0.5F;
+        super(0.5f);
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererTNT.java b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererTNT.java
index 0055535fd..3121e5187 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererTNT.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/EntityRendererTNT.java
@@ -14,7 +14,7 @@ public class EntityRendererTNT extends EntityRenderer<EntityPrimedTNT>
 {
     public EntityRendererTNT()
     {
-        shadowSize = 0.5F;
+        super(0.5f);
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/MobRenderer.java b/game/client/src/main/java/net/minecraft/client/render/entity/MobRenderer.java
index d97611bf1..ae44e7b9a 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/MobRenderer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/MobRenderer.java
@@ -6,6 +6,7 @@ import net.minecraft.client.render.LightmapHelper;
 import net.minecraft.client.render.font.FontRenderer;
 import net.minecraft.client.render.tessellator.Tessellator;
 import net.minecraft.core.Global;
+import net.minecraft.core.entity.AgedMob;
 import net.minecraft.core.entity.Mob;
 import net.minecraft.core.util.helper.Color;
 import net.minecraft.core.util.helper.MathHelper;
@@ -25,8 +26,7 @@ public abstract class MobRenderer<T extends Mob> extends EntityRenderer<T>{
     private static final Logger LOGGER = LogUtils.getLogger();
     private final @NotNull List<@Nullable StaticEntityModel> setupModels = new ArrayList<>();
     public MobRenderer(final float shadowSize) {
-        super();
-        this.shadowSize = shadowSize;
+        super(shadowSize);
     }
     @Override
     public void render(final @NotNull Tessellator tessellator, final @NotNull T entity, final double x, final double y, final double z, final float yaw, final float partialTick) {
@@ -115,6 +115,24 @@ public abstract class MobRenderer<T extends Mob> extends EntityRenderer<T>{
             }
             GL11.glRotatef(rotationProgress * getMaxDeathRotation(entity), 0.0F, 0.0F, 1.0F);
         }
+
+        if (entity instanceof AgedMob) {
+            final AgedMob agedMob = (AgedMob) entity;
+            final float ageScale = agedMob.getMobAge().getAgeScale();
+            GL11.glScalef(ageScale, ageScale, ageScale);
+        }
+    }
+
+    @Override
+    public float getShadowSize(final @NotNull T entity) {
+        final float baseSize = super.getShadowSize(entity);
+        if (entity instanceof AgedMob) {
+            final AgedMob agedMob = (AgedMob) entity;
+            final float ageScale = agedMob.getMobAge().getAgeScale();
+            return baseSize * ageScale;
+        }
+
+        return baseSize;
     }
 
     protected float getLimbSwing(final @NotNull T entity, final float partialTick) {
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererChicken.java b/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererChicken.java
index 329e8e135..4a6dfa0e1 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererChicken.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererChicken.java
@@ -4,6 +4,7 @@ import net.minecraft.core.entity.animal.MobChicken;
 import net.minecraft.core.util.helper.MathHelper;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.lwjgl.opengl.GL11;
 import org.useless.dragonfly.models.entity.BoneTransform;
 import org.useless.dragonfly.models.entity.StaticEntityModel;
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererQuadruped.java b/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererQuadruped.java
index e2bc5c981..21007b1af 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererQuadruped.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererQuadruped.java
@@ -1,10 +1,12 @@
 package net.minecraft.client.render.entity;
 
+import net.minecraft.core.entity.AgedMob;
 import net.minecraft.core.entity.Mob;
 import net.minecraft.core.entity.animal.MobCow;
 import net.minecraft.core.util.helper.MathHelper;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.lwjgl.opengl.GL11;
 import org.useless.dragonfly.models.entity.BoneTransform;
 import org.useless.dragonfly.models.entity.StaticEntityModel;
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/texturepack/TexturePackList.java b/game/client/src/main/java/net/minecraft/client/render/texturepack/TexturePackList.java
index d7749fae6..d1f26d2ba 100644
--- a/game/client/src/main/java/net/minecraft/client/render/texturepack/TexturePackList.java
+++ b/game/client/src/main/java/net/minecraft/client/render/texturepack/TexturePackList.java
@@ -96,6 +96,7 @@ public class TexturePackList
 			refresh();
 		}
 	}
+
 	public void refresh(){
 		mc.textureManager.refreshTexturesAndDisplayErrors();
 		mc.renderGlobal.loadRenderers();
diff --git a/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererSign.java b/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererSign.java
index 3fdff365e..183cec6de 100644
--- a/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererSign.java
+++ b/game/client/src/main/java/net/minecraft/client/render/tileentity/TileEntityRendererSign.java
@@ -40,6 +40,8 @@ public class TileEntityRendererSign extends TileEntityRenderer<TileEntitySign>
         final Block<?> block = tileEntity.getBlock();
         if (block == null || !(block.getLogic() instanceof BlockLogicSign)) return; // Check to make sure the block for the entity is present before rendering, some edge cases have the tile entity loaded before the block data and can cause a crash
 
+        GL11.glColor4f(1, 1, 1, 1);
+
         GL11.glEnable(GL12.GL_RESCALE_NORMAL);
         GL11.glPushMatrix();
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/window/GameWindowGLFW.java b/game/client/src/main/java/net/minecraft/client/render/window/GameWindowGLFW.java
index f0c84768c..edf66c196 100644
--- a/game/client/src/main/java/net/minecraft/client/render/window/GameWindowGLFW.java
+++ b/game/client/src/main/java/net/minecraft/client/render/window/GameWindowGLFW.java
@@ -5,19 +5,26 @@ import static org.lwjgl.glfw.GLFW.*;
 
 import java.awt.*;
 import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.util.HashMap;
 import java.util.Map;
+import java.nio.IntBuffer;
+import java.util.Objects;
 
 import javax.imageio.ImageIO;
 import javax.swing.JFrame;
 
 import com.mojang.logging.CategorizedLogger;
 import com.mojang.logging.LogUtils;
+import com.mojang.logging.LogUtils;
 import net.minecraft.core.Global;
 import net.minecraft.core.util.helper.Color;
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.BufferUtils;
 import org.lwjgl.PointerBuffer;
 import org.lwjgl.glfw.GLFW;
 import org.lwjgl.glfw.GLFWImage;
@@ -35,6 +42,8 @@ import com.github.zarzelcow.legacylwjgl3.implementation.input.InputImplementatio
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.render.PanelCrashReport;
 import net.minecraft.core.UnexpectedThrowable;
+import org.lwjgl.stb.STBImage;
+import org.slf4j.Logger;
 
 public class GameWindowGLFW implements GameWindow {
     public static final CategorizedLogger LOGGER = LogUtils.getLogger();
@@ -122,7 +131,7 @@ public class GameWindowGLFW implements GameWindow {
                 int res = resolutions[i];
                 ByteBuffer icon = getImageBuffer("/icon/" + res + ".png");
                 if (icon == null) {
-                    LOGGER.error("Icon at resolution '" + res + "' is null! Skipping icons!");
+                    LOGGER.error("Icon at resolution '{}' is null! Skipping icons!", res);
                     gb.close();
                     break iconLabel;
                 }
@@ -410,33 +419,36 @@ public class GameWindowGLFW implements GameWindow {
         }
     }
 
+    private static ByteBuffer buffer = BufferUtils.createByteBuffer(1024 * 1024);
     /**
      * Reads the specified image resource and returns the raw data as a ByteBuffer.
      *
      * @param resource the resource to read
      * @return the resource data
      */
-    public static ByteBuffer getImageBuffer(String resource) {
-        try {
-            BufferedImage image = ImageIO.read(GameWindowGLFW.class.getResourceAsStream(resource));
-            ByteBuffer buf = createByteBuffer(image.getWidth() * image.getHeight() * 4);
-            // Manually filling out the damn buffer since I can't find any utility that actually does it properly
-            // I don't understand why icon loading for LWJGL3 is such a pain in the ass too
-            int index = 0;
-            for (int y = 0; y < image.getHeight(); y++) {
-                for (int x = 0; x < image.getWidth(); x++) {
-                    int color = image.getRGB(x, y);
-                    buf.put(index + 0, (byte) Color.redFromInt(color)); // Red
-                    buf.put(index + 1, (byte) Color.greenFromInt(color)); // Green
-                    buf.put(index + 2, (byte) Color.blueFromInt(color)); // Blue
-                    buf.put(index + 3, (byte) Color.alphaFromInt(color)); // Alpha
-                    index += 4;
+    public static @Nullable ByteBuffer getImageBuffer(final String resource) {
+        try(final InputStream stream = GameWindowGLFW.class.getResourceAsStream(resource)) {
+            if (stream == null) {
+                LOGGER.error("No icon could be found at '{}'!", resource);
+                return null;
+            }
+            buffer.clear();
+            int value;
+            while ((value = stream.read()) != -1) {
+                if (buffer.remaining() < 1) {
+                    final ByteBuffer newBuf = BufferUtils.createByteBuffer(buffer.capacity() * 2);
+                    newBuf.put(buffer);
+                    buffer = newBuf;
                 }
+                buffer.put((byte) value);
             }
-            buf.flip();
-            return buf;
-        } catch (Exception e) {
-            LOGGER.warn("Couldn't open icon image.", e);
+            final IntBuffer x = BufferUtils.createIntBuffer(1);
+            final IntBuffer y = BufferUtils.createIntBuffer(1);
+            final IntBuffer comp = BufferUtils.createIntBuffer(1);
+            return (ByteBuffer) Objects.requireNonNull((ByteBuffer) STBImage.stbi_load_from_memory((ByteBuffer) (buffer.flip()), x, y, comp, STBImage.STBI_rgb_alpha),
+                STBImage::stbi_failure_reason);
+        } catch (final Exception e) {
+            LOGGER.error("Couldn't open icon image...", e);
             return null;
         }
     }
diff --git a/game/client/src/main/java/net/minecraft/client/world/chunk/provider/ChunkProviderStatic.java b/game/client/src/main/java/net/minecraft/client/world/chunk/provider/ChunkProviderStatic.java
index 211de19b9..8f93b4bd1 100644
--- a/game/client/src/main/java/net/minecraft/client/world/chunk/provider/ChunkProviderStatic.java
+++ b/game/client/src/main/java/net/minecraft/client/world/chunk/provider/ChunkProviderStatic.java
@@ -17,7 +17,7 @@ import java.util.Arrays;
 public class ChunkProviderStatic
     implements IChunkProvider
 {
-    public static final int CHUNK_RADIUS = 64; // Radius should *always* be a power of 2!
+    public static final int CHUNK_RADIUS = 32; // Radius should *always* be a power of 2!
     public static final int CHUNK_MASK = CHUNK_RADIUS * 2 - 1;
 
     private static final Logger LOGGER = LogUtils.getLogger();
diff --git a/game/client/src/main/resources/assets/minecraft/textures/gui/title/majonk.png b/game/client/src/main/resources/assets/minecraft/textures/gui/title/majonk.png
index 3a5746028..254236c64 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/gui/title/majonk.png and b/game/client/src/main/resources/assets/minecraft/textures/gui/title/majonk.png differ
diff --git a/game/core/build.gradle.kts b/game/core/build.gradle.kts
index c084dc811..90b5c0cbd 100644
--- a/game/core/build.gradle.kts
+++ b/game/core/build.gradle.kts
@@ -21,15 +21,7 @@ run {
 repositories {
     mavenCentral()
     maven("https://libraries.minecraft.net/")
-
-    ivy {
-        url = uri("https://github.com/Better-than-Adventure")
-        patternLayout {
-            artifact("[organisation]/releases/download/[revision]/brigadier-[revision].jar")
-            m2Compatible
-        }
-        metadataSources { artifact() }
-    }
+    maven("https://jitpack.io")
 }
 
 dependencies {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicGrass.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicGrass.java
index a1869363a..59f78ec80 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicGrass.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicGrass.java
@@ -36,8 +36,7 @@ public class BlockLogicGrass extends BlockLogic
                 int x1 = (x + rand.nextInt(3)) - 1;
                 int y1 = (y + rand.nextInt(5)) - 3;
                 int z1 = (z + rand.nextInt(3)) - 1;
-                int above = world.getBlockId(x1, y1 + 1, z1);
-                if (world.getBlockId(x1, y1, z1) == dirt.id() && world.getBlockLightValue(x1, y1 + 1, z1) >= 4 && Blocks.lightBlock[above] <= 2) {
+                if (world.getBlockId(x1, y1, z1) == dirt.id() && world.getBlockLightValue(x1, y1 + 1, z1) >= 4 && Blocks.lightBlock[world.getBlockId(x1, y1 + 1, z1)] <= 2) {
                     world.setBlockWithNotify(x1, y1, z1, block.id());
                 }
             }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java
index a916e98f7..b91269fb9 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicLamp.java
@@ -1,9 +1,12 @@
 package net.minecraft.core.block;
 
 import net.minecraft.core.block.entity.TileEntity;
+import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.DyeColor;
 import net.minecraft.core.util.helper.Side;
 import net.minecraft.core.world.World;
@@ -21,12 +24,12 @@ public class BlockLogicLamp extends BlockLogic implements IPainted {
 
     @Override
     public void onBlockPlacedByWorld(World world, int x, int y, int z) {
-        if (isInverted) {
-            if (isActive && (world.hasDirectSignal(x, y, z) || world.hasNeighborSignal(x, y, z))) {
+        if (this.isInverted) {
+            if (this.isActive && (world.hasDirectSignal(x, y, z) || world.hasNeighborSignal(x, y, z))) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_IDLE.id(), world.getBlockMetadata(x, y, z));
             }
         } else {
-            if (isActive && !(world.hasDirectSignal(x, y, z) || world.hasNeighborSignal(x, y, z))) {
+            if (this.isActive && !(world.hasDirectSignal(x, y, z) || world.hasNeighborSignal(x, y, z))) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_IDLE.id(), world.getBlockMetadata(x, y, z));
             }
         }
@@ -40,16 +43,16 @@ public class BlockLogicLamp extends BlockLogic implements IPainted {
 
         final boolean hasSignal = world.hasDirectSignal(x, y, z) || world.hasNeighborSignal(x, y, z);
 
-        if (isInverted) {
-            if (hasSignal && isActive) {
+        if (this.isInverted) {
+            if (hasSignal && this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_IDLE.id(), world.getBlockMetadata(x, y, z));
-            } else if (!hasSignal && !isActive) {
+            } else if (!hasSignal && !this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_ACTIVE.id(), world.getBlockMetadata(x, y, z));
             }
         } else {
-            if (hasSignal && !isActive) {
+            if (hasSignal && !this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_ACTIVE.id(), world.getBlockMetadata(x, y, z));
-            } else if (!hasSignal && isActive) {
+            } else if (!hasSignal && this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_IDLE.id(), world.getBlockMetadata(x, y, z));
             }
         }
@@ -59,28 +62,60 @@ public class BlockLogicLamp extends BlockLogic implements IPainted {
     public void onNeighborBlockChange(World world, int x, int y, int z, int blockId) {
         final boolean hasSignal = world.hasDirectSignal(x, y, z) || world.hasNeighborSignal(x, y, z);
 
-        if (isInverted) {
-            if (hasSignal && isActive) {
+        if (this.isInverted) {
+            if (hasSignal && this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_IDLE.id(), world.getBlockMetadata(x, y, z));
-            } else if (!hasSignal && !isActive) {
+            } else if (!hasSignal && !this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_ACTIVE.id(), world.getBlockMetadata(x, y, z));
             }
         } else {
-            if (hasSignal && !isActive) {
+            if (hasSignal && !this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_ACTIVE.id(), world.getBlockMetadata(x, y, z));
-            } else if (!hasSignal && isActive) {
+            } else if (!hasSignal && this.isActive) {
                 world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_IDLE.id(), world.getBlockMetadata(x, y, z));
             }
         }
     }
 
     @Override
-    public ItemStack[] getBreakResult(World world, EnumDropCause dropCause, int meta, TileEntity tileEntity) {
-        if (isInverted) {
-            return new ItemStack[] { new ItemStack(Blocks.LAMP_INVERTED_ACTIVE, 1, meta) };
+    public boolean onBlockRightClicked(final World world, final int x, final int y, final int z, final Player player, final Side side, final double xHit, final double yHit) {
+        if (this.isInverted) {
+            if (this.isActive) {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_IDLE.id(), world.getBlockMetadata(x, y, z));
+            } else {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_ACTIVE.id(), world.getBlockMetadata(x, y, z));
+            }
         } else {
-            return new ItemStack[] { new ItemStack(Blocks.LAMP_IDLE, 1, meta) };
+            if (this.isActive) {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_IDLE.id(), world.getBlockMetadata(x, y, z));
+            } else {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_ACTIVE.id(), world.getBlockMetadata(x, y, z));
+            }
         }
+
+        return true;
+    }
+
+    @Override
+    public void onActivatorInteract(final World world, final int x, final int y, final int z, final TileEntityActivator activator, final Direction direction) {
+        if (this.isInverted) {
+            if (this.isActive) {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_IDLE.id(), world.getBlockMetadata(x, y, z));
+            } else {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_ACTIVE.id(), world.getBlockMetadata(x, y, z));
+            }
+        } else {
+            if (this.isActive) {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_IDLE.id(), world.getBlockMetadata(x, y, z));
+            } else {
+                world.setBlockAndMetadataWithNotify(x, y, z, Blocks.LAMP_INVERTED_ACTIVE.id(), world.getBlockMetadata(x, y, z));
+            }
+        }
+    }
+
+    @Override
+    public ItemStack[] getBreakResult(World world, EnumDropCause dropCause, int meta, TileEntity tileEntity) {
+        return new ItemStack[] { new ItemStack(Blocks.LAMP_IDLE, 1, meta) };
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSignPainted.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSignPainted.java
index 7860a4b14..7c9562c06 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSignPainted.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSignPainted.java
@@ -1,5 +1,9 @@
 package net.minecraft.core.block;
 
+import net.minecraft.core.block.entity.TileEntity;
+import net.minecraft.core.enums.EnumDropCause;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.item.Items;
 import net.minecraft.core.util.helper.DyeColor;
 import net.minecraft.core.world.World;
 
@@ -33,4 +37,9 @@ public class BlockLogicSignPainted extends BlockLogicSign implements IPainted{
     public void setColor(World world, int x, int y, int z, DyeColor color) {
         world.setBlockMetadataWithNotify(x, y, z, color.blockMeta << 4 | stripColorFromMetadata(world.getBlockMetadata(x, y, z)));
     }
+
+    @Override
+    public ItemStack[] getBreakResult(final World world, final EnumDropCause dropCause, final int meta, final TileEntity tileEntity) {
+        return new ItemStack[] {new ItemStack(Items.SIGN_PAINTED, 1, fromMetadata(meta).itemMeta)};
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/Blocks.java b/game/core/src/main/java/net/minecraft/core/block/Blocks.java
index 6007f9098..d3b4d5984 100644
--- a/game/core/src/main/java/net/minecraft/core/block/Blocks.java
+++ b/game/core/src/main/java/net/minecraft/core/block/Blocks.java
@@ -1083,7 +1083,7 @@ public final class Blocks {
         .withTags(BlockTags.MINEABLE_BY_PICKAXE);
     public static final Block<BlockLogicSlab> SLAB_COBBLE_NETHERRACK = register("slab.cobble.netherrack", "minecraft:block/slab_cobble_netherrack", 806, (BlockLogicSupplier<BlockLogicSlab>)b -> new BlockLogicSlab(b, Blocks.COBBLE_NETHERRACK))
         .withSound(BlockSounds.STONE).withHardness(0.4F)
-        .withTags(BlockTags.MINEABLE_BY_PICKAXE);
+        .withTags(BlockTags.MINEABLE_BY_PICKAXE, BlockTags.INFINITE_BURN);
     public static final Block<BlockLogicSlab> SLAB_BRICK_NETHERRACK = register("slab.brick.netherrack", "minecraft:block/slab_brick_netherrack", 807, (BlockLogicSupplier<BlockLogicSlab>)b -> new BlockLogicSlab(b, Blocks.BRICK_NETHERRACK))
         .withSound(BlockSounds.STONE).withHardness(0.4F)
         .withTags(BlockTags.MINEABLE_BY_PICKAXE);
@@ -1141,7 +1141,7 @@ public final class Blocks {
     public static final Block<BlockLogicLamp> LAMP_INVERTED_ACTIVE = register("lamp.inverted.active", "minecraft:block/lamp_inverted_active", 853, (BlockLogicSupplier<BlockLogicLamp>)b -> new BlockLogicLamp(b, true, true))
         .withSound(BlockSounds.GLASS).withLightEmission(0.9375F).withHardness(0.5F).withDisabledNeighborNotifyOnMetadataChange()
         .setBlockItem(ItemBlockLamp::new)
-        .withTags(BlockTags.MINEABLE_BY_PICKAXE);
+        .withTags(BlockTags.NOT_IN_CREATIVE_MENU, BlockTags.MINEABLE_BY_PICKAXE);
 
     public static final Block<?> LANTERN_FIREFLY_GREEN = register("lantern.firefly.green", "minecraft:block/lantern_firefly_green", 870,
         (b) -> new BlockLogicLanternFirefly(b, MobFireflyCluster.FireflyColor.GREEN, () -> Items.LANTERN_FIREFLY_GREEN))
@@ -1274,7 +1274,6 @@ public final class Blocks {
         .withSound(BlockSounds.STONE).withHardness(1.5F)
         .withTags(BlockTags.MINEABLE_BY_PICKAXE, BlockTags.NOT_IN_CREATIVE_MENU);
 
-
     public static <T extends BlockLogic> Block<T> register(String key, String namespaceId, int id, BlockLogicSupplier<T> logicSupplier) {
         Block<T> container = new Block<>(id, key, namespaceId, logicSupplier);
 
diff --git a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRule.java b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRule.java
index 62530693d..bcae8c7bd 100644
--- a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRule.java
+++ b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRule.java
@@ -1,31 +1,32 @@
 package net.minecraft.core.data.gamerule;
 
 import com.mojang.nbt.tags.CompoundTag;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class GameRule<T>
 {
-    private final String key;
+    private final @NotNull String key;
     private final T defaultValue;
 
-    public GameRule(String key, T defaultValue)
+    public GameRule(@NotNull final String key, final T defaultValue)
     {
         this.key = key;
         this.defaultValue = defaultValue;
     }
 
-    public String getKey()
+    public @NotNull String getKey()
     {
-        return key;
+        return this.key;
     }
 
     public T getDefaultValue()
     {
-        return defaultValue;
+        return this.defaultValue;
     }
 
-    public abstract void writeToNBT(CompoundTag tag, T value);
+    public abstract void writeToNBT(@NotNull CompoundTag tag, T value);
 
-    public abstract T readFromNBT(CompoundTag tag);
+    public abstract T readFromNBT(@NotNull CompoundTag tag);
 
-    public abstract T parseFromString(String string);
+    public abstract T parseFromString(@NotNull String string);
 }
diff --git a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleBoolean.java b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleBoolean.java
index dc581317d..1d26b64ce 100644
--- a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleBoolean.java
+++ b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleBoolean.java
@@ -3,6 +3,8 @@ package net.minecraft.core.data.gamerule;
 import com.mojang.nbt.tags.ByteTag;
 import com.mojang.nbt.tags.CompoundTag;
 import com.mojang.nbt.tags.Tag;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public class GameRuleBoolean
     extends GameRule<Boolean>
@@ -12,7 +14,7 @@ public class GameRuleBoolean
     }
 
     @Override
-    public void writeToNBT(CompoundTag tag, Boolean value)
+    public void writeToNBT(@NotNull CompoundTag tag, Boolean value)
     {
         ByteTag ruleTag = new ByteTag();
         ruleTag.setValue((byte)(value ? 1 : 0));
@@ -20,7 +22,7 @@ public class GameRuleBoolean
     }
 
     @Override
-    public Boolean readFromNBT(CompoundTag tag)
+    public Boolean readFromNBT(@NotNull CompoundTag tag)
     {
         Tag<?> ruleTag = tag.getTag(getKey());
         if (ruleTag instanceof ByteTag)
@@ -33,7 +35,7 @@ public class GameRuleBoolean
     }
 
     @Override
-    public Boolean parseFromString(String string)
+    public Boolean parseFromString(@NotNull String string)
     {
         if (string.equalsIgnoreCase("true")) return true;
         if (string.equalsIgnoreCase("false")) return false;
@@ -44,7 +46,7 @@ public class GameRuleBoolean
         }
         catch (NumberFormatException e)
         {
-            return null;
+            return getDefaultValue();
         }
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleInteger.java b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleInteger.java
new file mode 100644
index 000000000..23ef1ac6a
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRuleInteger.java
@@ -0,0 +1,41 @@
+package net.minecraft.core.data.gamerule;
+
+import com.mojang.nbt.tags.CompoundTag;
+import com.mojang.nbt.tags.IntTag;
+import com.mojang.nbt.tags.Tag;
+import org.jetbrains.annotations.NotNull;
+
+public class GameRuleInteger extends GameRule<Integer> {
+    public GameRuleInteger(final String key, final Integer defaultValue) {
+        super(key, defaultValue);
+    }
+
+    @Override
+    public void writeToNBT(final @NotNull CompoundTag tag, final Integer value) {
+        final IntTag ruleTag = new IntTag(value);
+        tag.put(getKey(), ruleTag);
+    }
+
+    @Override
+    public Integer readFromNBT(final @NotNull CompoundTag tag) {
+        Tag<?> ruleTag = tag.getTag(getKey());
+        if (ruleTag instanceof IntTag)
+        {
+            return ((IntTag) ruleTag).getValue();
+        }
+
+        return getDefaultValue();
+    }
+
+    @Override
+    public Integer parseFromString(final @NotNull String string) {
+        try
+        {
+            return Integer.parseInt(string);
+        }
+        catch (NumberFormatException e)
+        {
+            return getDefaultValue();
+        }
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRules.java b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRules.java
index 8c0210fae..6a79d2d0a 100644
--- a/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRules.java
+++ b/game/core/src/main/java/net/minecraft/core/data/gamerule/GameRules.java
@@ -16,6 +16,7 @@ public abstract class GameRules
     public static GameRuleBoolean DO_WEATHER_CYCLE = register(new GameRuleBoolean("doWeatherCycle", true));
     public static GameRuleBoolean DO_FIRE_SPREAD = register(new GameRuleBoolean("doFireSpread", true));
     public static GameRuleBoolean INSTANT_HEALING = register(new GameRuleBoolean("instantHealing", false));
+    public static GameRuleInteger RANDOM_TICK_SPEED = register(new GameRuleInteger("randomTickSpeed", 4));
 
     public static <T extends GameRule<?>> T register(T gameRule)
     {
diff --git a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/ItemStackJsonAdapter.java b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/ItemStackJsonAdapter.java
index d30aad4c1..321ff8109 100644
--- a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/ItemStackJsonAdapter.java
+++ b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/ItemStackJsonAdapter.java
@@ -8,6 +8,8 @@ import com.google.gson.JsonParseException;
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 import com.mojang.logging.LogUtils;
+import com.mojang.nbt.NbtIo;
+import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.item.Item;
 import net.minecraft.core.item.ItemStack;
@@ -59,13 +61,17 @@ public class ItemStackJsonAdapter implements JsonDeserializer<ItemStack>, JsonSe
         }
         int amount = 1;
         int meta = 0;
+        CompoundTag tag = null;
         if (obj.has("amount")) {
             amount = obj.get("amount").getAsInt();
         }
         if (obj.has("meta")) {
             meta = obj.get("meta").getAsInt();
         }
-        return new ItemStack(item, amount, meta);
+        if (obj.has("tag")) {
+            tag = NbtIo.fromJson(obj.get("tag"));
+        }
+        return new ItemStack(item, amount, meta, tag);
     }
 
     @Override
@@ -79,6 +85,9 @@ public class ItemStackJsonAdapter implements JsonDeserializer<ItemStack>, JsonSe
         if (verbose || src.getMetadata() != 0) {
             obj.addProperty("meta",src.getMetadata());
         }
+        if (verbose || !src.getData().getValue().isEmpty()) {
+            obj.add("tag", NbtIo.toJson(src.getData()));
+        }
         return obj;
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCraftingShaped.java b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCraftingShaped.java
index 1138fbddf..2a9c91fb3 100644
--- a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCraftingShaped.java
+++ b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCraftingShaped.java
@@ -1,5 +1,6 @@
 package net.minecraft.core.data.registry.recipe.entry;
 
+import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.data.registry.recipe.SearchQuery;
 import net.minecraft.core.data.registry.Registries;
 import net.minecraft.core.data.registry.recipe.HasJsonAdapter;
@@ -188,7 +189,7 @@ public class RecipeEntryCraftingShaped extends RecipeEntryCrafting<RecipeSymbol[
 
     @Override
     public ItemStack getCraftingResult(ContainerCrafting containerCrafting) {
-        return new ItemStack(getOutput().itemID, getOutput().stackSize, getOutput().getMetadata());
+        return getOutput().copy();
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/entity/AgedMob.java b/game/core/src/main/java/net/minecraft/core/entity/AgedMob.java
new file mode 100644
index 000000000..1609ca2c5
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/entity/AgedMob.java
@@ -0,0 +1,7 @@
+package net.minecraft.core.entity;
+
+import org.jetbrains.annotations.NotNull;
+
+public interface AgedMob {
+    @NotNull MobAge getMobAge();
+}
diff --git a/game/core/src/main/java/net/minecraft/core/entity/Entity.java b/game/core/src/main/java/net/minecraft/core/entity/Entity.java
index 342f01bd1..45b628142 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/Entity.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/Entity.java
@@ -185,13 +185,13 @@ public abstract class Entity
         return false;
     }
 
-    protected void setSize(float width, float height)
+    protected final void setSize(float width, float height)
     {
         this.bbWidth = width;
         this.bbHeight = height;
     }
 
-    public void setRot(float yRot, float xRot)
+    public final void setRot(float yRot, float xRot)
     {
         this.yRot = yRot % 360F;
         this.xRot = xRot % 360F;
diff --git a/game/core/src/main/java/net/minecraft/core/entity/Mob.java b/game/core/src/main/java/net/minecraft/core/entity/Mob.java
index 7ac1d3fdd..d58874bc8 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/Mob.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/Mob.java
@@ -153,7 +153,7 @@ public abstract class Mob extends Entity
         this.noAI = SPAWN_NO_AI;
     }
 
-    protected void setTextureIdentifier(final  @NotNull String namespace, final @NotNull String id) {
+    protected final void setTextureIdentifier(final  @NotNull String namespace, final @NotNull String id) {
         this.textureIdentifier = new NamespaceID(namespace, id);
         this.basePath = String.format("/assets/%s/textures/entity/%s/", this.textureIdentifier.namespace, this.textureIdentifier.value);
         this.defaultTexture = this.basePath + "0.png";
@@ -252,23 +252,32 @@ public abstract class Mob extends Entity
         onLabelled();
     }
 
+    protected float getPitchModifier() {
+        if (this instanceof AgedMob) {
+            final float scale = ((AgedMob) this).getMobAge().getAgeScale();
+            return 0.8f - (scale * 0.8f);
+        } else {
+            return 0.0f;
+        }
+    }
+
     public void playLivingSound()
     {
         String s = getLivingSound();
         if(s != null && !world.isClientSide)
         {
-            world.playSoundAtEntity(null, this, s, getSoundVolume(), (random.nextFloat() - random.nextFloat()) * 0.2F + 1.0F);
+            world.playSoundAtEntity(null, this, s, getSoundVolume(), (random.nextFloat() - random.nextFloat()) * 0.2F + 1.0F + getPitchModifier());
         }
     }
     
     public void playHurtSound()
     {
-        world.playSoundAtEntity(null, this, getHurtSound(), getSoundVolume(), (random.nextFloat() - random.nextFloat()) * 0.2F + 1.0F);
+        world.playSoundAtEntity(null, this, getHurtSound(), getSoundVolume(), (random.nextFloat() - random.nextFloat()) * 0.2F + 1.0F + getPitchModifier());
     }
     
     public void playDeathSound()
     {
-        world.playSoundAtEntity(null, this, getDeathSound(), getSoundVolume(), (random.nextFloat() - random.nextFloat()) * 0.2F + 1.0F);
+        world.playSoundAtEntity(null, this, getDeathSound(), getSoundVolume(), (random.nextFloat() - random.nextFloat()) * 0.2F + 1.0F + getPitchModifier());
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/entity/MobAge.java b/game/core/src/main/java/net/minecraft/core/entity/MobAge.java
new file mode 100644
index 000000000..831606235
--- /dev/null
+++ b/game/core/src/main/java/net/minecraft/core/entity/MobAge.java
@@ -0,0 +1,121 @@
+package net.minecraft.core.entity;
+
+import com.mojang.nbt.tags.CompoundTag;
+import net.minecraft.core.Global;
+import net.minecraft.core.util.phys.AABB;
+import net.minecraft.core.world.World;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class MobAge
+{
+    public static @NotNull MobAge newRandom(final @NotNull Mob mob, final int maxAge, final int minRandomAge, final int maxRandomAge) {
+        return new MobAge(
+            mob,
+            maxAge,
+            minRandomAge + mob.random.nextInt(maxRandomAge - minRandomAge)
+        );
+    }
+
+    public static final int YEAR_LENGTH_DAYS = 14 * 4;
+
+    private final @NotNull Mob mob;
+    private final int maxAge;
+    private @Nullable AABB boundingBox = null;
+
+    private int ageDays;
+    private long ticksInDay = 0;
+    private long lastTicked = -1;
+
+    public MobAge(final @NotNull Mob mob, final int maxAge, final int age) {
+        this.mob = mob;
+        this.maxAge = maxAge;
+        this.ageDays = age;
+    }
+
+    public int getAge() {
+        return this.ageDays;
+    }
+
+    public int getMaxAge() {
+        return this.maxAge;
+    }
+
+    public void setAge(final int age) {
+        this.ageDays = age;
+    }
+
+    public boolean isAtMaxAge() {
+        return getAge() >= getMaxAge();
+    }
+
+    public float getAgeScale() {
+        // Ranges from 0.6 to 1.2
+        return 0.6f + (((float) getAge() / (float) getMaxAge()) * 0.6f);
+    }
+
+    public void tick(final @NotNull World world) {
+
+        if (this.boundingBox == null) {
+            final @Nullable AABB mobBB = this.mob.getBb();
+            if (mobBB != null) {
+                this.boundingBox = mobBB.copyPermanent();
+            }
+        }
+        if (isAtMaxAge()) {
+            return;
+        }
+
+        final int currentAge = this.getAge();
+
+        // Update mob age
+        if (this.lastTicked == -1) {
+            this.lastTicked = world.getWorldTime();
+        }
+        if (world.getWorldTime() < this.lastTicked) {
+            this.lastTicked = world.getWorldTime();
+        }
+
+        this.ticksInDay += world.getWorldTime() - this.lastTicked;
+        this.lastTicked = world.getWorldTime();
+        while (this.ticksInDay >= Global.DAY_LENGTH_TICKS) {
+            this.ageDays++;
+            this.ticksInDay -= Global.DAY_LENGTH_TICKS;
+        }
+
+        // Update mob attributes
+        if (currentAge < this.maxAge && this.boundingBox != null) {
+            // Update mob size
+            final float size = getAgeScale();
+
+            this.mob.getBb().minX = this.boundingBox.minX * size;
+            this.mob.getBb().minY = this.boundingBox.minY * size;
+            this.mob.getBb().minZ = this.boundingBox.minZ * size;
+            this.mob.getBb().maxX = this.boundingBox.maxX * size;
+            this.mob.getBb().maxY = this.boundingBox.maxY * size;
+            this.mob.getBb().maxZ = this.boundingBox.maxZ * size;
+        }
+    }
+
+    public void writeTag(final @NotNull CompoundTag mobTag) {
+        final @NotNull CompoundTag ageTag = new CompoundTag();
+        ageTag.putInt("age", this.ageDays);
+        ageTag.putLong("ticksInDay", this.ticksInDay);
+        ageTag.putLong("lastTicked", this.lastTicked);
+
+        mobTag.put("age", ageTag);
+    }
+
+    public void readTag(final @NotNull CompoundTag mobTag) {
+        final @Nullable CompoundTag ageTag = mobTag.getCompoundOrDefault("age", null);
+        if (ageTag == null) {
+            this.ageDays = this.maxAge;
+            this.ticksInDay = 0;
+            this.lastTicked = -1;
+        } else {
+            this.ageDays = ageTag.getInteger("age");
+            this.ticksInDay = ageTag.getLong("ticksInDay");
+            this.lastTicked = ageTag.getLong("lastTicked");
+        }
+    }
+}
diff --git a/game/core/src/main/java/net/minecraft/core/entity/animal/MobChicken.java b/game/core/src/main/java/net/minecraft/core/entity/animal/MobChicken.java
index d42023c55..579cc22b0 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/animal/MobChicken.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/animal/MobChicken.java
@@ -2,14 +2,17 @@ package net.minecraft.core.entity.animal;
 
 import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.WeightedRandomLootObject;
+import net.minecraft.core.entity.AgedMob;
+import net.minecraft.core.entity.MobAge;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.item.Items;
 import net.minecraft.core.item.tag.ItemTags;
-import net.minecraft.core.util.collection.NamespaceID;
 import net.minecraft.core.world.World;
 import org.jetbrains.annotations.NotNull;
 
-public class MobChicken extends MobAnimal
+public class MobChicken
+    extends MobAnimal
+    implements AgedMob
 {
     public float flap;
     public float flapSpeed;
@@ -17,6 +20,7 @@ public class MobChicken extends MobAnimal
     public float oFlap;
     public float flapping;
     public int eggTimer;
+    private final @NotNull MobAge age;
 
     public MobChicken(World world)
     {
@@ -28,7 +32,14 @@ public class MobChicken extends MobAnimal
         setSize(0.4F, 0.8F);
         eggTimer = random.nextInt(6000) + 6000;
         mobDrops.add(new WeightedRandomLootObject(Items.FEATHER_CHICKEN.getDefaultStack(), 0, 2));
+        this.age = MobAge.newRandom(this, MobAge.YEAR_LENGTH_DAYS * 3, 14, (int) (MobAge.YEAR_LENGTH_DAYS * 2.5f));
     }
+
+    @Override
+    public @NotNull MobAge getMobAge() {
+        return this.age;
+    }
+
     @Override
     public int getMaxHealth(){
         return 4;
@@ -65,23 +76,27 @@ public class MobChicken extends MobAnimal
             dropItem(Items.EGG_CHICKEN.id, 1);
             eggTimer = random.nextInt(6000) + 6000;
         }
+
+        this.getMobAge().tick(world);
     }
 
     @Override
-	protected void causeFallDamage(float distance)
-    {
+    public void readAdditionalSaveData(@NotNull final CompoundTag tag) {
+        super.readAdditionalSaveData(tag);
+
+        this.age.readTag(tag);
     }
 
     @Override
-	public void addAdditionalSaveData(@NotNull CompoundTag tag)
-    {
+    public void addAdditionalSaveData(@NotNull final CompoundTag tag) {
         super.addAdditionalSaveData(tag);
+
+        this.age.writeTag(tag);
     }
 
     @Override
-	public void readAdditionalSaveData(@NotNull CompoundTag tag)
+	protected void causeFallDamage(float distance)
     {
-        super.readAdditionalSaveData(tag);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java b/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java
index fc904f58d..7932cf40f 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java
@@ -1,18 +1,22 @@
 package net.minecraft.core.entity.animal;
 
 import net.minecraft.core.WeightedRandomLootObject;
+import net.minecraft.core.entity.AgedMob;
+import net.minecraft.core.entity.MobAge;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.item.Items;
 import net.minecraft.core.item.ItemBucketEmpty;
 import net.minecraft.core.item.ItemStack;
 import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.item.tag.ItemTags;
-import net.minecraft.core.util.collection.NamespaceID;
 import net.minecraft.core.world.World;
 import org.jetbrains.annotations.NotNull;
 
-public class MobCow extends MobAnimal
+public class MobCow
+    extends MobAnimal
+    implements AgedMob
 {
+    private final @NotNull MobAge age;
 
     public MobCow(World world)
     {
@@ -20,18 +24,35 @@ public class MobCow extends MobAnimal
         setTextureIdentifier("minecraft", "cow");
         setSize(0.9F, 1.3F);
         mobDrops.add(new WeightedRandomLootObject(Items.LEATHER.getDefaultStack(), 1, 5));
+        this.age = MobAge.newRandom(this, MobAge.YEAR_LENGTH_DAYS * 5, 14, MobAge.YEAR_LENGTH_DAYS * 4);
+    }
+
+    @Override
+    public void onLivingUpdate() {
+        super.onLivingUpdate();
+
+        this.getMobAge().tick(this.world);
+    }
+
+    @Override
+    public @NotNull MobAge getMobAge() {
+        return this.age;
     }
 
     @Override
 	public void addAdditionalSaveData(@NotNull CompoundTag tag)
     {
         super.addAdditionalSaveData(tag);
+
+        this.age.writeTag(tag);
     }
 
     @Override
 	public void readAdditionalSaveData(@NotNull CompoundTag tag)
     {
         super.readAdditionalSaveData(tag);
+
+        this.age.readTag(tag);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/entity/animal/MobPig.java b/game/core/src/main/java/net/minecraft/core/entity/animal/MobPig.java
index 800b23e3a..a4d546a7a 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/animal/MobPig.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/animal/MobPig.java
@@ -4,7 +4,9 @@ import net.minecraft.core.WeightedRandomLootObject;
 import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.tag.BlockTags;
+import net.minecraft.core.entity.AgedMob;
 import net.minecraft.core.entity.EntityLightning;
+import net.minecraft.core.entity.MobAge;
 import net.minecraft.core.entity.monster.MobZombiePig;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.item.ItemStack;
@@ -17,10 +19,15 @@ import org.jetbrains.annotations.NotNull;
 import java.util.ArrayList;
 import java.util.List;
 
-public class MobPig extends MobAnimal
+public class MobPig
+    extends MobAnimal
+    implements AgedMob
 {
     public static final int DATA_SADDLE_ID = 16;
 
+    public List<WeightedRandomLootObject> burningMobDrops = new ArrayList<>();
+    private final @NotNull MobAge age;
+
     public MobPig(World world)
     {
         super(world);
@@ -28,6 +35,12 @@ public class MobPig extends MobAnimal
         setSize(0.9F, 0.9F);
         mobDrops.add(new WeightedRandomLootObject(Items.FOOD_PORKCHOP_RAW.getDefaultStack(), 1, 2));
         burningMobDrops.add(new WeightedRandomLootObject(Items.FOOD_PORKCHOP_COOKED.getDefaultStack(), 1, 2));
+        age = MobAge.newRandom(this, MobAge.YEAR_LENGTH_DAYS * 4, 14, MobAge.YEAR_LENGTH_DAYS * 3);
+    }
+
+    @Override
+    public @NotNull MobAge getMobAge() {
+        return this.age;
     }
 
     @Override
@@ -40,6 +53,7 @@ public class MobPig extends MobAnimal
     {
         super.addAdditionalSaveData(tag);
         tag.putBoolean("Saddle", getSaddled());
+        this.age.writeTag(tag);
     }
 
     @Override
@@ -47,6 +61,14 @@ public class MobPig extends MobAnimal
     {
         super.readAdditionalSaveData(tag);
         setSaddled(tag.getBoolean("Saddle"));
+        this.age.readTag(tag);
+    }
+
+    @Override
+    public void onLivingUpdate() {
+        super.onLivingUpdate();
+
+        this.age.tick(this.world);
     }
 
     @Override
@@ -145,5 +167,4 @@ public class MobPig extends MobAnimal
 
         return Blocks.blocksList[itemStack.itemID].hasTag(BlockTags.PIGS_FAVOURITE_BLOCK);
     }
-    public List<WeightedRandomLootObject> burningMobDrops = new ArrayList<>();
 }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/animal/MobSheep.java b/game/core/src/main/java/net/minecraft/core/entity/animal/MobSheep.java
index a45ebe472..f4bb60d89 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/animal/MobSheep.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/animal/MobSheep.java
@@ -4,7 +4,9 @@ import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.tag.BlockTags;
+import net.minecraft.core.entity.AgedMob;
 import net.minecraft.core.entity.EntityItem;
+import net.minecraft.core.entity.MobAge;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.enums.EnumBlockSoundEffectType;
 import net.minecraft.core.item.ItemStack;
@@ -18,18 +20,27 @@ import org.jetbrains.annotations.NotNull;
 
 import java.util.Random;
 
-public class MobSheep extends MobAnimal
+public class MobSheep
+    extends MobAnimal
+    implements AgedMob
 {
     public static final int DATA_WOOL_STATE_ID = 16;
     public static final int MASK_WOOL_COLOR = 0b0000_1111;
     public static final int MASK_SHEARED = 0b0001_0000;
     public static final int DATA_EATING_ID = 17;
 
+    private int growthTimer;
+    public int timeSheepEating;
+    public int prevTimeSheepEating;
+    private final @NotNull MobAge age;
+
     public MobSheep(World world)
     {
         super(world);
         setTextureIdentifier("minecraft", "sheep");
         setSize(0.9F, 1.3F);
+
+        age = MobAge.newRandom(this, MobAge.YEAR_LENGTH_DAYS * 4, 14, MobAge.YEAR_LENGTH_DAYS * 3);
     }
 
     @Override
@@ -45,6 +56,7 @@ public class MobSheep extends MobAnimal
     {
         setFleeceColor(getRandomFleeceColor(random));
     }
+
     @Override
     protected void dropDeathItems()
     {
@@ -54,6 +66,12 @@ public class MobSheep extends MobAnimal
             dropItem(new ItemStack(Blocks.WOOL.id(), 1, getFleeceColor().blockMeta), 0.0F);
         }
     }
+
+    @Override
+    public @NotNull MobAge getMobAge() {
+        return this.age;
+    }
+
     @Override
     public boolean interact(@NotNull Player player)
     {
@@ -105,6 +123,8 @@ public class MobSheep extends MobAnimal
         tag.putBoolean("Sheared", getSheared());
         tag.putByte("Color", (byte)getFleeceColor().blockMeta);
         tag.putShort("GrowthTimer", (short) growthTimer);
+
+        this.age.writeTag(tag);
     }
 
     @Override
@@ -114,6 +134,8 @@ public class MobSheep extends MobAnimal
         setSheared(tag.getBoolean("Sheared"));
         setFleeceColor(DyeColor.colorFromBlockMeta(tag.getByte("Color")));
         setGrowthTimer(tag.getShort("GrowthTimer"));
+
+        this.age.readTag(tag);
     }
 
     @Override
@@ -139,7 +161,6 @@ public class MobSheep extends MobAnimal
 		if (getSheared()) {
 			growthTimer++;
 		}
-
 		
 		if (growthTimer > 400) {
 			int blockX = MathHelper.floor(x);
@@ -188,6 +209,8 @@ public class MobSheep extends MobAnimal
 				}
 			}
 		}
+
+        this.age.tick(this.world);
     }
 
     @Override
@@ -289,9 +312,4 @@ public class MobSheep extends MobAnimal
     public void setGrowthTimer(int growthTimer){
         this.growthTimer = growthTimer;
     }
-
-    private int growthTimer;
-    public int timeSheepEating;
-    public int prevTimeSheepEating;
-
 }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/animal/MobWolf.java b/game/core/src/main/java/net/minecraft/core/entity/animal/MobWolf.java
index 39dd8a85e..eeda93f1a 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/animal/MobWolf.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/animal/MobWolf.java
@@ -1,9 +1,6 @@
 package net.minecraft.core.entity.animal;
 
-import net.minecraft.core.entity.Entity;
-import net.minecraft.core.entity.EntityItem;
-import net.minecraft.core.entity.IItemHolding;
-import net.minecraft.core.entity.Mob;
+import net.minecraft.core.entity.*;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.entity.projectile.ProjectileArrow;
 import net.minecraft.core.item.ItemFood;
@@ -25,7 +22,9 @@ import org.jetbrains.annotations.Nullable;
 
 import java.util.*;
 
-public class MobWolf extends MobAnimal implements IItemHolding
+public class MobWolf
+    extends MobAnimal
+    implements IItemHolding, AgedMob
 {
     public static final Map<ArmorMaterial, IArmorItem> ARMOR_MATERIALS = new HashMap<>();
     static {
@@ -53,6 +52,7 @@ public class MobWolf extends MobAnimal implements IItemHolding
     private float timeWolfIsShaking;
     private float prevTimeWolfIsShaking;
     private ItemStack armor = null;
+    private final @NotNull MobAge age;
 
     public MobWolf(World world)
     {
@@ -62,6 +62,7 @@ public class MobWolf extends MobAnimal implements IItemHolding
         setSize(0.8F, 0.8F);
         moveSpeed = 1.1F;
         scoreValue = 500;
+        age = MobAge.newRandom(this, MobAge.YEAR_LENGTH_DAYS * 2, 14, MobAge.YEAR_LENGTH_DAYS * 2);
     }
 
     @Override
@@ -78,6 +79,11 @@ public class MobWolf extends MobAnimal implements IItemHolding
         return super.sendDeathMessage(entityKilledBy) || isWolfTamed();
     }
 
+    @Override
+    public @NotNull MobAge getMobAge() {
+        return this.age;
+    }
+
     @Override
     public void spawnInit() {
         if(random.nextInt(25 /*Used to be 100, but I think having it that high would make it excessively rare*/) == 0){
@@ -183,6 +189,8 @@ public class MobWolf extends MobAnimal implements IItemHolding
             tag.putCompound("Armor", armorTag);
         }
         UUIDHelper.writeToTag(tag, getWolfOwner(), "OwnerUUID");
+
+        age.writeTag(tag);
     }
 
     @Override
@@ -235,6 +243,8 @@ public class MobWolf extends MobAnimal implements IItemHolding
             setWolfOwner(ownerUUID);
             setWolfTamed(true);
         }
+
+        age.readTag(tag);
     }
 
     @Override
@@ -409,6 +419,8 @@ public class MobWolf extends MobAnimal implements IItemHolding
             prevTimeWolfIsShaking = 0.0F;
             world.sendTrackedEntityStatusUpdatePacket(this, (byte)8);
         }
+
+        this.age.tick(this.world);
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/net/NetworkManager.java b/game/core/src/main/java/net/minecraft/core/net/NetworkManager.java
index 56421ff3d..7f7cde819 100644
--- a/game/core/src/main/java/net/minecraft/core/net/NetworkManager.java
+++ b/game/core/src/main/java/net/minecraft/core/net/NetworkManager.java
@@ -14,6 +14,7 @@ public class NetworkManager
     private static final Logger LOGGER = LogUtils.getLogger();
 	private final boolean silent;
 	public static int PACKET_DELAY = 100;
+    public static int TIMEOUT_TIME_TICKS = 90 * 20;
 
     public static final Object threadCounterLock = new Object();
     public static int readThreads;
@@ -66,7 +67,7 @@ public class NetworkManager
         packetListener = nethandler;
         try
         {
-            socket.setSoTimeout(30000);
+            socket.setSoTimeout(TIMEOUT_TIME_TICKS * 1000/20);
             socket.setTrafficClass(0x10 | 0x08);
         }
         catch(SocketException socketexception)
@@ -303,7 +304,7 @@ public class NetworkManager
         }
         if(incoming.isEmpty())
         {
-            if(noInputTicks++ == 1200)
+            if(noInputTicks++ == TIMEOUT_TIME_TICKS)
             {
                 networkShutdown("disconnect.timeout", new Object[0]);
             }
diff --git a/game/core/src/main/java/net/minecraft/core/util/phys/AABB.java b/game/core/src/main/java/net/minecraft/core/util/phys/AABB.java
index 55acecb25..3135a9cb7 100644
--- a/game/core/src/main/java/net/minecraft/core/util/phys/AABB.java
+++ b/game/core/src/main/java/net/minecraft/core/util/phys/AABB.java
@@ -287,6 +287,10 @@ public class AABB
         return getTemporaryBB(minX, minY, minZ, maxX, maxY, maxZ);
     }
 
+    public AABB copyPermanent() {
+        return getPermanentBB(minX, minY, minZ, maxX, maxY, maxZ);
+    }
+
 	public HitResult clip(Vec3 start, Vec3 end) {
 		Vec3 vec32 = start.clipX(end, minX);
 		Vec3 vec3d3 = start.clipX(end, maxX);
diff --git a/game/core/src/main/java/net/minecraft/core/world/SpawnerMobs.java b/game/core/src/main/java/net/minecraft/core/world/SpawnerMobs.java
index 2e9befd64..b80086510 100644
--- a/game/core/src/main/java/net/minecraft/core/world/SpawnerMobs.java
+++ b/game/core/src/main/java/net/minecraft/core/world/SpawnerMobs.java
@@ -99,6 +99,8 @@ public final class SpawnerMobs
                     }
                 }
 
+                if (!world.isChunkLoaded(chunk.x, chunk.z)) continue;
+
                 final int blockX = chunk.x * Chunk.CHUNK_SIZE_X;
                 final int blockZ = chunk.z * Chunk.CHUNK_SIZE_Z;
                 final Biome biome = world.getBlockBiome(blockX, world.getHeightBlocks() - 1, blockZ);
@@ -146,7 +148,7 @@ public final class SpawnerMobs
                         ix += world.rand.nextInt(range) - world.rand.nextInt(range);
                         iy += world.rand.nextInt(2) - world.rand.nextInt(2);
                         iz += world.rand.nextInt(range) - world.rand.nextInt(range);
-                        if (!canCreatureTypeSpawnAtLocation(creatureType, world, ix, iy, iz)) continue;
+                        if (!world.isBlockLoaded(ix, iy, iz) || !canCreatureTypeSpawnAtLocation(creatureType, world, ix, iy, iz)) continue;
 
                         final double dx = ix + 0.5D;
                         final double dy = iy;
diff --git a/game/core/src/main/java/net/minecraft/core/world/World.java b/game/core/src/main/java/net/minecraft/core/world/World.java
index 0ba9e0f16..98f67822b 100644
--- a/game/core/src/main/java/net/minecraft/core/world/World.java
+++ b/game/core/src/main/java/net/minecraft/core/world/World.java
@@ -2093,6 +2093,20 @@ public class World implements WorldSource {
         return null;
     }
 
+    public @Nullable Entity getEntityByID(int id)
+    {
+        for(int i = 0; i < loadedEntityList.size(); i++)
+        {
+            Entity entity = loadedEntityList.get(i);
+            if(entity.id == id)
+            {
+                return entity;
+            }
+        }
+
+        return null;
+    }
+
     public String getNumLoadedEntitiesString()
     {
         return "All: " + loadedEntityList.size();
@@ -2388,7 +2402,7 @@ public class World implements WorldSource {
             }
             
             for (int s = 0; s < Chunk.CHUNK_SECTIONS; s++) {
-                for (int i = 0; i < 10; i++) {
+                for (int i = 0; i < getGameRuleValue(GameRules.RANDOM_TICK_SPEED); i++) {
                     updateLCG = updateLCG * 3 + 0x3c6ef35f;
                     int iRandVal = updateLCG >> 2;
                     int iBlockX = iRandVal & (Chunk.CHUNK_SIZE_X - 1);
diff --git a/game/core/src/main/java/net/minecraft/core/world/config/season/SeasonConfigCycle.java b/game/core/src/main/java/net/minecraft/core/world/config/season/SeasonConfigCycle.java
index a324c291b..903d7c202 100644
--- a/game/core/src/main/java/net/minecraft/core/world/config/season/SeasonConfigCycle.java
+++ b/game/core/src/main/java/net/minecraft/core/world/config/season/SeasonConfigCycle.java
@@ -1,6 +1,7 @@
 package net.minecraft.core.world.config.season;
 
 import net.minecraft.core.world.season.Season;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
 import java.util.Map;
@@ -9,12 +10,12 @@ public class SeasonConfigCycle
     extends SeasonConfig
 {
     private final List<Season> seasons;
-    private final Map<Season, Integer> seasonLengthMap;
+    private final SeasonMapInternal seasonLengthMap;
 
-    protected SeasonConfigCycle(List<Season> seasons, Map<Season, Integer> seasonLengthMap)
+    protected SeasonConfigCycle(List<Season> seasons, @NotNull Map<Season, Integer> seasonLengthMap)
     {
         this.seasons = seasons;
-        this.seasonLengthMap = seasonLengthMap;
+        this.seasonLengthMap = new SeasonMapInternal(seasonLengthMap);
     }
 
     public List<Season> getSeasons()
@@ -26,4 +27,29 @@ public class SeasonConfigCycle
     {
         return seasonLengthMap.getOrDefault(season, 0);
     }
+
+    private static class SeasonMapInternal { // Exists since seasonLengthMap.getOrDefault is slow for how often it can be called
+        private final Season[] seasons;
+        private final int[] lengths;
+        public SeasonMapInternal(final @NotNull Map<Season, Integer> seasonLengthMap) {
+            this.seasons = new Season[seasonLengthMap.size()];
+            this.lengths = new int[seasonLengthMap.size()];
+
+            int i = 0;
+            for (final Map.Entry<Season, Integer> entry : seasonLengthMap.entrySet()) {
+                this.seasons[i] = entry.getKey();
+                this.lengths[i] = entry.getValue();
+                i++;
+            }
+        }
+
+        public int getOrDefault(final Season season, final int defaultValue) {
+            for (int i = 0; i < this.seasons.length; i++) {
+                if (this.seasons[i].equals(season)) {
+                    return this.lengths[i];
+                }
+            }
+            return defaultValue;
+        }
+    }
 }
diff --git a/game/core/src/main/resources/lang/en_US/gui.lang b/game/core/src/main/resources/lang/en_US/gui.lang
index 07fdc652d..a6e2b5d42 100644
--- a/game/core/src/main/resources/lang/en_US/gui.lang
+++ b/game/core/src/main/resources/lang/en_US/gui.lang
@@ -266,6 +266,7 @@ gui.options.page.debug.button.cheat=Enable Cheats
 gui.options.page.debug.button.crash.soft=Crash Soft
 gui.options.page.debug.button.crash.hard=Crash Hard
 gui.options.page.debug.button.music.skip=Force Ambient Music
+gui.options.page.debug.button.garbage_collect=Run Garbage Collector
 
 gui.options.page.search.title=Search
 
diff --git a/game/core/src/main/resources/lang/en_US/tile.lang b/game/core/src/main/resources/lang/en_US/tile.lang
index dd9f90954..04ef7794a 100644
--- a/game/core/src/main/resources/lang/en_US/tile.lang
+++ b/game/core/src/main/resources/lang/en_US/tile.lang
@@ -838,7 +838,7 @@ tile.sign.post.planks.oak.desc=A free-standing wooden sign.
 tile.sign.wall.planks.oak.name=Wooden Wall Sign
 tile.sign.wall.planks.oak.desc=An wooden sign placed on a wall.
 
-tile.sign.wall.planks.oak.painted.name=Mystery Wooden Chest
+tile.sign.wall.planks.oak.painted.name=Mystery Wooden sign
 tile.sign.wall.planks.oak.painted.desc=How did you get this?!
 tile.sign.wall.planks.oak.painted.black.name=Black Wooden Wall Sign
 tile.sign.wall.planks.oak.painted.black.desc=An wooden sign placed on a wall.
diff --git a/game/server/build.gradle.kts b/game/server/build.gradle.kts
index b527a5cd0..212354ef1 100644
--- a/game/server/build.gradle.kts
+++ b/game/server/build.gradle.kts
@@ -14,15 +14,7 @@ val include: Configuration by configurations.creating
 repositories {
     mavenCentral()
     maven("https://libraries.minecraft.net/")
-
-    ivy {
-        url = uri("https://github.com/Better-than-Adventure")
-        patternLayout {
-            artifact("[organisation]/releases/download/[revision]/brigadier-[revision].jar")
-            m2Compatible
-        }
-        metadataSources { artifact() }
-    }
+    maven("https://jitpack.io")
 }
 
 dependencies {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1b50d95b0..afe96605a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,7 +5,7 @@ lwjgl = "3.3.4"
 [libraries]
 jetbrains-annotations = { module = "org.jetbrains:annotations", version = "24.0.0" }
 gson = { module = "com.google.code.gson:gson", version = "2.10.1" }
-brigadier = { module = "bta-brigadier:bta-brigadier", version = "1.0.1"}
+brigadier = { module = "com.github.Better-than-Adventure:bta-brigadier", version = "1.0.1"}
 slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.7" }
 log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
 log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
diff --git a/util/datagen/build.gradle.kts b/util/datagen/build.gradle.kts
index b24a50db5..70d2ee8d4 100644
--- a/util/datagen/build.gradle.kts
+++ b/util/datagen/build.gradle.kts
@@ -4,15 +4,8 @@ plugins {
 
 repositories {
     mavenCentral()
+    maven("https://jitpack.io")
     maven("https://libraries.minecraft.net/")
-    ivy {
-        url = uri("https://github.com/Better-than-Adventure")
-        patternLayout {
-            artifact("[organisation]/releases/download/[revision]/brigadier-[revision].jar")
-            m2Compatible
-        }
-        metadataSources { artifact() }
-    }
 }
 
 dependencies {
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java b/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
index 0b64c0ae7..0482f3a12 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
@@ -1118,8 +1118,8 @@ class WorkbenchGenerator {
         RecipeBuilder.Shapeless(CORE_NAMESPACE)
             .addInput(new ItemStack(Items.DYE, 1, DyeColor.GRAY.itemMeta))
             .addInput(new ItemStack(Items.DYE, 1, DyeColor.GRAY.itemMeta))
-            .addInput(new ItemStack(Items.DYE, 1, DyeColor.BLACK.itemMeta))
-            .create("black_dye_gray_dye_gray_dye_to_light_gray_dye", new ItemStack(Items.DYE, 3, DyeColor.SILVER.itemMeta));
+            .addInput(new ItemStack(Items.DYE, 1, DyeColor.WHITE.itemMeta))
+            .create("white_dye_gray_dye_gray_dye_to_light_gray_dye", new ItemStack(Items.DYE, 3, DyeColor.SILVER.itemMeta));
 
         RecipeBuilder.Shapeless(CORE_NAMESPACE)
             .addInput(new ItemStack(Items.DYE, 1, DyeColor.BLUE.itemMeta))
@@ -1247,20 +1247,6 @@ class WorkbenchGenerator {
                 .create(color.colorID + "_lamp_convert", new ItemStack(Blocks.LAMP_IDLE, 1, color.blockMeta));
         }
 
-        for (DyeColor color : DyeColor.values()) {
-            RecipeBuilder.Shapeless(CORE_NAMESPACE)
-                .addInput(Blocks.LAMP_IDLE, color.blockMeta)
-                .addInput(Blocks.TORCH_REDSTONE_ACTIVE)
-                .create(color.colorID + "_lamp_invert", new ItemStack(Blocks.LAMP_INVERTED_ACTIVE, 1, color.blockMeta));
-        }
-
-        for (DyeColor color : DyeColor.values()) {
-            RecipeBuilder.Shapeless(CORE_NAMESPACE)
-                .addInput("minecraft:lamps_inverted")
-                .addInput(new ItemStack(Items.DYE, 1, color.itemMeta))
-                .create(color.colorID + "_lamp_inverted_convert", new ItemStack(Blocks.LAMP_INVERTED_ACTIVE, 1, color.blockMeta));
-        }
-
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
                 "X",
@@ -1356,7 +1342,6 @@ class WorkbenchGenerator {
         Registries.RECIPES.WORKBENCH.register(String.format(format, "fence_gates_planks"), new RecipeEntryUndyeing(new RecipeSymbol("minecraft:fence_gates_planks"), new ItemStack(Blocks.FENCE_GATE_PLANKS_OAK)));
         Registries.RECIPES.WORKBENCH.register(String.format(format, "trapdoor_planks"), new RecipeEntryUndyeing(new RecipeSymbol("minecraft:trapdoor_planks"), new ItemStack(Blocks.TRAPDOOR_PLANKS_OAK)));
         Registries.RECIPES.WORKBENCH.register(String.format(format, "lamps"), new RecipeEntryUndyeing(new RecipeSymbol("minecraft:lamps"), new ItemStack(Blocks.LAMP_IDLE)));
-        Registries.RECIPES.WORKBENCH.register(String.format(format, "lamps_inverted"), new RecipeEntryUndyeing(new RecipeSymbol("minecraft:lamps_inverted"), new ItemStack(Blocks.LAMP_INVERTED_IDLE)));
         Registries.RECIPES.WORKBENCH.register(String.format(format, "wools"), new RecipeEntryUndyeing(new RecipeSymbol("minecraft:wools"), new ItemStack(Blocks.WOOL)));
         Registries.RECIPES.WORKBENCH.register(String.format(format, "chests"), new RecipeEntryUndyeing(new RecipeSymbol("minecraft:chests"), new ItemStack(Blocks.CHEST_PLANKS_OAK)));
     }
diff --git a/util/nbt/build.gradle.kts b/util/nbt/build.gradle.kts
index bf526f429..b73d22a8e 100644
--- a/util/nbt/build.gradle.kts
+++ b/util/nbt/build.gradle.kts
@@ -13,6 +13,7 @@ dependencies {
     testImplementation(platform("org.junit:junit-bom:5.10.0"))
     testImplementation("org.junit.jupiter:junit-jupiter")
 
+    implementation(libs.gson)
     implementation(libs.jetbrains.annotations)
 }
 
diff --git a/util/nbt/src/main/java/com/mojang/nbt/NbtIo.java b/util/nbt/src/main/java/com/mojang/nbt/NbtIo.java
index c4b20110e..10c6461ad 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/NbtIo.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/NbtIo.java
@@ -1,17 +1,32 @@
 package com.mojang.nbt;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
 import com.mojang.nbt.tags.CompoundTag;
 import com.mojang.nbt.tags.Tag;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
+import java.util.Map;
 import java.util.zip.GZIPInputStream;
 import java.util.zip.GZIPOutputStream;
 
 public class NbtIo
 {
+    public static @NotNull CompoundTag fromJson(@NotNull JsonElement element) throws JsonParseException {
+        if (!element.isJsonObject()) throw new JsonParseException("Root NBT Json element must be a JsonObject!");
+        final CompoundTag compoundTag = new CompoundTag();
+        compoundTag.fromJson(element);
+        return compoundTag;
+    }
+
+    public static @NotNull JsonElement toJson(@NotNull CompoundTag tag) {
+        return tag.toJson();
+    }
+
     @SuppressWarnings("unused")
-    public static CompoundTag readCompressed(InputStream inputStream)
+    public static @NotNull CompoundTag readCompressed(InputStream inputStream)
         throws IOException
     {
         try (DataInputStream datainputstream = new DataInputStream(new GZIPInputStream(inputStream)))
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/ByteArrayTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/ByteArrayTag.java
index b399301c5..2bb14a00f 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/ByteArrayTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/ByteArrayTag.java
@@ -1,5 +1,9 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -26,6 +30,32 @@ public class ByteArrayTag extends Tag<byte @NotNull []>
         dos.write(getValue());
     }
 
+    @Override
+    public void fromJson(@NotNull final JsonElement element) throws JsonParseException {
+        if (!element.isJsonArray()) throw new JsonParseException("ByteArrayTag json value must be a json array!");
+        final JsonArray array = element.getAsJsonArray();
+        final byte[] bytes = new byte[array.size()];
+        for (int i = 0; i < bytes.length; i++) {
+            final JsonElement e = array.get(i);
+            if (!e.isJsonPrimitive()) throw new JsonParseException("ByteArrayTag json value must be a json array values must all be numbers!");
+            final JsonPrimitive p = e.getAsJsonPrimitive();
+            if (!p.isNumber()) throw new JsonParseException("ByteArrayTag json value must be a json array values must all be numbers!");
+            bytes[i] = p.getAsByte();
+        }
+        setValue(bytes);
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        final byte[] bytes = getValue();
+        final JsonArray array = new JsonArray(bytes.length);
+        for (int i = 0; i < bytes.length; i++) {
+            array.add(bytes[i]);
+        }
+        return array;
+    }
+
     @Override
     void read(@NotNull DataInput dis)
         throws IOException
@@ -38,7 +68,7 @@ public class ByteArrayTag extends Tag<byte @NotNull []>
     @Override
     public byte getId()
     {
-        return TAG_BYTE_ARRAY;
+        return TagID.TAG_BYTE_ARRAY.getId();
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/ByteTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/ByteTag.java
index 1e7c306af..de6b1d810 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/ByteTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/ByteTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -34,7 +37,7 @@ public class ByteTag extends Tag<@NotNull Byte>
     @Override
     public byte getId()
     {
-        return TAG_BYTE;
+        return TagID.TAG_BYTE.getId();
     }
 
     @Override
@@ -42,4 +45,19 @@ public class ByteTag extends Tag<@NotNull Byte>
     {
         return "\"" + getTagName() + "\": " + getValue();
     }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid byteTag!");
+        }
+        final JsonPrimitive primitive = json.getAsJsonPrimitive();
+        if (!primitive.isNumber()) throw new JsonParseException("Json element '" + json + "' is not a number!");
+        setValue(primitive.getAsByte());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
+    }
 }
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/CompoundTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/CompoundTag.java
index 8a6d7938e..11008e673 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/CompoundTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/CompoundTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -39,6 +42,26 @@ public class CompoundTag extends Tag<@NotNull Map<String, Tag<?>>>
         dos.writeByte(0);
     }
 
+    @Override
+    public void fromJson(@NotNull JsonElement element) throws JsonParseException {
+        if (!element.isJsonObject()) throw new JsonParseException("CompoundTag element must be a JsonObject!");
+        final JsonObject object = element.getAsJsonObject();
+        getValue().clear();
+        for (final Map.Entry<String, JsonElement> entry : object.entrySet()) {
+            put(entry.getKey(), Tag.deserializeFromJson(entry.getValue()));
+        }
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        final JsonObject out = new JsonObject();
+        for (final Map.Entry<String, Tag<?>> entry : getValue().entrySet()) {
+            out.add(entry.getKey(), Tag.serializeToJson(entry.getValue()));
+        }
+        return out;
+    }
+
 
     @Override
     public void read(@NotNull DataInput dis)
@@ -66,7 +89,7 @@ public class CompoundTag extends Tag<@NotNull Map<String, Tag<?>>>
     @Override
     public byte getId()
     {
-        return TAG_COMPOUND;
+        return TagID.TAG_COMPOUND.getId();
     }
 
     public Tag<?> getTagOrDefault(final String key, Tag<?> defaultTag)
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleArrayTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleArrayTag.java
index afc2b2f3c..a54dabaac 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleArrayTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleArrayTag.java
@@ -1,5 +1,9 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -30,6 +34,28 @@ public class DoubleArrayTag extends Tag<double @NotNull []>
         dos.write(bytes);
     }
 
+    @Override
+    public void fromJson(@NotNull final JsonElement element) throws JsonParseException {
+        if (!element.isJsonArray()) throw new JsonParseException("DoubleArrayTag json value must be a json array!");
+        final JsonArray array = element.getAsJsonArray();
+        final double[] doubles = new double[array.size()];
+        for (int i = 0; i < doubles.length; i++) {
+            doubles[i] = array.get(i).getAsDouble();
+        }
+        setValue(doubles);
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        final double[] doubles = getValue();
+        final JsonArray array = new JsonArray(doubles.length);
+        for (int i = 0; i < doubles.length; i++) {
+            array.add(doubles[i]);
+        }
+        return array;
+    }
+
     @Override
     void read(@NotNull DataInput dis)
         throws IOException
@@ -44,7 +70,7 @@ public class DoubleArrayTag extends Tag<double @NotNull []>
     @Override
     public byte getId()
     {
-        return TAG_DOUBLE_ARRAY;
+        return TagID.TAG_DOUBLE_ARRAY.getId();
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleTag.java
index 890c87c3d..c29321a22 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/DoubleTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -24,6 +27,19 @@ public class DoubleTag extends Tag<@NotNull Double>
         dos.writeDouble(getValue());
     }
 
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid doubleTag!");
+        }
+        setValue(json.getAsDouble());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
+    }
+
     @Override
     void read(@NotNull DataInput dis)
         throws IOException
@@ -34,7 +50,7 @@ public class DoubleTag extends Tag<@NotNull Double>
     @Override
     public byte getId()
     {
-        return TAG_DOUBLE;
+        return TagID.TAG_DOUBLE.getId();
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/EndTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/EndTag.java
index c83e0a9ee..2a5ba8ab5 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/EndTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/EndTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonParseException;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -23,10 +26,21 @@ public class EndTag extends Tag<Void>
     {
     }
 
+    @Override
+    public void fromJson(@NotNull JsonElement element) throws JsonParseException {
+
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        return JsonNull.INSTANCE;
+    }
+
     @Override
     public byte getId()
     {
-        return TAG_END;
+        return TagID.TAG_END.getId();
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/FloatTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/FloatTag.java
index 8266f004a..098f57e94 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/FloatTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/FloatTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -34,7 +37,20 @@ public class FloatTag extends Tag<@NotNull Float>
     @Override
     public byte getId()
     {
-        return TAG_FLOAT;
+        return TagID.TAG_FLOAT.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid floatTag!");
+        }
+        setValue(json.getAsFloat());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/IntTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/IntTag.java
index 03c94a1b6..365e73e34 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/IntTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/IntTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -34,7 +37,22 @@ public class IntTag extends Tag<@NotNull Integer>
     @Override
     public byte getId()
     {
-        return TAG_INT;
+        return TagID.TAG_INT.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid intTag!");
+        }
+        final JsonPrimitive primitive = json.getAsJsonPrimitive();
+        if (!primitive.isNumber()) throw new JsonParseException("Json element '" + json + "' is not a number!");
+        setValue(primitive.getAsInt());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/ListTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/ListTag.java
index b769ac4aa..b5d84173b 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/ListTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/ListTag.java
@@ -1,5 +1,9 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
 import com.mojang.nbt.UnknownTagException;
 import org.jetbrains.annotations.NotNull;
 
@@ -44,6 +48,36 @@ public class ListTag
 
     }
 
+    @Override
+    public void fromJson(@NotNull final JsonElement element) throws JsonParseException {
+        if (!element.isJsonObject()) throw new JsonParseException("ListTag JsonElement must be a json object!");
+        final JsonObject asObject = element.getAsJsonObject();
+        if (!asObject.has("type")) throw new JsonParseException("ListTag must have a defined type!");
+        final TagID type = TagID.fromIdString(asObject.get("type").getAsString());
+        getValue().clear();
+        if (asObject.has("list")) {
+            final JsonArray list = asObject.getAsJsonArray("list");
+            for (final JsonElement e : list) {
+                final Tag<?> entry = type.getNewTag();
+                entry.fromJson(e);
+                addTag(entry);
+            }
+        }
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        final JsonObject output = new JsonObject();
+        output.addProperty("type", TagID.fromId(tagType).getIdString());
+        final JsonArray list = new JsonArray(getValue().size());
+        for (final Tag<?> tag : getValue()) {
+            list.add(tag.toJson());
+        }
+        output.add("list", list);
+        return output;
+    }
+
     @Override
     void read(@NotNull DataInput dis)
         throws IOException
@@ -56,7 +90,7 @@ public class ListTag
         for(int i = 0; i < length; i++)
         {
             try {
-                Tag<?> tag = newTag(tagType);
+                Tag<?> tag = TagID.fromId(tagType).getNewTag();
                 tag.read(dis);
                 getValue().add(tag);
             } catch (IllegalArgumentException illegalArgumentException) {
@@ -68,7 +102,7 @@ public class ListTag
     @Override
     public byte getId()
     {
-        return TAG_LIST;
+        return TagID.TAG_LIST.getId();
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/LongArrayTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/LongArrayTag.java
index 8170d973b..6c55902bd 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/LongArrayTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/LongArrayTag.java
@@ -1,5 +1,9 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.DataInput;
@@ -43,7 +47,33 @@ public class LongArrayTag extends Tag<long @NotNull []>{
 
     @Override
     public byte getId() {
-        return TAG_LONG_ARRAY;
+        return TagID.TAG_LONG_ARRAY.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement element) throws JsonParseException {
+        if (!element.isJsonArray()) throw new JsonParseException("LongArrayTag json value must be a json array!");
+        final JsonArray array = element.getAsJsonArray();
+        final long[] longs = new long[array.size()];
+        for (int i = 0; i < longs.length; i++) {
+            final JsonElement e = array.get(i);
+            if (!e.isJsonPrimitive()) throw new JsonParseException("LongArrayTag json value must be a json array values must all be numbers!");
+            final JsonPrimitive p = e.getAsJsonPrimitive();
+            if (!p.isNumber()) throw new JsonParseException("LongArrayTag json value must be a json array values must all be numbers!");
+            longs[i] = p.getAsLong();
+        }
+        setValue(longs);
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        final long[] longs = getValue();
+        final JsonArray array = new JsonArray(longs.length);
+        for (int i = 0; i < longs.length; i++) {
+            array.add(longs[i]);
+        }
+        return array;
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/LongTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/LongTag.java
index 2dd4bc1f7..d7c9f61a2 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/LongTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/LongTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -34,7 +37,22 @@ public class LongTag extends Tag<@NotNull Long>
     @Override
     public byte getId()
     {
-        return TAG_LONG;
+        return TagID.TAG_LONG.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid longTag!");
+        }
+        final JsonPrimitive primitive = json.getAsJsonPrimitive();
+        if (!primitive.isNumber()) throw new JsonParseException("Json element '" + json + "' is not a number!");
+        setValue(primitive.getAsLong());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/ShortArrayTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/ShortArrayTag.java
index fb202da64..e61702166 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/ShortArrayTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/ShortArrayTag.java
@@ -1,5 +1,9 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -45,7 +49,33 @@ public class ShortArrayTag extends Tag<short @NotNull []>
     @Override
     public byte getId()
     {
-        return TAG_SHORT_ARRAY;
+        return TagID.TAG_SHORT_ARRAY.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement element) throws JsonParseException {
+        if (!element.isJsonArray()) throw new JsonParseException("ShortArrayTag json value must be a json array!");
+        final JsonArray array = element.getAsJsonArray();
+        final short[] shorts = new short[array.size()];
+        for (int i = 0; i < shorts.length; i++) {
+            final JsonElement e = array.get(i);
+            if (!e.isJsonPrimitive()) throw new JsonParseException("ShortArrayTag json value must be a json array values must all be numbers!");
+            final JsonPrimitive p = e.getAsJsonPrimitive();
+            if (!p.isNumber()) throw new JsonParseException("ShortArrayTag json value must be a json array values must all be numbers!");
+            shorts[i] = p.getAsShort();
+        }
+        setValue(shorts);
+    }
+
+    @Override
+    @NotNull
+    public JsonElement toJson() {
+        final short[] shorts = getValue();
+        final JsonArray array = new JsonArray(shorts.length);
+        for (int i = 0; i < shorts.length; i++) {
+            array.add(shorts[i]);
+        }
+        return array;
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/ShortTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/ShortTag.java
index 6a1d17782..bf50c6545 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/ShortTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/ShortTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -34,7 +37,22 @@ public class ShortTag extends Tag<@NotNull Short>
     @Override
     public byte getId()
     {
-        return TAG_SHORT;
+        return TagID.TAG_SHORT.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid shortTag!");
+        }
+        final JsonPrimitive primitive = json.getAsJsonPrimitive();
+        if (!primitive.isNumber()) throw new JsonParseException("Json element '" + json + "' is not a number!");
+        setValue(primitive.getAsShort());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/StringTag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/StringTag.java
index 2972ac776..05fd29499 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/StringTag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/StringTag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
 import org.jetbrains.annotations.NotNull;
 
 import java.io.*;
@@ -34,7 +37,22 @@ public class StringTag extends Tag<@NotNull String>
     @Override
     public byte getId()
     {
-        return TAG_STRING;
+        return TagID.TAG_STRING.getId();
+    }
+
+    @Override
+    public void fromJson(@NotNull final JsonElement json) throws JsonParseException {
+        if (!json.isJsonPrimitive()) {
+            throw new JsonParseException("Json element '" + json + "' is not a valid stringTag!");
+        }
+        final JsonPrimitive primitive = json.getAsJsonPrimitive();
+        if (!primitive.isString()) throw new JsonParseException("Json element '" + json + "' is not a string!");
+        setValue(primitive.getAsString());
+    }
+
+    @Override
+    public @NotNull JsonElement toJson() {
+        return new JsonPrimitive(getValue());
     }
 
     @Override
diff --git a/util/nbt/src/main/java/com/mojang/nbt/tags/Tag.java b/util/nbt/src/main/java/com/mojang/nbt/tags/Tag.java
index cb2918cfc..abf683851 100644
--- a/util/nbt/src/main/java/com/mojang/nbt/tags/Tag.java
+++ b/util/nbt/src/main/java/com/mojang/nbt/tags/Tag.java
@@ -1,5 +1,8 @@
 package com.mojang.nbt.tags;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
 import com.mojang.nbt.UnknownTagException;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -8,24 +11,10 @@ import java.io.*;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 public abstract class Tag<T>
 {
-    static final int TAG_END = 0;
-    static final int TAG_BYTE = 1;
-    static final int TAG_SHORT = 2;
-    static final int TAG_INT = 3;
-    static final int TAG_LONG = 4;
-    static final int TAG_FLOAT = 5;
-    static final int TAG_DOUBLE = 6;
-    static final int TAG_BYTE_ARRAY = 7;
-    static final int TAG_STRING = 8;
-    static final int TAG_LIST = 9;
-    static final int TAG_COMPOUND = 10;
-    static final int TAG_SHORT_ARRAY = 11;
-    static final int TAG_DOUBLE_ARRAY = 12;
-    static final int TAG_LONG_ARRAY = 13;
-
     /**
      * The key of the tag.
      */
@@ -58,6 +47,9 @@ public abstract class Tag<T>
     abstract void write(@NotNull DataOutput dos)
         throws IOException;
 
+    public abstract void fromJson(@NotNull JsonElement element) throws JsonParseException;
+    public abstract @NotNull JsonElement toJson();
+
     /**
      * Fetches the ID of the tag's type.
      * @return The ID of the tag's type.
@@ -116,11 +108,11 @@ public abstract class Tag<T>
     {
         byte typeId = dis.readByte();
 
-        if(typeId == TAG_END) {
+        if(typeId == TagID.TAG_END.getId()) {
             return new EndTag();
         }
         try {
-            Tag<?> tag = newTag(typeId);
+            Tag<?> tag = TagID.fromId(typeId).getNewTag();
             tag.name = dis.readUTF();
             tag.read(dis);
             return tag;
@@ -140,7 +132,7 @@ public abstract class Tag<T>
         throws IOException
     {
         dos.writeByte(tag.getId());
-        if(tag.getId() != TAG_END)
+        if(tag.getId() != TagID.TAG_END.getId())
         {
             dos.writeUTF(tag.getTagName());
             tag.write(dos);
@@ -196,84 +188,100 @@ public abstract class Tag<T>
         throw new IllegalArgumentException("No NBT Tag type for type '" + type + "'!");
     }
 
-    /**
-     * Creates a tag according to the given type ID.
-     * @param type The ID of the type of tag to create.
-     * @return The created tag, or null if the type ID doesn't match any tag types.
-     */
-    @SuppressWarnings("unused")
-    public static @NotNull Tag<?> newTag(byte type) {
-        switch (type) {
-            case TAG_END:
-                return new EndTag();
-            case TAG_BYTE:
-                return new ByteTag();
-            case TAG_SHORT:
-                return new ShortTag();
-            case TAG_INT:
-                return new IntTag();
-            case TAG_LONG:
-                return new LongTag();
-            case TAG_FLOAT:
-                return new FloatTag();
-            case TAG_DOUBLE:
-                return new DoubleTag();
-            case TAG_BYTE_ARRAY:
-                return new ByteArrayTag();
-            case TAG_STRING:
-                return new StringTag();
-            case TAG_LIST:
-                return new ListTag();
-            case TAG_COMPOUND:
-                return new CompoundTag();
-            case TAG_SHORT_ARRAY:
-                return new ShortArrayTag();
-            case TAG_DOUBLE_ARRAY:
-                return new DoubleArrayTag();
-            case TAG_LONG_ARRAY:
-                return new LongArrayTag();
+    public static @NotNull JsonElement serializeToJson(Tag<?> tag) {
+        final JsonObject tagObject = new JsonObject();
+        tagObject.addProperty("type", TagID.fromId(tag.getId()).getIdString());
+        tagObject.add("value", tag.toJson());
+        return tagObject;
+    }
+
+    public static @NotNull Tag<?> deserializeFromJson(@NotNull JsonElement element) throws JsonParseException {
+        if (!element.isJsonObject()) throw new JsonParseException("Tag element must be a JsonObject! " + element);
+        JsonObject object = element.getAsJsonObject();
+        if (!object.has("type")) throw new JsonParseException("Tag element does not have type defined!");
+        String type = object.get("type").getAsString();
+        Tag<?> tag = TagID.fromIdString(type).getNewTag();
+        if (object.has("value")) {
+            tag.fromJson(object.get("value"));
         }
-        throw new IllegalArgumentException("No NBT Tag type for type '" + type + "'!");
+        return tag;
     }
 
-    /**
-     * Gets a string representing a tag's type.
-     * @param type The type ID corresponding to the type of tag.
-     * @return A string representing the tag type ID.
-     */
-    @SuppressWarnings("unused")
-    public static @NotNull String getTagName(byte type)
-    {
-        switch (type) {
-            case TAG_END:
-                return "TAG_End";
-            case TAG_BYTE:
-                return "TAG_Byte";
-            case TAG_SHORT:
-                return "TAG_Short";
-            case TAG_INT:
-                return "TAG_Int";
-            case TAG_LONG:
-                return "TAG_Long";
-            case TAG_FLOAT:
-                return "TAG_Float";
-            case TAG_DOUBLE:
-                return "TAG_Double";
-            case TAG_BYTE_ARRAY:
-                return "TAG_Byte_Array";
-            case TAG_STRING:
-                return "TAG_String";
-            case TAG_LIST:
-                return "TAG_List";
-            case TAG_COMPOUND:
-                return "TAG_Compound";
-            case TAG_SHORT_ARRAY:
-                return "TAG_Short_Array";
-            case TAG_DOUBLE_ARRAY:
-                return "TAG_Double_Array";
-            case TAG_LONG_ARRAY:
-                return "TAG_Long_Array";
+    public enum TagID {
+        TAG_END(0, "TAG_End", EndTag::new),
+        TAG_BYTE(1, "TAG_Byte", ByteTag::new),
+        TAG_SHORT(2, "TAG_Short", ShortTag::new),
+        TAG_INT(3, "TAG_Int", IntTag::new),
+        TAG_LONG(4, "TAG_Long", LongTag::new),
+        TAG_FLOAT(5, "TAG_Float", FloatTag::new),
+        TAG_DOUBLE(6, "TAG_Double", DoubleTag::new),
+        TAG_BYTE_ARRAY(7, "TAG_Byte_Array", ByteArrayTag::new),
+        TAG_STRING(8, "TAG_String", StringTag::new),
+        TAG_LIST(9, "TAG_List", ListTag::new),
+        TAG_COMPOUND(10, "TAG_Compound", CompoundTag::new),
+        TAG_SHORT_ARRAY(11, "TAG_Short_Array", ShortArrayTag::new),
+        TAG_DOUBLE_ARRAY(12, "TAG_Double_Array", DoubleArrayTag::new),
+        TAG_LONG_ARRAY(13, "TAG_Long_Array", LongArrayTag::new);
+
+        private final byte id;
+        private final @NotNull String name;
+        private final @NotNull Supplier<? extends @NotNull Tag<?>> tagSupplier;
+        TagID(final int id, @NotNull final String name, @NotNull final Supplier<? extends @NotNull Tag<?>> tagSupplier) {
+            this.id = (byte) id;
+            this.name = name;
+            this.tagSupplier = tagSupplier;
+        }
+
+        public byte getId() {
+            return this.id;
+        }
+
+        public @NotNull String getIdString() {
+            return this.name;
+        }
+
+        public @NotNull Tag<?> getNewTag() {
+            return this.tagSupplier.get();
+        }
+
+        public static @NotNull TagID fromId(final byte id) {
+            switch (id) {
+                case 0: return TAG_END;
+                case 1: return TAG_BYTE;
+                case 2: return TAG_SHORT;
+                case 3: return TAG_INT;
+                case 4: return TAG_LONG;
+                case 5: return TAG_FLOAT;
+                case 6: return TAG_DOUBLE;
+                case 7: return TAG_BYTE_ARRAY;
+                case 8: return TAG_STRING;
+                case 9: return TAG_LIST;
+                case 10: return TAG_COMPOUND;
+                case 11: return TAG_SHORT_ARRAY;
+                case 12: return TAG_DOUBLE_ARRAY;
+                case 13: return TAG_LONG_ARRAY;
+            }
+            throw new IllegalArgumentException("No NBT Tag type for id '" + id + "'!");
+        }
+
+        public static @NotNull TagID fromIdString(final @NotNull String idString) {
+            switch (idString) {
+                case "TAG_End": return TAG_END;
+                case "TAG_Byte": return TAG_BYTE;
+                case "TAG_Short": return TAG_SHORT;
+                case "TAG_Int": return TAG_INT;
+                case "TAG_Long": return TAG_LONG;
+                case "TAG_Float": return TAG_FLOAT;
+                case "TAG_Double": return TAG_DOUBLE;
+                case "TAG_Byte_Array": return TAG_BYTE_ARRAY;
+                case "TAG_String": return TAG_STRING;
+                case "TAG_List": return TAG_LIST;
+                case "TAG_Compound": return TAG_COMPOUND;
+                case "TAG_Short_Array": return TAG_SHORT_ARRAY;
+                case "TAG_Double_Array": return TAG_DOUBLE_ARRAY;
+                case "TAG_Long_Array": return TAG_LONG_ARRAY;
+            }
+            throw new IllegalArgumentException("No NBT Tag type for type '" + idString + "'!");
         }
-        throw new IllegalArgumentException("No NBT Tag type for type '" + type + "'!");
     }
 }
diff --git a/util/nbt/src/test/java/TestNbtIo.java b/util/nbt/src/test/java/TestNbtIo.java
index 3ba692855..3a480a11b 100644
--- a/util/nbt/src/test/java/TestNbtIo.java
+++ b/util/nbt/src/test/java/TestNbtIo.java
@@ -1,3 +1,8 @@
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.google.gson.stream.JsonWriter;
 import com.mojang.nbt.NbtIo;
 import com.mojang.nbt.tags.ByteArrayTag;
 import com.mojang.nbt.tags.ByteTag;
@@ -16,10 +21,16 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
+import java.io.BufferedOutputStream;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.nio.file.Files;
 
 public class TestNbtIo {
@@ -216,38 +227,38 @@ public class TestNbtIo {
     public void ioTest() {
         File out = new File("./testWork");
         out.mkdirs();
-        File fileOut = new File(out, "test.dat");
+        File fileOutDat = new File(out, "test.dat");
         {
 
             CompoundTag dataTag = new CompoundTag();
             dataTag.putString("Hello", "World!");
 
-            fileOut.delete();
+            fileOutDat.delete();
             try {
-                NbtIo.write(dataTag, new DataOutputStream(Files.newOutputStream(fileOut.toPath())));
+                NbtIo.write(dataTag, new DataOutputStream(Files.newOutputStream(fileOutDat.toPath())));
             } catch (IOException e) {
                 Assertions.fail("Failed to write uncompressed test tag!", e);
             }
             try {
-                CompoundTag readBackTag = NbtIo.read(new DataInputStream(Files.newInputStream(fileOut.toPath())));
+                CompoundTag readBackTag = NbtIo.read(new DataInputStream(Files.newInputStream(fileOutDat.toPath())));
                 Assertions.assertEquals(dataTag, readBackTag);
             } catch (IOException e) {
                 Assertions.fail("Failed to read uncompressed test tag!", e);
             }
-            fileOut.delete();
+            fileOutDat.delete();
 
             try {
-                NbtIo.writeCompressed(dataTag, new DataOutputStream(Files.newOutputStream(fileOut.toPath())));
+                NbtIo.writeCompressed(dataTag, new DataOutputStream(Files.newOutputStream(fileOutDat.toPath())));
             } catch (IOException e) {
                 Assertions.fail("Failed to write compressed test tag!", e);
             }
             try {
-                CompoundTag readBackTag = NbtIo.readCompressed(Files.newInputStream(fileOut.toPath()));
+                CompoundTag readBackTag = NbtIo.readCompressed(Files.newInputStream(fileOutDat.toPath()));
                 Assertions.assertEquals(dataTag, readBackTag);
             } catch (IOException e) {
                 Assertions.fail("Failed to read compressed test tag!", e);
             }
-            fileOut.delete();
+            fileOutDat.delete();
         }
 
         {
@@ -275,32 +286,80 @@ public class TestNbtIo {
             dataTag.put("List", listTag);
 
 
-            fileOut.delete();
+            fileOutDat.delete();
             try {
-                NbtIo.write(dataTag, new DataOutputStream(Files.newOutputStream(fileOut.toPath())));
+                NbtIo.write(dataTag, new DataOutputStream(Files.newOutputStream(fileOutDat.toPath())));
             } catch (IOException e) {
                 Assertions.fail("Failed to write uncompressed test tag!", e);
             }
             try {
-                CompoundTag readBackTag = NbtIo.read(new DataInputStream(Files.newInputStream(fileOut.toPath())));
+                CompoundTag readBackTag = NbtIo.read(new DataInputStream(Files.newInputStream(fileOutDat.toPath())));
                 Assertions.assertEquals(dataTag, readBackTag);
             } catch (IOException e) {
                 Assertions.fail("Failed to read uncompressed test tag!", e);
             }
-            fileOut.delete();
+            fileOutDat.delete();
 
             try {
-                NbtIo.writeCompressed(dataTag, new DataOutputStream(Files.newOutputStream(fileOut.toPath())));
+                NbtIo.writeCompressed(dataTag, new DataOutputStream(Files.newOutputStream(fileOutDat.toPath())));
             } catch (IOException e) {
                 Assertions.fail("Failed to write compressed test tag!", e);
             }
             try {
-                CompoundTag readBackTag = NbtIo.readCompressed(Files.newInputStream(fileOut.toPath()));
+                CompoundTag readBackTag = NbtIo.readCompressed(Files.newInputStream(fileOutDat.toPath()));
                 Assertions.assertEquals(dataTag, readBackTag);
             } catch (IOException e) {
                 Assertions.fail("Failed to read compressed test tag!", e);
             }
-            fileOut.delete();
+            fileOutDat.delete();
+        }
+    }
+
+    @Test
+    @DisplayName("Test JsonIO")
+    public void jsonIoTest() {
+        File out = new File("./testWork");
+        out.mkdirs();
+        File fileOutJson = new File(out, "test.json");
+        {
+            CompoundTag dataTag = new CompoundTag();
+            dataTag.put("String", new StringTag("String"));
+            dataTag.put("Byte", new ByteTag(Byte.MAX_VALUE));
+            dataTag.put("Short", new ShortTag(Short.MAX_VALUE));
+            dataTag.put("Int", new IntTag(Integer.MAX_VALUE));
+            dataTag.put("Long", new LongTag(Long.MAX_VALUE));
+            dataTag.put("Float", new FloatTag(Float.MAX_VALUE));
+            dataTag.put("Double", new DoubleTag(Double.MAX_VALUE));
+            dataTag.put("ByteArray", new ByteArrayTag(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}));
+            dataTag.put("ShortArray", new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}));
+//        tag.put("IntArray", new IntArrayTag(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}));
+            dataTag.put("LongArray", new LongArrayTag(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}));
+//        tag.put("FloatArray", new FloatArrayTag(new float[]{Float.NaN, Float.NEGATIVE_INFINITY, Float.MIN_VALUE, 0, Float.MAX_VALUE, Float.POSITIVE_INFINITY}));
+            dataTag.put("DoubleArray", new DoubleArrayTag(new double[]{Double.NaN, Double.NEGATIVE_INFINITY, Double.MIN_VALUE, 0, Double.MAX_VALUE, Double.POSITIVE_INFINITY}));
+
+            ListTag listTag = new ListTag();
+            listTag.addTag(new IntTag(0));
+            listTag.addTag(new IntTag(1));
+            listTag.addTag(new IntTag(2));
+            listTag.addTag(new IntTag(3));
+            listTag.addTag(new IntTag(4));
+            dataTag.put("List", listTag);
+
+            Gson gson = new GsonBuilder().setPrettyPrinting().create();
+
+            try (FileWriter writer = new FileWriter(fileOutJson)){
+                JsonElement element = NbtIo.toJson(dataTag);
+                writer.write(gson.toJson(element));
+            } catch (IOException e) {
+                Assertions.fail("Failed to write json test tag!", e);
+            }
+            try (InputStream stream = Files.newInputStream(fileOutJson.toPath())) {
+                CompoundTag readBackTag = NbtIo.fromJson(JsonParser.parseReader(new InputStreamReader(stream)));
+                Assertions.assertEquals(dataTag, readBackTag);
+            } catch (IOException e) {
+                Assertions.fail("Failed to read json test tag!", e);
+            }
+            fileOutJson.delete();
         }
     }
 }
diff --git a/util/nbt/src/test/java/TestTag.java b/util/nbt/src/test/java/TestTag.java
index b66c41579..94ec0d38e 100644
--- a/util/nbt/src/test/java/TestTag.java
+++ b/util/nbt/src/test/java/TestTag.java
@@ -26,40 +26,40 @@ import java.util.Map;
 public class TestTag {
     @Test
     public void testTagNames() {
-        Assertions.assertEquals("TAG_End", Tag.getTagName((byte) 0));
-        Assertions.assertEquals("TAG_Byte", Tag.getTagName((byte) 1));
-        Assertions.assertEquals("TAG_Short", Tag.getTagName((byte) 2));
-        Assertions.assertEquals("TAG_Int", Tag.getTagName((byte) 3));
-        Assertions.assertEquals("TAG_Long", Tag.getTagName((byte) 4));
-        Assertions.assertEquals("TAG_Float", Tag.getTagName((byte) 5));
-        Assertions.assertEquals("TAG_Double", Tag.getTagName((byte) 6));
-        Assertions.assertEquals("TAG_Byte_Array", Tag.getTagName((byte) 7));
-        Assertions.assertEquals("TAG_String", Tag.getTagName((byte) 8));
-        Assertions.assertEquals("TAG_List", Tag.getTagName((byte) 9));
-        Assertions.assertEquals("TAG_Compound", Tag.getTagName((byte) 10));
-        Assertions.assertEquals("TAG_Short_Array", Tag.getTagName((byte) 11));
-        Assertions.assertEquals("TAG_Double_Array", Tag.getTagName((byte) 12));
-        Assertions.assertEquals("TAG_Long_Array", Tag.getTagName((byte) 13));
-        Assertions.assertThrows(IllegalArgumentException.class, () -> Tag.getTagName((byte) - 1));
+        Assertions.assertEquals("TAG_End", Tag.TagID.fromId((byte) 0).getIdString());
+        Assertions.assertEquals("TAG_Byte", Tag.TagID.fromId((byte) 1).getIdString());
+        Assertions.assertEquals("TAG_Short", Tag.TagID.fromId((byte) 2).getIdString());
+        Assertions.assertEquals("TAG_Int", Tag.TagID.fromId((byte) 3).getIdString());
+        Assertions.assertEquals("TAG_Long", Tag.TagID.fromId((byte) 4).getIdString());
+        Assertions.assertEquals("TAG_Float", Tag.TagID.fromId((byte) 5).getIdString());
+        Assertions.assertEquals("TAG_Double", Tag.TagID.fromId((byte) 6).getIdString());
+        Assertions.assertEquals("TAG_Byte_Array", Tag.TagID.fromId((byte) 7).getIdString());
+        Assertions.assertEquals("TAG_String", Tag.TagID.fromId((byte) 8).getIdString());
+        Assertions.assertEquals("TAG_List", Tag.TagID.fromId((byte) 9).getIdString());
+        Assertions.assertEquals("TAG_Compound", Tag.TagID.fromId((byte) 10).getIdString());
+        Assertions.assertEquals("TAG_Short_Array", Tag.TagID.fromId((byte) 11).getIdString());
+        Assertions.assertEquals("TAG_Double_Array", Tag.TagID.fromId((byte) 12).getIdString());
+        Assertions.assertEquals("TAG_Long_Array", Tag.TagID.fromId((byte) 13).getIdString());
+        Assertions.assertThrows(IllegalArgumentException.class, () -> Tag.TagID.fromId((byte) - 1).getIdString());
     }
 
     @Test
     public void testTagTypes() {
-        Assertions.assertInstanceOf(EndTag.class, Tag.newTag((byte) 0));
-        Assertions.assertInstanceOf(ByteTag.class, Tag.newTag((byte) 1));
-        Assertions.assertInstanceOf(ShortTag.class, Tag.newTag((byte) 2));
-        Assertions.assertInstanceOf(IntTag.class, Tag.newTag((byte) 3));
-        Assertions.assertInstanceOf(LongTag.class, Tag.newTag((byte) 4));
-        Assertions.assertInstanceOf(FloatTag.class, Tag.newTag((byte) 5));
-        Assertions.assertInstanceOf(DoubleTag.class, Tag.newTag((byte) 6));
-        Assertions.assertInstanceOf(ByteArrayTag.class, Tag.newTag((byte) 7));
-        Assertions.assertInstanceOf(StringTag.class, Tag.newTag((byte) 8));
-        Assertions.assertInstanceOf(ListTag.class, Tag.newTag((byte) 9));
-        Assertions.assertInstanceOf(CompoundTag.class, Tag.newTag((byte) 10));
-        Assertions.assertInstanceOf(ShortArrayTag.class, Tag.newTag((byte) 11));
-        Assertions.assertInstanceOf(DoubleArrayTag.class, Tag.newTag((byte) 12));
-        Assertions.assertInstanceOf(LongArrayTag.class, Tag.newTag((byte) 13));
-        Assertions.assertThrows(IllegalArgumentException.class, () -> Tag.newTag((byte) - 1));
+        Assertions.assertInstanceOf(EndTag.class, Tag.TagID.fromId((byte) 0).getNewTag());
+        Assertions.assertInstanceOf(ByteTag.class, Tag.TagID.fromId((byte) 1).getNewTag());
+        Assertions.assertInstanceOf(ShortTag.class, Tag.TagID.fromId((byte) 2).getNewTag());
+        Assertions.assertInstanceOf(IntTag.class, Tag.TagID.fromId((byte) 3).getNewTag());
+        Assertions.assertInstanceOf(LongTag.class, Tag.TagID.fromId((byte) 4).getNewTag());
+        Assertions.assertInstanceOf(FloatTag.class, Tag.TagID.fromId((byte) 5).getNewTag());
+        Assertions.assertInstanceOf(DoubleTag.class, Tag.TagID.fromId((byte) 6).getNewTag());
+        Assertions.assertInstanceOf(ByteArrayTag.class, Tag.TagID.fromId((byte) 7).getNewTag());
+        Assertions.assertInstanceOf(StringTag.class, Tag.TagID.fromId((byte) 8).getNewTag());
+        Assertions.assertInstanceOf(ListTag.class, Tag.TagID.fromId((byte) 9).getNewTag());
+        Assertions.assertInstanceOf(CompoundTag.class, Tag.TagID.fromId((byte) 10).getNewTag());
+        Assertions.assertInstanceOf(ShortArrayTag.class, Tag.TagID.fromId((byte) 11).getNewTag());
+        Assertions.assertInstanceOf(DoubleArrayTag.class, Tag.TagID.fromId((byte) 12).getNewTag());
+        Assertions.assertInstanceOf(LongArrayTag.class, Tag.TagID.fromId((byte) 13).getNewTag());
+        Assertions.assertThrows(IllegalArgumentException.class, () -> Tag.TagID.fromId((byte) - 1).getNewTag());
     }
 
     @Test
diff --git a/util/seed-viewer/build.gradle.kts b/util/seed-viewer/build.gradle.kts
index b71d5edae..ed88b92e6 100644
--- a/util/seed-viewer/build.gradle.kts
+++ b/util/seed-viewer/build.gradle.kts
@@ -13,15 +13,8 @@ val include: Configuration by configurations.creating
 
 repositories {
     mavenCentral()
+    maven("https://jitpack.io")
     maven("https://libraries.minecraft.net/")
-    ivy {
-        url = uri("https://github.com/Better-than-Adventure")
-        patternLayout {
-            artifact("[organisation]/releases/download/[revision]/brigadier-[revision].jar")
-            m2Compatible
-        }
-        metadataSources { artifact() }
-    }
 }
 
 dependencies {
