diff --git a/game/client/src/main/java/net/betterthanadventure/utils/DataDumper.java b/game/client/src/main/java/net/betterthanadventure/utils/DataDumper.java
index 5e06c9c25..1e4457da4 100644
--- a/game/client/src/main/java/net/betterthanadventure/utils/DataDumper.java
+++ b/game/client/src/main/java/net/betterthanadventure/utils/DataDumper.java
@@ -71,8 +71,7 @@ public class DataDumper implements MinecraftAccessor {
         Blocks.init();
         Items.init();
 
-        File wikiDumpFile = new File(getMinecraftDir(), "infodump");
-        wikiDumpFile.mkdirs();
+        File wikiDumpFile = DumpFolder.resolve(getMinecraftDir());
         BufferedWriter writer;
 
         writer = new BufferedWriter(new FileWriter(new File(wikiDumpFile, "blocks.csv")));
diff --git a/game/client/src/main/java/net/betterthanadventure/utils/DumpFolder.java b/game/client/src/main/java/net/betterthanadventure/utils/DumpFolder.java
new file mode 100644
index 000000000..c2596db40
--- /dev/null
+++ b/game/client/src/main/java/net/betterthanadventure/utils/DumpFolder.java
@@ -0,0 +1,21 @@
+package net.betterthanadventure.utils;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Shared location for all debug dump output (data dumper CSVs, wiki dump, inventory sprites).
+ */
+public final class DumpFolder {
+    /** Name of the dump folder inside the Minecraft directory. */
+    public static final String NAME = "dump";
+
+    private DumpFolder() {}
+
+    public static @NotNull File resolve(@NotNull File minecraftDir) {
+        final File dir = new File(minecraftDir, NAME);
+        dir.mkdirs();
+        return dir;
+    }
+}
diff --git a/game/client/src/main/java/net/betterthanadventure/utils/InvSpriteAPNGDumper.java b/game/client/src/main/java/net/betterthanadventure/utils/InvSpriteAPNGDumper.java
new file mode 100644
index 000000000..438361555
--- /dev/null
+++ b/game/client/src/main/java/net/betterthanadventure/utils/InvSpriteAPNGDumper.java
@@ -0,0 +1,213 @@
+package net.betterthanadventure.utils;
+
+import com.mojang.logging.LogUtils;
+import net.betterthanadventure.utils.InvSpriteDumper.InvSpriteEntry;
+import net.minecraft.client.Minecraft;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.List;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+
+
+public final class InvSpriteAPNGDumper {
+    private static final Logger LOGGER = LogUtils.getLogger();
+
+    private static final int DEFAULT_FRAME_COUNT = 60;
+    private static final int DEFAULT_TICKS_PER_FRAME = 1;
+
+    private InvSpriteAPNGDumper() {}
+
+    public static @NotNull File dump(@NotNull File minecraftDir) throws IOException {
+        return dump(minecraftDir, DEFAULT_FRAME_COUNT, DEFAULT_TICKS_PER_FRAME);
+    }
+
+    public static @NotNull File dump(@NotNull File minecraftDir, int frameCount, int ticksPerFrame) throws IOException {
+        final File dumpDir = DumpFolder.resolve(minecraftDir);
+        final File apngFile = new File(dumpDir, "InvSprite.apng");
+
+        final Minecraft mc = Minecraft.getMinecraft();
+        final List<InvSpriteEntry> entries = InvSpriteDumper.collectInvSpriteEntries();
+
+        final int[] fbo = InvSpriteDumper.createFramebuffer();
+        final var originalColors = InvSpriteDumper.applyBiomeSeasonColors();
+        try (APNGSequenceWriter apng = new APNGSequenceWriter(apngFile, frameCount, ticksPerFrame * 50, true)) {
+            // Render and stream each frame
+            for (int frame = 0; frame < frameCount; frame++) {
+                advanceAnimations(mc, ticksPerFrame);
+                final BufferedImage sheet = InvSpriteDumper.newSheet(entries.size());
+                InvSpriteDumper.renderSheet(fbo[1], entries, sheet);
+                apng.writeFrame(sheet);
+            }
+        } finally {
+            InvSpriteDumper.restoreBlockColors(originalColors);
+            InvSpriteDumper.destroyFramebuffer(fbo);
+        }
+
+        LOGGER.info("Dumped {} frame animated inventory sprite sheet to {}", frameCount, apngFile.getName());
+        return dumpDir;
+    }
+
+    private static void advanceAnimations(@NotNull Minecraft mc, int ticks) {
+        for (int i = 0; i < ticks; i++) {
+            mc.textureManager.updateDynamicTextures(false);
+        }
+    }
+
+    //modified apng writer
+    private static final class APNGSequenceWriter implements Closeable {
+        private static final byte[] SIGNATURE = {(byte) 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'};
+
+        private final DataOutputStream out;
+        private final int frameCount;
+        private final int delayNum;
+        private final int numPlays;
+        private final CRC32 crc = new CRC32();
+
+        private int sequenceNumber = 0;
+        private int width = -1;
+        private int height = -1;
+        private boolean wroteHeader = false;
+        private boolean wroteFirstFrame = false;
+
+        APNGSequenceWriter(@NotNull File file, int frameCount, int delayMillis, boolean loop) throws IOException {
+            this.frameCount = frameCount;
+            this.delayNum = Math.max(1, delayMillis);
+            // num_plays of 0 means loop forever; otherwise play once.
+            this.numPlays = loop ? 0 : 1;
+            this.out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(file.toPath())));
+            this.out.write(SIGNATURE);
+        }
+
+        void writeFrame(@NotNull BufferedImage frame) throws IOException {
+            if (!this.wroteHeader) {
+                this.width = frame.getWidth();
+                this.height = frame.getHeight();
+                writeHeaderChunks();
+                this.wroteHeader = true;
+            } else if (frame.getWidth() != this.width || frame.getHeight() != this.height) {
+                throw new IOException("APNG frames must all share the same dimensions");
+            }
+
+            writeFrameControl();
+
+            final byte[] compressed = deflate(toRawScanlines(frame));
+            if (!this.wroteFirstFrame) {
+                writeChunk("IDAT", compressed);
+                this.wroteFirstFrame = true;
+            } else {
+                // fdAT data is prefixed with its own sequence number, followed by the compressed scanlines.
+                final byte[] data = new byte[compressed.length + 4];
+                writeInt(data, 0, this.sequenceNumber++);
+                System.arraycopy(compressed, 0, data, 4, compressed.length);
+                writeChunk("fdAT", data);
+            }
+        }
+
+        private void writeHeaderChunks() throws IOException {
+            final byte[] ihdr = new byte[13];
+            writeInt(ihdr, 0, this.width);
+            writeInt(ihdr, 4, this.height);
+            ihdr[8] = 8;  // bit depth
+            ihdr[9] = 6;  // color type: truecolor with alpha (RGBA)
+            ihdr[10] = 0; // compression: deflate
+            ihdr[11] = 0; // filter: adaptive
+            ihdr[12] = 0; // interlace: none
+            writeChunk("IHDR", ihdr);
+
+            final byte[] actl = new byte[8];
+            writeInt(actl, 0, this.frameCount);
+            writeInt(actl, 4, this.numPlays);
+            writeChunk("acTL", actl);
+        }
+
+        private void writeFrameControl() throws IOException {
+            final byte[] fctl = new byte[26];
+            writeInt(fctl, 0, this.sequenceNumber++);
+            writeInt(fctl, 4, this.width);
+            writeInt(fctl, 8, this.height);
+            writeInt(fctl, 12, 0); // x offset
+            writeInt(fctl, 16, 0); // y offset
+            writeShort(fctl, 20, this.delayNum); // delay numerator (ms)
+            writeShort(fctl, 22, 1000);    // delay denominator -> delay in milliseconds
+            fctl[24] = 1; // dispose_op = APNG_DISPOSE_OP_BACKGROUND: clear to transparent before next frame
+            fctl[25] = 0; // blend_op = APNG_BLEND_OP_SOURCE: overwrite pixels (keep exact alpha)
+            writeChunk("fcTL", fctl);
+        }
+
+        private byte @NotNull [] toRawScanlines(@NotNull BufferedImage image) {
+            final int w = this.width;
+            final int h = this.height;
+            final byte[] raw = new byte[h * (1 + w * 4)];
+            final int[] row = new int[w];
+            int pos = 0;
+            for (int y = 0; y < h; y++) {
+                image.getRGB(0, y, w, 1, row, 0, w);
+                raw[pos++] = 0; // filter type: None
+                for (int x = 0; x < w; x++) {
+                    final int argb = row[x];
+                    raw[pos++] = (byte) ((argb >> 16) & 0xFF);  // R
+                    raw[pos++] = (byte) ((argb >> 8) & 0xFF);   // G
+                    raw[pos++] = (byte) (argb & 0xFF);          // B
+                    raw[pos++] = (byte) ((argb >>> 24) & 0xFF); // A
+                }
+            }
+            return raw;
+        }
+
+        private static byte @NotNull [] deflate(byte @NotNull [] data) {
+            final Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION);
+            deflater.setInput(data);
+            deflater.finish();
+            final ByteArrayOutputStream buffer = new ByteArrayOutputStream(Math.max(64, data.length / 2));
+            final byte[] chunk = new byte[8192];
+            while (!deflater.finished()) {
+                final int n = deflater.deflate(chunk);
+                buffer.write(chunk, 0, n);
+            }
+            deflater.end();
+            return buffer.toByteArray();
+        }
+
+        private void writeChunk(@NotNull String type, byte @NotNull [] data) throws IOException {
+            this.out.writeInt(data.length);
+            final byte[] typeBytes = type.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
+            this.out.write(typeBytes);
+            this.out.write(data);
+
+            this.crc.reset();
+            this.crc.update(typeBytes);
+            this.crc.update(data);
+            this.out.writeInt((int) this.crc.getValue());
+        }
+
+        private static void writeInt(byte @NotNull [] dst, int offset, int value) {
+            dst[offset] = (byte) ((value >>> 24) & 0xFF);
+            dst[offset + 1] = (byte) ((value >>> 16) & 0xFF);
+            dst[offset + 2] = (byte) ((value >>> 8) & 0xFF);
+            dst[offset + 3] = (byte) (value & 0xFF);
+        }
+
+        private static void writeShort(byte @NotNull [] dst, int offset, int value) {
+            dst[offset] = (byte) ((value >>> 8) & 0xFF);
+            dst[offset + 1] = (byte) (value & 0xFF);
+        }
+
+        @Override
+        public void close() throws IOException {
+            try (OutputStream ignored = this.out) {
+                writeChunk("IEND", new byte[0]);
+            }
+        }
+    }
+}
diff --git a/game/client/src/main/java/net/betterthanadventure/utils/InvSpriteDumper.java b/game/client/src/main/java/net/betterthanadventure/utils/InvSpriteDumper.java
new file mode 100644
index 000000000..079891e1b
--- /dev/null
+++ b/game/client/src/main/java/net/betterthanadventure/utils/InvSpriteDumper.java
@@ -0,0 +1,373 @@
+package net.betterthanadventure.utils;
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.render.Lighting;
+import net.minecraft.client.render.block.color.BlockColor;
+import net.minecraft.client.render.block.color.BlockColorCustom;
+import net.minecraft.client.render.block.color.BlockColorDispatcher;
+import net.minecraft.client.render.colorizer.ColorMap;
+import net.minecraft.client.render.colorizer.Colorizer;
+import net.minecraft.client.render.colorizer.Colorizers;
+import net.minecraft.client.render.item.model.ItemModel;
+import net.minecraft.client.render.item.model.ItemModelDispatcher;
+import net.minecraft.client.render.renderer.GLRenderer;
+import net.minecraft.client.render.renderer.State;
+import net.minecraft.client.render.texturepack.TexturePack;
+import net.minecraft.client.util.helper.Buffer;
+import net.minecraft.client.util.helper.Textures;
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.IPainted;
+import net.minecraft.core.item.Item;
+import net.minecraft.core.item.ItemBucket;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.lang.I18n;
+import net.minecraft.core.util.collection.NamespaceID;
+import net.minecraft.core.util.helper.DyeColor;
+import net.minecraft.core.util.helper.LightIndexHelper;
+import net.minecraft.core.world.WorldSource;
+import net.minecraft.core.world.biome.Biomes;
+import net.minecraft.core.world.pos.TilePosc;
+import net.minecraft.core.world.season.Seasons;
+import org.jetbrains.annotations.NotNull;
+import org.lwjgl.opengl.GL41;
+import org.slf4j.Logger;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Debug utility that renders every registered block and item exactly as it appears in the inventory
+ * into a single {@code InvSprite.png} sheet and writes a Lua module mapping each name to its sprite index,
+ * matching the format used by the BTA wiki.
+ */
+public final class InvSpriteDumper {
+    private static final Logger LOGGER = LogUtils.getLogger();
+
+    private static final int SPRITE_SIZE = 32;
+    private static final int SHEET_SIZE = 512;
+    private static final int COLUMNS = SHEET_SIZE / SPRITE_SIZE;
+    private static final int FILLER_INDEX = 0;
+    private static final int GENERATED_START_INDEX = 500;
+
+    private InvSpriteDumper() {}
+
+    public static @NotNull File dump(@NotNull File minecraftDir) throws IOException {
+        final File dumpDir = DumpFolder.resolve(minecraftDir);
+
+        final List<InvSpriteEntry> entries = collectInvSpriteEntries();
+
+        final BufferedImage sheet = newSheet(entries.size());
+
+        final int[] fbo = createFramebuffer();
+        // Render biome/season as if in a mid-summer forest
+        final Map<Block<?>, BlockColor> originalColors = applyBiomeSeasonColors();
+        try {
+            renderSheet(fbo[1], entries, sheet);
+        } finally {
+            restoreBlockColors(originalColors);
+            destroyFramebuffer(fbo);
+        }
+
+        final File pngFile = new File(dumpDir, "InvSprite.png");
+        Textures.saveImage(pngFile, sheet);
+
+        final File luaFile = new File(dumpDir, "invsprite.lua");
+        try (Writer writer = Files.newBufferedWriter(luaFile.toPath(), StandardCharsets.UTF_8)) {
+            writeLua(writer, entries);
+        }
+
+        LOGGER.info("Dumped {} inventory sprites to {} and {}", entries.size(), pngFile.getName(), luaFile.getName());
+        return dumpDir;
+    }
+
+    private static void writeLua(@NotNull Writer writer, @NotNull List<InvSpriteEntry> entries) throws IOException {
+        writer.write("return {\n");
+        writer.write("\tsettings = {\n");
+        writer.write("\t\talign = 'middle',\n");
+        writer.write("\t\timage = 'InvSprite.png',\n");
+        writer.write("\t\tsheetsize = " + SHEET_SIZE + ",\n");
+        writer.write("\t\tsize = " + SPRITE_SIZE + ",\n");
+        writer.write("\t},\n");
+        writer.write("\tids = {\n");
+        writer.write("\t\t-- Manually-added entries (ex. removed/legacy features) occupy indices " + FILLER_INDEX + "-" + (GENERATED_START_INDEX - 1) + ".\n");
+        writer.write("\t\t[\"Filler\"] = " + FILLER_INDEX + ",\n");
+        writer.write("\t\t-- Generated entries start at " + GENERATED_START_INDEX + ".\n");
+        for (int i = 0; i < entries.size(); i++) {
+            writer.write("\t\t[\"" + escape(entries.get(i).name()) + "\"] = " + (GENERATED_START_INDEX + i) + ",\n");
+        }
+        writer.write("\t}\n");
+        writer.write("}\n");
+    }
+
+    static @NotNull BufferedImage newSheet(int entryCount) {
+        final int maxIndex = Math.max(GENERATED_START_INDEX + entryCount - 1, FILLER_INDEX);
+        final int rows = maxIndex / COLUMNS + 1;
+        return new BufferedImage(SHEET_SIZE, rows * SPRITE_SIZE, BufferedImage.TYPE_INT_ARGB);
+    }
+
+    static void renderSheet(int colorTexture, @NotNull List<InvSpriteEntry> entries, @NotNull BufferedImage sheet) {
+        // Filler stone block at index 0; manual (removed/legacy) entries occupy indices up to GENERATED_START_INDEX.
+        blit(sheet, renderSprite(colorTexture, new ItemStack(Blocks.STONE, 1, 0)), FILLER_INDEX);
+        for (int i = 0; i < entries.size(); i++) {
+            blit(sheet, renderSprite(colorTexture, entries.get(i).stack()), GENERATED_START_INDEX + i);
+        }
+    }
+
+    private static @NotNull BufferedImage renderSprite(int colorTexture, @NotNull ItemStack stack) {
+        GL41.glViewport(0, 0, SPRITE_SIZE, SPRITE_SIZE);
+
+        GLRenderer.pushFrame();
+        GLRenderer.projectionM4f().identity().ortho(0.0f, 16.0f, 16.0f, 0.0f, 1000.0f, 3000.0f);
+        GLRenderer.viewM4f().identity().translate(0.0f, 0.0f, -2000.0f);
+        GLRenderer.modelM4f().identity();
+
+        GL41.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
+        GL41.glClear(GL41.GL_COLOR_BUFFER_BIT | GL41.GL_DEPTH_BUFFER_BIT);
+
+        Lighting.enableInventoryLight();
+        GLRenderer.setColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+        GLRenderer.enableState(State.DEPTH_TEST);
+        try {
+            final ItemModel model = ItemModelDispatcher.getInstance().getDispatch(stack.getItem());
+            model.renderGui(GLRenderer.getTessellator(), null, stack, 0, 0, LightIndexHelper.lightIndex2i(15, 15), 1.0f);
+        } catch (final Exception e) {
+            LOGGER.error("Failed to render inventory sprite for '{}'", stack.getItem().namespaceID, e);
+        }
+        GLRenderer.disableState(State.DEPTH_TEST);
+        Lighting.disable();
+        GLRenderer.popFrame();
+
+        final int size = SPRITE_SIZE * SPRITE_SIZE * 4;
+        Buffer.checkBufferSize(size);
+        final ByteBuffer buffer = Buffer.buffer;
+        buffer.position(0).limit(size);
+        GL41.glPixelStorei(GL41.GL_PACK_ALIGNMENT, 1);
+        GL41.glBindTexture(GL41.GL_TEXTURE_2D, colorTexture);
+        GL41.glGetTexImage(GL41.GL_TEXTURE_2D, 0, GL41.GL_RGBA, GL41.GL_UNSIGNED_BYTE, buffer);
+        buffer.position(0);
+
+        return Textures.getImage(buffer, SPRITE_SIZE, SPRITE_SIZE, true, true);
+    }
+
+    private static void blit(@NotNull BufferedImage sheet, @NotNull BufferedImage sprite, int index) {
+        final int x = (index % COLUMNS) * SPRITE_SIZE;
+        final int y = (index / COLUMNS) * SPRITE_SIZE;
+        final int[] pixels = sprite.getRGB(0, 0, SPRITE_SIZE, SPRITE_SIZE, null, 0, SPRITE_SIZE);
+        sheet.setRGB(x, y, SPRITE_SIZE, SPRITE_SIZE, pixels, 0, SPRITE_SIZE);
+    }
+
+    static int @NotNull [] createFramebuffer() {
+        final int fbo = GL41.glGenFramebuffers();
+        GL41.glBindFramebuffer(GL41.GL_FRAMEBUFFER, fbo);
+
+        final int colorTexture = GL41.glGenTextures();
+        GL41.glBindTexture(GL41.GL_TEXTURE_2D, colorTexture);
+        GL41.glTexImage2D(GL41.GL_TEXTURE_2D, 0, GL41.GL_RGBA8, SPRITE_SIZE, SPRITE_SIZE, 0, GL41.GL_RGBA, GL41.GL_UNSIGNED_BYTE, (ByteBuffer) null);
+        GL41.glTexParameteri(GL41.GL_TEXTURE_2D, GL41.GL_TEXTURE_MIN_FILTER, GL41.GL_NEAREST);
+        GL41.glTexParameteri(GL41.GL_TEXTURE_2D, GL41.GL_TEXTURE_MAG_FILTER, GL41.GL_NEAREST);
+        GL41.glFramebufferTexture2D(GL41.GL_FRAMEBUFFER, GL41.GL_COLOR_ATTACHMENT0, GL41.GL_TEXTURE_2D, colorTexture, 0);
+
+        final int depthBuffer = GL41.glGenRenderbuffers();
+        GL41.glBindRenderbuffer(GL41.GL_RENDERBUFFER, depthBuffer);
+        GL41.glRenderbufferStorage(GL41.GL_RENDERBUFFER, GL41.GL_DEPTH_COMPONENT24, SPRITE_SIZE, SPRITE_SIZE);
+        GL41.glFramebufferRenderbuffer(GL41.GL_FRAMEBUFFER, GL41.GL_DEPTH_ATTACHMENT, GL41.GL_RENDERBUFFER, depthBuffer);
+
+        final int status = GL41.glCheckFramebufferStatus(GL41.GL_FRAMEBUFFER);
+        if (status != GL41.GL_FRAMEBUFFER_COMPLETE) {
+            throw new IllegalStateException("InvSprite framebuffer incomplete: 0x" + Integer.toHexString(status));
+        }
+
+        return new int[]{fbo, colorTexture, depthBuffer};
+    }
+
+    static void destroyFramebuffer(int @NotNull [] fbo) {
+        GL41.glBindFramebuffer(GL41.GL_FRAMEBUFFER, 0);
+        GL41.glDeleteRenderbuffers(fbo[2]);
+        GL41.glDeleteTextures(fbo[1]);
+        GL41.glDeleteFramebuffers(fbo[0]);
+
+        final Minecraft mc = Minecraft.getMinecraft();
+        GL41.glViewport(0, 0, mc.gameWindow.getWidthPixels(), mc.gameWindow.getHeightPixels());
+    }
+
+
+    public record InvSpriteEntry(@NotNull String name, @NotNull ItemStack stack) {}
+
+    public static @NotNull List<InvSpriteEntry> collectInvSpriteEntries() {
+        final I18n i18n = I18n.getInstance();
+        final List<InvSpriteEntry> entries = new ArrayList<>();
+
+        for (final Block<?> block : Blocks.blocksList) {
+            if (block == null || block.id() == 0) continue;
+
+            if (block.getLogic() instanceof IPainted painted && WikiDumper.hasPaintedColorNames(i18n, block.getKey())) {
+                collectPaintedBlockEntries(entries, i18n, block, painted);
+            } else {
+                final String name = i18n.translateKey(block.getKey() + ".name");
+                entries.add(new InvSpriteEntry(name, new ItemStack(block.asItem(), 1, 0)));
+            }
+        }
+
+        for (final Item item : Item.itemsList) {
+            if (item == null) continue;
+            if (item.id < Blocks.blocksList.length) continue;
+            if (item instanceof ItemBucket bucket) {
+                collectBucketEntries(entries, i18n, bucket);
+                continue;
+            }
+            collectItemEntries(entries, i18n, item);
+        }
+
+        return entries;
+    }
+
+    private static void collectPaintedBlockEntries(@NotNull List<InvSpriteEntry> entries, @NotNull I18n i18n,
+                                                   @NotNull Block<?> block, @NotNull IPainted painted) {
+        final String key = block.getKey();
+
+        int minMeta = Integer.MAX_VALUE;
+        for (final DyeColor color : DyeColor.values()) {
+            minMeta = Math.min(minMeta, painted.toMetadata(color));
+        }
+
+        final String mysteryName = i18n.translateKey(key + ".name");
+        entries.add(new InvSpriteEntry(mysteryName, new ItemStack(block.asItem(), 1, minMeta)));
+
+        for (final DyeColor color : DyeColor.values()) {
+            final String colorKey = key + "." + color.colorID;
+            final String colorName = i18n.translateKey(colorKey + ".name");
+            entries.add(new InvSpriteEntry(colorName, new ItemStack(block.asItem(), 1, painted.toMetadata(color))));
+        }
+    }
+
+    private static void collectItemEntries(@NotNull List<InvSpriteEntry> entries, @NotNull I18n i18n, @NotNull Item item) {
+        final String baseKey = item.getKey();
+
+        final Map<String, Integer> variants = new LinkedHashMap<>();
+        for (int meta = 0; meta <= DyeColor.MASK_COLOR; meta++) {
+            String languageKey;
+            try {
+                languageKey = item.getLanguageKey(new ItemStack(item, 1, meta));
+            } catch (final Exception e) {
+                languageKey = baseKey;
+            }
+            if (WikiDumper.hasTranslation(i18n, languageKey + ".name")) {
+                variants.putIfAbsent(languageKey, meta);
+            }
+        }
+
+        final boolean variesByMetadata = variants.size() > 1
+                || (variants.size() == 1 && !variants.containsKey(baseKey));
+
+        if (!variesByMetadata) {
+            entries.add(new InvSpriteEntry(WikiDumper.getWikiDisplayName(item, i18n), new ItemStack(item, 1, 0)));
+            return;
+        }
+
+        if (!variants.containsKey(baseKey) && WikiDumper.hasTranslation(i18n, baseKey + ".name")) {
+            entries.add(new InvSpriteEntry(WikiDumper.getWikiDisplayName(item, i18n), new ItemStack(item, 1, 0)));
+        }
+
+        for (final Map.Entry<String, Integer> variant : variants.entrySet()) {
+            final String name = i18n.translateKey(variant.getKey() + ".name");
+            entries.add(new InvSpriteEntry(name, new ItemStack(item, 1, variant.getValue())));
+        }
+    }
+
+    private static void collectBucketEntries(@NotNull List<InvSpriteEntry> entries, @NotNull I18n i18n, @NotNull ItemBucket bucket) {
+        entries.add(new InvSpriteEntry(i18n.translateKey(bucket.getKey() + ".name"), new ItemStack(bucket, 1, 0)));
+
+        for (final NamespaceID stateId : ItemBucket.getRegisteredStateIds()) {
+            if (ItemBucket.STATE_EMPTY.equals(stateId)) continue;
+
+            final ItemStack sample = ItemBucket.createItemStack(bucket, stateId, 1);
+            final String stateName = i18n.translateKey(bucket.getLanguageKey(sample) + ".name");
+
+            for (int charges = 1; charges <= bucket.maxCharges; charges++) {
+                final ItemStack stack = ItemBucket.createItemStack(bucket, stateId, charges);
+                final String name = bucket.maxCharges > 1 ? stateName + " (" + charges + ")" : stateName;
+                entries.add(new InvSpriteEntry(name, stack));
+            }
+        }
+    }
+
+    static @NotNull Map<Block<?>, BlockColor> applyBiomeSeasonColors() {
+        final BlockColorDispatcher dispatcher = BlockColorDispatcher.getInstance();
+        final Map<String, Integer> colorByColorizer = new HashMap<>();
+        final Map<Block<?>, BlockColor> originals = new LinkedHashMap<>();
+
+        for (final Block<?> block : Blocks.blocksList) {
+            if (block == null) continue;
+            if (!dispatcher.hasDispatch(block)) continue;
+            if (!(dispatcher.getDispatch(block) instanceof BlockColorCustom custom)) continue;
+
+            final int color = colorByColorizer.computeIfAbsent(custom.colorizer.name, n -> biomeSeasonColor(custom.colorizer));
+            originals.put(block, custom);
+            dispatcher.addDispatch(block, new BiomeSeasonBlockColor(color));
+        }
+
+        return originals;
+    }
+
+    static void restoreBlockColors(@NotNull Map<Block<?>, BlockColor> originals) {
+        final BlockColorDispatcher dispatcher = BlockColorDispatcher.getInstance();
+        for (final Map.Entry<Block<?>, BlockColor> entry : originals.entrySet()) {
+            dispatcher.addDispatch(entry.getKey(), entry.getValue());
+        }
+    }
+
+    private static int biomeSeasonColor(@NotNull Colorizer colorizer) {
+        final Minecraft mc = Minecraft.getMinecraft();
+        final String base = "/assets/minecraft/textures/colormap/" + colorizer.name + "/";
+        final String defaultPath = base + "default.png";
+
+        final TexturePack pack = Colorizers.findTexturePackWithFile(mc, defaultPath);
+        if (pack == null) return 0xFFFFFFFF;
+
+        final String seasonPath = base + Seasons.OVERWORLD_SUMMER.getId().replace('.', '_').toLowerCase() + ".png";
+        ColorMap colorMap = pack.hasFile(seasonPath) ? Colorizers.loadColorData(pack, seasonPath) : null;
+        if (colorMap == null) colorMap = Colorizers.loadColorData(pack, defaultPath);
+        if (colorMap == null) return 0xFFFFFFFF;
+
+        final int rgb = colorMap.getColor(Biomes.OVERWORLD_FOREST.defaultTemperature, Biomes.OVERWORLD_FOREST.defaultHumidity);
+        return 0xFF000000 | (rgb & 0xFFFFFF);
+    }
+
+    private static final class BiomeSeasonBlockColor extends BlockColor {
+        private final int color;
+
+        private BiomeSeasonBlockColor(int color) {
+            this.color = color;
+        }
+
+        @Override
+        public int getFallbackColor(int meta, int tintIndex) {
+            return this.color;
+        }
+
+        @Override
+        public int getWorldColor(@NotNull WorldSource source, @NotNull TilePosc tilePos, int tintIndex) {
+            return this.color;
+        }
+    }
+
+    private static @NotNull String escape(@NotNull String value) {
+        return value
+                .replace("\\", "\\\\")
+                .replace("\"", "\\\"")
+                .replace("\r", " ")
+                .replace("\n", " ");
+    }
+}
diff --git a/game/client/src/main/java/net/betterthanadventure/utils/WikiDumper.java b/game/client/src/main/java/net/betterthanadventure/utils/WikiDumper.java
new file mode 100644
index 000000000..c84a790fb
--- /dev/null
+++ b/game/client/src/main/java/net/betterthanadventure/utils/WikiDumper.java
@@ -0,0 +1,216 @@
+package net.betterthanadventure.utils;
+
+import net.minecraft.core.block.Block;
+import net.minecraft.core.block.Blocks;
+import net.minecraft.core.block.IPainted;
+import net.minecraft.core.block.tag.BlockTags;
+import net.minecraft.core.data.tag.ITaggable;
+import net.minecraft.core.data.tag.Tag;
+import net.minecraft.core.item.Item;
+import net.minecraft.core.item.ItemDiscMusic;
+import net.minecraft.core.item.ItemFood;
+import net.minecraft.core.item.ItemStack;
+import net.minecraft.core.item.tag.ItemTags;
+import net.minecraft.core.item.tool.ItemToolPickaxe;
+import net.minecraft.core.lang.I18n;
+import net.minecraft.core.util.helper.DyeColor;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Debug utility that dumps every registered block and item into a Lua table
+ * matching the format used by the BTA wiki in Data Value Table.
+ */
+public final class WikiDumper {
+    private WikiDumper() {}
+
+    public static @NotNull File dump(@NotNull File minecraftDir) throws IOException {
+        final File outFile = new File(DumpFolder.resolve(minecraftDir), "wiki_dump.lua");
+        try (Writer writer = Files.newBufferedWriter(outFile.toPath(), StandardCharsets.UTF_8)) {
+            writeDump(writer);
+        }
+        return outFile;
+    }
+
+    private static void writeDump(@NotNull Writer writer) throws IOException {
+        final I18n i18n = I18n.getInstance();
+
+        writer.write("-- Block: { key, namespaceId, id, hardness, blastResistance, miningLevel, stackSize, description, tags }\n");
+        writer.write("-- Item:  { key, namespaceId, id, durability, restores, stackSize, description, tags }\n");
+
+        writer.write("return {\n");
+
+        writer.write("\t[\"Block\"] = {\n");
+        boolean first = true;
+        for (final Block<?> block : Blocks.blocksList) {
+            if (block == null || block.id() == 0) continue;
+
+            if (block.getLogic() instanceof IPainted painted && hasPaintedColorNames(i18n, block.getKey())) {
+                first = writePaintedBlockEntries(writer, i18n, block, painted, first);
+            } else {
+                if (!first) writer.write(",\n");
+                first = false;
+
+                final String name = i18n.translateKey(block.getKey() + ".name");
+                writer.write(formatEntry(name, block.getKey(), block.namespaceId().toString(), String.valueOf(block.id()),
+                    blockExtra(block, block.getKey(), i18n)));
+            }
+        }
+        writer.write("\n\t},\n");
+
+        writer.write("\t[\"Item\"] = {\n");
+        first = true;
+        for (final Item item : Item.itemsList) {
+            if (item == null) continue;
+            if (item.id < Blocks.blocksList.length) continue;
+            first = writeItemEntries(writer, i18n, item, first);
+        }
+        writer.write("\n\t}\n");
+
+        writer.write("}\n");
+    }
+
+    public static @NotNull String getWikiDisplayName(@NotNull Item item, @NotNull I18n i18n) {
+        if (item instanceof ItemDiscMusic disc && disc.recordName != null) {
+            return "Music Disc - " + i18n.translateKey(disc.recordName);
+        }
+        return i18n.translateKey(item.getKey() + ".name");
+    }
+
+    private static boolean writeItemEntries(@NotNull Writer writer, @NotNull I18n i18n, @NotNull Item item, boolean first) throws IOException {
+        final String baseKey = item.getKey();
+        final String namespaceId = item.namespaceID.toString();
+        final String id = String.valueOf(item.id);
+
+        final Map<String, Integer> variants = new LinkedHashMap<>();
+        for (int meta = 0; meta <= DyeColor.MASK_COLOR; meta++) {
+            String languageKey;
+            try {
+                languageKey = item.getLanguageKey(new ItemStack(item, 1, meta));
+            } catch (final Exception e) {
+                languageKey = baseKey;
+            }
+            if (hasTranslation(i18n, languageKey + ".name")) {
+                variants.putIfAbsent(languageKey, meta);
+            }
+        }
+
+        final boolean variesByMetadata = variants.size() > 1
+            || (variants.size() == 1 && !variants.containsKey(baseKey));
+
+        if (!variesByMetadata) {
+            if (!first) writer.write(",\n");
+            writer.write(formatEntry(getWikiDisplayName(item, i18n), baseKey, namespaceId, id, itemExtra(item, baseKey, i18n)));
+            return false;
+        }
+
+        if (!variants.containsKey(baseKey) && hasTranslation(i18n, baseKey + ".name")) {
+            if (!first) writer.write(",\n");
+            first = false;
+            writer.write(formatEntry(getWikiDisplayName(item, i18n), baseKey, namespaceId, id, itemExtra(item, baseKey, i18n)));
+        }
+
+        for (final Map.Entry<String, Integer> variant : variants.entrySet()) {
+            if (!first) writer.write(",\n");
+            first = false;
+            final String name = i18n.translateKey(variant.getKey() + ".name");
+            writer.write(formatEntry(name, variant.getKey(), namespaceId, id + ":" + variant.getValue(),
+                itemExtra(item, variant.getKey(), i18n)));
+        }
+
+        return first;
+    }
+
+    public static boolean hasPaintedColorNames(@NotNull I18n i18n, @NotNull String key) {
+        return hasTranslation(i18n, key + "." + DyeColor.WHITE.colorID + ".name");
+    }
+
+    public static boolean hasTranslation(@NotNull I18n i18n, @NotNull String key) {
+        return !i18n.translateKey(key).equals(key);
+    }
+
+    private static boolean writePaintedBlockEntries(@NotNull Writer writer, @NotNull I18n i18n,
+                                                    @NotNull Block<?> block, @NotNull IPainted painted,
+                                                    boolean first) throws IOException {
+        final String key = block.getKey();
+        final String namespaceId = block.namespaceId().toString();
+        final String id = String.valueOf(block.id());
+
+        int minMeta = Integer.MAX_VALUE;
+        int maxMeta = Integer.MIN_VALUE;
+        for (final DyeColor color : DyeColor.values()) {
+            final int meta = painted.toMetadata(color);
+            minMeta = Math.min(minMeta, meta);
+            maxMeta = Math.max(maxMeta, meta);
+        }
+
+        final String mysteryName = i18n.translateKey(key + ".name");
+        final String mysteryMeta = minMeta == maxMeta ? String.valueOf(minMeta) : "[" + minMeta + "-" + maxMeta + "]";
+        if (!first) writer.write(",\n");
+        first = false;
+        writer.write(formatEntry(mysteryName, key, namespaceId, id + ":" + mysteryMeta, blockExtra(block, key, i18n)));
+
+        for (final DyeColor color : DyeColor.values()) {
+            final String colorKey = key + "." + color.colorID;
+            final String colorName = i18n.translateKey(colorKey + ".name");
+            writer.write(",\n");
+            writer.write(formatEntry(colorName, colorKey, namespaceId, id + ":" + painted.toMetadata(color),
+                blockExtra(block, colorKey, i18n)));
+        }
+
+        return first;
+    }
+
+    private static @NotNull String formatEntry(@NotNull String name, @NotNull String key, @NotNull String namespaceId, @NotNull String id, @NotNull String extra) {
+        return "\t\t[\"" + escape(name) + "\"] = { \"" + key + "\", \"" + namespaceId + "\", \"" + id + "\", " + extra + " }";
+    }
+
+    private static @NotNull String blockExtra(@NotNull Block<?> block, @NotNull String descKey, @NotNull I18n i18n) {
+        final float hardness = block.getHardness();
+        final float blastResistance = block.getBlastResistance(null);
+        final int miningLevel = ItemToolPickaxe.miningLevels.getOrDefault(block, 0);
+        final int stackSize = block.asItem().getItemStackLimit(null);
+        final String description = i18n.translateKey(descKey + ".desc");
+        final String tags = tagList(BlockTags.TAG_LIST, block);
+        return hardness + ", " + blastResistance + ", " + miningLevel + ", " + stackSize
+                + ", \"" + escape(description) + "\", \"" + escape(tags) + "\"";
+    }
+
+
+    private static @NotNull String itemExtra(@NotNull Item item, @NotNull String descKey, @NotNull I18n i18n) {
+        final int durability = item.getMaxDamage();
+        final int restores = item instanceof ItemFood food ? food.getHealAmount(new ItemStack(item)) : 0;
+        final int stackSize = item.getItemStackLimit(null);
+        final String description = i18n.translateKey(descKey + ".desc");
+        final String tags = tagList(ItemTags.TAG_LIST, item);
+        return durability + ", " + restores + ", " + stackSize
+                + ", \"" + escape(description) + "\", \"" + escape(tags) + "\"";
+    }
+
+    private static <T extends ITaggable<T>> @NotNull String tagList(@NotNull List<Tag<T>> tagList, @NotNull T taggable) {
+        final StringBuilder builder = new StringBuilder();
+        for (final Tag<T> tag : tagList) {
+            if (tag.appliesTo(taggable)) {
+                if (!builder.isEmpty()) builder.append(", ");
+                builder.append(tag.getName());
+            }
+        }
+        return builder.toString();
+    }
+
+    private static @NotNull String escape(@NotNull String value) {
+        return value
+                .replace("\\", "\\\\")
+                .replace("\"", "\\\"")
+                .replace("\r", " ")
+                .replace("\n", " ");
+    }
+}
diff --git a/game/client/src/main/java/net/minecraft/client/Minecraft.java b/game/client/src/main/java/net/minecraft/client/Minecraft.java
index 7ae77976a..943af4200 100644
--- a/game/client/src/main/java/net/minecraft/client/Minecraft.java
+++ b/game/client/src/main/java/net/minecraft/client/Minecraft.java
@@ -1384,7 +1384,7 @@ public class Minecraft
                 if (delta <= 200L) {
                     int scrollDelta = Mouse.getEventDWheel();
                     if (scrollDelta != 0) {
-                        if (this.thePlayer.noPhysics && this.toggleFlyPressed) {
+                        if (this.thePlayer.hasNoPhysics() && this.toggleFlyPressed) {
                             final float steps = 20;
 
                             int step = Math.round(GameSettings.FLIGHT_SPEED.value * steps);
@@ -1415,7 +1415,7 @@ public class Minecraft
             if (this.toggleFlyPressed && !GameSettings.KEY_FLY.isPressed()) {
                 this.toggleFlyPressed = false;
                 if (this.flyPressedTime + 400 > System.currentTimeMillis()) {
-                    this.thePlayer.setNoclip(!this.thePlayer.noPhysics);
+                    this.thePlayer.setNoclip(!this.thePlayer.hasNoPhysics());
                 }
             }
             while (Keyboard.next()) {
@@ -1950,7 +1950,7 @@ public class Minecraft
                 }
             }
             if (controller.digitalPad.left.pressedThisFrame() && this.thePlayer.getGamemode().hasPlayerFlight()) {
-                this.thePlayer.setNoclip(!this.thePlayer.noPhysics);
+                this.thePlayer.setNoclip(!this.thePlayer.hasNoPhysics());
             }
             if (controller.buttonLeftShoulder.pressedThisFrame()) {
                 this.thePlayer.inventory.changeCurrentSlot(1);
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 9c354cfe4..46ca4805d 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
@@ -92,7 +92,7 @@ public class PlayerLocal extends Player {
     }
 
     public @Nullable AABBdc getCollisionAABB() {
-        if (this.noPhysics) {
+        if (this.hasNoPhysics()) {
             return null;
         } else {
             return super.getCollisionAABB();
@@ -117,7 +117,7 @@ public class PlayerLocal extends Player {
         }
         this.inventoryMenu = newContainer;
         if (!gamemode.hasPlayerFlight()) {
-            this.noPhysics = false;
+            setNoclip(false);
         }
 
         this.fireImmune = gamemode.hasFireImmunity();
@@ -144,7 +144,7 @@ public class PlayerLocal extends Player {
         }
 
         this.prevTimeInPortal = this.timeInPortal;
-        if (this.inPortal && !this.noPhysics) {
+        if (this.inPortal && !this.hasNoPhysics()) {
             if (!this.world.isClientSide && this.vehicle != null) {
                 startRiding(null);
             }
@@ -192,7 +192,7 @@ public class PlayerLocal extends Player {
         checkAndPushInTile(this.x - (double) this.bbWidth * 0.35D, this.bb.minY + 0.5D, this.z - (double) this.bbWidth * 0.35D);
         checkAndPushInTile(this.x + (double) this.bbWidth * 0.35D, this.bb.minY + 0.5D, this.z - (double) this.bbWidth * 0.35D);
         checkAndPushInTile(this.x + (double) this.bbWidth * 0.35D, this.bb.minY + 0.5D, this.z + (double) this.bbWidth * 0.35D);
-        boolean canSprint = this.world.getGameRuleValue(GameRules.ALLOW_SPRINTING) && !this.noPhysics;
+        boolean canSprint = this.world.getGameRuleValue(GameRules.ALLOW_SPRINTING) && !this.hasNoPhysics();
         if (this.onGround && !forwardInput && this.input.moveForward >= f && !isSprinting() && canSprint) {
             if (this.sprintTimer == 0) {
                 this.sprintTimer = 7;
@@ -388,12 +388,19 @@ public class PlayerLocal extends Player {
     public void animate4() {
     }
 
+    @Override
+    public void revokeAchievement(Stat statbase) {
+        if (statbase != null && statbase.isAchievement()) {
+            this.mc.statsCounter.remove(statbase);
+        }
+    }
+
     @Override
     public void addStat(@Nullable Stat stat, int i) {
         if (stat == null) {
             return;
         }
-        if (stat.isAchievement()) {
+        if (i > 0 && stat.isAchievement()) {
             Achievement achievement = (Achievement) stat;
             if (achievement.parent == null || this.mc.statsCounter.isUnlocked(achievement.parent)) {
                 if (!this.mc.statsCounter.isUnlocked(achievement)) {
@@ -404,6 +411,7 @@ public class PlayerLocal extends Player {
         } else {
             this.mc.statsCounter.add(stat, i);
         }
+
     }
 
     @Override
@@ -413,7 +421,7 @@ public class PlayerLocal extends Player {
 
     @Override
     protected boolean checkAndPushInTile(double x, double y, double z) {
-        if (this.noPhysics) return false;
+        if (this.hasNoPhysics()) return false;
         final boolean isShort = this.isDwarf || this.isSneaking();
         final int blockX = MathHelper.floor(x);
         final int blockY = MathHelper.floor(y);
diff --git a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java
index b0cc54832..556f24d0d 100644
--- a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java
+++ b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerLocalMultiplayer.java
@@ -150,6 +150,12 @@ public class PlayerLocalMultiplayer extends PlayerLocal implements Player.Player
         }
     }
 
+    @Override
+    public void setNoclip(boolean noclip) {
+        super.setNoclip(noclip);
+        this.sendQueue.addToSendQueue(noclip ? new PacketUpdatePlayerState(PacketUpdatePlayerState.STATE_NO_CLIP) : new PacketUpdatePlayerState(PacketUpdatePlayerState.STATE_DO_CLIP));
+    }
+
     @Override
     public void dropCurrentItem(boolean dropFullStack) {
         sendQueue.addToSendQueue(new PacketPlayerAction(dropFullStack ? PacketPlayerAction.ACTION_DROP_ITEM_STACK : PacketPlayerAction.ACTION_DROP_ITEM_SINGLE, 0, 0, 0, Side.NONE, 0.0, 0.0));
diff --git a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerRemote.java b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerRemote.java
index fc4ce6ff5..5ebb6ca53 100644
--- a/game/client/src/main/java/net/minecraft/client/entity/player/PlayerRemote.java
+++ b/game/client/src/main/java/net/minecraft/client/entity/player/PlayerRemote.java
@@ -31,7 +31,7 @@ public class PlayerRemote extends Player
         this.username = username;
         this.uuid = uuid;
         footSize = 0.0F;
-        noPhysics = true;
+        setNoPhysics(true);
         sleepOffY = 0.25F;
         viewScale = 10D;
         
diff --git a/game/client/src/main/java/net/minecraft/client/gui/TexturedButtonElement.java b/game/client/src/main/java/net/minecraft/client/gui/TexturedButtonElement.java
index f8f270919..9838ff6c8 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/TexturedButtonElement.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/TexturedButtonElement.java
@@ -2,6 +2,7 @@ package net.minecraft.client.gui;
 
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.render.renderer.GLRenderer;
+import net.minecraft.client.render.window.CursorShape;
 
 public class TexturedButtonElement extends ButtonElement {
     private final String texturePath;
@@ -23,6 +24,11 @@ public class TexturedButtonElement extends ButtonElement {
         mc.textureManager.loadTexture(this.texturePath).bind();
         GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
         boolean isHovered = mouseX >= this.xPosition && mouseY >= this.yPosition && mouseX < this.xPosition + this.width && mouseY < this.yPosition + this.height;
+
+        if (isHovered && this.visible && this.enabled) {
+            mc.currentScreen.setDesiredCursor(CursorShape.HAND);
+        }
+
         if (!this.enabled) {
             drawTexturedModalRect(this.xPosition, this.yPosition, this.u, this.v, this.width, this.height);
         } else if (!isHovered) {
diff --git a/game/client/src/main/java/net/minecraft/client/gui/achievements/ScreenAchievements.java b/game/client/src/main/java/net/minecraft/client/gui/achievements/ScreenAchievements.java
index 8485dd9dc..1fef0562c 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/achievements/ScreenAchievements.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/achievements/ScreenAchievements.java
@@ -791,22 +791,31 @@ public class ScreenAchievements extends Screen
         return Math.sin(((double)(System.currentTimeMillis() % period) / period) * Math.PI * 2D) * amplitude;
     }
     private void drawConnectingLines(int mouseX, int mouseY, double shiftX, double shiftY){
+        drawConnectingLinesPass(mouseX, mouseY, shiftX, shiftY, false);
+        drawConnectingLinesPass(mouseX, mouseY, shiftX, shiftY, true);
+    }
+
+    private void drawConnectingLinesPass(int mouseX, int mouseY, double shiftX, double shiftY, boolean unlockedPass){
         double zoom = this.viewportZoom;
 
         for(AchievementPage.AchievementEntry entry : this.currentPage.getAchievementEntries()) {
             Achievement ach = entry.achievement;
-            AchievementPage.AchievementEntry parentEntry = this.currentPage.getEntry(ach.parent);
-            if(parentEntry == null) {
+            Achievement parent = ach.parent;
+            if (parent == null || !this.currentPage.hasAchievement(parent)) {
                 continue;
             }
-            if (!this.currentPage.hasAchievement(ach) || !this.currentPage.hasAchievement(ach.parent)){
+            if (isSecretUndiscovered(ach) || isSecretUndiscovered(parent)) {
+                continue;
+            }
+            boolean unlocked = this.statsCounter.isUnlocked(ach);
+            if (unlocked != unlockedPass) {
                 continue;
             }
+            AchievementPage.AchievementEntry parentEntry = this.currentPage.getEntry(parent);
             double childX = (this.viewportLeft + this.viewportWidth /2d) + ((entry.x * ACHIEVEMENT_CELL_WIDTH - shiftX) + 11) * zoom;
             double childY = (this.viewportTop + this.viewportHeight /2d) + ((entry.y * ACHIEVEMENT_CELL_HEIGHT - shiftY) + 11) * zoom;
             double parentX = (this.viewportLeft + this.viewportWidth /2d) + ((parentEntry.x * ACHIEVEMENT_CELL_WIDTH - shiftX) + 11) * zoom;
             double parentY = (this.viewportTop + this.viewportHeight /2d) + ((parentEntry.y * ACHIEVEMENT_CELL_HEIGHT - shiftY) + 11) * zoom;
-            boolean unlocked = this.statsCounter.isUnlocked(ach);
             boolean canUnlock = this.statsCounter.canUnlock(ach);
 
             final double zoomOff = 11 * zoom;
@@ -848,6 +857,10 @@ public class ScreenAchievements extends Screen
         }
     }
 
+    public boolean isSecretUndiscovered(@NotNull Achievement achievement) {
+        return achievement.getType().hidden() && !this.statsCounter.isUnlocked(achievement);
+    }
+
     @Nullable
     private Achievement drawAchievementIcons(int mouseX, int mouseY, double shiftX, double shiftY){
         double zoom = this.viewportZoom;
@@ -855,6 +868,7 @@ public class ScreenAchievements extends Screen
         Achievement hoveredAchievment = null;
         for(AchievementPage.AchievementEntry entry : this.currentPage.getAchievementEntries()) {
             Achievement ach = entry.achievement;
+            boolean secretUndiscovered = isSecretUndiscovered(ach);
             double achViewX = (this.viewportLeft + this.viewportWidth /2d) + (entry.x * ACHIEVEMENT_CELL_WIDTH - shiftX ) * zoom;
             double achViewY = (this.viewportTop + this.viewportHeight /2d) + (entry.y * ACHIEVEMENT_CELL_HEIGHT - shiftY) * zoom;
             if(achViewX < this.viewportLeft - ACHIEVEMENT_CELL_WIDTH * zoom || achViewY < this.viewportTop - ACHIEVEMENT_CELL_HEIGHT * zoom || achViewX > this.viewportRight || achViewY > this.viewportBottom) { // Continue if outside viewport
@@ -864,7 +878,7 @@ public class ScreenAchievements extends Screen
             if(this.statsCounter.isUnlocked(ach)) {
                 float brightness = 1.0F;
                 GLRenderer.setColor4f(brightness, brightness, brightness, 1.0F);
-            } else if(this.statsCounter.canUnlock(ach)) {
+            } else if(!secretUndiscovered && this.statsCounter.canUnlock(ach)) {
                 // Flicker if can unlock
                 float brightness = timeSin(1, 600) >= 0.6 ? 0.6F : 0.8F;
                 GLRenderer.setColor4f(brightness, brightness, brightness, 1.0F);
@@ -876,23 +890,25 @@ public class ScreenAchievements extends Screen
 
             drawGuiIconDouble(achViewX - (ACHIEVEMENT_ICON_WIDTH - ACHIEVEMENT_CELL_WIDTH) * zoom, achViewY - (ACHIEVEMENT_ICON_HEIGHT - ACHIEVEMENT_CELL_HEIGHT) * zoom, ACHIEVEMENT_ICON_WIDTH * zoom, ACHIEVEMENT_ICON_HEIGHT * zoom, this.currentPage.getAchievementIcon(ach));
 
-            if(!this.statsCounter.canUnlock(ach)) {
+            if(!secretUndiscovered && !this.statsCounter.canUnlock(ach)) {
                 float brightness = 0.1F;
                 GLRenderer.setColor4f(brightness, brightness, brightness, 1.0F);
             }
 
-            GLRenderer.pushFrame();
-            GLRenderer.globalSetLightEnabled(true);
-            GLRenderer.enableState(State.CULL_FACE);
-            ItemStack achievementItem = ach.iconStack;
+            if (!secretUndiscovered) {
+                GLRenderer.pushFrame();
+                GLRenderer.globalSetLightEnabled(true);
+                GLRenderer.enableState(State.CULL_FACE);
+                ItemStack achievementItem = ach.iconStack;
 
-            GLRenderer.modelM4f().translate((float) (achViewX + 3 * zoom), (float) (achViewY + 3 * zoom), 0);
-            GLRenderer.modelM4f().scale((float) zoom, (float) zoom, 1);
-//            ItemModelDispatcher.getInstance().getDispatch(achievementItem).renderItemIntoGui(GLRenderer.getTessellator(), mc.font, mc.textureManager, achievementItem.getDefaultStack(), 0, 0, 1.0f);
-            ItemModelDispatcher.getInstance().getDispatch(achievementItem).renderGui(GLRenderer.getTessellator(), null, achievementItem, 0, 0, LightIndexHelper.lightIndex2i(15, 15), 1f);
+                GLRenderer.modelM4f().translate((float) (achViewX + 3 * zoom), (float) (achViewY + 3 * zoom), 0);
+                GLRenderer.modelM4f().scale((float) zoom, (float) zoom, 1);
+    //            ItemModelDispatcher.getInstance().getDispatch(achievementItem).renderItemIntoGui(GLRenderer.getTessellator(), mc.font, mc.textureManager, achievementItem.getDefaultStack(), 0, 0, 1.0f);
+                ItemModelDispatcher.getInstance().getDispatch(achievementItem).renderGui(GLRenderer.getTessellator(), null, achievementItem, 0, 0, LightIndexHelper.lightIndex2i(15, 15), 1f);
 
-            GLRenderer.globalSetLightEnabled(false);
-            GLRenderer.popFrame();
+                GLRenderer.globalSetLightEnabled(false);
+                GLRenderer.popFrame();
+            }
 
             GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
 
@@ -904,10 +920,21 @@ public class ScreenAchievements extends Screen
         return hoveredAchievment;
     }
     private void drawAchievementToolTip(Achievement achievement, int mouseX, int mouseY){
-        String name = achievement.getStatName();
-        String desc = achievement.getDescription();
         final int padding = this.tooltip.getPadding();
         int[] boxRender;
+        if (isSecretUndiscovered(achievement)) {
+            String name = I18n.getInstance().translateKey("gui.achievements.label.secret");
+            String desc = I18n.getInstance().translateKey("gui.achievements.label.secret.desc");
+            int boxWidth = Math.max(Math.max(MathHelper.ceil(this.fontRenderer.stringWidthDouble(name)), MathHelper.ceil(this.fontRenderer.stringWidthDouble(desc))), TOOLTIP_BOX_WIDTH_MIN);
+            int boxHeight = MathHelper.ceil(this.fontRenderer.heightOfConstrainedChars(desc, boxWidth));
+            boxRender = this.tooltip.drawBackground(mouseX, mouseY, TOOLTIP_OFF_X, TOOLTIP_OFF_Y, boxWidth + 3, boxHeight + 3 + 12);
+            this.fontRenderer.renderWidthConstrained(desc, boxRender[0] + padding, boxRender[1] + 12 + padding, boxWidth).setColor(0xff705050).call();
+            drawStringShadow(this.fontRenderer, name, boxRender[0] + padding, boxRender[1] + padding, achievement.getType().colorNameLocked());
+            return;
+        }
+
+        String name = achievement.getStatName();
+        String desc = achievement.getDescription();
         if(this.statsCounter.canUnlock(achievement)) { // Can unlock/ is unlocked
             int boxWidth = Math.max(MathHelper.ceil(this.fontRenderer.stringWidthDouble(name)), TOOLTIP_BOX_WIDTH_MIN);
             int boxHeight = MathHelper.ceil(this.fontRenderer.heightOfConstrainedChars(desc, boxWidth));
diff --git a/game/client/src/main/java/net/minecraft/client/gui/achievements/data/AchievementPages.java b/game/client/src/main/java/net/minecraft/client/gui/achievements/data/AchievementPages.java
index 9d8b31348..2f5448dad 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/achievements/data/AchievementPages.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/achievements/data/AchievementPages.java
@@ -52,17 +52,39 @@ public class AchievementPages {
         overworldPage.addAchievement(Achievements.TRIPLE_HIT, 11, -2);
 
         netherPage = register(new AchievementPageNether("gui.achievements.page.nether", Blocks.COBBLE_NETHERRACK.getDefaultStack()));
-        netherPage.addAchievement(Achievements.ENTER_NETHER, 2, 0);
-        netherPage.addAchievement(Achievements.GET_NETHERCOAL, 1, 2);
-        netherPage.addAchievement(Achievements.LIGHT_SIGN, 4, 0);
-        netherPage.addAchievement(Achievements.SWIM_NETHER, 3, 2);
-        netherPage.addAchievement(Achievements.HIT_FIREBALL, 3, -2);
-        netherPage.addAchievement(Achievements.SLEEP_NETHER, 1, -2);
 //        netherPage.addAchievement(Achievements.ENTER_WARRENS, 0, 5);
-        netherPage.addAchievement(Achievements.MOST_WANTED, 0, -1);
-        netherPage.addAchievement(Achievements.FAST_TRAVEL, 0, 1);
-        netherPage.addAchievement(Achievements.GET_STEEL_BLAST_FURNACE, 0, 4);
-        netherPage.addAchievement(Achievements.OBTAIN_STEEL, -2, 4);
+
+        netherPage.addAchievement(Achievements.ENTER_NETHER, 0, 0);
+        netherPage.addAchievement(Achievements.MOST_WANTED, -2, 1);
+        netherPage.addAchievement(Achievements.FAST_TRAVEL, 2, 1);
+        netherPage.addAchievement(Achievements.DISCOVER_SULFURPOOLS, 4, 0);
+        netherPage.addAchievement(Achievements.COLLECT_ACID, 5, 2);
+        netherPage.addAchievement(Achievements.CONVERT_ACID_TO_SULFUR,  7, 2);
+        netherPage.addAchievement(Achievements.CRAFT_ACID_COBBLE_TO_STONE, 6, 1);
+        netherPage.addAchievement(Achievements.CONVERT_ACID_TO_STONE, 6, 3);
+        netherPage.addAchievement(Achievements.ACID_BATH, 7, -1);
+        netherPage.addAchievement(Achievements.LIGHT_SIGN, 2, -1);
+        netherPage.addAchievement(Achievements.DISCOVER_VOLCANICISLANDS, -4, 0);
+        netherPage.addAchievement(Achievements.FLING_VENT, -4, 2);
+        netherPage.addAchievement(Achievements.WHAT_WAS_THAT, -6, 1);
+        netherPage.addAchievement(Achievements.COLLECT_PUMICE, -6, -1);
+        netherPage.addAchievement(Achievements.GET_NETHERCOAL, -1, -2);
+        netherPage.addAchievement(Achievements.GET_STEEL_BLAST_FURNACE, -3, -3);
+        netherPage.addAchievement(Achievements.OBTAIN_STEEL, -5, -3);
+        netherPage.addAchievement(Achievements.CRAFT_STEEL_BUCKET, -6, -4);
+        netherPage.addAchievement(Achievements.DISCOVER_CRYSTALFOREST, 0, 4);
+        netherPage.addAchievement(Achievements.COLLECT_RUBYGLASS, 2, 4);
+        netherPage.addAchievement(Achievements.CRAFT_CONDUIT, 3, 3);
+        netherPage.addAchievement(Achievements.SWIM_NETHER, -2, 3);
+        netherPage.addAchievement(Achievements.DISCOVER_OLDWORLD, 0, -4);
+        netherPage.addAchievement(Achievements.TRIGGER_EMBER, -2, -5);
+        netherPage.addAchievement(Achievements.HIT_FIREBALL, 2, -3);
+        netherPage.addAchievement(Achievements.SLEEP_NETHER, 4, -4);
+
+
+
+
+
     }
     static {
         init();
diff --git a/game/client/src/main/java/net/minecraft/client/gui/achievements/pages/AchievementPageNether.java b/game/client/src/main/java/net/minecraft/client/gui/achievements/pages/AchievementPageNether.java
index 3353e308d..96da2d4e7 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/achievements/pages/AchievementPageNether.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/achievements/pages/AchievementPageNether.java
@@ -51,7 +51,7 @@ public class AchievementPageNether extends AchievementPage {
         final int offsetY = tileY + random.nextInt(3) - random.nextInt(3);
 
         int r = random.nextInt(50);
-        IconCoordinate texture = getTextureFromBlock(Blocks.COBBLE_NETHERRACK);
+        IconCoordinate texture = random.nextBoolean() ? getTextureFromBlock(Blocks.NETHERRACK) : getTextureFromBlock(Blocks.COBBLE_NETHERRACK);
         if (offsetY >= bedrockLayer) {
             texture = getTextureFromBlock(Blocks.BEDROCK);
         } else if (r < 10) {
@@ -86,6 +86,15 @@ public class AchievementPageNether extends AchievementPage {
                 cave--;
             }
 
+        } else if (layerCache.id == 1) {
+            // Remove covered icons so the deeper layer only shows through carved caves
+            IconCoordinate[] foreground = screen.getLayer(0).getData();
+            IconCoordinate[] background = layerCache.getData();
+            for (int i = 0; i < background.length; i++) {
+                if (foreground[i] != null){
+                    background[i] = null;
+                }
+            }
         }
     }
 
@@ -96,7 +105,7 @@ public class AchievementPageNether extends AchievementPage {
 
     @Override
     public int backgroundLayers() {
-        return 1;
+        return 2;
     }
 
     @Override
diff --git a/game/client/src/main/java/net/minecraft/client/gui/container/ScreenFlagEditor.java b/game/client/src/main/java/net/minecraft/client/gui/container/ScreenFlagEditor.java
index 9bef9bdc4..1f646c930 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/container/ScreenFlagEditor.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/container/ScreenFlagEditor.java
@@ -225,7 +225,7 @@ public class ScreenFlagEditor
             this.colorLabels[this.selectedColor].setTextColor(0xFF404040);
             this.colorLabels[this.selectedColor].setShadow(false);
             this.selectedColor = button.id - 100;
-            this.colorLabels[this.selectedColor].setTextColor(0xFFFFFFF);
+            this.colorLabels[this.selectedColor].setTextColor(0xFFFFFFFF);
             this.colorLabels[this.selectedColor].setShadow(true);
         }
     }
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 6274f1555..e0b69fe01 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
@@ -191,12 +191,12 @@ public class RecipePageCrafting
         for (SlotGuidebook slot : this.slots) {
             if (getIsMouseOverSlot(slot, x, y, mouseX, mouseY)) mouseOverSlot = slot;
             GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
-            if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
-                boolean showDescription = DescriptionPromptEnum.showDescription();
-                String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
-                if (!str.isEmpty()) {
-                    this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
-                }
+        }
+        if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
+            boolean showDescription = DescriptionPromptEnum.showDescription();
+            String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
+            if (!str.isEmpty()) {
+                this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
             }
         }
     }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/mobs/GuidebookPageMob.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/mobs/GuidebookPageMob.java
index 1d6fae08b..12ac8a0f4 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/mobs/GuidebookPageMob.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/mobs/GuidebookPageMob.java
@@ -280,12 +280,12 @@ public class GuidebookPageMob
         for (SlotGuidebook slot : this.slots) {
             if (getIsMouseOverSlot(slot, x, y, mouseX, mouseY)) mouseOverSlot = slot;
             GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
-            if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
-                boolean showDescription = DescriptionPromptEnum.showDescription();
-                String str = tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
-                if (!str.isEmpty()) {
-                    tooltipElement.render(str, mouseX, mouseY, 8, -8);
-                }
+        }
+        if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
+            boolean showDescription = DescriptionPromptEnum.showDescription();
+            String str = tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
+            if (!str.isEmpty()) {
+                tooltipElement.render(str, mouseX, mouseY, 8, -8);
             }
         }
 
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageBlastSmelting.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageBlastSmelting.java
index f9b070c9d..a29be00b7 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageBlastSmelting.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageBlastSmelting.java
@@ -149,12 +149,12 @@ public class RecipePageBlastSmelting
         for (SlotGuidebook slot : this.slots) {
             if (getIsMouseOverSlot(slot, x, y, mouseX, mouseY)) mouseOverSlot = slot;
             GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
-            if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
-                boolean showDescription = DescriptionPromptEnum.showDescription();
-                String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
-                if (!str.isEmpty()) {
-                    this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
-                }
+        }
+        if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
+            boolean showDescription = DescriptionPromptEnum.showDescription();
+            String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
+            if (!str.isEmpty()) {
+                this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
             }
         }
     }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageSmelting.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageSmelting.java
index 73c390edb..a5758ed29 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageSmelting.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/smelting/RecipePageSmelting.java
@@ -144,12 +144,12 @@ public class RecipePageSmelting
         for (SlotGuidebook slot : this.slots) {
             if (getIsMouseOverSlot(slot, x, y, mouseX, mouseY)) mouseOverSlot = slot;
             GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
-            if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
-                boolean showDescription = DescriptionPromptEnum.showDescription();
-                String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
-                if (!str.isEmpty()) {
-                    this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
-                }
+        }
+        if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
+            boolean showDescription = DescriptionPromptEnum.showDescription();
+            String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
+            if (!str.isEmpty()) {
+                this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
             }
         }
     }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/guidebook/trommeling/RecipePageTrommel.java b/game/client/src/main/java/net/minecraft/client/gui/guidebook/trommeling/RecipePageTrommel.java
index e32768445..b34d35a68 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/guidebook/trommeling/RecipePageTrommel.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/guidebook/trommeling/RecipePageTrommel.java
@@ -191,12 +191,12 @@ public class RecipePageTrommel
         for (SlotGuidebook slot : this.slots) {
             if (getIsMouseOverSlot(slot, x, y, mouseX, mouseY)) mouseOverSlot = slot;
             GLRenderer.setColor4f(1.0F, 1.0F, 1.0F, 1.0F);
-            if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
-                boolean showDescription = DescriptionPromptEnum.showDescription();
-                String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
-                if (!str.isEmpty()) {
-                    this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
-                }
+        }
+        if (mouseOverSlot != null && mouseOverSlot.hasItem()) {
+            boolean showDescription = DescriptionPromptEnum.showDescription();
+            String str = this.tooltipElement.getTooltipText(mouseOverSlot.getItemStack(), showDescription, mouseOverSlot);
+            if (!str.isEmpty()) {
+                this.tooltipElement.render(str, mouseX, mouseY, 8, -8);
             }
         }
     }
diff --git a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/ScreenModelViewer.java b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/ScreenModelViewer.java
index f93d80aea..05f044b47 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/ScreenModelViewer.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/ScreenModelViewer.java
@@ -86,7 +86,7 @@ public class ScreenModelViewer extends ScreenPhotoMode implements ListenerTextFi
         this.mc.currentWorld.entities.clear();
         this.mc.currentWorld.tileEntityList.clear();
         this.mc.thePlayer.absMoveTo(0.5, 64.5, 0.5, 180, 0);
-        this.mc.thePlayer.noPhysics = true;
+        this.mc.thePlayer.setNoclip(true);
         this.mc.textureManager.refreshTexturesAndDisplayErrors();
         this.mc.textureManager.updateDynamicTextures(false);
 
diff --git a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryEntity.java b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryEntity.java
index ad43af76d..60f1217b3 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryEntity.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/modelviewer/categories/ViewerCategoryEntity.java
@@ -195,7 +195,7 @@ public class ViewerCategoryEntity extends ModelViewerCategory<ModelViewerCategor
         removeDisplayEntity();
         displayEntity = entity;
         if (displayEntity != null){
-            displayEntity.noPhysics = true;
+            displayEntity.setNoPhysics(true);
 
             displayEntity.absMoveTo(mc.thePlayer.x, mc.thePlayer.y + mc.thePlayer.getHeadHeight() - displayEntity.bbHeight/2, mc.thePlayer.z, displayEntity.yRot, displayEntity.xRot);
             displayEntity.lerpMotion(0, 0, 0);
diff --git a/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java b/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java
index 1481a19ca..d2dbd742c 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/options/data/OptionsPages.java
@@ -3,6 +3,10 @@ package net.minecraft.client.gui.options.data;
 import com.mojang.logging.LogUtils;
 import com.mojang.nbt.NbtIo;
 import net.betterthanadventure.utils.DataDumper;
+import net.betterthanadventure.utils.DumpFolder;
+import net.betterthanadventure.utils.InvSpriteAPNGDumper;
+import net.betterthanadventure.utils.InvSpriteDumper;
+import net.betterthanadventure.utils.WikiDumper;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.gui.ScreenControllerSetup;
 import net.minecraft.client.gui.ScreenFontTest;
@@ -39,12 +43,12 @@ import net.minecraft.core.block.Blocks;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.item.Items;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.useless.seedviewer.SeedViewerMain;
 import paulscode.sound.SoundSystem;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.List;
 
 
@@ -417,9 +421,61 @@ public abstract class OptionsPages {
                 }))
                 .withComponent(new ShortcutComponent("gui.options.page.debug.button.open.datadumper", () -> {
                     DataDumper.main(new String[0]);
-                    FileOpener.open(new File(mc.getMinecraftDir(), "infodump"));
+                    FileOpener.open(new File(mc.getMinecraftDir(), DumpFolder.NAME));
                     Global.accessor = mc;
                 }))
+                .withComponent(new ShortcutComponent("gui.options.page.debug.button.dump.wiki", () -> {
+                    try {
+                        WikiDumper.dump(mc.getMinecraftDir());
+                        FileOpener.open(new File(mc.getMinecraftDir(), DumpFolder.NAME));
+                    } catch (IOException e) {
+                        LogUtils.getLogger().error("Failed to write wiki dump", e);
+                    }
+                }))
+                .withComponent(new ShortcutComponent("gui.options.page.debug.button.dump.invsprite", () -> {
+                    if (mc.currentWorld == null) return; // Biome/season tints only resolve inside a world
+                    try {
+                        FileOpener.open(InvSpriteDumper.dump(mc.getMinecraftDir()));
+                    } catch (IOException e) {
+                        LogUtils.getLogger().error("Failed to write inventory sprite dump", e);
+                    }
+                }) {
+                    private boolean hovered = false;
+
+                    @Override
+                    public void render(@NotNull ScreenOptions screenOptions, final int x, final int y, final int width, final int relativeMouseX, final int relativeMouseY) {
+                        this.button.enabled = mc.currentWorld != null;
+                        this.hovered = relativeMouseX >= 0 && relativeMouseX <= width && relativeMouseY >= 0 && relativeMouseY <= getHeight();
+                        super.render(screenOptions, x, y, width, relativeMouseX, relativeMouseY);
+                    }
+
+                    @Override
+                    public @Nullable String getTooltipTranslationKey() {
+                        return (this.hovered && mc.currentWorld == null) ? "gui.options.page.debug.button.dump.invsprite.tooltip" : null;
+                    }
+                })
+                .withComponent(new ShortcutComponent("gui.options.page.debug.button.dump.invsprite_apng", () -> {
+                    if (mc.currentWorld == null) return; // Biome/season tints only resolve inside a world.
+                    try {
+                        FileOpener.open(InvSpriteAPNGDumper.dump(mc.getMinecraftDir()));
+                    } catch (IOException e) {
+                        LogUtils.getLogger().error("Failed to write animated inventory sprite dump", e);
+                    }
+                }) {
+                    private boolean hovered = false;
+
+                    @Override
+                    public void render(@NotNull ScreenOptions screenOptions, final int x, final int y, final int width, final int relativeMouseX, final int relativeMouseY) {
+                        this.button.enabled = mc.currentWorld != null;
+                        this.hovered = relativeMouseX >= 0 && relativeMouseX <= width && relativeMouseY >= 0 && relativeMouseY <= getHeight();
+                        super.render(screenOptions, x, y, width, relativeMouseX, relativeMouseY);
+                    }
+
+                    @Override
+                    public @Nullable String getTooltipTranslationKey() {
+                        return (this.hovered && mc.currentWorld == null) ? "gui.options.page.debug.button.dump.invsprite.tooltip" : null;
+                    }
+                })
                 .withComponent(new ShortcutComponent("gui.options.page.debug.button.open.achievements", () -> mc.displayScreen(new ScreenAchievements(mc.currentScreen, mc.statsCounter))))
             )
             .withComponent(new OptionsCategory("gui.options.page.debug.category.test")
diff --git a/game/client/src/main/java/net/minecraft/client/gui/popup/ListComponent.java b/game/client/src/main/java/net/minecraft/client/gui/popup/ListComponent.java
index 9cecac3bd..ad2554f33 100644
--- a/game/client/src/main/java/net/minecraft/client/gui/popup/ListComponent.java
+++ b/game/client/src/main/java/net/minecraft/client/gui/popup/ListComponent.java
@@ -1,9 +1,11 @@
 package net.minecraft.client.gui.popup;
 
+import net.minecraft.client.gui.TextFieldElement;
 import net.minecraft.client.render.Scissor;
 import net.minecraft.client.render.renderer.GLRenderer;
 import net.minecraft.client.render.renderer.Shaders;
 import net.minecraft.client.render.tessellator.TessellatorGeneral;
+import net.minecraft.core.lang.I18n;
 import net.minecraft.core.sound.SoundCategory;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -17,15 +19,27 @@ import net.minecraft.core.util.helper.MathHelper;
 public class ListComponent
     extends InteractivePopupComponent<Integer> {
     private final @NotNull Minecraft minecraft = Minecraft.getMinecraft();
+
     private int selectedItem;
     private float scrollAmount = 0;
-    private final @NotNull ButtonElement @NotNull [] buttons;
+
+    private final @NotNull ButtonElement @NotNull [] unfilteredButtons;
+    private final @NotNull String @Nullable [] unfilteredButtonTooltips;
+
+    // These hold the buttons after the filter is applied. You SHOULD use these.
+    private ButtonElement[] filteredButtons = null;
+    private int[] filteredButtonsIndexes = null;
+
+    // permanent buffer for the search so we avoid allocating. Do NOT use.
+    private final @NotNull ButtonElement @NotNull [] temporaryFilterButtonBuffer;
+    private final int @NotNull [] temporaryFilterIndexBuffer;
+
     private final int height;
     private int lastX = 0;
     private int lastY = 0;
     private final boolean closeOnItemSelected;
     private final @NotNull TooltipElement tooltip;
-    private final @NotNull String @Nullable [] buttonTooltips;
+
 
     private int scrollbarX;
     private int scrollbarY;
@@ -35,6 +49,8 @@ public class ListComponent
     private int clickY;
     private float previousScrollAmount = 0.0f;
 
+    private TextFieldElement searchBar = null;
+
     public ListComponent(
         final int width,
         final int height,
@@ -47,25 +63,67 @@ public class ListComponent
 
         this.height = height;
         this.closeOnItemSelected = closeOnItemSelected;
+
         assert this.minecraft != null;
+
         this.tooltip = new TooltipElement(this.minecraft);
+        this.searchBar = new TextFieldElement(this.getParent(), this.minecraft.font, 0, 0, this.getWidth(), 20, "", I18n.getInstance().translateKey("gui.inventory.creative.label.search"));
 
         this.selectedItem = selectedItem;
-        this.buttonTooltips = buttonTooltips;
+        this.unfilteredButtonTooltips = buttonTooltips;
 
-        this.buttons = new ButtonElement[buttonLabels.length];
-        for (int i = 0; i < this.buttons.length; i++) {
-            this.buttons[i] = new ButtonElement(i, 0, 0, width - 2 - 6, 20, buttonLabels[i]);
+        this.unfilteredButtons = new ButtonElement[buttonLabels.length];
+        for (int i = 0; i < this.unfilteredButtons.length; i++) {
+            this.unfilteredButtons[i] = new ButtonElement(i, 0, 0, width - 2 - 6, 20, buttonLabels[i]);
             if (i == selectedItem) {
-                this.buttons[i].enabled = false;
+                this.unfilteredButtons[i].enabled = false;
             }
         }
 
         // Scroll so selected item is in view
         final float ratio = (float) (this.getScrollableHeight() + this.height) / this.getScrollableHeight();
-        float sc = (selectedItem / (float) (this.buttons.length - 4)) * ratio;
+        float sc = (selectedItem / (float) (this.unfilteredButtons.length - 4)) * ratio;
         sc -= ((float) this.height / this.getScrollableHeight());
         this.scroll(sc);
+
+        this.temporaryFilterButtonBuffer = new ButtonElement[this.unfilteredButtons.length];
+        this.temporaryFilterIndexBuffer  = new int[this.unfilteredButtons.length];
+
+        updateButtonFilter();
+        this.searchBar.setTextChangeListener(textFieldElement -> updateButtonFilter());
+    }
+
+    // I doubt this is a very good filter allocation wise. But this isn't performance critical anyway.
+    // - kheppy
+    private boolean filterString(String filter, String input) {
+        return input.toLowerCase().contains(filter.toLowerCase());
+    }
+
+    public void updateButtonFilter() {
+        int count = 0;
+
+        for (int i = 0; i < this.unfilteredButtons.length; i++) {
+            var btn = this.unfilteredButtons[i];
+
+            if (filterString(this.searchBar.getText(), btn.displayString)) {
+                temporaryFilterButtonBuffer[count] = btn;
+                temporaryFilterIndexBuffer[count] = i;
+                count += 1;
+            }
+        }
+
+        if (this.filteredButtons == null || this.filteredButtons.length != count) {
+            this.scrollAmount = 0;
+            this.filteredButtons = new ButtonElement[count];
+            this.filteredButtonsIndexes = new int[count];
+        }
+
+        System.arraycopy(temporaryFilterButtonBuffer, 0, this.filteredButtons, 0, count);
+        System.arraycopy(temporaryFilterIndexBuffer, 0, this.filteredButtonsIndexes, 0, count);
+    }
+
+    private int getSearchBarOffset() {
+        return this.searchBar.height + 5;
     }
 
     private boolean canScroll() {
@@ -95,32 +153,39 @@ public class ListComponent
         this.lastX = x;
         this.lastY = y;
 
+        this.searchBar.xPosition = x;
+        this.searchBar.yPosition = y;
+        this.searchBar.drawTextBox();
+        this.searchBar.updateCursor(Minecraft.getMinecraft(), mouseX, mouseY);
+
         // Do scroll
         if (this.canScroll() && this.mouseInRegion(x, y, mouseX, mouseY)) {
             final float wheel = Mouse.getDWheel();
             if (wheel != 0.0f)
                 this.scroll(wheel / -12.0f);
         }
+
         GLRenderer.pushFrame();
         GLRenderer.setShader(Shaders.COLOR);
-        this.drawBackground(x, y);
-        this.drawScrollbar(x, y, mouseY);
+        this.drawBackground(x, y + getSearchBarOffset());
+        this.drawScrollbar(x, y + getSearchBarOffset(), mouseY);
 
         GLRenderer.popFrame();
 
-        Scissor.enable(x + 1, y + 1, this.getWidth() - 2, this.getHeight() - 2);
+        Scissor.enable(x + 1, y + getSearchBarOffset() + 1, this.getWidth() - 2, this.getHeight() - 2 - getSearchBarOffset());
 
         // Draw buttons
-        for (int i = 0; i < this.buttons.length; i++) {
+        for (int i = 0; i < this.filteredButtons.length; ++i) {
             if (this.canScroll()) {
-                this.buttons[i].width = this.getWidth() - 2 - 6;
+                this.filteredButtons[i].width = this.getWidth() - 2 - 6;
             } else {
-                this.buttons[i].width = this.getWidth() - 2;
+                this.filteredButtons[i].width = this.getWidth() - 2;
             }
 
-            final ButtonElement button = this.buttons[i];
+            ButtonElement button = this.filteredButtons[i];
             button.xPosition = x + 1;
-            button.yPosition = (y + 1) + (i * 20) - this.getScrollPixels();
+            button.yPosition = (y + getSearchBarOffset() + 1) + i * 20 - this.getScrollPixels();
+
             if (this.mouseInRegion(x, y, mouseX, mouseY)) {
                 button.drawButton(this.minecraft, mouseX, mouseY);
             } else {
@@ -130,13 +195,17 @@ public class ListComponent
 
         Scissor.disable();
 
-        if (this.buttonTooltips != null && this.mouseInRegion(x, y, mouseX, mouseY)) {
-            for (int i = 0; i < this.buttons.length; i++) {
-                final ButtonElement button = this.buttons[i];
+        if (this.unfilteredButtonTooltips != null && this.mouseInRegion(x, y, mouseX, mouseY)) {
+
+            for(int i = 0; i < this.filteredButtons.length; ++i) {
+                ButtonElement button = this.filteredButtons[i];
+
                 if (button.isHovered(mouseX, mouseY)) {
-                    this.tooltip.render(this.buttonTooltips[i], mouseX, mouseY, 0, 0);
+                    final int buttonIndex = this.filteredButtonsIndexes[i];
+                    this.tooltip.render(this.unfilteredButtonTooltips[buttonIndex], mouseX, mouseY, 0, 0);
                     break;
                 }
+
             }
         }
     }
@@ -146,17 +215,19 @@ public class ListComponent
     }
 
     private int getScrollPixels() {
-        return (int) (this.scrollAmount * (this.getScrollableHeight() - (this.height - 2)));
+        return (int) (this.scrollAmount * (this.getScrollableHeight() - (this.height - 2 - this.getSearchBarOffset())));
     }
 
     private int getScrollableHeight() {
-        return 20 * this.buttons.length;
+        return 20 * (this.filteredButtons != null ? this.filteredButtons.length : this.unfilteredButtons.length);
     }
 
     @Override
     public void onClick(final int x, final int y, final int button) {
         super.onClick(x, y, button);
 
+        this.searchBar.mouseClicked(x, y, button);
+
         final int scrollableHeight = this.getScrollableHeight();
         final int displayRegionHeight = this.height - 2;
         final boolean canScroll = scrollableHeight > displayRegionHeight;
@@ -169,9 +240,9 @@ public class ListComponent
         }
 
         if (this.mouseInRegion(this.lastX, this.lastY, x, y)) {
-            for (final ButtonElement b : this.buttons) {
+            for (final ButtonElement b : this.filteredButtons) {
                 if (b.mouseClicked(this.minecraft, x, y)) {
-                    for (final ButtonElement b2 : this.buttons) {
+                    for (final ButtonElement b2 : this.filteredButtons) {
                         b2.enabled = true;
                     }
 
@@ -191,6 +262,18 @@ public class ListComponent
         }
     }
 
+    @Override
+    public void onKeyDown(int keyCode, char c) {
+        super.onKeyDown(keyCode, c);
+        this.searchBar.textboxKeyTyped(c, keyCode);
+    }
+
+    @Override
+    public void tick() {
+        super.tick();
+        searchBar.updateCursorCounter();
+    }
+
     @Override
     public void mouseMovedOrUp(final int x, final int y, final int button) {
         super.mouseMovedOrUp(x, y, button);
@@ -205,15 +288,15 @@ public class ListComponent
         final @NotNull TessellatorGeneral tessellator = GLRenderer.getTessellator();
         tessellator.startDrawingQuads();
         tessellator.setColorOpaque1i(0xA0A0A0);
-        tessellator.addVertex(x, y + this.height, 0.0D);
-        tessellator.addVertex(x + this.getWidth(), y + this.height, 0.0D);
+        tessellator.addVertex(x, y + this.height - this.getSearchBarOffset(), 0.0D);
+        tessellator.addVertex(x + this.getWidth(), y + this.height - this.getSearchBarOffset(), 0.0D);
         tessellator.addVertex(x + this.getWidth(), y, 0.0D);
         tessellator.addVertex(x, y, 0.0D);
         tessellator.draw();
         tessellator.startDrawingQuads();
         tessellator.setColorOpaque1i(0x000000);
-        tessellator.addVertex(x + 1, y + this.height - 1, 0.0D);
-        tessellator.addVertex(x + this.getWidth() - 1, y + this.height - 1, 0.0D);
+        tessellator.addVertex(x + 1, y + this.height - this.getSearchBarOffset() - 1, 0.0D);
+        tessellator.addVertex(x + this.getWidth() - 1, y + this.height - this.getSearchBarOffset() - 1, 0.0D);
         tessellator.addVertex(x + this.getWidth() - 1, y + 1, 0.0D);
         tessellator.addVertex(x + 1, y + 1, 0.0D);
         tessellator.draw();
@@ -221,7 +304,7 @@ public class ListComponent
 
     private void drawScrollbar(final int x, final int y, final int mouseY) {
         final int scrollableHeight = this.getScrollableHeight();
-        final int displayRegionHeight = this.height - 2;
+        final int displayRegionHeight = this.height - 2 - this.getSearchBarOffset();
 
         if (!this.canScroll()) {
             return;
diff --git a/game/client/src/main/java/net/minecraft/client/input/PlayerInput.java b/game/client/src/main/java/net/minecraft/client/input/PlayerInput.java
index 830b7e6a7..5546ff18b 100644
--- a/game/client/src/main/java/net/minecraft/client/input/PlayerInput.java
+++ b/game/client/src/main/java/net/minecraft/client/input/PlayerInput.java
@@ -38,7 +38,7 @@ public class PlayerInput {
 		}
 		
 		if(GameSettings.KEY_SNEAK.isKeyOrMouse(keyCode, mouseCode)) {
-			if(GameSettings.SNEAK_TOGGLE.value && !this.mc.thePlayer.noPhysics) {
+			if(GameSettings.SNEAK_TOGGLE.value && !this.mc.thePlayer.hasNoPhysics()) {
 				if(pressed) {
                     this.sneak = !this.sneak;
 				}
@@ -99,7 +99,7 @@ public class PlayerInput {
             this.moveForward = -this.mc.controllerInput.joyLeft.getY();
             this.moveStrafe = -this.mc.controllerInput.joyLeft.getX();
             this.jump = this.mc.controllerInput.buttonA.isPressed();
-			if (this.mc.thePlayer.noPhysics) {
+			if (this.mc.thePlayer.hasNoPhysics()) {
                 this.sneak = this.mc.controllerInput.joyRight.getButton().isPressed();
 			} else {
 				if(this.mc.controllerInput.joyRight.getButton().pressedThisFrame()) {
@@ -117,7 +117,7 @@ public class PlayerInput {
         this.moveForward = MathHelper.clamp(this.moveForward, -1, 1);
         this.moveStrafe = MathHelper.clamp(this.moveStrafe, -1, 1);
 
-		if(entityplayer.isSneaking() && !entityplayer.noPhysics) {
+		if(entityplayer.isSneaking() && !entityplayer.hasNoPhysics()) {
             this.moveStrafe *= 0.3f;
             this.moveForward *= 0.3f;
 		}
diff --git a/game/client/src/main/java/net/minecraft/client/net/handler/PacketHandlerClient.java b/game/client/src/main/java/net/minecraft/client/net/handler/PacketHandlerClient.java
index aec3987af..47d2bc3f8 100644
--- a/game/client/src/main/java/net/minecraft/client/net/handler/PacketHandlerClient.java
+++ b/game/client/src/main/java/net/minecraft/client/net/handler/PacketHandlerClient.java
@@ -1022,6 +1022,10 @@ public class PacketHandlerClient extends PacketHandler {
             return;
         }
         if (stat.clientside) return; // Don't allow server to give client side only stats
+        if (packetStatistic.valueChange < 0 && stat.isAchievement()) {
+            this.mc.thePlayer.revokeAchievement(stat);
+            return;
+        }
         this.mc.thePlayer.addStat(stat, packetStatistic.valueChange);
     }
 
diff --git a/game/client/src/main/java/net/minecraft/client/render/ItemRenderer.java b/game/client/src/main/java/net/minecraft/client/render/ItemRenderer.java
index ba93e9343..4fdeb33d1 100644
--- a/game/client/src/main/java/net/minecraft/client/render/ItemRenderer.java
+++ b/game/client/src/main/java/net/minecraft/client/render/ItemRenderer.java
@@ -147,7 +147,7 @@ public class ItemRenderer {
             renderFireInFirstPerson(partialTicks);
         }
 
-        if (this.mc.thePlayer.isInWall() && !this.mc.thePlayer.noPhysics) {
+        if (this.mc.thePlayer.isInWall() && !this.mc.thePlayer.hasNoPhysics()) {
             TilePos blockPos = this.mc.activeCamera.getTilePos(partialTicks);
 
             TextureRegistry.worldAtlas.bind();
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 3ae20b3f5..3fe26f5b6 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
@@ -1115,7 +1115,7 @@ public final class RenderGlobal
 
         final int pendingChunkSize = this.dirtyChunks.size();
 
-        float nearCheck = camera instanceof EntityCamera entityCamera && entityCamera.mob.noPhysics ? 16 : 2048;
+        float nearCheck = camera instanceof EntityCamera entityCamera && entityCamera.mob.hasNoPhysics() ? 16 : 2048;
         for (int i = 0; i < pendingChunkSize; i++) {
             final ChunkRenderer renderer = this.dirtyChunks.get(i);
             if (renderer.distanceToCameraSquared(camera) > nearCheck) {
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 e28df35d0..c2507eac2 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
@@ -62,13 +62,13 @@ public class ItemDragHandler extends Gui {
 			this.dragOverBuffer.clear();
 			if (Math.max(this.lastX, this.lastY) == Integer.MAX_VALUE) {
 				final var slot = this.container.getSlotAtPosition(mouseX, mouseY);
-				if (slot != null) dragOverBuffer.add(slot);
+				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;
-			if (this.dragOverBuffer.size() == 0) this.onDragOutsideOfSlots();
+			if (this.dragOverBuffer.isEmpty()) this.onDragOutsideOfSlots();
 			else for (final var slot : this.dragOverBuffer) onDragOverSlot(slot);
 
 			if (this.draggedSlots.size() <= 1) return;
@@ -85,15 +85,16 @@ public class ItemDragHandler extends Gui {
 			}else {
 				this.renderItemStackCount = this.draggingItemStack.stackSize / this.draggedSlots.size();
 			}
-			
+
+			int depositedCount = 0;
 			for(int i = 0; i < this.draggedSlots.size(); i++) {
-				drawSlotOverlayWhileDragging(this.draggedSlots.get(i));
+				depositedCount += drawSlotOverlayWhileDragging(this.draggedSlots.get(i));
 			}
 			
 			if(isCreativeDragging) {
 				this.renderItemStack.stackSize = 1;
 			}else {
-				this.renderItemStack.stackSize = this.draggingItemStack.stackSize - (this.draggedSlots.size() * this.renderItemStackCount);
+				this.renderItemStack.stackSize = this.draggingItemStack.stackSize - depositedCount;
 			}
 		}
 	}
@@ -117,7 +118,7 @@ public class ItemDragHandler extends Gui {
 
 	protected void onDragOutsideOfSlots() {}
 	
-	private void drawSlotOverlayWhileDragging(Slot slot) {
+	private int drawSlotOverlayWhileDragging(Slot slot) {
 		if(slot == null) throw new NullPointerException("Slot is null");
 		if(this.draggingItemStack == null) throw new NullPointerException("Stack is null!");
 
@@ -135,7 +136,9 @@ public class ItemDragHandler extends Gui {
 		if(stack != null) {
             this.renderItemStack.stackSize = Math.min(this.renderItemStack.getMaxStackSize(), this.renderItemStack.stackSize + stack.stackSize);
 		}
+		this.renderItemStack.stackSize = Math.min(this.renderItemStack.stackSize, slot.getMaxStackSize());
         this.itemElement.render(this.renderItemStack, slot.x, slot.y);
+		return this.renderItemStack.stackSize - (stack != null ? stack.stackSize : 0);
 	}
 	
 	public void mouseEvent(final int x, final int y, int button, boolean pressed) {
diff --git a/game/client/src/main/resources/credits.txt b/game/client/src/main/resources/credits.txt
index 2c4c38d9e..f8d77498e 100644
--- a/game/client/src/main/resources/credits.txt
+++ b/game/client/src/main/resources/credits.txt
@@ -65,6 +65,7 @@ kheprep
 --Pumpkin pie textures
 --Pumpkin crop bush texture
 --Assistance with bug fixes
+--Popup component search bar
 Luke (@loquatdev)
 --Assistance with extended controller support
 LukeisStuff
diff --git a/game/core/src/main/java/net/minecraft/core/achievement/Achievements.java b/game/core/src/main/java/net/minecraft/core/achievement/Achievements.java
index 83e08154c..79eaa680a 100644
--- a/game/core/src/main/java/net/minecraft/core/achievement/Achievements.java
+++ b/game/core/src/main/java/net/minecraft/core/achievement/Achievements.java
@@ -31,7 +31,7 @@ public abstract class Achievements
             .registerAchievement();
     public static Achievement DOWN_THE_DRAIN =
             new Achievement(NamespaceID.fromPool("minecraft", "down_the_drain"), "downTheDrain", Blocks.MESH, ACQUIRE_IRON)
-                    .registerAchievement();
+            .registerAchievement();
     public static Achievement GET_DIAMONDS =
         new Achievement(NamespaceID.fromPool("minecraft", "get_diamonds"), "getDiamonds", Items.DIAMOND, ACQUIRE_IRON)
             .setType(Achievement.TYPE_SPECIAL)
@@ -118,35 +118,95 @@ public abstract class Achievements
 
     // Nether achievements
     public static Achievement ENTER_NETHER =
-        new Achievement(NamespaceID.fromPool("minecraft", "enter_nether"), "enterNether", Blocks.OBSIDIAN, null)
+        new Achievement(NamespaceID.fromPool("minecraft", "enter_nether"), "enterNether", Blocks.NETHERRACK, null)
             .setType(Achievement.TYPE_SPECIAL)
             .registerAchievement();
     public static Achievement GET_NETHERCOAL =
         new Achievement(NamespaceID.fromPool("minecraft", "get_nethercoal"), "getNethercoal", Items.NETHERCOAL, ENTER_NETHER)
             .setType(Achievement.TYPE_NORMAL)
             .registerAchievement();
+    public static Achievement DISCOVER_SULFURPOOLS =
+        new Achievement(NamespaceID.fromPool("minecraft", "discover_sulfurpools"), "discoverSulfurPools", Blocks.BRIMTHAW, ENTER_NETHER)
+            .setType(Achievement.TYPE_SPECIAL)
+            .registerAchievement();
+    public static Achievement DISCOVER_VOLCANICISLANDS =
+        new Achievement(NamespaceID.fromPool("minecraft", "discover_volcanicislands"), "discoverVolcanicIslands", Blocks.MAGMA, ENTER_NETHER)
+            .setType(Achievement.TYPE_SPECIAL)
+            .registerAchievement();
+    public static Achievement FLING_VENT =
+        new Achievement(NamespaceID.fromPool("minecraft", "flingVent"), "flingVent", Items.GUNPOWDER, DISCOVER_VOLCANICISLANDS)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement COLLECT_PUMICE =
+        new Achievement(NamespaceID.fromPool("minecraft", "collect_pumice"), "collectPumice", Blocks.PUMICE_WET, DISCOVER_VOLCANICISLANDS)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement WHAT_WAS_THAT =
+        new Achievement(NamespaceID.fromPool("minecraft", "explode_stacked_vent"), "explodeStackedVent", Blocks.THERMAL_VENT, DISCOVER_VOLCANICISLANDS)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement DISCOVER_CRYSTALFOREST =
+        new Achievement(NamespaceID.fromPool("minecraft", "discover_crystalforest"), "discoverCrystalForest", Blocks.RUBYGLASS_COLUMN, ENTER_NETHER)
+            .setType(Achievement.TYPE_SPECIAL)
+            .registerAchievement();
+    public static Achievement COLLECT_RUBYGLASS =
+        new Achievement(NamespaceID.fromPool("minecraft", "collect_rubyglass"), "collectRubyglass", Items.RUBYGLASS, DISCOVER_CRYSTALFOREST)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement CRAFT_CONDUIT =
+        new Achievement(NamespaceID.fromPool("minecraft", "craft_conduit"), "craftConduit", Blocks.CONDUIT, COLLECT_RUBYGLASS)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement DISCOVER_OLDWORLD =
+        new Achievement(NamespaceID.fromPool("minecraft", "discover_oldworld"), "discoverOldWorld", Blocks.BLOCK_ASH, ENTER_NETHER)
+            .setType(Achievement.TYPE_SPECIAL)
+            .registerAchievement();
+    public static Achievement TRIGGER_EMBER =
+        new Achievement(NamespaceID.fromPool("minecraft", "trigger_ember"), "triggerEmber", Blocks.EMBER, null)
+            .setType(Achievement.TYPE_SECRET)
+            .registerAchievement();
+    public static Achievement COLLECT_ACID =
+        new Achievement(NamespaceID.fromPool("minecraft", "collect_acid"), "collectAcid", ItemBucket.createItemStack(Items.BUCKET_IRON, ItemBucket.STATE_ACID, 1), DISCOVER_SULFURPOOLS)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement CONVERT_ACID_TO_SULFUR =
+        new Achievement(NamespaceID.fromPool("minecraft", "acid_sulfur_conversion"), "acidSulfurConversion", Blocks.SULFUR, COLLECT_ACID)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement CONVERT_ACID_TO_STONE =
+        new Achievement(NamespaceID.fromPool("minecraft", "acid_stone_generation"), "acidStoneGeneration", Blocks.STONE, COLLECT_ACID)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement CRAFT_ACID_COBBLE_TO_STONE =
+        new Achievement(NamespaceID.fromPool("minecraft", "acid_cobble_conversion"), "acidCobbleConversion", Blocks.COBBLE_STONE, COLLECT_ACID)
+            .setType(Achievement.TYPE_NORMAL)
+            .registerAchievement();
+    public static Achievement ACID_BATH =
+        new Achievement(NamespaceID.fromPool("minecraft", "acid_bath"), "acidBath", ItemBucket.createItemStack(Items.BUCKET_STEEL, ItemBucket.STATE_ACID, 3), null)
+            .setType(Achievement.TYPE_SECRET)
+            .registerAchievement();
     public static Achievement LIGHT_SIGN =
         new Achievement(NamespaceID.fromPool("minecraft", "light_sign"), "lightSign", Items.DUST_GLOWSTONE, ENTER_NETHER)
             .setType(Achievement.TYPE_NORMAL)
             .registerAchievement();
     public static Achievement SWIM_NETHER =
-        new Achievement(NamespaceID.fromPool("minecraft", "swim_nether"), "swimNether", ItemBucket.createItemStack(Items.BUCKET_IRON, ItemBucket.STATE_WATER, 1), ENTER_NETHER)
-            .setType(Achievement.TYPE_SPECIAL)
+        new Achievement(NamespaceID.fromPool("minecraft", "swim_nether"), "swimNether", Blocks.RUBYGLASS_CRYSTAL, ENTER_NETHER)
+            .setType(Achievement.TYPE_NORMAL)
             .registerAchievement();
     public static Achievement HIT_FIREBALL =
         new Achievement(NamespaceID.fromPool("minecraft", "hit_fireball"), "hitFireball", Items.AMMO_FIREBALL, ENTER_NETHER)
             .setType(Achievement.TYPE_SPECIAL)
             .registerAchievement();
     public static Achievement SLEEP_NETHER =
-        new Achievement(NamespaceID.fromPool("minecraft", "sleep_nether"), "sleepNether", Items.BED, ENTER_NETHER)
-            .setType(Achievement.TYPE_SPECIAL)
+        new Achievement(NamespaceID.fromPool("minecraft", "sleep_nether"), "sleepNether", Items.BED, null)
+            .setType(Achievement.TYPE_SECRET)
             .registerAchievement();
     public static Achievement MOST_WANTED =
         new Achievement(NamespaceID.fromPool("minecraft", "most_wanted"), "mostWanted", Items.TOOL_SWORD_GOLD, ENTER_NETHER)
             .setType(Achievement.TYPE_NORMAL)
             .registerAchievement();
-    public static Achievement FAST_TRAVEL = // TODO
-        new Achievement(NamespaceID.fromPool("minecraft", "fast_travel"), "fastTravel", Blocks.GRASS, ENTER_NETHER)
+    public static Achievement FAST_TRAVEL =
+        new Achievement(NamespaceID.fromPool("minecraft", "fast_travel"), "fastTravel", Blocks.OBSIDIAN, ENTER_NETHER)
             .setType(Achievement.TYPE_NORMAL)
             .registerAchievement();
     public static final Achievement GET_STEEL_BLAST_FURNACE =
@@ -156,6 +216,9 @@ public abstract class Achievements
     public static final Achievement OBTAIN_STEEL =
         new Achievement(NamespaceID.fromPool("minecraft", "obtain_steel"), "obtainSteel", Items.INGOT_STEEL, GET_STEEL_BLAST_FURNACE)
             .registerAchievement();
+    public static final Achievement CRAFT_STEEL_BUCKET =
+        new Achievement(NamespaceID.fromPool("minecraft", "craft_steel_bucket"), "craftSteelBucket", Items.BUCKET_STEEL, OBTAIN_STEEL)
+            .registerAchievement();
 
     public static void init(){};
 }
diff --git a/game/core/src/main/java/net/minecraft/core/achievement/stat/StatsCounter.java b/game/core/src/main/java/net/minecraft/core/achievement/stat/StatsCounter.java
index 1b9a6ece7..2f903e673 100644
--- a/game/core/src/main/java/net/minecraft/core/achievement/stat/StatsCounter.java
+++ b/game/core/src/main/java/net/minecraft/core/achievement/stat/StatsCounter.java
@@ -50,9 +50,26 @@ public class StatsCounter {
 		modified = true;
 	}
 
+	public void remove(Stat stat) {
+		if (stat == null) {
+			return;
+		}
+		if (!sessionStats.containsKey(stat) && !savedStats.containsKey(stat)) {
+			return;
+		}
+		sessionStats.removeInt(stat);
+		savedStats.removeInt(stat);
+		modified = true;
+	}
+
 	private void addToMap(Object2IntMap<Stat> map, Stat stat, int amount) {
 		int currentValue = map.getOrDefault(stat, 0);
-		map.put(stat, currentValue + amount);
+		int newValue = currentValue + amount;
+		if (newValue <= 0) {
+			map.removeInt(stat);
+		} else {
+			map.put(stat, newValue);
+		}
 	}
 
 	@Unmodifiable
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicEmber.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicEmber.java
index e32631874..525836a0b 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicEmber.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicEmber.java
@@ -1,19 +1,17 @@
 package net.minecraft.core.block;
 
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.entity.animal.MobWolf;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.util.helper.DamageType;
-import net.minecraft.core.util.helper.MathHelper;
-import net.minecraft.core.util.phys.HitResult;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.WorldSource;
 import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.joml.Vector3d;
 import org.joml.primitives.AABBd;
 import org.joml.primitives.AABBdc;
 
@@ -26,19 +24,15 @@ public class BlockLogicEmber extends BlockLogic {
 
 	@Override
 	public void onEntityCollision(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Entity entity) {
-		if(entity instanceof Player || entity instanceof MobWolf){
-			this.ignite(world, tilePos, entity);
-		}
-	}
-
-	@Override
-	public void onEntityWalkedOn(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Entity entity) {
-		if(entity instanceof Player || entity instanceof MobWolf){ // We do a little trolling here...
+		if(!world.isClientSide && (entity instanceof Player || entity instanceof MobWolf)){
 			this.ignite(world, tilePos, entity);
 		}
 	}
 
 	public void ignite(@NotNull World world, @NotNull TilePosc tilePos, Entity entity) {
+		if (!world.isClientSide && entity instanceof Player player) {
+			player.addStat(Achievements.TRIGGER_EMBER, 1);
+		}
 		if (entity != null) {
 			entity.hurt(null, 8, DamageType.BLAST);
 			entity.remainingFireTicks = 100;
@@ -85,4 +79,10 @@ public class BlockLogicEmber extends BlockLogic {
 			world.spawnParticle("largesmoke", xPos, yPos + 0.30, zPos, 0.0F, 0.015F, 0.0F, 0, false);
 		}
 	}
+
+	@Override
+	public @Nullable AABBdc getCollisionAABB(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
+		double f = 0.25/16d; // TODO making the hitbox slightly shorter so `onEntityCollision` triggers consistently, need a method that actually reliably is called when walked on or touched `onEntityWalkedOn` is only applied when its the block responsible for footstep noises and `onEntityCollision` only happens consistently when intersecting the bounds of a block
+		return new AABBd(tilePos.x(), tilePos.y(), tilePos.z(), tilePos.x() + 1, tilePos.y() + 1 - f, tilePos.z() + 1);
+	}
 }
\ No newline at end of file
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluid.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluid.java
index f969d4b79..e7428b752 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluid.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluid.java
@@ -120,7 +120,7 @@ public abstract class BlockLogicFluid extends BlockLogic {
 
     @Override
     public void onEntityInside(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Entity entity, @NotNull Vector3d velocityDirection) {
-        if (entity.noPhysics) return;
+        if (entity.hasNoPhysics()) return;
         velocityDirection.add(getFlow(world, tilePos));
     }
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java
index 3acad5f13..29190bc4f 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicFluidFlowing.java
@@ -3,24 +3,24 @@ package net.minecraft.core.block;
 import net.minecraft.core.block.tag.BlockTags;
 import net.minecraft.core.enums.EnumDropCause;
 import net.minecraft.core.block.material.Material;
+import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
 
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+
+import java.util.Arrays;
 import java.util.Random;
 
 public class BlockLogicFluidFlowing extends BlockLogicFluid {
     int maxCount;
-    boolean @NotNull [] result;
-    int @NotNull [] distance;
     public final @NotNull Block<?> blockStill;
 
     public BlockLogicFluidFlowing(@NotNull Block<?> block, @NotNull Material material, final @NotNull Fluid fluid, @NotNull Block<?> blockStill) {
         super(block, material, fluid);
         this.maxCount = 0;
-        this.result = new boolean[4];
-        this.distance = new int[4];
         this.blockStill = blockStill;
     }
 
@@ -69,7 +69,7 @@ public class BlockLogicFluidFlowing extends BlockLogicFluid {
                 } else {
                     world.setBlockDataNotify(tilePos, localFlowDecay);
                     world.scheduleBlockUpdate(tilePos, this.block, tickDelay(world, tilePos));
-                    world.notifyBlocksOfNeighborChange(tilePos, this.block);
+                    // world.notifyBlocksOfNeighborChange(tilePos, this.block);
                 }
             } else {
                 setFluidStill(world, tilePos);
@@ -78,39 +78,30 @@ public class BlockLogicFluidFlowing extends BlockLogicFluid {
             setFluidStill(world, tilePos);
         }
         if (canSpreadTo(world, tilePos.down(queryPos))) {
-            Block<?> b = world.getBlockType(tilePos.down(queryPos));
+            final TilePosc spreadPos = tilePos.down(queryPos);
+            Block<?> b = world.getBlockType(spreadPos);
             if (b != null) {
-                b.dropWithCause(world, EnumDropCause.WORLD, tilePos.down(queryPos), world.getBlockData(tilePos.down(queryPos)), null, null);
+                b.dropWithCause(world, EnumDropCause.WORLD, spreadPos, world.getBlockData(spreadPos), null, null);
             }
             if (localFlowDecay >= 8) {
-                world.setBlockTypeDataNotify(tilePos.down(queryPos), this.block, localFlowDecay);
+                world.setBlockTypeDataNotify(spreadPos, this.block, localFlowDecay);
             } else {
-                world.setBlockTypeDataNotify(tilePos.down(queryPos), this.block, localFlowDecay + 8);
+                world.setBlockTypeDataNotify(spreadPos, this.block, localFlowDecay + 8);
             }
         } else if (localFlowDecay >= 0 && (localFlowDecay == 0 || isFluidBlocking(world, tilePos.down(queryPos)))) {
-            boolean[] spreadFlags = getSpread(world, tilePos);
-            int k1 = localFlowDecay + flowDecayMod;
-            if (localFlowDecay >= 8) {
-                k1 = 1;
-            }
-            if (k1 >= 8) {
-                return;
-            }
-            if (spreadFlags[0]) {
-                flowIntoBlock(world, tilePos.west(queryPos), k1);
-            }
-            if (spreadFlags[1]) {
-                flowIntoBlock(world, tilePos.east(queryPos), k1);
-            }
-            if (spreadFlags[2]) {
-                flowIntoBlock(world, tilePos.north(queryPos), k1);
-            }
-            if (spreadFlags[3]) {
-                flowIntoBlock(world, tilePos.south(queryPos), k1);
-            }
+            final int meta = (localFlowDecay < MAX_SPREAD)
+                ? localFlowDecay + flowDecayMod
+                : 1;
+            final int spreadDistance = (MAX_SPREAD - localFlowDecay) / flowDecayMod - 1;
+            if (spreadDistance <= 0) return;
+
+            final byte spreadFlags = getSpreadDirections(world, tilePos, spreadDistance);
+            if ((spreadFlags & MASK_W) != 0) flowIntoBlock(world, tilePos.west(queryPos), meta);
+            if ((spreadFlags & MASK_E) != 0) flowIntoBlock(world, tilePos.east(queryPos), meta);
+            if ((spreadFlags & MASK_N) != 0) flowIntoBlock(world, tilePos.north(queryPos), meta);
+            if ((spreadFlags & MASK_S) != 0) flowIntoBlock(world, tilePos.south(queryPos), meta);
         }
     }
-
     private void flowIntoBlock(@NotNull World world, @NotNull TilePos tilePos, int meta) {
         if (canSpreadTo(world, tilePos)) {
             this.fluid.onFlowIntoBlock(this, world, tilePos, meta);
@@ -118,79 +109,188 @@ public class BlockLogicFluidFlowing extends BlockLogicFluid {
         }
     }
 
-    private int getSlopeDistance(@NotNull World world, @NotNull TilePosc tilePos, int l, int i1) {
-        int j1 = 1000;
-        TilePos queryPos = new TilePos(tilePos);
-        for (int d = 0; d < 4; d++) {
-            if (d == 0 && i1 == 1 || d == 1 && i1 == 0 || d == 2 && i1 == 3 || d == 3 && i1 == 2) {
-                continue;
-            }
-            int checkX = tilePos.x();
-            int checkY = tilePos.y();
-            int checkZ = tilePos.z();
-            switch (d) {
-                case 0 -> checkX--;
-                case 1 -> checkX++;
-                case 2 -> checkZ--;
-                case 3 -> checkZ++;
-            }
-            queryPos.set(checkX, checkY, checkZ);
-            if (isFluidBlocking(world, queryPos) || world.getBlockMaterial(queryPos) == this.material && world.getBlockData(queryPos) == 0) {
-                continue;
-            }
-            if (!isFluidBlocking(world, queryPos.set(checkX, checkY - 1, checkZ))) {
-                return l;
-            }
-            if (l >= this.fluid.getMaxHorizontalSpread(this, world, tilePos)) {
-                continue;
-            }
-            int k2 = getSlopeDistance(world, queryPos.set(checkX, checkY, checkZ), l + 1, d);
-            if (k2 < j1) {
-                j1 = k2;
-            }
-        }
+    private static final int MAX_SPREAD = 8;
+    private static final int POS_OBSTRUCTED = Integer.MAX_VALUE, POS_EMPTY = Integer.MAX_VALUE - 1;
+
+    private static final byte MASK_W = 0b0001, MASK_E = 0b0010, MASK_N = 0b0100, MASK_S = 0b1000;
+    private static final int INDEX_W = pos2index(-1, 0), INDEX_E = pos2index(+1, 0), INDEX_N = pos2index(0, -1), INDEX_S = pos2index(0, +1);
 
-        return j1;
+    private static final int[] MAP_1 = new int[256];
+    private static final IntArrayList BUF_1 = new IntArrayList();
+    private static final IntArrayList BUF_2 = new IntArrayList();
+
+    private static final int pos2index(final int x, final int z) {
+        assert Math.abs(x) < 8 && Math.abs(z) < 8;
+        return ((x & 0xF)) | ((z & 0xF) << 4);
     }
 
-    private boolean[] getSpread(@NotNull World world, @NotNull TilePosc tilePos) {
-        TilePos queryPos = new TilePos(tilePos);
-        for (int d = 0; d < 4; d++) {
-            this.distance[d] = 1000;
-            int checkX = tilePos.x();
-            int checkY = tilePos.y();
-            int checkZ = tilePos.z();
-            switch (d) {
-                case 0 -> checkX--;
-                case 1 -> checkX++;
-                case 2 -> checkZ--;
-                case 3 -> checkZ++;
-            }
-            queryPos.set(checkX, checkY, checkZ);
-            if (isFluidBlocking(world, queryPos) || world.getBlockMaterial(queryPos) == this.material && world.getBlockData(queryPos) == 0) {
-                continue;
+    private static int x, z;
+    private static final void index2pos(final int i) {
+        x = (i) & 0xF;
+        z = (i>>4) & 0xF;
+        if (x >= 8) x |= -8;
+        if (z >= 8) z |= -8;
+    }
+
+    private byte getSpreadDirections(@NotNull World world, @NotNull TilePosc tilePos, int spreadDistance) {
+        assert spreadDistance >= 0 && spreadDistance < MAX_SPREAD;
+
+        final int ox = tilePos.x();
+        final int oz = tilePos.z();
+        final int oy = tilePos.y();
+
+        BUF_1.clear();
+        BUF_2.clear();
+        final var holeBuf = BUF_1;
+        final var obstacleBuf = BUF_2;
+        final var distanceMap = MAP_1;
+        Arrays.fill(distanceMap, POS_EMPTY);
+
+        final var queryPos = new TilePos();
+        byte neighboringObstaclesMask = 0b0000;
+
+        int layer;
+        for (layer = 1; layer <= spreadDistance; layer++) {
+            final int obstaclesBeforeLayer = obstacleBuf.size();
+
+            for (int x = -layer; x <= layer; x++) {
+                final int rz = layer - Math.abs(x);
+                for (int z = -rz;; z = rz) {
+                    queryPos.set(ox + x, oy, oz + z);
+                    final int i = pos2index(x, z);
+
+                    check: {
+                        final boolean isObstructed; isObstructed: {
+                            final Block<?> b = world.getBlockType(queryPos);
+                            if (b == Blocks.AIR) {
+                                isObstructed = false; break isObstructed;
+                            }
+                            final boolean isFluidBlocking = 
+                               !(b.getLogic() instanceof BlockLogicFluid) &&
+                               !b.hasTag(BlockTags.BROKEN_BY_FLUIDS) &&
+                               !b.hasTag(BlockTags.PLACE_OVERWRITES);
+                            if (isFluidBlocking) {
+                                isObstructed = true; break isObstructed;
+                            }
+        
+                            final boolean isMatchingSource =
+                                b.getMaterial() == this.material &&
+                                world.getBlockData(queryPos) == 0;
+                            isObstructed = isMatchingSource;
+                        }
+                        if (isObstructed) {
+                            obstacleBuf.add(i);
+                            distanceMap[i] = POS_OBSTRUCTED;
+                            if (layer != 1) break check;
+
+                            if (x != 0) {
+                                assert Math.abs(x) == 1 && z == 0;
+                                neighboringObstaclesMask |= (x == -1) ? MASK_W : MASK_E;
+                            } else {
+                                assert Math.abs(z) == 1 && x == 0;
+                                neighboringObstaclesMask |= (z == -1) ? MASK_N : MASK_S;
+                            }
+                            break check;
+                        }
+ 
+                        final boolean isHole; isHole: {
+                            final Block<?> b = world.getBlockType(queryPos.down());
+                            if (b == Blocks.AIR) {
+                                isHole = true; break isHole;
+                            }
+                            final boolean isFluidBlocking = 
+                               !(b.getLogic() instanceof BlockLogicFluid) &&
+                               !b.hasTag(BlockTags.BROKEN_BY_FLUIDS) &&
+                               !b.hasTag(BlockTags.PLACE_OVERWRITES);
+                            isHole = !isFluidBlocking;
+                        }
+                        if (isHole) {
+                            holeBuf.add(i);
+                            distanceMap[i] = 0;
+                            break check;
+                        }
+                    }
+
+                    // iterate once if rz == 0, twice otherwise
+                    if (z == rz) break;
+                }
             }
-            if (!isFluidBlocking(world, queryPos.set(checkX, checkY - 1, checkZ))) {
-                this.distance[d] = 0;
-            } else {
-                this.distance[d] = getSlopeDistance(world, queryPos.set(checkX, checkY, checkZ), 1, d);
+
+            final int blocksThisLayer = 4*layer;
+            final int holesThisLayer = holeBuf.size();
+
+            // no obstacles nor holes were found before this layer, therefore
+            // 1) holes this layer are the closest holes,
+            // 2) distance to holes is simply manhattan distance.
+            if (obstaclesBeforeLayer == 0 && holesThisLayer != 0) {
+                if (holesThisLayer == blocksThisLayer) return 0b1111;
+                byte mask = 0b0000;
+                for (int _i = 0; _i < holeBuf.size(); _i++) {
+                    /*x, z =*/ index2pos(holeBuf.getInt(_i));
+                    if (x != 0) mask |= (x < 0) ? MASK_W : MASK_E;
+                    if (z != 0) mask |= (z < 0) ? MASK_N : MASK_S;
+                }
+                return mask;
             }
+
+            // every block in this layer are obstacles,
+            // therefore no point in continuing the search
+            if (obstacleBuf.size() - obstaclesBeforeLayer == blocksThisLayer) break;
         }
 
-        int i1 = this.distance[0];
-        for (int k1 = 1; k1 < 4; k1++) {
-            if (this.distance[k1] < i1) {
-                i1 = this.distance[k1];
+        // there exist no reachable holes, therefore we only care about adjacent obstacles
+        if (holeBuf.size() == 0) return (byte)~neighboringObstaclesMask;
+
+        // there exist some holes that are potentially obstructed.
+        // calculate the distance map with BFS from holes
+        final var updateBuf = holeBuf;
+        for (int _i = 0; _i < updateBuf.size(); _i++) {
+            final int i = updateBuf.getInt(_i);
+            final int d = distanceMap[i];
+            if (d == POS_OBSTRUCTED) continue;
+            /*x, z =*/ index2pos(i);
+
+            for (int q = 0; q < 4; q++) {
+                int x2 = x, z2 = z;
+                switch (q) {
+                    case 0 -> { x2 -= 1; }
+                    case 1 -> { x2 += 1; }
+                    case 2 -> { z2 -= 1; }
+                    case 3 -> { z2 += 1; }
+                }
+                final int od = Math.abs(x2) + Math.abs(z2);
+
+                // we do not update distances outside the current searched diamond,
+                // nor do we update distances across the origin (this makes the distance values
+                // across the origin incorrect, but does not change the comparison results).
+                if (od >= layer || od == 0) continue;
+
+                final int i2 = pos2index(x2, z2);
+                final int d2 = distanceMap[i2];
+
+                if (d2 == POS_OBSTRUCTED) continue;
+                if (d2 > d + 1) {
+                    distanceMap[i2] = d + 1;
+                    updateBuf.push(i2);
+                }
             }
         }
 
-        for (int l1 = 0; l1 < 4; l1++) {
-            this.result[l1] = this.distance[l1] == i1;
-        }
+        final int dW = distanceMap[INDEX_W];
+        final int dE = distanceMap[INDEX_E];
+        final int dN = distanceMap[INDEX_N];
+        final int dS = distanceMap[INDEX_S];
 
-        return this.result;
-    }
+        final int dm = Math.min(Math.min(dW, dE), Math.min(dN, dS));
+        byte mask = 0;
+        if (dW == dm) mask |= 0b0001;
+        if (dE == dm) mask |= 0b0010;
+        if (dN == dm) mask |= 0b0100;
+        if (dS == dm) mask |= 0b1000;
 
+        return mask;
+    }
+ 
     private boolean isFluidBlocking(@NotNull World world, @NotNull TilePos tilePos) {
         Block<?> b = world.getBlockType(tilePos);
         return !(b.getLogic() instanceof BlockLogicFluid) && !b.hasTag(BlockTags.BROKEN_BY_FLUIDS) && !b.hasTag(BlockTags.PLACE_OVERWRITES);
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicNodeRubyglass.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicNodeRubyglass.java
index 52c12fe06..009ce2575 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicNodeRubyglass.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicNodeRubyglass.java
@@ -1,8 +1,8 @@
 package net.minecraft.core.block;
 
 import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
-import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.enums.EnumDropCause;
+import net.minecraft.core.block.entity.TileEntity;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.item.Items;
 import net.minecraft.core.world.World;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java
index 8e8075dc0..f8ce3cecf 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRotatable.java
@@ -9,7 +9,7 @@ import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
 
-public abstract class BlockLogicRotatable extends BlockLogic {
+public class BlockLogicRotatable extends BlockLogic {
     public static final int MASK_DIRECTION = 0b0000_0111;
 
     public BlockLogicRotatable(@NotNull Block<?> block, @NotNull Material material) {
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassCrystal.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassCrystal.java
index 4ce22dac6..41ff6a93b 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassCrystal.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicRubyglassCrystal.java
@@ -44,11 +44,6 @@ public class BlockLogicRubyglassCrystal extends BlockLogicTransparent {
 		if (data == 0) world.scheduleBlockUpdate(tilePos, this.block, tickDelay());
 	}
 
-	@Override
-	public void onEntityWalkedOn(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Entity entity) {
-		this.onEntityCollision(world, tilePos, entity);
-	}
-
 	@Override
 	public float getAmbientOcclusionStrength(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
 		return 0.0F;
@@ -93,7 +88,7 @@ public class BlockLogicRubyglassCrystal extends BlockLogicTransparent {
 
 	@Override
 	public @Nullable AABBdc getCollisionAABB(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
-		double f = 0.25/16d; // TODO making the hitbox slightly shorter so `onEntityWalkedOn` triggers consistently, need a method that actually reliably is called when walked on or touched `onEntityWalkedOn` is only applied when its the block responsible for footstep noises and `onEntityWalkedOn` only happens consistently when intersecting the bounds of a block
+		double f = 0.25/16d; // TODO making the hitbox slightly shorter so `onEntityCollision` triggers consistently, need a method that actually reliably is called when walked on or touched `onEntityWalkedOn` is only applied when its the block responsible for footstep noises and `onEntityCollision` only happens consistently when intersecting the bounds of a block
 		return new AABBd(tilePos.x(), tilePos.y(), tilePos.z(), tilePos.x() + 1, tilePos.y() + 1 - f, tilePos.z() + 1);
 	}
 
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSoulSand.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSoulSand.java
index b93cf2b38..c4b158c72 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSoulSand.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSoulSand.java
@@ -1,5 +1,6 @@
 package net.minecraft.core.block;
 
+import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.entity.Entity;
 import net.minecraft.core.world.World;
@@ -12,12 +13,20 @@ import org.joml.primitives.AABBdc;
 
 public class BlockLogicSoulSand extends BlockLogic
 {
+    public static final Int2IntArrayMap pocketVariantMap = new Int2IntArrayMap();
 
     public BlockLogicSoulSand(Block<?> block)
     {
         super(block, Materials.SOUL_SAND);
     }
 
+    static {
+        pocketVariantMap.put(Blocks.GLOOMSTONE.id(), Blocks.SOULSAND.id());
+        pocketVariantMap.put(Blocks.COBBLE_GLOOMSTONE.id(), Blocks.SOULSAND.id());
+        pocketVariantMap.put(Blocks.BLOCK_ASH.id(), Blocks.SOULSAND.id());
+        pocketVariantMap.put(Blocks.SLATE.id(), Blocks.SOULSAND.id());
+    }
+
     @Override
     public @Nullable AABBdc getCollisionAABB(@NotNull WorldSource source, @NotNull TilePosc tilePos) {
         double f = 2/16d;
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSulfur.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSulfur.java
index 4570f258a..b904c1c85 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicSulfur.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicSulfur.java
@@ -1,7 +1,7 @@
 package net.minecraft.core.block;
 
+import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
 import net.minecraft.core.block.entity.TileEntity;
-import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.entity.Mob;
 import net.minecraft.core.entity.player.Player;
@@ -10,22 +10,22 @@ import net.minecraft.core.item.Item;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.item.Items;
 import net.minecraft.core.sound.SoundCategory;
-import net.minecraft.core.util.helper.Direction;
 import net.minecraft.core.util.helper.Side;
-import net.minecraft.core.world.LevelListener;
 import net.minecraft.core.world.World;
-import net.minecraft.core.world.pos.TilePos;
 import net.minecraft.core.world.pos.TilePosc;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.Random;
-
 public class BlockLogicSulfur extends BlockLogic {
+	public static final Int2IntArrayMap pocketVariantMap = new Int2IntArrayMap();
 
 	public BlockLogicSulfur(@NotNull Block<?> block) {
 		super(block, Materials.SOUL_SAND);
-		block.setTicking(true);
+	}
+
+	static {
+		pocketVariantMap.put(Blocks.BRIMSTONE.id(), Blocks.SULFUR.id());
+		pocketVariantMap.put(Blocks.BRIMSAND.id(), Blocks.SULFUR.id());
 	}
 
 	@Override
@@ -36,11 +36,6 @@ public class BlockLogicSulfur extends BlockLogic {
 		};
 	}
 
-	@Override
-	public int tickDelay() {
-		return 20;
-	}
-
 	@Override
 	public void onNeighborChanged(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Block<?> block) {
 		world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay() + world.rand.nextInt(5) - world.rand.nextInt(5));
@@ -51,32 +46,6 @@ public class BlockLogicSulfur extends BlockLogic {
 		world.scheduleBlockUpdate(tilePos, this.block, this.tickDelay() + world.rand.nextInt(5) - world.rand.nextInt(5));
 	}
 
-	public boolean canMelt(@NotNull World world, @NotNull TilePosc tilePos) {
-		int acidCount = 0;
-		TilePos queryPos = new TilePos();
-
-		for (Direction dir : Direction.ID_MAP) {
-			Block<?> block = world.getBlockType(tilePos.add(dir, queryPos));
-			Material adjacentMaterial = block.getMaterial();
-			if (adjacentMaterial == Materials.LAVA) return false;
-			if (adjacentMaterial == Materials.WATER) return false;
-			if (adjacentMaterial == Materials.ACID) {
-				acidCount++;
-			}
-		}
-
-		return acidCount >= 3;
-	}
-
-	@Override
-	public void updateTick(@NotNull World world, @NotNull TilePosc tilePos, @NotNull Random rand, boolean isRandomTick) {
-		if (this.canMelt(world, tilePos)) {
-			world.playBlockEvent(tilePos, LevelListener.EVENT_ACID_SPREAD, 0);
-			world.setBlockTypeNotify(tilePos, Blocks.FLUID_ACID_FLOWING);
-		}
-
-	}
-
 	@Override
 	public void onPlacedByMob(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final @NotNull Mob mob, final double xHit, final double yHit) {
 		world.playSoundEffect(
@@ -97,14 +66,14 @@ public class BlockLogicSulfur extends BlockLogic {
 		super.onDestroyedByPlayer(world, tilePos, side, data, player, item);
 
 		world.playSoundEffect(
-				player,
-				SoundCategory.WORLD_SOUNDS,
-				tilePos.x() + 0.5,
-				tilePos.y() + 0.5,
-				tilePos.z() + 0.5,
-				"step.stone",
-				1.0F,
-				0.8F
+			player,
+			SoundCategory.WORLD_SOUNDS,
+			tilePos.x() + 0.5,
+			tilePos.y() + 0.5,
+			tilePos.z() + 0.5,
+			"step.stone",
+			1.0F,
+			0.8F
 		);
 	}
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/BlockLogicThermalVent.java b/game/core/src/main/java/net/minecraft/core/block/BlockLogicThermalVent.java
index ce20f709a..04f08add0 100644
--- a/game/core/src/main/java/net/minecraft/core/block/BlockLogicThermalVent.java
+++ b/game/core/src/main/java/net/minecraft/core/block/BlockLogicThermalVent.java
@@ -1,8 +1,13 @@
 package net.minecraft.core.block;
 
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.entity.Entity;
+import net.minecraft.core.entity.Mob;
 import net.minecraft.core.entity.player.Player;
+import net.minecraft.core.util.helper.Side;
+import net.minecraft.core.world.Dimension;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.particle.ParticleEmitterRange;
 import net.minecraft.core.world.pos.TilePos;
@@ -19,6 +24,15 @@ public class BlockLogicThermalVent extends BlockLogic {
     public static int DORMANT_STATE = 0;
     public static int VENTING_STATE = 4;
 
+    private static final Long2IntOpenHashMap STACKED_VENT_OWNERS = new Long2IntOpenHashMap();
+
+    private static long packStackedVentKey(final @NotNull World world, final @NotNull TilePosc tilePos) {
+        return ((long) world.getDimension().id << 60)
+            | ((long) (tilePos.x() & 0x3FFFFF) << 40)
+            | ((long) (tilePos.z() & 0x3FFFFF) << 20)
+            | (long) (tilePos.y() & 0xFFFFF);
+    }
+
     public BlockLogicThermalVent(final @NotNull Block<?> block) {
         super(block, Materials.BASALT);
         block.setTicking(true);
@@ -134,17 +148,48 @@ public class BlockLogicThermalVent extends BlockLogic {
 
 
         if (!world.isClientSide) {
+            final long ventKey = packStackedVentKey(world, tilePos);
+            final int ownerId = STACKED_VENT_OWNERS.containsKey(ventKey) ? STACKED_VENT_OWNERS.remove(ventKey) : -1;
             alertNearbyPlayers(world, tilePos);
 
             final List<Entity> entities = world.getEntitiesWithinAABB(Entity.class, new AABBd(tilePos.x() - 1, tilePos.y(), tilePos.z() - 1, tilePos.x() + 2, tilePos.y() + 7, tilePos.z() + 2));
             for (final @NotNull Entity entity : entities) {
                 entity.fling(0, 2, 0, 1f);
+                if (entity instanceof Player player) {
+                    player.addStat(Achievements.FLING_VENT, 1);
+                }
+            }
+
+            if (ownerId != -1) {
+                for (final Player player : world.players) {
+                    if (player.id == ownerId) {
+                        player.addStat(Achievements.WHAT_WAS_THAT, 1);
+                        break;
+                    }
+                }
             }
 
             world.setBlockData(tilePos, DORMANT_STATE);
         }
     }
 
+    @Override
+    public void onPlacedByMob(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Side side, final @NotNull Mob mob, final double xHit, final double yHit) {
+        if (!world.isClientSide && mob instanceof Player player
+            && world.getDimension() == Dimension.OVERWORLD
+            && hasVentBelow(world, tilePos)) {
+            STACKED_VENT_OWNERS.put(packStackedVentKey(world, tilePos), player.id);
+        }
+    }
+
+    @Override
+    public void onRemoved(final @NotNull World world, final @NotNull TilePosc tilePos, final int data) {
+        super.onRemoved(world, tilePos, data);
+        if (!world.isClientSide) {
+            STACKED_VENT_OWNERS.remove(packStackedVentKey(world, tilePos));
+        }
+    }
+
     @Override
     public void onPlacedByWorld(final @NotNull World world, final @NotNull TilePosc tilePos) {
         super.onPlacedByWorld(world, tilePos);
diff --git a/game/core/src/main/java/net/minecraft/core/block/FluidHardening.java b/game/core/src/main/java/net/minecraft/core/block/FluidHardening.java
index fef77ac99..2e6e82020 100644
--- a/game/core/src/main/java/net/minecraft/core/block/FluidHardening.java
+++ b/game/core/src/main/java/net/minecraft/core/block/FluidHardening.java
@@ -1,14 +1,19 @@
 package net.minecraft.core.block;
 
+import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.material.Material;
 import net.minecraft.core.block.material.Materials;
-import net.minecraft.core.sound.SoundCategory;
+import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.pos.TilePosc;
 import net.minecraft.core.world.type.tag.WorldTypeTags;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 public final class FluidHardening {
+    private static final int RECENT_PLACEMENT_TICKS = 240;
+    private static final double RECENT_PLACEMENT_RADIUS_SQ = 16.0 * 16.0;
+
     private FluidHardening() {
     }
 
@@ -59,6 +64,7 @@ public final class FluidHardening {
             world.setBlockTypeNotify(tilePos, data == 0 ? Blocks.OBSIDIAN : Blocks.GLOOMSTONE);
         } else {
             world.setBlockTypeNotify(tilePos, Blocks.LIMESTONE);
+            notifyAcidStoneCreated(world, tilePos, Blocks.LIMESTONE);
         }
         BlockLogicFluid.fizz(world, tilePos);
         return true;
@@ -89,10 +95,13 @@ public final class FluidHardening {
             world.setBlockTypeNotify(tilePos, Blocks.OBSIDIAN);
         } else if (data <= 2) {
             world.setBlockTypeNotify(tilePos, Blocks.GRANITE);
+            notifyAcidStoneCreated(world, tilePos, Blocks.GRANITE);
         } else if (data <= 4) {
             world.setBlockTypeNotify(tilePos, Blocks.STONE);
+            notifyAcidStoneCreated(world, tilePos, Blocks.STONE);
         } else {
             world.setBlockTypeNotify(tilePos, Blocks.BASALT);
+            notifyAcidStoneCreated(world, tilePos, Blocks.BASALT);
         }
         BlockLogicFluid.fizz(world, tilePos);
         return true;
@@ -101,10 +110,26 @@ public final class FluidHardening {
     private static boolean hardenAcidWithLava(final @NotNull World world, final @NotNull TilePosc tilePos) {
         final int data = world.getBlockData(tilePos) & 0x0F;
         if (world.getWorldType().hasTag(WorldTypeTags.NETHER)) {
-            world.setBlockTypeNotify(tilePos, data == 0 ? Blocks.BRIMTHAW : Blocks.BRIMSTONE);
-            fizzAcidNether(world, tilePos);
-        } else {
-            hardenAcidOverworld(world, tilePos, data);
+            if(data == 0) {
+                world.setBlockTypeNotify(tilePos, Blocks.BRIMTHAW);
+            } else {
+                world.setBlockTypeNotify(tilePos, Blocks.BRIMSTONE);
+            }
+        }
+        else {
+            if (data == 0) {
+                world.setBlockTypeNotify(tilePos, Blocks.BRIMTHAW);
+            } else if (data <= 2) {
+                world.setBlockTypeNotify(tilePos, Blocks.GRANITE);
+                notifyAcidStoneCreated(world, tilePos, Blocks.GRANITE);
+            } else if (data <= 4) {
+                world.setBlockTypeNotify(tilePos, Blocks.STONE);
+                notifyAcidStoneCreated(world, tilePos, Blocks.STONE);
+            } else {
+                world.setBlockTypeNotify(tilePos, Blocks.BASALT);
+                notifyAcidStoneCreated(world, tilePos, Blocks.BASALT);
+            }
+            BlockLogicFluid.fizz(world, tilePos);
         }
         return true;
     }
@@ -112,31 +137,65 @@ public final class FluidHardening {
     private static boolean hardenAcidWithWater(final @NotNull World world, final @NotNull TilePosc tilePos) {
         final int data = world.getBlockData(tilePos) & 0x0F;
         if (world.getWorldType().hasTag(WorldTypeTags.NETHER)) {
-            world.setBlockTypeNotify(tilePos, data == 0 ? Blocks.BRIMTHAW : Blocks.BRIMSTONE);
-            fizzAcidNether(world, tilePos);
-        } else {
-            hardenAcidOverworld(world, tilePos, data);
+            if(data == 0) {
+                world.setBlockTypeNotify(tilePos, Blocks.BRIMTHAW);
+            } else {
+                world.setBlockTypeNotify(tilePos, Blocks.BRIMSTONE);
+            }
+        }
+        else {
+            if (data == 0) {
+                world.setBlockTypeNotify(tilePos, Blocks.SULFUR);
+                notifyAcidConvertedToSulfur(world, tilePos);
+            } else if (data <= 2) {
+                world.setBlockTypeNotify(tilePos, Blocks.GRANITE);
+                notifyAcidStoneCreated(world, tilePos, Blocks.GRANITE);
+            } else if (data <= 4) {
+                world.setBlockTypeNotify(tilePos, Blocks.STONE);
+                notifyAcidStoneCreated(world, tilePos, Blocks.STONE);
+            } else {
+                world.setBlockTypeNotify(tilePos, Blocks.BASALT);
+                notifyAcidStoneCreated(world, tilePos, Blocks.BASALT);
+            }
+            BlockLogicFluid.fizz(world, tilePos);
         }
         return true;
     }
 
-    private static void hardenAcidOverworld(final @NotNull World world, final @NotNull TilePosc tilePos, final int data) {
-        if (data == 0) {
-            world.setBlockTypeNotify(tilePos, Blocks.SULFUR);
-        } else if (data <= 2) {
-            world.setBlockTypeNotify(tilePos, Blocks.GRANITE);
-        } else if (data <= 4) {
-            world.setBlockTypeNotify(tilePos, Blocks.STONE);
-        } else {
-            world.setBlockTypeNotify(tilePos, Blocks.BASALT);
+    private static void notifyAcidStoneCreated(final @NotNull World world, final @NotNull TilePosc tilePos, final @NotNull Block<?> block) {
+        if (world.isClientSide || !Block.hasLogicClass(block, BlockLogicStone.class)) {
+            return;
+        }
+        final Player credit = findRecentPlacer(world, tilePos, true);
+        if (credit != null) {
+            credit.addStat(Achievements.CONVERT_ACID_TO_STONE, 1);
+        }
+    }
+
+    private static void notifyAcidConvertedToSulfur(final @NotNull World world, final @NotNull TilePosc tilePos) {
+        if (world.isClientSide) {
+            return;
+        }
+        Player credit = findRecentPlacer(world, tilePos, true);
+        if (credit == null) {
+            credit = findRecentPlacer(world, tilePos, false);
+        }
+        if (credit != null) {
+            credit.addStat(Achievements.CONVERT_ACID_TO_SULFUR, 1);
         }
-        BlockLogicFluid.fizz(world, tilePos);
     }
 
-    private static void fizzAcidNether(final @NotNull World world, final @NotNull TilePosc tilePos) {
-        world.playSoundEffect(null, SoundCategory.WORLD_SOUNDS, (float) tilePos.x() + 0.5F, (float) tilePos.y() + 0.5F, (float) tilePos.z() + 0.5F, "random.fizz", 0.5F, 2.6F + (world.rand.nextFloat() - world.rand.nextFloat()) * 0.8F);
-        for (int i = 0; i < 8; ++i) {
-            world.spawnParticle("largesmoke", (double) tilePos.x() + Math.random(), (double) tilePos.y() + 1.01, (double) tilePos.z() + Math.random(), 0.0F, 0.0F, 0.0F, 0, false);
+    private static @Nullable Player findRecentPlacer(final @NotNull World world, final @NotNull TilePosc tilePos, final boolean acid) {
+        final long now = world.getTotalWorldTime();
+        final int dimensionId = world.getDimension().id;
+        for (final Player player : world.players) {
+            final Player.FluidPlacement placement = acid ? player.lastAcidPlacement : player.lastWaterPlacement;
+            if (placement == null) continue;
+            if (placement.dimensionId() != dimensionId) continue;
+            if (now - placement.tick() > RECENT_PLACEMENT_TICKS) continue;
+            if (tilePos.distanceSquared(placement.x(), placement.y(), placement.z()) > RECENT_PLACEMENT_RADIUS_SQ) continue;
+            return player;
         }
+        return null;
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java
index 087ecd16f..189a41fec 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/BlockLogicPistonBase.java
@@ -323,7 +323,7 @@ public class BlockLogicPistonBase extends BlockLogic {
         // note that flingEntity is performed before flingBlock, therefore no need to 
         // check for flingBlock
         for (final var entity : entities) {
-            if (entity.noPhysics) continue;
+            if (entity.hasNoPhysics()) continue;
             entity.fling(vx, vy, vz, 1f);
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java b/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java
index 158c6cec8..e011cab22 100644
--- a/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java
+++ b/game/core/src/main/java/net/minecraft/core/block/piston/TileEntityMovingPistonBlock.java
@@ -157,7 +157,7 @@ public class TileEntityMovingPistonBlock extends TileEntity implements IMovingBl
 
             final var entities = this.worldObj.getEntitiesWithinAABBExcludingEntity(null, this.sweptCollision);
             for (final var e : entities) {
-                if (e.noPhysics) continue;
+                if (e.hasNoPhysics()) continue;
                 e.move(dir.x, dir.y, dir.z);
             }
 
diff --git a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/RecipeRegistry.java b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/RecipeRegistry.java
index 0c44fe617..620ab5f79 100644
--- a/game/core/src/main/java/net/minecraft/core/data/registry/recipe/RecipeRegistry.java
+++ b/game/core/src/main/java/net/minecraft/core/data/registry/recipe/RecipeRegistry.java
@@ -10,6 +10,7 @@ import net.minecraft.core.data.registry.recipe.entry.*;
 import net.minecraft.core.item.Item;
 import net.minecraft.core.item.ItemStack;
 import net.minecraft.core.player.inventory.container.ContainerCrafting;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.*;
 
@@ -165,13 +166,19 @@ public class RecipeRegistry extends Registry<RecipeNamespace> {
     }
 
     public ItemStack findMatchingRecipe(ContainerCrafting inventorycrafting)
+    {
+        RecipeEntryCrafting<?, ?> recipe = findMatchingCraftingRecipe(inventorycrafting);
+        return recipe != null ? recipe.getCraftingResult(inventorycrafting) : null;
+    }
+
+    public @Nullable RecipeEntryCrafting<?, ?> findMatchingCraftingRecipe(ContainerCrafting inventorycrafting)
     {
         for(int i = 0; i < getAllCraftingRecipes().size(); i++)
         {
             RecipeEntryCrafting<?,?> recipe = getAllCraftingRecipes().get(i);
             if(recipe.matches(inventorycrafting))
             {
-                return recipe.getCraftingResult(inventorycrafting);
+                return recipe;
             }
         }
 
@@ -180,16 +187,8 @@ public class RecipeRegistry extends Registry<RecipeNamespace> {
 
     public ItemStack[] onCraftResult(ContainerCrafting inventorycrafting)
     {
-        for(int i = 0; i < getAllCraftingRecipes().size(); i++)
-        {
-            RecipeEntryCrafting<?,?> recipe = getAllCraftingRecipes().get(i);
-            if(recipe.matches(inventorycrafting))
-            {
-                return recipe.onCraftResult(inventorycrafting);
-            }
-        }
-
-        return null;
+        RecipeEntryCrafting<?, ?> recipe = findMatchingCraftingRecipe(inventorycrafting);
+        return recipe != null ? recipe.onCraftResult(inventorycrafting) : null;
     }
 
     public void addRecipe(String recipeKey, ItemStack itemstack, boolean consumeContainerItem, Object... aobj) {
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 c37a5bb63..d23c84461 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
@@ -49,6 +49,7 @@ public abstract class Entity
     public static final int FLAG_SNEAKING = 1;
     public static final int FLAG_RIDING = 2;
     public static final int FLAG_SPRINTING = 3;
+    public static final int FLAG_NO_PHYSICS = 4;
 
     // Misc/IDK
     public boolean slide = true;
@@ -96,7 +97,6 @@ public abstract class Entity
     private final @NotNull AABBd workingBox4 = new AABBd();
 
     // Movement/Physics
-    public boolean noPhysics = false;
     public float pushthrough = 0.0F;
     public float walkDistO = 0.0F;
     public float walkDist = 0.0F;
@@ -432,7 +432,7 @@ public abstract class Entity
     }
 
     public void move(double xd, double yd, double zd) {
-        if (this.noPhysics) {
+        if (hasNoPhysics()) {
             this.onGround = false;
             this.bb.translate(xd, yd, zd);
             this.x = (this.bb.minX + this.bb.maxX) / 2.0;
@@ -743,7 +743,7 @@ public abstract class Entity
     }
 
     protected void checkFallDamage(final double yd, final boolean onGround) {
-        if (this.vehicle != null || this.noPhysics) {
+        if (this.vehicle != null || hasNoPhysics()) {
             this.fallDistance = 0;
         } else if (onGround) {
             if (this.fallDistance > 0.0F) {
@@ -1091,7 +1091,7 @@ public abstract class Entity
     }
 
     public void push(@NotNull Entity entity) {
-        if (entity.passenger == this || entity.vehicle == this || entity.noPhysics || !entity.isPickable() || !this.collidesWith(entity) || !entity.collidesWith(this)) {
+        if (entity.passenger == this || entity.vehicle == this || entity.hasNoPhysics() || !entity.isPickable() || !this.collidesWith(entity) || !entity.collidesWith(this)) {
             return;
         }
 
@@ -1465,6 +1465,14 @@ public abstract class Entity
         setSharedFlag(FLAG_SNEAKING, flag);
     }
 
+    public boolean hasNoPhysics() {
+        return getSharedFlag(FLAG_NO_PHYSICS);
+    }
+
+    public void setNoPhysics(boolean flag) {
+        setSharedFlag(FLAG_NO_PHYSICS, flag);
+    }
+
     protected boolean getSharedFlag(int i) {
         return (this.entityData.getByte(0) & 1 << i) != 0;
     }
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 41355d1ae..67cee9925 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
@@ -221,6 +221,15 @@ public class EntityItem extends Entity
                 }
                 if(this.item.itemID == Items.NETHERCOAL.id) {
                     player.triggerAchievement(Achievements.GET_NETHERCOAL);
+                }
+                if(this.item.itemID == Items.INGOT_STEEL.id) {
+                    player.triggerAchievement(Achievements.OBTAIN_STEEL);
+                }
+                if(this.item.itemID == Items.RUBYGLASS.id) {
+                    player.triggerAchievement(Achievements.COLLECT_RUBYGLASS);
+                }
+                if(this.item.itemID == Blocks.PUMICE_WET.id()) {
+                    player.triggerAchievement(Achievements.COLLECT_PUMICE);
                 }
 				if(this.item.itemID == Blocks.ICE.id()) {
 					player.triggerAchievement(Achievements.CRUSH_BLOCKS);
diff --git a/game/core/src/main/java/net/minecraft/core/entity/EntityItemHoming.java b/game/core/src/main/java/net/minecraft/core/entity/EntityItemHoming.java
index 4cd2339ae..cdcc39bc0 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/EntityItemHoming.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/EntityItemHoming.java
@@ -51,7 +51,7 @@ public class EntityItemHoming extends EntityItem {
                 }
 
                 if (this.homingTicks > 10) {
-                    this.noPhysics = true;
+                    setNoPhysics(true);
                 }
             }
         }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/Mob.java b/game/core/src/main/java/net/minecraft/core/entity/Mob.java
index 8b3ded235..153d56e63 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/Mob.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/Mob.java
@@ -286,7 +286,7 @@ public abstract class Mob extends Entity {
             this.livingSoundTime = -getAmbientSoundInterval();
             playLivingSound();
         }
-        if (isAlive() && isInWall() && !this.noPhysics) {
+        if (isAlive() && isInWall() && !hasNoPhysics()) {
             hurt(null, 1, null);
         }
         if (this.fireImmune || this.world.isClientSide) {
@@ -693,7 +693,7 @@ public abstract class Mob extends Entity {
         this.muteStepSounds = false;
         this.isSkating = false;
 
-        if (this.noPhysics) {
+        if (hasNoPhysics()) {
             int vertical = 0;
             if (isSneaking()) vertical--;
             if (this.isJumping) vertical++;
@@ -992,7 +992,7 @@ public abstract class Mob extends Entity {
     }
 
     protected void jump() {
-        if (this.noPhysics) {
+        if (hasNoPhysics()) {
             return;
         }
         this.yd = this.jumpHeight;
diff --git a/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java b/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java
index 12e6eb0e4..f87d16447 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/animal/MobCow.java
@@ -78,17 +78,18 @@ public class MobCow
         return 0.4F;
     }
 
-    @Override
-    public boolean interact(@NotNull Player player) {
-        ItemStack itemstack = player.inventory.getCurrentItem();
-
-        if (itemstack != null && itemstack.getItem() instanceof ItemBucket) {
-            ItemBucket.useBucket(itemstack, player, this.world, ItemBucket.STATE_MILK);
-            return true;
-        }
-
-        return super.interact(player);
-    }
+    // // upstream design makes implementing it here not ideal
+    // @Override
+    // public boolean interact(@NotNull Player player) {
+    //     ItemStack itemstack = player.inventory.getCurrentItem();
+
+    //     if (itemstack != null && itemstack.getItem() instanceof ItemBucket) {
+    //         ItemBucket.useBucket(itemstack, player, this.world, ItemBucket.STATE_MILK);
+    //         return true;
+    //     }
+
+    //     return super.interact(player);
+    // }
 
     @Override
     public boolean isFavouriteItem(ItemStack itemStack)
diff --git a/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonsterArmored.java b/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonsterArmored.java
index 5fbd9c81f..c939dfa40 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonsterArmored.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/monster/MobMonsterArmored.java
@@ -23,7 +23,7 @@ public abstract class MobMonsterArmored<T extends IArmorShape>
 
     protected static final int DATA_ITEM_HELD = 24;
     protected static final int DATA_ARMOR_START = 25;
-    protected static final int FLAG_LEFT_HANDED = 4;
+    protected static final int FLAG_LEFT_HANDED = 5;
 
     public MobMonsterArmored(final @NotNull World world) {
         super(world);
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 f145f1694..6edabf547 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
@@ -3,7 +3,6 @@ package net.minecraft.core.entity.player;
 import com.mojang.nbt.tags.*;
 import net.minecraft.core.block.BlockLogicBed;
 import net.minecraft.core.block.entity.*;
-import net.minecraft.core.block.material.Materials;
 import net.minecraft.core.data.registry.Registries;
 import net.minecraft.core.entity.*;
 import net.minecraft.core.entity.animal.MobPig;
@@ -55,6 +54,9 @@ import net.minecraft.core.world.Dimension;
 import net.minecraft.core.world.ICarriable;
 import net.minecraft.core.world.SpawnerMobs;
 import net.minecraft.core.world.World;
+import net.minecraft.core.world.biome.Biome;
+import net.minecraft.core.world.biome.BiomeTags;
+import net.minecraft.core.world.biome.Biomes;
 import net.minecraft.core.world.chunk.ChunkCoordinates;
 import net.minecraft.core.world.chunk.provider.ChunkProvider;
 import net.minecraft.core.world.pos.TilePos;
@@ -92,6 +94,7 @@ public abstract class Player
     public @NotNull Gamemode gamemode = Gamemodes.SURVIVAL;
     //    public byte userType;
     public int score;
+    private int acidStandingTicks;
     public List<String> messageHistory = new ArrayList<>();
     public float cameraVelocityOld;
     public float cameraVelocity;
@@ -114,6 +117,9 @@ public abstract class Player
     private @Nullable TilePos playerSpawnPoint;
     private @Nullable TilePos lastDeathPoint;
     private @Nullable TilePos startMinecartRidingPoint;
+    public record FluidPlacement(int dimensionId, int x, int y, int z, long tick) {}
+    public @Nullable FluidPlacement lastAcidPlacement;
+    public @Nullable FluidPlacement lastWaterPlacement;
     public int timeUntilPortal;
     protected boolean inPortal;
     public int portalID;
@@ -281,7 +287,7 @@ public abstract class Player
 
         final boolean wSneak = wantSneak();
 
-        if (wSneak || this.noPhysics) {
+        if (wSneak || hasNoPhysics()) {
             this.sneaking = wSneak;
         } else {
             if (boundsClear(getBoundsForState(MOTION_STATE_DEFAULT))) {
@@ -313,6 +319,23 @@ public abstract class Player
             }
         }
         super.tick();
+        if (!this.world.isClientSide
+            && this.world.getDimension() == Dimension.NETHER
+            && this.tickCount % 100 == 0) {
+            final Biome biome = this.world.getBlockBiome(new TilePos(this));
+            if (biome.hasTag(BiomeTags.HAS_SULFUR_POOLS)) {
+                addStat(Achievements.DISCOVER_SULFURPOOLS, 1);
+            }
+            if (biome == Biomes.NETHER_VOLCANIC_ISLANDS) {
+                addStat(Achievements.DISCOVER_VOLCANICISLANDS, 1);
+            }
+            if (biome.hasTag(BiomeTags.HAS_CRYSTAL_VEINS)) {
+                addStat(Achievements.DISCOVER_CRYSTALFOREST, 1);
+            }
+            if (biome.hasTag(BiomeTags.HAS_SURFACE_ASH)) {
+                addStat(Achievements.DISCOVER_OLDWORLD, 1);
+            }
+        }
         if (this.heldObject != null) {
             this.heldObject.heldTick(this.world, this);
         }
@@ -454,9 +477,20 @@ public abstract class Player
             this.acidExitDelayLeft = 0;
             this.acidPostExitPeak = 0;
             this.acidPostExitDrainTicksLeft = 0;
+            this.acidStandingTicks = 0;
             return;
         }
         super.acidTick();
+        if (!this.world.isClientSide) {
+            if (isInAcid()) {
+                this.acidStandingTicks++;
+                if (this.acidStandingTicks > 200) {
+                    triggerAchievement(Achievements.ACID_BATH);
+                }
+            } else {
+                this.acidStandingTicks = 0;
+            }
+        }
     }
 
     public boolean shouldShowAcidVisualEffects() {
@@ -794,7 +828,7 @@ public abstract class Player
 
     @Override
     public void push(@NotNull Entity entity) {
-        if (this.noPhysics || !getGamemode().canInteract()) return;
+        if (hasNoPhysics() || !getGamemode().canInteract()) return;
         super.push(entity);
     }
 
@@ -905,7 +939,7 @@ public abstract class Player
         } else {
             this.setGamemodeOnLogin(Gamemodes.SURVIVAL);
         }
-        this.noPhysics = tag.getBoolean("Noclip") && this.gamemode.hasPlayerFlight();
+        setNoPhysics(tag.getBoolean("Noclip") && this.gamemode.hasPlayerFlight());
         this.sneaking = tag.getBoolean("Sneaking");
         if (this.sleeping) {
             this.bedTilePos = new TilePos(this);
@@ -933,7 +967,7 @@ public abstract class Player
         tag.putBoolean("Sleeping", this.sleeping);
         tag.putShort("SleepTimer", (short) this.sleepTimer);
         tag.putString("Gamemode", this.gamemode.getId());
-        tag.putBoolean("Noclip", this.noPhysics && this.gamemode.hasPlayerFlight());
+        tag.putBoolean("Noclip", hasNoPhysics() && this.gamemode.hasPlayerFlight());
         tag.putBoolean("Sneaking", this.sneaking);
         if (this.playerSpawnPoint != null) {
             tag.putInt("SpawnX", this.playerSpawnPoint.x);
@@ -1472,6 +1506,9 @@ public abstract class Player
         addStat(statbase, 1);
     }
 
+    public void revokeAchievement(Stat statbase) {
+    }
+
     public void addStat(@Nullable Stat stat, int i) {
     }
 
@@ -1640,7 +1677,7 @@ public abstract class Player
         }
         this.inventoryMenu = newContainer;
         if (!gamemode.hasPlayerFlight()) {
-            this.noPhysics = false;
+            setNoPhysics(false);
         }
 
         this.fireImmune = gamemode.hasFireImmunity();
@@ -1652,8 +1689,8 @@ public abstract class Player
     }
 
     public void setNoclip(final boolean noclip) {
-        this.noPhysics = noclip;
-        if (this.noPhysics) {
+        setNoPhysics(noclip);
+        if (hasNoPhysics()) {
             this.yd += 0.079f;
         }
     }
diff --git a/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileArrowGolden.java b/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileArrowGolden.java
index 4ef9a4701..a0ca22147 100644
--- a/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileArrowGolden.java
+++ b/game/core/src/main/java/net/minecraft/core/entity/projectile/ProjectileArrowGolden.java
@@ -12,21 +12,21 @@ public class ProjectileArrowGolden extends ProjectileArrow
     public ProjectileArrowGolden(World world)
     {
         super(world, TYPE_GOLDEN);
-        this.noPhysics = true;
+        setNoPhysics(true);
         stack = new ItemStack(Items.AMMO_ARROW_GOLD);
     }
 
     public ProjectileArrowGolden(World world, double x, double y, double z)
     {
         super(world, x, y, z, TYPE_GOLDEN);
-        this.noPhysics = true;
+        setNoPhysics(true);
         stack = new ItemStack(Items.AMMO_ARROW_GOLD);
     }
 
     public ProjectileArrowGolden(World world, Mob owner, boolean doesArrowBelongToPlayer)
     {
         super(world, owner, doesArrowBelongToPlayer, TYPE_GOLDEN);
-        this.noPhysics = true;
+        setNoPhysics(true);
         stack = new ItemStack(Items.AMMO_ARROW_GOLD);
     }
 
diff --git a/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java b/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java
index 19fb9d14e..198181725 100644
--- a/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java
+++ b/game/core/src/main/java/net/minecraft/core/item/ItemBucket.java
@@ -1,6 +1,7 @@
 package net.minecraft.core.item;
 
 import com.mojang.nbt.tags.CompoundTag;
+import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.BlockLogicFluid;
 import net.minecraft.core.block.Blocks;
@@ -30,6 +31,8 @@ import org.joml.primitives.AABBd;
 import java.util.*;
 import java.util.function.BiConsumer;
 
+// TODO: the abstractions here might be too overcomplicatted
+// ik reworking things might be a pain for modders but this really is not designed in a good way
 public abstract class ItemBucket extends ItemFood implements IItemContainer {
 
     /**
@@ -244,14 +247,6 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
         // return STATE_EMPTY.equals(currentStateId) || getCharges(selfStack) < this.maxCharges;
     }
 
-    @Override
-    public boolean interactsWithEntity(@NotNull ItemStack selfStack, @NotNull Entity entity) {
-        if (!super.interactsWithEntity(selfStack, entity)) return false;
-        final NamespaceID currentStateId = getState(selfStack);
-        final BucketState currentState = getBucketState(currentStateId);
-        return currentState.canPlace() || !currentState.isEdible();
-    }
-
     @Override
     public @Nullable ItemStack onUse(@NotNull ItemStack selfStack, @NotNull World world, @NotNull Player player) {
         final NamespaceID currentStateId = getState(selfStack);
@@ -303,19 +298,11 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
 
     @Override
     public boolean useOnEntity(@NotNull ItemStack selfStack, @NotNull Player player, @NotNull Mob mob) {
-        final NamespaceID currentStateId = getState(selfStack);
-        final BucketState currentState = getBucketState(currentStateId);
-
-        if (currentState.isEdible() && !currentState.canPlace()) return false;
-
-        final int oldSize = selfStack.stackSize;
-        var newStack = handleEntityClick(selfStack, mob.world, player, mob);
-        if (newStack.stackSize <= 0) newStack = null;
-
-        final boolean isDifferent = oldSize != newStack.stackSize || newStack != selfStack;
-        if (isDifferent) player.inventory.setItem(player.inventory.getCurrentSlot(), newStack);
+        if (mob instanceof MobCow) {
+            return useBucket(selfStack, player, player.world, STATE_MILK);
+        }
 
-        return isDifferent;
+        return false;
     }
 
     // @Override
@@ -392,6 +379,9 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
         if (STATE_EMPTY.equals(currentState)) {
             setState(itemStack, targetState);
             setCharges(itemStack, 1);
+            if (STATE_ACID.equals(targetState) && !world.isClientSide) {
+                player.addStat(Achievements.COLLECT_ACID, 1);
+            }
         } else {
             setCharges(itemStack, getCharges(itemStack) + 1);
         }
@@ -422,6 +412,16 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
         final Block<?> replacedBlock = world.getBlockType(tilePos);
         final int replacedData = world.getBlockData(tilePos);
 
+        if (!world.isClientSide) {
+            if (STATE_ACID.equals(stateId)) {
+                player.lastAcidPlacement = new Player.FluidPlacement(
+                    world.getDimension().id, tilePos.x(), tilePos.y(), tilePos.z(), world.getTotalWorldTime());
+            } else if (STATE_WATER.equals(stateId)) {
+                player.lastWaterPlacement = new Player.FluidPlacement(
+                    world.getDimension().id, tilePos.x(), tilePos.y(), tilePos.z(), world.getTotalWorldTime());
+            }
+        }
+
         if (!world.setBlockTypeNotify(tilePos, bucketState.fluidBlock())) return result;
 
         replacedBlock.dropWithCause(world, EnumDropCause.WORLD, tilePos, replacedData, null, null);
@@ -448,9 +448,9 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
         return itemStack;
     }
 
-    protected ItemStack handleEntityClick(@NotNull ItemStack itemStack, @NotNull World world, @NotNull Player player, @NotNull Entity mob) {
-        return handleAirClick(itemStack, world, player);
-    }
+    // protected ItemStack handleEntityClick(@NotNull ItemStack itemStack, @NotNull World world, @NotNull Player player, @NotNull Entity mob) {
+    //     return handleAirClick(itemStack, world, player);
+    // }
 
     protected ItemStack handleEdibleConsumption(@NotNull ItemStack itemStack, @NotNull World world, @NotNull Player player) {
         NamespaceID state = getState(itemStack);
@@ -472,22 +472,23 @@ public abstract class ItemBucket extends ItemFood implements IItemContainer {
         return itemStack;
     }
 
-    public static void useBucket(ItemStack itemStack, Player player, World world, NamespaceID targetState) {
+    public static boolean useBucket(ItemStack itemStack, Player player, World world, NamespaceID targetState) {
         boolean isStacked = itemStack.stackSize > 1;
         NamespaceID currentState = getState(itemStack);
 
         int limit = (itemStack.getItem() instanceof ItemBucket bucket) ? bucket.maxCharges : 1;
 
-        if (STATE_EMPTY.equals(currentState) || (currentState.equals(targetState) && getCharges(itemStack) < limit)) {
-            if (!isStacked) {
-                if (STATE_EMPTY.equals(currentState)) {
-                    setState(itemStack, targetState);
-                }
-                setCharges(itemStack, getCharges(itemStack) + 1);
-            } else {
-                splitBucket(itemStack, player, targetState, world);
+        final boolean canUse = STATE_EMPTY.equals(currentState) || (currentState.equals(targetState) && getCharges(itemStack) < limit);
+        if (!canUse) return false;
+        if (!isStacked) {
+            if (STATE_EMPTY.equals(currentState)) {
+                setState(itemStack, targetState);
             }
+            setCharges(itemStack, getCharges(itemStack) + 1);
+        } else {
+            splitBucket(itemStack, player, targetState, world);
         }
+        return true;
     }
 
     protected static ItemStack splitBucket(@NotNull ItemStack itemStack, @NotNull Player player, NamespaceID state, @NotNull World world) {
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 99c349963..cc2f87441 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
@@ -4,20 +4,20 @@ import com.mojang.brigadier.Command;
 import com.mojang.brigadier.CommandDispatcher;
 import com.mojang.brigadier.builder.ArgumentBuilderLiteral;
 import com.mojang.brigadier.builder.ArgumentBuilderRequired;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
 import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
 import net.minecraft.core.achievement.Achievement;
 import net.minecraft.core.achievement.Achievements;
-import net.minecraft.core.entity.Entity;
-import net.minecraft.core.entity.Mob;
 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.arguments.ArgumentTypeAchievement;
-import net.minecraft.core.net.command.arguments.ArgumentTypeEntity;
-import net.minecraft.core.net.command.helpers.EntitySelector;
+import net.minecraft.core.net.command.exceptions.CommandExceptions;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 public class CommandAchievement implements CommandManager.CommandRegistry {
@@ -27,70 +27,130 @@ public class CommandAchievement implements CommandManager.CommandRegistry {
         dispatcher.register(((ArgumentBuilderLiteral.<CommandSource>literal("achievement"))
             .requires(CommandSource::hasAdmin)
             .then((ArgumentBuilderLiteral.<CommandSource>literal("grant"))
-                .then(ArgumentBuilderRequired.<CommandSource, EntitySelector>argument("entities", ArgumentTypeEntity.usernames())
-                    .then(ArgumentBuilderRequired.<CommandSource, Achievement>argument("achievement", ArgumentTypeAchievement.achievement())
-                        .executes(c -> {
-                            CommandSource source = c.getSource();
-                            List<? extends Entity> entities = c.getArgument("entities", EntitySelector.class).get(source);
-                            Achievement achievement = c.getArgument("achievement", Achievement.class);
-
-                            if (entities.size() == 1 && ((Player) entities.get(0)).getStat(achievement) != 0) {
-                                throw PLAYER_ALREADY_HAS_ACHIEVEMENT.create();
-                            }
-
-                            List<Achievement> achievements = new ArrayList<>();
-                            achievements.add(achievement);
-
-                            while (achievements.get(achievements.size() - 1).parent != null) {
-                                achievements.add(achievements.get(achievements.size() - 1).parent);
-                            }
-                            for (int i = 0; i < achievements.size(); i++) {
-                                for (Entity entity : entities) {
-                                    ((Player) entity).triggerAchievement(achievements.get(achievements.size() - 1 - i));
-                                }
-                            }
-
-                            sendContextualMessage(source, entities, achievement);
-
-                            return Command.SINGLE_SUCCESS;
-                        }))
-                    .then(ArgumentBuilderLiteral.<CommandSource>literal("*")
-                        .executes(c -> {
-                            CommandSource source = c.getSource();
-                            List<? extends Entity> entities = c.getArgument("entities", EntitySelector.class).get(source);
-
-                            for (Achievement achievement : Achievements.achievementList) {
-                                List<Achievement> achievements = new ArrayList<>();
-                                achievements.add(achievement);
-                                while (achievements.get(achievements.size() - 1).parent != null) {
-                                    achievements.add(achievements.get(achievements.size() - 1).parent);
-                                }
-                                for (int i = 0; i < achievements.size(); i++) {
-                                    for (Entity entity : entities) {
-                                        ((Player) entity).triggerAchievement(achievements.get(achievements.size() - 1 - i));
-                                    }
-                                }
-                            }
-
-                            sendWildcardContextualMessage(source, entities);
-
-                            return Command.SINGLE_SUCCESS;
-                        }))))));
+                .then(ArgumentBuilderRequired.<CommandSource, Achievement>argument("achievement", ArgumentTypeAchievement.achievement())
+                    .executes(c -> {
+                        CommandSource source = c.getSource();
+                        Player player = requireSender(source);
+                        Achievement achievement = c.getArgument("achievement", Achievement.class);
+
+                        if (player.getStat(achievement) != 0) {
+                            throw PLAYER_ALREADY_HAS_ACHIEVEMENT.create();
+                        }
+
+                        grantAchievements(player, collectWithAncestors(achievement));
+                        source.sendTranslatableMessage("command.commands.achievement.grant.success_single_entity", achievement.getStatName().trim(), player.getDisplayName());
+
+                        return Command.SINGLE_SUCCESS;
+                    }))
+                .then(ArgumentBuilderLiteral.<CommandSource>literal("*")
+                    .executes(c -> {
+                        CommandSource source = c.getSource();
+                        Player player = requireSender(source);
+
+                        for (Achievement achievement : Achievements.achievementList) {
+                            grantAchievements(player, collectWithAncestors(achievement));
+                        }
+
+                        source.sendTranslatableMessage("command.commands.achievement.grant.all.success_single_entity", player.getDisplayName());
+
+                        return Command.SINGLE_SUCCESS;
+                    })))
+            .then((ArgumentBuilderLiteral.<CommandSource>literal("revoke"))
+                .then(ArgumentBuilderRequired.<CommandSource, Achievement>argument("achievement", ArgumentTypeAchievement.achievement())
+                    .executes(c -> {
+                        CommandSource source = c.getSource();
+                        Player player = requireSender(source);
+                        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());
+
+                        return Command.SINGLE_SUCCESS;
+                    }))
+                .then(ArgumentBuilderLiteral.<CommandSource>literal("*")
+                    .executes(c -> {
+                        CommandSource source = c.getSource();
+                        Player player = requireSender(source);
+
+                        revokeAchievements(player, collectAllAchievementsByDepthDesc());
+                        source.sendTranslatableMessage("command.commands.achievement.revoke.all.success_single_entity", player.getDisplayName());
+
+                        return Command.SINGLE_SUCCESS;
+                    })))));
+    }
+
+    private static @NotNull Player requireSender(CommandSource source) throws CommandSyntaxException {
+        Player player = source.getSender();
+        if (player == null) {
+            throw CommandExceptions.notInWorld().create();
+        }
+        return player;
+    }
+
+    private static void grantAchievements(Player player, List<Achievement> achievements) {
+        for (int i = 0; i < achievements.size(); i++) {
+            Achievement achievement = achievements.get(achievements.size() - 1 - i);
+            player.triggerAchievement(achievement);
+        }
     }
 
-    private static void sendContextualMessage(CommandSource source, List<? extends Entity> entities, Achievement achievement) {
-        if (entities.size() == 1) {
-            source.sendTranslatableMessage("command.commands.achievement.grant.success_single_entity", achievement.getStatName().trim(), ((Mob) entities.get(0)).getDisplayName());
-        } else {
-            source.sendTranslatableMessage("command.commands.achievement.grant.success_multiple_entities", achievement.getStatName(), entities.size());
+    private static void revokeAchievements(Player player, List<Achievement> achievements) {
+        for (Achievement achievement : achievements) {
+            player.revokeAchievement(achievement);
+        }
+    }
+
+    private static List<Achievement> collectWithAncestors(Achievement achievement) {
+        List<Achievement> achievements = new ArrayList<>();
+        achievements.add(achievement);
+
+        while (achievements.get(achievements.size() - 1).parent != null) {
+            achievements.add(achievements.get(achievements.size() - 1).parent);
+        }
+
+        return achievements;
+    }
+
+    private static List<Achievement> collectWithDescendants(Achievement achievement) {
+        List<Achievement> achievements = new ArrayList<>();
+
+        for (Achievement candidate : Achievements.achievementList) {
+            if (isDescendantOf(candidate, achievement)) {
+                achievements.add(candidate);
+            }
+        }
+
+        achievements.sort(Comparator.comparingInt(CommandAchievement::getDepth).reversed());
+        return achievements;
+    }
+
+    private static List<Achievement> collectAllAchievementsByDepthDesc() {
+        List<Achievement> achievements = new ArrayList<>(Achievements.achievementList);
+        achievements.sort(Comparator.comparingInt(CommandAchievement::getDepth).reversed());
+        return achievements;
+    }
+
+    private static boolean isDescendantOf(Achievement achievement, Achievement ancestor) {
+        if (achievement == ancestor) {
+            return true;
+        }
+        Achievement parent = achievement.parent;
+        while (parent != null) {
+            if (parent == ancestor) {
+                return true;
+            }
+            parent = parent.parent;
         }
+        return false;
     }
 
-    private static void sendWildcardContextualMessage(CommandSource source, List<? extends Entity> entities) {
-        if (entities.size() == 1) {
-            source.sendTranslatableMessage("command.commands.achievement.grant.all.success_single_entity", ((Mob) entities.get(0)).getDisplayName());
-        } else {
-            source.sendTranslatableMessage("command.commands.achievement.grant.all.success_multiple_entities", entities.size());
+    private static int getDepth(Achievement achievement) {
+        int depth = 0;
+        Achievement parent = achievement.parent;
+        while (parent != null) {
+            depth++;
+            parent = parent.parent;
         }
+        return depth;
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/net/packet/PacketStatistic.java b/game/core/src/main/java/net/minecraft/core/net/packet/PacketStatistic.java
index a7e0005fd..7c8d649e5 100644
--- a/game/core/src/main/java/net/minecraft/core/net/packet/PacketStatistic.java
+++ b/game/core/src/main/java/net/minecraft/core/net/packet/PacketStatistic.java
@@ -1,7 +1,6 @@
 package net.minecraft.core.net.packet;
 
 import net.minecraft.core.net.handler.PacketHandler;
-import net.minecraft.core.util.HardIllegalArgumentException;
 import net.minecraft.core.util.collection.NamespaceID;
 
 import java.io.*;
diff --git a/game/core/src/main/java/net/minecraft/core/net/packet/PacketUpdatePlayerState.java b/game/core/src/main/java/net/minecraft/core/net/packet/PacketUpdatePlayerState.java
index 596b23723..ea700c1ed 100644
--- a/game/core/src/main/java/net/minecraft/core/net/packet/PacketUpdatePlayerState.java
+++ b/game/core/src/main/java/net/minecraft/core/net/packet/PacketUpdatePlayerState.java
@@ -9,6 +9,8 @@ public class PacketUpdatePlayerState extends Packet
     public static final int STATE_SNEAK = 1;
     public static final int STATE_UN_SNEAK = 2;
     public static final int STATE_LEAVE_BED = 3;
+    public static final int STATE_NO_CLIP = 4;
+    public static final int STATE_DO_CLIP = 5;
     public int state;
 
     public PacketUpdatePlayerState()
diff --git a/game/core/src/main/java/net/minecraft/core/player/inventory/InventorySorter.java b/game/core/src/main/java/net/minecraft/core/player/inventory/InventorySorter.java
index d2b418fa6..6c14a87ce 100644
--- a/game/core/src/main/java/net/minecraft/core/player/inventory/InventorySorter.java
+++ b/game/core/src/main/java/net/minecraft/core/player/inventory/InventorySorter.java
@@ -1,8 +1,11 @@
 package net.minecraft.core.player.inventory;
 
+import it.unimi.dsi.fastutil.ints.IntSet;
 import net.minecraft.core.item.ItemStack;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 
 public abstract class InventorySorter {
@@ -11,28 +14,21 @@ public abstract class InventorySorter {
     }
 
     public static void sortInventory(final ItemStack[] inventory, final int startIndex, final int endIndex) {
+        sortInventory(inventory, null, startIndex, endIndex);
+    }
+
+    public static void sortInventory(final ItemStack[] inventory, final @Nullable IntSet lockedSlots, final int startIndex, final int endIndex) {
         final List<ItemStack> items = new ArrayList<>();
 
         for (int i = startIndex; i <= endIndex; i++) {
-            if (inventory[i] == null) {
+            if (inventory[i] == null || (lockedSlots != null && lockedSlots.contains(i))) {
                 continue;
             }
             items.add(inventory[i]);
             inventory[i] = null;
         }
 
-        items.sort((a, b) -> {
-            final int aId = a.itemID;
-            final int bId = b.itemID;
-
-            if (aId == bId) {
-                return 0;
-            }
-            if (aId < bId) {
-                return -1;
-            }
-            return 1;
-        });
+        items.sort(Comparator.comparingInt(a -> a.itemID));
 
         // Merge stacks
         for (int i = 0; i < items.size(); i++) {
@@ -80,8 +76,16 @@ public abstract class InventorySorter {
             return 1;
         });
 
-        for (int i = 0; i < items.size(); i++) {
-            inventory[startIndex + i] = items.get(i);
+        int invIndex = startIndex;
+        for (int i = 0; i < items.size();) {
+            if (lockedSlots != null && lockedSlots.contains(invIndex)) {
+                invIndex++;
+                continue;
+            }
+            inventory[invIndex] = items.get(i);
+
+            invIndex++;
+            i++;
         }
     }
 }
diff --git a/game/core/src/main/java/net/minecraft/core/player/inventory/container/ContainerInventory.java b/game/core/src/main/java/net/minecraft/core/player/inventory/container/ContainerInventory.java
index ebfa5e378..df3040bd7 100644
--- a/game/core/src/main/java/net/minecraft/core/player/inventory/container/ContainerInventory.java
+++ b/game/core/src/main/java/net/minecraft/core/player/inventory/container/ContainerInventory.java
@@ -4,6 +4,8 @@ package net.minecraft.core.player.inventory.container;
 import com.mojang.nbt.tags.CompoundTag;
 import com.mojang.nbt.tags.ListTag;
 
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntSet;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.block.tag.BlockTags;
 import net.minecraft.core.entity.player.Player;
@@ -422,7 +424,10 @@ public class ContainerInventory
 
     @Override
     public void sort() {
-        InventorySorter.sortInventory(this.mainInventory, HOTBAR_SIZE, MAIN_INVENTORY_SIZE - 1);
+        class Cache {private static final IntSet intSet = new IntArraySet();}
+        Cache.intSet.clear();
+        if (currentSlotLocked()) Cache.intSet.add(this.currentSlot);
+        InventorySorter.sortInventory(this.mainInventory, Cache.intSet, HOTBAR_SIZE, MAIN_INVENTORY_SIZE - 1);
     }
 
     public void transferAllContents(final @NotNull ContainerInventory inventory) {
diff --git a/game/core/src/main/java/net/minecraft/core/player/inventory/menu/MenuAbstract.java b/game/core/src/main/java/net/minecraft/core/player/inventory/menu/MenuAbstract.java
index 21e256655..f558bd76f 100644
--- a/game/core/src/main/java/net/minecraft/core/player/inventory/menu/MenuAbstract.java
+++ b/game/core/src/main/java/net/minecraft/core/player/inventory/menu/MenuAbstract.java
@@ -710,12 +710,12 @@ public abstract class MenuAbstract
 				final var srcStack = slot.getItemStack();
 				if (srcStack == null) {
 					final var stack = draggingItemStack.copy();
-					stack.stackSize = stack.getMaxStackSize();
+					stack.stackSize = Math.min(stack.getMaxStackSize(), slot.getMaxStackSize());
 					slot.set(stack);
 					slot.setChanged();
 				} else {
 					if (!srcStack.canStackWith(draggingItemStack)) continue;
-					srcStack.stackSize = srcStack.getMaxStackSize();
+					srcStack.stackSize = Math.min(srcStack.getMaxStackSize(), slot.getMaxStackSize());
 					slot.setChanged();
 				}
 			}
@@ -747,7 +747,7 @@ public abstract class MenuAbstract
 			int amount = itemsPerSlot;
 			amount = Math.min(amount, slot.getMaxStackSize());
 			if(stackInSlot != null) {
-				amount = Math.min(amount, stackInSlot.getMaxStackSize() - stackInSlot.stackSize);
+				amount = Math.min(amount, Math.min(stackInSlot.getMaxStackSize(), slot.getMaxStackSize()) - stackInSlot.stackSize);
 			}
 			amount = Math.min(amount, draggingItemStack.stackSize);
 			if(amount <= 0) {
diff --git a/game/core/src/main/java/net/minecraft/core/player/inventory/slot/SlotResult.java b/game/core/src/main/java/net/minecraft/core/player/inventory/slot/SlotResult.java
index 3e57f99a4..6bd157f89 100644
--- a/game/core/src/main/java/net/minecraft/core/player/inventory/slot/SlotResult.java
+++ b/game/core/src/main/java/net/minecraft/core/player/inventory/slot/SlotResult.java
@@ -3,6 +3,7 @@ package net.minecraft.core.player.inventory.slot;
 import net.minecraft.core.achievement.Achievements;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.data.registry.Registries;
+import net.minecraft.core.data.registry.recipe.entry.RecipeEntryCrafting;
 import net.minecraft.core.entity.player.Player;
 import net.minecraft.core.item.*;
 import net.minecraft.core.item.tool.ItemToolHoe;
@@ -10,6 +11,7 @@ import net.minecraft.core.item.tool.ItemToolPickaxe;
 import net.minecraft.core.item.tool.ItemToolSword;
 import net.minecraft.core.player.inventory.container.Container;
 import net.minecraft.core.player.inventory.container.ContainerCrafting;
+import org.jetbrains.annotations.Nullable;
 
 public class SlotResult extends Slot
 {
@@ -60,7 +62,16 @@ public class SlotResult extends Slot
 				|| itemStack.itemID == Items.ARMOR_LEGGINGS_CHAINMAIL.id) {
 			thePlayer.addStat(Achievements.REPAIR_ARMOR, 1);
 		}
+		if(item.id == Items.INGOT_STEEL.id) thePlayer.addStat(Achievements.OBTAIN_STEEL, 1);
+		if(item.id == Items.BUCKET_STEEL.id) thePlayer.addStat(Achievements.CRAFT_STEEL_BUCKET, 1);
+		if(item.id == Blocks.CONDUIT.id()) thePlayer.addStat(Achievements.CRAFT_CONDUIT, 1);
 
+		if (!thePlayer.world.isClientSide) {
+			final @Nullable RecipeEntryCrafting<?, ?> recipe = Registries.RECIPES.findMatchingCraftingRecipe((ContainerCrafting) craftSlots);
+			if (recipe != null && recipe.toString().startsWith("minecraft:workbench/acid_conversion_")) {
+				thePlayer.addStat(Achievements.CRAFT_ACID_COBBLE_TO_STONE, 1);
+			}
+		}
 		Registries.RECIPES.onCraftResult((ContainerCrafting) craftSlots);
 	}
     
diff --git a/game/core/src/main/java/net/minecraft/core/world/Explosion.java b/game/core/src/main/java/net/minecraft/core/world/Explosion.java
index 9fd51bf2c..7a0caad5e 100644
--- a/game/core/src/main/java/net/minecraft/core/world/Explosion.java
+++ b/game/core/src/main/java/net/minecraft/core/world/Explosion.java
@@ -25,7 +25,7 @@ public class Explosion {
     protected final @NotNull World world;
     public final @Nullable Entity exploder;
     public boolean isFlaming;
-    protected Random ExplosionRNG;
+    protected Random explosionRNG;
     public double explosionX;
     public double explosionY;
     public double explosionZ;
@@ -35,7 +35,7 @@ public class Explosion {
 
     public Explosion(@NotNull World world, @Nullable Entity exploder, double x, double y, double z, float explosionSize) {
         this.isFlaming = false;
-        this.ExplosionRNG = new Random();
+        this.explosionRNG = new Random();
         this.destroyedBlockPositions = new HashSet<>();
         this.world = world;
         this.exploder = exploder;
@@ -175,7 +175,7 @@ public class Explosion {
                     float proc = 1 - (((IArmorWearing<?>) entity).getTotalProtectionAmount(DamageType.BLAST) / 2f);
                     flingForce *= proc;
                 }
-                if (!entity.noPhysics) {
+                if (!entity.hasNoPhysics()) {
                     entity.fling(xComp * flingForce, yComp * flingForce, zComp * flingForce, 1);
                 }
             }
@@ -191,7 +191,7 @@ public class Explosion {
             int y1 = chunkposition.y;
             int z1 = chunkposition.z;
 
-            if (this.world.getBlockId(x1, y1, z1) == Blocks.AIR.id() && Blocks.solid[this.world.getBlockId(x1, y1 - 1, z1)] && this.ExplosionRNG.nextInt(3) == 0) {
+            if (this.world.getBlockId(x1, y1, z1) == Blocks.AIR.id() && Blocks.solid[this.world.getBlockId(x1, y1 - 1, z1)] && this.explosionRNG.nextInt(3) == 0) {
                 this.world.setBlockWithNotify(x1, y1, z1, Blocks.FIRE.id());
             }
         }
diff --git a/game/core/src/main/java/net/minecraft/core/world/ExplosionCannonball.java b/game/core/src/main/java/net/minecraft/core/world/ExplosionCannonball.java
index a00844f49..c9f9b2674 100644
--- a/game/core/src/main/java/net/minecraft/core/world/ExplosionCannonball.java
+++ b/game/core/src/main/java/net/minecraft/core/world/ExplosionCannonball.java
@@ -53,7 +53,7 @@ public class ExplosionCannonball extends Explosion {
                     float proc = 1 - entArmored.getTotalProtectionAmount(DamageType.BLAST) / 2f;
                     flingForce *= proc;
                 }
-                if (!entity.noPhysics) {
+                if (!entity.hasNoPhysics()) {
                     entity.fling(xComp * flingForce, yComp * flingForce, zComp * flingForce, 1);
                 }
             }
@@ -69,9 +69,9 @@ public class ExplosionCannonball extends Explosion {
             Vector3d d = new Vector3d(0, 0, 1);
             for (int i = 0; i < 64; i++) {
                 d.set(0, 0, 0.25);
-                d.rotateY(ExplosionRNG.nextDouble() * Math.PI_TIMES_2);
-                d.rotateX(ExplosionRNG.nextDouble() * Math.PI_TIMES_2);
-                d.mul(1 - ExplosionRNG.nextDouble() * 0.25d);
+                d.rotateY(explosionRNG.nextDouble() * Math.PI_TIMES_2);
+                d.rotateX(explosionRNG.nextDouble() * Math.PI_TIMES_2);
+                d.mul(1 - explosionRNG.nextDouble() * 0.25d);
                 this.world.spawnParticle("explode", this.explosionX + d.x(), this.explosionY + d.y(), this.explosionZ + d.z(), d.x() * 2, d.y() * 2, d.z() * 2, 0, false);
                 this.world.spawnParticle("smoke", this.explosionX + d.x(), this.explosionY + d.y(), this.explosionZ + d.z(), d.x() * 2, d.y() * 2, d.z() * 2, 0, false);
             }
diff --git a/game/core/src/main/java/net/minecraft/core/world/biome/provider/BiomeProviderNether.java b/game/core/src/main/java/net/minecraft/core/world/biome/provider/BiomeProviderNether.java
index 199476ffc..3f5b4bce2 100644
--- a/game/core/src/main/java/net/minecraft/core/world/biome/provider/BiomeProviderNether.java
+++ b/game/core/src/main/java/net/minecraft/core/world/biome/provider/BiomeProviderNether.java
@@ -176,14 +176,12 @@ public final class BiomeProviderNether extends BiomeProvider {
                 0.5, 1.0,
                 0.0, 1.0));
 
-        // Mid/Warm Biome
         brm.addRange(Biomes.NETHER_SHELF, new BiomeRange(
                 0.70, 0.75,
                 0.0, 1.0,
                 0.5, 1.0,
                 0.0, 1.0));
 
-        // Mid/Cool Biomes
         brm.addRange(Biomes.NETHER_CRYSTAL_FOREST, new BiomeRange(
                 0.45, 0.70,
                 0.1, 1.0,
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/LavaFloeLargeFeature.java b/game/core/src/main/java/net/minecraft/core/world/generate/LavaFloeLargeFeature.java
index 0358fc8aa..0108944dd 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/LavaFloeLargeFeature.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/LavaFloeLargeFeature.java
@@ -74,7 +74,8 @@ public class LavaFloeLargeFeature
                             if(random.nextInt(20) == 0) {
                                 this.trySetBlock(result, x, y, z, Blocks.PUMICE_WET.id());
                             } else {
-                                this.trySetBlock(result, x, y, z, Blocks.COBBLE_NETHERRACK.id());
+                                int floeBlockId = random.nextInt(2) == 0 ? Blocks.NETHERRACK.id() : Blocks.COBBLE_NETHERRACK.id();
+                                this.trySetBlock(result, x, y, z, floeBlockId);
                             }
                         }
                         for (int y = oceanY + (int) (height - 2); y < oceanY + (height + 1); y++) {
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/RubyglassFloeLargeFeature.java b/game/core/src/main/java/net/minecraft/core/world/generate/RubyglassFloeLargeFeature.java
index 39cb00389..9d4f333b3 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/RubyglassFloeLargeFeature.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/RubyglassFloeLargeFeature.java
@@ -63,7 +63,8 @@ public class RubyglassFloeLargeFeature extends LargeFeature {
 						}
 
 						for (int y = oceanY; y < oceanY + depth; ++y) {
-							this.trySetBlock(result, x, y, z, Blocks.COBBLE_NETHERRACK.id());
+							int floeBlockId = random.nextInt(2) == 0 ? Blocks.NETHERRACK.id() : Blocks.COBBLE_NETHERRACK.id();
+							this.trySetBlock(result, x, y, z, floeBlockId);
 						}
 					}
 				}
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/ChunkDecoratorNether.java b/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/ChunkDecoratorNether.java
index efa7e670d..b1d2967af 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/ChunkDecoratorNether.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/ChunkDecoratorNether.java
@@ -3,6 +3,8 @@ package net.minecraft.core.world.generate.chunk.perlin.nether;
 import net.minecraft.core.block.Block;
 import net.minecraft.core.block.BlockLogicOreNetherCoal;
 import net.minecraft.core.block.BlockLogicNodeRubyglass;
+import net.minecraft.core.block.BlockLogicSoulSand;
+import net.minecraft.core.block.BlockLogicSulfur;
 import net.minecraft.core.block.Blocks;
 import net.minecraft.core.world.World;
 import net.minecraft.core.world.biome.Biome;
@@ -113,6 +115,28 @@ public class ChunkDecoratorNether
                 .withPositionSelector(PositionSelectors.HeightRangeUniform)
                 .withPlacementMethod(new PlacementMethod.TriesPerChunk(1000)));
 
+        this.register("minecraft:decoration/nether/default/sulfur_pocket", (new ChunkDecorationBuilder(new WorldFeatureOre(BlockLogicSulfur.pocketVariantMap, 24)))
+                .withBiomeMask(new Biome[]{Biomes.NETHER_SULFUR_POOLS})
+                .withPositionSelector(PositionSelectors.HeightRangeUniform)
+                .withPlacementMethod((feature, world, chunk, random) -> {
+                    int rangeY = world.getWorldType().getMaxY(world) - world.getWorldType().getMinY(world) + 1;
+                    int tries = (int) (20 * (rangeY / 128f));
+                    for (int i = 0; i < tries; i++) {
+                        feature.placeFeature(world, chunk, random);
+                    }
+                }));
+
+        this.register("minecraft:decoration/nether/default/soulsand_pocket", (new ChunkDecorationBuilder(new WorldFeatureOre(BlockLogicSoulSand.pocketVariantMap, 32)))
+                .withBiomeMask(new Biome[]{Biomes.NETHER_OLD_WORLD, Biomes.NETHER_OLD_WORLD_DESERT})
+                .withPositionSelector(PositionSelectors.HeightRangeUniform)
+                .withPlacementMethod((feature, world, chunk, random) -> {
+                    int rangeY = world.getWorldType().getMaxY(world) - world.getWorldType().getMinY(world) + 1;
+                    int tries = (int) (20 * (rangeY / 128f));
+                    for (int i = 0; i < tries; i++) {
+                        feature.placeFeature(world, chunk, random);
+                    }
+                }));
+
         // Water Features
         this.register("minecraft:decoration/nether/default/water_spring", (new ChunkDecorationBuilder(new WorldFeatureNetherSpring(Blocks.FLUID_WATER_FLOWING.id())))
                 .withBiomeMask(new Biome[]{Biomes.NETHER_CRYSTAL_FOREST, Biomes.NETHER_CRYSTAL_PLAINS})
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/SurfaceGeneratorNether.java b/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/SurfaceGeneratorNether.java
index 367b6cf12..b75207a26 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/SurfaceGeneratorNether.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/SurfaceGeneratorNether.java
@@ -32,6 +32,7 @@ public class SurfaceGeneratorNether implements SurfaceGenerator {
     private final short cobbleNetherrackId = (short) Blocks.COBBLE_NETHERRACK.id();
     private final short cobbleNetherrackCrystallineId = (short) Blocks.COBBLE_NETHERRACK_CRYSTALLINE.id();
     private final short cobbleGloomstoneId = (short) Blocks.COBBLE_GLOOMSTONE.id();
+    private final short gloomstoneId = (short) Blocks.GLOOMSTONE.id();
     private final short glowstoneId = (short) Blocks.GLOWSTONE.id();
     private final short ashId = (short) Blocks.BLOCK_ASH.id();
     private final short slateId = (short) Blocks.SLATE.id();
@@ -139,7 +140,7 @@ public class SurfaceGeneratorNether implements SurfaceGenerator {
                             } else if (biome == Biomes.NETHER_SULFUR_POOLS) {
                                 fillerBlock = sulfurId;
                             } else if (biome == Biomes.NETHER_OLD_WORLD || biome == Biomes.NETHER_OLD_WORLD_DESERT) {
-                                fillerBlock = cobbleGloomstoneId;
+                                fillerBlock = (rand.nextInt(2) == 0) ? gloomstoneId : cobbleGloomstoneId;
                             } else {
                                 fillerBlock = netherrackId;
                             }
@@ -164,16 +165,16 @@ public class SurfaceGeneratorNether implements SurfaceGenerator {
                         currentLayerDepth = soilThickness + 2;
 
                         if (y >= absoluteOceanY - 1) {
-                            result.setBlock(x, y, z, this.cragOrShelfFloorMixIfCobble(biome, topBlock, rand));
+                            result.setBlock(x, y, z, this.mixStoneVariant(biome, topBlock, rand));
                         } else {
                             // if the block is below ocean level, set it to the chosen filler block
-                            result.setBlock(x, y, z, this.cragOrShelfFloorMixIfCobble(biome, fillerBlock, rand));
+                            result.setBlock(x, y, z, this.mixStoneVariant(biome, fillerBlock, rand));
                         }
                         continue;
                     }
 
                     if (currentLayerDepth > 0) {
-                        result.setBlock(x, y, z, this.cragOrShelfFloorMixIfCobble(biome, fillerBlock, rand));
+                        result.setBlock(x, y, z, this.mixStoneVariant(biome, fillerBlock, rand));
                         currentLayerDepth--;
                     }
 
@@ -184,9 +185,10 @@ public class SurfaceGeneratorNether implements SurfaceGenerator {
 
                     if (this.generateStoneVariants && currentLayerDepth <= 0) {
                         int stoneBlockId = cachedBiomeStoneBlockId;
-                        // Crag/shelf is the only biome with a randomized variant per block.
                         if (biome == Biomes.NETHER_CRAG || biome == Biomes.NETHER_SHELF) {
                             stoneBlockId = (rand.nextInt(2) == 0) ? netherrackId : cobbleNetherrackId;
+                        } else if (biome == Biomes.NETHER_OLD_WORLD || biome == Biomes.NETHER_OLD_WORLD_DESERT) {
+                            stoneBlockId = (rand.nextInt(2) == 0) ? gloomstoneId : cobbleGloomstoneId;
                         }
                         result.setBlock(x, y, z, stoneBlockId);
                     }
@@ -195,11 +197,15 @@ public class SurfaceGeneratorNether implements SurfaceGenerator {
         }
     }
 
-    private short cragOrShelfFloorMixIfCobble(final @NotNull Biome biome, final short blockId, final @NotNull Random rand) {
-        if ((biome != Biomes.NETHER_CRAG && biome != Biomes.NETHER_SHELF) || blockId != this.cobbleNetherrackId) {
-            return blockId;
+    private short mixStoneVariant(final @NotNull Biome biome, final short blockId, final @NotNull Random rand) {
+        if ((biome == Biomes.NETHER_CRAG || biome == Biomes.NETHER_SHELF || biome == Biomes.NETHER_CRYSTAL_PLAINS || biome == Biomes.NETHER_CRYSTAL_FOREST) && blockId == this.cobbleNetherrackId) {
+            return rand.nextInt(2) == 0 ? this.netherrackId : this.cobbleNetherrackId;
         }
-        return (short) (rand.nextInt(2) == 0 ? this.netherrackId : this.cobbleNetherrackId);
+        if ((biome == Biomes.NETHER_OLD_WORLD || biome == Biomes.NETHER_OLD_WORLD_DESERT)
+            && (blockId == this.cobbleGloomstoneId || blockId == this.gloomstoneId)) {
+            return rand.nextInt(2) == 0 ? this.gloomstoneId : this.cobbleGloomstoneId;
+        }
+        return blockId;
     }
 
     private int computeBiomeStoneBlock(Biome biome, int worldFillBlock) {
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/TerrainGeneratorNether.java b/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/TerrainGeneratorNether.java
index 79450252d..0d0b35cb0 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/TerrainGeneratorNether.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/chunk/perlin/nether/TerrainGeneratorNether.java
@@ -24,6 +24,7 @@ public class TerrainGeneratorNether extends TerrainGeneratorLerp {
     private final int fillerBlockId;
     private final int lavaId;
     private final int cobbleNetherrackId;
+    private final int netherrackId;
     private final int obsidianId;
 
     private final ThreadLocal<int[]> oceanFluidByXZ = ThreadLocal.withInitial(() -> new int[256]);
@@ -45,6 +46,7 @@ public class TerrainGeneratorNether extends TerrainGeneratorLerp {
         this.waterId = Blocks.FLUID_WATER_STILL.id();
         this.lavaId = Blocks.FLUID_LAVA_STILL.id();
         this.cobbleNetherrackId = Blocks.COBBLE_NETHERRACK.id();
+        this.netherrackId = Blocks.NETHERRACK.id();
         this.obsidianId = Blocks.OBSIDIAN.id();
 
         this.oceanY = world.getWorldType().getOceanY();
@@ -98,6 +100,10 @@ public class TerrainGeneratorNether extends TerrainGeneratorLerp {
                 return this.rubyglassId;
             }
 
+            if (fluidId == this.cobbleNetherrackId) {
+                return this.rand.nextInt(2) == 0 ? this.netherrackId : this.cobbleNetherrackId;
+            }
+
             return fluidId;
         } else {
             return this.airId;
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeaturePillar.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeaturePillar.java
index df53c5ce8..f66d83ca7 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeaturePillar.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeaturePillar.java
@@ -151,7 +151,10 @@ public class WorldFeaturePillar
                         int xW = x0 + MathHelper.floor(t[0]);
                         int yW = y0 + MathHelper.floor(t[1]);
                         int zW = z0 + MathHelper.floor(t[2]);
-                        world.setBlockWithNotify(xW, yW, zW, blockId);
+                        int placed = (blockId == Blocks.COBBLE_NETHERRACK.id() && random.nextInt(2) == 0)
+                            ? Blocks.NETHERRACK.id()
+                            : blockId;
+                        world.setBlockWithNotify(xW, yW, zW, placed);
                     }
                 }
             }
diff --git a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureRoofSpire.java b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureRoofSpire.java
index d60ce0ab1..82229f5e4 100644
--- a/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureRoofSpire.java
+++ b/game/core/src/main/java/net/minecraft/core/world/generate/feature/WorldFeatureRoofSpire.java
@@ -23,6 +23,8 @@ public class WorldFeatureRoofSpire extends WorldFeature {
 			pos.up();
 		}
 
+		final boolean randomizeCobbleNetherrack = boulderBlock == Blocks.COBBLE_NETHERRACK;
+
 		if (this.hasSpace(world, pos.x, pos.y, pos.z)) {
 			Vector3i highestOffset = new Vector3i(0, 0, 0);
 			TilePos centerPos = new TilePos(pos.x, pos.y, pos.z);
@@ -41,7 +43,7 @@ public class WorldFeatureRoofSpire extends WorldFeature {
 						Block<?> currentBlock = world.getBlockType(queryPos);
 
 						if (currentBlock != Blocks.BEDROCK) {
-							world.setBlockType(queryPos, boulderBlock);
+							world.setBlockType(queryPos, pickSpireBlock(randomizeCobbleNetherrack, random));
 						}
 					}
 				}
@@ -49,13 +51,20 @@ public class WorldFeatureRoofSpire extends WorldFeature {
 
 			for (int y1 = 0; y1 < 2; ++y1) {
 				centerPos.add(highestOffset.x, -(highestOffset.y + y1), highestOffset.z, queryPos);
-				world.setBlockType(queryPos, boulderBlock);
+				world.setBlockType(queryPos, pickSpireBlock(randomizeCobbleNetherrack, random));
 			}
 		}
 
 		return true;
 	}
 
+	private Block<?> pickSpireBlock(boolean randomizeCobbleNetherrack, Random random) {
+		if (randomizeCobbleNetherrack && random.nextInt(2) == 0) {
+			return Blocks.NETHERRACK;
+		}
+		return boulderBlock;
+	}
+
 	private boolean hasSpace(World world, int xc, int y, int zc) {
 		TilePos queryPos = new TilePos();
 
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/command.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/command.lang
index 9c29fc03c..a22884b0d 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/command.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/command.lang
@@ -67,6 +67,11 @@ command.commands.achievement.grant.success_multiple_entities=Granted achievement
 command.commands.achievement.grant.success_single_entity=Granted achievement §5[%s]§r to %s
 command.commands.achievement.grant.all.success_multiple_entities=Granted all achievements to %s entities
 command.commands.achievement.grant.all.success_single_entity=Granted all achievements to %s
+command.commands.achievement.revoke.exception_does_not_have_achievement=Player does not have achievement
+command.commands.achievement.revoke.success_multiple_entities=Revoked achievement §5[%s]§r from %s entities
+command.commands.achievement.revoke.success_single_entity=Revoked achievement §5[%s]§r from %s
+command.commands.achievement.revoke.all.success_multiple_entities=Revoked all achievements from %s entities
+command.commands.achievement.revoke.all.success_single_entity=Revoked all achievements from %s
 command.commands.seed.success=Seed: §5[%s]§r
 command.commands.seed.copied_to_clipboard=Copied to clipboard!
 command.commands.clear.success_single_item_single_entity=%s item slot cleared
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 8d0e43643..de2a55a7a 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
@@ -7,6 +7,8 @@ gui.achievements.button.done=Done
 gui.achievements.label.title=Achievements
 gui.achievements.label.taken=Taken!
 gui.achievements.label.requires=Requires '%1$s'
+gui.achievements.label.secret=???
+gui.achievements.label.secret.desc=???
 gui.achievements.label.completion=%1$s Complete!
 
 gui.achievements.page.vanilla.name=Vanilla
@@ -273,6 +275,10 @@ gui.options.page.debug.category.test=Tests
 gui.options.page.debug.button.open.model_viewer=Open Model Viewer
 gui.options.page.debug.button.open.biome_finder=Open Seed Viewer
 gui.options.page.debug.button.open.datadumper=Run Data Dumper
+gui.options.page.debug.button.dump.wiki=Dump Formatted Wiki Data
+gui.options.page.debug.button.dump.invsprite=Dump InvSprites PNG
+gui.options.page.debug.button.dump.invsprite_apng=Dump InvSprites Animated PNG
+gui.options.page.debug.button.dump.invsprite.tooltip=Load a world first: \n Biome and season colors only render in-world.
 gui.options.page.debug.button.open.sound_test=Open Sound Test
 gui.options.page.debug.button.open.texture_viewer=Open Texture Viewer
 gui.options.page.debug.button.open.font_tester=Open Font Test
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/stats.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/stats.lang
index 0a3bad62e..ce960cede 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/stats.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/stats.lang
@@ -124,4 +124,36 @@ achievement.fastTravel.desc=Use the Nether to travel more than 8000 blocks in th
 achievement.blastFurnace=Blast Processing
 achievement.blastFurnace.desc=Construct a blast furnace out of eight iron bars and nether coal.
 achievement.obtainSteel=What a Steel!
-achievement.obtainSteel.desc=Combine coal and iron to acquire steel, a strong alloy.
\ No newline at end of file
+achievement.obtainSteel.desc=Combine coal and iron to acquire steel, a strong alloy.
+achievement.craftSteelBucket=Heavy Haul
+achievement.craftSteelBucket.desc=Craft a steel bucket.
+achievement.discoverSulfurPools=Something Smells...
+achievement.discoverSulfurPools.desc=Discover the Sulfur Pools.
+achievement.discoverVolcanicIslands=Having a Blast
+achievement.discoverVolcanicIslands.desc=Discover the Volcanic Islands.
+achievement.flingVent=Blasting off Again!
+achievement.flingVent.desc=Stand atop a thermal vent as it goes off.
+achievement.collectPumice=Hidden Pores
+achievement.collectPumice.desc=Collect a block of molten pumice.
+achievement.explodeStackedVent=What was that?
+achievement.explodeStackedVent.desc=Place a thermal vent on top of another in the overworld and wait for it to erupt.
+achievement.discoverCrystalForest=BzZzZzt!
+achievement.discoverCrystalForest.desc=Discover the Crystal Forest.
+achievement.collectRubyglass=Crystalline Harvest
+achievement.collectRubyglass.desc=Collect rubyglass from a crystal node.
+achievement.craftConduit=Go with the Flow
+achievement.craftConduit.desc=Craft a conduit.
+achievement.discoverOldWorld=Uncanny...
+achievement.discoverOldWorld.desc=Discover the Old World.
+achievement.triggerEmber=Suprise!
+achievement.triggerEmber.desc=Trigger an ember block to exlode by walking ontop of it.
+achievement.collectAcid=Acquire Acid
+achievement.collectAcid.desc=Collect acid in a bucket.
+achievement.acidSulfurConversion=Caustic Cooling
+achievement.acidSulfurConversion.desc=Convert acid to sulfur with water in the overworld.
+achievement.acidStoneGeneration=Smooth Operator
+achievement.acidStoneGeneration.desc=Use acid to harden fluids into smooth stone.
+achievement.acidCobbleConversion=Reconstitution
+achievement.acidCobbleConversion.desc=Mix acid with cobble or sand in the workbench.
+achievement.acidBath=Garra Acida
+achievement.acidBath.desc=Sit in acid for more than 10 seconds.
\ No newline at end of file
diff --git a/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang b/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang
index a92cd646b..b13a636c7 100644
--- a/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang
+++ b/game/core/src/main/resources/assets/minecraft/lang/en_US/tile.lang
@@ -1549,6 +1549,22 @@ tile.statue.pigman.lower.name=Pigman Statue (Lower)
 tile.statue.pigman.lower.desc=How?
 tile.statue.pigman.upper.name=Pigman Statue (Upper)
 tile.statue.pigman.upper.desc=How?
+tile.statue.slate.lower.name=Slate Statue (Lower)
+tile.statue.slate.lower.desc=How?
+tile.statue.slate.upper.name=Slate Statue (Upper)
+tile.statue.slate.upper.desc=How?
+tile.statue.permafrost.lower.name=Permafrost Statue (Lower)
+tile.statue.permafrost.lower.desc=How?
+tile.statue.permafrost.upper.name=Permafrost Statue (Upper)
+tile.statue.permafrost.upper.desc=How?
+tile.statue.netherrack.lower.name=Netherrack Statue (Lower)
+tile.statue.netherrack.lower.desc=How?
+tile.statue.netherrack.upper.name=Netherrack Statue (Upper)
+tile.statue.netherrack.upper.desc=How?
+tile.statue.gloomstone.lower.name=Gloomstone Statue (Lower)
+tile.statue.gloomstone.lower.desc=How?
+tile.statue.gloomstone.upper.name=Gloomstone Statue (Upper)
+tile.statue.gloomstone.upper.desc=How?
 
 tile.matcher.name=Matcher
 tile.matcher.desc=Matches the block in front to the block behind.
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 b9979de3a..a9dd6427b 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
@@ -81,7 +81,7 @@ public class PlayerServer extends Player implements ContainerListener {
 	public void moveEntityWithHeading(float moveStrafing, float moveForward)
     {
         super.moveEntityWithHeading(moveStrafing, moveForward);
-        if (noPhysics)
+        if (hasNoPhysics())
         {
             yd = 0.0D;
             onGround = true;
@@ -108,7 +108,7 @@ public class PlayerServer extends Player implements ContainerListener {
         inventoryMenu.addSlotListener(this);
         if (!gamemode.hasPlayerFlight())
         {
-            noPhysics = false;
+            setNoPhysics(false);
         }
 
         fireImmune = gamemode.hasFireImmunity();
@@ -117,7 +117,7 @@ public class PlayerServer extends Player implements ContainerListener {
     @Override
 	public @Nullable AABBdc getCollisionAABB()
     {
-        if (noPhysics)
+        if (hasNoPhysics())
         {
             return null;
         }
@@ -183,6 +183,17 @@ public class PlayerServer extends Player implements ContainerListener {
         playerController.tick();
         ticksOfInvuln--;
         containerMenu.broadcastChanges();
+
+        ItemStack heldItem = inventory.getHeldItemStack();
+        if (!ItemStack.areItemStacksEqual(heldItem, lastHeldItem))
+        {
+            if (playerNetServerHandler != null)
+            {
+                playerNetServerHandler.sendPacket(new PacketContainerSetSlot(-1, -1, heldItem));
+            }
+            lastHeldItem = heldItem == null ? null : heldItem.copy();
+        }
+
         for (int i = 0; i < 5; i++)
         {
             ItemStack itemstack = getEquipmentInSlot(i);
@@ -288,7 +299,7 @@ public class PlayerServer extends Player implements ContainerListener {
             }
         }
 
-        if (inPortal && !noPhysics)
+        if (inPortal && !hasNoPhysics())
         {
             Dimension targetDim = ((BlockLogicPortal) Blocks.blocksList[portalID].getLogic()).targetDimension;
             boolean netherAllowed = mcServer.propertyManager.getBooleanProperty("allow-nether", true);
@@ -677,6 +688,14 @@ public class PlayerServer extends Player implements ContainerListener {
         yRot = f3;
     }
 
+    @Override
+	public void revokeAchievement(Stat stat) {
+        if (stat == null) {
+            return;
+        }
+        playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, -1));
+    }
+
     @Override
 	public void addStat(@Nullable Stat stat, int i)
     {
@@ -686,12 +705,21 @@ public class PlayerServer extends Player implements ContainerListener {
         }
         if (!stat.clientside)
         {
-            for (; i > 100; i -= 100) { // send value change in groups of 100
-                playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, 100));
-            }
-
             if (i > 0) {
-                playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, i));
+                for (; i > 100; i -= 100) {
+                    playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, 100));
+                }
+                if (i > 0) {
+                    playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, i));
+                }
+            } else if (i < 0) {
+                int remaining = -i;
+                for (; remaining > 100; remaining -= 100) {
+                    playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, -100));
+                }
+                if (remaining > 0) {
+                    playerNetServerHandler.sendPacket(new PacketStatistic(stat.statId, -remaining));
+                }
             }
         }
     }
@@ -788,6 +816,7 @@ public class PlayerServer extends Player implements ContainerListener {
     private ItemStack[] playerInventory = {
             null, null, null, null, null
     };
+    private ItemStack lastHeldItem;
     private int currentWindowId;
     public boolean isChangingQuantityOnly;
 }
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 b4b3edf6c..2bb059989 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
@@ -545,16 +545,27 @@ public class PacketHandlerServer extends PacketHandler
     @Override
     public void handleUpdatePlayerState(@NotNull PacketUpdatePlayerState packetUpdatePlayerState) {
         if (!this.playerEntity.isAlive()) return;
-        if (packetUpdatePlayerState.state == PacketUpdatePlayerState.STATE_SNEAK) {
-            this.playerEntity.setSneaking(true);
-            if (this.playerEntity.vehicle != null) {
-                this.playerEntity.vehicle.ejectRider();
+        switch (packetUpdatePlayerState.state) {
+            case PacketUpdatePlayerState.STATE_SNEAK -> {
+                this.playerEntity.setSneaking(true);
+                if (this.playerEntity.vehicle != null) {
+                    this.playerEntity.vehicle.ejectRider();
+                }
+            }
+            case PacketUpdatePlayerState.STATE_UN_SNEAK -> this.playerEntity.setSneaking(false);
+            case PacketUpdatePlayerState.STATE_LEAVE_BED -> {
+                this.playerEntity.wakeUpPlayer(false, true);
+                this.hasMoved = false;
+            }
+            case PacketUpdatePlayerState.STATE_NO_CLIP -> {
+                this.playerEntity.setNoclip(true);
+                if (!this.playerEntity.gamemode.hasPlayerFlight()) {
+                    this.playerEntity.setNoclip(false);
+                }
+            }
+            case PacketUpdatePlayerState.STATE_DO_CLIP -> {
+                this.playerEntity.setNoclip(false);
             }
-        } else if (packetUpdatePlayerState.state == PacketUpdatePlayerState.STATE_UN_SNEAK) {
-            this.playerEntity.setSneaking(false);
-        } else if (packetUpdatePlayerState.state == PacketUpdatePlayerState.STATE_LEAVE_BED) {
-            this.playerEntity.wakeUpPlayer(false, true);
-            this.hasMoved = false;
         }
     }
 
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 ca95a3382..4d7899c9d 100644
--- a/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
+++ b/util/datagen/src/main/java/net/minecraft/datagen/WorkbenchGenerator.java
@@ -485,7 +485,8 @@ class WorkbenchGenerator {
             .addInput(Items.SULFUR)
             .addInput(Items.CLAY)
             .addInput(new ItemStack(Items.DYE, 1, DyeColor.WHITE.itemMeta))
-            .create("dirt", new ItemStack(Blocks.DIRT, 2));
+            .addInput(Items.AMMO_PEBBLE)
+            .create("dirt", new ItemStack(Blocks.DIRT, 4));
     }
 
 
