diff --git a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java
index e68589a8f..ec0cb4155 100644
--- a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java
+++ b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocal.java
@@ -33,6 +33,7 @@ import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.entity.vehicle.EntityBoat;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.lang.I18n;
+import net.minecraft.core.net.ChatEmotes;
 import net.minecraft.core.net.command.TextFormatting;
 import net.minecraft.client.net.command.ClientCommandSource;
 import net.minecraft.core.player.Session;
@@ -336,7 +337,7 @@ public class PlayerLocal extends Player {
                 this.mc.thePlayer.sendMessage(TextFormatting.RED + e.getMessage());
             }
         } else {
-            this.mc.hudIngame.addChatMessage("<" + getDisplayName() + TextFormatting.RESET + "> " + TextFormatting.WHITE + s);
+            this.mc.hudIngame.addChatMessage("<" + getDisplayName() + TextFormatting.RESET + "> " + TextFormatting.WHITE + ChatEmotes.process(s));
         }
     }
 
diff --git a/game/client/src/main/java/net/minecraft/client/gui/IntegerSliderElement.java b/game/client/src/main/java/net/minecraft/client/gui/IntegerSliderElement.java
index 950bb3256..71b58e899 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/IntegerSliderElement.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/IntegerSliderElement.java
@@ -60,7 +60,7 @@ public class IntegerSliderElement extends ButtonElement {
         int maxValue = this.option.size() - 1;
         if (this.dragging) {
             int segments = maxValue;
-            float percentage = (float) (mouseX - (this.xPosition + 4) - (this.width / (2 * segments))) / (float) (this.width - sliderWidth);
+            float percentage = (float)(mouseX - (this.xPosition + 4) - this.width / (this.width * maxValue)) / (float)(this.width - sliderWidth);
 
             if (percentage > 1.0f)
                 percentage = 1.0f;
diff --git a/game/client/src/main/java/net/minecraft/client/gui/chat/GuiElementChatEmotePicker.java b/game/client/src/main/java/net/minecraft/client/gui/chat/GuiElementChatEmotePicker.java
index 7d95ca20e..58a6a410b 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/chat/GuiElementChatEmotePicker.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/chat/GuiElementChatEmotePicker.java
@@ -11,6 +11,9 @@ import java.util.Map;
 public class GuiElementChatEmotePicker extends Gui {
     private static final int CELL_WIDTH = 16;
     private static final int CELL_HEIGHT = 16;
+    private static final int SCROLLBAR_WIDTH = 2;
+    private static final int BACKGROUND_COLOR = 0xC0000000;
+    private static final int SCROLLBAR_COLOR = 0xFF808080;
 
     private final Minecraft mc;
     private final ScreenChat parent;
@@ -34,8 +37,8 @@ public class GuiElementChatEmotePicker extends Gui {
         this.height = yCells * CELL_HEIGHT;
 
         this.yCellsInner = (int) Math.ceil((double) ChatEmotes.getEmotes().size() / xCells);
-        this.heightInner = yCellsInner * CELL_HEIGHT;
-        this.maxScroll = heightInner - height;
+        this.heightInner = Math.max(yCellsInner * CELL_HEIGHT, 1);
+        this.maxScroll = Math.max(heightInner - height, 0);
 
         this.emotes = new Map.Entry[yCellsInner][xCells];
         int i = 0;
@@ -65,9 +68,19 @@ public class GuiElementChatEmotePicker extends Gui {
     }
 
     public void draw(int x, int y, int mouseX, int mouseY) {
+        draw(x, y, mouseX, mouseY, 1.0f, x, y);
+    }
+
+
+    public void draw(int x, int y, int mouseX, int mouseY, float scale, int anchorX, int anchorY) {
         drawOuter(x, y);
 
-        Scissor.enable(x, y, width, height);
+        Scissor.enable(
+            Math.round(anchorX + (x - anchorX) * scale),
+            Math.round(anchorY + (y - anchorY) * scale),
+            Math.round(width * scale),
+            Math.round(height * scale)
+        );
         drawInner(x, y, mouseX, mouseY);
         Scissor.disable();
     }
@@ -90,19 +103,21 @@ public class GuiElementChatEmotePicker extends Gui {
     }
 
     private void drawOuter(int x, int y) {
-        drawRect(x, y, x + width + 2, y + height, 0xFF000000);
+        drawRect(x, y, x + width + SCROLLBAR_WIDTH, y + height, BACKGROUND_COLOR);
 
-        // Draw scrollbar
+        // Draw scrollbar, unless everything already fits
+        if (maxScroll <= 0) return;
         int scrollbarHeight = height * height / heightInner;
         int scrollbarY = y + scroll * (height - scrollbarHeight) / maxScroll;
-        drawRect(x + width, scrollbarY, x + width + 2, scrollbarY + scrollbarHeight, 0xFF808080);
+        drawRect(x + width, scrollbarY, x + width + SCROLLBAR_WIDTH, scrollbarY + scrollbarHeight, SCROLLBAR_COLOR);
     }
 
     private void drawInner(int x, int y, int mouseX, int mouseY) {
         mouseX = mouseX - x;
         mouseY = mouseY - y;
-        int cellX = mouseX / CELL_WIDTH;
-        int cellY = (mouseY + scroll) / CELL_HEIGHT;
+        boolean mouseInside = mouseX >= 0 && mouseX < width && mouseY >= 0 && mouseY < height;
+        int cellX = mouseInside ? mouseX / CELL_WIDTH : -1;
+        int cellY = mouseInside ? (mouseY + scroll) / CELL_HEIGHT : -1;
 
         for (int cy = 0; cy < emotes.length; cy++) {
             Map.Entry<String, Character>[] row = emotes[cy];
diff --git a/game/client/src/main/java/net/minecraft/client/gui/chat/ScreenChat.java b/game/client/src/main/java/net/minecraft/client/gui/chat/ScreenChat.java
index 94e1074de..33e0470ac 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/chat/ScreenChat.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/chat/ScreenChat.java
@@ -3,6 +3,7 @@ package net.minecraft.client.gui.chat;
 import com.mojang.logging.LogUtils;
 import net.minecraft.client.gui.Screen;
 import net.minecraft.client.gui.hud.component.ComponentAnchor;
+import net.minecraft.client.gui.hud.component.HudComponentChatInput;
 import net.minecraft.client.gui.hud.component.HudComponents;
 import net.minecraft.client.gui.keyboard.ScreenKeyboard;
 import net.minecraft.client.gui.text.TextFieldEditor;
@@ -17,6 +18,9 @@ import net.minecraft.client.gui.text.ITextField;
 import org.lwjgl.input.Mouse;
 import org.slf4j.Logger;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class ScreenChat extends Screen implements ITextField {
 	protected static final Logger LOGGER = LogUtils.getLogger();
 	public String message;
@@ -27,6 +31,7 @@ public class ScreenChat extends Screen implements ITextField {
 	protected GuiElementChatEmotePicker emotePicker;
 	protected boolean showEmotePicker = false;
 	private GuiElementChatSuggestions suggestionsElement;
+	private final List<Integer> lineStarts = new ArrayList<>();
 
 	public ScreenChat() {
 		this("");
@@ -37,6 +42,7 @@ public class ScreenChat extends Screen implements ITextField {
         this.updateCounter = 0;
         this.editor = new TextFieldEditor(this);
         this.editor.setCursor(10000);
+        this.lineStarts.add(0);
 	}
 
 	@Override
@@ -50,7 +56,7 @@ public class ScreenChat extends Screen implements ITextField {
                 this.mc,
                 this.editor,
 				this,
-				(parent, child, minecraft, followParameters) -> HudComponents.CHAT_INPUT.getLayout().getComponentX(HudComponents.CHAT_INPUT, minecraft.resolution.getScaledWidthScreenCoords()) + 14 + (followParameters ? this.suggestionsElement.getDefaultParameterPosition() - 1 : 0),
+				(parent, child, minecraft, followParameters) -> HudComponents.CHAT_INPUT.getLayout().getComponentX(HudComponents.CHAT_INPUT, minecraft.resolution.getScaledWidthScreenCoords()) + HudComponentChatInput.FIELD_X_OFFSET + (followParameters ? getFieldXOffset(MathHelper.clamp(this.suggestionsElement.getSuggestionRangeStart(), 0, this.message.length())) - 1 : 0),
 				(parent, child, minecraft, followParameters) -> HudComponents.CHAT_INPUT.getLayout().getComponentY(HudComponents.CHAT_INPUT, minecraft.resolution.getScaledHeightScreenCoords()) - 2,
 				ComponentAnchor.BOTTOM_LEFT
 			);
@@ -83,6 +89,7 @@ public class ScreenChat extends Screen implements ITextField {
 	public void removed() {
 		Keyboard.enableRepeatEvents(false);
         HudComponents.CHAT.chatScroll = 0;
+        HudComponents.CHAT_INPUT.setLineCount(1);
 	}
 
 	@Override
@@ -206,19 +213,26 @@ public class ScreenChat extends Screen implements ITextField {
 	@Override
 	public void render(int mx, int my, float partialTick) {
 		try {
-			int inputX = HudComponents.CHAT_INPUT.getLayout().getComponentX(HudComponents.CHAT_INPUT, this.width);
-			int inputY = HudComponents.CHAT_INPUT.getLayout().getComponentY(HudComponents.CHAT_INPUT, this.height);
-			float scale = HudComponents.CHAT_INPUT.getScale();
-			int boxWidth = HudComponents.CHAT_INPUT.getBaseXSize();
+			HudComponentChatInput input = HudComponents.CHAT_INPUT;
+
+			updateWrappedLines(input.getTextWrapWidth());
+			input.setLineCount(this.lineStarts.size());
+
+			int inputX = input.getLayout().getComponentX(input, this.width);
+			int inputY = input.getLayout().getComponentY(input, this.height);
+			float scale = input.getScale();
+			int boxWidth = input.getBaseXSize();
+			int boxHeight = input.getBoxHeight();
 
 			// Convert mouse coords to component-local (unscaled) space for hit-testing.
 			int localMx = unscaleX(mx, inputX, scale);
 			int localMy = unscaleY(my, inputY, scale);
 
-			// Draw emote picker button
+			// Draw emote picker button, kept level with the bottom line of the box
+			int buttonY = inputY + boxHeight - 12;
 			int emotePickerButtonColor = 0x80000000;
 			int emotePickerTextColor = 0xE0E0E0;
-			if (localMx >= inputX && localMx < inputX + 12 && localMy >= inputY && localMy < inputY + 12)
+			if (localMx >= inputX && localMx < inputX + 12 && localMy >= buttonY && localMy < buttonY + 12)
 			{
 				emotePickerButtonColor = 0x80ffffff;
 				emotePickerTextColor = 0xffffffa0;
@@ -232,27 +246,29 @@ public class ScreenChat extends Screen implements ITextField {
 				GLRenderer.modelM4f().translate(-inputX, -inputY, 0.0f);
 			}
 
-			drawRect(inputX, inputY, inputX + 12, inputY + 12, emotePickerButtonColor);
-			drawStringCenteredShadow(this.fontRenderer, Character.toString('\u263a'), inputX + 6, inputY + 2, emotePickerTextColor);
+			drawRect(inputX, buttonY, inputX + 12, buttonY + 12, emotePickerButtonColor);
+			drawStringCenteredShadow(this.fontRenderer, Character.toString('\u263a'), inputX + 6, buttonY + 2, emotePickerTextColor);
+
+			drawRect(inputX + HudComponentChatInput.FIELD_X_OFFSET, inputY, inputX + boxWidth, inputY + boxHeight, 0x80000000);
+
+			int suggestionStart = MathHelper.clamp(this.suggestionsElement.getSuggestionRangeStart(), 0, this.message.length());
+			drawStringShadow(this.fontRenderer, this.suggestionsElement.getSuggestionPreview(),
+				inputX + HudComponentChatInput.FIELD_X_OFFSET + getFieldXOffset(suggestionStart),
+				inputY + 2 + getFieldYOffset(suggestionStart), 0xFFFFFF);
 
-			drawRect(inputX + 14, inputY, inputX + boxWidth, inputY + 12, 0x80000000);
+			drawWrappedMessage(inputX, inputY);
 
-			drawStringShadow(this.fontRenderer, this.suggestionsElement.getSuggestionPreview(), inputX + 14 + this.suggestionsElement.getDefaultParameterPosition(), inputY + 2, 0xFFFFFF);
 			boolean drawCursor = (this.updateCounter / 6) % 2 == 0;
-			int cursor = this.editor.getCursor();
-			drawStringShadow(this.fontRenderer, this.suggestionsElement.colorCodeText(this.message, true), inputX + 16, inputY + 2, 0xE0E0E0);
 			if (drawCursor)
 			{
-				int width = MathHelper.ceil(this.fontRenderer.stringWidthDouble(this.message));
-				if (cursor < this.message.length())
-				{
-					width = MathHelper.ceil(this.fontRenderer.stringWidthDouble(this.message.substring(0, cursor)));
-				}
-				drawStringShadow(this.fontRenderer, "_", inputX + 16 + width, inputY + 2, 0xE0E0E0);
+				int cursor = MathHelper.clamp(this.editor.getCursor(), 0, this.message.length());
+				drawStringShadow(this.fontRenderer, "_",
+					inputX + HudComponentChatInput.FIELD_X_OFFSET + getFieldXOffset(cursor),
+					inputY + 2 + getFieldYOffset(cursor), 0xE0E0E0);
 			}
 
 			if (this.showEmotePicker) {
-                this.emotePicker.draw(inputX, inputY - 2 - this.emotePicker.getHeight(), localMx, localMy);
+                this.emotePicker.draw(inputX, inputY - 2 - this.emotePicker.getHeight(), localMx, localMy, scale, inputX, inputY);
 			}
 
 			if (scaled) {
@@ -278,19 +294,130 @@ public class ScreenChat extends Screen implements ITextField {
 		return Math.round(anchorY + (screenY - anchorY) / scale);
 	}
 
+	/**
+	 * Word wraps {@link #message} into {@link #lineStarts}. Unlike the font renderer's own splitting
+	 * this keeps indices into the original string.
+	 */
+	private void updateWrappedLines(int wrapWidth) {
+		this.lineStarts.clear();
+		this.lineStarts.add(0);
+
+		double lineWidth = 0;
+		double wordWidth = 0;
+		int wordStart = 0;
+		for (int i = 0; i < this.message.length(); i++) {
+			char c = this.message.charAt(i);
+			// format codes don't take up any space
+			if (c == '§' && i + 1 < this.message.length()) {
+				i++;
+				continue;
+			}
+
+			double charWidth = this.fontRenderer.stringWidthDouble(String.valueOf(c));
+			if (c == ' ') {
+				lineWidth += wordWidth + charWidth;
+				wordWidth = 0;
+				wordStart = i + 1;
+				continue;
+			}
+
+			if (lineWidth + wordWidth + charWidth > wrapWidth) {
+				int currentLineStart = this.lineStarts.get(this.lineStarts.size() - 1);
+				if (wordStart > currentLineStart) {
+					// Carry the whole word down to the next line
+					this.lineStarts.add(wordStart);
+					lineWidth = 0;
+				} else if (i > currentLineStart) {
+					// single word is too long, so break it midway
+					this.lineStarts.add(i);
+					wordStart = i;
+					wordWidth = 0;
+					lineWidth = 0;
+				}
+			}
+			wordWidth += charWidth;
+		}
+	}
+
+	/** Index of the wrapped line that the given index into {@link #message} sits on. */
+	private int getLineForIndex(int index) {
+		for (int i = this.lineStarts.size() - 1; i > 0; i--) {
+			if (this.lineStarts.get(i) <= index) return i;
+		}
+		return 0;
+	}
+
+	/** X offset of an index into {@link #message}, measured from the left edge of the text box. */
+	private int getFieldXOffset(int index) {
+		int lineStart = this.lineStarts.get(getLineForIndex(index));
+		return 2 + MathHelper.ceil(this.fontRenderer.stringWidthDouble(TextFieldEditor.substring(this.message, lineStart, index)));
+	}
+
+	/** Y offset of an index into {@link #message}, measured from the first line of the text box. */
+	private int getFieldYOffset(int index) {
+		return getLineForIndex(index) * HudComponentChatInput.LINE_HEIGHT;
+	}
+
+	private void drawWrappedMessage(int inputX, int inputY) {
+		String colored = this.suggestionsElement.colorCodeText(this.message, true);
+		int[] coloredIndices = mapRawIndicesToColored(colored, this.message.length());
+
+		for (int line = 0; line < this.lineStarts.size(); line++) {
+			int start = this.lineStarts.get(line);
+			int end = line + 1 < this.lineStarts.size() ? this.lineStarts.get(line + 1) : this.message.length();
+			String text = TextFieldEditor.substring(colored, coloredIndices[start], coloredIndices[end]);
+
+			drawStringShadow(this.fontRenderer, formattingAt(colored, coloredIndices[start]) + text,
+				inputX + HudComponentChatInput.FIELD_X_OFFSET + 2,
+				inputY + 2 + line * HudComponentChatInput.LINE_HEIGHT, 0xE0E0E0);
+		}
+	}
+
+	/** Maps each index of the raw message onto the matching index of its color coded form.*/
+	private static int[] mapRawIndicesToColored(String colored, int rawLength) {
+		int[] indices = new int[rawLength + 1];
+		int raw = 0;
+		int i = 0;
+		while (i < colored.length() && raw < rawLength) {
+			if (colored.charAt(i) == '§' && i + 1 < colored.length()) {
+				i += 2;
+				continue;
+			}
+			indices[raw++] = i++;
+		}
+		while (raw <= rawLength) {
+			indices[raw++] = colored.length();
+		}
+		return indices;
+	}
+
+	/** The last formatting code appearing before the given index, or an empty string if there is none. */
+	private static String formattingAt(String colored, int index) {
+		String formatting = "";
+		for (int i = 0; i + 1 < Math.min(index, colored.length()); i++) {
+			if (colored.charAt(i) == '§') {
+				formatting = colored.substring(i, i + 2);
+				i++;
+			}
+		}
+		return formatting;
+	}
+
 	@Override
 	public void mouseClicked(int mx, int my, int buttonNum) {
 		super.mouseClicked(mx, my, buttonNum);
 		try {
 			boolean handled = false;
 			if (buttonNum == 0) {
-				int inputX = HudComponents.CHAT_INPUT.getLayout().getComponentX(HudComponents.CHAT_INPUT, this.width);
-				int inputY = HudComponents.CHAT_INPUT.getLayout().getComponentY(HudComponents.CHAT_INPUT, this.height);
-				float scale = HudComponents.CHAT_INPUT.getScale();
+				HudComponentChatInput input = HudComponents.CHAT_INPUT;
+				int inputX = input.getLayout().getComponentX(input, this.width);
+				int inputY = input.getLayout().getComponentY(input, this.height);
+				float scale = input.getScale();
 				int localMx = unscaleX(mx, inputX, scale);
 				int localMy = unscaleY(my, inputY, scale);
+				int buttonY = inputY + input.getBoxHeight() - 12;
 
-				if (localMx >= inputX && localMx < inputX + 12 && localMy >= inputY && localMy < inputY + 12) {
+				if (localMx >= inputX && localMx < inputX + 12 && localMy >= buttonY && localMy < buttonY + 12) {
                     this.showEmotePicker = !this.showEmotePicker;
 					handled = true;
 				}
diff --git a/game/client/src/main/java/net/minecraft/client/gui/container/ScreenContainerAbstract.java b/game/client/src/main/java/net/minecraft/client/gui/container/ScreenContainerAbstract.java
index fe7d2dbb7..0fac3002c 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/container/ScreenContainerAbstract.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/container/ScreenContainerAbstract.java
@@ -309,11 +309,7 @@ public abstract class ScreenContainerAbstract extends Screen {
                 }
                 Slot slot = this.inventorySlots.getSlot(getSlotId(mx, my));
                 if (slot != null) {
-                    if (slot.getItemStack() == null) {
-                        this.mc.playerController.handleInventoryMouseClick(this.inventorySlots.containerId, InventoryAction.SORT, new int[]{slot.index, 64}, this.mc.thePlayer);
-                    } else {
-                        this.mc.playerController.handleInventoryMouseClick(this.inventorySlots.containerId, InventoryAction.CREATIVE_GRAB, new int[]{slot.index, 64}, this.mc.thePlayer);
-                    }
+                    creativeGrabStack(slot);
                 }
                 return;
             }
@@ -345,6 +341,15 @@ public abstract class ScreenContainerAbstract extends Screen {
         int catalogIndex = slot instanceof SlotCreative creativeSlot ? MenuInventoryCreative.getCreativeItemIndex(creativeSlot.item) : -1;
         return new int[]{slot.index, amount, stack != null ? stack.itemID : 0, stack != null ? stack.getMetadata() : 0, catalogIndex};
     }
+    
+    protected void creativeGrabStack(@NotNull Slot slot) {
+        ItemStack stack = slot.getItemStack();
+        if (stack == null) {
+            this.mc.playerController.handleInventoryMouseClick(this.inventorySlots.containerId, InventoryAction.SORT, new int[]{slot.index, 64}, this.mc.thePlayer);
+        } else {
+            this.mc.playerController.handleInventoryMouseClick(this.inventorySlots.containerId, InventoryAction.CREATIVE_GRAB, creativeSlotArgs(slot, stack.getMaxStackSize()), this.mc.thePlayer);
+        }
+    }
 
     protected int[] hotbarSwapArgs(int slotId, int hotbarSlotNumber) {
         Slot slot = this.inventorySlots.getSlot(slotId);
@@ -354,6 +359,10 @@ public abstract class ScreenContainerAbstract extends Screen {
         return new int[]{slotId, hotbarSlotNumber};
     }
 
+    public void clickSlot(@NotNull Slot slot, int mouseButton) {
+        clickInventory(slot.x + 8 + (this.width - this.xSize) / 2, slot.y + 8 + (this.height - this.ySize) / 2, mouseButton);
+    }
+
     /**
      * This method controls what InventoryAction is sent to the server
      */
@@ -374,12 +383,7 @@ public abstract class ScreenContainerAbstract extends Screen {
         if (!this.mc.thePlayer.getGamemode().hasBlockConsumption()) {
             // Duplicate items with middle mouse button
             if (mouseButton == 2) {
-                Slot slot = this.inventorySlots.getSlot(slotId);
-                if (slot.getItemStack() == null) {
-                    this.mc.playerController.handleInventoryMouseClick(this.inventorySlots.containerId, InventoryAction.SORT, new int[]{slotId, 64}, this.mc.thePlayer);
-                } else {
-                    this.mc.playerController.handleInventoryMouseClick(this.inventorySlots.containerId, InventoryAction.CREATIVE_GRAB, new int[]{slotId, 64}, this.mc.thePlayer);
-                }
+                creativeGrabStack(this.inventorySlots.getSlot(slotId));
                 return;
             }
         }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/SlotGuidebook.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/SlotGuidebook.java
index e1bcf2f0f..188d635b1 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/SlotGuidebook.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/SlotGuidebook.java
@@ -7,6 +7,7 @@ import net.minecraft.core.WeightedRandomLootObject;
 import net.minecraft.core.achievement.stat.StatList;
 import net.minecraft.core.data.registry.recipe.RecipeEntryBase;
 import net.minecraft.core.data.registry.recipe.RecipeSymbol;
+import net.minecraft.core.data.registry.recipe.SearchQuery;
 import net.minecraft.core.data.registry.recipe.entry.RecipeEntryTrommel;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.item.ItemStack;
@@ -14,6 +15,7 @@ import net.minecraft.core.player.gamemode.Gamemodes;
 import net.minecraft.core.player.inventory.slot.Slot;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
 
@@ -21,9 +23,11 @@ public class SlotGuidebook extends Slot
 {
 	public @Nullable ItemStack item;
 	public @Nullable RecipeSymbol symbol;
+	public @Nullable List<ItemStack> searchFilteredItems;
 	public @Nullable RecipeEntryBase<?,?,?> recipe;
 	public int recipeIndex = 0;
 	public int recipeAmount = 0;
+	private int cycleIndex = 0;
 	public boolean isOutput = false;
 	public SlotGuidebook(final int id, final int x, final int y, final @Nullable RecipeSymbol symbol, final boolean discovered, final @Nullable RecipeEntryBase<?,?,?> recipe)
 	{
@@ -31,7 +35,7 @@ public class SlotGuidebook extends Slot
 		this.index = id;
 		this.symbol = symbol;
 		if(symbol != null && symbol.getItemGroup() != null){
-			showRandomItem();
+			randomizeCycleStart();
 		} else if (symbol != null) {
             this.item = symbol.resolve().get(0);
 			if(recipe instanceof RecipeEntryTrommel entryTrommel){
@@ -50,7 +54,7 @@ public class SlotGuidebook extends Slot
 		super(null, id, x, y);
 		this.symbol = symbol;
 		if(symbol != null && symbol.getItemGroup() != null){
-			showRandomItem();
+			randomizeCycleStart();
 		} else if (symbol != null) {
             this.item = symbol.resolve().get(0);
 		}
@@ -104,16 +108,53 @@ public class SlotGuidebook extends Slot
 
 	public void showRandomItem(){
 		if(this.symbol != null && this.symbol.getItemGroup() != null){
-			Random r = new Random();
-			List<ItemStack> list = this.symbol.resolve();
-			ItemStack newItem = list.get(r.nextInt(list.size()));
-			if (list.size() > 1) {
-				while (newItem == this.item){
-					newItem = list.get(r.nextInt(list.size()));
-				}
+			List<ItemStack> list = this.searchFilteredItems != null ? this.searchFilteredItems : this.symbol.resolve();
+			if (list.isEmpty()) return;
+			this.cycleIndex = (this.cycleIndex + 1) % list.size();
+			this.item = list.get(this.cycleIndex);
+		}
+	}
+
+	public void randomizeCycleStart(){
+		if(this.symbol != null && this.symbol.getItemGroup() != null){
+			List<ItemStack> list = this.searchFilteredItems != null ? this.searchFilteredItems : this.symbol.resolve();
+			if (!list.isEmpty()){
+				this.cycleIndex = new Random().nextInt(list.size());
+				this.item = list.get(this.cycleIndex);
+			}
+		}
+	}
+
+	public void applySearchFilter(@Nullable SearchQuery query){
+		if(query == null || this.symbol == null || this.symbol.getItemGroup() == null) return;
+		List<ItemStack> all = this.symbol.resolve();
+		if(all == null || all.size() < 2) return;
+		List<ItemStack> matches = new ArrayList<>();
+		for(ItemStack stack : all){
+			if(itemMatchesQuery(stack, query)) matches.add(stack);
+		}
+		if(!matches.isEmpty() && matches.size() < all.size()){
+			this.searchFilteredItems = matches;
+			this.item = matches.get(0);
+		}
+	}
+
+	private static boolean itemMatchesQuery(ItemStack stack, SearchQuery query){
+		if(query.query.getLeft() == SearchQuery.QueryType.NAME){
+			String term = query.query.getRight();
+			if(term == null || term.isEmpty()) return false;
+			return query.strict
+				? stack.getDisplayName().equalsIgnoreCase(term)
+				: stack.getDisplayName().toLowerCase().contains(term.toLowerCase());
+		} else if(query.query.getLeft() == SearchQuery.QueryType.GROUP && query.query.getRight() != null && !query.query.getRight().isEmpty()){
+			try {
+				List<ItemStack> groupStacks = new RecipeSymbol(query.query.getRight()).resolve();
+				return groupStacks != null && groupStacks.contains(stack);
+			} catch (RuntimeException e){
+				return false;
 			}
-            this.item = newItem;
 		}
+		return false;
 	}
 	public boolean getIsDiscovered(Player player){
 		if (this.item == null || this.discovered) return true;
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/GuidebookSectionCrafting.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/GuidebookSectionCrafting.java
index dc3d3fd41..3ad07e202 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/GuidebookSectionCrafting.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/GuidebookSectionCrafting.java
@@ -2,20 +2,26 @@ package net.minecraft.client.gui.guidebook.crafting;
 
 import net.minecraft.client.gui.guidebook.GuidebookPage;
 import net.minecraft.client.gui.guidebook.SearchableGuidebookSection;
-import net.minecraft.core.data.registry.recipe.SearchQuery;
 import net.minecraft.core.block.Blocks;
+import net.minecraft.core.data.registry.recipe.SearchQuery;
 import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCrafting;
 import net.minecraft.core.data.registry.Registries;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.util.collection.Pair;
 import net.minecraft.core.util.helper.MathHelper;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 public class GuidebookSectionCrafting
     extends SearchableGuidebookSection {
+
     private final List<GuidebookPage> pages = new ArrayList<>();
     private Pair<String,List<GuidebookPage>> filteredPages = null;
 
@@ -26,8 +32,7 @@ public class GuidebookSectionCrafting
 
     public void reloadRecipes(){
         pages.clear();
-        List<RecipeEntryCrafting<?, ?>> allRecipes = new ArrayList<>(Registries.RECIPES.getAllCraftingRecipes());
-        allRecipes.removeIf((R)->(!RecipePageCrafting.guidebookSupportsRecipe(R)));
+        List<RecipeEntryCrafting<?, ?>> allRecipes = getSupportedRecipes();
         int totalRecipes = allRecipes.size();
         int totalPages = MathHelper.ceilInt(totalRecipes, RecipePageCrafting.RECIPES_PER_PAGE);
         for (int i = 0; i < totalPages; i++) {
@@ -51,10 +56,14 @@ public class GuidebookSectionCrafting
     public List<GuidebookPage> searchPages(SearchQuery query) {
         if(filteredPages == null || !Objects.equals(filteredPages.getLeft(), query.rawQuery)) {
             ArrayList<RecipeEntryCrafting<?, ?>> filteredRecipes = new ArrayList<>();
-            List<RecipeEntryCrafting<?, ?>> allRecipes = new ArrayList<>(Registries.RECIPES.getAllCraftingRecipes());
-            allRecipes.removeIf((R)->(!RecipePageCrafting.guidebookSupportsRecipe(R)));
-            for (RecipeEntryCrafting<?, ?> recipe : allRecipes) {
-                if(recipe.matchesQueryIgnoreExceptions(query)){
+            for (RecipeEntryCrafting<?, ?> recipe : getSupportedRecipes()) {
+                if (recipe instanceof RecipeEntryCraftingGroup group) {
+                    // Lock a consolidated entry to the variants that actually match the query.
+                    RecipeEntryCraftingGroup narrowed = group.filterToQuery(query);
+                    if (narrowed != null) {
+                        filteredRecipes.add(narrowed);
+                    }
+                } else if (recipe.matchesQueryIgnoreExceptions(query)) {
                     filteredRecipes.add(recipe);
                 }
             }
@@ -65,7 +74,7 @@ public class GuidebookSectionCrafting
                 int j = i * RecipePageCrafting.RECIPES_PER_PAGE;
                 ArrayList<RecipeEntryCrafting<?, ?>> recipes = new ArrayList<>(filteredRecipes.subList(Math.min(j, filteredRecipeSize), Math.min(j + RecipePageCrafting.RECIPES_PER_PAGE, filteredRecipeSize)));
                 if (!recipes.isEmpty()) {
-                    filteredPages.add(new RecipePageCrafting(this, recipes));
+                    filteredPages.add(new RecipePageCrafting(this, recipes, query));
                 }
             }
             this.filteredPages = Pair.of(query.rawQuery, filteredPages);
@@ -74,4 +83,33 @@ public class GuidebookSectionCrafting
             return filteredPages.getRight();
         }
     }
+
+    private static @NotNull List<RecipeEntryCrafting<?, ?>> getSupportedRecipes() {
+        List<RecipeEntryCrafting<?, ?>> allRecipes = new ArrayList<>(Registries.RECIPES.getAllCraftingRecipes());
+        allRecipes.removeIf((R) -> (!RecipePageCrafting.guidebookSupportsRecipe(R)));
+        return consolidateGroups(allRecipes);
+    }
+
+    private static @NotNull List<RecipeEntryCrafting<?, ?>> consolidateGroups(@NotNull List<RecipeEntryCrafting<?, ?>> recipes) {
+        Map<String, List<RecipeEntryCrafting<?, ?>>> groups = new LinkedHashMap<>();
+        for (RecipeEntryCrafting<?, ?> recipe : recipes) {
+            if (recipe.consolidationGroup != null) {
+                groups.computeIfAbsent(recipe.consolidationGroup, (k) -> new ArrayList<>()).add(recipe);
+            }
+        }
+
+        List<RecipeEntryCrafting<?, ?>> result = new ArrayList<>();
+        Set<String> emitted = new HashSet<>();
+        for (RecipeEntryCrafting<?, ?> recipe : recipes) {
+            String group = recipe.consolidationGroup;
+            if (group != null && groups.get(group).size() >= 2) {
+                if (emitted.add(group)) {
+                    result.add(new RecipeEntryCraftingGroup(groups.get(group)));
+                }
+                continue;
+            }
+            result.add(recipe);
+        }
+        return result;
+    }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipeEntryCraftingGroup.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipeEntryCraftingGroup.java
new file mode 100644
index 000000000..07b82e2ee
--- /dev/null
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipeEntryCraftingGroup.java
@@ -0,0 +1,57 @@
+package net.minecraft.client.gui.guidebook.crafting;
+
+import net.minecraft.core.data.registry.recipe.SearchQuery;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCrafting;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.player.inventory.container.ContainerCrafting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class RecipeEntryCraftingGroup extends RecipeEntryCrafting<Void, Void> {
+    public final RecipeEntryCrafting<?, ?> representative;
+    public final List<RecipeEntryCrafting<?, ?>> variants;
+
+    public RecipeEntryCraftingGroup(List<RecipeEntryCrafting<?, ?>> variants) {
+        this.variants = variants;
+        this.representative = variants.get(0);
+        this.parent = representative.parent;
+    }
+
+    @Override
+    public boolean matches(ContainerCrafting containerCrafting) {
+        return false;
+    }
+
+    @Override
+    public boolean matchesQuery(SearchQuery query) {
+        for (RecipeEntryCrafting<?, ?> variant : variants) {
+            if (variant.matchesQueryIgnoreExceptions(query)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public RecipeEntryCraftingGroup filterToQuery(SearchQuery query) {
+        List<RecipeEntryCrafting<?, ?>> matching = new ArrayList<>();
+        for (RecipeEntryCrafting<?, ?> variant : variants) {
+            if (variant.matchesQueryIgnoreExceptions(query)) {
+                matching.add(variant);
+            }
+        }
+        if (matching.isEmpty()) return null;
+        if (matching.size() == variants.size()) return this;
+        return new RecipeEntryCraftingGroup(matching);
+    }
+
+    @Override
+    public ItemStack getCraftingResult(ContainerCrafting containerCrafting) {
+        return representative.getCraftingResult(containerCrafting);
+    }
+
+    @Override
+    public int getRecipeSize() {
+        return representative.getRecipeSize();
+    }
+}
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipePageCrafting.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipePageCrafting.java
index e0b69fe01..37421a4a6 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipePageCrafting.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/RecipePageCrafting.java
@@ -8,6 +8,7 @@ import net.minecraft.client.gui.guidebook.GuidebookSection;
 import net.minecraft.client.gui.guidebook.GuidebookSections;
 import net.minecraft.client.gui.guidebook.GuidebookPageManager;
 import net.minecraft.client.gui.guidebook.RecipePage;
+import net.minecraft.client.gui.guidebook.crafting.displays.DisplayAdapterCraftingGroup;
 import net.minecraft.client.gui.guidebook.crafting.displays.DisplayAdapterDyeing;
 import net.minecraft.client.gui.guidebook.crafting.displays.DisplayAdapterLabel;
 import net.minecraft.client.gui.guidebook.crafting.displays.DisplayAdapterLabelDye;
@@ -67,6 +68,7 @@ public class RecipePageCrafting
         recipeToDisplayAdapterMap.put(RecipeEntryMapDuplication.class, new DisplayAdapterMapDuplication());
         recipeToDisplayAdapterMap.put(RecipeEntryUndyeing.class, new DisplayAdapterUndye());
         recipeToDisplayAdapterMap.put(RecipeEntryDyeing.class, new DisplayAdapterDyeing());
+        recipeToDisplayAdapterMap.put(RecipeEntryCraftingGroup.class, new DisplayAdapterCraftingGroup());
     }
 
     private final TooltipElement tooltipElement;
@@ -74,7 +76,7 @@ public class RecipePageCrafting
     private static final Minecraft mc = Minecraft.getMinecraft();
     private static long ticks = 0;
 
-    public RecipePageCrafting(GuidebookSection section, List<RecipeEntryCrafting<?, ?>> recipes) {
+    public RecipePageCrafting(GuidebookSection section, List<RecipeEntryCrafting<?, ?>> recipes, SearchQuery searchQuery) {
         super(section);
         this.recipes = recipes;
         this.slots = new ArrayList<>();
@@ -88,6 +90,11 @@ public class RecipePageCrafting
             if (recipeToDisplayAdapterMap.containsKey(recipe.getClass())) {
                 RecipeDisplayAdapter adapter = recipeToDisplayAdapterMap.get(recipe.getClass());
                 List<SlotGuidebook> recipeSlots = adapter.getSlots(recipe, recipeAmount, xOffset, yOffset);
+                if (searchQuery != null) {
+                    for (SlotGuidebook slot : recipeSlots) {
+                        slot.applySearchFilter(searchQuery);
+                    }
+                }
                 this.map.put(recipe, recipeSlots);
                 this.slots.addAll(recipeSlots);
                 yOffset += 10;
@@ -97,6 +104,10 @@ public class RecipePageCrafting
 
     }
 
+    public RecipePageCrafting(GuidebookSection section, List<RecipeEntryCrafting<?, ?>> recipes) {
+        this(section, recipes, null);
+    }
+
     @Override
     public void onTick() {
         ticks++;
@@ -110,6 +121,13 @@ public class RecipePageCrafting
         }
     }
 
+    @Override
+    public void onBecomeVisible() {
+        for (SlotGuidebook slot : this.slots) {
+            slot.randomizeCycleStart();
+        }
+    }
+
     @Override
     protected void renderForeground(TextureManager re, FontRenderer sr, int x, int y, int mouseX, int mouseY, float partialTicks) {
         if (this.recipes.isEmpty()) {
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/displays/DisplayAdapterCraftingGroup.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/displays/DisplayAdapterCraftingGroup.java
new file mode 100644
index 000000000..d120be396
--- /dev/null
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/crafting/displays/DisplayAdapterCraftingGroup.java
@@ -0,0 +1,221 @@
+package net.minecraft.client.gui.guidebook.crafting.displays;
+
+import net.minecraft.client.gui.guidebook.SlotGuidebook;
+import net.minecraft.client.gui.guidebook.crafting.RecipeEntryCraftingGroup;
+import net.minecraft.core.data.registry.recipe.RecipeEntryBase;
+import net.minecraft.core.data.registry.recipe.RecipeSymbol;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCrafting;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCraftingShaped;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCraftingShapeless;
+import net.minecraft.core.item.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+public class DisplayAdapterCraftingGroup implements RecipeDisplayAdapter<RecipeEntryCraftingGroup> {
+    @Override
+    public List<SlotGuidebook> getSlots(@NotNull RecipeEntryCraftingGroup recipe, int recipeAmount, int xOffset, int yOffset) {
+        List<RecipeEntryCrafting<?, ?>> variants = recipe.variants;
+
+        List<List<RecipeSymbol>> variantInputs = new ArrayList<>();
+        List<ItemStack> variantOutputs = new ArrayList<>();
+        for (RecipeEntryCrafting<?, ?> variant : variants) {
+            variantInputs.add(inputSymbols(variant));
+            variantOutputs.add(outputStack(variant));
+        }
+
+        GroupCursor cursor = new GroupCursor(variants.size());
+        if (recipe.representative instanceof RecipeEntryCraftingShaped shaped) {
+            return buildGrid(shaped.recipeWidth, shaped.recipeHeight, variantInputs, variantOutputs, recipeAmount, xOffset, yOffset, cursor, recipe);
+        }
+        return buildShapeless(variantInputs, variantOutputs, recipeAmount, xOffset, yOffset, cursor, recipe);
+    }
+
+    private static List<RecipeSymbol> inputSymbols(RecipeEntryCrafting<?, ?> variant) {
+        if (variant instanceof RecipeEntryCraftingShaped shaped) return Arrays.asList(shaped.getInput());
+        if (variant instanceof RecipeEntryCraftingShapeless shapeless) return shapeless.getInput();
+        return new ArrayList<>();
+    }
+
+    private static ItemStack outputStack(RecipeEntryCrafting<?, ?> variant) {
+        if (variant instanceof RecipeEntryCraftingShaped shaped) return shaped.getOutput();
+        if (variant instanceof RecipeEntryCraftingShapeless shapeless) return shapeless.getOutput();
+        return null;
+    }
+
+    private static List<SlotGuidebook> buildGrid(
+            int width, int height, List<List<RecipeSymbol>> variantInputs, List<ItemStack> variantOutputs,
+            int recipeAmount, int xOffset, int yOffset, GroupCursor cursor, RecipeEntryBase<?, ?, ?> recipe) {
+        List<SlotGuidebook> recipeSlots = new ArrayList<>();
+        boolean[] driverAssigned = {false};
+
+        if (height > 2 || width > 2) {
+            for (int i = 0; i < 9; i++) {
+                final int slotX = i % 3;
+                final int slotY = i / 3;
+                final int x = 1 + 18 * (i % 3) + xOffset;
+                final int y = 1 + 18 * ((i / 3) + recipeAmount * 3) + yOffset;
+                if (slotX >= width || slotY >= height) {
+                    recipeSlots.add(new SlotGuidebook(i, x, y, null, false, recipe));
+                } else {
+                    recipeSlots.add(makeInputSlot(i, x, y, slotX + slotY * width, variantInputs, cursor, driverAssigned, recipe));
+                }
+            }
+            int centerY = (recipeSlots.get(recipeSlots.size() - 1).y + recipeSlots.get(recipeSlots.size() - 8).y) / 2;
+            recipeSlots.add(makeOutputSlot(9, 81 + xOffset, centerY, variantOutputs, cursor, driverAssigned, recipe));
+        } else {
+            for (int i = 0; i < 4; i++) {
+                final int slotX = i % 2;
+                final int slotY = i / 2;
+                final int x = 13 + 18 * (i % 2) + xOffset;
+                final int y = 10 + 18 * ((i / 2) + recipeAmount * 3) + yOffset;
+                if (slotX >= width || slotY >= height) {
+                    recipeSlots.add(new SlotGuidebook(i, x, y, null, false, recipe));
+                } else {
+                    recipeSlots.add(makeInputSlot(i, x, y, slotX + slotY * width, variantInputs, cursor, driverAssigned, recipe));
+                }
+            }
+            int centerY = (recipeSlots.get(recipeSlots.size() - 1).y + recipeSlots.get(recipeSlots.size() - 3).y) / 2;
+            recipeSlots.add(makeOutputSlot(4, 81 + xOffset, centerY, variantOutputs, cursor, driverAssigned, recipe));
+        }
+        return recipeSlots;
+    }
+
+    private static List<SlotGuidebook> buildShapeless(List<List<RecipeSymbol>> variantInputs, List<ItemStack> variantOutputs,
+                                                      int recipeAmount, int xOffset, int yOffset, GroupCursor cursor, RecipeEntryBase<?, ?, ?> recipe) {
+        List<SlotGuidebook> recipeSlots = new ArrayList<>();
+        boolean[] driverAssigned = {false};
+        int inputCount = variantInputs.get(0).size();
+
+        if (inputCount > 4) {
+            for (int i = 0; i < 9; i++) {
+                final int x = 1 + 18 * (i % 3) + xOffset;
+                final int y = 1 + 18 * ((i / 3) + recipeAmount * 3) + yOffset;
+                if (i >= inputCount) {
+                    recipeSlots.add(new SlotGuidebook(i, x, y, null, false, recipe));
+                } else {
+                    recipeSlots.add(makeInputSlot(i, x, y, i, variantInputs, cursor, driverAssigned, recipe));
+                }
+            }
+            int centerY = (recipeSlots.get(recipeSlots.size() - 1).y + recipeSlots.get(recipeSlots.size() - 8).y) / 2;
+            recipeSlots.add(makeOutputSlot(9, 81 + xOffset, centerY, variantOutputs, cursor, driverAssigned, recipe));
+        } else {
+            for (int i = 0; i < 4; i++) {
+                final int x = 13 + 18 * (i % 2) + xOffset;
+                final int y = 10 + 18 * ((i / 2) + recipeAmount * 3) + yOffset;
+                if (i >= inputCount) {
+                    recipeSlots.add(new SlotGuidebook(i, x, y, null, false, recipe));
+                } else {
+                    recipeSlots.add(makeInputSlot(i, x, y, i, variantInputs, cursor, driverAssigned, recipe));
+                }
+            }
+            int centerY = (recipeSlots.get(recipeSlots.size() - 1).y + recipeSlots.get(recipeSlots.size() - 3).y) / 2;
+            recipeSlots.add(makeOutputSlot(4, 81 + xOffset, centerY, variantOutputs, cursor, driverAssigned, recipe));
+        }
+        return recipeSlots;
+    }
+
+    private static @NotNull SlotGuidebook makeInputSlot(int id, int x, int y, int inputIndex, @NotNull List<List<RecipeSymbol>> variantInputs,
+                                                        GroupCursor cursor, boolean[] driverAssigned, RecipeEntryBase<?, ?, ?> recipe) {
+        List<RecipeSymbol> symbols = new ArrayList<>();
+        for (List<RecipeSymbol> inputs : variantInputs) {
+            symbols.add(inputs.get(inputIndex));
+        }
+        boolean driver = !driverAssigned[0];
+        if (driver) driverAssigned[0] = true;
+        return SlotGuidebookCraftingGroup.forInputs(id, x, y, symbols, cursor, driver, recipe);
+    }
+
+    private static @NotNull SlotGuidebook makeOutputSlot(int id, int x, int y, @NotNull List<ItemStack> variantOutputs,
+                                                         GroupCursor cursor, boolean[] driverAssigned, RecipeEntryBase<?, ?, ?> recipe) {
+        List<ItemStack> frames = new ArrayList<>();
+        for (ItemStack output : variantOutputs) {
+            frames.add(output == null ? null : output.copy());
+        }
+        boolean driver = !driverAssigned[0];
+        if (driver) driverAssigned[0] = true;
+        return SlotGuidebookCraftingGroup.forOutputs(id, x, y, frames, cursor, driver, recipe).setAsOutput();
+    }
+
+    static final class GroupCursor {
+        private static final Random RANDOM = new Random();
+        final int variantCount;
+        int main;
+        int nested;
+
+        GroupCursor(int variantCount) {
+            this.variantCount = variantCount;
+            randomizeStart();
+        }
+
+        void advance() {
+            main = variantCount <= 0 ? 0 : (main + 1) % variantCount;
+            nested++;
+        }
+
+        void randomizeStart() {
+            main = variantCount <= 0 ? 0 : RANDOM.nextInt(variantCount);
+            nested = RANDOM.nextInt(4096);
+        }
+    }
+
+    public static class SlotGuidebookCraftingGroup extends SlotGuidebook {
+        private final List<RecipeSymbol> symbols; // input slots; null for output slots
+        private final List<ItemStack> items;      // output slots; null for input slots
+        private final GroupCursor cursor;
+        private final boolean driver;
+
+        private SlotGuidebookCraftingGroup(int id, int x, int y, List<RecipeSymbol> symbols, List<ItemStack> items,
+                                           GroupCursor cursor, boolean driver, RecipeEntryBase<?, ?, ?> recipe) {
+            super(id, x, y, null, false, recipe);
+            this.symbols = symbols;
+            this.items = items;
+            this.cursor = cursor;
+            this.driver = driver;
+            resolve();
+        }
+
+        static SlotGuidebookCraftingGroup forInputs(int id, int x, int y, List<RecipeSymbol> symbols, GroupCursor cursor, boolean driver, RecipeEntryBase<?, ?, ?> recipe) {
+            return new SlotGuidebookCraftingGroup(id, x, y, symbols, null, cursor, driver, recipe);
+        }
+
+        static SlotGuidebookCraftingGroup forOutputs(int id, int x, int y, List<ItemStack> items, GroupCursor cursor, boolean driver, RecipeEntryBase<?, ?, ?> recipe) {
+            return new SlotGuidebookCraftingGroup(id, x, y, null, items, cursor, driver, recipe);
+        }
+
+        @Override
+        public SlotGuidebookCraftingGroup setAsOutput() {
+            super.setAsOutput();
+            return this;
+        }
+
+        private void resolve() {
+            int index = cursor.variantCount <= 0 ? 0 : Math.min(cursor.main, cursor.variantCount - 1);
+            if (symbols != null) {
+                if (symbols.isEmpty()) { this.item = null; return; }
+                RecipeSymbol symbol = symbols.get(index);
+                if (symbol == null) { this.item = null; return; }
+                List<ItemStack> members = symbol.resolve();
+                if (members == null || members.isEmpty()) { this.item = null; return; }
+                this.item = members.get(Math.floorMod(cursor.nested, members.size()));
+            } else if (items != null) {
+                this.item = items.isEmpty() ? null : items.get(index);
+            }
+        }
+
+        @Override
+        public void showRandomItem() {
+            if (driver) cursor.advance();
+            resolve();
+        }
+
+        @Override
+        public void randomizeCycleStart() {
+            if (driver) cursor.randomizeStart();
+            resolve();
+        }
+    }
+}
diff --git a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChat.java b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChat.java
index 665fac4a2..db08a3f14 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChat.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChat.java
@@ -3,6 +3,7 @@ package net.minecraft.client.gui.hud.component;
 import net.minecraft.client.gui.Gui;
 import net.minecraft.client.gui.hud.HudIngame;
 import net.minecraft.client.gui.hud.component.layout.Layout;
+import net.minecraft.client.gui.options.components.BooleanToggleComponent;
 import net.minecraft.client.gui.options.components.FloatSliderComponent;
 import net.minecraft.client.option.GameSettings;
 import net.minecraft.core.enums.ChatVisibility;
@@ -15,8 +16,10 @@ import net.minecraft.client.gui.chat.ScreenChat;
 import net.minecraft.core.util.helper.MathHelper;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Deque;
 import java.util.List;
 
 public class HudComponentChat extends HudComponentMovable {
@@ -28,11 +31,22 @@ public class HudComponentChat extends HudComponentMovable {
     public static final int MAX_RESIZE_HEIGHT = 1000;
     private static final int CHAT_PADDING = 2;
     private static final int LINE_SPACING = 2;
+    private static final int LINE_HEIGHT = 9;
+    private static final int LINE_STEP = LINE_HEIGHT + LINE_SPACING;
     public final @NotNull List<@NotNull ChatLine> chatMessageList = new ArrayList<>();
     public int chatScroll = 0;
 
+    /** Newest first, one entry per visual line */
+    private final @NotNull List<@NotNull VisualLine> visualLines = new ArrayList<>();
+    private boolean visualLinesDirty = true;
+    private int visualLinesWrapWidth = -1;
+    private Object visualLinesFont = null;
+
+    private static final boolean DEFAULT_FULL_WIDTH_BACKDROP = false;
+
     private int customWidth;
     private int customHeight;
+    private boolean fullWidthBackdrop = DEFAULT_FULL_WIDTH_BACKDROP;
 
     public HudComponentChat(String key, int xSize, int ySize, Layout layout) {
         super(key, xSize, ySize, layout);
@@ -60,6 +74,13 @@ public class HudComponentChat extends HudComponentMovable {
             value -> this.customHeight = Math.round(value),
             value -> Math.round(value) + "px"
         ));
+
+        addOptionComponentSupplier(() -> new BooleanToggleComponent(
+            "gui.options.hudeditor.chat_full_width_backdrop",
+            DEFAULT_FULL_WIDTH_BACKDROP,
+            () -> this.fullWidthBackdrop,
+            value -> this.fullWidthBackdrop = value
+        ));
     }
 
     @Override
@@ -74,7 +95,7 @@ public class HudComponentChat extends HudComponentMovable {
 
     @Override
     protected String toSettingsParametersString() {
-        return "w=" + this.customWidth + ",h=" + this.customHeight;
+        return "w=" + this.customWidth + ",h=" + this.customHeight + ",backdrop=" + (this.fullWidthBackdrop ? 1 : 0);
     }
 
     @Override
@@ -91,6 +112,8 @@ public class HudComponentChat extends HudComponentMovable {
                     this.customWidth = MathHelper.clamp(parsed, MIN_CHAT_WIDTH, MAX_RESIZE_WIDTH);
                 } else if (key.equals("h")) {
                     this.customHeight = MathHelper.clamp(parsed, MIN_CHAT_HEIGHT, MAX_RESIZE_HEIGHT);
+                } else if (key.equals("backdrop")) {
+                    this.fullWidthBackdrop = parsed != 0;
                 }
             } catch (NumberFormatException ignored) {}
         }
@@ -156,6 +179,7 @@ public class HudComponentChat extends HudComponentMovable {
 
     public void clearChatMessages() {
         this.chatMessageList.clear();
+        this.visualLinesDirty = true;
     }
 
     public void addChatMessage(final CharSequence s) {
@@ -163,121 +187,195 @@ public class HudComponentChat extends HudComponentMovable {
         while (this.chatMessageList.size() > 100) {
             this.chatMessageList.remove(this.chatMessageList.size() - 1);
         }
+        this.visualLinesDirty = true;
     }
 
     private int getMessageWrapWidth() {
         return Math.max(1, this.customWidth - (CHAT_PADDING * 2));
     }
 
+    /**
+     * A single wrapped line of a chat message. Multi line messages produce several of these,
+     * all sharing the {@link ChatLine} they came from so they fade out together.
+     */
+    private static class VisualLine {
+        final @NotNull ChatLine source;
+        final @NotNull String text;
+
+        VisualLine(@NotNull ChatLine source, @NotNull String text) {
+            this.source = source;
+            this.text = text;
+        }
+    }
+
+    /**
+     * Returns every chat message split into wrapped lines, newest line first. The result is cached
+     * until the message list, the wrap width or the font changes.
+     */
+    private @NotNull List<@NotNull VisualLine> getVisualLines(int wrapWidth) {
+        Object font = mc.font.getFont();
+        if (!this.visualLinesDirty && this.visualLinesWrapWidth == wrapWidth && this.visualLinesFont == font) {
+            return this.visualLines;
+        }
+
+        this.visualLines.clear();
+        List<String> split = new ArrayList<>();
+        for (ChatLine line : this.chatMessageList) {
+
+            mc.font.splitCharsIntoLines(line.message, wrapWidth, split);
+            carryFormattingBetweenLines(split);
+
+            for (int i = split.size() - 1; i >= 0; i--) {
+                this.visualLines.add(new VisualLine(line, split.get(i)));
+            }
+
+        }
+
+        this.visualLinesDirty = false;
+        this.visualLinesWrapWidth = wrapWidth;
+        this.visualLinesFont = font;
+        return this.visualLines;
+    }
+
+    /**
+     * Re-apply codes that were still active at the end of the previous line to the start of the next one.
+     */
+    private static void carryFormattingBetweenLines(@NotNull List<String> lines) {
+        StringBuilder active = new StringBuilder();
+        Deque<String> pushed = new ArrayDeque<>();
+        for (int i = 0; i < lines.size(); i++) {
+            String line = lines.get(i);
+            if (i > 0 && !active.isEmpty()) {
+                lines.set(i, active + line);
+            }
+            for (int j = 0; j < line.length() - 1; j++) {
+                if (line.charAt(j) != '§') continue;
+                if (line.charAt(j + 1) == '<') {
+                    int end = line.indexOf('>', j + 2);
+                    if (end == -1) continue;
+                    active.setLength(0);
+                    active.append(line, j, end + 1);
+                    j = end;
+                    continue;
+                }
+
+                char code = Character.toLowerCase(line.charAt(j + 1));
+                j++;
+                if (code == '+') {
+                    pushed.push(active.toString());
+                } else if (code == '-') {
+                    active.setLength(0);
+                    if (!pushed.isEmpty()) active.append(pushed.pop());
+                } else if (code == 'r') {
+                    active.setLength(0);
+                } else {
+                    // A color code clears every style before it, so drop what we have.
+                    if ((code >= '0' && code <= '9') || (code >= 'a' && code <= 'f')) {
+                        active.setLength(0);
+                    }
+                    active.append('§').append(code);
+                }
+            }
+        }
+    }
+
     private void drawChat(HudIngame hud, int xSizeScreen, int ySizeScreen) {
         int x = this.getLayout().getComponentX(this, xSizeScreen);
         int y = this.getLayout().getComponentY(this, ySizeScreen);
 
         int wrapWidth = getMessageWrapWidth();
-        int singleLineHeight = 9 + LINE_SPACING;
-        int linesToShow = Math.max(1, (this.customHeight - CHAT_PADDING * 2) / singleLineHeight);
+        int linesToShow = Math.max(1, (this.customHeight - CHAT_PADDING * 2) / LINE_STEP);
         boolean ignoreFadeout = false;
         if (mc.currentScreen instanceof ScreenChat) {
             int acceptableSize = ySizeScreen - 48 - 48;
-            linesToShow = Math.max(linesToShow, acceptableSize / singleLineHeight);
+            linesToShow = Math.max(linesToShow, acceptableSize / LINE_STEP);
             ignoreFadeout = true;
         }
 
-        if (this.chatScroll > this.chatMessageList.size() - linesToShow)
-            this.chatScroll = this.chatMessageList.size() - linesToShow;
-        if (this.chatScroll < 0) this.chatScroll = 0;
+        List<VisualLine> lines = getVisualLines(wrapWidth);
+
+        this.chatScroll = MathHelper.clamp(this.chatScroll, 0, Math.max(0, lines.size() - linesToShow));
 
         GLRenderer.enableState(State.BLEND);
         GLRenderer.setBlendFunc(BlendFactor.SRC_ALPHA, BlendFactor.ONE_MINUS_SRC_ALPHA);
 
-        int linesUsed = Math.min(linesToShow, this.chatMessageList.size() - this.chatScroll);
-        int totalHeight = computeTotalHeight(this.chatMessageList, this.chatScroll, linesUsed, ignoreFadeout, wrapWidth);
+        int linesUsed = Math.min(linesToShow, lines.size() - this.chatScroll);
+        int totalHeight = 0;
+        for (int i = 0; i < linesUsed; i++) {
+            if (getLineAlpha(lines.get(this.chatScroll + i).source, ignoreFadeout) > 0) totalHeight += LINE_STEP;
+        }
 
         float yMult = GameSettings.VERTICAL_CHAT_TEXT_ALIGNMENT.value.multiplier;
         int alignmentSpace = this.getBaseYSize() - (CHAT_PADDING * 2) - totalHeight;
 
         int currentY = y + this.getBaseYSize() - CHAT_PADDING - (int) (alignmentSpace * (1 - yMult));
-        for (int i = 0; i < linesToShow; i++) {
-            if (this.chatScroll + i >= this.chatMessageList.size()) continue;
-            final ChatLine line = this.chatMessageList.get(this.chatScroll + i);
-            if (line.updateCounter >= 190 && !ignoreFadeout) {
-                continue;
-            }
-            double fadeAmount = (double) line.updateCounter / 200D;
-            fadeAmount = 1.0D - fadeAmount;
-            fadeAmount *= 10D;
-            if (fadeAmount < 0.0D) {
-                fadeAmount = 0.0D;
-            }
-            if (fadeAmount > 1.0D) {
-                fadeAmount = 1.0D;
-            }
-            fadeAmount *= fadeAmount;
-            int alpha = (int) (255D * fadeAmount);
-            if (ignoreFadeout) {
-                alpha = 255;
-            }
+        for (int i = 0; i < linesUsed; i++) {
+            final VisualLine line = lines.get(this.chatScroll + i);
+            int alpha = getLineAlpha(line.source, ignoreFadeout);
             if (alpha > 0) {
-                final CharSequence message = line.message;
-                final int h = MathHelper.ceil(mc.font.heightOfConstrainedChars(message, wrapWidth));
-                currentY -= (h + LINE_SPACING);
-                drawAlignedLine(hud, mc.font, message, x, currentY, h, alpha, wrapWidth);
+                currentY -= LINE_STEP;
+                drawAlignedLine(hud, mc.font, line.text, x, currentY, alpha, wrapWidth);
             }
         }
         GLRenderer.disableState(State.BLEND);
     }
 
+    private static int getLineAlpha(@NotNull ChatLine line, boolean ignoreFadeout) {
+        if (ignoreFadeout) return 255;
+        if (line.updateCounter >= 190) return 0;
+        double fadeAmount = 1.0D - (double) line.updateCounter / 200D;
+        fadeAmount *= 10D;
+        if (fadeAmount < 0.0D) {
+            fadeAmount = 0.0D;
+        }
+        if (fadeAmount > 1.0D) {
+            fadeAmount = 1.0D;
+        }
+        fadeAmount *= fadeAmount;
+        return (int) (255D * fadeAmount);
+    }
+
     private void drawLines(Gui gui, FontRenderer sr, @NotNull List<String> messages, int x, int y, int alpha) {
         GLRenderer.enableState(State.BLEND);
         GLRenderer.setBlendFunc(BlendFactor.SRC_ALPHA, BlendFactor.ONE_MINUS_SRC_ALPHA);
 
         int wrapWidth = getMessageWrapWidth();
-        int maxContentHeight = Math.max(1, this.getBaseYSize() - (CHAT_PADDING * 2));
-        List<String> visibleMessages = new ArrayList<>();
-        int totalHeight = 0;
+        int maxLines = Math.max(1, (this.getBaseYSize() - (CHAT_PADDING * 2)) / LINE_STEP);
+
+        List<String> visibleLines = new ArrayList<>();
+        List<String> split = new ArrayList<>();
         for (String message : messages) {
-            int lineHeight = MathHelper.ceil(sr.heightOfConstrainedChars(message, wrapWidth)) + LINE_SPACING;
-            if (totalHeight + lineHeight > maxContentHeight) {
-                break;
+            if (visibleLines.size() >= maxLines) break;
+            sr.splitCharsIntoLines(message, wrapWidth, split);
+            carryFormattingBetweenLines(split);
+            for (int i = split.size() - 1; i >= 0 && visibleLines.size() < maxLines; i--) {
+                visibleLines.add(split.get(i));
             }
-            visibleMessages.add(message);
-            totalHeight += lineHeight;
         }
 
         float yMult = GameSettings.VERTICAL_CHAT_TEXT_ALIGNMENT.value.multiplier;
+        int totalHeight = visibleLines.size() * LINE_STEP;
         int alignmentSpace = this.getBaseYSize() - (CHAT_PADDING * 2) - totalHeight;
 
         int currentY = y + this.getBaseYSize() - CHAT_PADDING - (int) (alignmentSpace * (1 - yMult));
-        for (String message : visibleMessages) {
-            final int h = MathHelper.ceil(sr.heightOfConstrainedChars(message, wrapWidth));
-            currentY -= (h + LINE_SPACING);
-            drawAlignedLine(gui, sr, message, x, currentY, h, alpha, wrapWidth);
+        for (String line : visibleLines) {
+            currentY -= LINE_STEP;
+            drawAlignedLine(gui, sr, line, x, currentY, alpha, wrapWidth);
         }
 
         GLRenderer.disableState(State.BLEND);
     }
 
-    private void drawAlignedLine(@NotNull Gui gui, @NotNull FontRenderer sr, CharSequence message, int x, int currentY, int h, int alpha, int wrapWidth) {
+    /** Draws one already wrapped line, honoring the horizontal alignment option. */
+    private void drawAlignedLine(@NotNull Gui gui, @NotNull FontRenderer sr, CharSequence line, int x, int currentY, int alpha, int wrapWidth) {
         float xMult = GameSettings.HORIZONTAL_CHAT_TEXT_ALIGNMENT.value.multiplier;
-        int textWidth = Math.min(wrapWidth, (int) sr.stringWidthDouble(message));
+        int textWidth = Math.min(wrapWidth, MathHelper.ceil(sr.stringWidthDouble(line)));
         int boxWidth = this.getBaseXSize() - (CHAT_PADDING * 2);
         int renderX = x + CHAT_PADDING + (int) ((boxWidth - textWidth) * xMult);
-        int boxLeft = renderX - 1;
-        int boxRight = renderX + textWidth + 1;
-        gui.drawRect(boxLeft, currentY - 1, boxRight, currentY + h + 1, alpha / 2 << 24);
+        int backdropLeft = this.fullWidthBackdrop ? x : renderX - 1;
+        int backdropRight = this.fullWidthBackdrop ? x + this.getBaseXSize() : renderX + textWidth + 1;
+        gui.drawRect(backdropLeft, currentY - 1, backdropRight, currentY + LINE_HEIGHT + 1, alpha / 2 << 24);
         GLRenderer.enableState(State.BLEND);
-        sr.renderWidthConstrained(message, renderX, currentY, wrapWidth).setShadow().setColor(0xFFFFFF + (alpha << 24)).call();
-    }
-
-    private int computeTotalHeight(List<ChatLine> messages, int scroll, int linesUsed, boolean ignoreFadeout, int wrapWidth) {
-        int total = 0;
-        for (int i = 0; i < linesUsed; i++) {
-            int idx = scroll + i;
-            if (idx >= messages.size()) continue;
-            ChatLine line = messages.get(idx);
-            if (line.updateCounter >= 190 && !ignoreFadeout) continue;
-            total += MathHelper.ceil(mc.font.heightOfConstrainedChars(line.message, wrapWidth)) + LINE_SPACING;
-        }
-        return total;
+        sr.render(line, renderX, currentY).setShadow().setColor(0xFFFFFF + (alpha << 24)).call();
     }
 }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChatInput.java b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChatInput.java
index d3f78bb73..b01eba9cd 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChatInput.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/hud/component/HudComponentChatInput.java
@@ -11,7 +11,12 @@ import net.minecraft.client.render.renderer.State;
 import org.jetbrains.annotations.NotNull;
 
 public class HudComponentChatInput extends HudComponentMovable {
+    public static final int LINE_HEIGHT = 10;
+    public static final int FIELD_X_OFFSET = 14;
+    private static final int FIELD_PADDING = 2;
+
     private HudComponentChat linkedChat;
+    private int lineCount = 1;
 
     public HudComponentChatInput(String key, int xSize, int ySize, Layout layout) {
         super(key, xSize, ySize, layout);
@@ -21,11 +26,32 @@ public class HudComponentChatInput extends HudComponentMovable {
         this.linkedChat = chat;
     }
 
+    public void setLineCount(int lineCount) {
+        this.lineCount = Math.max(1, lineCount);
+    }
+
+    public int getLineCount() {
+        return this.lineCount;
+    }
+
+    public int getTextWrapWidth() {
+        return Math.max(1, getBaseXSize() - FIELD_X_OFFSET - FIELD_PADDING * 2);
+    }
+
+    public int getBoxHeight() {
+        return FIELD_PADDING + this.lineCount * LINE_HEIGHT;
+    }
+
     @Override
     public int getBaseXSize() {
         return this.linkedChat != null ? this.linkedChat.getBaseXSize() : super.getBaseXSize();
     }
 
+    @Override
+    public int getBaseYSize() {
+        return super.getBaseYSize() + (this.lineCount - 1) * LINE_HEIGHT;
+    }
+
     @Override
     public OptionGuiScaleOverride getGuiScaleOverrideOption() {
         return this.linkedChat != null ? this.linkedChat.getGuiScaleOverrideOption() : super.getGuiScaleOverrideOption();
diff --git a/game/client/src/main/java/net/minecraft/client/render/PostProcessingManager.java b/game/client/src/main/java/net/minecraft/client/render/PostProcessingManager.java
index 9e56daab6..8e5b5df1c 100644
--- a/game/client/src/main/java/net/minecraft/client/render/PostProcessingManager.java
+++ b/game/client/src/main/java/net/minecraft/client/render/PostProcessingManager.java
@@ -95,13 +95,13 @@ public class PostProcessingManager {
         this.lastHumidity = humidity;
 
         // Perform base temperature change
-        if (this.mc.currentWorld.getWorldType().hasTag(WorldTypeTags.OVERWORLD)) {
+        if (ccValue != 0 && this.mc.currentWorld.getWorldType().hasTag(WorldTypeTags.OVERWORLD)) {
             this.lastTemperature = temperature;
             final double tt = (temperature * 2) - 1.0; // Ranges from -1.0 to 1.0
             this.current.saturation += (float) (0.25 * tt * surfaceness);
             this.current.contrast += (float) (-0.1 * tt * surfaceness);
         }
-        if (this.mc.currentWorld.getWorldType().hasTag(WorldTypeTags.HOT)) {
+        if (ccValue != 0 && this.mc.currentWorld.getWorldType().hasTag(WorldTypeTags.HOT)) {
             this.lastTemperature = temperature;
             if (temperature < 0.5) {
                 final double tt = temperature * 2; // Ranges from 0.0 to 1.0
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 3fe26f5b6..d7db4cc43 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
@@ -16,6 +16,8 @@ import net.minecraft.client.render.culling.CameraFrustum;
 import net.minecraft.client.render.particle.Particle;
 import net.minecraft.client.render.particle.ParticleDispatcher;
 import net.minecraft.client.render.particle.ParticleEntry;
+import net.minecraft.client.render.particle.ParticleSiphon;
+import net.minecraft.core.block.material.Materials;
 import net.minecraft.client.render.renderer.BlendFactor;
 import net.minecraft.client.render.renderer.DrawMode;
 import net.minecraft.client.render.renderer.GLRenderer;
@@ -230,6 +232,7 @@ public final class RenderGlobal
         world.addListener(this);
         AURORA_PROVIDER.init(world);
         this.allChanged();
+        this.mc.terrainRenderer.resetChunkSorting();
     }
 
     public void allChanged() {
@@ -923,159 +926,194 @@ public final class RenderGlobal
         GLRenderer.popFrame();
     }
 
+    private @Nullable RenderBuffer cloudBuffer;
+    private int cachedRadius = -1;
+
     public void renderCloudsFancy(float partialTick) {
         GLRenderer.pushFrame();
         GLRenderer.setLightmapCoord2i(15, 15);
+        assert this.mc.currentWorld != null;
         WorldTypeFX worldTypeFX = WorldTypeFXDispatcher.getInstance().getDispatch(this.mc.currentWorld.getWorldType());
         this.mc.renderer.beginRenderClouds(partialTick);
-
-        float celestialAngle = this.world.getCelestialAngle(partialTick);
+        float celestialAngle = world.getCelestialAngle(partialTick);
         float[] sunriseColor = worldTypeFX.getSunriseColor(celestialAngle, partialTick);
-        float rSun = 0.0f, gSun = 0.0f, bSun = 0.0f, aSun = 0.0f;
+        float rSun = 0.0F;
+        float gSun = 0.0F;
+        float bSun = 0.0F;
+        float aSun = 0.0F;
         if (sunriseColor != null) {
             rSun = sunriseColor[0];
             gSun = sunriseColor[1];
             bSun = sunriseColor[2];
-            aSun = sunriseColor[3] / 2;
+            aSun = sunriseColor[3] / 2.0F;
         }
 
-        Vector3fc dimensionColor = this.world.getDimensionColor(this.mc.activeCamera, partialTick);
-        final float rDim = dimensionColor.x();
-        final float gDim = dimensionColor.y();
-        final float bDim = dimensionColor.z();
+        Vector3fc dimensionColor = world.getDimensionColor(this.mc.activeCamera, partialTick);
+        float rDim = dimensionColor.x();
+        float gDim = dimensionColor.y();
+        float bDim = dimensionColor.z();
 
         GLRenderer.disableState(State.CULL_FACE);
-        float cameraY = (float) this.mc.activeCamera.getY(partialTick);
-        TessellatorShader tessellator = GLRenderer.getTessellator();
-        float ss = 12F;
-        float h = 4F;
-        double xo = (this.mc.activeCamera.getX(partialTick) + (double) ((this.lastCloudOffsetX + (this.cloudOffsetX - this.lastCloudOffsetX) * partialTick) * 0.03F)) / (double) ss;
-        double zo = (this.mc.activeCamera.getZ(partialTick) + (double) ((this.lastCloudOffsetZ + (this.cloudOffsetZ - this.lastCloudOffsetZ) * partialTick) * 0.03F)) / (double) ss + 0.33D;
-        float yy = (worldTypeFX.getCloudHeight(this.world) - cameraY) + 0.33F + getCloudHeightModifier();
-        int xOffs = MathHelper.floor(xo / 2048D);
-        int zOffs = MathHelper.floor(zo / 2048D);
+
+        float cameraY = (float)this.mc.activeCamera.getY(partialTick);
+
+        float modelScale = 12.0F;
+        float deltaHeight = 4.0F;
+        ICamera camera = mc.activeCamera;
+
+        double xo = (camera.getX(partialTick) + (lastCloudOffsetX + (cloudOffsetX - lastCloudOffsetX) * partialTick) * 0.03F) / modelScale;
+        double zo = (camera.getZ(partialTick) + (lastCloudOffsetZ + (cloudOffsetZ - lastCloudOffsetZ) * partialTick) * 0.03F) / modelScale + 0.33;
+        float cloudMinY = worldTypeFX.getCloudHeight(this.world) - cameraY + 0.33F + getCloudHeightModifier();
+        int xOffs = MathHelper.floor(xo / 2048.0);
+        int zOffs = MathHelper.floor(zo / 2048.0);
         xo -= xOffs * 2048;
         zo -= zOffs * 2048;
 
-        boolean noBFCMode = ( (yy > -h - 1) && (yy <= h + 1) );
-        if( noBFCMode ) {
+        boolean noBFCMode = cloudMinY > -deltaHeight - 1.0F && cloudMinY <= deltaHeight + 1.0F;
+
+        if (noBFCMode) {
             GLRenderer.disableState(State.CULL_FACE);
-        }
-        else {
+        } else {
             GLRenderer.enableState(State.CULL_FACE);
         }
 
         this.textureManager.loadTexture("/assets/minecraft/textures/environment/clouds.png").bind();
         GLRenderer.enableState(State.BLEND);
         GLRenderer.setBlendFunc(BlendFactor.SRC_ALPHA, BlendFactor.ONE_MINUS_SRC_ALPHA);
-        float scale = 1F / 256F;
-        float uo = (float) MathHelper.floor(xo) * scale;
-        float vo = (float) MathHelper.floor(zo) * scale;
-        float xoffs2 = (float) (xo - (double) MathHelper.floor(xo));
-        float zoffs2 = (float) (zo - (double) MathHelper.floor(zo));
-        final int D = 8;
-        final byte radius = (byte) Math.max(1, 3 * GameSettings.CLOUD_RENDER_DISTANCE.value);
-        float e = 1F / 1024F;
-        GLRenderer.modelM4f().scale(ss, 1.0F, ss);
+        float scale = 0.00390625F;
+
+        float xOffset = (float)xo;
+        float zOffset = (float)zo;
 
+        byte radius = (byte)Math.max(1.0F, 3.0F * GameSettings.CLOUD_RENDER_DISTANCE.value);
         float red = rDim + (rSun - rDim) * aSun;
         float green = gDim + (gSun - gDim) * aSun;
         float blue = bDim + (bSun - bDim) * aSun;
 
-        tessellator.startDrawingQuads();
-        {
-            float width = (radius * 2 + 2) * D;
-            float xx = (-radius - 1) * D;
-            float zz = (-radius - 1) * D;
-            float xp = xx - xoffs2;
-            float zp = zz - zoffs2;
-            if (yy > -h - 1.0F) {
-                tessellator.setColor4f(red * 0.7F, green * 0.7F, blue * 0.7F, 0.8F);
-                tessellator.setNormal(0.0F, -1F, 0.0F);
-                tessellator.addVertexWithUV(xp + 0.0F, yy + 0.0F, zp + 0.0F, (xx + 0.0F) * scale + uo, (zz + 0.0F) * scale + vo);
-                tessellator.addVertexWithUV(xp + width, yy + 0.0F, zp + 0.0F, (xx + width) * scale + uo, (zz + 0.0F) * scale + vo);
-                tessellator.addVertexWithUV(xp + width, yy + 0.0F, zp + width, (xx + width) * scale + uo, (zz + width) * scale + vo);
-                tessellator.addVertexWithUV(xp + 0.0F, yy + 0.0F, zp + width, (xx + 0.0F) * scale + uo, (zz + width) * scale + vo);
-            }
-            if (yy <= h + 1.0F) {
-                tessellator.setColor4f(red, green, blue, 0.8F);
-                tessellator.setNormal(0.0F, 1.0F, 0.0F);
-                tessellator.addVertexWithUV(xp + 0.0F, (yy + h) - e, zp + 0.0F, (xx + 0.0F) * scale + uo, (zz + 0.0F) * scale + vo);
-                tessellator.addVertexWithUV(xp + 0.0F, (yy + h) - e, zp + width, (xx + 0.0F) * scale + uo, (zz + width) * scale + vo);
-                tessellator.addVertexWithUV(xp + width, (yy + h) - e, zp + width, (xx + width) * scale + uo, (zz + width) * scale + vo);
-                tessellator.addVertexWithUV(xp + width, (yy + h) - e, zp + 0.0F, (xx + width) * scale + uo, (zz + 0.0F) * scale + vo);
+        boolean needsRebuild = this.cloudBuffer == null || radius != cachedRadius;
+        if (needsRebuild) {
+            if (cloudBuffer != null) {
+                cloudBuffer.delete();
             }
+            cloudBuffer = meshFancyClouds(
+                    deltaHeight,
+                    scale,
+                    radius
+            );
+            cachedRadius = radius;
         }
-        for (int ix = -radius - 1; ix <= radius + 1; ix++) {
-            for (int iz = -radius - 1; iz <= radius + 1; iz++) {
-                float xx = ix * D;
-                float zz = iz * D;
-                float cloudX = xx - xoffs2;
-                float cloudZ = zz - zoffs2;
-                tessellator.setColor4f(red * 0.9F, green * 0.9F, blue * 0.9F, 0.8F);
-                if (ix > -1) {
-                    tessellator.setNormal(-1F, 0.0F, 0.0F);
-                    for (int s = 0; s < D; s++) {
-                        final float u = (xx + s + 0.5F) * scale + uo;
-                        tessellator.addVertexWithUV(cloudX + s + 0.0F, yy + 0.0F, cloudZ + 0.0F, u, (zz + 0.0F) * scale + vo);
-                        tessellator.addVertexWithUV(cloudX + s + 0.0F, yy + 0.0F, cloudZ + D, u, (zz + D) * scale + vo);
-                        tessellator.addVertexWithUV(cloudX + s + 0.0F, yy + h, cloudZ + D, u, (zz + D) * scale + vo);
-                        tessellator.addVertexWithUV(cloudX + s + 0.0F, yy + h, cloudZ + 0.0F, u, (zz + 0.0F) * scale + vo);
+
+        GLRenderer.modelM4f()
+                .translate(0.0F, cloudMinY, 0.0F)
+                .scale(modelScale, 1.0F, modelScale);
+
+        GLRenderer.setShader(Shaders.CLOUDS);
+        GLRenderer.globalSetUniforms(Shaders.CLOUDS);
+
+        GLRenderer.getShader().uniformFloat("xOffset", xOffset);
+        GLRenderer.getShader().uniformFloat("zOffset", zOffset);
+//        GLRenderer.getShader().uniformVec3f("uColor", red, green, blue);
+        GLRenderer.setColor3f(red, green, blue);
+
+        GLRenderer.setColorMask(false, false, false, true);
+        GLRenderer.render(this.cloudBuffer);
+        GLRenderer.setColorMask(true, true, true, true);
+
+        GLRenderer.render(this.cloudBuffer);
+
+        GLRenderer.popFrame();
+        this.mc.renderer.endRenderClouds(partialTick);
+    }
+
+    private RenderBuffer meshFancyClouds(
+            float deltaHeight,
+            float scale,
+            byte radius
+    ) {
+        TessellatorShader tessellator = GLRenderer.getTessellator();
+
+        float e = 9.765625E-4F;
+        tessellator.startDrawingQuads();
+        float width = (radius * 2 + 2) * 8;
+        float scaledX = (-radius - 1) * 8;
+        float scaledZ = (-radius - 1) * 8;
+        float adjustedX = scaledX;
+        float adjustedZ = scaledZ;
+
+        // Bottom faces
+        tessellator.setColor4f(0.7F, 0.7F, 0.7F, 0.8F);
+        tessellator.setNormal(0.0F, -1.0F, 0.0F);
+        tessellator.addVertexWithUV(adjustedX, 0.0F, adjustedZ, scaledX * scale, scaledZ * scale);
+        tessellator.addVertexWithUV(adjustedX + width, 0.0F, adjustedZ, (scaledX + width) * scale, scaledZ * scale);
+        tessellator.addVertexWithUV(adjustedX + width, 0.0F, adjustedZ + width, (scaledX + width) * scale, (scaledZ + width) * scale);
+        tessellator.addVertexWithUV(adjustedX, 0.0F, adjustedZ + width, scaledX * scale, (scaledZ + width) * scale);
+
+        // Top faces
+        tessellator.setColor4f(1, 1, 1, 0.8F);
+        tessellator.setNormal(0.0F, 1.0F, 0.0F);
+        tessellator.addVertexWithUV(adjustedX, deltaHeight - e, adjustedZ, scaledX * scale, scaledZ * scale);
+        tessellator.addVertexWithUV(adjustedX, deltaHeight - e, adjustedZ + width, scaledX * scale, (scaledZ + width) * scale);
+        tessellator.addVertexWithUV(adjustedX + width, deltaHeight - e, adjustedZ + width, (scaledX + width) * scale, (scaledZ + width) * scale);
+        tessellator.addVertexWithUV(adjustedX + width, deltaHeight - e, adjustedZ, (scaledX + width) * scale, scaledZ * scale);
+
+        // Side faces
+        for (int x = -radius - 1; x <= radius + 1; x++) {
+            for (int z = -radius - 1; z <= radius + 1; z++) {
+                scaledZ = x * 8;
+                adjustedX = z * 8;
+                adjustedZ = scaledZ;
+
+                float cloudZ = adjustedX;
+
+                tessellator.setColor4f(0.9F, 0.9F, 0.9F, 0.8F);
+                if (x > -1) {
+                    tessellator.setNormal(-1.0F, 0.0F, 0.0F);
+                    for (int delta = 0; delta < 8; delta++) {
+                        float u = (scaledZ + delta + 0.5F) * scale;
+                        tessellator.addVertexWithUV(adjustedZ + delta, 0.0F, cloudZ, u, adjustedX * scale);
+                        tessellator.addVertexWithUV(adjustedZ + delta, 0.0F, cloudZ + 8.0F, u, (adjustedX + 8.0F) * scale);
+                        tessellator.addVertexWithUV(adjustedZ + delta, deltaHeight, cloudZ + 8.0F, u, (adjustedX + 8.0F) * scale);
+                        tessellator.addVertexWithUV(adjustedZ + delta, deltaHeight, cloudZ, u, adjustedX * scale);
                     }
                 }
 
-                if (ix <= 1) {
+                if (x <= 1) {
                     tessellator.setNormal(1.0F, 0.0F, 0.0F);
-                    for (int s = 0; s < D; s++) {
-                        final float u = (xx + s + 0.5F) * scale + uo;
-                        tessellator.addVertexWithUV((cloudX + s + 1.0F) - e, yy + 0.0F, cloudZ + 0.0F, u, (zz + 0.0F) * scale + vo);
-                        tessellator.addVertexWithUV((cloudX + s + 1.0F) - e, yy + h, cloudZ + 0.0F, u, (zz + 0.0F) * scale + vo);
-                        tessellator.addVertexWithUV((cloudX + s + 1.0F) - e, yy + h, cloudZ + D, u, (zz + D) * scale + vo);
-                        tessellator.addVertexWithUV((cloudX + s + 1.0F) - e, yy + 0.0F, cloudZ + D, u, (zz + D) * scale + vo);
+                    for (int delta = 0; delta < 8; delta++) {
+                        float u = (scaledZ + delta + 0.5F) * scale;
+                        tessellator.addVertexWithUV(adjustedZ + delta + 1.0F - e, 0.0F, cloudZ, u, adjustedX * scale);
+                        tessellator.addVertexWithUV(adjustedZ + delta + 1.0F - e, deltaHeight, cloudZ, u, adjustedX * scale);
+                        tessellator.addVertexWithUV(adjustedZ + delta + 1.0F - e, deltaHeight, cloudZ + 8.0F, u, (adjustedX + 8.0F) * scale);
+                        tessellator.addVertexWithUV(adjustedZ + delta + 1.0F - e, 0.0F, cloudZ + 8.0F, u, (adjustedX + 8.0F) * scale);
                     }
                 }
 
-                tessellator.setColor4f(red * 0.8F, green * 0.8F, blue * 0.8F, 0.8F);
-                tessellator.setNormal(0.0F, 0.0F, -1F);
-                if (iz > -1) {
-                    for (int s = 0; s < D; s++) {
-                        final float v = (zz + s + 0.5F) * scale + vo;
-                        tessellator.addVertexWithUV(cloudX + 0.0F, yy + 0.0F, cloudZ + s + 0.0F, (xx + 0.0F) * scale + uo, v);
-                        tessellator.addVertexWithUV(cloudX + 0.0F, yy + h, cloudZ + s + 0.0F, (xx + 0.0F) * scale + uo, v);
-                        tessellator.addVertexWithUV(cloudX + D, yy + h, cloudZ + s + 0.0F, (xx + D) * scale + uo, v);
-                        tessellator.addVertexWithUV(cloudX + D, yy + 0.0F, cloudZ + s + 0.0F, (xx + D) * scale + uo, v);
+                tessellator.setColor4f(0.8F, 0.8F, 0.8F, 0.8F);
+                tessellator.setNormal(0.0F, 0.0F, -1.0F);
+                if (z > -1) {
+                    for (int s = 0; s < 8; s++) {
+                        float v = (adjustedX + s + 0.5F) * scale;
+                        tessellator.addVertexWithUV(adjustedZ, 0.0F, cloudZ + s, scaledZ * scale, v);
+                        tessellator.addVertexWithUV(adjustedZ, deltaHeight, cloudZ + s, scaledZ * scale, v);
+                        tessellator.addVertexWithUV(adjustedZ + 8.0F, deltaHeight, cloudZ + s, (scaledZ + 8.0F) * scale, v);
+                        tessellator.addVertexWithUV(adjustedZ + 8.0F, 0.0F, cloudZ + s, (scaledZ + 8.0F) * scale, v);
                     }
                 }
 
                 tessellator.setNormal(0.0F, 0.0F, 1.0F);
-                if (iz <= 1) {
-                    for (int s = 0; s < D; s++) {
-                        final float v = (zz + s + 0.5F) * scale + vo;
-                        tessellator.addVertexWithUV(cloudX + 0.0, yy + 0.0, (cloudZ + s + 1.0) - e, (xx + 0.0) * scale + uo, v);
-                        tessellator.addVertexWithUV(cloudX + D, yy + 0.0, (cloudZ + s + 1.0) - e, (xx + D) * scale + uo, v);
-                        tessellator.addVertexWithUV(cloudX + D, yy + h, (cloudZ + s + 1.0) - e, (xx + D) * scale + uo, v);
-                        tessellator.addVertexWithUV(cloudX + 0., yy + h, (cloudZ + s + 1.0) - e, (xx + 0.0) * scale + uo, v);
+                if (z <= 1) {
+                    for (int delta = 0; delta < 8; delta++) {
+                        float v = (adjustedX + delta + 0.5F) * scale;
+                        tessellator.addVertexWithUV(adjustedZ, 0.0F, cloudZ + delta + 1.0 - e, scaledZ * scale, v);
+                        tessellator.addVertexWithUV(adjustedZ + 8.0F, 0.0F, cloudZ + delta + 1.0 - e, (scaledZ + 8.0F) * scale, v);
+                        tessellator.addVertexWithUV(adjustedZ + 8.0F, deltaHeight, cloudZ + delta + 1.0 - e, (scaledZ + 8.0F) * scale, v);
+                        tessellator.addVertexWithUV(adjustedZ, deltaHeight, cloudZ + delta + 1.0 - e, scaledZ * scale, v);
                     }
                 }
-
             }
-
         }
-        GLRenderer.setShader(Shaders.CLOUDS);
-        GLRenderer.setColorMask(false, false, false, true);
-        tessellator.draw();
-
-        // Hack for not needing to record the cloud buffer twice
-        GLRenderer.setColorMask(true, true, true, true);
-        GLRenderer.getShader().bind();
-        GL41.glBindVertexArray(tessellator.vaos[tessellator.config]);
-        GL41.glBindBuffer(GL41.GL_ARRAY_BUFFER, tessellator.vbos[tessellator.config]);
-        GL41.glDrawArrays(tessellator.drawMode.cap, 0, tessellator.vertexCount);
-        GL41.glBindVertexArray(0);
-
 
-        GLRenderer.popFrame();
-        this.mc.renderer.endRenderClouds(partialTick);
+        return tessellator.record(GL41.glGenVertexArrays(), GL41.glGenBuffers());
     }
 
     private final DirtyChunkRendererSorter dirtyChunkRendererSorter = new DirtyChunkRendererSorter();
@@ -1879,6 +1917,39 @@ public final class RenderGlobal
         addParticle(particleKey, x, y, z, motionX, motionY, motionZ, data, 16D, serverSided);
     }
 
+    private void spawnSiphonParticle(
+        @NotNull TilePos meshPos, @NotNull Direction face,
+        double minRadius, double radiusRange,
+        double minDepth, double depthRange,
+        @NotNull ParticleSiphon.Look look)
+    {
+
+        final java.util.Random rand = this.world.rand;
+
+        double angle = rand.nextDouble() * java.lang.Math.PI * 2.0;
+        double radius = minRadius + rand.nextDouble() * radiusRange;
+        double depth = minDepth + rand.nextDouble() * depthRange;
+
+        double offA = java.lang.Math.cos(angle) * radius;
+        double offB = java.lang.Math.sin(angle) * radius;
+
+        double px = meshPos.x + 0.5 + face.offsetX() * (0.5 + depth);
+        double py = meshPos.y + 0.5 + face.offsetY() * (0.5 + depth);
+        double pz = meshPos.z + 0.5 + face.offsetZ() * (0.5 + depth);
+
+        switch (face.axis()) {
+            case Y -> { px += offA; pz += offB; }
+            case Z -> { px += offA; py += offB; }
+            default -> { py += offA; pz += offB; }
+        }
+
+        if (this.world.getBlockMaterial(new TilePos(px, py, pz)) != Materials.WATER) {
+            return;
+        }
+
+        this.mc.particleEngine.add(new ParticleSiphon(this.world, px, py, pz, meshPos, face, this.world.dimension, look));
+    }
+
     @Override
     public void addParticle(String particleId, double x, double y, double z,
                             double motionX, double motionY, double motionZ, int data, double maxDistance, boolean serverSided) {
@@ -2044,6 +2115,27 @@ public final class RenderGlobal
 
                 break;
 
+            case EVENT_MESH_SIPHON_PARTICLES: // Mesh siphon funnel
+
+                Direction meshFace = Direction.fromIdOrDefault(null, data);
+                if (meshFace == null) break;
+
+                TilePos meshPos = new TilePos(x, y, z);
+
+                for (int i = 0; i < 2; i++) {
+                    spawnSiphonParticle(meshPos, meshFace, 0.10, 0.32, 0.10, 0.80, ParticleSiphon.Look.BUBBLE);
+                }
+
+                if (random.nextInt(2) == 0) {
+                    spawnSiphonParticle(meshPos, meshFace, 0.10, 0.30, 0.15, 0.65, ParticleSiphon.Look.SPLASH);
+                }
+
+                for (int i = 0; i < 2; i++) {
+                    spawnSiphonParticle(meshPos, meshFace, 0.06, 0.32, 0.03, 0.16, ParticleSiphon.Look.BUBBLE);
+                }
+
+                break;
+
             case EVENT_ACID_SPREAD:
                 this.world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) x + 0.5D, (double) y + 0.5D, (double) z + 0.5D, "tile.acidbubble", 0.75F, 1.0F);
                 this.world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (double) x + 0.5D, (double) y + 0.5D, (double) z + 0.5D, "random.fizz", 0.5F, 2.6F + (random.nextFloat() - random.nextFloat()) * 0.8F);
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 3bb84b6bb..2e3339d3c 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
@@ -259,7 +259,6 @@ public final class BlockModelDispatcher
         addDispatch(new BlockModelGenericAxis<>(Blocks.LOG_OAK_MOSSY, loadDataModel("minecraft:block/log/oak_mossy")));
         addDispatch(new BlockModelGenericAxis<>(Blocks.LOG_THORN, loadDataModel("minecraft:block/log/thorn")));
         addDispatch(new BlockModelGenericAxis<>(Blocks.LOG_PALM, loadDataModel("minecraft:block/log/palm")));
-        addDispatch(new BlockModelGenericAxis<>(Blocks.LOG_PETRIFIED, loadDataModel("minecraft:block/log/petrified")));
 
         addDispatch(new BlockModelGenericLeaves<>(Blocks.LEAVES_OAK, "minecraft:block/leaves/oak"));
         addDispatch(new BlockModelGenericLeaves<>(Blocks.LEAVES_OAK_RETRO, "minecraft:block/leaves/oak_retro"));
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 28ea1fa62..150f2615e 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
@@ -1,5 +1,6 @@
 package net.minecraft.client.render.entity;
 
+import net.minecraft.client.Minecraft;
 import net.minecraft.client.option.GameSettings;
 import net.minecraft.client.render.font.FontRenderer;
 import net.minecraft.client.render.renderer.BlendFactor;
@@ -7,6 +8,11 @@ import net.minecraft.client.render.renderer.GLRenderer;
 import net.minecraft.client.render.renderer.Shaders;
 import net.minecraft.client.render.renderer.State;
 import net.minecraft.core.entity.EntityDispatcher;
+import net.minecraft.core.entity.animal.MobButterfly;
+import net.minecraft.core.item.Item;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.item.Items;
+import net.minecraft.core.net.command.TextFormatting;
 import net.minecraft.core.util.collection.NamespaceID;
 import net.minecraft.core.util.helper.LightIndexHelper;
 import net.minecraft.core.world.pos.TilePos;
@@ -328,6 +334,77 @@ public abstract class EntityRenderer<T extends Entity> {
         }
     }
 
+    private boolean isCharSequenceBlank(CharSequence cs) {
+        if (cs == null || cs.isEmpty()) {
+            return true;
+        }
+        for (int i = 0; i < cs.length(); i++) {
+            final char c = cs.charAt(i);
+            if (c == TextFormatting.SPECIAL_CHAR) {
+                i++;
+                continue;
+            }
+            if (!Character.isWhitespace(c) && !Character.isSpaceChar(c)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    protected void renderLivingLabel(final @NotNull TessellatorGeneral t, final @NotNull T entity, final @NotNull CharSequence text, final double x, final double y, final double z, final int maxDistance, final boolean depthTest) {
+
+        boolean holdingLabel = false;
+        if (Minecraft.getMinecraft().thePlayer != null) {
+            ItemStack heldStack = Minecraft.getMinecraft().thePlayer.getHeldItem();
+            if (heldStack != null) {
+                holdingLabel = heldStack.getItem() == Items.LABEL;
+            }
+        }
+
+        if (!holdingLabel && isCharSequenceBlank(text)) {
+            return;
+        }
+
+
+        final float cameraDistance = (float) Minecraft.getMinecraft().activeCamera.distanceTo(entity);
+        if (cameraDistance > maxDistance) {
+            return;
+        }
+        final FontRenderer sr = getFont();
+        final float scale = 1.6f / 60f;
+
+        GLRenderer.pushFrame();
+        GLRenderer.modelM4f().translate((float) x, (float) y + entity.getHeadHeight() + 0.8f, (float) z);
+        GLRenderer.modelM4f().rotateY(Math.toRadians(-this.renderDispatcher.viewLerpYaw));
+        GLRenderer.modelM4f().rotateX(Math.toRadians(this.renderDispatcher.viewLerpPitch));
+        GLRenderer.modelM4f().scale(-scale, -scale, scale);
+        GLRenderer.globalSetLightEnabled(false);
+        GLRenderer.setDepthMask(false);
+        if (!depthTest) GLRenderer.disableState(State.DEPTH_TEST);
+        GLRenderer.enableState(State.BLEND);
+        GLRenderer.setBlendFunc(BlendFactor.SRC_ALPHA, BlendFactor.ONE_MINUS_SRC_ALPHA);
+
+        GLRenderer.pushFrame();
+        GLRenderer.setShader(Shaders.COLOR);
+        t.startDrawingQuads();
+        final int halfTextWidth = sr.stringWidth(text) / 2;
+        t.setColor4f(0.0F, 0.0F, 0.0F, 0.25F);
+        t.addVertex(-halfTextWidth - 1, -1, 0.0D);
+        t.addVertex(-halfTextWidth - 1, 8, 0.0D);
+        t.addVertex(halfTextWidth, 8, 0.0D);
+        t.addVertex(halfTextWidth, -1, 0.0D);
+        t.draw();
+        GLRenderer.popFrame();
+
+        sr.render(text, -halfTextWidth, 0).setColor(0x20ffffff).call();
+        if (!depthTest) GLRenderer.enableState(State.DEPTH_TEST);
+        GLRenderer.setDepthMask(true);
+        sr.render(text, -halfTextWidth, 0).setColor(0xFFFFFF).call();
+        GLRenderer.globalSetLightEnabled(true);
+        GLRenderer.disableState(State.BLEND);
+        GLRenderer.popFrame();
+    }
+
     public @NotNull FontRenderer getFont() {
         return this.renderDispatcher.font;
     }
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 3393df3ae..57bdb44a4 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
@@ -188,46 +188,6 @@ public abstract class MobRenderer<T extends Mob> extends EntityRenderer<T> {
         }
     }
 
-    protected void renderLivingLabel(final @NotNull TessellatorGeneral t, final @NotNull T entity, final @NotNull CharSequence text, final double x, final double y, final double z, final int maxDistance, final boolean depthTest) {
-        final float cameraDistance = (float) Minecraft.getMinecraft().activeCamera.distanceTo(entity);
-        if (cameraDistance > maxDistance) {
-            return;
-        }
-        final FontRenderer sr = getFont();
-        final float scale = 1.6f / 60f;
-
-        GLRenderer.pushFrame();
-        GLRenderer.modelM4f().translate((float) x, (float) y + entity.getHeadHeight() + 0.8f, (float) z);
-        GLRenderer.modelM4f().rotateY(Math.toRadians(-this.renderDispatcher.viewLerpYaw));
-        GLRenderer.modelM4f().rotateX(Math.toRadians(this.renderDispatcher.viewLerpPitch));
-        GLRenderer.modelM4f().scale(-scale, -scale, scale);
-        GLRenderer.globalSetLightEnabled(false);
-        GLRenderer.setDepthMask(false);
-        if (!depthTest) GLRenderer.disableState(State.DEPTH_TEST);
-        GLRenderer.enableState(State.BLEND);
-        GLRenderer.setBlendFunc(BlendFactor.SRC_ALPHA, BlendFactor.ONE_MINUS_SRC_ALPHA);
-
-        GLRenderer.pushFrame();
-        GLRenderer.setShader(Shaders.COLOR);
-        t.startDrawingQuads();
-        final int halfTextWidth = sr.stringWidth(text) / 2;
-        t.setColor4f(0.0F, 0.0F, 0.0F, 0.25F);
-        t.addVertex(-halfTextWidth - 1, -1, 0.0D);
-        t.addVertex(-halfTextWidth - 1, 8, 0.0D);
-        t.addVertex(halfTextWidth, 8, 0.0D);
-        t.addVertex(halfTextWidth, -1, 0.0D);
-        t.draw();
-        GLRenderer.popFrame();
-
-        sr.render(text, -halfTextWidth, 0).setColor(0x20ffffff).call();
-        if (!depthTest) GLRenderer.enableState(State.DEPTH_TEST);
-        GLRenderer.setDepthMask(true);
-        sr.render(text, -halfTextWidth, 0).setColor(0xFFFFFF).call();
-        GLRenderer.globalSetLightEnabled(true);
-        GLRenderer.disableState(State.BLEND);
-        GLRenderer.popFrame();
-    }
-
     protected int getOverlayColor(final @NotNull T entity, final float partialTick) {
         return 0;
     }
diff --git a/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererButterfly.java b/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererButterfly.java
index a3a303683..fe66802e4 100644
--- a/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererButterfly.java
+++ b/game/client/src/main/java/net/minecraft/client/render/entity/MobRendererButterfly.java
@@ -22,6 +22,10 @@ public class MobRendererButterfly extends EntityRenderer<MobButterfly> {
         final float scale = 0.45f + (sizeRandom.nextFloat() * 0.35f);
         final float yRot = MathHelper.lerp(butterfly.yRotO, butterfly.yRot, partialTick);
         drawButterfly(tessellator, butterfly.tickCount, scale, yRot, x, y, z, partialTick, this.renderDispatcher.textureManager, getTexture(butterfly));
+
+        if (!butterfly.nickname.isEmpty()) {
+            renderLivingLabel(tessellator, butterfly, butterfly.getDisplayName(), x, y, z, 64, true);
+        }
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/render/particle/ParticleSiphon.java b/game/client/src/main/java/net/minecraft/client/render/particle/ParticleSiphon.java
new file mode 100644
index 000000000..5cf1c61ba
--- /dev/null
+++ b/game/client/src/main/java/net/minecraft/client/render/particle/ParticleSiphon.java
@@ -0,0 +1,95 @@
+package net.minecraft.client.render.particle;
+
+import net.minecraft.core.block.entity.TileEntityMesh;
+import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.util.helper.Direction;
+import net.minecraft.core.world.Dimension;
+import net.minecraft.core.world.World;
+import net.minecraft.core.world.pos.TilePos;
+import org.jetbrains.annotations.NotNull;
+import org.joml.Vector3d;
+
+/**
+ * A bubble caught in a mesh's siphon. Samples {@link TileEntityMesh#computeSiphonVelocity}
+ * every tick exactly like a siphoned item does
+ */
+public class ParticleSiphon extends Particle {
+    public enum Look {
+        BUBBLE,
+        SPLASH
+    }
+
+    private final @NotNull TilePos meshPos;
+    private final @NotNull Direction direction;
+    private final double wanderPhase;
+    private final double swirlJitter;
+    private final double pullJitter;
+    private final @NotNull Vector3d velocity = new Vector3d();
+
+    public ParticleSiphon(@NotNull World world, double x, double y, double z,
+                          @NotNull TilePos meshPos, @NotNull Direction direction,
+                          @NotNull Dimension dimension, @NotNull Look look) {
+        super(world, x, y, z, 0.0, 0.0, 0.0);
+
+        this.meshPos = meshPos;
+        this.direction = direction;
+
+        this.noPhysics = true;
+        this.gravity = 0.0f;
+
+        this.rCol = this.gCol = this.bCol = 1.0F;
+        if (look == Look.SPLASH) {
+            this.tex = (dimension == Dimension.NETHER ? ParticleTextureCache.ICON_SPLASH_RAIN_BLOOD : ParticleTextureCache.ICON_SPLASH_RAIN)[random.nextInt(4)];
+            this.size = this.size * (random.nextFloat() * 0.3F + 0.25F);
+        } else {
+            this.tex = dimension == Dimension.NETHER ? ParticleTextureCache.ICON_BUBBLE_BOILING : ParticleTextureCache.ICON_BUBBLE;
+            this.size = this.size * (random.nextFloat() * 0.4F + 0.2F);
+        }
+
+        this.wanderPhase = random.nextFloat() * Math.PI * 2.0;
+        this.swirlJitter = 0.8 + random.nextFloat() * 0.4;
+        this.pullJitter = 0.85 + random.nextFloat() * 0.3;
+
+        this.xd = this.yd = this.zd = 0.0;
+        this.lifetime = (int)((60 + random.nextInt(40)) / TileEntityMesh.SIPHON_SPEED);
+    }
+
+    @Override
+    public void tick() {
+        this.cachedLightmapCoord = calcLightIndex(1f);
+        this.xo = this.x;
+        this.yo = this.y;
+        this.zo = this.z;
+
+        double wander = Math.sin(this.age * 0.15 + this.wanderPhase) * 0.1;
+        TileEntityMesh.computeSiphonVelocity(this.meshPos, this.direction, this.x, this.y, this.z,
+            wander, this.swirlJitter, this.pullJitter, this.velocity);
+
+        this.xd = this.velocity.x;
+        this.yd = this.velocity.y;
+        this.zd = this.velocity.z;
+        this.move(this.xd, this.yd, this.zd);
+
+        this.age++;
+        if (this.lifetime-- <= 0) {
+            remove();
+            return;
+        }
+
+        double dx = (this.meshPos.x + 0.5) - this.x;
+        double dy = (this.meshPos.y + 0.5) - this.y;
+        double dz = (this.meshPos.z + 0.5) - this.z;
+        if (dx * dx + dy * dy + dz * dz < 0.35 * 0.35) {
+            remove();
+            return;
+        }
+
+        if (this.world.getBlockMaterial(Cache.queryPos.set(this.x, this.y, this.z)) != Materials.WATER) {
+            remove();
+        }
+    }
+
+    private static final class Cache {
+        private static final @NotNull TilePos queryPos = new TilePos();
+    }
+}
diff --git a/game/client/src/main/java/net/minecraft/client/render/renderer/GLRenderer.java b/game/client/src/main/java/net/minecraft/client/render/renderer/GLRenderer.java
index 2f7cb3cd8..ca38a8a69 100644
--- a/game/client/src/main/java/net/minecraft/client/render/renderer/GLRenderer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/renderer/GLRenderer.java
@@ -164,6 +164,7 @@ public class GLRenderer {
         shader.uniformFloat("uLightMapStrength", instance.currentLightMapStrength);
         shader.uniformFloat("uAlphaTest", instance.currentAlphaTest);
         shader.uniformVec4f("uColor", Color.redFromInt(instance.currentColor)/255f, Color.greenFromInt(instance.currentColor)/255f, Color.blueFromInt(instance.currentColor)/255f, Color.alphaFromInt(instance.currentColor)/255f);
+        shader.uniformFloat("uCelestialAngle", (instance.mc.currentWorld != null ? instance.mc.currentWorld.getCelestialAngle(instance.mc.timer.partialTicks) : 0f));
         shader.uniformFloat("uCurrentTick", instance.mc.ticksRan + instance.mc.timer.partialTicks);
         shader.uniformFloat("uPartialTick", instance.mc.timer.partialTicks);
         shader.uniformFloat("frameTimeCounter", ShaderHelper.getFrameTimeCounter());
diff --git a/game/client/src/main/java/net/minecraft/client/render/shader/framebuffer/FrameBuffer.java b/game/client/src/main/java/net/minecraft/client/render/shader/framebuffer/FrameBuffer.java
index c787d848f..137740ba5 100644
--- a/game/client/src/main/java/net/minecraft/client/render/shader/framebuffer/FrameBuffer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/shader/framebuffer/FrameBuffer.java
@@ -22,6 +22,7 @@ public abstract class FrameBuffer {
     private final @NotNull Vector2ic size;
 
     private boolean deleted = false;
+    private boolean validated = false;
 
     protected FrameBuffer(final @NotNull FrameBufferAttachment @NotNull ... attachments) {
         if (attachments.length == 0) {
@@ -67,9 +68,12 @@ public abstract class FrameBuffer {
 
         this.id = GL41.glGenFramebuffers();
 
+        GL41.glBindFramebuffer(GL41.GL_FRAMEBUFFER, this.id);
         for (final @NotNull FrameBufferAttachment attachment : attachments) {
             attachment.attachToFramebuffer(this);
         }
+        this.checkComplete();
+        GL41.glBindFramebuffer(GL41.GL_FRAMEBUFFER, 0);
     }
 
     public @Nullable FrameBufferAttachment getAttachment(final @NotNull AttachmentType attachmentType) {
@@ -103,10 +107,6 @@ public abstract class FrameBuffer {
         return this.deleted;
     }
 
-    public boolean isComplete() {
-        return GL41.glCheckFramebufferStatus(this.id) == GL41.GL_FRAMEBUFFER_COMPLETE;
-    }
-
     private void checkComplete() {
         final int status = GL41.glCheckFramebufferStatus(GL41.GL_FRAMEBUFFER);
 
@@ -152,9 +152,9 @@ public abstract class FrameBuffer {
             throw new IllegalStateException("Frame buffer deleted!");
         }
 
-        this.whileBound(this::checkComplete);
-
         this.blitInner();
+
+        GL41.glBindFramebuffer(GL41.GL_FRAMEBUFFER, 0);
     }
 
     public final void bind() {
@@ -162,8 +162,6 @@ public abstract class FrameBuffer {
             throw new IllegalStateException("Frame buffer deleted!");
         }
 
-        this.whileBound(this::checkComplete);
-
         this.bindInner();
     }
 
@@ -174,8 +172,6 @@ public abstract class FrameBuffer {
             throw new IllegalStateException("Frame buffer deleted!");
         }
 
-        this.whileBound(this::checkComplete);
-
         GL41.glBindFramebuffer(GL41.GL_FRAMEBUFFER, 0);
     }
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRenderer.java b/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRenderer.java
index cff4b9765..66dc9ca31 100644
--- a/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRenderer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRenderer.java
@@ -16,8 +16,10 @@ public abstract class TerrainRenderer {
 	}
 	
 	public abstract void renderSolidTerrain(float partialTicks);
-	
+
 	public abstract void renderTranslucentTerrain(float partialTicks);
+
+	public void resetChunkSorting() { }
 	
 	public abstract @NotNull ChunkRenderer createChunkRenderer(
 		@NotNull World world,
diff --git a/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRendererMultiDraw.java b/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRendererMultiDraw.java
index 0d09677e7..c46509529 100644
--- a/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRendererMultiDraw.java
+++ b/game/client/src/main/java/net/minecraft/client/render/terrain/TerrainRendererMultiDraw.java
@@ -43,6 +43,7 @@ public class TerrainRendererMultiDraw extends TerrainRenderer {
     private final @NotNull Vector3d renderPos = new Vector3d();
     private final @NotNull Vector3d prevSort = new Vector3d();
     private final @NotNull Vector3d d = new Vector3d();
+    private boolean forceResort = true;
 
     public TerrainRendererMultiDraw(final @NotNull Minecraft minecraft) {
         super(minecraft);
@@ -62,7 +63,8 @@ public class TerrainRendererMultiDraw extends TerrainRenderer {
 
         this.d.set(this.renderPos).sub(this.prevSort);
 
-        if (this.d.dot(this.d) > 64) {
+        if (this.forceResort || this.d.dot(this.d) > 64) {
+            this.forceResort = false;
             this.prevSort.set(this.renderPos);
 
             this.mc.renderGlobal.resortChunks(
@@ -189,6 +191,11 @@ public class TerrainRendererMultiDraw extends TerrainRenderer {
         return renderList;
     }
 
+    @Override
+    public void resetChunkSorting() {
+        this.forceResort = true;
+    }
+
     @Override
     public @NotNull ChunkRenderer createChunkRenderer(
         final @NotNull World world,
diff --git a/game/client/src/main/java/net/minecraft/client/render/terrain/VertexBuffer.java b/game/client/src/main/java/net/minecraft/client/render/terrain/VertexBuffer.java
index f52c164ab..937819794 100644
--- a/game/client/src/main/java/net/minecraft/client/render/terrain/VertexBuffer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/terrain/VertexBuffer.java
@@ -36,7 +36,7 @@ public class VertexBuffer {
         this.vbo = glGenBuffers();
         this.vao = GL41.glGenVertexArrays();
         glBindBuffer(GL_ARRAY_BUFFER, this.vbo);
-        glBufferData(GL_ARRAY_BUFFER, this.capacity, GL_DYNAMIC_DRAW);
+        glBufferData(GL_ARRAY_BUFFER, this.capacity, GL_STATIC_DRAW);
         glBindBuffer(GL_ARRAY_BUFFER, 0);
     }
 
@@ -139,7 +139,7 @@ public class VertexBuffer {
 
         final int newBuffer = glGenBuffers();
         glBindBuffer(GL_ARRAY_BUFFER, newBuffer);
-        glBufferData(GL_ARRAY_BUFFER, newCapacity, GL_DYNAMIC_DRAW);
+        glBufferData(GL_ARRAY_BUFFER, newCapacity, GL_STATIC_DRAW);
 
         OpenGLHelper.checkError("create expand buffer");
 
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 52f300e11..7678ac024 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
@@ -173,6 +173,7 @@ public class TileEntityRendererSign extends TileEntityRenderer<TileEntitySign> {
     private static void drawTexturedModalRect(final double width, final double height, final boolean blended, final @NotNull IconCoordinate coordinate) {
         if (blended) {
             GLRenderer.globalSetLightEnabled(false);
+            GLRenderer.setLightMapStrength(0);
         }
         coordinate.parentAtlas.bind();
         final TessellatorGeneral tessellator = GLRenderer.getTessellator();
@@ -184,6 +185,7 @@ public class TileEntityRendererSign extends TileEntityRenderer<TileEntitySign> {
         tessellator.addVertexWithUV(width / 2, height / 2, 0, coordinate.getIconUMax(), coordinate.getIconVMin());
         tessellator.draw();
         GLRenderer.globalSetLightEnabled(true);
+        GLRenderer.setLightMapStrength(1);
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/util/helper/ItemDragHandler.java b/game/client/src/main/java/net/minecraft/client/util/helper/ItemDragHandler.java
index c2507eac2..928f1b498 100644
--- a/game/client/src/main/java/net/minecraft/client/util/helper/ItemDragHandler.java
+++ b/game/client/src/main/java/net/minecraft/client/util/helper/ItemDragHandler.java
@@ -36,6 +36,9 @@ public class ItemDragHandler extends Gui {
 	protected ItemStack draggingItemStack;
 	protected final List<Slot> draggedSlots = new ArrayList<>();
 
+	protected int modDragButton = -1;
+	protected final List<Slot> modDraggedSlots = new ArrayList<>();
+
 	private int lastX = Integer.MAX_VALUE; private int lastY = Integer.MAX_VALUE;
 	private final ArrayList<Slot> dragOverBuffer = new ArrayList<>(2);
 
@@ -58,16 +61,22 @@ public class ItemDragHandler extends Gui {
         this.renderOffsetX = 0;
         this.renderOffsetY = 0;
 		
-		if(isDragging()) {
-			this.dragOverBuffer.clear();
-			if (Math.max(this.lastX, this.lastY) == Integer.MAX_VALUE) {
-				final var slot = this.container.getSlotAtPosition(mouseX, mouseY);
-				if (slot != null) this.dragOverBuffer.add(slot);
-			} else {
-				this.container.getSlotsBetweenPositions(this.lastX, this.lastY, mouseX, mouseY, this.dragOverBuffer);
+		if(isModDragging()) {
+			sweepSlotsSinceLastFrame(mouseX, mouseY);
+			for (final var slot : this.dragOverBuffer) {
+				if (this.modDraggedSlots.contains(slot)) {
+					continue;
+				}
+				this.modDraggedSlots.add(slot);
+				if (slot.hasItem()) {
+					this.container.clickSlot(slot, this.modDragButton);
+				}
 			}
-			this.lastX = mouseX;
-			this.lastY = mouseY;
+			return;
+		}
+
+		if(isDragging()) {
+			sweepSlotsSinceLastFrame(mouseX, mouseY);
 			if (this.dragOverBuffer.isEmpty()) this.onDragOutsideOfSlots();
 			else for (final var slot : this.dragOverBuffer) onDragOverSlot(slot);
 
@@ -98,7 +107,19 @@ public class ItemDragHandler extends Gui {
 			}
 		}
 	}
-	
+
+	private void sweepSlotsSinceLastFrame(int mouseX, int mouseY) {
+		this.dragOverBuffer.clear();
+		if (Math.max(this.lastX, this.lastY) == Integer.MAX_VALUE) {
+			final var slot = this.container.getSlotAtPosition(mouseX, mouseY);
+			if (slot != null) this.dragOverBuffer.add(slot);
+		} else {
+			this.container.getSlotsBetweenPositions(this.lastX, this.lastY, mouseX, mouseY, this.dragOverBuffer);
+		}
+		this.lastX = mouseX;
+		this.lastY = mouseY;
+	}
+
 	protected void onDragOverSlot(Slot slot) {
 		if(slot == null) return;
 		
@@ -156,11 +177,15 @@ public class ItemDragHandler extends Gui {
 		if(pressed) {
 			if(mod) {
                 this.cancelRelease.add(button);
+				startModDragging(x, y, slot, button);
                 this.container.clickInventory(x, y, button);
 			}else {
 				mousePressed(x, y, slot, button);
 			}
 		}else {
+			if(button == this.modDragButton) {
+				stopModDragging();
+			}
 			if(this.cancelRelease.contains(button)) {
                 this.cancelRelease.remove(button);
 				return;
@@ -169,6 +194,29 @@ public class ItemDragHandler extends Gui {
 		}
 
     }
+
+	private void startModDragging(final int x, final int y, final @Nullable Slot slot, int button) {
+		if(button != 0 && button != 1) {
+			return;
+		}
+        this.modDragButton = button;
+        this.modDraggedSlots.clear();
+		if(slot != null) {
+            this.modDraggedSlots.add(slot);
+		}
+		this.lastX = x;
+		this.lastY = y;
+	}
+
+	protected final void stopModDragging() {
+        this.modDragButton = -1;
+        this.modDraggedSlots.clear();
+		this.lastX = this.lastY = Integer.MAX_VALUE;
+	}
+
+	public boolean isModDragging() {
+		return this.modDragButton != -1;
+	}
 	
 	public void click(InventoryAction action, int[] args) {
         this.mc.playerController.handleInventoryMouseClick(this.container.inventorySlots.containerId, action, args, this.mc.thePlayer);
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/active_front.png b/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/active_front.png
index e9cd558a4..65a1d6a18 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/active_front.png and b/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/active_front.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/idle_front.png b/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/idle_front.png
index 759ddd82a..b7efd41d6 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/idle_front.png and b/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/idle_front.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/side.png b/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/side.png
index 2ae603dfd..0aabaa65f 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/side.png and b/game/client/src/main/resources/assets/minecraft/textures/block/motion_sensor/side.png differ
diff --git a/game/client/src/main/resources/assets/minecraft/textures/entity/sheep/wool_overlay.png b/game/client/src/main/resources/assets/minecraft/textures/entity/sheep/wool_overlay.png
index fedd749ba..3491d4243 100644
Binary files a/game/client/src/main/resources/assets/minecraft/textures/entity/sheep/wool_overlay.png and b/game/client/src/main/resources/assets/minecraft/textures/entity/sheep/wool_overlay.png differ
diff --git a/game/client/src/main/resources/credits.txt b/game/client/src/main/resources/credits.txt
index 02d46efa7..7d4766ad1 100644
--- a/game/client/src/main/resources/credits.txt
+++ b/game/client/src/main/resources/credits.txt
@@ -32,8 +32,12 @@ Snek
 *Special Thanks
 AndroidDr (@Pedro270707)
 --Brigadier command parser implementation
+Apollointhehouse
+--Fancy Cloud Optimizations
 Astronomical360
 --Eucalyptus log texture tweak
+big sir (@.xdddd)
+--Integer Slider fix
 bones (@frazzelbones)
 --Retired BTA helper
 Celeste
diff --git a/game/client/src/main/resources/shaders/clouds.fsh b/game/client/src/main/resources/shaders/clouds.fsh
index a482602c3..67f978238 100644
--- a/game/client/src/main/resources/shaders/clouds.fsh
+++ b/game/client/src/main/resources/shaders/clouds.fsh
@@ -26,15 +26,16 @@ layout (std140) uniform Matrices {
 uniform float uAlphaTest;
 uniform int isEyeInLiquid;
 uniform vec2 uViewPortSize;
+uniform vec4 uColor;
 
 uniform sampler2D colortex;
 uniform sampler2D lighttex;
 
 float distance() {
-	vec4 fragPos = matrices.projectionInv * vec4((gl_FragCoord.xy / uViewPortSize) * 2.0 - 1.0, gl_FragCoord.z * 2.0 - 1.0, 1.0);
+	vec4 fragPos = matrices.viewInv * matrices.projectionInv * vec4((gl_FragCoord.xy / uViewPortSize) * 2.0 - 1.0, gl_FragCoord.z * 2.0 - 1.0, 1.0);
 	fragPos /= fragPos.w;
 
-	return length(fragPos.xyz);
+	return length(fragPos.xz);
 }
 
 void main() {
@@ -50,7 +51,7 @@ void main() {
 		discard;
 	}
 	
-	vec4 col = Color;
+	vec4 col = Color * uColor;
 
 	float fogStrength = 0.0;
 
diff --git a/game/client/src/main/resources/shaders/clouds.vsh b/game/client/src/main/resources/shaders/clouds.vsh
index cb437bb97..04756272e 100644
--- a/game/client/src/main/resources/shaders/clouds.vsh
+++ b/game/client/src/main/resources/shaders/clouds.vsh
@@ -1,25 +1,40 @@
 #version 410 core
+
 layout (location = 0) in vec3 aPos;
 layout (location = 1) in vec4 aColor;
 layout (location = 2) in vec2 aUV;
-//layout (location = 3) in vec2 aLightmap;
-//layout (location = 4) in vec3 aNormal;
 
 out vec4 Color;
 out vec2 TexCoord;
 
-uniform mat4 model;
+uniform float xOffset;
+uniform float zOffset;
 
 layout (std140) uniform Matrices {
-	mat4 projection;
-	mat4 projectionInv;
-	mat4 view;
-	mat4 viewInv;
+    mat4 projection;
+    mat4 projectionInv;
+    mat4 view;
+    mat4 viewInv;
 } matrices;
 
+uniform mat4 model;
+
 void main() {
-	TexCoord = aUV;
-	Color = aColor;
+    Color = aColor;
+
+    float xInt = floor(xOffset);
+    float zInt = floor(zOffset);
+    float xFrac = fract(xOffset);
+    float zFrac = fract(zOffset);
+
+    TexCoord = aUV + vec2(
+        xInt * 1/256f,
+        zInt * 1/256f
+    );
+
+    vec3 pos = aPos;
+    pos.x -= xFrac;
+    pos.z -= zFrac;
 
-	gl_Position = matrices.projection * matrices.view * model * vec4(aPos, 1);
-}
\ No newline at end of file
+    gl_Position = matrices.projection * matrices.view * model * vec4(pos, 1.0);
+}
diff --git a/game/core/src/main/java/net/minecraft/core/InventoryAction.java b/game/core/src/main/java/net/minecraft/core/InventoryAction.java
index 217dc78f6..65d757ad6 100644
--- a/game/core/src/main/java/net/minecraft/core/InventoryAction.java
+++ b/game/core/src/main/java/net/minecraft/core/InventoryAction.java
@@ -78,19 +78,23 @@ public enum InventoryAction {
 		setId(CREATIVE_GRAB, 13);
 		setId(CREATIVE_MOVE, 14);
 		setId(CREATIVE_DELETE, 15);
-		setId(CREATIVE_DRAG, 20);
 
 		setId(INTERACT_SLOT, 16);
 		setId(INTERACT_GRABBED, 17);
-		
+
 		setId(EQUIP_ARMOR, 18);
 
 		setId(SORT, 19);
 
 		setId(LOCK, 20);
+
+		setId(CREATIVE_DRAG, 21);
 	}
-	
+
 	private static void setId(InventoryAction action, int id) {
+		if(actions[id] != null) {
+			throw new IllegalStateException("Inventory action id " + id + " is already used by " + actions[id] + ", cannot also assign it to " + action + "!");
+		}
 		actions[id] = action;
 		action.id = id;
 	}
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java
index 8c1af0d91..df602716e 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicConduit.java
@@ -72,6 +72,7 @@ public class BlockLogicConduit extends BlockLogic
     private static final int CONNECT_LEVER = pc++;
     private static final int CONNECT_TORCH = pc++;
     private static final int CONNECT_REDSTONE_LANTERN = pc++;
+    private static final int CONNECT_REDSTONE_ORE = pc++;
     private static final int CONNECT_REDSTONE_BLOCK = pc++;
     private static final int CONNECT_MOTION_SENSOR = pc++;
     private static final int CONNECT_MATCHER = pc++;
@@ -426,6 +427,10 @@ public class BlockLogicConduit extends BlockLogic
                         }
                     }
                 }
+
+                if (nBlockType.isEmittingSignal(world1, nTilePos, side)) {
+                    return PROPAGATION_LENGTH;
+                }
             } else {
                 if (nBlockType == Blocks.PUMPKIN_REDSTONE) {
                     Side pumpkinFace = Side.fromId(world1.getBlockData(nTilePos));
@@ -584,6 +589,9 @@ public class BlockLogicConduit extends BlockLogic
         if (nBlock == Blocks.BLOCK_REDSTONE) {
             return CONNECT_REDSTONE_BLOCK;
         }
+        if (logic instanceof BlockLogicOreRedstone) {
+            return CONNECT_REDSTONE_ORE;
+        }
         if (logic instanceof BlockLogicRepeater) {
             Side axis = BlockLogicRepeater.getSideFromMeta(nData);
             return (side == axis || side == axis.opposite()) ? CONNECT_REPEATER : CONNECT_NONE;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java
index e413fb88e..5b7288751 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicMesh.java
@@ -4,10 +4,14 @@ import net.minecraft.core.block.entity.TileEntityMesh;
 import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.EntityItem;
+import net.minecraft.core.util.helper.Direction;
+import net.minecraft.core.world.LevelListener;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
 
+import java.util.Random;
+
 public class BlockLogicMesh extends BlockLogicTransparent
 {
 
@@ -23,6 +27,16 @@ public class BlockLogicMesh extends BlockLogicTransparent
         return false;
     }
 
+    @Override
+    public void animationTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand) {
+        if (!(world.getTileEntity(tilePos) instanceof TileEntityMesh mesh)) return;
+
+        Direction direction = mesh.getSiphonDirection();
+        if (direction == null) return;
+
+        world.playBlockEvent(tilePos, LevelListener.EVENT_MESH_SIPHON_PARTICLES, direction.id);
+    }
+
     @Override
     public boolean collidesWithEntity(@NotNull Entity entity, @NotNull World world, @NotNull TilePosc tilePos) {
         return !(entity instanceof EntityItem);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicStatue.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicStatue.java
index 3b136a987..6b542f61f 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicStatue.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicStatue.java
@@ -159,7 +159,7 @@ public class BlockLogicStatue extends BlockLogic implements BlockLogic.MatcherEn
 
     @Override
     public boolean pushesEntities(@NotNull WorldSource source, @NotNull TilePosc tilePos, @NotNull Class<? extends Entity> entityClass) {
-        return true;
+        return false;
     }
 
     @Override
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 c78a3e4f8..2ec2342a0 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
@@ -468,9 +468,6 @@ public final class Blocks {
     public static final @NotNull Block<?> LOG_PALM = register("log.palm", "minecraft:block/log_palm", 287, BlockLogicLog::new)
         .withSound(BlockSounds.WOOD).withHardness(2.0F)
         .withTags(BlockTags.FENCES_CONNECT, BlockTags.MINEABLE_BY_AXE);
-    public static final @NotNull Block<?> LOG_PETRIFIED = register("log.petrified", "minecraft:block/log_petrified", 288, b -> new BlockLogicAxisAligned(b, Materials.STONE))
-            .withSound(BlockSounds.STONE).withHardness(8.0F)
-            .withTags(BlockTags.FENCES_CONNECT, BlockTags.CHAINLINK_FENCES_CONNECT, BlockTags.MINEABLE_BY_PICKAXE);
 
     public static final @NotNull Block<?> LEAVES_OAK = register("leaves.oak", "minecraft:block/leaves_oak", 290, BlockLogicLeavesOak::new)
         .withSound(BlockSounds.GRASS).withHardness(0.2F).withLightBlock(1)
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java
index 6f08d6bd5..d5b4f7c59 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMesh.java
@@ -2,6 +2,8 @@ package net.minecraft.core.block.entity;
 import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.material.Materials;
+import net.minecraft.core.util.helper.MathHelper;
 import net.minecraft.core.current.wire.WireHandler;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.EntityItem;
@@ -9,7 +11,9 @@ import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.util.helper.Axis;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.world.pos.TilePos;
+import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
+import org.joml.Vector3d;
 import org.jetbrains.annotations.Nullable;
 import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
@@ -20,8 +24,18 @@ import java.util.stream.Collectors;
 public class TileEntityMesh extends TileEntity {
     public final @NotNull List<EntityItem> trackedItemsSiphoning = new ArrayList<>(List.of());
     protected final Map<Direction, Boolean> siphonDirValidityCache = new EnumMap<>(Direction.class);
-    protected final int siphonWidth = 5;
-    protected final int siphonHeight = 2;
+    public static final int SIPHON_WIDTH = 5;
+    public static final int SIPHON_HEIGHT = 2;
+    public static final double SIPHON_SPEED = 0.75;
+    protected final int siphonWidth = SIPHON_WIDTH;
+    protected final int siphonHeight = SIPHON_HEIGHT;
+    private final Vector3d siphonVelocity = new Vector3d();
+    private final TilePos pathQueryPos = new TilePos();
+    private static final int REACH_CAPACITY = SIPHON_WIDTH * SIPHON_WIDTH * SIPHON_HEIGHT;
+    private final boolean[] reachableWater = new boolean[REACH_CAPACITY];
+    private final int[] reachQueue = new int[REACH_CAPACITY];
+    private int reachBaseX, reachBaseY, reachBaseZ;
+    private int reachSpanX, reachSpanY, reachSpanZ;
     public int ticksRan = 0;
     public TileEntityMesh() {
     }
@@ -32,88 +46,291 @@ public class TileEntityMesh extends TileEntity {
             return;
         }
 
+        if (ticksRan == 0){
+            ticksRan = worldObj.rand.nextInt(360);
+        }
+        ticksRan++;
+
+        handleSiphon(getSiphonDirection());
+    }
+
+    public @Nullable Direction getSiphonDirection(){
+        if (!isSiphonEnabled()) {
+            return null;
+        }
+
         siphonDirValidityCache.clear();
         for (Direction direction : WireHandler.Directions.ALL) {
             siphonDirValidityCache.put(direction, this.isValidWater(direction));
         }
-        handleSiphon(siphonDirValidityCache);
+        return getSingleValidFace(siphonDirValidityCache);
+    }
 
-        if (ticksRan == 0){
-            ticksRan = worldObj.rand.nextInt(360);
-        }
-        ticksRan++;
+    protected boolean isSiphonEnabled(){
+        return true;
     }
 
     @Override
-    public void readAdditionalData(@NotNull CompoundTag compoundTag) {
-
+    public void invalidate() {
+        super.invalidate();
+        releaseAllTrackedItems();
     }
 
     @Override
-    public void writeAdditionalData(@NotNull CompoundTag compoundTag) {
+    public void readAdditionalData(@NotNull CompoundTag compoundTag) {}
 
-    }
+    @Override
+    public void writeAdditionalData(@NotNull CompoundTag compoundTag) {}
 
-    protected void handleSiphon(@NotNull Map<Direction, Boolean> validSiphonDirections){
+    protected void handleSiphon(@Nullable Direction direction){
         assert this.worldObj != null;
 
-        Direction direction = getSingleValidFace(validSiphonDirections);
-        if(direction == Direction.NONE) { return; }
+        if(direction == null) {
+            releaseAllTrackedItems();
+            return;
+        }
 
         AABBdc siphonBox = getSiphonBox(tilePos, direction);
         List<Entity> entitiesWithinSiphonBox = this.worldObj.getEntitiesWithinAABBExcludingEntity(null, siphonBox);
 
-        if (!this.worldObj.isClientSide) {
-            removeTrackedItems(entitiesWithinSiphonBox);
-        }
+        computeReachableWater(siphonBox, direction);
+        removeTrackedItems(entitiesWithinSiphonBox);
 
         for (Entity entity : entitiesWithinSiphonBox)
         {
             if (entity instanceof EntityItem item && entity.isInWater())
             {
-                if(item.meshSiphoning == null && item.siphonDirection == Direction.NONE){
-                    item.siphonDirection = direction.opposite();
-                    item.meshSiphoning = this;
+                if (!canSiphonItem(item)) continue;
+                if (!canReachItem(item)) continue;
+                if (!tryClaimItem(item, direction)) continue;
+
+                if (!this.worldObj.isClientSide) {
+                    Player player = entity.world.getClosestPlayer(tilePos.x, tilePos.y, tilePos.z, 12);
+                    if(player != null) {
+                        player.triggerAchievement(Achievements.DOWN_THE_DRAIN);
+                    }
                 }
+                addItemsToBeTracked(item);
+                siphonItemIntoBlock(item, direction);
+            }
+        }
+    }
 
-                if(direction.opposite().equals(item.siphonDirection) && item.meshSiphoning == this) {
 
-                    if (!this.worldObj.isClientSide) {
-                        Player player = entity.world.getClosestPlayer(tilePos.x, tilePos.y, tilePos.z, 12);
-                        if(player != null) {
-                            player.triggerAchievement(Achievements.DOWN_THE_DRAIN);
-                        }
-                        addItemsToBeTracked(item);
-                    }
-                    siphonItemIntoBlock(item, direction);
+    protected boolean canSiphonItem(@NotNull EntityItem item){
+        return true;
+    }
+
+    protected void computeReachableWater(@NotNull AABBdc siphonBox, @NotNull Direction direction){
+        assert this.worldObj != null;
+
+        reachBaseX = MathHelper.floor(siphonBox.minX());
+        reachBaseY = MathHelper.floor(siphonBox.minY());
+        reachBaseZ = MathHelper.floor(siphonBox.minZ());
+        reachSpanX = (int) Math.ceil(siphonBox.maxX()) - reachBaseX;
+        reachSpanY = (int) Math.ceil(siphonBox.maxY()) - reachBaseY;
+        reachSpanZ = (int) Math.ceil(siphonBox.maxZ()) - reachBaseZ;
+
+        Arrays.fill(reachableWater, false);
+
+        int seed = reachIndex(
+            this.tilePos.x + direction.offsetX(),
+            this.tilePos.y + direction.offsetY(),
+            this.tilePos.z + direction.offsetZ()
+        );
+        if (seed < 0) {
+            return;
+        }
+
+        reachableWater[seed] = true;
+        int head = 0;
+        int tail = 0;
+        reachQueue[tail++] = seed;
+
+        while (head < tail) {
+            int index = reachQueue[head++];
+
+            int x = reachBaseX + (index / reachSpanZ) / reachSpanY;
+            int y = reachBaseY + (index / reachSpanZ) % reachSpanY;
+            int z = reachBaseZ + index % reachSpanZ;
+
+            for (Direction step : WireHandler.Directions.ALL) {
+                int nextX = x + step.offsetX();
+                int nextY = y + step.offsetY();
+                int nextZ = z + step.offsetZ();
+
+                if (!movesAwayFromMesh(x, y, z, nextX, nextY, nextZ)) {
+                    continue;
+                }
+
+                int neighbour = reachIndex(nextX, nextY, nextZ);
+                if (neighbour < 0 || reachableWater[neighbour]) {
+                    continue;
                 }
+
+                if (this.worldObj.getBlockMaterial(pathQueryPos.set(nextX, nextY, nextZ)) != Materials.WATER) {
+                    continue;
+                }
+
+                reachableWater[neighbour] = true;
+                reachQueue[tail++] = neighbour;
+            }
+        }
+    }
+
+    private boolean movesAwayFromMesh(int x, int y, int z, int nextX, int nextY, int nextZ){
+        if (nextX != x) return Math.abs(nextX - this.tilePos.x) > Math.abs(x - this.tilePos.x);
+        if (nextY != y) return Math.abs(nextY - this.tilePos.y) > Math.abs(y - this.tilePos.y);
+        return Math.abs(nextZ - this.tilePos.z) > Math.abs(z - this.tilePos.z);
+    }
+
+    private int reachIndex(int x, int y, int z){
+        int localX = x - reachBaseX;
+        int localY = y - reachBaseY;
+        int localZ = z - reachBaseZ;
+
+        if (localX < 0 || localY < 0 || localZ < 0
+            || localX >= reachSpanX || localY >= reachSpanY || localZ >= reachSpanZ) {
+            return -1;
+        }
+
+        return (localX * reachSpanY + localY) * reachSpanZ + localZ;
+    }
+
+    protected boolean canReachItem(@NotNull EntityItem item){
+        return isInReachableWater(item) && hasLineOfSightTo(item);
+    }
+
+    protected boolean hasLineOfSightTo(@NotNull EntityItem item){
+        if (this.worldObj == null) {
+            return false;
+        }
+
+        final double originX = item.x;
+        final double originY = item.y;
+        final double originZ = item.z;
+
+        final double dx = (this.tilePos.x + 0.5) - originX;
+        final double dy = (this.tilePos.y + 0.5) - originY;
+        final double dz = (this.tilePos.z + 0.5) - originZ;
+
+        int x = MathHelper.floor(originX);
+        int y = MathHelper.floor(originY);
+        int z = MathHelper.floor(originZ);
+
+        final int stepX = dx > 0 ? 1 : (dx < 0 ? -1 : 0);
+        final int stepY = dy > 0 ? 1 : (dy < 0 ? -1 : 0);
+        final int stepZ = dz > 0 ? 1 : (dz < 0 ? -1 : 0);
+
+        final double tDeltaX = stepX == 0 ? Double.MAX_VALUE : Math.abs(1.0 / dx);
+        final double tDeltaY = stepY == 0 ? Double.MAX_VALUE : Math.abs(1.0 / dy);
+        final double tDeltaZ = stepZ == 0 ? Double.MAX_VALUE : Math.abs(1.0 / dz);
+
+        double tMaxX = stepX == 0 ? Double.MAX_VALUE : (stepX > 0 ? (x + 1 - originX) : (originX - x)) * tDeltaX;
+        double tMaxY = stepY == 0 ? Double.MAX_VALUE : (stepY > 0 ? (y + 1 - originY) : (originY - y)) * tDeltaY;
+        double tMaxZ = stepZ == 0 ? Double.MAX_VALUE : (stepZ > 0 ? (z + 1 - originZ) : (originZ - z)) * tDeltaZ;
+
+        int guard = 4 * (SIPHON_WIDTH + SIPHON_HEIGHT);
+        while (!(x == this.tilePos.x && y == this.tilePos.y && z == this.tilePos.z)) {
+            if (guard-- <= 0) {
+                return false;
+            }
+
+            if (tMaxX < tMaxY && tMaxX < tMaxZ) {
+                x += stepX;
+                tMaxX += tDeltaX;
+            } else if (tMaxY < tMaxZ) {
+                y += stepY;
+                tMaxY += tDeltaY;
+            } else {
+                z += stepZ;
+                tMaxZ += tDeltaZ;
+            }
+
+            if (x == this.tilePos.x && y == this.tilePos.y && z == this.tilePos.z) {
+                break;
+            }
+
+            if (this.worldObj.getBlockMaterial(pathQueryPos.set(x, y, z)) != Materials.WATER) {
+                return false;
             }
         }
+
+        return true;
+    }
+
+    protected boolean isInReachableWater(@NotNull EntityItem item){
+        int index = reachIndex(MathHelper.floor(item.x), MathHelper.floor(item.y), MathHelper.floor(item.z));
+        if (index >= 0 && reachableWater[index]) {
+            return true;
+        }
+
+        int below = reachIndex(MathHelper.floor(item.x), MathHelper.floor(item.y - 0.25), MathHelper.floor(item.z));
+        return below >= 0 && reachableWater[below];
+    }
+
+
+    protected boolean tryClaimItem(@NotNull EntityItem item, @NotNull Direction direction){
+        TileEntityMesh currentOwner = item.meshSiphoning;
+
+        if (currentOwner == this) {
+            item.siphonDirection = direction.opposite();
+            return true;
+        }
+
+        if (currentOwner != null && !currentOwner.isInvalid() && currentOwner.worldObj == this.worldObj) {
+            double ownerDist = currentOwner.getDistanceFrom(item.x, item.y, item.z);
+            if (ownerDist <= this.getDistanceFrom(item.x, item.y, item.z)) {
+                return false;
+            }
+            currentOwner.releaseItem(item);
+        }
+
+        item.meshSiphoning = this;
+        item.siphonDirection = direction.opposite();
+        return true;
     }
 
     protected void removeTrackedItems(@NotNull List<Entity> entitiesWithinSiphonBox){
         Iterator<EntityItem> iterator = trackedItemsSiphoning.iterator();
         while (iterator.hasNext()) {
             EntityItem item = iterator.next();
-            if (!entitiesWithinSiphonBox.contains(item)) {
-                item.siphonDirection = Direction.NONE;
-                item.meshSiphoning = null;
+            if (item.removed || !canSiphonItem(item) || !canReachItem(item) || !entitiesWithinSiphonBox.contains(item)) {
+                clearOwnership(item);
                 iterator.remove();
-                break;
             }
         }
     }
 
+    protected void releaseAllTrackedItems(){
+        for (EntityItem item : trackedItemsSiphoning) {
+            clearOwnership(item);
+        }
+        trackedItemsSiphoning.clear();
+    }
+
+    protected void releaseItem(@NotNull EntityItem item){
+        trackedItemsSiphoning.remove(item);
+        clearOwnership(item);
+    }
+
+    private void clearOwnership(@NotNull EntityItem item){
+        if (item.meshSiphoning == this) {
+            item.meshSiphoning = null;
+            item.siphonDirection = null;
+        }
+    }
+
     protected void addItemsToBeTracked(@NotNull EntityItem item){
         if(!trackedItemsSiphoning.contains(item))
             trackedItemsSiphoning.add((item));
     }
 
-    protected @NotNull Direction getSingleValidFace(@NotNull Map<Direction, Boolean> validSiphonDirections){
-        //If none are valid or if more than one side is valid, returns Direction.NONE
+    protected @Nullable Direction getSingleValidFace(@NotNull Map<Direction, Boolean> validSiphonDirections){
+        //If none are valid or if more than one side is valid, returns null
         //else, returns the valid Direction
 
-        Direction direction = Direction.NONE;
+        Direction direction = null;
 
         int amountTrue = 0;
         for (boolean value : validSiphonDirections.values()) {
@@ -135,7 +352,7 @@ public class TileEntityMesh extends TileEntity {
         }
 
         if (direction == Direction.DOWN){
-            return Direction.NONE;
+            return null;
         }
 
         return direction;
@@ -188,79 +405,105 @@ public class TileEntityMesh extends TileEntity {
         return new AABBd(minX, minY, minZ, maxX, maxY, maxZ);
     }
 
+    // Deterministic item noise so that the items agree between client and server
+    protected static double itemNoise(@NotNull EntityItem item, int salt){
+        int h = item.id * 0x9E3779B9 + salt * 0x85EBCA6B;
+        h ^= h >>> 15;
+        h *= 0x2545F491;
+        h ^= h >>> 13;
+        return (h >>> 8) / (double)(1 << 24);
+    }
+
     protected void siphonItemIntoBlock(@NotNull EntityItem item, @NotNull Direction direction){
 
-        double targetX = this.tilePos.x + 0.5;
-        double targetY = this.tilePos.y + 0.5;
-        double targetZ = this.tilePos.z + 0.5;
+        double wanderPhase = itemNoise(item, 1) * Math.PI * 2.0;
+        double swirlJitter = 0.8 + itemNoise(item, 3) * 0.4;
+        double pullJitter  = 0.85 + itemNoise(item, 4) * 0.3;
+        double wander      = Math.sin(item.age * 0.15 + wanderPhase) * 0.1;
+
+        computeSiphonVelocity(this.tilePos, direction, item.x, item.y, item.z,
+                wander, swirlJitter, pullJitter, this.siphonVelocity);
+
+        item.xd = this.siphonVelocity.x;
+        item.yd = this.siphonVelocity.y;
+        item.zd = this.siphonVelocity.z;
+    }
+
+    public static @NotNull Vector3d computeSiphonVelocity(
+            @NotNull TilePosc meshPos, @NotNull Direction direction,
+            double x, double y, double z,
+            double wander, double swirlJitter, double pullJitter,
+            @NotNull Vector3d out
+    ){
+        double targetX = meshPos.x() + 0.5 + (direction.axis() == Axis.X ? 0.0 : wander);
+        double targetY = meshPos.y() + 0.5 + (direction.axis() == Axis.Y ? 0.0 : wander * 0.5);
+        double targetZ = meshPos.z() + 0.5 + (direction.axis() == Axis.Z ? 0.0 : -wander);
 
         //difference vectors
-        double diffX = targetX - item.x;
-        double diffY = targetY - item.y;
-        double diffZ = targetZ - item.z;
+        double diffX = targetX - x;
+        double diffY = targetY - y;
+        double diffZ = targetZ - z;
 
-        //distance from item
+        //distance from the mesh
         double dist = Math.sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ);
 
         //we don't divide by zero in this household
-        if (dist > 0.01) {
-
-            //Siphon variables yippee!!!!
-            double maxRange         = Math.max(siphonWidth / 2.0, siphonHeight);
-            double clampedDist      = Math.min(dist, maxRange);
-            double proximityFactor  = 1.0 + (maxRange / (clampedDist + 0.5));
-            double pullStrength     = 0.05;
-            double dynSwirlStrength = (pullStrength * 1.5) * proximityFactor;
-
-            /*
-            //normalize the pull speed and apply the dynamic speed multiplier
-            double pullX = (diffX / dist) * speedMultiplier;
-            double pullY = (diffY / dist) * speedMultiplier;
-            double pullZ = (diffZ / dist) * speedMultiplier;
-            */
-
-            //non-normalize implementation
-            double speedMultiplier  = pullStrength * proximityFactor;
-            double pullX = diffX  * speedMultiplier;
-            double pullY = diffY  * speedMultiplier;
-            double pullZ = diffZ  * speedMultiplier;
-
-            //axis check to make sure the swirl behaves correctly depending on facing direction
-            double sX = 0, sY = 0, sZ = 0;
-            if (direction.axis() == Axis.Y) {
-                sX = -diffZ;
-                sZ = diffX;
-            } else if (direction.axis() == Axis.Z) {
-                sX = -diffY;
-                sY = diffX;
-            } else {
-                sY = -diffZ;
-                sZ = diffY;
-            }
+        if (dist <= 0.01) {
+            return out.set(0.0, 0.0, 0.0);
+        }
 
-            double swirlDist = Math.sqrt(sX * sX + sY * sY + sZ * sZ);
-            if (swirlDist > 0.01) {
-                // swirl also gets faster as it gets closer
-                sX = (sX / swirlDist) * dynSwirlStrength;
-                sY = (sY / swirlDist) * dynSwirlStrength;
-                sZ = (sZ / swirlDist) * dynSwirlStrength;
-            }
+        //Siphon variables yippee!!!!
+        double maxRange         = Math.max(SIPHON_WIDTH / 2.0, SIPHON_HEIGHT);
+        double clampedDist      = Math.min(dist, maxRange);
+        double proximityFactor  = 1.0 + (maxRange / (clampedDist + 0.5));
+        double pullStrength     = 0.05 * pullJitter;
+        double swirlFade        = Math.min(1.0, dist / 0.75);
+        double dynSwirlStrength = (pullStrength * 1.5) * proximityFactor * swirlJitter * swirlFade;
+
+        double speedMultiplier  = pullStrength * proximityFactor;
+
+        double lateralBoost = 3.0;
+        double pullX = diffX * speedMultiplier * (direction.axis() == Axis.X ? 1.0 : lateralBoost);
+        double pullY = diffY * speedMultiplier * (direction.axis() == Axis.Y ? 1.0 : lateralBoost);
+        double pullZ = diffZ * speedMultiplier * (direction.axis() == Axis.Z ? 1.0 : lateralBoost);
+
+        //axis check to make sure the swirl behaves correctly depending on facing direction
+        double sX = 0, sY = 0, sZ = 0;
+        if (direction.axis() == Axis.Y) {
+            sX = -diffZ;
+            sZ = diffX;
+        } else if (direction.axis() == Axis.Z) {
+            sX = -diffY;
+            sY = diffX;
+        } else {
+            sY = -diffZ;
+            sZ = diffY;
+        }
 
-            item.xd = pullX + sX;
-            item.yd = pullY + sY;
-            item.zd = pullZ + sZ;
-
-            //kinda gave up here, don't know what I was on but it kinda works so..?
-            double slowStrength = 0.5 - (0.2 * (1.0 - (clampedDist / maxRange)));
-            switch (direction) {
-                case UP    -> {if (item.yd < 0) item.yd *= slowStrength;}
-                case DOWN  -> {if (item.yd > 0) item.yd *= slowStrength;}
-                case NORTH -> {if (item.zd > 0) item.zd *= slowStrength;}
-                case SOUTH -> {if (item.zd < 0) item.zd *= slowStrength;}
-                case WEST  -> {if (item.xd > 0) item.xd *= slowStrength;}
-                case EAST  -> {if (item.xd < 0) item.xd *= slowStrength;}
-            }
+        double swirlDist = Math.sqrt(sX * sX + sY * sY + sZ * sZ);
+        if (swirlDist > 0.01) {
+            // swirl also gets faster as it gets closer
+            sX = (sX / swirlDist) * dynSwirlStrength;
+            sY = (sY / swirlDist) * dynSwirlStrength;
+            sZ = (sZ / swirlDist) * dynSwirlStrength;
+        } else {
+            sX = sY = sZ = 0;
         }
+
+        out.set(pullX + sX, pullY + sY, pullZ + sZ);
+
+        //kinda gave up here, don't know what I was on but it kinda works so..?
+        double slowStrength = 0.5 - (0.2 * (1.0 - (clampedDist / maxRange)));
+        switch (direction) {
+            case UP    -> {if (out.y < 0) out.y *= slowStrength;}
+            case DOWN  -> {if (out.y > 0) out.y *= slowStrength;}
+            case NORTH -> {if (out.z > 0) out.z *= slowStrength;}
+            case SOUTH -> {if (out.z < 0) out.z *= slowStrength;}
+            case WEST  -> {if (out.x > 0) out.x *= slowStrength;}
+            case EAST  -> {if (out.x < 0) out.x *= slowStrength;}
+        }
+
+        return out.mul(SIPHON_SPEED);
     }
 
     protected boolean isValidWater(@NotNull Direction direction){
diff --git a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java
index 1680d12e8..afcaa9081 100644
--- a/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java
+++ b/game/core/src/main/java/net/minecraft/core/block/entity/TileEntityMeshGold.java
@@ -1,51 +1,44 @@
 package net.minecraft.core.block.entity;
 
 import com.mojang.nbt.tags.CompoundTag;
-import net.minecraft.core.achievement.Achievements;
-import net.minecraft.core.current.wire.WireHandler;
-import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.EntityItem;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.net.packet.Packet;
 import net.minecraft.core.net.packet.PacketTileEntityData;
-import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.world.World;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.joml.primitives.AABBdc;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
 
 public class TileEntityMeshGold extends TileEntityMesh {
     @Nullable
     public ItemStack filterItem;
-    public int ticksRan = 0;
+
     public TileEntityMeshGold() {
     }
 
     @Override
     public void tick()
     {
-        if (this.worldObj == null || this.worldObj.isClientSide) {
+        if (this.worldObj == null) {
             return;
         }
 
-        if(filterItem != null){
-            siphonDirValidityCache.clear();
-            for (Direction direction : WireHandler.Directions.ALL) {
-                siphonDirValidityCache.put(direction, this.isValidWater(direction));
-            }
-            handleSiphon(siphonDirValidityCache);
-        }
-
         if (ticksRan == 0){
             ticksRan = worldObj.rand.nextInt(360);
         }
         ticksRan++;
+
+        if (this.worldObj.isClientSide) {
+            return;
+        }
+
+        handleSiphon(getSiphonDirection());
+    }
+
+    @Override
+    protected boolean isSiphonEnabled(){
+        return this.filterItem != null;
     }
 
     @Override
@@ -97,39 +90,11 @@ public class TileEntityMeshGold extends TileEntityMesh {
     }
 
     @Override
-    protected void handleSiphon(@NotNull Map<Direction, Boolean> validSiphonDirections){
-        //null-checks were already made
-        assert this.worldObj != null;
-        assert this.filterItem != null;
-
-        Direction direction = getSingleValidFace(validSiphonDirections);
-        if(direction == Direction.NONE){return;}
-        AABBdc siphonBox = getSiphonBox(tilePos, direction);
-        List<Entity> entitiesWithinSiphonBox = this.worldObj.getEntitiesWithinAABBExcludingEntity(null, siphonBox);
-        removeTrackedItems(entitiesWithinSiphonBox);
-
-        for (Entity entity : entitiesWithinSiphonBox)
-        {
-            if (entity instanceof EntityItem item && entity.isInWater())
-            {
-                if(item.meshSiphoning == null && item.siphonDirection == Direction.NONE){
-                    item.siphonDirection = direction.opposite();
-                    item.meshSiphoning = this;
-                }
-                if(direction.opposite() == item.siphonDirection && item.meshSiphoning == this) {
-                    if (item.item.getItem() == this.filterItem.getItem() && item.item.getMetadata() == this.filterItem.getMetadata()) {
-
-                        Player player = entity.world.getClosestPlayer(tilePos.x, tilePos.y, tilePos.z, 12);
-                        if(player != null) {
-                            player.triggerAchievement(Achievements.DOWN_THE_DRAIN);
-                        }
-
-                        siphonItemIntoBlock(item, direction);
-                        addItemsToBeTracked(item);
-                    }
-                }
-            }
-        }
+    protected boolean canSiphonItem(@NotNull EntityItem item){
+        return this.filterItem != null
+            && item.item != null
+            && item.item.getItem() == this.filterItem.getItem()
+            && item.item.getMetadata() == this.filterItem.getMetadata();
     }
 
     @Override
diff --git a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapedJsonAdapter.java b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapedJsonAdapter.java
index 5cc1a36ee..371cc2092 100644
--- a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapedJsonAdapter.java
+++ b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapedJsonAdapter.java
@@ -25,7 +25,9 @@ public class RecipeCraftingShapedJsonAdapter implements RecipeJsonAdapter<Recipe
         ItemStack result = context.deserialize(obj.get("result").getAsJsonObject(),ItemStack.class);
         boolean consumeContainers = obj.has("consumeContainers") ? obj.get("consumeContainers").getAsBoolean() : DEFAULT_CONSUME_CONTAINER;
         boolean mirror = obj.has("mirror") ? obj.get("mirror").getAsBoolean() : DEFAULT_ALLOW_MIRROR;
-        return RecipeRegistry.parseRecipe(pattern, symbols, result, consumeContainers, mirror);
+        RecipeEntryCraftingShaped recipe = RecipeRegistry.parseRecipe(pattern, symbols, result, consumeContainers, mirror);
+        if (obj.has("group")) recipe.consolidationGroup = obj.get("group").getAsString();
+        return recipe;
     }
 
     @Override
@@ -75,6 +77,9 @@ public class RecipeCraftingShapedJsonAdapter implements RecipeJsonAdapter<Recipe
         if (src.allowMirrored != DEFAULT_ALLOW_MIRROR) {
             obj.addProperty("mirror",src.allowMirrored);
         }
+        if (src.consolidationGroup != null) {
+            obj.addProperty("group", src.consolidationGroup);
+        }
         return obj;
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapelessJsonAdapter.java b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapelessJsonAdapter.java
index a70c1008c..005400415 100644
--- a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapelessJsonAdapter.java
+++ b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/adapter/RecipeCraftingShapelessJsonAdapter.java
@@ -16,7 +16,9 @@ public class RecipeCraftingShapelessJsonAdapter implements RecipeJsonAdapter<Rec
         JsonObject obj = json.getAsJsonObject();
         List<RecipeSymbol> symbols = obj.get("inputs").getAsJsonArray().asList().stream().map((E)->context.<RecipeSymbol>deserialize(E,RecipeSymbol.class)).collect(Collectors.toList());
         ItemStack result = context.deserialize(obj.get("result").getAsJsonObject(),ItemStack.class);
-        return new RecipeEntryCraftingShapeless(symbols,result);
+        RecipeEntryCraftingShapeless recipe = new RecipeEntryCraftingShapeless(symbols,result);
+        if (obj.has("group")) recipe.consolidationGroup = obj.get("group").getAsString();
+        return recipe;
     }
 
     @Override
@@ -27,6 +29,9 @@ public class RecipeCraftingShapelessJsonAdapter implements RecipeJsonAdapter<Rec
         List<RecipeSymbol> symbols = src.getInput();
         obj.add("inputs",context.serialize(symbols));
         obj.add("result",context.serialize(src.getOutput()));
+        if (src.consolidationGroup != null) {
+            obj.addProperty("group", src.consolidationGroup);
+        }
         return obj;
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCrafting.java b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCrafting.java
index c8796b640..e8b816128 100644
--- a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCrafting.java
+++ b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/entry/RecipeEntryCrafting.java
@@ -8,6 +8,8 @@ import net.minecraft.core.player.inventory.container.ContainerCrafting;
 
 public abstract class RecipeEntryCrafting<I,O> extends RecipeEntryBase<I,O,Void> {
 
+    public String consolidationGroup = null;
+
     public RecipeEntryCrafting(I input, O output) {
         super(input, output, null);
     }
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 ebba016a8..cf52c9be9 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
@@ -164,6 +164,7 @@ public abstract class Entity
     public boolean isWalking = false;
     protected boolean muteStepSounds = false;
     public float footSize = 0.0F;
+    public float stepDownSize = 0.0F;
 
     // Tick
     private boolean firstTick = true;
@@ -445,6 +446,7 @@ public abstract class Entity
             this.z = (this.bb.minZ + this.bb.maxZ) / 2.0;
             return;
         }
+        boolean wasOnGround = this.onGround;
         this.ySlideOffset *= 0.4f;
         double d3 = this.x;
         double d4 = this.z;
@@ -608,6 +610,22 @@ public abstract class Entity
                 this.bb.set(workingBox4);
             }
         }
+
+        if (this.stepDownSize > 0.0f
+            && wasOnGround
+            && !isSneaking()
+            && (oldXd != 0.0 || oldZd != 0.0)
+            && yd < 0.0 && oldYd == yd) {
+            double snap = -this.stepDownSize;
+            List<AABBdc> below = this.world.getCubes(this, MathHelper.aabbExpand(this.bb, 0.0, snap, 0.0, workingBox2));
+            for (int i = 0; i < below.size(); i++) {
+                snap = MathHelper.aabbClipYCollide(below.get(i), this.bb, snap);
+            }
+            if (snap > -this.stepDownSize) {
+                this.bb.translate(0.0, snap, 0.0);
+                yd += snap;
+            }
+        }
         //fix for entity jitter when blasted by cannonballs (can cause some client-side jitter when entity is under a ceiling. unsure exactly why)
         if (!this.world.isClientSide || this instanceof Player || !(this instanceof Mob) || this.locallySimulated) {
             this.x = (this.bb.minX + this.bb.maxX) / 2D;
diff --git a/game/core/src/main/java/net/minecraft/core/entity/EntityItem.java b/game/core/src/main/java/net/minecraft/core/entity/EntityItem.java
index 552eacf7e..e4b4dd745 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/EntityItem.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/EntityItem.java
@@ -28,6 +28,7 @@ import net.minecraft.core.util.helper.MathHelper;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePos;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.joml.primitives.AABBd;
 
 public class EntityItem extends Entity
@@ -35,7 +36,7 @@ public class EntityItem extends Entity
     public ItemStack item;
     public int lifetime;
     public int age;
-    public Direction siphonDirection = Direction.NONE;
+    public @Nullable Direction siphonDirection = null;
     public TileEntityMesh meshSiphoning = null;
     public int pickupDelay;
     private int health;
@@ -459,7 +460,7 @@ public class EntityItem extends Entity
     }
 
     public boolean isBeingSiphoned(){
-        return siphonDirection != Direction.NONE;
+        return siphonDirection != null;
     }
 
     @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 be0aa4c9a..3bdbf6ee9 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
@@ -40,6 +40,7 @@ public class MobPig
         super(world);
         setTextureIdentifier("minecraft", "pig");
         setSize(0.9F, 0.9F);
+        this.stepDownSize = 0.5F;
         this.mobDrops.add(new WeightedRandomLootObject(Items.FOOD_PORKCHOP_RAW.getDefaultStack(), 1, 2));
         this.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);
diff --git a/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonster.java b/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonster.java
index 4b5c6377f..74cc5cb67 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonster.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonster.java
@@ -93,6 +93,7 @@ public abstract class MobMonster extends MobPathfinder
                 return true;
             }
             if (attacker != this) {
+                if (attacker instanceof Player player && !player.gamemode.hasHostileMobs()) return true;
                 this.target = attacker;
             }
             return true;
diff --git a/game/core/src/main/java/net/minecraft/core/entity/monster/MobSkeleton.java b/game/core/src/main/java/net/minecraft/core/entity/monster/MobSkeleton.java
index 21a1d54af..bdc2ecc6e 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/monster/MobSkeleton.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/monster/MobSkeleton.java
@@ -55,13 +55,6 @@ public class MobSkeleton extends MobMonsterArmored<HumanArmorShape>
         entityData.define(DATA_ATTACK_TIME, attackTime, Integer.class);
     }
 
-    @Override
-    public void spawnInit() {
-        super.spawnInit();
-
-        setHeldItem(DEFAULT_HELD_ITEM.copy());
-    }
-
     @Override
 	public void onLivingUpdate()
     {
diff --git a/game/core/src/main/java/net/minecraft/core/entity/monster/MobZombiePig.java b/game/core/src/main/java/net/minecraft/core/entity/monster/MobZombiePig.java
index 80250b69e..795fc27fd 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/monster/MobZombiePig.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/monster/MobZombiePig.java
@@ -15,6 +15,7 @@ import net.minecraft.core.world.World;
 import net.minecraft.core.util.helper.DamageType;
 import net.minecraft.core.world.pos.TilePos;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.joml.primitives.AABBd;
 
 import java.util.ArrayList;
@@ -41,13 +42,6 @@ public class MobZombiePig extends MobZombie {
         this.scoreValue = 500;
     }
 
-    @Override
-    public void spawnInit() {
-        super.spawnInit();
-
-        setHeldItem(DEFAULT_HELD_ITEM.copy());
-    }
-
     @Override
     public void tick() {
         if (this.tickCount % 200 == 0 && this.target instanceof Player && ((Player) this.target).getStat(Achievements.MOST_WANTED) <= 0) {
@@ -173,4 +167,9 @@ public class MobZombiePig extends MobZombie {
     public boolean hurtByNetherWater() {
         return false;
     }
+
+    @Override
+    public @Nullable ItemStack getHeldItem() {
+        return DEFAULT_HELD_ITEM;
+    }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/player/Player.java b/game/core/src/main/java/net/minecraft/core/entity/player/Player.java
index d7dc626da..d77fea5ab 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/player/Player.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/player/Player.java
@@ -180,7 +180,9 @@ public abstract class Player
 
     protected void setupScale() {
         this.isDwarf = this.world.getGameRuleValue(GameRules.DWARF_MODE);
-        if (this.isDwarf) {
+        if (this.sleeping) {
+            setSize(0.2f, 0.2f);
+        } else if (this.isDwarf) {
             setSize(0.6f, isSneaking() ? 0.5f : 0.8f);
         } else {
             setSize(0.6f, isSneaking() ? 1.5f : 1.8f);
@@ -1268,9 +1270,9 @@ public abstract class Player
                 damage++;
             }
             boolean wasAlive = entity.isAlive();
-            entity.hurt(this, damage, DamageType.COMBAT);
+            boolean damageDealt = entity.hurt(this, damage, DamageType.COMBAT);
             ItemStack itemstack = getCurrentEquippedItem();
-            if (itemstack != null && (entity instanceof Mob) && wasAlive) {
+            if (damageDealt && itemstack != null && (entity instanceof Mob) && wasAlive) {
                 itemstack.hitEntity((Mob) entity, this);
                 if (itemstack.stackSize <= 0) {
                     destroyCurrentEquippedItem();
@@ -1350,23 +1352,23 @@ public abstract class Player
         if (this.world.isBlockLoaded(x, y, z)) {
             int meta = this.world.getBlockMetadata(x, y, z);
             int dir = BlockLogicBed.DIRECTION.get(meta);
-            float xOff = 0.5F;
-            float zOff = 0.5F;
+            double xOff = 0.5D;
+            double zOff = 0.5D;
 
             if (dir == 0) {
-                zOff = 0.9f;
+                zOff = 0.9D;
             } else if (dir == 1) {
-                xOff = 0.1f;
+                xOff = 0.1D;
             } else if (dir == 2) {
-                zOff = 0.1f;
+                zOff = 0.1D;
             } else if (dir == 3) {
-                xOff = 0.9f;
+                xOff = 0.9D;
             }
 
             func_22052_e(dir);
-            setPos(x + xOff, y + 0.9375F, z + zOff);
+            setPos(x + xOff, y + 0.9375D, z + zOff);
         } else {
-            setPos(x + 0.5F, y + 0.9375F, z + 0.5F);
+            setPos(x + 0.5D, y + 0.9375D, z + 0.5D);
         }
         this.sleeping = true;
         this.sleepTimer = 0;
@@ -1401,6 +1403,8 @@ public abstract class Player
     }
 
     public void wakeUpPlayer(boolean flag, boolean flag1) {
+        // Cleared before setupScale so the standing hitbox is restored for the setPos below
+        this.sleeping = false;
         setupScale();
         TilePos bedPos = this.bedTilePos;
         Block<?> b;
@@ -1411,9 +1415,8 @@ public abstract class Player
             if (emptyPos == null) {
                 emptyPos = bedPos.up(new TilePos());
             }
-            setPos((float) emptyPos.x + 0.5F, (float) emptyPos.y + this.heightOffset + 0.1F, (float) emptyPos.z + 0.5F);
+            setPos(emptyPos.x + 0.5D, emptyPos.y + (double) this.heightOffset + 0.1D, emptyPos.z + 0.5D);
         }
-        this.sleeping = false;
         if (!this.world.isClientSide && flag1) {
             this.world.updateEnoughPlayersSleepingFlag(this);
         }
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemLabel.java b/game/core/src/main/java/net/minecraft/core/item/ItemLabel.java
index 4a3831aa0..b2a2a3b9d 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemLabel.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemLabel.java
@@ -4,6 +4,7 @@ import com.b100.utils.StringUtils;
 import net.minecraft.core.block.entity.TileEntityActivator;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.Mob;
+import net.minecraft.core.entity.animal.MobButterfly;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.world.World;
@@ -23,6 +24,11 @@ public class ItemLabel extends Item {
         setMaxStackSize(1);
     }
 
+    @Override
+    public boolean interactsWithEntity(@NotNull ItemStack selfStack, @NotNull Entity entity) {
+        return super.interactsWithEntity(selfStack, entity) || entity instanceof MobButterfly;
+    }
+
     @Override
     public @Nullable ItemStack onUse(final @NotNull ItemStack selfStack, final @NotNull World world, final @NotNull Player player) {
         if (selfStack.hasCustomName()) {
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemWandNBT.java b/game/core/src/main/java/net/minecraft/core/item/ItemWandNBT.java
index 3e0999e68..45116aa20 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemWandNBT.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemWandNBT.java
@@ -2,7 +2,10 @@ package net.minecraft.core.item;
 
 import com.mojang.nbt.tags.CompoundTag;
 import net.minecraft.core.block.entity.TileEntity;
+import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.Mob;
+import net.minecraft.core.entity.animal.MobButterfly;
+import net.minecraft.core.entity.animal.MobFireflyCluster;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.player.inventory.slot.Slot;
 import net.minecraft.core.util.helper.Side;
@@ -17,6 +20,11 @@ public class ItemWandNBT extends Item{
         super(translationKey, namespaceId, id);
     }
 
+    @Override
+    public boolean interactsWithEntity(@NotNull ItemStack selfStack, @NotNull Entity entity) {
+        return super.interactsWithEntity(selfStack, entity) || entity instanceof MobButterfly || entity instanceof MobFireflyCluster;
+    }
+
     @Override
     public ItemStack onInventoryInteract(@NotNull Player player, @NotNull Slot slot, @Nullable ItemStack stackInSlot, boolean isItemGrabbed) {
         if (slot.getItemStack() != null) {
diff --git a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolPickaxe.java b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolPickaxe.java
index 030363c7b..5b4410d79 100644
--- a/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolPickaxe.java
+++ b/game/core/src/main/java/net/minecraft/core/item/tool/ItemToolPickaxe.java
@@ -16,7 +16,6 @@ public class ItemToolPickaxe extends ItemTool {
 	
 	static {
 		miningLevels.put(Blocks.OBSIDIAN, 3);
-		miningLevels.put(Blocks.LOG_PETRIFIED, 3);
 		
 		miningLevels.put(Blocks.BLOCK_DIAMOND, 2);
 		miningLevels.put(Blocks.ORE_DIAMOND_STONE, 2);
diff --git a/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandAchievement.java b/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandAchievement.java
index cc2f87441..982e2fd4b 100644
--- a/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandAchievement.java
+++ b/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandAchievement.java
@@ -12,6 +12,7 @@ import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.lang.I18n;
 import net.minecraft.core.net.command.CommandManager;
 import net.minecraft.core.net.command.CommandSource;
+import net.minecraft.core.net.command.TextFormatting;
 import net.minecraft.core.net.command.arguments.ArgumentTypeAchievement;
 import net.minecraft.core.net.command.exceptions.CommandExceptions;
 import org.jetbrains.annotations.NotNull;
@@ -38,7 +39,7 @@ public class CommandAchievement implements CommandManager.CommandRegistry {
                         }
 
                         grantAchievements(player, collectWithAncestors(achievement));
-                        source.sendTranslatableMessage("command.commands.achievement.grant.success_single_entity", achievement.getStatName().trim(), player.getDisplayName());
+                        source.sendTranslatableMessage("command.commands.achievement.grant.success_single_entity", achievement.getStatName().trim(), TextFormatting.scoped(player.getDisplayName()));
 
                         return Command.SINGLE_SUCCESS;
                     }))
@@ -51,7 +52,7 @@ public class CommandAchievement implements CommandManager.CommandRegistry {
                             grantAchievements(player, collectWithAncestors(achievement));
                         }
 
-                        source.sendTranslatableMessage("command.commands.achievement.grant.all.success_single_entity", player.getDisplayName());
+                        source.sendTranslatableMessage("command.commands.achievement.grant.all.success_single_entity", TextFormatting.scoped(player.getDisplayName()));
 
                         return Command.SINGLE_SUCCESS;
                     })))
@@ -63,7 +64,7 @@ public class CommandAchievement implements CommandManager.CommandRegistry {
                         Achievement achievement = c.getArgument("achievement", Achievement.class);
 
                         revokeAchievements(player, collectWithDescendants(achievement));
-                        source.sendTranslatableMessage("command.commands.achievement.revoke.success_single_entity", achievement.getStatName().trim(), player.getDisplayName());
+                        source.sendTranslatableMessage("command.commands.achievement.revoke.success_single_entity", achievement.getStatName().trim(), TextFormatting.scoped(player.getDisplayName()));
 
                         return Command.SINGLE_SUCCESS;
                     }))
@@ -73,7 +74,7 @@ public class CommandAchievement implements CommandManager.CommandRegistry {
                         Player player = requireSender(source);
 
                         revokeAchievements(player, collectAllAchievementsByDepthDesc());
-                        source.sendTranslatableMessage("command.commands.achievement.revoke.all.success_single_entity", player.getDisplayName());
+                        source.sendTranslatableMessage("command.commands.achievement.revoke.all.success_single_entity", TextFormatting.scoped(player.getDisplayName()));
 
                         return Command.SINGLE_SUCCESS;
                     })))));
diff --git a/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandKill.java b/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandKill.java
index d3dce5ee7..dcd42795a 100644
--- a/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandKill.java
+++ b/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandKill.java
@@ -12,6 +12,7 @@ import net.minecraft.core.lang.I18n;
 import net.minecraft.core.net.command.CommandManager;
 import net.minecraft.core.net.command.util.CommandHelper;
 import net.minecraft.core.net.command.CommandSource;
+import net.minecraft.core.net.command.TextFormatting;
 import net.minecraft.core.net.command.arguments.ArgumentTypeEntity;
 import net.minecraft.core.net.command.exceptions.CommandExceptions;
 import net.minecraft.core.net.command.helpers.EntitySelector;
@@ -32,7 +33,7 @@ public class CommandKill implements CommandManager.CommandRegistry {
 
                     sender.killPlayer();
 
-                    source.sendTranslatableMessage("command.commands.kill.single_entity", sender.getDisplayName());
+                    source.sendTranslatableMessage("command.commands.kill.single_entity", TextFormatting.scoped(sender.getDisplayName()));
 
                     return Command.SINGLE_SUCCESS;
                 })
diff --git a/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandTeleport.java b/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandTeleport.java
index 171d645df..4709551af 100644
--- a/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandTeleport.java
+++ b/game/core/src/main/java/net/minecraft/core/net/command/commands/CommandTeleport.java
@@ -11,6 +11,7 @@ import net.minecraft.core.net.command.CommandManager;
 import net.minecraft.core.net.command.arguments.ArgumentTypeDimension;
 import net.minecraft.core.net.command.util.CommandHelper;
 import net.minecraft.core.net.command.CommandSource;
+import net.minecraft.core.net.command.TextFormatting;
 import net.minecraft.core.net.command.arguments.ArgumentTypeEntity;
 import net.minecraft.core.net.command.arguments.ArgumentTypeVector3d;
 import net.minecraft.core.net.command.exceptions.CommandExceptions;
@@ -34,7 +35,7 @@ public class CommandTeleport implements CommandManager.CommandRegistry {
                             if (source.getSender() != null) {
                                 source.movePlayerToDimension(source.getSender(), dimension.id);
                                 source.teleportPlayerToPos(source.getSender(), targetCoordinates.getX(source), targetCoordinates.getY(source, true), targetCoordinates.getZ(source));
-                                source.sendTranslatableMessage("command.commands.teleport.dimension_location.success_single_entity", source.getSender().getDisplayName(), dimension.languageKey, targetCoordinates.getX(source), targetCoordinates.getY(source, true), targetCoordinates.getZ(source));
+                                source.sendTranslatableMessage("command.commands.teleport.dimension_location.success_single_entity", TextFormatting.scoped(source.getSender().getDisplayName()), dimension.languageKey, targetCoordinates.getX(source), targetCoordinates.getY(source, true), targetCoordinates.getZ(source));
                             } else {
                                 throw CommandExceptions.notInWorld().create();
                             }
@@ -47,7 +48,7 @@ public class CommandTeleport implements CommandManager.CommandRegistry {
 
                         if (source.getSender() != null) {
                             source.movePlayerToDimension(source.getSender(), dimension.id);
-                            source.sendTranslatableMessage("command.commands.teleport.dimension.success_single_entity", source.getSender().getDisplayName(), dimension.languageKey);
+                            source.sendTranslatableMessage("command.commands.teleport.dimension.success_single_entity", TextFormatting.scoped(source.getSender().getDisplayName()), dimension.languageKey);
                         } else {
                             throw CommandExceptions.notInWorld().create();
                         }
@@ -62,7 +63,7 @@ public class CommandTeleport implements CommandManager.CommandRegistry {
 
                             if (source.getSender() != null) {
                                 source.teleportPlayerToPos(source.getSender(), targetCoordinates.getX(source), targetCoordinates.getY(source, true), targetCoordinates.getZ(source));
-                                source.sendTranslatableMessage("command.commands.teleport.location.success_single_entity", source.getSender().getDisplayName(), targetCoordinates.getX(source), targetCoordinates.getY(source, true), targetCoordinates.getZ(source));
+                                source.sendTranslatableMessage("command.commands.teleport.location.success_single_entity", TextFormatting.scoped(source.getSender().getDisplayName()), targetCoordinates.getX(source), targetCoordinates.getY(source, true), targetCoordinates.getZ(source));
                             } else {
                                 throw CommandExceptions.notInWorld().create();
                             }
diff --git a/game/core/src/main/java/net/minecraft/core/net/command/util/CommandHelper.java b/game/core/src/main/java/net/minecraft/core/net/command/util/CommandHelper.java
index 00c9503b1..2592309e7 100644
--- a/game/core/src/main/java/net/minecraft/core/net/command/util/CommandHelper.java
+++ b/game/core/src/main/java/net/minecraft/core/net/command/util/CommandHelper.java
@@ -10,6 +10,7 @@ import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.net.command.CommandSource;
+import net.minecraft.core.net.command.TextFormatting;
 import net.minecraft.core.net.command.helpers.IntegerCoordinates;
 import net.minecraft.core.util.collection.NamespaceID;
 import net.minecraft.core.util.helper.MathHelper;
@@ -102,7 +103,7 @@ public class CommandHelper {
     }
 
     public static String getEntityName(Entity entity) {
-        return Entity.getNameFromEntity(entity, true);
+        return TextFormatting.scoped(Entity.getNameFromEntity(entity, true));
     }
 
     public static int getVolume(CommandSource source, IntegerCoordinates first, IntegerCoordinates second) throws CommandSyntaxException {
diff --git a/game/core/src/main/java/net/minecraft/core/player/inventory/CreativeMenuContents.java b/game/core/src/main/java/net/minecraft/core/player/inventory/CreativeMenuContents.java
index fde062915..ee8b0f514 100644
--- a/game/core/src/main/java/net/minecraft/core/player/inventory/CreativeMenuContents.java
+++ b/game/core/src/main/java/net/minecraft/core/player/inventory/CreativeMenuContents.java
@@ -207,8 +207,7 @@ public final class CreativeMenuContents {
 
 		list.add(new ItemStack(Items.HANDCANNON_UNLOADED));
 		list.add(new ItemStack(Items.AMMO_CHARGE_EXPLOSIVE));
-		list.add(new ItemStack(Items.AMMO_FIREBALL));
-		addGap(list, 2);
+		addGap(list, 3);
 		list.add(new ItemStack(Items.TOOL_CALENDAR));
 		newLine(list);
 	}
diff --git a/game/core/src/main/java/net/minecraft/core/world/LevelListener.java b/game/core/src/main/java/net/minecraft/core/world/LevelListener.java
index 96a6e15e7..deb4521fd 100644
--- a/game/core/src/main/java/net/minecraft/core/world/LevelListener.java
+++ b/game/core/src/main/java/net/minecraft/core/world/LevelListener.java
@@ -66,6 +66,10 @@ public interface LevelListener
      * data: unused
      */
     int EVENT_ACID_SPREAD = 1010;
+    /**
+     * data: the {@link net.minecraft.core.util.helper.Direction} id of the mesh's open water face
+     */
+    int EVENT_MESH_SIPHON_PARTICLES = 1011;
 
 	/**
 	 * Redraw block in singleplayer and send block change packet in multiplayer. <br>
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreeThorn.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreeThorn.java
index 191d15527..17a83e367 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreeThorn.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/tree/WorldFeatureTreeThorn.java
@@ -8,14 +8,17 @@ import java.util.ArrayList;
 import java.util.List;
 
 public class WorldFeatureTreeThorn extends WorldFeatureSpoonerTreeProcedural {
+    private final int baseHeight;
+
     @MethodParametersAnnotation(names = {"height", "trunkId", "trunkData", "leavesId", "leavesData"})
     public WorldFeatureTreeThorn(int height, int trunkId, int trunkData, int leavesId, int leavesData) {
         super(height, trunkId, trunkData, leavesId, leavesData);
+        this.baseHeight = height;
     }
 
     @Override
     protected void prepare(World world) {
-        this.height = height + random.nextInt(3);
+        this.height = baseHeight + random.nextInt(3);
         this.foliageShape = new double[] { 4.5, 3.5, 2 };
         this.branchSlope = 1;
         this.trunkRadius = 1;
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/gui.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/gui.lang
index d97a06674..3bc1b04c6 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/gui.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/gui.lang
@@ -320,6 +320,7 @@ gui.options.hudeditor.snap_to_fill=Snap To Fill
 gui.options.hudeditor.scale=GUI Scale
 gui.options.hudeditor.chat_width=Chat Width
 gui.options.hudeditor.chat_height=Chat Height
+gui.options.hudeditor.chat_full_width_backdrop=Full Width Chat Backdrop
 
 gui.sound_test.label.title=Sound Test
 gui.sound_test.label.repo=Sound Repository: %s
diff --git a/game/server/src/main/java/net/minecraft/server/entity/player/PlayerServer.java b/game/server/src/main/java/net/minecraft/server/entity/player/PlayerServer.java
index 3d3aab87e..385f139a7 100644
--- a/game/server/src/main/java/net/minecraft/server/entity/player/PlayerServer.java
+++ b/game/server/src/main/java/net/minecraft/server/entity/player/PlayerServer.java
@@ -470,7 +470,12 @@ public class PlayerServer extends Player implements ContainerListener {
         if (isPlayerSleeping())
         {
             EntityTrackerImpl entitytracker = mcServer.getEntityTracker(dimension);
-            entitytracker.sendPacketToTrackedPlayersAndTrackedEntity(this, new PacketAnimate(this, 3));
+            PacketAnimate wakePacket = new PacketAnimate(this, 3);
+            entitytracker.sendPacketToTrackedPlayers(this, wakePacket);
+            if (playerNetServerHandler != null)
+            {
+                playerNetServerHandler.sendPacket(wakePacket);
+            }
             super.wakeUpPlayer(flag, flag1);
         }
         if (playerNetServerHandler != null)
diff --git a/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java b/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java
index f9368fbc7..7c8f7725f 100644
--- a/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java
+++ b/game/server/src/main/java/net/minecraft/server/net/handler/PacketHandlerServer.java
@@ -558,8 +558,9 @@ public class PacketHandlerServer extends PacketHandler
             }
             case PacketUpdatePlayerState.STATE_UN_SNEAK -> this.playerEntity.setSneaking(false);
             case PacketUpdatePlayerState.STATE_LEAVE_BED -> {
-                this.playerEntity.wakeUpPlayer(false, true);
-                this.hasMoved = false;
+                if (this.playerEntity.isPlayerSleeping()) {
+                    this.playerEntity.wakeUpPlayer(false, true);
+                }
             }
             case PacketUpdatePlayerState.STATE_NO_CLIP -> {
                 this.playerEntity.setNoclip(true);
diff --git a/game/server/src/main/java/net/minecraft/server/world/WorldServer.java b/game/server/src/main/java/net/minecraft/server/world/WorldServer.java
index 216a26b2b..7e2f79cc2 100644
--- a/game/server/src/main/java/net/minecraft/server/world/WorldServer.java
+++ b/game/server/src/main/java/net/minecraft/server/world/WorldServer.java
@@ -135,8 +135,6 @@ public class WorldServer extends World {
 
         send_message: {
             if (MinecraftServer.getInstance() == null) break send_message;
-            if (players.size() <= 1) break send_message;
-            if (this.getPlayersRequiredToSkipNight() <= 1) break send_message;
 
             Player sleeper = null;
             for (final var p : players) {
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/ConsolidationGrouper.java b/util/datagen/src/main/java/net/minecraft/datagen/ConsolidationGrouper.java
new file mode 100644
index 000000000..2f96f67e6
--- /dev/null
+++ b/util/datagen/src/main/java/net/minecraft/datagen/ConsolidationGrouper.java
@@ -0,0 +1,108 @@
+package net.minecraft.datagen;
+
+import net.minecraft.core.data.registry.recipe.RecipeGroup;
+import net.minecraft.core.data.registry.recipe.RecipeSymbol;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCrafting;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCraftingShaped;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCraftingShapeless;
+import net.minecraft.core.item.ItemStack;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+final class ConsolidationGrouper {
+    private ConsolidationGrouper() {}
+
+    private static final String[][] FAMILY_PREFIXES = {
+        {"pressure_plate_", "pressure_plates"},
+        {"fence_gate_", "fence_gates"},
+        {"fence_", "fences"},
+        {"slab_", "slabs"},
+        {"stairs_", "stairs"},
+        {"wall_", "walls"},
+        {"button_", "buttons"},
+        {"trapdoor_", "trapdoors"},
+        {"chest_", "chests"},
+        {"door_", "doors"},
+        {"sign", "signs"},
+        {"brick_", "bricks"},
+        {"planks_", "planks"},
+    };
+
+    private static final Set<String> MATERIALS = Set.of(
+        "diamond", "gold", "iron", "steel", "lapis", "olivine", "quartz", "rubyglass", "clay", "glass");
+
+    private static final Set<String> RAW_STONE = Set.of(
+        "stone", "basalt", "granite", "limestone", "permafrost", "netherrack",
+        "gloomstone", "sandstone", "marble", "brimstone", "slate");
+
+    static void apply(RecipeGroup<?> workbench) {
+        for (Object o : workbench) {
+            if (!(o instanceof RecipeEntryCrafting<?, ?> recipe)) continue;
+            if (recipe.consolidationGroup != null) continue; // hand-declared groups win
+            ItemStack output = outputOf(recipe);
+            if (output == null) continue;
+            String family = familyOf(cleanName(output));
+            if (family == null) continue;
+            String bucket = bucketOfInputs(recipe);
+            if (bucket == null) continue;
+            recipe.consolidationGroup = bucket + "_" + family;
+        }
+    }
+
+    private static String familyOf(String outputName) {
+        for (String[] entry : FAMILY_PREFIXES) {
+            if (outputName.startsWith(entry[0])) return entry[1];
+        }
+
+        if (outputName.endsWith("_polished")) return "polished";
+        return null;
+    }
+
+    private static String bucketOfInputs(RecipeEntryCrafting<?, ?> recipe) {
+        for (RecipeSymbol symbol : inputsOf(recipe)) {
+            if (symbol == null || symbol.getItemGroup() != null || symbol.getStack() == null) continue;
+            String bucket = bucketOf(cleanName(symbol.getStack()));
+            if (bucket != null) return bucket;
+        }
+        return null;
+    }
+
+    static String bucketOf(String inputName) {
+        if (inputName.startsWith("planks_oak") || inputName.startsWith("log_")) return "wood"; // planks + logs
+        if (inputName.startsWith("cobble_")) return "cobble";
+        if (inputName.endsWith("_polished")) return "polished";
+        if (inputName.startsWith("brick_") || inputName.endsWith("_brick")) {
+            return containsMaterial(inputName) ? "material_brick" : "stone_brick";
+        }
+        if (RAW_STONE.contains(inputName)) return "stone";
+        if (containsMaterial(inputName)) return "material";
+        return null;
+    }
+
+    private static boolean containsMaterial(String name) {
+        for (String material : MATERIALS) {
+            if (name.contains(material)) return true;
+        }
+        return false;
+    }
+
+    private static String cleanName(ItemStack stack) {
+        String value = stack.getItem().namespaceID.value();
+        int slash = value.indexOf('/');
+        return slash >= 0 ? value.substring(slash + 1) : value;
+    }
+
+    private static ItemStack outputOf(RecipeEntryCrafting<?, ?> recipe) {
+        if (recipe instanceof RecipeEntryCraftingShaped shaped) return shaped.getOutput();
+        if (recipe instanceof RecipeEntryCraftingShapeless shapeless) return shapeless.getOutput();
+        return null;
+    }
+
+    private static List<RecipeSymbol> inputsOf(RecipeEntryCrafting<?, ?> recipe) {
+        if (recipe instanceof RecipeEntryCraftingShaped shaped) return Arrays.asList(shaped.getInput());
+        if (recipe instanceof RecipeEntryCraftingShapeless shapeless) return shapeless.getInput();
+        return List.of();
+    }
+}
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/RecipesGenerator.java b/util/datagen/src/main/java/net/minecraft/datagen/RecipesGenerator.java
index 8d1c21f92..c69e02cfe 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/RecipesGenerator.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/RecipesGenerator.java
@@ -57,6 +57,7 @@ public class RecipesGenerator {
         List<String> remainingEntries;
 
         WorkbenchGenerator.generate();
+        ConsolidationGrouper.apply(Registries.RECIPES.WORKBENCH);
         if (DEBUG) {
             remainingEntries = getMissingIds(Registries.RECIPES.WORKBENCH, new File("src/main/resources/recipes/workbench.json"));
             if (remainingEntries.isEmpty()) {
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 ec3d8fba8..828a9b86e 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
@@ -18,18 +18,20 @@ import static net.minecraft.datagen.RecipesGenerator.CORE_NAMESPACE;
 
 class WorkbenchGenerator {
     public static void generate() {
-        generatePickaxes();
-        generateAxes();
-        generateShovels();
-        generateHoes();
-        generateSwords();
+
+        RecipeBuilder.consolidationGroup("pickaxes", WorkbenchGenerator::generatePickaxes);
+        RecipeBuilder.consolidationGroup("axes", WorkbenchGenerator::generateAxes);
+        RecipeBuilder.consolidationGroup("shovels", WorkbenchGenerator::generateShovels);
+        RecipeBuilder.consolidationGroup("hoes", WorkbenchGenerator::generateHoes);
+        RecipeBuilder.consolidationGroup("swords", WorkbenchGenerator::generateSwords);
+
         generateMiscTools();
 
-        generateHelmets();
-        generateChestPlates();
-        generateLeggings();
-        generateBoots();
-        generateWolfArmors();
+        RecipeBuilder.consolidationGroup("helmets", WorkbenchGenerator::generateHelmets);
+        RecipeBuilder.consolidationGroup("chestplates", WorkbenchGenerator::generateChestPlates);
+        RecipeBuilder.consolidationGroup("leggings", WorkbenchGenerator::generateLeggings);
+        RecipeBuilder.consolidationGroup("boots", WorkbenchGenerator::generateBoots);
+        RecipeBuilder.consolidationGroup("wolf_armors", WorkbenchGenerator::generateWolfArmors);
 
         generateBlockCompression();
         generateBlockDecompression();
@@ -61,8 +63,8 @@ class WorkbenchGenerator {
         generateUndyeingRecipes();
         generateDyeingRecipes();
 
-        generateStatueRecipes();
-        generateAcidConversions();
+        RecipeBuilder.consolidationGroup("statues", WorkbenchGenerator::generateStatueRecipes);
+        RecipeBuilder.consolidationGroup("acid_conversions", WorkbenchGenerator::generateAcidConversions);
 
         generateMiscNewRecipes();
 
@@ -92,18 +94,12 @@ class WorkbenchGenerator {
             .addInput('X', Items.WHEAT)
             .create("basket", Items.BASKET.getDefaultStack());
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#",
-                "#",
-                "#")
-            .addInput('#', Items.PAPER)
-            .create("book_vertical", Items.BOOK.getDefaultStack());
-
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape("###")
-            .addInput('#', Items.PAPER)
-            .create("book_horizontal", Items.BOOK.getDefaultStack());
+        RecipeBuilder.Shapeless(CORE_NAMESPACE)
+            .addInput(Items.PAPER)
+            .addInput(Items.PAPER)
+            .addInput(Items.PAPER)
+            .addInput(Items.LEATHER)
+            .create("book", Items.BOOK.getDefaultStack());
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -198,21 +194,23 @@ class WorkbenchGenerator {
             .addInput('W', "minecraft:wools")
             .create("painting", new ItemStack(Items.PAINTING, 1, Items.PAINTING.getMaxDamage()));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "XXX",
-                "X X",
-                "XXX")
-            .addInput('X', Items.INGOT_IRON)
-            .create("mesh", new ItemStack(Blocks.MESH, 8));
+        RecipeBuilder.consolidationGroup("mesh", () -> {
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "XXX",
+                    "X X",
+                    "XXX")
+                .addInput('X', Items.INGOT_IRON)
+                .create("mesh", new ItemStack(Blocks.MESH, 8));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "XXX",
-                "X X",
-                "XXX")
-            .addInput('X', Items.INGOT_GOLD)
-            .create("mesh_gold", new ItemStack(Blocks.MESH_GOLD, 8));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "XXX",
+                    "X X",
+                    "XXX")
+                .addInput('X', Items.INGOT_GOLD)
+                .create("mesh_gold", new ItemStack(Blocks.MESH_GOLD, 8));
+        });
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -258,40 +256,57 @@ class WorkbenchGenerator {
         shape22.addInput('#', Items.AMMO_SNOWBALL).create("snow_block", Blocks.BLOCK_SNOW.getDefaultStack());
         shape22.addInput('#', Items.STRING).create("string_to_wool", Blocks.WOOL.getDefaultStack());
         shape22.addInput('#', Items.AMMO_PEBBLE).create("pebbles_to_cobblestone", new ItemStack(Blocks.COBBLE_STONE, 2));
-        shape22.addInput('#', Blocks.SAND).create("sandstone", new ItemStack(Blocks.SANDSTONE, 2));
+
+        RecipeBuilder.consolidationGroup("sand_to_stone", () ->
+            shape22.addInput('#', Blocks.SAND).create("sandstone", new ItemStack(Blocks.SANDSTONE, 2))
+        );
+        RecipeBuilder.consolidationGroup("sand_to_stone", () ->
+            shape22.addInput('#', Blocks.BRIMSAND).create("brimstone", new ItemStack(Blocks.BRIMSTONE,2))
+        );
+
         shape22.addInput('#', "minecraft:planks").create("workbench", Blocks.WORKBENCH.getDefaultStack());
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#X",
-                "X#")
-            .addInput('#', Items.AMMO_PEBBLE)
-            .addInput('X', Items.COAL)
-            .create("pebbles_to_basalt", new ItemStack(Blocks.COBBLE_BASALT, 2));
+        RecipeBuilder.consolidationGroup("cobble_variants", () -> {
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Items.AMMO_PEBBLE)
+                .addInput('X', Items.COAL)
+                .create("pebbles_to_basalt", new ItemStack(Blocks.COBBLE_BASALT, 2));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#X",
-                "X#")
-            .addInput('#', Items.AMMO_PEBBLE)
-            .addInput('X', Items.QUARTZ)
-            .create("pebbles_to_granite", new ItemStack(Blocks.COBBLE_GRANITE, 2));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Items.AMMO_PEBBLE)
+                .addInput('X', Items.QUARTZ)
+                .create("pebbles_to_granite", new ItemStack(Blocks.COBBLE_GRANITE, 2));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#X",
-                "X#")
-            .addInput('#', Items.AMMO_PEBBLE)
-            .addInput('X', Blocks.SAND)
-            .create("pebbles_to_limestone", new ItemStack(Blocks.COBBLE_LIMESTONE, 2));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Items.AMMO_PEBBLE)
+                .addInput('X', Blocks.SAND)
+                .create("pebbles_to_limestone", new ItemStack(Blocks.COBBLE_LIMESTONE, 2));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#X",
-                "X#")
-            .addInput('#', Items.AMMO_PEBBLE)
-            .addInput('X', Blocks.ICE)
-            .create("pebbles_to_permafrost", new ItemStack(Blocks.COBBLE_PERMAFROST, 2));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Items.AMMO_PEBBLE)
+                .addInput('X', Blocks.ICE)
+                .create("pebbles_to_permafrost", new ItemStack(Blocks.COBBLE_PERMAFROST, 2));
+
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Items.AMMO_PEBBLE)
+                .addInput('X', Blocks.OBSIDIAN)
+                .create("pebbles_to_gloomstone", new ItemStack(Blocks.COBBLE_GLOOMSTONE, 2));
+        });
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -316,21 +331,23 @@ class WorkbenchGenerator {
             .addInput('#', Items.STRING)
             .create("rope", new ItemStack(Items.ROPE, 2));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#X",
-                "X#")
-            .addInput('#', Blocks.GLASS)
-            .addInput('X', Items.INGOT_STEEL_CRUDE)
-            .create("steel_glass", new ItemStack(Blocks.GLASS_STEEL, 4));
+        RecipeBuilder.consolidationGroup("treated_glass", () -> {
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Blocks.GLASS)
+                .addInput('X', Items.INGOT_STEEL_CRUDE)
+                .create("steel_glass", new ItemStack(Blocks.GLASS_STEEL, 4));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "#X",
-                "X#")
-            .addInput('#', Blocks.GLASS)
-            .addInput('X', Items.OLIVINE)
-            .create("tinted_glass", new ItemStack(Blocks.GLASS_TINTED, 4));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "#X",
+                    "X#")
+                .addInput('#', Blocks.GLASS)
+                .addInput('X', Items.OLIVINE)
+                .create("tinted_glass", new ItemStack(Blocks.GLASS_TINTED, 4));
+        });
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -340,37 +357,50 @@ class WorkbenchGenerator {
             .addInput('X', "minecraft:planks")
             .create("paper_wall", new ItemStack(Blocks.PAPER_WALL, 4));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "X",
-                "#")
-            .addInput('#', Items.STICK)
-            .addInput('X', Items.COAL)
-            .create("torch_coal", new ItemStack(Blocks.TORCH_COAL, 4));
+        RecipeBuilder.consolidationGroup("torches", () -> {
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "X",
+                    "#")
+                .addInput('#', Items.STICK)
+                .addInput('X', Items.COAL)
+                .create("torch_coal", new ItemStack(Blocks.TORCH_COAL, 4));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "X",
-                "#")
-            .addInput('#', Items.STICK)
-            .addInput('X', Items.COAL, 1)
-            .create("torch_charcoal", new ItemStack(Blocks.TORCH_COAL, 4));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "X",
+                    "#")
+                .addInput('#', Items.STICK)
+                .addInput('X', Items.COAL, 1)
+                .create("torch_charcoal", new ItemStack(Blocks.TORCH_COAL, 4));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "X",
-                "#")
-            .addInput('#', Items.STICK)
-            .addInput('X', Items.NETHERCOAL)
-            .create("torch_nethercoal", new ItemStack(Blocks.TORCH_COAL, 8));
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                .setShape(
+                    "X",
+                    "#")
+                .addInput('#', Items.STICK)
+                .addInput('X', Items.NETHERCOAL)
+                .create("torch_nethercoal", new ItemStack(Blocks.TORCH_COAL, 8));
+        });
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "X",
-                "#")
-            .addInput('X', Blocks.PUMPKIN_CARVED_IDLE)
-            .addInput('#', Blocks.TORCH_COAL)
-            .create("carved_pumpkin_to_jack_o_lantern", Blocks.PUMPKIN_CARVED_ACTIVE.getDefaultStack());
+
+        RecipeBuilder.consolidationGroup("jack_o_lanterns", () -> {
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                    .setShape(
+                            "X",
+                            "#")
+                    .addInput('X', Blocks.PUMPKIN_CARVED_IDLE)
+                    .addInput('#', Blocks.TORCH_COAL)
+                    .create("carved_pumpkin_to_jack_o_lantern", Blocks.PUMPKIN_CARVED_ACTIVE.getDefaultStack());
+
+            RecipeBuilder.Shaped(CORE_NAMESPACE)
+                    .setShape(
+                            "X",
+                            "#")
+                    .addInput('X', Blocks.PUMPKIN_CARVED_IDLE)
+                    .addInput('#', Blocks.TORCH_REDSTONE_ACTIVE)
+                    .create("carved_pumpkin_to_redstone_jack_o_lantern", Blocks.PUMPKIN_REDSTONE.getDefaultStack());
+        });
 
         RecipeBuilder.Shapeless(CORE_NAMESPACE)
             .addInput(Blocks.SPONGE_DRY)
@@ -386,23 +416,23 @@ class WorkbenchGenerator {
             .addInput(Blocks.PUMPKIN)
             .create("pumpkin_to_pumpkin_seeds", new ItemStack(Items.SEEDS_PUMPKIN, 4));
 
-        RecipeBuilder.Shapeless(CORE_NAMESPACE)
-            .addInput(Blocks.SLATE)
-            .create("slate_to_slate_layers", new ItemStack(Blocks.LAYER_SLATE, 8));
+        RecipeBuilder.consolidationGroup("layers", () -> {
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Blocks.SLATE)
+                .create("slate_to_slate_layers", new ItemStack(Blocks.LAYER_SLATE, 8));
 
-        RecipeBuilder.Shapeless(CORE_NAMESPACE)
-            .addInput(Blocks.BLOCK_ASH)
-            .create("ash_to_ash_layers", new ItemStack(Blocks.LAYER_ASH, 8));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Blocks.BLOCK_ASH)
+                .create("ash_to_ash_layers", new ItemStack(Blocks.LAYER_ASH, 8));
 
-        RecipeBuilder.Shapeless(CORE_NAMESPACE)
-            .addInput(Blocks.LEAVES_OAK)
-            .create("oak_leaves_to_leaf_pile", new ItemStack(Blocks.LAYER_LEAVES_OAK, 8));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Blocks.LEAVES_OAK)
+                .create("oak_leaves_to_leaf_pile", new ItemStack(Blocks.LAYER_LEAVES_OAK, 8));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "##")
-            .addInput('#', Items.AMMO_SNOWBALL)
-            .create("snowballs_to_snow_layer", new ItemStack(Blocks.LAYER_SNOW, 2));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Blocks.BLOCK_SNOW)
+                .create("snow_block_to_snow_layers", new ItemStack(Blocks.LAYER_SNOW, 8));
+        });
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -452,23 +482,25 @@ class WorkbenchGenerator {
             .addInput('S', Items.SULFUR)
             .create("ember", Blocks.EMBER.getDefaultStack());
 
-        RecipeBuilder.Shapeless(CORE_NAMESPACE)
-            .addInput(Items.SULFUR)
-            .addInput(Items.COAL)
-            .addInput(Items.PAPER)
-            .create("gunpowder_with_coal", new ItemStack(Items.GUNPOWDER, 2));
-
-        RecipeBuilder.Shapeless(CORE_NAMESPACE)
-            .addInput(Items.SULFUR)
-            .addInput(new ItemStack(Items.COAL, 1, 1))
-            .addInput(Items.PAPER)
-            .create("gunpowder_with_charcoal", new ItemStack(Items.GUNPOWDER, 2));
-
-        RecipeBuilder.Shapeless(CORE_NAMESPACE)
-            .addInput(Items.SULFUR)
-            .addInput(Items.NETHERCOAL)
-            .addInput(Items.PAPER)
-            .create("gunpowder_with_nethercoal", new ItemStack(Items.GUNPOWDER, 4));
+        RecipeBuilder.consolidationGroup("gunpowder", () -> {
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Items.SULFUR)
+                .addInput(Items.COAL)
+                .addInput(Items.PAPER)
+                .create("gunpowder_with_coal", new ItemStack(Items.GUNPOWDER, 2));
+
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Items.SULFUR)
+                .addInput(new ItemStack(Items.COAL, 1, 1))
+                .addInput(Items.PAPER)
+                .create("gunpowder_with_charcoal", new ItemStack(Items.GUNPOWDER, 2));
+
+            RecipeBuilder.Shapeless(CORE_NAMESPACE)
+                .addInput(Items.SULFUR)
+                .addInput(Items.NETHERCOAL)
+                .addInput(Items.PAPER)
+                .create("gunpowder_with_nethercoal", new ItemStack(Items.GUNPOWDER, 4));
+        });
 
         RecipeBuilder.Shapeless(CORE_NAMESPACE)
             .addInput(Blocks.RUBYGLASS_COLUMN)
@@ -576,11 +608,15 @@ class WorkbenchGenerator {
                 " X",
                 "X ");
 
-        shears.addInput('X', Items.INGOT_IRON).create("shears", Items.TOOL_SHEARS.getDefaultStack());
-        shears.addInput('X', Items.INGOT_STEEL).create("steel_shears", Items.TOOL_SHEARS_STEEL.getDefaultStack());
+        RecipeBuilder.consolidationGroup("shears", () -> {
+            shears.addInput('X', Items.INGOT_IRON).create("shears", Items.TOOL_SHEARS.getDefaultStack());
+            shears.addInput('X', Items.INGOT_STEEL).create("steel_shears", Items.TOOL_SHEARS_STEEL.getDefaultStack());
+        });
 
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Items.INGOT_IRON).addInput(Items.FLINT).create("fire_striker", Items.TOOL_FIRESTRIKER_IRON.getDefaultStack());
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Items.INGOT_STEEL).addInput(Items.FLINT).create("steel_fire_striker", Items.TOOL_FIRESTRIKER_STEEL.getDefaultStack());
+        RecipeBuilder.consolidationGroup("fire_strikers", () -> {
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Items.INGOT_IRON).addInput(Items.FLINT).create("fire_striker", Items.TOOL_FIRESTRIKER_IRON.getDefaultStack());
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Items.INGOT_STEEL).addInput(Items.FLINT).create("steel_fire_striker", Items.TOOL_FIRESTRIKER_STEEL.getDefaultStack());
+        });
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -784,37 +820,43 @@ class WorkbenchGenerator {
                 "XXX"
             );
 
-        block.addInput('X', Items.INGOT_GOLD).create("block_of_gold", Blocks.BLOCK_GOLD.getDefaultStack());
-        block.addInput('X', Items.INGOT_IRON).create("block_of_iron", Blocks.BLOCK_IRON.getDefaultStack());
-        block.addInput('X', Items.INGOT_STEEL).create("block_of_steel", Blocks.BLOCK_STEEL.getDefaultStack());
-        block.addInput('X', Items.DYE, DyeColor.BLUE.itemMeta).create("block_of_lapis_lazuli", Blocks.BLOCK_LAPIS.getDefaultStack());
-        block.addInput('X', Items.DUST_REDSTONE).create("block_of_redstone", Blocks.BLOCK_REDSTONE.getDefaultStack());
-        block.addInput('X', Items.DIAMOND).create("block_of_diamond", Blocks.BLOCK_DIAMOND.getDefaultStack());
-        block.addInput('X', Items.QUARTZ).create("block_of_quartz", Blocks.BLOCK_QUARTZ.getDefaultStack());
-        block.addInput('X', Items.SUGARCANE).create("block_of_sugarcane", Blocks.BLOCK_SUGARCANE.getDefaultStack());
-        block.addInput('X', Items.RUBYGLASS).create("block_of_rubyglass", Blocks.BLOCK_RUBYGLASS.getDefaultStack());
-
-        blockFuel.addInput('X', Items.COAL, 0).create("block_of_coal", Blocks.BLOCK_COAL.getDefaultStack());
-        blockFuel.addInput('X', Items.COAL, 1).create("block_of_charcoal", Blocks.BLOCK_CHARCOAL.getDefaultStack());
-        blockFuel.addInput('X', Items.NETHERCOAL).create("block_of_nethercoal", Blocks.BLOCK_NETHER_COAL.getDefaultStack());
-        blockFuel.addInput('X', Items.OLIVINE).create("block_of_olivine", Blocks.BLOCK_OLIVINE.getDefaultStack());
+        RecipeBuilder.consolidationGroup("material_blocks", () -> {
+            block.addInput('X', Items.INGOT_GOLD).create("block_of_gold", Blocks.BLOCK_GOLD.getDefaultStack());
+            block.addInput('X', Items.INGOT_IRON).create("block_of_iron", Blocks.BLOCK_IRON.getDefaultStack());
+            block.addInput('X', Items.INGOT_STEEL).create("block_of_steel", Blocks.BLOCK_STEEL.getDefaultStack());
+            block.addInput('X', Items.DYE, DyeColor.BLUE.itemMeta).create("block_of_lapis_lazuli", Blocks.BLOCK_LAPIS.getDefaultStack());
+            block.addInput('X', Items.DUST_REDSTONE).create("block_of_redstone", Blocks.BLOCK_REDSTONE.getDefaultStack());
+            block.addInput('X', Items.DIAMOND).create("block_of_diamond", Blocks.BLOCK_DIAMOND.getDefaultStack());
+            block.addInput('X', Items.QUARTZ).create("block_of_quartz", Blocks.BLOCK_QUARTZ.getDefaultStack());
+            block.addInput('X', Items.SUGARCANE).create("block_of_sugarcane", Blocks.BLOCK_SUGARCANE.getDefaultStack());
+            block.addInput('X', Items.RUBYGLASS).create("block_of_rubyglass", Blocks.BLOCK_RUBYGLASS.getDefaultStack());
+        });
+
+        RecipeBuilder.consolidationGroup("burnable_material_blocks", () -> {
+            blockFuel.addInput('X', Items.COAL, 0).create("block_of_coal", Blocks.BLOCK_COAL.getDefaultStack());
+            blockFuel.addInput('X', Items.COAL, 1).create("block_of_charcoal", Blocks.BLOCK_CHARCOAL.getDefaultStack());
+            blockFuel.addInput('X', Items.NETHERCOAL).create("block_of_nethercoal", Blocks.BLOCK_NETHER_COAL.getDefaultStack());
+            blockFuel.addInput('X', Items.OLIVINE).create("block_of_olivine", Blocks.BLOCK_OLIVINE.getDefaultStack());
+        });
     }
 
     private static void generateBlockDecompression() {
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_GOLD).create("block_of_gold_to_gold_ingot", new ItemStack(Items.INGOT_GOLD, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_IRON).create("block_of_iron_to_iron_ingot", new ItemStack(Items.INGOT_IRON, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_STEEL).create("block_of_steel_to_steel_ingot", new ItemStack(Items.INGOT_STEEL, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_LAPIS).create("block_of_lapis_lazuli_to_lapis_lazuli", new ItemStack(Items.DYE, 9, DyeColor.BLUE.itemMeta));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_REDSTONE).create("block_of_redstone_to_redstone", new ItemStack(Items.DUST_REDSTONE, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_DIAMOND).create("block_of_diamond_to_diamond", new ItemStack(Items.DIAMOND, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_QUARTZ).create("block_of_quartz_to_quartz", new ItemStack(Items.QUARTZ, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_RUBYGLASS).create("block_of_rubyglass_to_rubyglass", new ItemStack(Items.RUBYGLASS, 9));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_SUGARCANE).create("block_of_sugarcane_to_sugarcane", new ItemStack(Items.SUGARCANE, 9));
-
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_COAL).create("block_of_coal_to_coal", new ItemStack(Items.COAL, 8, 0));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_CHARCOAL).create("block_of_charcoal_to_charcoal", new ItemStack(Items.COAL, 8, 1));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_NETHER_COAL).create("block_of_nethercoal_to_nethercoal", new ItemStack(Items.NETHERCOAL, 8));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_OLIVINE).create("block_of_olivine_to_olivine", new ItemStack(Items.OLIVINE, 8));
+        RecipeBuilder.consolidationGroup("material_block_breakdown", () -> {
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_GOLD).create("block_of_gold_to_gold_ingot", new ItemStack(Items.INGOT_GOLD, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_IRON).create("block_of_iron_to_iron_ingot", new ItemStack(Items.INGOT_IRON, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_STEEL).create("block_of_steel_to_steel_ingot", new ItemStack(Items.INGOT_STEEL, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_LAPIS).create("block_of_lapis_lazuli_to_lapis_lazuli", new ItemStack(Items.DYE, 9, DyeColor.BLUE.itemMeta));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_REDSTONE).create("block_of_redstone_to_redstone", new ItemStack(Items.DUST_REDSTONE, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_DIAMOND).create("block_of_diamond_to_diamond", new ItemStack(Items.DIAMOND, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_QUARTZ).create("block_of_quartz_to_quartz", new ItemStack(Items.QUARTZ, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_RUBYGLASS).create("block_of_rubyglass_to_rubyglass", new ItemStack(Items.RUBYGLASS, 9));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_SUGARCANE).create("block_of_sugarcane_to_sugarcane", new ItemStack(Items.SUGARCANE, 9));
+
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_COAL).create("block_of_coal_to_coal", new ItemStack(Items.COAL, 8, 0));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_CHARCOAL).create("block_of_charcoal_to_charcoal", new ItemStack(Items.COAL, 8, 1));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_NETHER_COAL).create("block_of_nethercoal_to_nethercoal", new ItemStack(Items.NETHERCOAL, 8));
+            RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.BLOCK_OLIVINE).create("block_of_olivine_to_olivine", new ItemStack(Items.OLIVINE, 8));
+        });
     }
 
     private static void generateBricks() {
@@ -825,26 +867,31 @@ class WorkbenchGenerator {
             );
 
         final int stackSize = 4;
-        bricks.addInput('X', Items.RUBYGLASS).create("rubyglass_bricks", new ItemStack(Blocks.BRICK_RUBYGLASS, stackSize));
-        bricks.addInput('X', Items.DIAMOND).create("diamond_bricks", new ItemStack(Blocks.BRICK_DIAMOND, stackSize));
-        bricks.addInput('X', Items.BRICK_CLAY).create("clay_bricks", new ItemStack(Blocks.BRICK_CLAY, stackSize));
-        bricks.addInput('X', Items.INGOT_GOLD).create("golden_bricks", new ItemStack(Blocks.BRICK_GOLD, stackSize));
-        bricks.addInput('X', Items.INGOT_IRON).create("iron_bricks", new ItemStack(Blocks.BRICK_IRON, stackSize));
-        bricks.addInput('X', Items.INGOT_STEEL_CRUDE).create("steel_bricks", new ItemStack(Blocks.BRICK_STEEL, stackSize));
-        bricks.addInput('X', Items.DYE, DyeColor.BLUE.itemMeta).create("lapis_lazuli_bricks", new ItemStack(Blocks.BRICK_LAPIS, stackSize));
-        bricks.addInput('X', Items.OLIVINE).create("olivine_bricks", new ItemStack(Blocks.BRICK_OLIVINE, stackSize));
-        bricks.addInput('X', Items.QUARTZ).create("quartz_bricks", new ItemStack(Blocks.BRICK_QUARTZ, stackSize));
-        bricks.addInput('X', Blocks.SANDSTONE).create("sandstone_bricks", new ItemStack(Blocks.BRICK_SANDSTONE, stackSize));
-        bricks.addInput('X', Blocks.STONE).create("stone_bricks", new ItemStack(Blocks.BRICK_STONE, stackSize));
-        bricks.addInput('X', Blocks.BASALT).create("basalt_bricks", new ItemStack(Blocks.BRICK_BASALT, stackSize));
-        bricks.addInput('X', Blocks.GRANITE).create("granite_bricks", new ItemStack(Blocks.BRICK_GRANITE, stackSize));
-        bricks.addInput('X', Blocks.LIMESTONE).create("limestone_bricks", new ItemStack(Blocks.BRICK_LIMESTONE, stackSize));
-        bricks.addInput('X', Blocks.MARBLE).create("marble_bricks", new ItemStack(Blocks.BRICK_MARBLE, stackSize));
-        bricks.addInput('X', Blocks.SLATE).create("slate_bricks", new ItemStack(Blocks.BRICK_SLATE, stackSize));
-        bricks.addInput('X', Blocks.PERMAFROST).create("permafrost_bricks", new ItemStack(Blocks.BRICK_PERMAFROST, stackSize));
-        bricks.addInput('X', Blocks.NETHERRACK).create("netherrack_bricks", new ItemStack(Blocks.BRICK_NETHERRACK, stackSize));
-        bricks.addInput('X', Blocks.GLOOMSTONE).create("gloomstone_bricks", new ItemStack(Blocks.BRICK_GLOOMSTONE, stackSize));
-        bricks.addInput('X', Blocks.STONE_POLISHED).create("polished_stone_bricks", new ItemStack(Blocks.BRICK_STONE_POLISHED, stackSize));
+        RecipeBuilder.consolidationGroup("material_type_bricks", () -> {
+            bricks.addInput('X', Items.RUBYGLASS).create("rubyglass_bricks", new ItemStack(Blocks.BRICK_RUBYGLASS, stackSize));
+            bricks.addInput('X', Items.DIAMOND).create("diamond_bricks", new ItemStack(Blocks.BRICK_DIAMOND, stackSize));
+            bricks.addInput('X', Items.BRICK_CLAY).create("clay_bricks", new ItemStack(Blocks.BRICK_CLAY, stackSize));
+            bricks.addInput('X', Items.INGOT_GOLD).create("golden_bricks", new ItemStack(Blocks.BRICK_GOLD, stackSize));
+            bricks.addInput('X', Items.INGOT_IRON).create("iron_bricks", new ItemStack(Blocks.BRICK_IRON, stackSize));
+            bricks.addInput('X', Items.INGOT_STEEL_CRUDE).create("steel_bricks", new ItemStack(Blocks.BRICK_STEEL, stackSize));
+            bricks.addInput('X', Items.DYE, DyeColor.BLUE.itemMeta).create("lapis_lazuli_bricks", new ItemStack(Blocks.BRICK_LAPIS, stackSize));
+            bricks.addInput('X', Items.OLIVINE).create("olivine_bricks", new ItemStack(Blocks.BRICK_OLIVINE, stackSize));
+            bricks.addInput('X', Items.QUARTZ).create("quartz_bricks", new ItemStack(Blocks.BRICK_QUARTZ, stackSize));
+        });
+        RecipeBuilder.consolidationGroup("stone_type_bricks", () -> {
+            bricks.addInput('X', Blocks.SANDSTONE).create("sandstone_bricks", new ItemStack(Blocks.BRICK_SANDSTONE, stackSize));
+            bricks.addInput('X', Blocks.STONE).create("stone_bricks", new ItemStack(Blocks.BRICK_STONE, stackSize));
+            bricks.addInput('X', Blocks.BASALT).create("basalt_bricks", new ItemStack(Blocks.BRICK_BASALT, stackSize));
+            bricks.addInput('X', Blocks.GRANITE).create("granite_bricks", new ItemStack(Blocks.BRICK_GRANITE, stackSize));
+            bricks.addInput('X', Blocks.LIMESTONE).create("limestone_bricks", new ItemStack(Blocks.BRICK_LIMESTONE, stackSize));
+            bricks.addInput('X', Blocks.MARBLE).create("marble_bricks", new ItemStack(Blocks.BRICK_MARBLE, stackSize));
+            bricks.addInput('X', Blocks.SLATE).create("slate_bricks", new ItemStack(Blocks.BRICK_SLATE, stackSize));
+            bricks.addInput('X', Blocks.PERMAFROST).create("permafrost_bricks", new ItemStack(Blocks.BRICK_PERMAFROST, stackSize));
+            bricks.addInput('X', Blocks.NETHERRACK).create("netherrack_bricks", new ItemStack(Blocks.BRICK_NETHERRACK, stackSize));
+            bricks.addInput('X', Blocks.GLOOMSTONE).create("gloomstone_bricks", new ItemStack(Blocks.BRICK_GLOOMSTONE, stackSize));
+            bricks.addInput('X', Blocks.BRIMSTONE).create("brimstone_bricks", new ItemStack(Blocks.BRICK_BRIMSTONE, stackSize));
+            bricks.addInput('X', Blocks.STONE_POLISHED).create("polished_stone_bricks", new ItemStack(Blocks.BRICK_STONE_POLISHED, stackSize));
+        });
     }
 
     private static void generateSlabs() {
@@ -1065,7 +1112,6 @@ class WorkbenchGenerator {
         RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.LOG_PALM).create(String.format(format, "palm", "yellow"), new ItemStack(Blocks.PLANKS_OAK_PAINTED, stackSize, DyeColor.YELLOW.blockMeta));
         RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.LOG_THORN).create(String.format(format, "thorn", "red"), new ItemStack(Blocks.PLANKS_OAK_PAINTED, stackSize, DyeColor.RED.blockMeta));
         RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.LOG_PINE).create(String.format(format, "pine", "brown"), new ItemStack(Blocks.PLANKS_OAK_PAINTED, stackSize, DyeColor.BROWN.blockMeta));
-        RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Blocks.LOG_PETRIFIED).create(String.format(format, "petrified", "gray"), new ItemStack(Blocks.PLANKS_OAK_PAINTED, 2, DyeColor.SILVER.blockMeta));
     }
 
     private static void generatePolishedStones() {
@@ -1094,9 +1140,11 @@ class WorkbenchGenerator {
                 " X "
             ).addInput('#', Items.DUST_REDSTONE);
 
-        info.addInput('X', "minecraft:planks").create("rotary_calendar", Items.TOOL_CALENDAR.getDefaultStack());
-        info.addInput('X', Items.INGOT_IRON).create("compass", Items.TOOL_COMPASS.getDefaultStack());
-        info.addInput('X', Items.INGOT_GOLD).create("clock", Items.TOOL_CLOCK.getDefaultStack());
+        RecipeBuilder.consolidationGroup("info_tools", () -> {
+            info.addInput('X', "minecraft:planks").create("rotary_calendar", Items.TOOL_CALENDAR.getDefaultStack());
+            info.addInput('X', Items.INGOT_IRON).create("compass", Items.TOOL_COMPASS.getDefaultStack());
+            info.addInput('X', Items.INGOT_GOLD).create("clock", Items.TOOL_CLOCK.getDefaultStack());
+        });
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
@@ -1129,6 +1177,15 @@ class WorkbenchGenerator {
             .addInput(new ItemStack(Items.DYE, 1, DyeColor.BROWN.itemMeta))
             .create("cookie", new ItemStack(Items.FOOD_COOKIE, 16));
 
+        RecipeBuilder.Shaped(CORE_NAMESPACE)
+            .setShape(
+                "GGG",
+                "GAG",
+                "GGG")
+            .addInput('G', Items.INGOT_GOLD)
+            .addInput('A', Items.FOOD_APPLE)
+            .create("apple_gold", Items.FOOD_APPLE_GOLD.getDefaultStack());
+
         RecipeBuilderShaped cakeBase = RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
                 "AAA",
@@ -1393,14 +1450,6 @@ class WorkbenchGenerator {
             .addInput('I', "minecraft:stones")
             .create("redstone_timer", new ItemStack(Items.TIMER, 1));
 
-        RecipeBuilder.Shaped(CORE_NAMESPACE)
-            .setShape(
-                "X",
-                "#")
-            .addInput('X', Blocks.PUMPKIN_CARVED_IDLE)
-            .addInput('#', Blocks.TORCH_REDSTONE_ACTIVE)
-            .create("carved_pumpkin_to_redstone_jack_o_lantern", Blocks.PUMPKIN_REDSTONE.getDefaultStack());
-
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
                 "X X",
@@ -1442,9 +1491,9 @@ class WorkbenchGenerator {
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
             .setShape(
-                "#Q#",
-                "#C#",
-                "#Q#"
+                "###",
+                "QCQ",
+                "###"
             )
             .addInput('#', Blocks.NETHERRACK)
             .addInput('Q', Items.QUARTZ)
@@ -1453,11 +1502,11 @@ class WorkbenchGenerator {
 
         RecipeBuilder.Shaped(CORE_NAMESPACE)
                 .setShape(
-                        "XOX",
-                        "ORO",
-                        "XOX")
-                .addInput('R', Items.DUST_REDSTONE)
-                .addInput('O', Items.RUBYGLASS)
+                        "XRX",
+                        "DDD",
+                        "XRX")
+                .addInput('D', Items.DUST_REDSTONE)
+                .addInput('R', Items.RUBYGLASS)
                 .addInput('X', Blocks.NETHERRACK)
                 .create("conduit", new ItemStack(Blocks.CONDUIT, 1));
 
@@ -1474,19 +1523,23 @@ class WorkbenchGenerator {
     }
 
     private static void generateScrapRecipes() {
-        Registries.RECIPES.WORKBENCH.register("scrap_chainmail_boots",        new RecipeEntryScrap(Items.ARMOR_BOOTS_CHAINMAIL,       Items.CHAINLINK, 4));
-        Registries.RECIPES.WORKBENCH.register("scrap_chainmail_leggings",     new RecipeEntryScrap(Items.ARMOR_LEGGINGS_CHAINMAIL,    Items.CHAINLINK, 4));
-        Registries.RECIPES.WORKBENCH.register("scrap_chainmail_chestplate",   new RecipeEntryScrap(Items.ARMOR_CHESTPLATE_CHAINMAIL,  Items.CHAINLINK, 4));
-        Registries.RECIPES.WORKBENCH.register("scrap_chainmail_helmet",       new RecipeEntryScrap(Items.ARMOR_HELMET_CHAINMAIL,      Items.CHAINLINK, 4));
-        Registries.RECIPES.WORKBENCH.register("scrap_chainmail_wolf",         new RecipeEntryScrap(Items.ARMOR_WOLF_CHAINMAIL,        Items.CHAINLINK, 4));
+        RecipeBuilder.consolidationGroup("scrap", () -> {
+            Registries.RECIPES.WORKBENCH.register("scrap_chainmail_boots",        new RecipeEntryScrap(Items.ARMOR_BOOTS_CHAINMAIL,       Items.CHAINLINK, 4));
+            Registries.RECIPES.WORKBENCH.register("scrap_chainmail_leggings",     new RecipeEntryScrap(Items.ARMOR_LEGGINGS_CHAINMAIL,    Items.CHAINLINK, 4));
+            Registries.RECIPES.WORKBENCH.register("scrap_chainmail_chestplate",   new RecipeEntryScrap(Items.ARMOR_CHESTPLATE_CHAINMAIL,  Items.CHAINLINK, 4));
+            Registries.RECIPES.WORKBENCH.register("scrap_chainmail_helmet",       new RecipeEntryScrap(Items.ARMOR_HELMET_CHAINMAIL,      Items.CHAINLINK, 4));
+            Registries.RECIPES.WORKBENCH.register("scrap_chainmail_wolf",         new RecipeEntryScrap(Items.ARMOR_WOLF_CHAINMAIL,        Items.CHAINLINK, 4));
+        });
     }
 
     private static void generateRepairableRecipes() {
-         Registries.RECIPES.WORKBENCH.register("repair_chainmail_boots",       new RecipeEntryRepairable(Items.ARMOR_BOOTS_CHAINMAIL.getDefaultStack(),        new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
-         Registries.RECIPES.WORKBENCH.register("repair_chainmail_leggings",    new RecipeEntryRepairable(Items.ARMOR_LEGGINGS_CHAINMAIL.getDefaultStack(),     new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
-         Registries.RECIPES.WORKBENCH.register("repair_chainmail_chestplate",  new RecipeEntryRepairable(Items.ARMOR_CHESTPLATE_CHAINMAIL.getDefaultStack(),   new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
-         Registries.RECIPES.WORKBENCH.register("repair_chainmail_helmet",      new RecipeEntryRepairable(Items.ARMOR_HELMET_CHAINMAIL.getDefaultStack(),       new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
-         Registries.RECIPES.WORKBENCH.register("repair_chainmail_wolf",        new RecipeEntryRepairable(Items.ARMOR_WOLF_CHAINMAIL.getDefaultStack(),         new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
+        RecipeBuilder.consolidationGroup("repair", () -> {
+            Registries.RECIPES.WORKBENCH.register("repair_chainmail_boots",       new RecipeEntryRepairable(Items.ARMOR_BOOTS_CHAINMAIL.getDefaultStack(),        new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
+            Registries.RECIPES.WORKBENCH.register("repair_chainmail_leggings",    new RecipeEntryRepairable(Items.ARMOR_LEGGINGS_CHAINMAIL.getDefaultStack(),     new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
+            Registries.RECIPES.WORKBENCH.register("repair_chainmail_chestplate",  new RecipeEntryRepairable(Items.ARMOR_CHESTPLATE_CHAINMAIL.getDefaultStack(),   new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
+            Registries.RECIPES.WORKBENCH.register("repair_chainmail_helmet",      new RecipeEntryRepairable(Items.ARMOR_HELMET_CHAINMAIL.getDefaultStack(),       new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
+            Registries.RECIPES.WORKBENCH.register("repair_chainmail_wolf",        new RecipeEntryRepairable(Items.ARMOR_WOLF_CHAINMAIL.getDefaultStack(),         new RecipeSymbol(Items.CHAINLINK.getDefaultStack())));
+        });
     }
 
     private static void generateToolRecipes() {
@@ -1494,10 +1547,22 @@ class WorkbenchGenerator {
     }
 
     private static void generateStatueRecipes() {
-        final String[] statueNames = new String[]{"stone", "basalt", "limestone", "granite", "marble", "slate", "permafrost", "netherrack", "gloomstone"};
-        final Item[] statueItems = new Item[]{Items.STATUE_STONE, Items.STATUE_BASALT, Items.STATUE_LIMESTONE, Items.STATUE_GRANITE, Items.STATUE_MARBLE, Items.STATUE_SLATE, Items.STATUE_PERMAFROST, Items.STATUE_NETHERRACK, Items.STATUE_GLOOMSTONE};
-        final Block<?>[] statueBodyBlocks = new Block[]{Blocks.STONE, Blocks.BASALT, Blocks.LIMESTONE, Blocks.GRANITE, Blocks.MARBLE, Blocks.SLATE, Blocks.PERMAFROST, Blocks.NETHERRACK, Blocks.GLOOMSTONE};
-        final Block<?>[] statueBaseBlocks = new Block[]{Blocks.SLAB_STONE_POLISHED, Blocks.SLAB_BASALT_POLISHED, Blocks.SLAB_LIMESTONE_POLISHED, Blocks.SLAB_GRANITE_POLISHED, Blocks.SLAB_CAPSTONE_MARBLE, Blocks.SLAB_SLATE_POLISHED, Blocks.SLAB_PERMAFROST_POLISHED, Blocks.SLAB_NETHERRACK_POLISHED, Blocks.SLAB_GLOOMSTONE_POLISHED};
+        final String[] statueNames = new String[]{
+                "stone", "basalt", "limestone",
+                "granite", "marble", "slate",
+                "permafrost", "netherrack", "gloomstone"};
+        final Item[] statueItems = new Item[]{
+                Items.STATUE_STONE, Items.STATUE_BASALT, Items.STATUE_LIMESTONE,
+                Items.STATUE_GRANITE, Items.STATUE_MARBLE, Items.STATUE_SLATE,
+                Items.STATUE_PERMAFROST, Items.STATUE_NETHERRACK, Items.STATUE_GLOOMSTONE};
+        final Block<?>[] statueBodyBlocks = new Block[]{
+                Blocks.STONE, Blocks.BASALT, Blocks.LIMESTONE,
+                Blocks.GRANITE, Blocks.MARBLE, Blocks.SLATE,
+                Blocks.PERMAFROST, Blocks.NETHERRACK, Blocks.GLOOMSTONE};
+        final Block<?>[] statueBaseBlocks = new Block[]{
+                Blocks.SLAB_STONE_POLISHED, Blocks.SLAB_BASALT_POLISHED, Blocks.SLAB_LIMESTONE_POLISHED,
+                Blocks.SLAB_GRANITE_POLISHED, Blocks.SLAB_CAPSTONE_MARBLE, Blocks.SLAB_SLATE_POLISHED,
+                Blocks.SLAB_PERMAFROST_POLISHED, Blocks.SLAB_NETHERRACK_POLISHED, Blocks.SLAB_GLOOMSTONE_POLISHED};
 
         for (int i = 0; i < statueItems.length; i++) {
             RecipeBuilder.Shaped(CORE_NAMESPACE)
@@ -1546,16 +1611,13 @@ class WorkbenchGenerator {
 
     private static void generateMiscNewRecipes() {
         // Brimstone Formatting
-        RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("SS", "SS").addInput('S', Blocks.BRIMSAND).create("brimstone", new ItemStack(Blocks.BRIMSTONE, 2));
         RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("SSS").addInput('S', Blocks.BRIMSTONE).create("brimstone_slab", new ItemStack(Blocks.SLAB_BRIMSTONE, 6));
         RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("S  ", "SS ", "SSS").addInput('S', Blocks.BRIMSTONE).create("brimstone_stairs", new ItemStack(Blocks.STAIRS_BRIMSTONE, 6));
-        RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("SS", "SS").addInput('S', Blocks.BRIMSTONE).create("brimstone_bricks", new ItemStack(Blocks.BRICK_BRIMSTONE, 4));
         RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("SSS").addInput('S', Blocks.BRICK_BRIMSTONE).create("brick_brimstone_slab", new ItemStack(Blocks.SLAB_BRICK_BRIMSTONE, 6));
         RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("S  ", "SS ", "SSS").addInput('S', Blocks.BRICK_BRIMSTONE).create("brick_brimstone_stairs", new ItemStack(Blocks.STAIRS_BRICK_BRIMSTONE, 6));
 
         // Misc additions
         RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("SSS").addInput('S', Blocks.SLATE_POLISHED).create("polished_slate_slab", new ItemStack(Blocks.SLAB_SLATE_POLISHED, 6));
-        RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("PO", "OP").addInput('P', Items.AMMO_PEBBLE).addInput('O', Blocks.OBSIDIAN).create("pebbles_to_gloomstone", new ItemStack(Blocks.COBBLE_GLOOMSTONE, 2));
         RecipeBuilder.Shapeless(CORE_NAMESPACE).addInput(Items.SULFUR).addInput(Items.PAPER).addInput(Items.AMMO_ARROW).addInput(Items.SULFUR).create("flaming_arrow", new ItemStack(Items.AMMO_ARROW_FLAMING, 1));
         RecipeBuilder.Shaped(CORE_NAMESPACE).setShape("SS", "SS").addInput('S', Items.SULFUR).create("sulfur_block", new ItemStack(Blocks.SULFUR, 1));
 
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilder.java b/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilder.java
index 3fcfa9d36..305581fd4 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilder.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilder.java
@@ -182,6 +182,18 @@ public final class RecipeBuilder {
         return new RecipeBuilderTrommel(modID);
     }
 
+    public static String currentConsolidationGroup = null;
+
+    public static void consolidationGroup(String name, Runnable recipes) {
+        String previous = currentConsolidationGroup;
+        currentConsolidationGroup = name;
+        try {
+            recipes.run();
+        } finally {
+            currentConsolidationGroup = previous;
+        }
+    }
+
     public static boolean isExporting = false;
 
     /**
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShaped.java b/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShaped.java
index 1bafdf30a..f41c7013c 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShaped.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShaped.java
@@ -223,6 +223,8 @@ public class RecipeBuilderShaped extends RecipeBuilderBase{
         }
         RecipeGroup<RecipeEntryCrafting<?, ?>> group = ((RecipeGroup<RecipeEntryCrafting<?, ?>>) RecipeBuilder.getRecipeGroup(modID, "workbench", new RecipeSymbol(Blocks.WORKBENCH.getDefaultStack())));
         if (group.getItem(recipeID) != null) throw new IllegalArgumentException("RecipeID '" + recipeID + "' already exists!");
-        group.register(recipeID, new RecipeEntryCraftingShaped(width, height, recipe, outputStack, consumeContainer, allowMirrored));
+        RecipeEntryCraftingShaped entry = new RecipeEntryCraftingShaped(width, height, recipe, outputStack, consumeContainer, allowMirrored);
+        entry.consolidationGroup = RecipeBuilder.currentConsolidationGroup;
+        group.register(recipeID, entry);
     }
 }
diff --git a/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShapeless.java b/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShapeless.java
index f73b51f76..46ee1cf11 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShapeless.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/recipeBuilders/RecipeBuilderShapeless.java
@@ -85,6 +85,8 @@ public class RecipeBuilderShapeless extends RecipeBuilderBase{
     public void create(String recipeID, ItemStack outputStack) {
         RecipeGroup<RecipeEntryCrafting<?, ?>> group = ((RecipeGroup<RecipeEntryCrafting<?, ?>>) RecipeBuilder.getRecipeGroup(modID, "workbench", new RecipeSymbol(Blocks.WORKBENCH.getDefaultStack())));
         if (group.getItem(recipeID) != null) throw new IllegalArgumentException("RecipeID '" + recipeID + "' already exists!");
-        group.register(recipeID, new RecipeEntryCraftingShapeless(symbolShapelessList, outputStack));
+        RecipeEntryCraftingShapeless entry = new RecipeEntryCraftingShapeless(symbolShapelessList, outputStack);
+        entry.consolidationGroup = RecipeBuilder.currentConsolidationGroup;
+        group.register(recipeID, entry);
     }
 }
