diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index 72f50ff97..797d68767 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -2,15 +2,6 @@ name: Build Pull Request
 
 on: 
   pull_request:
-    paths-ignore:
-      - '.github/ISSUE_TEMPLATE/*.yml'
-      - '.idea/copyright/*.xml' 
-      - '.gitignore'
-      - 'CONTRIBUTING.md'
-      - 'LICENSE'
-      - 'Jenkinsfile '
-      - 'README.md'
-      - 'licenseheader.txt'
 
 jobs:
   build:
diff --git a/README.md b/README.md
index 7f9f6fe9e..8a1f456c7 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
 
 Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
 
-### Currently supporting Minecraft Bedrock 1.20.0 - 1.20.30 and Minecraft Java 1.20/1.20.1.
+### Currently supporting Minecraft Bedrock 1.20.0 - 1.20.32 and Minecraft Java 1.20.2
 
 ## Setting Up
 Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java
index a9d0d7376..5226221df 100644
--- a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java
+++ b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java
@@ -36,13 +36,13 @@ import java.util.Collection;
 public abstract class ExtensionManager {
 
     /**
-     * Gets an extension with the given name.
+     * Gets an extension by the given ID.
      *
-     * @param name the name of the extension
-     * @return an extension with the given name
+     * @param id the ID of the extension
+     * @return an extension with the given ID
      */
     @Nullable
-    public abstract Extension extension(@NonNull String name);
+    public abstract Extension extension(@NonNull String id);
 
     /**
      * Enables the given {@link Extension}.
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java
index b521e04ac..391b5aafd 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java
@@ -79,7 +79,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
         // Copied from ViaVersion.
         // https://github.com/ViaVersion/ViaVersion/blob/b8072aad86695cc8ec6f5e4103e43baf3abf6cc5/bungee/src/main/java/us/myles/ViaVersion/BungeePlugin.java#L43
         try {
-            ProtocolConstants.class.getField("MINECRAFT_1_19_3");
+            ProtocolConstants.class.getField("MINECRAFT_1_20_2");
         } catch (NoSuchFieldException e) {
             getLogger().warning("      / \\");
             getLogger().warning("     /   \\");
diff --git a/bootstrap/fabric/build.gradle.kts b/bootstrap/fabric/build.gradle.kts
index c260703a5..e392a4ef9 100644
--- a/bootstrap/fabric/build.gradle.kts
+++ b/bootstrap/fabric/build.gradle.kts
@@ -118,7 +118,7 @@ modrinth {
     syncBodyFrom.set(rootProject.file("README.md").readText())
 
     uploadFile.set(tasks.getByPath("remapModrinthJar"))
-    gameVersions.addAll("1.20", "1.20.1")
+    gameVersions.addAll("1.20.2")
 
     loaders.add("fabric")
     failSilently.set(true)
diff --git a/bootstrap/fabric/src/main/resources/fabric.mod.json b/bootstrap/fabric/src/main/resources/fabric.mod.json
index 4c442017a..f401e8f23 100644
--- a/bootstrap/fabric/src/main/resources/fabric.mod.json
+++ b/bootstrap/fabric/src/main/resources/fabric.mod.json
@@ -25,7 +25,7 @@
   "depends": {
     "fabricloader": ">=0.14.21",
     "fabric": "*",
-    "minecraft": ">=1.20",
+    "minecraft": ">=1.20.2",
     "fabric-permissions-api-v0": "*"
   }
 }
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
index 7395ecd7d..dd892bcf5 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
@@ -199,7 +199,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
 
                     commandMap.register(extension.description().id(), "geyserext", pluginCommand);
                 } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
-                    this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.description().name(), ex);
+                    this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex);
                 }
             }
         }
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java
index 6cbb9090b..001bb2497 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java
@@ -128,16 +128,6 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
 
         this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this, platform);
 
-        // Remove this in like a year
-        try {
-            // Should only exist on 1.0
-            Class.forName("org.geysermc.floodgate.FloodgateAPI");
-            geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.outdated",
-                    "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/"));
-            return;
-        } catch (ClassNotFoundException ignored) {
-        }
-
 //        if (geyserConfig.getRemote().authType() == AuthType.FLOODGATE && proxyServer.getPluginManager().getPlugin("floodgate").isEmpty()) {
 //            geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " "
 //                    + GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
diff --git a/core/src/main/java/org/geysermc/connector/GeyserConnector.java b/core/src/main/java/org/geysermc/connector/GeyserConnector.java
deleted file mode 100644
index 381282a2a..000000000
--- a/core/src/main/java/org/geysermc/connector/GeyserConnector.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.connector;
-
-import org.geysermc.api.Geyser;
-import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.geyser.GeyserImpl;
-
-import java.util.UUID;
-
-/**
- * Deprecated, please use {@link Geyser} or {@link GeyserImpl}.
- *
- * @deprecated legacy code
- */
-@Deprecated
-public class GeyserConnector {
-    public static final String NAME = GeyserImpl.NAME;
-    public static final String GIT_VERSION = GeyserImpl.GIT_VERSION; // A fallback for running in IDEs
-    public static final String VERSION = GeyserImpl.VERSION; // A fallback for running in IDEs
-
-    public static final String OAUTH_CLIENT_ID = GeyserImpl.OAUTH_CLIENT_ID;
-
-    private static final GeyserConnector INSTANCE = new GeyserConnector();
-
-    public static GeyserConnector getInstance() {
-        return INSTANCE;
-    }
-
-    public  boolean isShuttingDown() {
-        return GeyserImpl.getInstance().isShuttingDown();
-    }
-
-    public PlatformType getPlatformType() {
-        return GeyserImpl.getInstance().getPlatformType();
-    }
-
-    public void shutdown() {
-        GeyserImpl.getInstance().shutdown();
-    }
-
-    public void reload() {
-        GeyserImpl.getInstance().reload();
-    }
-
-    public GeyserSession getPlayerByXuid(String xuid) {
-        org.geysermc.geyser.session.GeyserSession session = GeyserImpl.getInstance().connectionByXuid(xuid);
-        if (session != null) {
-            return new GeyserSession(session);
-        } else {
-            return null;
-        }
-    }
-
-    public GeyserSession getPlayerByUuid(UUID uuid) {
-        org.geysermc.geyser.session.GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
-        if (session != null) {
-            return new GeyserSession(session);
-        } else {
-            return null;
-        }
-    }
-
-    public boolean isProductionEnvironment() {
-        return GeyserImpl.getInstance().isProductionEnvironment();
-    }
-}
diff --git a/core/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/core/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
deleted file mode 100644
index 6298a41f6..000000000
--- a/core/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.connector.network.session;
-
-import com.github.steveice10.packetlib.packet.Packet;
-import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
-import org.geysermc.connector.network.session.auth.AuthData;
-
-/**
- * Deprecated, legacy code. Serves as a wrapper around
- * the class used now.
- *
- * @deprecated legacy code
- */
-@Deprecated
-public class GeyserSession {
-    private final org.geysermc.geyser.session.GeyserSession handle;
-
-    public GeyserSession(org.geysermc.geyser.session.GeyserSession handle) {
-        this.handle = handle;
-    }
-
-    public AuthData getAuthData() {
-        return new AuthData(this.handle.getAuthData());
-    }
-
-    public boolean isMicrosoftAccount() {
-        return this.handle.isMicrosoftAccount();
-    }
-
-    public boolean isClosed() {
-        return this.handle.isClosed();
-    }
-
-    public String getRemoteAddress() {
-        return this.handle.remoteServer().address();
-    }
-
-    public int getRemotePort() {
-        return this.handle.remoteServer().port();
-    }
-
-    public int getRenderDistance() {
-        return this.handle.getServerRenderDistance();
-    }
-
-    public boolean isSentSpawnPacket() {
-        return this.handle.isSentSpawnPacket();
-    }
-
-    public boolean isLoggedIn() {
-        return this.handle.isLoggedIn();
-    }
-
-    public boolean isLoggingIn() {
-        return this.handle.isLoggingIn();
-    }
-
-    public boolean isSpawned() {
-        return this.handle.isSpawned();
-    }
-
-    public boolean isInteracting() {
-        return this.handle.isInteracting();
-    }
-
-    public boolean isCanFly() {
-        return this.handle.isCanFly();
-    }
-
-    public boolean isFlying() {
-        return this.handle.isFlying();
-    }
-
-    public void connect() {
-        this.handle.connect();
-    }
-
-    public void login() {
-        throw new UnsupportedOperationException();
-    }
-
-    public void authenticate(String username) {
-        this.handle.authenticate(username);
-    }
-
-    public void authenticate(String username, String password) {
-        this.handle.authenticate(username, password);
-    }
-
-    public void authenticateWithMicrosoftCode() {
-        this.handle.authenticateWithMicrosoftCode();
-    }
-
-    public void disconnect(String reason) {
-        this.handle.disconnect(reason);
-    }
-
-    public void close() {
-        throw new UnsupportedOperationException();
-    }
-
-    public void executeInEventLoop(Runnable runnable) {
-        this.handle.executeInEventLoop(runnable);
-    }
-
-    public String getName() {
-        return this.handle.bedrockUsername();
-    }
-
-    public boolean isConsole() {
-        return this.handle.isConsole();
-    }
-
-    public String getLocale() {
-        return this.handle.locale();
-    }
-
-    public void sendUpstreamPacket(BedrockPacket packet) {
-        this.handle.sendUpstreamPacket(packet);
-    }
-
-    public void sendUpstreamPacketImmediately(BedrockPacket packet) {
-        this.handle.sendUpstreamPacketImmediately(packet);
-    }
-
-    public void sendDownstreamPacket(Packet packet) {
-        this.handle.sendDownstreamPacket(packet);
-    }
-
-    public boolean hasPermission(String permission) {
-        return this.handle.hasPermission(permission);
-    }
-
-    public void sendAdventureSettings() {
-        this.handle.sendAdventureSettings();
-    }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index df60e13f6..151cd9770 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -43,7 +43,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
 import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.geysermc.api.Geyser;
-import org.geysermc.geyser.api.util.PlatformType;
 import org.geysermc.cumulus.form.Form;
 import org.geysermc.cumulus.form.util.FormBuilder;
 import org.geysermc.erosion.packet.Packets;
@@ -58,6 +57,7 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent;
 import org.geysermc.geyser.api.network.AuthType;
 import org.geysermc.geyser.api.network.BedrockListener;
 import org.geysermc.geyser.api.network.RemoteServer;
+import org.geysermc.geyser.api.util.PlatformType;
 import org.geysermc.geyser.command.GeyserCommandManager;
 import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.entity.EntityDefinitions;
@@ -485,12 +485,6 @@ public class GeyserImpl implements GeyserApi {
         }
 
         if (config.getRemote().authType() == AuthType.ONLINE) {
-            if (config.getUserAuths() != null && !config.getUserAuths().isEmpty()) {
-                getLogger().warning("The 'userAuths' config section is now deprecated, and will be removed in the near future! " +
-                        "Please migrate to the new 'saved-user-logins' config option: " +
-                        "https://wiki.geysermc.org/geyser/understanding-the-config/");
-            }
-
             // May be written/read to on multiple threads from each GeyserSession as well as writing the config
             savedRefreshTokens = new ConcurrentHashMap<>();
 
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserMain.java b/core/src/main/java/org/geysermc/geyser/GeyserMain.java
index 8726c1b24..4e60a79b8 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserMain.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserMain.java
@@ -28,6 +28,7 @@ package org.geysermc.geyser;
 import javax.swing.*;
 import java.io.InputStream;
 import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
 import java.util.Locale;
 import java.util.Scanner;
 
@@ -60,7 +61,7 @@ public class GeyserMain {
             helpStream = GeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/en_US.txt");
         }
 
-        Scanner help = new Scanner(helpStream).useDelimiter("\\Z");
+        Scanner help = new Scanner(helpStream, StandardCharsets.UTF_8).useDelimiter("\\Z");
         String line = "";
         while (help.hasNext()) {
             line = help.next();
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java
index ea2da51df..1ff12dea3 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java
@@ -44,7 +44,7 @@ public class StatisticsCommand extends GeyserCommand {
 
         session.setWaitingForStatistics(true);
         ServerboundClientCommandPacket ServerboundClientCommandPacket = new ServerboundClientCommandPacket(ClientCommand.STATS);
-        session.sendDownstreamPacket(ServerboundClientCommandPacket);
+        session.sendDownstreamGamePacket(ServerboundClientCommandPacket);
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java
index 6ff7f5fb2..aa8460872 100644
--- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java
+++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java
@@ -36,7 +36,6 @@ import org.geysermc.geyser.text.GeyserLocale;
 
 import java.nio.file.Path;
 import java.util.List;
-import java.util.Map;
 
 public interface GeyserConfiguration {
     /**
@@ -55,9 +54,6 @@ public interface GeyserConfiguration {
 
     List<String> getSavedUserLogins();
 
-    @Deprecated
-    Map<String, ? extends IUserAuthenticationInfo> getUserAuths();
-
     boolean isCommandSuggestions();
 
     @JsonIgnore
@@ -149,8 +145,6 @@ public interface GeyserConfiguration {
 
         void setPort(int port);
 
-        boolean isPasswordAuthentication();
-
         boolean isUseProxyProtocol();
 
         boolean isForwardHost();
@@ -173,18 +167,6 @@ public interface GeyserConfiguration {
         boolean replaceSpaces();
     }
 
-    interface IUserAuthenticationInfo {
-        String getEmail();
-
-        String getPassword();
-
-        /**
-         * Will be removed after Microsoft accounts are fully migrated
-         */
-        @Deprecated
-        boolean isMicrosoftAccount();
-    }
-
     interface IMetricsInfo {
 
         boolean isEnabled();
diff --git a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java
index 00cb92fae..56ca605db 100644
--- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java
+++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java
@@ -44,7 +44,6 @@ import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -79,8 +78,6 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
 
     public abstract Path getFloodgateKeyPath();
 
-    private Map<String, UserAuthenticationInfo> userAuths;
-
     @JsonProperty("command-suggestions")
     private boolean commandSuggestions = true;
 
@@ -287,10 +284,6 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
             return false;
         }
 
-        @Getter
-        @JsonProperty("allow-password-authentication")
-        private boolean passwordAuthentication = true;
-
         @Getter
         @JsonProperty("use-proxy-protocol")
         private boolean useProxyProtocol = false;
@@ -300,19 +293,6 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
         private boolean forwardHost = false;
     }
 
-    @Getter
-    @JsonIgnoreProperties(ignoreUnknown = true) // DO NOT REMOVE THIS! Otherwise, after we remove microsoft-account configs will not load
-    public static class UserAuthenticationInfo implements IUserAuthenticationInfo {
-        @AsteriskSerializer.Asterisk()
-        private String email;
-
-        @AsteriskSerializer.Asterisk()
-        private String password;
-
-        @JsonProperty("microsoft-account")
-        private boolean microsoftAccount = false;
-    }
-
     @Getter
     @JsonIgnoreProperties(ignoreUnknown = true)
     public static class MetricsInfo implements IMetricsInfo {
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java
index c88f90f19..0917465d4 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/InteractionEntity.java
@@ -68,7 +68,7 @@ public class InteractionEntity extends Entity {
             animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
             session.sendUpstreamPacket(animatePacket);
 
-            session.sendDownstreamPacket(new ServerboundSwingPacket(hand));
+            session.sendDownstreamGamePacket(new ServerboundSwingPacket(hand));
             return InteractionResult.SUCCESS;
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
index 30d6ac856..dca11dfcd 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
@@ -27,6 +27,7 @@ package org.geysermc.geyser.extension;
 
 import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.api.extension.Extension;
 import org.geysermc.geyser.api.extension.ExtensionDescription;
 import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
@@ -39,14 +40,17 @@ import java.nio.file.Path;
 
 public class GeyserExtensionClassLoader extends URLClassLoader {
     private final GeyserExtensionLoader loader;
+    private final ExtensionDescription description;
     private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
+    private boolean warnedForExternalClassAccess;
 
-    public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path) throws MalformedURLException {
+    public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, ExtensionDescription description) throws MalformedURLException {
         super(new URL[] { path.toUri().toURL() }, parent);
         this.loader = loader;
+        this.description = description;
     }
 
-    public Extension load(ExtensionDescription description) throws InvalidExtensionException {
+    public Extension load() throws InvalidExtensionException {
         try {
             Class<?> jarClass;
             try {
@@ -76,22 +80,32 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
     }
 
     protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFoundException {
-        if (name.startsWith("org.geysermc.geyser.") || name.startsWith("net.minecraft.")) {
-            throw new ClassNotFoundException(name);
-        }
-
         Class<?> result = this.classes.get(name);
         if (result == null) {
-            result = super.findClass(name);
-            if (result == null && checkGlobal) {
-                result = this.loader.classByName(name);
+            // Try to find class in current extension
+            try {
+                result = super.findClass(name);
+            } catch (ClassNotFoundException ignored) {
+                // If class is not found in current extension, check in the global class loader
+                // This is used for classes that are not in the extension, but are in other extensions
+                if (checkGlobal) {
+                    if (!warnedForExternalClassAccess) {
+                        GeyserImpl.getInstance().getLogger().warning("Extension " + this.description.name() + " loads class " + name + " from an external source. " +
+                                "This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
+                        warnedForExternalClassAccess = true;
+                    }
+                    result = this.loader.classByName(name);
+                }
             }
 
             if (result != null) {
+                // If class is found, cache it
                 this.loader.setClass(name, result);
+                this.classes.put(name, result);
+            } else {
+                // If class is not found, throw exception
+                throw new ClassNotFoundException(name);
             }
-
-            this.classes.put(name, result);
         }
         return result;
     }
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
index 7e998e413..da0fbffda 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
@@ -43,9 +43,9 @@ import java.io.Reader;
 import java.nio.file.*;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.regex.Pattern;
-import java.util.stream.Stream;
 
 @RequiredArgsConstructor
 public class GeyserExtensionLoader extends ExtensionLoader {
@@ -66,26 +66,38 @@ public class GeyserExtensionLoader extends ExtensionLoader {
         }
 
         Path parentFile = path.getParent();
-        Path dataFolder = parentFile.resolve(description.name());
+
+        // Extension folders used to be created by name; this changes them to the ID
+        Path oldDataFolder = parentFile.resolve(description.name());
+        Path dataFolder = parentFile.resolve(description.id());
+
+        if (Files.exists(oldDataFolder) && Files.isDirectory(oldDataFolder) && !oldDataFolder.equals(dataFolder)) {
+            try {
+                Files.move(oldDataFolder, dataFolder, StandardCopyOption.REPLACE_EXISTING);
+            } catch (IOException e) {
+                throw new InvalidExtensionException("Failed to move data folder for extension " + description.name(), e);
+            }
+        }
+
         if (Files.exists(dataFolder) && !Files.isDirectory(dataFolder)) {
             throw new InvalidExtensionException("The folder " + dataFolder + " is not a directory and is the data folder for the extension " + description.name() + "!");
         }
 
         final GeyserExtensionClassLoader loader;
         try {
-            loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), path);
+            loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), path, description);
         } catch (Throwable e) {
             throw new InvalidExtensionException(e);
         }
 
-        this.classLoaders.put(description.name(), loader);
+        this.classLoaders.put(description.id(), loader);
 
-        final Extension extension = loader.load(description);
+        final Extension extension = loader.load();
         return this.setup(extension, description, dataFolder, new GeyserExtensionEventBus(GeyserImpl.getInstance().eventBus(), extension));
     }
 
     private GeyserExtensionContainer setup(Extension extension, GeyserExtensionDescription description, Path dataFolder, ExtensionEventBus eventBus) {
-        GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name());
+        GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.id());
         return new GeyserExtensionContainer(extension, dataFolder, description, this, logger, eventBus);
     }
 
@@ -136,46 +148,46 @@ public class GeyserExtensionLoader extends ExtensionLoader {
             Map<String, GeyserExtensionContainer> loadedExtensions = new LinkedHashMap<>();
 
             Pattern[] extensionFilters = this.extensionFilters();
-            try (Stream<Path> entries = Files.walk(extensionsDirectory)) {
-                entries.forEach(path -> {
-                    if (Files.isDirectory(path)) {
+            List<Path> extensionPaths = Files.walk(extensionsDirectory).toList();
+            extensionPaths.forEach(path -> {
+                if (Files.isDirectory(path)) {
+                    return;
+                }
+
+                for (Pattern filter : extensionFilters) {
+                    if (!filter.matcher(path.getFileName().toString()).matches()) {
+                        return;
+                    }
+                }
+
+                try {
+                    GeyserExtensionDescription description = this.extensionDescription(path);
+
+                    String name = description.name();
+                    String id = description.id();
+                    if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
+                        GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
                         return;
                     }
 
-                    for (Pattern filter : extensionFilters) {
-                        if (!filter.matcher(path.getFileName().toString()).matches()) {
-                            return;
-                        }
+                    // Completely different API version
+                    if (description.majorApiVersion() != Geyser.api().majorApiVersion()) {
+                        GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
+                        return;
                     }
 
-                    try {
-                        GeyserExtensionDescription description = this.extensionDescription(path);
-
-                        String name = description.name();
-                        if (extensions.containsKey(name) || extensionManager.extension(name) != null) {
-                            GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
-                            return;
-                        }
-
-                        // Completely different API version
-                        if (description.majorApiVersion() != Geyser.api().majorApiVersion()) {
-                            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
-                            return;
-                        }
-
-                        // If the extension requires new API features, being backwards compatible
-                        if (description.minorApiVersion() > Geyser.api().minorApiVersion()) {
-                            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
-                            return;
-                        }
-
-                        extensions.put(name, path);
-                        loadedExtensions.put(name, this.loadExtension(path, description));
-                    } catch (Exception e) {
-                        GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
+                    // If the extension requires new API features, being backwards compatible
+                    if (description.minorApiVersion() > Geyser.api().minorApiVersion()) {
+                        GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
+                        return;
                     }
-                });
-            }
+
+                    extensions.put(id, path);
+                    loadedExtensions.put(id, this.loadExtension(path, description));
+                } catch (Exception e) {
+                    GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
+                }
+            });
 
             for (GeyserExtensionContainer container : loadedExtensions.values()) {
                 this.extensionContainers.put(container.extension(), container);
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java
index 5dd924301..3c41c4329 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java
@@ -52,8 +52,8 @@ public class GeyserExtensionManager extends ExtensionManager {
     }
 
     @Override
-    public Extension extension(@NonNull String name) {
-        return this.extensions.get(name);
+    public Extension extension(@NonNull String id) {
+        return this.extensions.get(id);
     }
 
     @Override
@@ -83,7 +83,7 @@ public class GeyserExtensionManager extends ExtensionManager {
         if (!extension.isEnabled()) {
             extension.setEnabled(true);
             GeyserImpl.getInstance().eventBus().register(extension, extension);
-            GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.description().name()));
+            GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.name()));
         }
     }
 
@@ -98,7 +98,7 @@ public class GeyserExtensionManager extends ExtensionManager {
             GeyserImpl.getInstance().eventBus().unregisterAll(extension);
 
             extension.setEnabled(false);
-            GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.description().name()));
+            GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.name()));
         }
     }
 
@@ -121,6 +121,6 @@ public class GeyserExtensionManager extends ExtensionManager {
 
     @Override
     public void register(@NonNull Extension extension) {
-        this.extensions.put(extension.name(), extension);
+        this.extensions.put(extension.description().id(), extension);
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java
index 5b0800e44..e99b901a4 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/AnvilContainer.java
@@ -82,14 +82,14 @@ public class AnvilContainer extends Container {
             correctRename = plainNewName;
             // Java Edition sends a packet every time an item is renamed even slightly in GUI. Fortunately, this works out for us now
             ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(plainNewName);
-            session.sendDownstreamPacket(renameItemPacket);
+            session.sendDownstreamGamePacket(renameItemPacket);
         } else {
             // Restore formatting for item since we're not renaming
             correctRename = MessageTranslator.convertMessageLenient(originalName);
             // Java Edition sends the original custom name when not renaming,
             // if there isn't a custom name an empty string is sent
             ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(plainOriginalName);
-            session.sendDownstreamPacket(renameItemPacket);
+            session.sendDownstreamGamePacket(renameItemPacket);
         }
 
         useJavaLevelCost = false;
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
index bfe5a7d9d..f31f6d82f 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
@@ -152,7 +152,7 @@ public final class ClickPlan {
                     changedItems
             );
 
-            session.sendDownstreamPacket(clickPacket);
+            session.sendDownstreamGamePacket(clickPacket);
         }
 
         session.getPlayerInventory().setCursor(simulatedCursor, session);
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java b/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java
index ddc44fbc4..e45667fe2 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/item/TippedArrowPotion.java
@@ -31,7 +31,7 @@ import java.util.Locale;
 
 /**
  * Potion identifiers and their respective Bedrock IDs used with arrows.
- * https://minecraft.gamepedia.com/Arrow#Item_Data
+ * https://minecraft.wiki/w/Arrow#Data_values
  */
 @Getter
 public enum TippedArrowPotion {
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
index ea4ef674b..aa7897038 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
@@ -120,7 +120,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
             // does not result in a FilterTextPacket
             String originalName = MessageTranslator.convertToPlainTextLenient(ItemUtils.getCustomName(input.getNbt()), session.locale());
             ServerboundRenameItemPacket renameItemPacket = new ServerboundRenameItemPacket(originalName);
-            session.sendDownstreamPacket(renameItemPacket);
+            session.sendDownstreamGamePacket(renameItemPacket);
 
             anvilContainer.setNewName(null);
         }
diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java b/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java
index fc3c86dd4..936248afe 100644
--- a/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java
+++ b/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java
@@ -52,11 +52,6 @@ public class GeyserAdvancement {
         return this.advancement.getId();
     }
 
-    @NonNull
-    public List<String> getCriteria() {
-        return this.advancement.getCriteria();
-    }
-
     @NonNull
     public List<List<String>> getRequirements() {
         return this.advancement.getRequirements();
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
index a686ba552..97ab18c6b 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/BlockStateValues.java
@@ -173,8 +173,8 @@ public final class BlockStateValues {
         }
 
         if (javaId.contains("wall_skull") || javaId.contains("wall_head")) {
-            String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7);
-            int rotation = switch (direction.substring(0, direction.length() - 1)) {
+            String direction = javaId.substring(javaId.lastIndexOf("facing=") + 7, javaId.lastIndexOf("powered=") - 1);
+            int rotation = switch (direction) {
                 case "north" -> 180;
                 case "west" -> 90;
                 case "east" -> 270;
diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
index 63cce329c..fb9684f77 100644
--- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
+++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
@@ -46,7 +46,9 @@ public final class GameProtocol {
      * Default Bedrock codec that should act as a fallback. Should represent the latest available
      * release of the game that Geyser supports.
      */
-    public static final BedrockCodec DEFAULT_BEDROCK_CODEC = Bedrock_v618.CODEC;
+    public static final BedrockCodec DEFAULT_BEDROCK_CODEC = Bedrock_v618.CODEC.toBuilder()
+        .minecraftVersion("1.20.31")
+        .build();
 
     /**
      * A list of all supported Bedrock versions that can join Geyser
@@ -66,7 +68,9 @@ public final class GameProtocol {
         SUPPORTED_BEDROCK_CODECS.add(Bedrock_v594.CODEC.toBuilder()
             .minecraftVersion("1.20.10/1.20.15")
             .build());
-        SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
+        SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
+            .minecraftVersion("1.20.30/1.20.31")
+            .build());
     }
 
     /**
diff --git a/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java b/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java
index 35d2d7f33..f19a46e6a 100644
--- a/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java
+++ b/core/src/main/java/org/geysermc/geyser/network/GeyserServerInitializer.java
@@ -47,6 +47,10 @@ public class GeyserServerInitializer extends BedrockServerInitializer {
         this.geyser = geyser;
     }
 
+    public DefaultEventLoopGroup getEventLoopGroup() {
+        return eventLoopGroup;
+    }
+
     @Override
     public void initSession(@Nonnull BedrockServerSession bedrockServerSession) {
         try {
@@ -72,4 +76,4 @@ public class GeyserServerInitializer extends BedrockServerInitializer {
     protected BedrockPeer createPeer(Channel channel) {
         return new GeyserBedrockPeer(channel, this::createSession);
     }
-}
\ No newline at end of file
+}
diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
index 6b43422e5..361aaffb9 100644
--- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
@@ -52,7 +52,6 @@ import org.geysermc.geyser.api.network.AuthType;
 import org.geysermc.geyser.api.pack.PackCodec;
 import org.geysermc.geyser.api.pack.ResourcePack;
 import org.geysermc.geyser.api.pack.ResourcePackManifest;
-import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.event.type.SessionLoadResourcePacksEventImpl;
 import org.geysermc.geyser.pack.GeyserResourcePack;
 import org.geysermc.geyser.registry.BlockRegistries;
@@ -257,21 +256,9 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
                 return true;
             }
         }
-        if (geyser.getConfig().getUserAuths() != null) {
-            GeyserConfiguration.IUserAuthenticationInfo info = geyser.getConfig().getUserAuths().get(bedrockUsername);
-
-            if (info != null) {
-                geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.stored_credentials", session.getAuthData().name()));
-                session.setMicrosoftAccount(info.isMicrosoftAccount());
-                session.authenticate(info.getEmail(), info.getPassword());
-                return true;
-            }
-        }
         PendingMicrosoftAuthentication.AuthenticationTask task = geyser.getPendingMicrosoftAuthentication().getTask(session.getAuthData().xuid());
         if (task != null) {
-            if (task.getAuthentication().isDone() && session.onMicrosoftLoginComplete(task)) {
-                return true;
-            }
+            return task.getAuthentication().isDone() && session.onMicrosoftLoginComplete(task);
         }
 
         return false;
diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java
index df9e1e9d9..6fbb29157 100644
--- a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java
+++ b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java
@@ -39,6 +39,7 @@ import io.netty.channel.kqueue.KQueueEventLoopGroup;
 import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.nio.NioDatagramChannel;
+import io.netty.util.concurrent.Future;
 import lombok.Getter;
 import net.jodah.expiringmap.ExpirationPolicy;
 import net.jodah.expiringmap.ExpiringMap;
@@ -58,6 +59,7 @@ import org.geysermc.geyser.network.netty.handler.RakPingHandler;
 import org.geysermc.geyser.network.netty.proxy.ProxyServerHandler;
 import org.geysermc.geyser.ping.GeyserPingInfo;
 import org.geysermc.geyser.ping.IGeyserPingPassthrough;
+import org.geysermc.geyser.skin.SkinProvider;
 import org.geysermc.geyser.text.GeyserLocale;
 import org.geysermc.geyser.translator.text.MessageTranslator;
 
@@ -83,14 +85,21 @@ public final class GeyserServer {
 
     private static final Transport TRANSPORT = compatibleTransport();
 
+    /**
+     * See {@link EventLoopGroup#shutdownGracefully(long, long, TimeUnit)}
+     */
+    private static final int SHUTDOWN_QUIET_PERIOD_MS = 100;
+    private static final int SHUTDOWN_TIMEOUT_MS = 500;
+
     private final GeyserImpl geyser;
-    private final EventLoopGroup group;
+    private EventLoopGroup group;
     private final ServerBootstrap bootstrap;
+    private EventLoopGroup playerGroup;
 
     @Getter
     private final ExpiringMap<InetSocketAddress, InetSocketAddress> proxiedAddresses;
 
-    private ChannelFuture future;
+    private ChannelFuture bootstrapFuture;
 
     public GeyserServer(GeyserImpl geyser, int threadCount) {
         this.geyser = geyser;
@@ -109,7 +118,7 @@ public final class GeyserServer {
 
     public CompletableFuture<Void> bind(InetSocketAddress address) {
         CompletableFuture<Void> future = new CompletableFuture<>();
-        this.future = this.bootstrap.bind(address).addListener(bindResult -> {
+        this.bootstrapFuture = this.bootstrap.bind(address).addListener(bindResult -> {
             if (bindResult.cause() != null) {
                 future.completeExceptionally(bindResult.cause());
                 return;
@@ -117,7 +126,7 @@ public final class GeyserServer {
             future.complete(null);
         });
 
-        Channel channel = this.future.channel();
+        Channel channel = this.bootstrapFuture.channel();
 
         // Add our ping handler
         channel.pipeline()
@@ -132,8 +141,19 @@ public final class GeyserServer {
     }
 
     public void shutdown() {
-        this.group.shutdownGracefully();
-        this.future.channel().closeFuture().syncUninterruptibly();
+        try {
+            Future<?> future1 = this.group.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            this.group = null;
+            Future<?> future2 = this.playerGroup.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            this.playerGroup = null;
+            future1.sync();
+            future2.sync();
+
+            SkinProvider.shutdown();
+        } catch (InterruptedException e) {
+            GeyserImpl.getInstance().getLogger().severe("Exception in shutdown process", e);
+        }
+        this.bootstrapFuture.channel().closeFuture().syncUninterruptibly();
     }
 
     private ServerBootstrap createBootstrap(EventLoopGroup group) {
@@ -149,11 +169,13 @@ public final class GeyserServer {
             }
         }
 
+        GeyserServerInitializer serverInitializer = new GeyserServerInitializer(this.geyser);
+        playerGroup = serverInitializer.getEventLoopGroup();
         return new ServerBootstrap()
                 .channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannel()))
                 .group(group)
                 .option(RakChannelOption.RAK_HANDLE_PING, true)
-                .childHandler(new GeyserServerInitializer(this.geyser));
+                .childHandler(serverInitializer);
     }
 
     public boolean onConnectionRequest(InetSocketAddress inetSocketAddress) {
@@ -217,7 +239,7 @@ public final class GeyserServer {
                 .version(GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()) // Required to not be empty as of 1.16.210.59. Can only contain . and numbers.
                 .ipv4Port(this.geyser.getConfig().getBedrock().port())
                 .ipv6Port(this.geyser.getConfig().getBedrock().port())
-                .serverId(future.channel().config().getOption(RakChannelOption.RAK_GUID));
+                .serverId(bootstrapFuture.channel().config().getOption(RakChannelOption.RAK_GUID));
 
         if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
             String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
diff --git a/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java b/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java
index dbc8e2e26..e37f8aa64 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/PacketTranslatorRegistry.java
@@ -27,9 +27,9 @@ package org.geysermc.geyser.registry;
 
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundDelimiterPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundTabListPacket;
+import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundChunkBatchStartPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundLightUpdatePacket;
 import io.netty.channel.EventLoop;
-import org.cloudburstmc.protocol.bedrock.packet.RequestPermissionsPacket;
 import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.registry.loader.RegistryLoaders;
 import org.geysermc.geyser.session.GeyserSession;
@@ -44,10 +44,10 @@ public class PacketTranslatorRegistry<T> extends AbstractMappedRegistry<Class<?
     private static final Set<Class<?>> IGNORED_PACKETS = Collections.newSetFromMap(new IdentityHashMap<>());
 
     static {
+        IGNORED_PACKETS.add(ClientboundChunkBatchStartPacket.class); // we don't track chunk batch sizes/periods
+        IGNORED_PACKETS.add(ClientboundDelimiterPacket.class); // Not implemented, spams logs
         IGNORED_PACKETS.add(ClientboundLightUpdatePacket.class); // Light is handled on Bedrock for us
         IGNORED_PACKETS.add(ClientboundTabListPacket.class); // Cant be implemented in Bedrock
-        IGNORED_PACKETS.add(ClientboundDelimiterPacket.class); // Not implemented, spams logs
-        IGNORED_PACKETS.add(RequestPermissionsPacket.class); // Bedrock client asks permission to switch default game mode, but we handle this ourselves
     }
 
     protected PacketTranslatorRegistry() {
diff --git a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java
index c9ed51166..e4bf43b93 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/populator/CustomBlockRegistryPopulator.java
@@ -72,9 +72,9 @@ public class CustomBlockRegistryPopulator {
 
     private static Set<CustomBlockData> CUSTOM_BLOCKS;
     private static Set<String> CUSTOM_BLOCK_NAMES;
-    private static Int2ObjectMap<CustomBlockState> BLOCK_STATE_OVERRIDES;
     private static Map<String, CustomBlockData> CUSTOM_BLOCK_ITEM_OVERRIDES;
     private static Map<JavaBlockState, CustomBlockState> NON_VANILLA_BLOCK_STATE_OVERRIDES;
+    private static Map<String, CustomBlockState> BLOCK_STATE_OVERRIDES_QUEUE;
 
     /**
      * Initializes custom blocks defined by API
@@ -82,9 +82,9 @@ public class CustomBlockRegistryPopulator {
     private static void populateBedrock() {
         CUSTOM_BLOCKS = new ObjectOpenHashSet<>();
         CUSTOM_BLOCK_NAMES = new ObjectOpenHashSet<>();
-        BLOCK_STATE_OVERRIDES = new Int2ObjectOpenHashMap<>();
         CUSTOM_BLOCK_ITEM_OVERRIDES = new HashMap<>();
         NON_VANILLA_BLOCK_STATE_OVERRIDES = new HashMap<>();
+        BLOCK_STATE_OVERRIDES_QUEUE = new HashMap<>();
 
         GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomBlocksEvent() {
             @Override
@@ -103,18 +103,11 @@ public class CustomBlockRegistryPopulator {
 
             @Override
             public void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState) {
-                int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1);
-                if (id == -1) {
-                    throw new IllegalArgumentException("Unknown Java block state. Identifier: " + javaIdentifier);
-                }
                 if (!CUSTOM_BLOCKS.contains(customBlockState.block())) {
                     throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name());
                 }
-                CustomBlockState oldBlockState = BLOCK_STATE_OVERRIDES.put(id, customBlockState);
-                if (oldBlockState != null) {
-                    GeyserImpl.getInstance().getLogger().debug("Duplicate block state override for Java Identifier: " +
-                            javaIdentifier + " Old override: " + oldBlockState.name() + " New override: " + customBlockState.name());
-                }
+                // We can't register these yet as we don't have the java block id registry populated
+                BLOCK_STATE_OVERRIDES_QUEUE.put(javaIdentifier, customBlockState);
             }
 
             @Override
@@ -139,10 +132,28 @@ public class CustomBlockRegistryPopulator {
      * Registers all vanilla custom blocks and skulls defined by API and mappings
      */
     private static void populateVanilla() {
+        Int2ObjectMap<CustomBlockState> blockStateOverrides = new Int2ObjectOpenHashMap<>();
+
         for (CustomSkull customSkull : BlockRegistries.CUSTOM_SKULLS.get().values()) {
             CUSTOM_BLOCKS.add(customSkull.getCustomBlockData());
         }
 
+        for(Map.Entry<String, CustomBlockState> entry : BLOCK_STATE_OVERRIDES_QUEUE.entrySet()) {
+            int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(entry.getKey(), -1);
+            if (id == -1) {
+                GeyserImpl.getInstance().getLogger().warning("Custom block state override for Java Identifier: " +
+                        entry.getKey() + " could not be registered as it is not a valid block state.");
+                continue;
+            }
+
+            CustomBlockState oldBlockState = blockStateOverrides.put(id, entry.getValue());
+            if (oldBlockState != null) {
+                GeyserImpl.getInstance().getLogger().warning("Duplicate block state override for Java Identifier: " +
+                        entry.getKey() + " Old override: " + oldBlockState.name() + " New override: " + entry.getValue().name());
+            }
+        }
+        BLOCK_STATE_OVERRIDES_QUEUE = null;
+
         Map<CustomBlockData, Set<Integer>> extendedCollisionBoxes = new HashMap<>();
         Map<BoxComponent, CustomBlockData> extendedCollisionBoxSet = new HashMap<>();
         MappingsConfigReader mappingsConfigReader = new MappingsConfigReader();
@@ -153,7 +164,7 @@ public class CustomBlockRegistryPopulator {
             }
             block.states().forEach((javaIdentifier, customBlockState) -> {
                 int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1);
-                BLOCK_STATE_OVERRIDES.put(id, customBlockState.state());
+                blockStateOverrides.put(id, customBlockState.state());
                 BoxComponent extendedCollisionBox = customBlockState.extendedCollisionBox();
                 if (extendedCollisionBox != null) {
                     CustomBlockData extendedCollisionBlock = extendedCollisionBoxSet.computeIfAbsent(extendedCollisionBox, box -> {
@@ -167,9 +178,9 @@ public class CustomBlockRegistryPopulator {
             });
         });
     
-        BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(BLOCK_STATE_OVERRIDES);
-        if (BLOCK_STATE_OVERRIDES.size() != 0) {
-            GeyserImpl.getInstance().getLogger().info("Registered " + BLOCK_STATE_OVERRIDES.size() + " custom block overrides.");
+        BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(blockStateOverrides);
+        if (blockStateOverrides.size() != 0) {
+            GeyserImpl.getInstance().getLogger().info("Registered " + blockStateOverrides.size() + " custom block overrides.");
         }
 
         BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.set(CUSTOM_BLOCK_ITEM_OVERRIDES);
diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
index f0d08f178..5a8512c2b 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -26,10 +26,7 @@
 package org.geysermc.geyser.session;
 
 import com.github.steveice10.mc.auth.data.GameProfile;
-import com.github.steveice10.mc.auth.exception.request.InvalidCredentialsException;
 import com.github.steveice10.mc.auth.exception.request.RequestException;
-import com.github.steveice10.mc.auth.service.AuthenticationService;
-import com.github.steveice10.mc.auth.service.MojangAuthenticationService;
 import com.github.steveice10.mc.auth.service.MsaAuthenticationService;
 import com.github.steveice10.mc.protocol.MinecraftConstants;
 import com.github.steveice10.mc.protocol.MinecraftProtocol;
@@ -45,15 +42,15 @@ import com.github.steveice10.mc.protocol.data.game.setting.ChatVisibility;
 import com.github.steveice10.mc.protocol.data.game.setting.SkinPart;
 import com.github.steveice10.mc.protocol.data.game.statistic.CustomStatistic;
 import com.github.steveice10.mc.protocol.data.game.statistic.Statistic;
+import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundClientInformationPacket;
 import com.github.steveice10.mc.protocol.packet.handshake.serverbound.ClientIntentionPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatCommandPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundChatPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundClientInformationPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
 import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundUseItemPacket;
-import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryPacket;
+import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryAnswerPacket;
 import com.github.steveice10.packetlib.BuiltinFlags;
 import com.github.steveice10.packetlib.Session;
 import com.github.steveice10.packetlib.event.session.ConnectedEvent;
@@ -253,10 +250,6 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
     @Setter
     private RemoteServer remoteServer;
 
-    @Deprecated
-    @Setter
-    private boolean microsoftAccount;
-
     private final SessionPlayerEntity playerEntity;
 
     private final AdvancementsCache advancementsCache;
@@ -355,7 +348,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
     private Vector2i lastChunkPosition = null;
     @Setter
     private int clientRenderDistance = -1;
-    private int serverRenderDistance;
+    private int serverRenderDistance = -1;
 
     // Exposed for GeyserConnect usage
     protected boolean sentSpawnPacket;
@@ -764,76 +757,20 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
     }
 
     public void authenticate(String username) {
-        authenticate(username, "");
-    }
-
-    public void authenticate(String username, String password) {
         if (loggedIn) {
             geyser.getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.auth.already_loggedin", username));
             return;
         }
 
         loggingIn = true;
+        // Always replace spaces with underscores to avoid illegal nicknames, e.g. with GeyserConnect
+        protocol = new MinecraftProtocol(username.replace(' ', '_'));
 
-        // Use a future to prevent timeouts as all the authentication is handled sync
-        CompletableFuture.supplyAsync(() -> {
-            try {
-                if (password != null && !password.isEmpty()) {
-                    AuthenticationService authenticationService;
-                    if (microsoftAccount) {
-                        authenticationService = new MsaAuthenticationService(GeyserImpl.OAUTH_CLIENT_ID);
-                    } else {
-                        authenticationService = new MojangAuthenticationService();
-                    }
-                    authenticationService.setUsername(username);
-                    authenticationService.setPassword(password);
-                    authenticationService.login();
-
-                    GameProfile profile = authenticationService.getSelectedProfile();
-                    if (profile == null) {
-                        // Java account is offline
-                        disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", clientData.getLanguageCode()));
-                        return null;
-                    }
-
-                    protocol = new MinecraftProtocol(profile, authenticationService.getAccessToken());
-                } else {
-                    // always replace spaces when using Floodgate,
-                    // as usernames with spaces cause issues with Bungeecord's login cycle.
-                    // However, this doesn't affect the final username as Floodgate is still in charge of that.
-                    // So if you have (for example) replace spaces enabled on Floodgate the spaces will re-appear.
-                    String validUsername = username;
-                    if (this.remoteServer.authType() == AuthType.FLOODGATE) {
-                        validUsername = username.replace(' ', '_');
-                    }
-
-                    protocol = new MinecraftProtocol(validUsername);
-                }
-            } catch (InvalidCredentialsException | IllegalArgumentException e) {
-                geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.auth.login.invalid", username));
-                disconnect(GeyserLocale.getPlayerLocaleString("geyser.auth.login.invalid.kick", getClientData().getLanguageCode()));
-            } catch (RequestException ex) {
-                disconnect(ex.getMessage());
-            }
-            return null;
-        }).whenComplete((aVoid, ex) -> {
-            if (ex != null) {
-                disconnect(ex.toString());
-            }
-            if (this.closed) {
-                if (ex != null) {
-                    geyser.getLogger().error("", ex);
-                }
-                // Client disconnected during the authentication attempt
-                return;
-            }
-
-            try {
-                connectDownstream();
-            } catch (Throwable t) {
-                t.printStackTrace();
-            }
-        });
+        try {
+            connectDownstream();
+        } catch (Throwable t) {
+            t.printStackTrace();
+        }
     }
 
     public void authenticateWithRefreshToken(String refreshToken) {
@@ -1240,7 +1177,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
                 if (position != null) {
                     ServerboundMovePlayerPosPacket packet = new ServerboundMovePlayerPosPacket(playerEntity.isOnGround(),
                             position.getX(), position.getY(), position.getZ());
-                    sendDownstreamPacket(packet);
+                    sendDownstreamGamePacket(packet);
                 }
                 lastMovementTimestamp = System.currentTimeMillis();
             }
@@ -1422,7 +1359,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
             return false;
         }
 
-        sendDownstreamPacket(useItemPacket);
+        sendDownstreamGamePacket(useItemPacket);
         playerEntity.setFlag(EntityFlag.BLOCKING, true);
         // Metadata should be updated later
         return true;
@@ -1456,7 +1393,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
         if (playerEntity.getFlag(EntityFlag.BLOCKING)) {
             ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM,
                     Vector3i.ZERO, Direction.DOWN, 0);
-            sendDownstreamPacket(releaseItemPacket);
+            sendDownstreamGamePacket(releaseItemPacket);
             playerEntity.setFlag(EntityFlag.BLOCKING, false);
             return true;
         }
@@ -1466,7 +1403,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
     public void requestOffhandSwap() {
         ServerboundPlayerActionPacket swapHandsPacket = new ServerboundPlayerActionPacket(PlayerAction.SWAP_HANDS, Vector3i.ZERO,
                 Direction.DOWN, 0);
-        sendDownstreamPacket(swapHandsPacket);
+        sendDownstreamGamePacket(swapHandsPacket);
     }
 
     @Override
@@ -1501,14 +1438,14 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
      * Sends a chat message to the Java server.
      */
     public void sendChat(String message) {
-        sendDownstreamPacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, null, 0, new BitSet()));
+        sendDownstreamGamePacket(new ServerboundChatPacket(message, Instant.now().toEpochMilli(), 0L, null, 0, new BitSet()));
     }
 
     /**
      * Sends a command to the Java server.
      */
     public void sendCommand(String command) {
-        sendDownstreamPacket(new ServerboundChatCommandPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet()));
+        sendDownstreamGamePacket(new ServerboundChatCommandPacket(command, Instant.now().toEpochMilli(), 0L, Collections.emptyList(), 0, new BitSet()));
     }
 
     public void setServerRenderDistance(int renderDistance) {
@@ -1660,6 +1597,39 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
         upstream.sendPacketImmediately(packet);
     }
 
+    /**
+     * Send a packet to the remote server if in the game state.
+     *
+     * @param packet the java edition packet from MCProtocolLib
+     */
+    public void sendDownstreamGamePacket(Packet packet) {
+        sendDownstreamPacket(packet, ProtocolState.GAME);
+    }
+
+    /**
+     * Send a packet to the remote server if in the login state.
+     *
+     * @param packet the java edition packet from MCProtocolLib
+     */
+    public void sendDownstreamLoginPacket(Packet packet) {
+        sendDownstreamPacket(packet, ProtocolState.LOGIN);
+    }
+
+    /**
+     * Send a packet to the remote server if in the specified state.
+     *
+     * @param packet the java edition packet from MCProtocolLib
+     * @param intendedState the state the client should be in
+     */
+    public void sendDownstreamPacket(Packet packet, ProtocolState intendedState) {
+        if (protocol.getState() != intendedState) {
+            geyser.getLogger().debug("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " state");
+            return;
+        }
+
+        sendDownstreamPacket(packet);
+    }
+
     /**
      * Send a packet to the remote server.
      *
@@ -1687,7 +1657,8 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
     }
 
     private void sendDownstreamPacket0(Packet packet) {
-        if (protocol.getState().equals(ProtocolState.GAME) || packet.getClass() == ServerboundCustomQueryPacket.class) {
+        ProtocolState state = protocol.getState();
+        if (state == ProtocolState.GAME || state == ProtocolState.CONFIGURATION || packet.getClass() == ServerboundCustomQueryAnswerPacket.class) {
             downstream.sendPacket(packet);
         } else {
             geyser.getLogger().debug("Tried to send downstream packet " + packet.getClass().getSimpleName() + " before connected to the server");
@@ -1802,7 +1773,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
                 // We're "flying locked" in this gamemode
                 flying = true;
                 ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(true);
-                sendDownstreamPacket(abilitiesPacket);
+                sendDownstreamGamePacket(abilitiesPacket);
             }
             abilities.add(Ability.FLYING);
         }
@@ -1846,8 +1817,11 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
         if (clientRenderDistance != -1) {
             // The client has sent a render distance
             return clientRenderDistance;
+        } else if (serverRenderDistance != -1) {
+            // only known once ClientboundLoginPacket is received
+            return serverRenderDistance;
         }
-        return serverRenderDistance;
+        return 2; // unfortunate default until we got more info
     }
 
     // We need to send our skin parts to the server otherwise java sees us with no hat, jacket etc
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java
index 1e1cc8b00..3562f0d4d 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/AdvancementsCache.java
@@ -97,7 +97,7 @@ public class AdvancementsCache {
                 } else {
                     // Send a packet indicating that we intend to open this particular advancement window
                     ServerboundSeenAdvancementsPacket packet = new ServerboundSeenAdvancementsPacket(id);
-                    session.sendDownstreamPacket(packet);
+                    session.sendDownstreamGamePacket(packet);
                     // Wait for a response there
                 }
             }
@@ -137,7 +137,7 @@ public class AdvancementsCache {
 
         builder.closedResultHandler(() -> {
             // Indicate that we have closed the current advancement tab
-            session.sendDownstreamPacket(new ServerboundSeenAdvancementsPacket());
+            session.sendDownstreamGamePacket(new ServerboundSeenAdvancementsPacket());
 
         }).validResultHandler((response) -> {
             if (response.clickedButtonId() < visibleAdvancements.size()) {
@@ -146,7 +146,7 @@ public class AdvancementsCache {
             } else {
                 buildAndShowMenuForm();
                 // Indicate that we have closed the current advancement tab
-                session.sendDownstreamPacket(new ServerboundSeenAdvancementsPacket());
+                session.sendDownstreamGamePacket(new ServerboundSeenAdvancementsPacket());
             }
         });
 
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/BookEditCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/BookEditCache.java
index 35bea7295..d1ebd0c78 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/BookEditCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/BookEditCache.java
@@ -68,7 +68,7 @@ public class BookEditCache {
             packet = null;
             return;
         }
-        session.sendDownstreamPacket(packet);
+        session.sendDownstreamGamePacket(packet);
         packet = null;
         lastBookUpdate = System.currentTimeMillis();
     }
diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java
index ab1dfbe2b..5acfc1f09 100644
--- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java
+++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java
@@ -25,9 +25,10 @@
 
 package org.geysermc.geyser.session.cache;
 
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundUpdateTagsPacket;
 import it.unimi.dsi.fastutil.ints.IntList;
 import it.unimi.dsi.fastutil.ints.IntLists;
+import org.checkerframework.checker.nullness.qual.Nullable;
 import org.geysermc.geyser.GeyserLogger;
 import org.geysermc.geyser.inventory.GeyserItemStack;
 import org.geysermc.geyser.item.type.Item;
@@ -112,7 +113,7 @@ public class TagCache {
         }
     }
 
-    private IntList load(int[] tags) {
+    private IntList load(int @Nullable[] tags) {
         if (tags == null) {
             return IntLists.EMPTY_LIST;
         }
diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java
index 41f750990..f491473be 100644
--- a/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java
+++ b/core/src/main/java/org/geysermc/geyser/skin/SkinProvider.java
@@ -58,7 +58,7 @@ import java.util.function.Predicate;
 
 public class SkinProvider {
     private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
-    static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
+    static ExecutorService EXECUTOR_SERVICE;
 
     static final Skin EMPTY_SKIN;
     static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, -1, true);
@@ -133,6 +133,20 @@ public class SkinProvider {
         WEARING_CUSTOM_SKULL_SLIM = new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.wearingCustomSkullSlim\"}}", wearingCustomSkullSlim, false);
     }
 
+    private static ExecutorService getExecutorService() {
+        if (EXECUTOR_SERVICE == null) {
+            EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
+        }
+        return EXECUTOR_SERVICE;
+    }
+
+    public static void shutdown() {
+        if (EXECUTOR_SERVICE != null) {
+            EXECUTOR_SERVICE.shutdown();
+            EXECUTOR_SERVICE = null;
+        }
+    }
+
     public static void registerCacheImageTask(GeyserImpl geyser) {
         // Schedule Daily Image Expiry if we are caching them
         if (geyser.getConfig().getCacheImages() > 0) {
@@ -302,7 +316,7 @@ public class SkinProvider {
 
             GeyserImpl.getInstance().getLogger().debug("Took " + (System.currentTimeMillis() - time) + "ms for " + playerId);
             return skinAndCape;
-        }, EXECUTOR_SERVICE);
+        }, getExecutorService());
     }
 
     static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
@@ -320,7 +334,7 @@ public class SkinProvider {
 
         CompletableFuture<Skin> future;
         if (newThread) {
-            future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
+            future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), getExecutorService())
                     .whenCompleteAsync((skin, throwable) -> {
                         skin.updated = true;
                         CACHED_JAVA_SKINS.put(textureUrl, skin);
@@ -349,7 +363,7 @@ public class SkinProvider {
 
         CompletableFuture<Cape> future;
         if (newThread) {
-            future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
+            future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), getExecutorService())
                     .whenCompleteAsync((cape, throwable) -> {
                         CACHED_JAVA_CAPES.put(capeUrl, cape);
                         requestedCapes.remove(capeUrl);
@@ -388,7 +402,7 @@ public class SkinProvider {
 
         CompletableFuture<Skin> future;
         if (newThread) {
-            future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), EXECUTOR_SERVICE)
+            future = CompletableFuture.supplyAsync(() -> supplyEars(skin, earsUrl), getExecutorService())
                     .whenCompleteAsync((outSkin, throwable) -> { });
         } else {
             Skin ears = supplyEars(skin, earsUrl); // blocking
@@ -620,7 +634,7 @@ public class SkinProvider {
                 }
                 return null;
             }
-        }, EXECUTOR_SERVICE);
+        }, getExecutorService());
     }
 
     /**
@@ -646,7 +660,7 @@ public class SkinProvider {
                 }
                 return null;
             }
-        }, EXECUTOR_SERVICE).thenCompose(uuid -> {
+        }, getExecutorService()).thenCompose(uuid -> {
             if (uuid == null) {
                 return CompletableFuture.completedFuture(null);
             }
diff --git a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java
index 260b45136..00866eda3 100644
--- a/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java
+++ b/core/src/main/java/org/geysermc/geyser/text/MinecraftLocale.java
@@ -76,7 +76,7 @@ public class MinecraftLocale {
     public static void downloadAndLoadLocale(String locale) {
         locale = locale.toLowerCase(Locale.ROOT);
         if (locale.equals("nb_no")) {
-            // Different locale code - https://minecraft.fandom.com/wiki/Language
+            // Different locale code - https://minecraft.wiki/w/Language
             locale = "no_no";
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
index db0702e13..00323ce83 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/BeaconInventoryTranslator.java
@@ -114,12 +114,12 @@ public class BeaconInventoryTranslator extends AbstractBlockInventoryTranslator
         // Input a beacon payment
         BeaconPaymentAction beaconPayment = (BeaconPaymentAction) request.getActions()[0];
         ServerboundSetBeaconPacket packet = new ServerboundSetBeaconPacket(toJava(beaconPayment.getPrimaryEffect()), toJava(beaconPayment.getSecondaryEffect()));
-        session.sendDownstreamPacket(packet);
+        session.sendDownstreamGamePacket(packet);
         return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
     }
 
     private OptionalInt toJava(int effectChoice) {
-        return effectChoice == 0 ? OptionalInt.empty() : OptionalInt.of(effectChoice);
+        return effectChoice == 0 ? OptionalInt.empty() : OptionalInt.of(effectChoice - 1);
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java
index a1c928c6b..0085a7550 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/EnchantingInventoryTranslator.java
@@ -129,7 +129,7 @@ public class EnchantingInventoryTranslator extends AbstractBlockInventoryTransla
             return rejectRequest(request);
         }
         ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getJavaId(), javaSlot);
-        session.sendDownstreamPacket(packet);
+        session.sendDownstreamGamePacket(packet);
         return acceptRequest(request, makeContainerEntries(session, inventory, IntSets.emptySet()));
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java
index ec0d4534d..92cd8a0d4 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LecternInventoryTranslator.java
@@ -102,7 +102,7 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
         if (session.isDroppingLecternBook()) {
             // We have to enter the inventory GUI to eject the book
             ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getJavaId(), 3);
-            session.sendDownstreamPacket(packet);
+            session.sendDownstreamGamePacket(packet);
             session.setDroppingLecternBook(false);
             InventoryUtils.closeInventory(session, inventory.getJavaId(), false);
         } else if (lecternContainer.getBlockEntityTag() == null) {
@@ -153,7 +153,7 @@ public class LecternInventoryTranslator extends BaseInventoryTranslator {
                 session.getLecternCache().add(position);
                 // Close the window - we will reopen it once the client has this data synced
                 ServerboundContainerClosePacket closeWindowPacket = new ServerboundContainerClosePacket(lecternContainer.getJavaId());
-                session.sendDownstreamPacket(closeWindowPacket);
+                session.sendDownstreamGamePacket(closeWindowPacket);
                 InventoryUtils.closeInventory(session, inventory.getJavaId(), false);
             }
         }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java
index 8fb98a284..0e43ba660 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/LoomInventoryTranslator.java
@@ -149,7 +149,7 @@ public class LoomInventoryTranslator extends AbstractBlockInventoryTranslator {
         // And the Java loom window has a fixed row/width of four
         // So... Number / 4 = row (so we don't have to bother there), and number % 4 is our column, which leads us back to our index. :)
         ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getJavaId(), index);
-        session.sendDownstreamPacket(packet);
+        session.sendDownstreamGamePacket(packet);
 
         GeyserItemStack inputCopy = inventory.getItem(0).copy(1);
         inputCopy.setNetId(session.getNextItemNetId());
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java
index e159827e8..c4f958ba1 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/MerchantInventoryTranslator.java
@@ -155,7 +155,7 @@ public class MerchantInventoryTranslator extends BaseInventoryTranslator {
 
     private ItemStackResponse handleTrade(GeyserSession session, Inventory inventory, ItemStackRequest request, int tradeChoice) {
         ServerboundSelectTradePacket packet = new ServerboundSelectTradePacket(tradeChoice);
-        session.sendDownstreamPacket(packet);
+        session.sendDownstreamGamePacket(packet);
 
         if (session.isEmulatePost1_13Logic()) {
             // 1.18 Java cooperates nicer than older versions
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
index b9468ac4f..613121dfd 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/PlayerInventoryTranslator.java
@@ -359,7 +359,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
                     }
 
                     ServerboundSetCreativeModeSlotPacket creativeDropPacket = new ServerboundSetCreativeModeSlotPacket(-1, sourceItem.getItemStack(dropAction.getCount()));
-                    session.sendDownstreamPacket(creativeDropPacket);
+                    session.sendDownstreamGamePacket(creativeDropPacket);
 
                     sourceItem.sub(dropAction.getCount());
                 }
@@ -494,7 +494,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
                         dropStack = new ItemStack(javaCreativeItem.getId(), dropAction.getCount(), javaCreativeItem.getNbt());
                     }
                     ServerboundSetCreativeModeSlotPacket creativeDropPacket = new ServerboundSetCreativeModeSlotPacket(-1, dropStack);
-                    session.sendDownstreamPacket(creativeDropPacket);
+                    session.sendDownstreamGamePacket(creativeDropPacket);
                     break;
                 }
                 default:
@@ -515,7 +515,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
         ItemStack itemStack = item.isEmpty() ? new ItemStack(-1, 0, null) : item.getItemStack();
 
         ServerboundSetCreativeModeSlotPacket creativePacket = new ServerboundSetCreativeModeSlotPacket(slot, itemStack);
-        session.sendDownstreamPacket(creativePacket);
+        session.sendDownstreamGamePacket(creativePacket);
     }
 
     private static boolean isCraftingGrid(ItemStackRequestSlotData slotInfoData) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java
index 0663866ed..50c040a0b 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/inventory/StonecutterInventoryTranslator.java
@@ -69,7 +69,7 @@ public class StonecutterInventoryTranslator extends AbstractBlockInventoryTransl
         if (container.getStonecutterButton() != button) {
             // Getting the index of the item in the Java stonecutter list
             ServerboundContainerButtonClickPacket packet = new ServerboundContainerButtonClickPacket(inventory.getJavaId(), button);
-            session.sendDownstreamPacket(packet);
+            session.sendDownstreamGamePacket(packet);
             container.setStonecutterButton(button);
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/DecoratedPotBlockEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/DecoratedPotBlockEntityTranslator.java
index e60342b27..1774d9c76 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/DecoratedPotBlockEntityTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/level/block/entity/DecoratedPotBlockEntityTranslator.java
@@ -41,6 +41,10 @@ public class DecoratedPotBlockEntityTranslator extends BlockEntityTranslator {
 
     @Override
     public void translateTag(NbtMapBuilder builder, CompoundTag tag, int blockState) {
+        if (tag == null) {
+            return;
+        }
+
         // exact same format
         if (tag.get("sherds") instanceof ListTag sherds) {
             List<String> translated = new ArrayList<>(4);
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java
index 60ff187f5..33fbaed30 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockAnimateTranslator.java
@@ -64,7 +64,7 @@ public class BedrockAnimateTranslator extends PacketTranslator<AnimatePacket> {
                                 // and Bedrock 1.19.51.
                                 // Note for the future: we should probably largely ignore this packet and instead replicate
                                 // all actions on our end, and send swings where needed.
-                                session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
+                                session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
                                 session.activateArmAnimationTicking();
                             }
                         },
@@ -77,12 +77,12 @@ public class BedrockAnimateTranslator extends PacketTranslator<AnimatePacket> {
                 // Packet value is a float of how long one has been rowing, so we convert that into a boolean
                 session.setSteeringLeft(packet.getRowingTime() > 0.0);
                 ServerboundPaddleBoatPacket steerLeftPacket = new ServerboundPaddleBoatPacket(session.isSteeringLeft(), session.isSteeringRight());
-                session.sendDownstreamPacket(steerLeftPacket);
+                session.sendDownstreamGamePacket(steerLeftPacket);
             }
             case ROW_RIGHT -> {
                 session.setSteeringRight(packet.getRowingTime() > 0.0);
                 ServerboundPaddleBoatPacket steerRightPacket = new ServerboundPaddleBoatPacket(session.isSteeringLeft(), session.isSteeringRight());
-                session.sendDownstreamPacket(steerRightPacket);
+                session.sendDownstreamGamePacket(steerRightPacket);
             }
         }
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockEntityDataTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockEntityDataTranslator.java
index f280f144f..bab5e59a5 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockEntityDataTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockBlockEntityDataTranslator.java
@@ -108,7 +108,7 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator<BlockEnti
             if (iterator < lines.length) lines[iterator] = newMessage.toString();
             Vector3i pos = Vector3i.from(tag.getInt("x"), tag.getInt("y"), tag.getInt("z"));
             ServerboundSignUpdatePacket signUpdatePacket = new ServerboundSignUpdatePacket(pos, lines, session.getWorldCache().isEditingSignOnFront());
-            session.sendDownstreamPacket(signUpdatePacket);
+            session.sendDownstreamGamePacket(signUpdatePacket);
 
         } else if (id.equals("JigsawBlock")) {
             // Client has just sent a jigsaw block update
@@ -120,7 +120,7 @@ public class BedrockBlockEntityDataTranslator extends PacketTranslator<BlockEnti
             String joint = tag.getString("joint");
             ServerboundSetJigsawBlockPacket jigsawPacket = new ServerboundSetJigsawBlockPacket(pos, name, target, pool,
                     finalState, joint);
-            session.sendDownstreamPacket(jigsawPacket);
+            session.sendDownstreamGamePacket(jigsawPacket);
         }
 
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandBlockUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandBlockUpdateTranslator.java
index 42e9277d3..b9561518e 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandBlockUpdateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandBlockUpdateTranslator.java
@@ -53,13 +53,13 @@ public class BedrockCommandBlockUpdateTranslator extends PacketTranslator<Comman
             boolean automatic = !packet.isRedstoneMode(); // Automatic = Always Active option in Java
             ServerboundSetCommandBlockPacket commandBlockPacket = new ServerboundSetCommandBlockPacket(
                     packet.getBlockPosition(), command, mode, outputTracked, isConditional, automatic);
-            session.sendDownstreamPacket(commandBlockPacket);
+            session.sendDownstreamGamePacket(commandBlockPacket);
         } else {
             ServerboundSetCommandMinecartPacket commandMinecartPacket = new ServerboundSetCommandMinecartPacket(
                     session.getEntityCache().getEntityByGeyserId(packet.getMinecartRuntimeEntityId()).getEntityId(),
                     command, outputTracked
             );
-            session.sendDownstreamPacket(commandMinecartPacket);
+            session.sendDownstreamGamePacket(commandMinecartPacket);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java
index 62dd39b12..e2552802f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockContainerCloseTranslator.java
@@ -55,7 +55,7 @@ public class BedrockContainerCloseTranslator extends PacketTranslator<ContainerC
         if (openInventory != null) {
             if (bedrockId == openInventory.getBedrockId()) {
                 ServerboundContainerClosePacket closeWindowPacket = new ServerboundContainerClosePacket(openInventory.getJavaId());
-                session.sendDownstreamPacket(closeWindowPacket);
+                session.sendDownstreamGamePacket(closeWindowPacket);
                 InventoryUtils.closeInventory(session, openInventory.getJavaId(), false);
             } else if (openInventory.isPending()) {
                 InventoryUtils.displayInventory(session, openInventory);
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
index a614663ed..bf437311d 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockInventoryTransactionTranslator.java
@@ -137,7 +137,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                             ServerboundContainerClickPacket dropPacket = new ServerboundContainerClickPacket(
                                     inventory.getJavaId(), inventory.getStateId(), hotbarSlot, clickType.actionType, clickType.action,
                                     inventory.getCursor().getItemStack(), changedItem);
-                            session.sendDownstreamPacket(dropPacket);
+                            session.sendDownstreamGamePacket(dropPacket);
                             return;
                         }
                         if (session.getPlayerInventory().getItemInHand().isEmpty()) {
@@ -150,7 +150,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                                 Direction.DOWN,
                                 0
                         );
-                        session.sendDownstreamPacket(dropPacket);
+                        session.sendDownstreamGamePacket(dropPacket);
 
                         if (dropAll) {
                             session.getPlayerInventory().setItemInHand(GeyserItemStack.EMPTY);
@@ -309,7 +309,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                                 packet.getClickPosition().getX(), packet.getClickPosition().getY(), packet.getClickPosition().getZ(),
                                 false,
                                 session.getWorldCache().nextPredictionSequence());
-                        session.sendDownstreamPacket(blockPacket);
+                        session.sendDownstreamGamePacket(blockPacket);
 
                         Item item = session.getPlayerInventory().getItemInHand().asItem();
                         if (packet.getItemInHand() != null) {
@@ -384,7 +384,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         }
 
                         ServerboundUseItemPacket useItemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
-                        session.sendDownstreamPacket(useItemPacket);
+                        session.sendDownstreamGamePacket(useItemPacket);
 
                         List<LegacySetItemSlotData> legacySlots = packet.getLegacySlots();
                         if (packet.getActions().size() == 1 && legacySlots.size() > 0) {
@@ -453,13 +453,13 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         if (itemFrameEntity != null) {
                             ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(),
                                     InteractAction.ATTACK, session.isSneaking());
-                            session.sendDownstreamPacket(attackPacket);
+                            session.sendDownstreamGamePacket(attackPacket);
                             break;
                         }
 
                         PlayerAction action = session.getGameMode() == GameMode.CREATIVE ? PlayerAction.START_DIGGING : PlayerAction.FINISH_DIGGING;
                         ServerboundPlayerActionPacket breakPacket = new ServerboundPlayerActionPacket(action, packet.getBlockPosition(), Direction.VALUES[packet.getBlockFace()], sequence);
-                        session.sendDownstreamPacket(breakPacket);
+                        session.sendDownstreamGamePacket(breakPacket);
                     }
                 }
                 break;
@@ -468,7 +468,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                     // Followed to the Minecraft Protocol specification outlined at wiki.vg
                     ServerboundPlayerActionPacket releaseItemPacket = new ServerboundPlayerActionPacket(PlayerAction.RELEASE_USE_ITEM, Vector3i.ZERO,
                             Direction.DOWN, 0);
-                    session.sendDownstreamPacket(releaseItemPacket);
+                    session.sendDownstreamGamePacket(releaseItemPacket);
                 }
                 break;
             case ITEM_USE_ON_ENTITY:
@@ -490,7 +490,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                         }
                         ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(entityId,
                                 InteractAction.ATTACK, session.isSneaking());
-                        session.sendDownstreamPacket(attackPacket);
+                        session.sendDownstreamGamePacket(attackPacket);
 
                         // Since 1.19.10, LevelSoundEventPackets are no longer sent by the client when attacking entities
                         CooldownUtils.sendCooldown(session);
@@ -510,7 +510,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
         Vector3f clickPosition = packet.getClickPosition().sub(entityPosition);
         boolean isSpectator = session.getGameMode() == GameMode.SPECTATOR;
         for (Hand hand : EntityUtils.HANDS) {
-            session.sendDownstreamPacket(new ServerboundInteractPacket(entity.getEntityId(),
+            session.sendDownstreamGamePacket(new ServerboundInteractPacket(entity.getEntityId(),
                     InteractAction.INTERACT_AT, clickPosition.getX(), clickPosition.getY(), clickPosition.getZ(),
                     hand, session.isSneaking()));
 
@@ -522,7 +522,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
             }
 
             if (!result.consumesAction()) {
-                session.sendDownstreamPacket(new ServerboundInteractPacket(entity.getEntityId(),
+                session.sendDownstreamGamePacket(new ServerboundInteractPacket(entity.getEntityId(),
                         InteractAction.INTERACT, hand, session.isSneaking()));
                 if (!isSpectator) {
                     result = entity.interact(hand);
@@ -532,7 +532,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
             if (result.consumesAction()) {
                 if (result.shouldSwing() && hand == Hand.OFF_HAND) {
                     // Currently, Bedrock will send us the arm swing packet in most cases. But it won't for offhand.
-                    session.sendDownstreamPacket(new ServerboundSwingPacket(hand));
+                    session.sendDownstreamGamePacket(new ServerboundSwingPacket(hand));
                     // Note here to look into sending the animation packet back to Bedrock
                 }
                 return;
@@ -629,7 +629,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
         lookAt(session, target);
 
         ServerboundUseItemPacket itemPacket = new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence());
-        session.sendDownstreamPacket(itemPacket);
+        session.sendDownstreamGamePacket(itemPacket);
         return true;
     }
 
@@ -671,7 +671,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
         ServerboundMovePlayerPosRotPacket returnPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), entity.getYaw(), entity.getPitch());
         // This matches Java edition behavior
         ServerboundMovePlayerPosRotPacket movementPacket = new ServerboundMovePlayerPosRotPacket(entity.isOnGround(), playerPosition.getX(), playerPosition.getY(), playerPosition.getZ(), yaw, pitch);
-        session.sendDownstreamPacket(movementPacket);
+        session.sendDownstreamGamePacket(movementPacket);
 
         if (session.getLookBackScheduledFuture() != null) {
             session.getLookBackScheduledFuture().cancel(false);
@@ -683,7 +683,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
                     // The player moved/rotated so there is no need to change their rotation back
                     return;
                 }
-                session.sendDownstreamPacket(returnPacket);
+                session.sendDownstreamGamePacket(returnPacket);
             }, 150, TimeUnit.MILLISECONDS));
         }
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockItemFrameDropItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockItemFrameDropItemTranslator.java
index 908599f51..f11fd202c 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockItemFrameDropItemTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockItemFrameDropItemTranslator.java
@@ -49,7 +49,7 @@ public class BedrockItemFrameDropItemTranslator extends PacketTranslator<ItemFra
         if (entity != null) {
             ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(entity.getEntityId(),
                     InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
-            session.sendDownstreamPacket(interactPacket);
+            session.sendDownstreamGamePacket(interactPacket);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java
index d28aafcb9..b2a34d904 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockLecternUpdateTranslator.java
@@ -58,7 +58,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
                     0, 0, 0, // Java doesn't care about these when dealing with a lectern
                     false,
                     session.getWorldCache().nextPredictionSequence());
-            session.sendDownstreamPacket(blockPacket);
+            session.sendDownstreamGamePacket(blockPacket);
         } else {
             // Bedrock wants to either move a page or exit
             if (!(session.getOpenInventory() instanceof LecternContainer lecternContainer)) {
@@ -69,7 +69,7 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
             if (lecternContainer.getCurrentBedrockPage() == packet.getPage()) {
                 // The same page means Bedrock is closing the window
                 ServerboundContainerClosePacket closeWindowPacket = new ServerboundContainerClosePacket(lecternContainer.getJavaId());
-                session.sendDownstreamPacket(closeWindowPacket);
+                session.sendDownstreamGamePacket(closeWindowPacket);
                 InventoryUtils.closeInventory(session, lecternContainer.getJavaId(), false);
             } else {
                 // Each "page" Bedrock gives to us actually represents two pages (think opening a book and seeing two pages)
@@ -83,12 +83,12 @@ public class BedrockLecternUpdateTranslator extends PacketTranslator<LecternUpda
                 if (newJavaPage > currentJavaPage) {
                     for (int i = currentJavaPage; i < newJavaPage; i++) {
                         ServerboundContainerButtonClickPacket clickButtonPacket = new ServerboundContainerButtonClickPacket(session.getOpenInventory().getJavaId(), 2);
-                        session.sendDownstreamPacket(clickButtonPacket);
+                        session.sendDownstreamGamePacket(clickButtonPacket);
                     }
                 } else {
                     for (int i = currentJavaPage; i > newJavaPage; i--) {
                         ServerboundContainerButtonClickPacket clickButtonPacket = new ServerboundContainerButtonClickPacket(session.getOpenInventory().getJavaId(), 1);
-                        session.sendDownstreamPacket(clickButtonPacket);
+                        session.sendDownstreamGamePacket(clickButtonPacket);
                     }
                 }
             }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java
index d045a6c24..f5086e29a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMobEquipmentTranslator.java
@@ -58,7 +58,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
         session.getPlayerInventory().setHeldItemSlot(newSlot);
 
         ServerboundSetCarriedItemPacket setCarriedItemPacket = new ServerboundSetCarriedItemPacket(newSlot);
-        session.sendDownstreamPacket(setCarriedItemPacket);
+        session.sendDownstreamGamePacket(setCarriedItemPacket);
 
         GeyserItemStack newItem = session.getPlayerInventory().getItemInHand();
 
@@ -66,7 +66,7 @@ public class BedrockMobEquipmentTranslator extends PacketTranslator<MobEquipment
             // Activate shield since we are already sneaking
             // (No need to send a release item packet - Java doesn't do this when swapping items)
             // Required to do it a tick later or else it doesn't register
-            session.scheduleInEventLoop(() -> session.sendDownstreamPacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence())),
+            session.scheduleInEventLoop(() -> session.sendDownstreamGamePacket(new ServerboundUseItemPacket(Hand.MAIN_HAND, session.getWorldCache().nextPredictionSequence())),
                     50, TimeUnit.MILLISECONDS);
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMoveEntityAbsoluteTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMoveEntityAbsoluteTranslator.java
index 4b5107bda..8a8749e34 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMoveEntityAbsoluteTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockMoveEntityAbsoluteTranslator.java
@@ -72,6 +72,6 @@ public class BedrockMoveEntityAbsoluteTranslator extends PacketTranslator<MoveEn
                 packet.getPosition().getX(), y, packet.getPosition().getZ(),
                 packet.getRotation().getY() - 90, packet.getRotation().getX()
         );
-        session.sendDownstreamPacket(ServerboundMoveVehiclePacket);
+        session.sendDownstreamGamePacket(ServerboundMoveVehiclePacket);
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java
index 1ccd5ced9..412a97e3a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockNetworkStackLatencyTranslator.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.bedrock;
 
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundKeepAlivePacket;
+import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundKeepAlivePacket;
 import org.cloudburstmc.protocol.bedrock.data.AttributeData;
 import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket;
 import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java
index 79833e6ae..1737364ff 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockPlayerInputTranslator.java
@@ -50,7 +50,7 @@ public class BedrockPlayerInputTranslator extends PacketTranslator<PlayerInputPa
                 packet.getInputMotion().getX(), packet.getInputMotion().getY(), packet.isJumping(), packet.isSneaking()
         );
 
-        session.sendDownstreamPacket(playerInputPacket);
+        session.sendDownstreamGamePacket(playerInputPacket);
 
         // Bedrock only sends movement vehicle packets while moving
         // This allows horses to take damage while standing on magma
@@ -83,7 +83,7 @@ public class BedrockPlayerInputTranslator extends PacketTranslator<PlayerInputPa
                         vehiclePosition.getX(), vehiclePosition.getY(), vehiclePosition.getZ(),
                         vehicle.getYaw() - 90, vehicle.getPitch()
                 );
-                session.sendDownstreamPacket(moveVehiclePacket);
+                session.sendDownstreamGamePacket(moveVehiclePacket);
             }
         }
     }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRequestAbilityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRequestAbilityTranslator.java
index 00cd5e5fd..be2b1f28a 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRequestAbilityTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRequestAbilityTranslator.java
@@ -42,6 +42,7 @@ public class BedrockRequestAbilityTranslator extends PacketTranslator<RequestAbi
 
     @Override
     public void translate(GeyserSession session, RequestAbilityPacket packet) {
+        // TODO: Since 1.20.30, this was replaced by a START_FLYING and STOP_FLYING case in BedrockActionTranslator
         if (packet.getAbility() == Ability.FLYING) {
             boolean isFlying = packet.isBoolValue();
             if (!isFlying && session.getGameMode() == GameMode.SPECTATOR) {
@@ -57,7 +58,7 @@ public class BedrockRequestAbilityTranslator extends PacketTranslator<RequestAbi
 
             session.setFlying(isFlying);
             ServerboundPlayerAbilitiesPacket abilitiesPacket = new ServerboundPlayerAbilitiesPacket(isFlying);
-            session.sendDownstreamPacket(abilitiesPacket);
+            session.sendDownstreamGamePacket(abilitiesPacket);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRespawnTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRespawnTranslator.java
index c89f7b6e0..7c4798f80 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRespawnTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockRespawnTranslator.java
@@ -39,7 +39,7 @@ public class BedrockRespawnTranslator extends PacketTranslator<RespawnPacket> {
     public void translate(GeyserSession session, RespawnPacket packet) {
         if (packet.getState() == RespawnPacket.State.CLIENT_READY) {
             ServerboundClientCommandPacket javaRespawnPacket = new ServerboundClientCommandPacket(ClientCommand.RESPAWN);
-            session.sendDownstreamPacket(javaRespawnPacket);
+            session.sendDownstreamGamePacket(javaRespawnPacket);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockShowCreditsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockShowCreditsTranslator.java
index 0aec2a5d9..3314975ef 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockShowCreditsTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockShowCreditsTranslator.java
@@ -39,7 +39,7 @@ public class BedrockShowCreditsTranslator extends PacketTranslator<ShowCreditsPa
     public void translate(GeyserSession session, ShowCreditsPacket packet) {
         if (packet.getStatus() == ShowCreditsPacket.Status.END_CREDITS) {
             ServerboundClientCommandPacket javaRespawnPacket = new ServerboundClientCommandPacket(ClientCommand.RESPAWN);
-            session.sendDownstreamPacket(javaRespawnPacket);
+            session.sendDownstreamGamePacket(javaRespawnPacket);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java
index f8b65bf9b..58d5c2018 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/BedrockEntityEventTranslator.java
@@ -49,7 +49,7 @@ public class BedrockEntityEventTranslator extends PacketTranslator<EntityEventPa
             case COMPLETE_TRADE -> {
                 // Not sent as of 1.18.10
                 ServerboundSelectTradePacket selectTradePacket = new ServerboundSelectTradePacket(packet.getData());
-                session.sendDownstreamPacket(selectTradePacket);
+                session.sendDownstreamGamePacket(selectTradePacket);
 
                 session.scheduleInEventLoop(() -> {
                     Inventory openInventory = session.getOpenInventory();
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java
index 1917ad6d1..bff097248 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockActionTranslator.java
@@ -26,10 +26,6 @@
 package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
 
 import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
 import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
 import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
 import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
@@ -97,7 +93,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
             case START_SWIMMING:
                 if (!entity.getFlag(EntityFlag.SWIMMING)) {
                     ServerboundPlayerCommandPacket startSwimPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING);
-                    session.sendDownstreamPacket(startSwimPacket);
+                    session.sendDownstreamGamePacket(startSwimPacket);
 
                     session.setSwimming(true);
                 }
@@ -106,7 +102,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 // Prevent packet spam when Bedrock players are crawling near the edge of a block
                 if (!session.getCollisionManager().mustPlayerCrawlHere()) {
                     ServerboundPlayerCommandPacket stopSwimPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING);
-                    session.sendDownstreamPacket(stopSwimPacket);
+                    session.sendDownstreamGamePacket(stopSwimPacket);
 
                     session.setSwimming(false);
                 }
@@ -114,45 +110,45 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
             case START_GLIDE:
                 // Otherwise gliding will not work in creative
                 ServerboundPlayerAbilitiesPacket playerAbilitiesPacket = new ServerboundPlayerAbilitiesPacket(false);
-                session.sendDownstreamPacket(playerAbilitiesPacket);
+                session.sendDownstreamGamePacket(playerAbilitiesPacket);
             case STOP_GLIDE:
                 ServerboundPlayerCommandPacket glidePacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_ELYTRA_FLYING);
-                session.sendDownstreamPacket(glidePacket);
+                session.sendDownstreamGamePacket(glidePacket);
                 break;
             case START_SNEAK:
                 ServerboundPlayerCommandPacket startSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
-                session.sendDownstreamPacket(startSneakPacket);
+                session.sendDownstreamGamePacket(startSneakPacket);
 
                 session.startSneaking();
                 break;
             case STOP_SNEAK:
                 ServerboundPlayerCommandPacket stopSneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SNEAKING);
-                session.sendDownstreamPacket(stopSneakPacket);
+                session.sendDownstreamGamePacket(stopSneakPacket);
 
                 session.stopSneaking();
                 break;
             case START_SPRINT:
                 if (!entity.getFlag(EntityFlag.SWIMMING)) {
                     ServerboundPlayerCommandPacket startSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SPRINTING);
-                    session.sendDownstreamPacket(startSprintPacket);
+                    session.sendDownstreamGamePacket(startSprintPacket);
                     session.setSprinting(true);
                 }
                 break;
             case STOP_SPRINT:
                 if (!entity.getFlag(EntityFlag.SWIMMING)) {
                     ServerboundPlayerCommandPacket stopSprintPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.STOP_SPRINTING);
-                    session.sendDownstreamPacket(stopSprintPacket);
+                    session.sendDownstreamGamePacket(stopSprintPacket);
                 }
                 session.setSprinting(false);
                 break;
             case DROP_ITEM:
                 ServerboundPlayerActionPacket dropItemPacket = new ServerboundPlayerActionPacket(PlayerAction.DROP_ITEM,
                         vector, Direction.VALUES[packet.getFace()], 0);
-                session.sendDownstreamPacket(dropItemPacket);
+                session.sendDownstreamGamePacket(dropItemPacket);
                 break;
             case STOP_SLEEP:
                 ServerboundPlayerCommandPacket stopSleepingPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.LEAVE_BED);
-                session.sendDownstreamPacket(stopSleepingPacket);
+                session.sendDownstreamGamePacket(stopSleepingPacket);
                 break;
             case START_BREAK: {
                 // Ignore START_BREAK when the player is CREATIVE to avoid Spigot receiving 2 packets it interpets as block breaking. https://github.com/GeyserMC/Geyser/issues/4021 
@@ -189,12 +185,12 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                 if (identifier.startsWith("minecraft:fire") || identifier.startsWith("minecraft:soul_fire")) {
                     ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING, fireBlockPos,
                             Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
-                    session.sendDownstreamPacket(startBreakingPacket);
+                    session.sendDownstreamGamePacket(startBreakingPacket);
                 }
 
                 ServerboundPlayerActionPacket startBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.START_DIGGING,
                         vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
-                session.sendDownstreamPacket(startBreakingPacket);
+                session.sendDownstreamGamePacket(startBreakingPacket);
                 break;
             }
             case CONTINUE_BREAK:
@@ -236,7 +232,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                         // Break the block
                         ServerboundPlayerActionPacket finishBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING,
                                 vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
-                        session.sendDownstreamPacket(finishBreakingPacket);
+                        session.sendDownstreamGamePacket(finishBreakingPacket);
                         session.setBlockBreakStartTime(0);
                         break;
                     }
@@ -253,13 +249,13 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                     if (itemFrameEntity != null) {
                         ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(itemFrameEntity.getEntityId(),
                                 InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
-                        session.sendDownstreamPacket(interactPacket);
+                        session.sendDownstreamGamePacket(interactPacket);
                         break;
                     }
                 }
 
                 ServerboundPlayerActionPacket abortBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.CANCEL_DIGGING, vector, Direction.DOWN, 0);
-                session.sendDownstreamPacket(abortBreakingPacket);
+                session.sendDownstreamGamePacket(abortBreakingPacket);
                 LevelEventPacket stopBreak = new LevelEventPacket();
                 stopBreak.setType(LevelEvent.BLOCK_STOP_BREAK);
                 stopBreak.setPosition(vector.toFloat());
@@ -287,7 +283,7 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
             case MISSED_SWING:
                 // TODO Re-evaluate after pre-1.20.10 is no longer supported?
                 if (session.getArmAnimationTicks() == -1) {
-                    session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
+                    session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
                     session.activateArmAnimationTicking();
 
                     // Send packet to Bedrock so it knows
@@ -297,6 +293,40 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
                     session.sendUpstreamPacket(animatePacket);
                 }
                 break;
+            case START_FLYING: // Since 1.20.30
+                 if (session.isCanFly()) {
+                     if (session.getGameMode() == GameMode.SPECTATOR) {
+                         // should already be flying
+                         session.sendAdventureSettings();
+                         break;
+                     }
+
+                     if (session.getPlayerEntity().getFlag(EntityFlag.SWIMMING) && session.getCollisionManager().isPlayerInWater()) {
+                         // As of 1.18.1, Java Edition cannot fly while in water, but it can fly while crawling
+                         // If this isn't present, swimming on a 1.13.2 server and then attempting to fly will put you into a flying/swimming state that is invalid on JE
+                         session.sendAdventureSettings();
+                         break;
+                     }
+
+                     session.setFlying(true);
+                     session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(true));
+                 } else {
+                     // update whether we can fly
+                     session.sendAdventureSettings();
+                     // stop flying
+                     PlayerActionPacket stopFlyingPacket = new PlayerActionPacket();
+                     stopFlyingPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
+                     stopFlyingPacket.setAction(PlayerActionType.STOP_FLYING);
+                     stopFlyingPacket.setBlockPosition(Vector3i.ZERO);
+                     stopFlyingPacket.setResultPosition(Vector3i.ZERO);
+                     stopFlyingPacket.setFace(0);
+                     session.sendUpstreamPacket(stopFlyingPacket);
+                 }
+                 break;
+            case STOP_FLYING:
+                session.setFlying(false);
+                session.sendDownstreamGamePacket(new ServerboundPlayerAbilitiesPacket(false));
+                break;
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java
index 49ce28167..bf59bd7bc 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockInteractTranslator.java
@@ -68,16 +68,16 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                 }
                 ServerboundInteractPacket interactPacket = new ServerboundInteractPacket(entity.getEntityId(),
                         InteractAction.INTERACT, Hand.MAIN_HAND, session.isSneaking());
-                session.sendDownstreamPacket(interactPacket);
+                session.sendDownstreamGamePacket(interactPacket);
                 break;
             case DAMAGE:
                 ServerboundInteractPacket attackPacket = new ServerboundInteractPacket(entity.getEntityId(),
                         InteractAction.ATTACK, Hand.MAIN_HAND, session.isSneaking());
-                session.sendDownstreamPacket(attackPacket);
+                session.sendDownstreamGamePacket(attackPacket);
                 break;
             case LEAVE_VEHICLE:
                 ServerboundPlayerCommandPacket sneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
-                session.sendDownstreamPacket(sneakPacket);
+                session.sendDownstreamGamePacket(sneakPacket);
 
                 Entity currentVehicle = session.getPlayerEntity().getVehicle();
                 if (currentVehicle != null) {
@@ -123,7 +123,7 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
                     if (ridingEntity instanceof AbstractHorseEntity || (ridingEntity != null && ridingEntity.getDefinition().entityType() == EntityType.CHEST_BOAT)) {
                         // This mob has an inventory of its own that we should open instead.
                         ServerboundPlayerCommandPacket openVehicleWindowPacket = new ServerboundPlayerCommandPacket(session.getPlayerEntity().getEntityId(), PlayerState.OPEN_VEHICLE_INVENTORY);
-                        session.sendDownstreamPacket(openVehicleWindowPacket);
+                        session.sendDownstreamGamePacket(openVehicleWindowPacket);
                     } else {
                         session.setOpenInventory(session.getPlayerInventory());
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java
index cae25e2a3..d81c0abab 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockMovePlayerTranslator.java
@@ -83,7 +83,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
             entity.setHeadYaw(headYaw);
             entity.setOnGround(packet.isOnGround());
 
-            session.sendDownstreamPacket(playerRotationPacket);
+            session.sendDownstreamGamePacket(playerRotationPacket);
         } else {
             if (session.getWorldBorder().isPassingIntoBorderBoundaries(packet.getPosition(), true)) {
                 return;
@@ -130,7 +130,7 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
                     entity.setOnGround(onGround);
 
                     // Send final movement changes
-                    session.sendDownstreamPacket(movePacket);
+                    session.sendDownstreamGamePacket(movePacket);
 
                     if (teleportThroughVoidFloor) {
                         // Work around there being a floor at the bottom of the world and teleport the player below it
diff --git a/core/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRequestPermissionsPacket.java
similarity index 60%
rename from core/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java
rename to core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRequestPermissionsPacket.java
index cca7aa48c..0eb648c43 100644
--- a/core/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRequestPermissionsPacket.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
@@ -23,33 +23,21 @@
  * @link https://github.com/GeyserMC/Geyser
  */
 
-package org.geysermc.connector.network.session.auth;
+package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
 
-import java.util.UUID;
+import org.cloudburstmc.protocol.bedrock.packet.RequestPermissionsPacket;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
 
 /**
- * Deprecated, legacy code. Serves as a wrapper around
- * the class used now.
- *
- * @deprecated legacy code
+ * Sent occasionally by a BDS client when opening the client side server settings menu.
  */
-@Deprecated
-public class AuthData {
-    private final org.geysermc.geyser.session.auth.AuthData handle;
+@Translator(packet = RequestPermissionsPacket.class)
+public class BedrockRequestPermissionsPacket extends PacketTranslator<RequestPermissionsPacket> {
 
-    public AuthData(org.geysermc.geyser.session.auth.AuthData handle) {
-        this.handle = handle;
-    }
-
-    public String getName() {
-        return this.handle.name();
-    }
-
-    public UUID getUUID() {
-        return this.handle.uuid();
-    }
-
-    public String getXboxUUID() {
-        return this.handle.xuid();
+    @Override
+    public void translate(GeyserSession session, RequestPermissionsPacket packet) {
+        session.sendAdventureSettings();
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRiderJumpTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRiderJumpTranslator.java
index 0d0ec4703..f7ac81219 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRiderJumpTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockRiderJumpTranslator.java
@@ -41,7 +41,7 @@ public class BedrockRiderJumpTranslator extends PacketTranslator<RiderJumpPacket
         Entity vehicle = session.getPlayerEntity().getVehicle();
         if (vehicle instanceof AbstractHorseEntity) {
             ServerboundPlayerCommandPacket playerCommandPacket = new ServerboundPlayerCommandPacket(vehicle.getEntityId(), PlayerState.START_HORSE_JUMP, packet.getJumpStrength());
-            session.sendDownstreamPacket(playerCommandPacket);
+            session.sendDownstreamGamePacket(playerCommandPacket);
         }
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java
index c818cc910..df28f7ca7 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDefaultGameTypeTranslator.java
@@ -41,7 +41,7 @@ public class BedrockSetDefaultGameTypeTranslator extends PacketTranslator<SetDef
      */
     @Override
     public void translate(GeyserSession session, SetDefaultGameTypePacket packet) {
-        if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) {
+        if (session.getOpPermissionLevel() >= 2 && session.hasPermission("geyser.settings.server")) {
             session.getGeyser().getWorldManager().setDefaultGameMode(session, GameMode.byId(packet.getGamemode()));
         }
         // Stop the client from updating their own Gamemode without telling the server
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java
index a36aa77df..b996a96b1 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetDifficultyTranslator.java
@@ -39,7 +39,7 @@ public class BedrockSetDifficultyTranslator extends PacketTranslator<SetDifficul
      */
     @Override
     public void translate(GeyserSession session, SetDifficultyPacket packet) {
-        if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) {
+        if (session.getOpPermissionLevel() >= 2 && session.hasPermission("geyser.settings.server")) {
             if (packet.getDifficulty() != session.getWorldCache().getDifficulty().ordinal()) {
                 session.getGeyser().getWorldManager().setDifficulty(session, Difficulty.from(packet.getDifficulty()));
             }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java
index a1c2c2987..2d8d420f8 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/entity/player/BedrockSetPlayerGameTypeTranslator.java
@@ -45,7 +45,7 @@ public class BedrockSetPlayerGameTypeTranslator extends PacketTranslator<SetPlay
     @Override
     public void translate(GeyserSession session, SetPlayerGameTypePacket packet) {
         // yes, if you are OP
-        if (session.getOpPermissionLevel() >= 2 || session.hasPermission("geyser.settings.server")) {
+        if (session.getOpPermissionLevel() >= 2 && session.hasPermission("geyser.settings.server")) {
             if (packet.getGamemode() != session.getGameMode().ordinal()) {
                 // Bedrock has more Gamemodes than Java, leading to cases 5 (for "default") and 6 (for "spectator") being sent
                 // https://github.com/CloudburstMC/Protocol/blob/3.0/bedrock-codec/src/main/java/org/cloudburstmc/protocol/bedrock/data/GameType.java
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java
index 2b48801d8..76cd82feb 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/world/BedrockLevelSoundEventTranslator.java
@@ -57,7 +57,7 @@ public class BedrockLevelSoundEventTranslator extends PacketTranslator<LevelSoun
             // ATTACK_NODAMAGE = player clicked air
             // This should only be revisited if Bedrock packets get full Java parity, or Bedrock starts sending arm
             // animation packets after ATTACK_NODAMAGE, OR ATTACK_NODAMAGE gets removed/isn't sent in the same spot
-            session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
+            session.sendDownstreamGamePacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
             session.activateArmAnimationTicking();
 
             // Send packet to Bedrock so it knows
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaClientboundResourcePacksPacket.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaClientboundResourcePacksPacket.java
new file mode 100644
index 000000000..cc2733076
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaClientboundResourcePacksPacket.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java;
+
+import com.github.steveice10.mc.protocol.data.game.ResourcePackStatus;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundResourcePackPacket;
+import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundResourcePackPacket;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
+
+@Translator(packet = ClientboundResourcePackPacket.class)
+public class JavaClientboundResourcePacksPacket extends PacketTranslator<ClientboundResourcePackPacket> {
+
+    @Override
+    public void translate(GeyserSession session, ClientboundResourcePackPacket packet) {
+        // We need to "answer" this to avoid timeout issues related to resource packs
+        // If packs are required, we need to lie to the server that we accepted them, as we get kicked otherwise.
+        if (packet.isRequired()) {
+            session.sendDownstreamPacket(new ServerboundResourcePackPacket(ResourcePackStatus.SUCCESSFULLY_LOADED));
+        } else {
+            session.sendDownstreamPacket(new ServerboundResourcePackPacket(ResourcePackStatus.DECLINED));
+        }
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java
index 80f21a1b3..2282d2d7b 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomPayloadTranslator.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundCustomPayloadPacket;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundCustomPayloadPacket;
 import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.GeyserLogger;
 import org.geysermc.geyser.session.GeyserSession;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomQueryTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomQueryTranslator.java
index 89df63898..6096af12d 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomQueryTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaCustomQueryTranslator.java
@@ -26,7 +26,7 @@
 package org.geysermc.geyser.translator.protocol.java;
 
 import com.github.steveice10.mc.protocol.packet.login.clientbound.ClientboundCustomQueryPacket;
-import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryPacket;
+import com.github.steveice10.mc.protocol.packet.login.serverbound.ServerboundCustomQueryAnswerPacket;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
@@ -40,8 +40,8 @@ public class JavaCustomQueryTranslator extends PacketTranslator<ClientboundCusto
     public void translate(GeyserSession session, ClientboundCustomQueryPacket packet) {
         // A vanilla client doesn't know any PluginMessage in the Login state, so we don't know any either.
         // Note: Fabric Networking API v1 will not let the client log in without sending this
-        session.sendDownstreamPacket(
-                new ServerboundCustomQueryPacket(packet.getMessageId(), null)
+        session.sendDownstreamLoginPacket(
+                new ServerboundCustomQueryAnswerPacket(packet.getMessageId(), null)
         );
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java
index 96b0e3dbd..8bf5ae4ba 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaDisconnectTranslator.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundDisconnectPacket;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundDisconnectPacket;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java
index 7f8500ce4..12f96360b 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaGameProfileTranslator.java
@@ -26,6 +26,7 @@
 package org.geysermc.geyser.translator.protocol.java;
 
 import com.github.steveice10.mc.auth.data.GameProfile;
+import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket;
 import com.github.steveice10.mc.protocol.packet.login.clientbound.ClientboundGameProfilePacket;
 import org.geysermc.geyser.api.network.AuthType;
 import org.geysermc.geyser.entity.type.player.PlayerEntity;
@@ -33,7 +34,11 @@ import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.skin.SkinManager;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
+import org.geysermc.geyser.util.PluginMessageUtils;
 
+/**
+ * ClientboundGameProfilePacket triggers protocol change LOGIN -> CONFIGURATION
+ */
 @Translator(packet = ClientboundGameProfilePacket.class)
 public class JavaGameProfileTranslator extends PacketTranslator<ClientboundGameProfilePacket> {
 
@@ -65,5 +70,9 @@ public class JavaGameProfileTranslator extends PacketTranslator<ClientboundGameP
         // We no longer need these variables; they're just taking up space in memory now
         session.setCertChainData(null);
         session.getClientData().setOriginalString(null);
+
+        // configuration phase stuff that the vanilla client replies with after receiving the GameProfilePacket
+        session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));
+        session.sendJavaClientSettings();
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java
index dc7b7f316..da8358da2 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaKeepAliveTranslator.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundKeepAlivePacket;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundKeepAlivePacket;
 import org.cloudburstmc.protocol.bedrock.packet.NetworkStackLatencyPacket;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
index 30a76a912..05d08a9e0 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaLoginTranslator.java
@@ -25,30 +25,20 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
+import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerSpawnInfo;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundLoginPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket;
-import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.github.steveice10.opennbt.tag.builtin.IntTag;
 import org.cloudburstmc.protocol.bedrock.data.GameRuleData;
 import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket;
 import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
-import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import org.geysermc.geyser.api.network.AuthType;
 import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
 import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
-import org.geysermc.geyser.level.JavaDimension;
 import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.TextDecoration;
-import org.geysermc.geyser.translator.level.BiomeTranslator;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
 import org.geysermc.geyser.util.ChunkUtils;
 import org.geysermc.geyser.util.DimensionUtils;
 import org.geysermc.geyser.util.EntityUtils;
-import org.geysermc.geyser.util.JavaCodecUtil;
-import org.geysermc.geyser.util.PluginMessageUtils;
-
-import java.util.Map;
 
 @Translator(packet = ClientboundLoginPacket.class)
 public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket> {
@@ -63,42 +53,22 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
             session.setErosionHandler(new GeyserboundHandshakePacketHandler(session));
         }
 
-        Map<String, JavaDimension> dimensions = session.getDimensions();
-        dimensions.clear();
-
-        JavaDimension.load(packet.getRegistry(), dimensions);
-
-        Int2ObjectMap<TextDecoration> chatTypes = session.getChatTypes();
-        chatTypes.clear();
-        for (CompoundTag tag : JavaCodecUtil.iterateAsTag(packet.getRegistry().get("minecraft:chat_type"))) {
-            // The ID is NOT ALWAYS THE SAME! ViaVersion as of 1.19 adds two registry entries that do NOT match vanilla.
-            int id = ((IntTag) tag.get("id")).getValue();
-            CompoundTag element = tag.get("element");
-            CompoundTag chat = element.get("chat");
-            TextDecoration textDecoration = null;
-            if (chat != null) {
-                textDecoration = new TextDecoration(chat);
-            }
-            chatTypes.put(id, textDecoration);
-        }
+        PlayerSpawnInfo spawnInfo = packet.getCommonPlayerSpawnInfo();
 
         // If the player is already initialized and a join game packet is sent, they
         // are swapping servers
         if (session.isSpawned()) {
-            String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), packet.getDimension());
+            String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), spawnInfo.getDimension());
             DimensionUtils.switchDimension(session, fakeDim);
 
             session.getWorldCache().removeScoreboard();
         }
-        session.setWorldName(packet.getWorldName());
+        session.setWorldName(spawnInfo.getWorldName());
         session.setLevels(packet.getWorldNames());
 
-        BiomeTranslator.loadServerBiomes(session, packet.getRegistry());
-        session.getTagCache().clear();
+        session.setGameMode(spawnInfo.getGameMode());
 
-        session.setGameMode(packet.getGameMode());
-
-        String newDimension = packet.getDimension();
+        String newDimension = spawnInfo.getDimension();
 
         boolean needsSpawnPacket = !session.isSentSpawnPacket();
         if (needsSpawnPacket) {
@@ -113,11 +83,11 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
 
         if (!needsSpawnPacket) {
             SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket();
-            playerGameTypePacket.setGamemode(EntityUtils.toBedrockGamemode(packet.getGameMode()).ordinal());
+            playerGameTypePacket.setGamemode(EntityUtils.toBedrockGamemode(spawnInfo.getGameMode()).ordinal());
             session.sendUpstreamPacket(playerGameTypePacket);
         }
 
-        entity.setLastDeathPosition(packet.getLastDeathPos());
+        entity.setLastDeathPosition(spawnInfo.getLastDeathPos());
 
         entity.updateBedrockMetadata();
 
@@ -130,16 +100,10 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
 
         session.setServerRenderDistance(packet.getViewDistance());
 
-        // TODO customize
+        // send this again now that we know the server render distance
+        // as the bedrock client isn't required to send a render distance
         session.sendJavaClientSettings();
 
-        session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:brand", PluginMessageUtils.getGeyserBrandData()));
-
-        // TODO don't send two packets
-//        if (true) {
-//            session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:register", Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8)));
-//        }
-        // register the plugin messaging channels used in Floodgate
         if (session.remoteServer().authType() == AuthType.FLOODGATE) {
             //todo
 //            session.sendDownstreamPacket(new ServerboundCustomPayloadPacket("minecraft:register", PluginMessageChannels.getFloodgateRegisterData()));
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaPingTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaPingTranslator.java
index faff85fec..c966f3abb 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaPingTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaPingTranslator.java
@@ -25,8 +25,8 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundPingPacket;
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundPongPacket;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundPingPacket;
+import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundPongPacket;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRegistryDataTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRegistryDataTranslator.java
new file mode 100644
index 000000000..f5c0dfde2
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRegistryDataTranslator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java;
+
+import com.github.steveice10.mc.protocol.packet.configuration.clientbound.ClientboundRegistryDataPacket;
+import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import com.github.steveice10.opennbt.tag.builtin.IntTag;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import org.geysermc.geyser.level.JavaDimension;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.text.TextDecoration;
+import org.geysermc.geyser.translator.level.BiomeTranslator;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
+import org.geysermc.geyser.util.JavaCodecUtil;
+
+import java.util.Map;
+
+@Translator(packet = ClientboundRegistryDataPacket.class)
+public class JavaRegistryDataTranslator extends PacketTranslator<ClientboundRegistryDataPacket> {
+
+    @Override
+    public void translate(GeyserSession session, ClientboundRegistryDataPacket packet) {
+        Map<String, JavaDimension> dimensions = session.getDimensions();
+        dimensions.clear();
+        JavaDimension.load(packet.getRegistry(), dimensions);
+
+        Int2ObjectMap<TextDecoration> chatTypes = session.getChatTypes();
+        chatTypes.clear();
+        for (CompoundTag tag : JavaCodecUtil.iterateAsTag(packet.getRegistry().get("minecraft:chat_type"))) {
+            // The ID is NOT ALWAYS THE SAME! ViaVersion as of 1.19 adds two registry entries that do NOT match vanilla.
+            int id = ((IntTag) tag.get("id")).getValue();
+            CompoundTag element = tag.get("element");
+            CompoundTag chat = element.get("chat");
+            TextDecoration textDecoration = null;
+            if (chat != null) {
+                textDecoration = new TextDecoration(chat);
+            }
+            chatTypes.put(id, textDecoration);
+        }
+
+        BiomeTranslator.loadServerBiomes(session, packet.getRegistry());
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java
index 71f5dc8fc..fb7536b19 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaRespawnTranslator.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
+import com.github.steveice10.mc.protocol.data.game.entity.player.PlayerSpawnInfo;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundRespawnPacket;
 import org.cloudburstmc.math.vector.Vector3f;
 import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
@@ -46,6 +47,7 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
     @Override
     public void translate(GeyserSession session, ClientboundRespawnPacket packet) {
         SessionPlayerEntity entity = session.getPlayerEntity();
+        PlayerSpawnInfo spawnInfo = packet.getCommonPlayerSpawnInfo();
 
         session.setSpawned(false);
 
@@ -56,13 +58,13 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
         session.setOpenInventory(null);
         session.setClosingInventory(false);
 
-        entity.setLastDeathPosition(packet.getLastDeathPos());
+        entity.setLastDeathPosition(spawnInfo.getLastDeathPos());
         entity.updateBedrockMetadata();
 
         SetPlayerGameTypePacket playerGameTypePacket = new SetPlayerGameTypePacket();
-        playerGameTypePacket.setGamemode(EntityUtils.toBedrockGamemode(packet.getGamemode()).ordinal());
+        playerGameTypePacket.setGamemode(EntityUtils.toBedrockGamemode(spawnInfo.getGameMode()).ordinal());
         session.sendUpstreamPacket(playerGameTypePacket);
-        session.setGameMode(packet.getGamemode());
+        session.setGameMode(spawnInfo.getGameMode());
 
         if (session.isRaining()) {
             LevelEventPacket stopRainPacket = new LevelEventPacket();
@@ -82,14 +84,14 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
             session.setThunder(false);
         }
 
-        String newDimension = packet.getDimension();
-        if (!session.getDimension().equals(newDimension) || !packet.getWorldName().equals(session.getWorldName())) {
+        String newDimension = spawnInfo.getDimension();
+        if (!session.getDimension().equals(newDimension) || !spawnInfo.getWorldName().equals(session.getWorldName())) {
             // Switching to a new world (based off the world name change or new dimension); send a fake dimension change
             if (DimensionUtils.javaToBedrock(session.getDimension()) == DimensionUtils.javaToBedrock(newDimension)) {
                 String fakeDim = DimensionUtils.getTemporaryDimension(session.getDimension(), newDimension);
                 DimensionUtils.switchDimension(session, fakeDim);
             }
-            session.setWorldName(packet.getWorldName());
+            session.setWorldName(spawnInfo.getWorldName());
             DimensionUtils.switchDimension(session, newDimension);
 
             ChunkUtils.loadDimension(session);
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java
index a899077f8..ae59cf0f8 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.translator.protocol.java;
 
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundUpdateTagsPacket;
+import com.github.steveice10.mc.protocol.packet.common.clientbound.ClientboundUpdateTagsPacket;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
index 50582974d..4b1483bbf 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaEntityEventTranslator.java
@@ -120,7 +120,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
                 if (fishingHook.getBedrockTargetId() == session.getPlayerEntity().getGeyserId()) {
                     Entity hookOwner = session.getEntityCache().getEntityByGeyserId(fishingHook.getBedrockOwnerId());
                     if (hookOwner != null) {
-                        // https://minecraft.gamepedia.com/Fishing_Rod#Hooking_mobs_and_other_entities
+                        // https://minecraft.wiki/w/Fishing_Rod#Hooking_mobs_and_other_entities
                         SetEntityMotionPacket motionPacket = new SetEntityMotionPacket();
                         motionPacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
                         motionPacket.setMotion(hookOwner.getPosition().sub(session.getPlayerEntity().getPosition()).mul(0.1f));
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java
index 128b051a9..e56a272ab 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/JavaUpdateMobEffectTranslator.java
@@ -42,8 +42,9 @@ public class JavaUpdateMobEffectTranslator extends PacketTranslator<ClientboundU
         if (entity == session.getPlayerEntity()) {
             session.getEffectCache().setEffect(packet.getEffect(), packet.getAmplifier());
         }
-        if (entity == null)
+        if (entity == null) {
             return;
+        }
 
         int duration = packet.getDuration();
         if (duration < 0) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java
index e711a517e..3debb7f5f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerInfoUpdateTranslator.java
@@ -29,6 +29,7 @@ import com.github.steveice10.mc.auth.data.GameProfile;
 import com.github.steveice10.mc.protocol.data.game.PlayerListEntry;
 import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundPlayerInfoUpdatePacket;
+import org.checkerframework.checker.nullness.qual.Nullable;
 import org.cloudburstmc.math.vector.Vector3f;
 import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
 import org.geysermc.geyser.GeyserImpl;
@@ -41,6 +42,7 @@ import org.geysermc.geyser.translator.protocol.Translator;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
 
 @Translator(packet = ClientboundPlayerInfoUpdatePacket.class)
 public class JavaPlayerInfoUpdateTranslator extends PacketTranslator<ClientboundPlayerInfoUpdatePacket> {
@@ -50,13 +52,24 @@ public class JavaPlayerInfoUpdateTranslator extends PacketTranslator<Clientbound
 
         if (actions.contains(PlayerListEntryAction.ADD_PLAYER)) {
             for (PlayerListEntry entry : packet.getEntries()) {
-                GameProfile profile = entry.getProfile();
+                @Nullable GameProfile profile = entry.getProfile();
+
+                UUID id = entry.getProfileId();
+                String name = null;
+                String texturesProperty = null;
+
+                if (profile != null) {
+                    name = profile.getName();
+
+                    GameProfile.Property textures = profile.getProperty("textures");
+                    if (textures != null) {
+                        texturesProperty = textures.getValue();
+                    }
+                }
+
+                boolean self = id.equals(session.getPlayerEntity().getUuid());
+
                 PlayerEntity playerEntity;
-                boolean self = profile.getId().equals(session.getPlayerEntity().getUuid());
-
-                GameProfile.Property textures = profile.getProperty("textures");
-                String texturesProperty = textures == null ? null : textures.getValue();
-
                 if (self) {
                     // Entity is ourself
                     playerEntity = session.getPlayerEntity();
@@ -66,17 +79,17 @@ public class JavaPlayerInfoUpdateTranslator extends PacketTranslator<Clientbound
                             session,
                             -1,
                             session.getEntityCache().getNextEntityId().incrementAndGet(),
-                            profile.getId(),
+                            id,
                             Vector3f.ZERO,
                             Vector3f.ZERO,
                             0, 0, 0,
-                            profile.getName(),
+                            name,
                             texturesProperty
                     );
 
                     session.getEntityCache().addPlayerEntity(playerEntity);
                 }
-                playerEntity.setUsername(profile.getName());
+                playerEntity.setUsername(name);
                 playerEntity.setTexturesProperty(texturesProperty);
 
                 if (self) {
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
index a2fc0c07c..f2c566a23 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/player/JavaPlayerPositionTranslator.java
@@ -133,9 +133,9 @@ public class JavaPlayerPositionTranslator extends PacketTranslator<ClientboundPl
     private void acceptTeleport(GeyserSession session, double x, double y, double z, float yaw, float pitch, int id) {
         // Confirm the teleport when we receive it to match Java edition
         ServerboundAcceptTeleportationPacket teleportConfirmPacket = new ServerboundAcceptTeleportationPacket(id);
-        session.sendDownstreamPacket(teleportConfirmPacket);
+        session.sendDownstreamGamePacket(teleportConfirmPacket);
         // Servers (especially ones like Hypixel) expect exact coordinates given back to them.
         ServerboundMovePlayerPosRotPacket positionPacket = new ServerboundMovePlayerPosRotPacket(false, x, y, z, yaw, pitch);
-        session.sendDownstreamPacket(positionPacket);
+        session.sendDownstreamGamePacket(positionPacket);
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java
index 6d25500b0..c17c9955d 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddEntityTranslator.java
@@ -31,11 +31,14 @@ import com.github.steveice10.mc.protocol.data.game.entity.object.ProjectileData;
 import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
 import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddEntityPacket;
 import org.cloudburstmc.math.vector.Vector3f;
+import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.entity.EntityDefinition;
 import org.geysermc.geyser.entity.type.*;
 import org.geysermc.geyser.entity.type.player.PlayerEntity;
 import org.geysermc.geyser.registry.Registries;
 import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.skin.SkinManager;
+import org.geysermc.geyser.text.GeyserLocale;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
 
@@ -44,15 +47,44 @@ public class JavaAddEntityTranslator extends PacketTranslator<ClientboundAddEnti
 
     @Override
     public void translate(GeyserSession session, ClientboundAddEntityPacket packet) {
+        EntityDefinition<?> definition = Registries.ENTITY_DEFINITIONS.get(packet.getType());
+        if (definition == null) {
+            session.getGeyser().getLogger().warning("Could not find an entity definition with type " + packet.getType());
+            return;
+        }
+
         Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
         Vector3f motion = Vector3f.from(packet.getMotionX(), packet.getMotionY(), packet.getMotionZ());
         float yaw = packet.getYaw();
         float pitch = packet.getPitch();
         float headYaw = packet.getHeadYaw();
 
-        EntityDefinition<?> definition = Registries.ENTITY_DEFINITIONS.get(packet.getType());
-        if (definition == null) {
-            session.getGeyser().getLogger().warning("Could not find an entity definition with type " + packet.getType());
+        if (packet.getType() == EntityType.PLAYER) {
+
+            PlayerEntity entity;
+            if (packet.getUuid().equals(session.getPlayerEntity().getUuid())) {
+                // Server is sending a fake version of the current player
+                entity = new PlayerEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
+                        session.getPlayerEntity().getUuid(), position, motion, yaw, pitch, headYaw, session.getPlayerEntity().getUsername(),
+                        session.getPlayerEntity().getTexturesProperty());
+            } else {
+                entity = session.getEntityCache().getPlayerEntity(packet.getUuid());
+                if (entity == null) {
+                    GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.entity.player.failed_list", packet.getUuid()));
+                    return;
+                }
+
+                entity.setEntityId(packet.getEntityId());
+                entity.setPosition(position);
+                entity.setYaw(yaw);
+                entity.setPitch(pitch);
+                entity.setHeadYaw(headYaw);
+                entity.setMotion(motion);
+            }
+            session.getEntityCache().cacheEntity(entity);
+
+            entity.sendPlayer();
+            SkinManager.requestAndHandleSkinAndCape(entity, session, null);
             return;
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddPlayerTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddPlayerTranslator.java
deleted file mode 100644
index 20a5f8213..000000000
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/entity/spawn/JavaAddPlayerTranslator.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.translator.protocol.java.entity.spawn;
-
-import com.github.steveice10.mc.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddPlayerPacket;
-import org.cloudburstmc.math.vector.Vector3f;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.entity.type.player.PlayerEntity;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.skin.SkinManager;
-import org.geysermc.geyser.text.GeyserLocale;
-import org.geysermc.geyser.translator.protocol.PacketTranslator;
-import org.geysermc.geyser.translator.protocol.Translator;
-
-@Translator(packet = ClientboundAddPlayerPacket.class)
-public class JavaAddPlayerTranslator extends PacketTranslator<ClientboundAddPlayerPacket> {
-
-    @Override
-    public void translate(GeyserSession session, ClientboundAddPlayerPacket packet) {
-        Vector3f position = Vector3f.from(packet.getX(), packet.getY(), packet.getZ());
-        float yaw = packet.getYaw();
-        float pitch = packet.getPitch();
-        float headYaw = packet.getYaw();
-
-        PlayerEntity entity;
-        if (packet.getUuid().equals(session.getPlayerEntity().getUuid())) {
-            // Server is sending a fake version of the current player
-            entity = new PlayerEntity(session, packet.getEntityId(), session.getEntityCache().getNextEntityId().incrementAndGet(),
-                    session.getPlayerEntity().getUuid(), position, Vector3f.ZERO, yaw, pitch, headYaw, session.getPlayerEntity().getUsername(),
-                    session.getPlayerEntity().getTexturesProperty());
-        } else {
-            entity = session.getEntityCache().getPlayerEntity(packet.getUuid());
-            if (entity == null) {
-                GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.entity.player.failed_list", packet.getUuid()));
-                return;
-            }
-
-            entity.setEntityId(packet.getEntityId());
-            entity.setPosition(position);
-            entity.setYaw(yaw);
-            entity.setPitch(pitch);
-            entity.setHeadYaw(headYaw);
-        }
-        session.getEntityCache().cacheEntity(entity);
-
-        entity.sendPlayer();
-        SkinManager.requestAndHandleSkinAndCape(entity, session, null);
-    }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java
index 3a18e0b78..8951f7171 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/inventory/JavaOpenScreenTranslator.java
@@ -63,7 +63,7 @@ public class JavaOpenScreenTranslator extends PacketTranslator<ClientboundOpenSc
                 InventoryUtils.closeInventory(session, openInventory.getJavaId(), true);
             }
             ServerboundContainerClosePacket closeWindowPacket = new ServerboundContainerClosePacket(packet.getContainerId());
-            session.sendDownstreamPacket(closeWindowPacket);
+            session.sendDownstreamGamePacket(closeWindowPacket);
             return;
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaChunkBatchFinishedTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaChunkBatchFinishedTranslator.java
new file mode 100644
index 000000000..115222c6c
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaChunkBatchFinishedTranslator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.translator.protocol.java.level;
+
+import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundChunkBatchFinishedPacket;
+import com.github.steveice10.mc.protocol.packet.ingame.serverbound.level.ServerboundChunkBatchReceivedPacket;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.translator.protocol.PacketTranslator;
+import org.geysermc.geyser.translator.protocol.Translator;
+
+@Translator(packet = ClientboundChunkBatchFinishedPacket.class)
+public class JavaChunkBatchFinishedTranslator extends PacketTranslator<ClientboundChunkBatchFinishedPacket> {
+
+    @Override
+    public void translate(GeyserSession session, ClientboundChunkBatchFinishedPacket packet) {
+        // server just sent a batch of LevelChunkWithLightPackets
+        // the vanilla client uses a ChunkBatchSizeCalculator to calculate the desiredChunksPerTick,
+        // but currently we just send an arbitrary value. server clamps the value between 0.01 and 64.
+        session.sendDownstreamGamePacket(new ServerboundChunkBatchReceivedPacket(20));
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaGameEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaGameEventTranslator.java
index 9174df756..fb78983bf 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaGameEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaGameEventTranslator.java
@@ -131,7 +131,7 @@ public class JavaGameEventTranslator extends PacketTranslator<ClientboundGameEve
                 switch ((EnterCreditsValue) packet.getValue()) {
                     case SEEN_BEFORE -> {
                         ServerboundClientCommandPacket javaRespawnPacket = new ServerboundClientCommandPacket(ClientCommand.RESPAWN);
-                        session.sendDownstreamPacket(javaRespawnPacket);
+                        session.sendDownstreamGamePacket(javaRespawnPacket);
                     }
                     case FIRST_TIME -> {
                         ShowCreditsPacket showCreditsPacket = new ShowCreditsPacket();
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java
index d59b40b8f..aecb304db 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaLevelEventTranslator.java
@@ -31,6 +31,7 @@ import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.Clientb
 import org.cloudburstmc.math.vector.Vector3f;
 import org.cloudburstmc.math.vector.Vector3i;
 import org.cloudburstmc.nbt.NbtMap;
+import org.cloudburstmc.protocol.bedrock.data.ParticleType;
 import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
 import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket;
 import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
@@ -103,6 +104,10 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
         effectPacket.setPosition(pos);
         effectPacket.setData(0);
         switch (levelEvent) {
+            case BRUSH_BLOCK_COMPLETE -> {
+                effectPacket.setType(ParticleType.BRUSH_DUST);
+                session.playSoundEvent(SoundEvent.BRUSH_COMPLETED, pos); // todo 1.20.2 verify this
+            }
             case COMPOSTER -> {
                 effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_CROP_GROWTH);
 
@@ -224,6 +229,7 @@ public class JavaLevelEventTranslator extends PacketTranslator<ClientboundLevelE
                 BonemealGrowEventData growEventData = (BonemealGrowEventData) packet.getData();
                 effectPacket.setData(growEventData.getParticleCount());
             }
+            case EGG_CRACK -> effectPacket.setType(ParticleType.VILLAGER_HAPPY); // both the lil green sparkle
             case ENDERDRAGON_FIREBALL_EXPLODE -> {
                 effectPacket.setType(org.cloudburstmc.protocol.bedrock.data.LevelEvent.PARTICLE_EYE_OF_ENDER_DEATH); // TODO
 
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSetTimeTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSetTimeTranslator.java
index f45d4bb97..b86b4247c 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSetTimeTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/level/JavaSetTimeTranslator.java
@@ -40,7 +40,7 @@ public class JavaSetTimeTranslator extends PacketTranslator<ClientboundSetTimePa
         // Java just sends a negative long if there is no daylight cycle
         long time = packet.getTime();
 
-        // https://minecraft.gamepedia.com/Day-night_cycle#24-hour_Minecraft_day
+        // https://minecraft.wiki/w/Day-night_cycle#24-hour_Minecraft_day
         SetTimePacket setTimePacket = new SetTimePacket();
         // We use modulus to prevent an integer overflow
         // 24000 is the range of ticks that a Minecraft day can be; we times by 8 so all moon phases are visible
diff --git a/core/src/main/java/org/geysermc/geyser/util/AttributeUtils.java b/core/src/main/java/org/geysermc/geyser/util/AttributeUtils.java
index 2958de436..17502eae8 100644
--- a/core/src/main/java/org/geysermc/geyser/util/AttributeUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/AttributeUtils.java
@@ -32,7 +32,7 @@ import com.github.steveice10.mc.protocol.data.game.entity.attribute.ModifierOper
 public class AttributeUtils {
     /**
      * Retrieve the base attribute value with all modifiers applied.
-     * https://minecraft.gamepedia.com/Attribute#Modifiers
+     * https://minecraft.wiki/w/Attribute#Modifiers
      * @param attribute The attribute to calculate the total value.
      * @return The finished attribute with all modifiers applied.
      */
diff --git a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
index 4f0eccfcb..23949d020 100644
--- a/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/BlockUtils.java
@@ -64,7 +64,7 @@ public final class BlockUtils {
         if (toolType.equals("shears")) return isShearsEffective ? 5.0 : 15.0;
         if (toolType.equals("")) return 1.0;
         return switch (toolTier) {
-            // https://minecraft.gamepedia.com/Breaking#Speed
+            // https://minecraft.wiki/w/Breaking#Speed
             case "wooden" -> 2.0;
             case "stone" -> 4.0;
             case "iron" -> 6.0;
@@ -100,7 +100,7 @@ public final class BlockUtils {
         return true;
     }
 
-    // https://minecraft.gamepedia.com/Breaking
+    // https://minecraft.wiki/w/Breaking
     private static double calculateBreakTime(double blockHardness, String toolTier, boolean canHarvestWithHand, boolean correctTool, boolean canTierMineBlock,
                                              String toolType, boolean isShearsEffective, int toolEfficiencyLevel, int hasteLevel, int miningFatigueLevel,
                                              boolean insideOfWaterWithoutAquaAffinity, boolean onGround) {
diff --git a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java
index acd2278fc..99e8f8d7d 100644
--- a/core/src/main/java/org/geysermc/geyser/util/FileUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/FileUtils.java
@@ -25,6 +25,8 @@
 
 package org.geysermc.geyser.util;
 
+import com.fasterxml.jackson.annotation.JsonSetter;
+import com.fasterxml.jackson.annotation.Nulls;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import org.geysermc.geyser.GeyserBootstrap;
@@ -53,7 +55,8 @@ public class FileUtils {
      * @throws IOException if the config could not be loaded
      */
     public static <T> T loadConfig(File src, Class<T> valueType) throws IOException {
-        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
+        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory())
+                .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
         return objectMapper.readValue(src, valueType);
     }
 
diff --git a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
index 9141ee7ab..0348eca11 100644
--- a/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/InventoryUtils.java
@@ -260,7 +260,7 @@ public class InventoryUtils {
             // If this is the item we're looking for
             if (geyserItem.getJavaId() == itemStack.getId() && Objects.equals(geyserItem.getNbt(), itemStack.getNbt())) {
                 ServerboundPickItemPacket packetToSend = new ServerboundPickItemPacket(i); // https://wiki.vg/Protocol#Pick_Item
-                session.sendDownstreamPacket(packetToSend);
+                session.sendDownstreamGamePacket(packetToSend);
                 return;
             }
         }
@@ -274,7 +274,7 @@ public class InventoryUtils {
             if ((slot - 36) != inventory.getHeldItemSlot()) {
                 setHotbarItem(session, slot);
             }
-            session.sendDownstreamPacket(actionPacket);
+            session.sendDownstreamGamePacket(actionPacket);
         }
     }
 
@@ -325,7 +325,7 @@ public class InventoryUtils {
             }
 
             ServerboundPickItemPacket packetToSend = new ServerboundPickItemPacket(i); // https://wiki.vg/Protocol#Pick_Item
-            session.sendDownstreamPacket(packetToSend);
+            session.sendDownstreamGamePacket(packetToSend);
             return;
         }
 
@@ -340,7 +340,7 @@ public class InventoryUtils {
                 if ((slot - 36) != inventory.getHeldItemSlot()) {
                     setHotbarItem(session, slot);
                 }
-                session.sendDownstreamPacket(actionPacket);
+                session.sendDownstreamGamePacket(actionPacket);
             } else {
                 session.getGeyser().getLogger().debug("Cannot find item for block " + itemName);
             }
diff --git a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java
index eabfe3a88..478a6ef96 100644
--- a/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/LoginEncryptionUtils.java
@@ -34,14 +34,12 @@ import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket;
 import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult;
 import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult.IdentityData;
 import org.cloudburstmc.protocol.bedrock.util.EncryptionUtils;
-import org.geysermc.cumulus.form.CustomForm;
 import org.geysermc.cumulus.form.ModalForm;
 import org.geysermc.cumulus.form.SimpleForm;
 import org.geysermc.cumulus.response.SimpleFormResponse;
 import org.geysermc.cumulus.response.result.FormResponseResult;
 import org.geysermc.cumulus.response.result.ValidFormResponseResult;
 import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.session.auth.AuthData;
 import org.geysermc.geyser.session.auth.BedrockClientData;
@@ -137,26 +135,16 @@ public class LoginEncryptionUtils {
         // Set DoDaylightCycle to false so the time doesn't accelerate while we're here
         session.setDaylightCycle(false);
 
-        GeyserConfiguration config = session.getGeyser().getConfig();
-        boolean isPasswordAuthEnabled = config.getRemote().isPasswordAuthentication();
-
         session.sendForm(
                 SimpleForm.builder()
                         .translator(GeyserLocale::getPlayerLocaleString, session.locale())
                         .title("geyser.auth.login.form.notice.title")
                         .content("geyser.auth.login.form.notice.desc")
-                        .optionalButton("geyser.auth.login.form.notice.btn_login.mojang", isPasswordAuthEnabled)
                         .button("geyser.auth.login.form.notice.btn_login.microsoft")
                         .button("geyser.auth.login.form.notice.btn_disconnect")
                         .closedOrInvalidResultHandler(() -> buildAndShowLoginWindow(session))
                         .validResultHandler((response) -> {
                             if (response.clickedButtonId() == 0) {
-                                session.setMicrosoftAccount(false);
-                                buildAndShowLoginDetailsWindow(session);
-                                return;
-                            }
-
-                            if (response.clickedButtonId() == 1) {
                                 session.authenticateWithMicrosoftCode();
                                 return;
                             }
@@ -212,19 +200,6 @@ public class LoginEncryptionUtils {
         };
     }
 
-    public static void buildAndShowLoginDetailsWindow(GeyserSession session) {
-        session.sendForm(
-                CustomForm.builder()
-                        .translator(GeyserLocale::getPlayerLocaleString, session.locale())
-                        .title("geyser.auth.login.form.details.title")
-                        .label("geyser.auth.login.form.details.desc")
-                        .input("geyser.auth.login.form.details.email", "account@geysermc.org", "")
-                        .input("geyser.auth.login.form.details.pass", "123456", "")
-                        .invalidResultHandler(() -> buildAndShowLoginDetailsWindow(session))
-                        .closedResultHandler(() -> buildAndShowLoginWindow(session))
-                        .validResultHandler((response) -> session.authenticate(response.next(), response.next())));
-    }
-
     /**
      * Shows the code that a user must input into their browser
      */
diff --git a/core/src/main/java/org/geysermc/geyser/util/PluginMessageUtils.java b/core/src/main/java/org/geysermc/geyser/util/PluginMessageUtils.java
index 032dd2af7..f6b91388f 100644
--- a/core/src/main/java/org/geysermc/geyser/util/PluginMessageUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/PluginMessageUtils.java
@@ -25,7 +25,7 @@
 
 package org.geysermc.geyser.util;
 
-import com.github.steveice10.mc.protocol.packet.ingame.serverbound.ServerboundCustomPayloadPacket;
+import com.github.steveice10.mc.protocol.packet.common.serverbound.ServerboundCustomPayloadPacket;
 import com.google.common.base.Charsets;
 import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.session.GeyserSession;
diff --git a/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkull.json b/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkull.json
index b18d1205b..b3c5533da 100644
--- a/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkull.json
+++ b/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkull.json
@@ -145,10 +145,9 @@
             {
               "origin": [ -0.1, 0.0, -2.0 ],
               "size": [ 4, 12, 4 ],
-              "uv": [ 0, 16 ]
+              "uv": [ 16, 48 ]
             }
-          ],
-          "mirror": true
+          ]
         },
 
         {
@@ -219,4 +218,4 @@
       }
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json b/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json
index 3855c92ec..94559e3f7 100644
--- a/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json
+++ b/core/src/main/resources/bedrock/skin/geometry.humanoid.wearingCustomSkullSlim.json
@@ -145,10 +145,9 @@
             {
               "origin": [ -0.1, 0.0, -2.0 ],
               "size": [ 4, 12, 4 ],
-              "uv": [ 0, 16 ]
+              "uv": [ 16, 48 ]
             }
-          ],
-          "mirror": true
+          ]
         },
 
         {
@@ -219,4 +218,4 @@
       }
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml
index 0034cac05..3f5a2f2e8 100644
--- a/core/src/main/resources/config.yml
+++ b/core/src/main/resources/config.yml
@@ -5,6 +5,10 @@
 #
 # GitHub: https://github.com/GeyserMC/Geyser
 # Discord: https://discord.gg/geysermc
+# Wiki: https://wiki.geysermc.org/
+#
+# NOTICE: See https://wiki.geysermc.org/geyser/setup/ for the setup guide. Many video tutorials are outdated.
+# In most cases, especially with server hosting providers, further hosting-specific configuration is required.
 # --------------------------------
 
 bedrock:
diff --git a/core/src/main/resources/mappings b/core/src/main/resources/mappings
index 587220aaf..31ce17e12 160000
--- a/core/src/main/resources/mappings
+++ b/core/src/main/resources/mappings
@@ -1 +1 @@
-Subproject commit 587220aafb55e80f2a70d6eac2d4b89dc0a005bd
+Subproject commit 31ce17e12e991bd841270b99f461641093f42564
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e79236297..3e4c20940 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,7 +15,7 @@ protocol-connection = "3.0.0.Beta1-20230908.171156-105"
 raknet = "1.0.0.CR1-20230703.195238-9"
 blockstateupdater="1.20.30-20230918.203831-4"
 mcauthlib = "d9d773e"
-mcprotocollib = "1.20-2-20230827.192136-1"
+mcprotocollib = "1.20.2-1-20231003.141424-6"
 adventure = "4.14.0"
 adventure-platform = "4.3.0"
 junit = "5.9.2"
@@ -25,13 +25,13 @@ jline = "3.21.0"
 terminalconsoleappender = "1.2.0"
 folia = "1.19.4-R0.1-SNAPSHOT"
 viaversion = "4.0.0"
-adapters = "1.9-SNAPSHOT"
+adapters = "1.10-SNAPSHOT"
 commodore = "2.2"
 bungeecord = "master-SNAPSHOT"
 velocity = "3.1.1"
-fabric-minecraft = "1.20"
+fabric-minecraft = "1.20.2"
 fabric-loader = "0.14.21"
-fabric-api = "0.83.0+1.20"
+fabric-api = "0.89.0+1.20.2"
 
 [libraries]
 base-api = { group = "org.geysermc.api", name = "base-api", version.ref = "base-api" }