From 897c4dcfecda8a416028c8eda2a1f8997ef93a49 Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Sat, 9 Jul 2022 18:39:02 -0400
Subject: [PATCH 1/2] Changes

---
 .../org/geysermc/geyser/api/GeyserApi.java    | 38 ++-------
 .../geysermc/geyser/api/command/Command.java  |  2 +-
 .../downstream/ServerDefineCommandsEvent.java |  6 +-
 .../api/item/custom/CustomItemData.java       |  2 +-
 .../api/item/custom/CustomItemOptions.java    |  2 +-
 .../item/custom/NonVanillaCustomItemData.java |  2 +-
 .../geysermc/geyser/api/network/AuthType.java | 18 ++--
 .../geyser/api/network/BedrockListener.java   |  4 +-
 .../geyser/api/provider/BuilderProvider.java  | 47 -----------
 .../geyser/api/provider/Provider.java         | 29 -------
 .../geyser/api/provider/ProviderManager.java  | 41 ---------
 .../standalone/GeyserStandaloneBootstrap.java |  2 +-
 .../geysermc/geyser/FloodgateKeyLoader.java   |  2 +-
 .../java/org/geysermc/geyser/GeyserImpl.java  | 84 ++++---------------
 .../configuration/GeyserConfiguration.java    | 32 +++----
 .../GeyserJacksonConfiguration.java           | 49 ++++++++++-
 .../geyser/network/BedrockListenerImpl.java   | 31 -------
 .../network/ConnectorServerEventHandler.java  |  6 +-
 .../geyser/network/QueryPacketHandler.java    |  6 +-
 .../geyser/network/RemoteServerImpl.java      | 32 -------
 .../geyser/network/UpstreamPacketHandler.java |  4 +-
 .../ping/GeyserLegacyPingPassthrough.java     |  4 +-
 .../geyser/registry/ProviderRegistries.java   | 42 ----------
 .../geysermc/geyser/registry/Registries.java  | 13 +--
 .../loader/ProviderRegistryLoader.java        | 23 +++--
 .../registry/provider/AbstractProvider.java   | 39 ---------
 .../provider/GeyserBuilderProvider.java       | 70 ----------------
 .../provider/GeyserProviderManager.java       | 36 --------
 .../geyser/session/GeyserSession.java         |  4 +-
 .../org/geysermc/geyser/skin/SkinManager.java |  2 +-
 30 files changed, 144 insertions(+), 528 deletions(-)
 delete mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/provider/BuilderProvider.java
 delete mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/provider/Provider.java
 delete mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/provider/ProviderManager.java
 delete mode 100644 core/src/main/java/org/geysermc/geyser/network/BedrockListenerImpl.java
 delete mode 100644 core/src/main/java/org/geysermc/geyser/network/RemoteServerImpl.java
 delete mode 100644 core/src/main/java/org/geysermc/geyser/registry/ProviderRegistries.java
 delete mode 100644 core/src/main/java/org/geysermc/geyser/registry/provider/AbstractProvider.java
 delete mode 100644 core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java
 delete mode 100644 core/src/main/java/org/geysermc/geyser/registry/provider/GeyserProviderManager.java

diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java b/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java
index d6cb3c25a..16bfe7070 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/GeyserApi.java
@@ -35,7 +35,6 @@ import org.geysermc.geyser.api.event.EventBus;
 import org.geysermc.geyser.api.extension.ExtensionManager;
 import org.geysermc.geyser.api.network.BedrockListener;
 import org.geysermc.geyser.api.network.RemoteServer;
-import org.geysermc.geyser.api.provider.ProviderManager;
 
 import java.util.List;
 import java.util.UUID;
@@ -44,24 +43,6 @@ import java.util.UUID;
  * Represents the API used in Geyser.
  */
 public interface GeyserApi extends GeyserApiBase {
-    /**
-     * Shuts down the current Geyser instance.
-     */
-    void shutdown();
-
-    /**
-     * Reloads the current Geyser instance.
-     */
-    void reload();
-
-    /**
-     * Gets if this Geyser instance is running in an IDE. This only needs to be used in cases where files
-     * expected to be in a jarfile are not present.
-     *
-     * @return if we are in a production environment
-     */
-    boolean isProductionEnvironment();
-
     /**
      * {@inheritDoc}
      */
@@ -101,11 +82,15 @@ public interface GeyserApi extends GeyserApiBase {
     CommandManager commandManager();
 
     /**
-     * Gets the {@link ProviderManager}.
+     * Provides an implementation for the specified API type.
      *
-     * @return the provider manager
+     * @param apiClass the builder class
+     * @param <R> the implementation type
+     * @param <T> the API type
+     * @return the builder instance
      */
-    ProviderManager providerManager();
+    @NonNull
+    <R extends T, T> R provider(@NonNull Class<T> apiClass, @Nullable Object... args);
 
     /**
      * Gets the {@link EventBus} for handling
@@ -131,15 +116,6 @@ public interface GeyserApi extends GeyserApiBase {
      */
     BedrockListener bedrockListener();
 
-    /**
-     * Gets the maximum number of players that
-     * can join this Geyser instance.
-     *
-     * @return the maximum number of players that
-     *         can join this Geyser instance
-     */
-    int maxPlayers();
-
     /**
      * Gets the current {@link GeyserApiBase} instance.
      *
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/command/Command.java b/api/geyser/src/main/java/org/geysermc/geyser/api/command/Command.java
index f9ae68ab3..0ad296669 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/command/Command.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/command/Command.java
@@ -105,7 +105,7 @@ public interface Command {
     }
 
     static <T extends CommandSource> Command.Builder<T> builder(Class<T> sourceType) {
-        return GeyserApi.api().providerManager().builderProvider().provideBuilder(Builder.class, sourceType);
+        return GeyserApi.api().provider(Builder.class, sourceType);
     }
 
     interface Builder<T extends CommandSource> {
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java
index ba7254c94..06412eb4c 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java
@@ -45,9 +45,10 @@ public class ServerDefineCommandsEvent extends ConnectionEvent implements Cancel
     }
 
     /**
-     * A mutable collection of the commands sent over.
+     * A collection of commands sent from the server. Any element in this collection can be removed, but no element can
+     * be added.
      *
-     * @return a mutable collection of the commands sent over
+     * @return a collection of the commands sent over
      */
     @NonNull
     public Set<? extends CommandInfo> commands() {
@@ -65,7 +66,6 @@ public class ServerDefineCommandsEvent extends ConnectionEvent implements Cancel
     }
 
     public interface CommandInfo {
-
         /**
          * Gets the name of the command.
          *
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java
index 7391c0680..17763fb77 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java
@@ -83,7 +83,7 @@ public interface CustomItemData {
     @Nullable CustomRenderOffsets renderOffsets();
 
     static CustomItemData.Builder builder() {
-        return GeyserApi.api().providerManager().builderProvider().provideBuilder(CustomItemData.Builder.class);
+        return GeyserApi.api().provider(CustomItemData.Builder.class);
     }
 
     interface Builder {
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java
index 037f2f05e..ec26a6e37 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemOptions.java
@@ -68,7 +68,7 @@ public interface CustomItemOptions {
     }
 
     static CustomItemOptions.Builder builder() {
-        return GeyserApi.api().providerManager().builderProvider().provideBuilder(CustomItemOptions.Builder.class);
+        return GeyserApi.api().provider(CustomItemOptions.Builder.class);
     }
 
     interface Builder {
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java
index 1df94f7ea..d2cef637a 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java
@@ -137,7 +137,7 @@ public interface NonVanillaCustomItemData extends CustomItemData {
     boolean isTool();
 
     static NonVanillaCustomItemData.Builder builder() {
-        return GeyserApi.api().providerManager().builderProvider().provideBuilder(NonVanillaCustomItemData.Builder.class);
+        return GeyserApi.api().provider(NonVanillaCustomItemData.Builder.class);
     }
 
     interface Builder extends CustomItemData.Builder {
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/network/AuthType.java b/api/geyser/src/main/java/org/geysermc/geyser/api/network/AuthType.java
index 5e1c2539d..3176f3384 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/network/AuthType.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/network/AuthType.java
@@ -25,16 +25,22 @@
 
 package org.geysermc.geyser.api.network;
 
+import java.util.Locale;
+
+/**
+ * The authentication types that a Java server can be on connection.
+ */
 public enum AuthType {
     OFFLINE,
     ONLINE,
+    /**
+     * The internal name for connecting to an online mode server without needing a Java account. The presence of this
+     * authentication type does not necessarily mean the Floodgate plugin is installed; it only means that this
+     * authentication type will be attempted.
+     */
     FLOODGATE;
 
-    public static final AuthType[] VALUES = values();
-
-    public static AuthType getById(int id) {
-        return id < VALUES.length ? VALUES[id] : OFFLINE;
-    }
+    private static final AuthType[] VALUES = values();
 
     /**
      * Convert the AuthType string (from config) to the enum, ONLINE on fail
@@ -44,7 +50,7 @@ public enum AuthType {
      * @return The converted AuthType
      */
     public static AuthType getByName(String name) {
-        String upperCase = name.toUpperCase();
+        String upperCase = name.toUpperCase(Locale.ROOT);
         for (AuthType type : VALUES) {
             if (type.name().equals(upperCase)) {
                 return type;
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java b/api/geyser/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java
index 648f83e47..58a597eb6 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java
@@ -48,7 +48,7 @@ public interface BedrockListener {
     int port();
 
     /**
-     * Gets the primary MOTD shown to Bedrock players.
+     * Gets the primary MOTD shown to Bedrock players if a ping passthrough setting is not enabled.
      * <p>
      * This is the first line that will be displayed.
      *
@@ -57,7 +57,7 @@ public interface BedrockListener {
     String primaryMotd();
 
     /**
-     * Gets the secondary MOTD shown to Bedrock players.
+     * Gets the secondary MOTD shown to Bedrock players if a ping passthrough setting is not enabled.
      * <p>
      * This is the second line that will be displayed.
      *
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/provider/BuilderProvider.java b/api/geyser/src/main/java/org/geysermc/geyser/api/provider/BuilderProvider.java
deleted file mode 100644
index b05a36f88..000000000
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/provider/BuilderProvider.java
+++ /dev/null
@@ -1,47 +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.api.provider;
-
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-/**
- * Allows for obtaining instances of a builder that are
- * used for constructing various data.
- */
-public interface BuilderProvider extends Provider {
-
-    /**
-     * Provides a builder for the specified builder type.
-     *
-     * @param builderClass the builder class
-     * @param <B> the resulting type
-     * @param <T> the builder type
-     * @return the builder instance
-     */
-    @NonNull
-    <B extends T, T> B provideBuilder(@NonNull Class<T> builderClass, @Nullable Object... args);
-}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/provider/Provider.java b/api/geyser/src/main/java/org/geysermc/geyser/api/provider/Provider.java
deleted file mode 100644
index 4463efeed..000000000
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/provider/Provider.java
+++ /dev/null
@@ -1,29 +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.api.provider;
-
-public interface Provider {
-}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/provider/ProviderManager.java b/api/geyser/src/main/java/org/geysermc/geyser/api/provider/ProviderManager.java
deleted file mode 100644
index 48ec99a3f..000000000
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/provider/ProviderManager.java
+++ /dev/null
@@ -1,41 +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.api.provider;
-
-/**
- * Holds a record of every {@link Provider} available
- * that allows for accessing various information throughout
- * the API.
- */
-public interface ProviderManager {
-
-    /**
-     * Returns the {@link BuilderProvider}.
-     *
-     * @return the builder provider
-     */
-    BuilderProvider builderProvider();
-}
diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java
index a89b18d1e..44194d75c 100644
--- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java
+++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java
@@ -197,7 +197,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
 
             handleArgsConfigOptions();
 
-            if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) {
+            if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) {
                 geyserConfig.setAutoconfiguredRemote(true); // Doesn't really need to be set but /shrug
                 geyserConfig.getRemote().setAddress("127.0.0.1");
             }
diff --git a/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java b/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java
index aaf45ce35..8b51228c8 100644
--- a/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/FloodgateKeyLoader.java
@@ -34,7 +34,7 @@ import java.nio.file.Path;
 
 public class FloodgateKeyLoader {
     public static Path getKeyPath(GeyserJacksonConfiguration config, Path floodgateDataFolder, Path geyserDataFolder, GeyserLogger logger) {
-        if (config.getRemote().getAuthType() != AuthType.FLOODGATE) {
+        if (config.getRemote().authType() != AuthType.FLOODGATE) {
             return geyserDataFolder.resolve(config.getFloodgateKeyFile());
         }
 
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index 4c3cf6fca..146cb985f 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -64,14 +64,10 @@ import org.geysermc.geyser.entity.EntityDefinitions;
 import org.geysermc.geyser.event.GeyserEventBus;
 import org.geysermc.geyser.extension.GeyserExtensionManager;
 import org.geysermc.geyser.level.WorldManager;
-import org.geysermc.geyser.network.BedrockListenerImpl;
 import org.geysermc.geyser.network.ConnectorServerEventHandler;
-import org.geysermc.geyser.network.GameProtocol;
-import org.geysermc.geyser.network.RemoteServerImpl;
 import org.geysermc.geyser.pack.ResourcePack;
 import org.geysermc.geyser.registry.BlockRegistries;
 import org.geysermc.geyser.registry.Registries;
-import org.geysermc.geyser.registry.provider.GeyserProviderManager;
 import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
@@ -90,7 +86,6 @@ import javax.naming.directory.InitialDirContext;
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
@@ -148,10 +143,6 @@ public class GeyserImpl implements GeyserApi {
 
     private final EventBus eventBus;
     private final GeyserExtensionManager extensionManager;
-    private final GeyserProviderManager providerManager = new GeyserProviderManager();
-
-    private final RemoteServer remoteServer;
-    private final BedrockListener bedrockListener;
 
     private Metrics metrics;
 
@@ -215,22 +206,6 @@ public class GeyserImpl implements GeyserApi {
             }
         }
 
-        this.remoteServer = new RemoteServerImpl(
-                config.getRemote().getAddress(),
-                config.getRemote().getPort(),
-                GameProtocol.getJavaProtocolVersion(),
-                GameProtocol.getJavaMinecraftVersion(),
-                config.getRemote().getAuthType()
-        );
-
-        this.bedrockListener = new BedrockListenerImpl(
-                config.getBedrock().getAddress(),
-                config.getBedrock().getPort(),
-                config.getBedrock().getMotd1(),
-                config.getBedrock().getMotd2(),
-                config.getBedrock().getServerName()
-        );
-
         double completeTime = (System.currentTimeMillis() - startupTime) / 1000D;
         String message = GeyserLocale.getLocaleStringLog("geyser.core.finish.done", new DecimalFormat("#.###").format(completeTime)) + " ";
         if (isGui) {
@@ -243,7 +218,7 @@ public class GeyserImpl implements GeyserApi {
 
         if (platformType == PlatformType.STANDALONE) {
             logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn"));
-        } else if (config.getRemote().getAuthType() == AuthType.FLOODGATE) {
+        } else if (config.getRemote().authType() == AuthType.FLOODGATE) {
             VersionCheckUtils.checkForOutdatedFloodgate(logger);
         }
     }
@@ -260,7 +235,7 @@ public class GeyserImpl implements GeyserApi {
 
         ResourcePack.loadPacks();
 
-        if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) {
+        if (platformType != PlatformType.STANDALONE && config.getRemote().address().equals("auto")) {
             // Set the remote address to localhost since that is where we are always connecting
             try {
                 config.getRemote().setAddress(InetAddress.getLocalHost().getHostAddress());
@@ -272,7 +247,7 @@ public class GeyserImpl implements GeyserApi {
                 config.getRemote().setAddress(InetAddress.getLoopbackAddress().getHostAddress());
             }
         }
-        String remoteAddress = config.getRemote().getAddress();
+        String remoteAddress = config.getRemote().address();
         // Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry.
         if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) {
             int remotePort;
@@ -298,24 +273,6 @@ public class GeyserImpl implements GeyserApi {
         // Ensure that PacketLib does not create an event loop for handling packets; we'll do that ourselves
         TcpSession.USE_EVENT_LOOP_FOR_PACKETS = false;
 
-        String branch = "unknown";
-        int buildNumber = -1;
-        if (this.isProductionEnvironment()) {
-            try (InputStream stream = bootstrap.getResource("git.properties")) {
-                Properties gitProperties = new Properties();
-                gitProperties.load(stream);
-                branch = gitProperties.getProperty("git.branch");
-                String build = gitProperties.getProperty("git.build.number");
-                if (build != null) {
-                    buildNumber = Integer.parseInt(build);
-                }
-            } catch (Throwable e) {
-                logger.error("Failed to read git.properties", e);
-            }
-        } else {
-            logger.debug("Not getting git properties for the news handler as we are in a development environment.");
-        }
-
         pendingMicrosoftAuthentication = new PendingMicrosoftAuthentication(config.getPendingAuthenticationTimeout());
 
         this.newsHandler = new NewsHandler(BRANCH, this.buildNumber());
@@ -335,7 +292,7 @@ public class GeyserImpl implements GeyserApi {
 
         boolean enableProxyProtocol = config.getBedrock().isEnableProxyProtocol();
         bedrockServer = new BedrockServer(
-                new InetSocketAddress(config.getBedrock().getAddress(), config.getBedrock().getPort()),
+                new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port()),
                 bedrockThreadCount,
                 EventLoops.commonGroup(),
                 enableProxyProtocol
@@ -358,11 +315,11 @@ public class GeyserImpl implements GeyserApi {
         if (shouldStartListener) {
             bedrockServer.bind().whenComplete((avoid, throwable) -> {
                 if (throwable == null) {
-                    logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().getAddress(),
-                            String.valueOf(config.getBedrock().getPort())));
+                    logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().address(),
+                            String.valueOf(config.getBedrock().port())));
                 } else {
-                    String address = config.getBedrock().getAddress();
-                    int port = config.getBedrock().getPort();
+                    String address = config.getBedrock().address();
+                    int port = config.getBedrock().port();
                     logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port)));
                     if (!"0.0.0.0".equals(address)) {
                         logger.info(ChatColor.GREEN + "Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0");
@@ -372,7 +329,7 @@ public class GeyserImpl implements GeyserApi {
             }).join();
         }
 
-        if (config.getRemote().getAuthType() == AuthType.FLOODGATE) {
+        if (config.getRemote().authType() == AuthType.FLOODGATE) {
             try {
                 Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
                 cipher = new AesCipher(new Base64Topping());
@@ -390,7 +347,7 @@ public class GeyserImpl implements GeyserApi {
             metrics = new Metrics(this, "GeyserMC", config.getMetrics().getUniqueId(), false, java.util.logging.Logger.getLogger(""));
             metrics.addCustomChart(new Metrics.SingleLineChart("players", sessionManager::size));
             // Prevent unwanted words best we can
-            metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().getAuthType().toString().toLowerCase(Locale.ROOT)));
+            metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().authType().toString().toLowerCase(Locale.ROOT)));
             metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::getPlatformName));
             metrics.addCustomChart(new Metrics.SimplePie("defaultLocale", GeyserLocale::getDefaultLocale));
             metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserImpl.VERSION));
@@ -474,7 +431,7 @@ public class GeyserImpl implements GeyserApi {
             metrics = null;
         }
 
-        if (config.getRemote().getAuthType() == AuthType.ONLINE) {
+        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: " +
@@ -552,7 +509,6 @@ public class GeyserImpl implements GeyserApi {
         return null;
     }
 
-    @Override
     public void shutdown() {
         bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown"));
         shuttingDown = true;
@@ -579,7 +535,6 @@ public class GeyserImpl implements GeyserApi {
         bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done"));
     }
 
-    @Override
     public void reload() {
         shutdown();
         this.extensionManager.enableExtensions();
@@ -592,10 +547,10 @@ public class GeyserImpl implements GeyserApi {
      *
      * @return true if the version number is not 'DEV'.
      */
-    @Override
     public boolean isProductionEnvironment() {
+        // First is if Blossom runs, second is if Blossom doesn't run
         // noinspection ConstantConditions - changes in production
-        return !"git-local/dev-0000000".equals(GeyserImpl.GIT_VERSION);
+        return !("git-local/dev-0000000".equals(GeyserImpl.GIT_VERSION) || "${gitVersion}".equals(GeyserImpl.GIT_VERSION));
     }
 
     @Override
@@ -609,8 +564,8 @@ public class GeyserImpl implements GeyserApi {
     }
 
     @Override
-    public GeyserProviderManager providerManager() {
-        return this.providerManager;
+    public <R extends T, T> @NonNull R provider(@NonNull Class<T> apiClass, @Nullable Object... args) {
+        return (R) Registries.PROVIDERS.get(apiClass).create(args);
     }
 
     @Override
@@ -619,17 +574,12 @@ public class GeyserImpl implements GeyserApi {
     }
 
     public RemoteServer defaultRemoteServer() {
-        return this.remoteServer;
+        return getConfig().getRemote();
     }
 
     @Override
     public BedrockListener bedrockListener() {
-        return this.bedrockListener;
-    }
-
-    @Override
-    public int maxPlayers() {
-        return this.getConfig().getMaxPlayers();
+        return getConfig().getBedrock();
     }
 
     public int buildNumber() {
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 d74d804df..7439b22f2 100644
--- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java
+++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserConfiguration.java
@@ -27,8 +27,10 @@ package org.geysermc.geyser.configuration;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import org.geysermc.geyser.GeyserLogger;
-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.network.CIDRMatcher;
+import org.geysermc.geyser.network.GameProtocol;
 import org.geysermc.geyser.text.GeyserLocale;
 
 import java.nio.file.Path;
@@ -109,20 +111,10 @@ public interface GeyserConfiguration {
 
     int getPendingAuthenticationTimeout();
 
-    interface IBedrockConfiguration {
-
-        String getAddress();
-
-        int getPort();
+    interface IBedrockConfiguration extends BedrockListener {
 
         boolean isCloneRemotePort();
 
-        String getMotd1();
-
-        String getMotd2();
-
-        String getServerName();
-
         int getCompressionLevel();
 
         boolean isEnableProxyProtocol();
@@ -135,23 +127,25 @@ public interface GeyserConfiguration {
         List<CIDRMatcher> getWhitelistedIPsMatchers();
     }
 
-    interface IRemoteConfiguration {
-
-        String getAddress();
-
-        int getPort();
+    interface IRemoteConfiguration extends RemoteServer {
 
         void setAddress(String address);
 
         void setPort(int port);
 
-        AuthType getAuthType();
-
         boolean isPasswordAuthentication();
 
         boolean isUseProxyProtocol();
 
         boolean isForwardHost();
+
+        default String minecraftVersion() {
+            return GameProtocol.getJavaMinecraftVersion();
+        }
+
+        default int protocolVersion() {
+            return GameProtocol.getJavaProtocolVersion();
+        }
     }
 
     interface IUserAuthenticationInfo {
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 77b351518..b80e60e49 100644
--- a/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java
+++ b/core/src/main/java/org/geysermc/geyser/configuration/GeyserJacksonConfiguration.java
@@ -153,24 +153,50 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
     @JsonProperty("pending-authentication-timeout")
     private int pendingAuthenticationTimeout = 120;
 
-    @Getter
     @JsonIgnoreProperties(ignoreUnknown = true)
     public static class BedrockConfiguration implements IBedrockConfiguration {
         @AsteriskSerializer.Asterisk(isIp = true)
         private String address = "0.0.0.0";
 
+        @Override
+        public String address() {
+            return address;
+        }
+
         @Setter
         private int port = 19132;
 
+        @Override
+        public int port() {
+            return port;
+        }
+
+        @Getter
         @JsonProperty("clone-remote-port")
         private boolean cloneRemotePort = false;
 
         private String motd1 = "GeyserMC";
+
+        @Override
+        public String primaryMotd() {
+            return motd1;
+        }
+
         private String motd2 = "Geyser";
 
+        @Override
+        public String secondaryMotd() {
+            return motd2;
+        }
+
         @JsonProperty("server-name")
         private String serverName = GeyserImpl.NAME;
 
+        @Override
+        public String serverName() {
+            return serverName;
+        }
+
         @JsonProperty("compression-level")
         private int compressionLevel = 6;
 
@@ -178,9 +204,11 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
             return Math.max(-1, Math.min(compressionLevel, 9));
         }
 
+        @Getter
         @JsonProperty("enable-proxy-protocol")
         private boolean enableProxyProtocol = false;
 
+        @Getter
         @JsonProperty("proxy-protocol-whitelisted-ips")
         private List<String> proxyProtocolWhitelistedIPs = Collections.emptyList();
 
@@ -202,28 +230,45 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
         }
     }
 
-    @Getter
     @JsonIgnoreProperties(ignoreUnknown = true)
     public static class RemoteConfiguration implements IRemoteConfiguration {
         @Setter
         @AsteriskSerializer.Asterisk(isIp = true)
         private String address = "auto";
 
+        @Override
+        public String address() {
+            return address;
+        }
+
         @JsonDeserialize(using = PortDeserializer.class)
         @Setter
         private int port = 25565;
 
+        @Override
+        public int port() {
+            return port;
+        }
+
         @Setter
         @JsonDeserialize(using = AuthTypeDeserializer.class)
         @JsonProperty("auth-type")
         private AuthType authType = AuthType.ONLINE;
 
+        @Override
+        public AuthType authType() {
+            return authType;
+        }
+
+        @Getter
         @JsonProperty("allow-password-authentication")
         private boolean passwordAuthentication = true;
 
+        @Getter
         @JsonProperty("use-proxy-protocol")
         private boolean useProxyProtocol = false;
 
+        @Getter
         @JsonProperty("forward-hostname")
         private boolean forwardHost = false;
     }
diff --git a/core/src/main/java/org/geysermc/geyser/network/BedrockListenerImpl.java b/core/src/main/java/org/geysermc/geyser/network/BedrockListenerImpl.java
deleted file mode 100644
index e617be36a..000000000
--- a/core/src/main/java/org/geysermc/geyser/network/BedrockListenerImpl.java
+++ /dev/null
@@ -1,31 +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.network;
-
-import org.geysermc.geyser.api.network.BedrockListener;
-
-public record BedrockListenerImpl(String address, int port, String primaryMotd, String secondaryMotd, String serverName) implements BedrockListener {
-}
diff --git a/core/src/main/java/org/geysermc/geyser/network/ConnectorServerEventHandler.java b/core/src/main/java/org/geysermc/geyser/network/ConnectorServerEventHandler.java
index e231cd08a..3bfbf118d 100644
--- a/core/src/main/java/org/geysermc/geyser/network/ConnectorServerEventHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/ConnectorServerEventHandler.java
@@ -108,7 +108,7 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
         pong.setNintendoLimited(false);
         pong.setProtocolVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion());
         pong.setVersion(GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()); // Required to not be empty as of 1.16.210.59. Can only contain . and numbers.
-        pong.setIpv4Port(config.getBedrock().getPort());
+        pong.setIpv4Port(config.getBedrock().port());
 
         if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
             String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
@@ -118,8 +118,8 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
             pong.setMotd(mainMotd.trim());
             pong.setSubMotd(subMotd.trim()); // Trimmed to shift it to the left, prevents the universe from collapsing on us just because we went 2 characters over the text box's limit.
         } else {
-            pong.setMotd(config.getBedrock().getMotd1());
-            pong.setSubMotd(config.getBedrock().getMotd2());
+            pong.setMotd(config.getBedrock().primaryMotd());
+            pong.setSubMotd(config.getBedrock().secondaryMotd());
         }
 
         if (config.isPassthroughPlayerCounts() && pingInfo != null) {
diff --git a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java
index f11851c1b..4a78d0bdb 100644
--- a/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/QueryPacketHandler.java
@@ -151,7 +151,7 @@ public class QueryPacketHandler {
             String[] javaMotd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
             motd = javaMotd[0].trim(); // First line of the motd.
         } else {
-            motd = geyser.getConfig().getBedrock().getMotd1();
+            motd = geyser.getConfig().getBedrock().primaryMotd();
         }
 
         // If passthrough player counts is enabled lets get players from the server
@@ -180,8 +180,8 @@ public class QueryPacketHandler {
         gameData.put("map", map);
         gameData.put("numplayers", currentPlayerCount);
         gameData.put("maxplayers", maxPlayerCount);
-        gameData.put("hostport", String.valueOf(geyser.getConfig().getBedrock().getPort()));
-        gameData.put("hostip", geyser.getConfig().getBedrock().getAddress());
+        gameData.put("hostport", String.valueOf(geyser.getConfig().getBedrock().port()));
+        gameData.put("hostip", geyser.getConfig().getBedrock().address());
 
         try {
             writeString(query, "GeyserMC");
diff --git a/core/src/main/java/org/geysermc/geyser/network/RemoteServerImpl.java b/core/src/main/java/org/geysermc/geyser/network/RemoteServerImpl.java
deleted file mode 100644
index a0d919c3a..000000000
--- a/core/src/main/java/org/geysermc/geyser/network/RemoteServerImpl.java
+++ /dev/null
@@ -1,32 +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.network;
-
-import org.geysermc.geyser.api.network.AuthType;
-import org.geysermc.geyser.api.network.RemoteServer;
-
-public record RemoteServerImpl(String address, int port, int protocolVersion, String minecraftVersion, AuthType authType) implements RemoteServer {
-}
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 33eca67a9..5fc254363 100644
--- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
@@ -31,7 +31,6 @@ import com.nukkitx.protocol.bedrock.data.ExperimentData;
 import com.nukkitx.protocol.bedrock.data.ResourcePackType;
 import com.nukkitx.protocol.bedrock.packet.*;
 import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
 import org.geysermc.geyser.api.network.AuthType;
 import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.pack.ResourcePack;
@@ -39,6 +38,7 @@ import org.geysermc.geyser.pack.ResourcePackManifest;
 import org.geysermc.geyser.registry.BlockRegistries;
 import org.geysermc.geyser.registry.Registries;
 import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
 import org.geysermc.geyser.text.GeyserLocale;
 import org.geysermc.geyser.util.LoginEncryptionUtils;
 import org.geysermc.geyser.util.MathUtils;
@@ -119,7 +119,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
     public boolean handle(ResourcePackClientResponsePacket packet) {
         switch (packet.getStatus()) {
             case COMPLETED:
-                if (geyser.getConfig().getRemote().getAuthType() != AuthType.ONLINE) {
+                if (geyser.getConfig().getRemote().authType() != AuthType.ONLINE) {
                     session.authenticate(session.getAuthData().name());
                 } else if (!couldLoginUserByName(session.getAuthData().name())) {
                     // We must spawn the white world
diff --git a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java
index c3a242501..74501c8ae 100644
--- a/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java
+++ b/core/src/main/java/org/geysermc/geyser/ping/GeyserLegacyPingPassthrough.java
@@ -76,8 +76,8 @@ public class GeyserLegacyPingPassthrough implements IGeyserPingPassthrough, Runn
     public void run() {
         try {
             Socket socket = new Socket();
-            String address = geyser.getConfig().getRemote().getAddress();
-            int port = geyser.getConfig().getRemote().getPort();
+            String address = geyser.getConfig().getRemote().address();
+            int port = geyser.getConfig().getRemote().port();
             socket.connect(new InetSocketAddress(address, port), 5000);
 
             ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream();
diff --git a/core/src/main/java/org/geysermc/geyser/registry/ProviderRegistries.java b/core/src/main/java/org/geysermc/geyser/registry/ProviderRegistries.java
deleted file mode 100644
index 46b44c8da..000000000
--- a/core/src/main/java/org/geysermc/geyser/registry/ProviderRegistries.java
+++ /dev/null
@@ -1,42 +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.registry;
-
-import org.geysermc.geyser.api.provider.Provider;
-import org.geysermc.geyser.registry.loader.ProviderRegistryLoader;
-import org.geysermc.geyser.registry.provider.GeyserBuilderProvider;
-import org.geysermc.geyser.registry.provider.ProviderSupplier;
-
-/**
- * Holds registries for the available {@link Provider}s
- */
-public class ProviderRegistries {
-
-    /**
-     * A registry containing all the providers for builders.
-     */
-    public static final SimpleMappedRegistry<Class<?>, ProviderSupplier> BUILDERS = SimpleMappedRegistry.create(GeyserBuilderProvider.INSTANCE, ProviderRegistryLoader::new);
-}
diff --git a/core/src/main/java/org/geysermc/geyser/registry/Registries.java b/core/src/main/java/org/geysermc/geyser/registry/Registries.java
index f1dd054f5..4b361ba4f 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/Registries.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/Registries.java
@@ -40,8 +40,6 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
-import net.kyori.adventure.key.Key;
-import org.geysermc.geyser.api.extension.ExtensionLoader;
 import org.geysermc.geyser.entity.EntityDefinition;
 import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment;
 import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
@@ -49,6 +47,7 @@ import org.geysermc.geyser.registry.loader.*;
 import org.geysermc.geyser.registry.populator.ItemRegistryPopulator;
 import org.geysermc.geyser.registry.populator.PacketRegistryPopulator;
 import org.geysermc.geyser.registry.populator.RecipeRegistryPopulator;
+import org.geysermc.geyser.registry.provider.ProviderSupplier;
 import org.geysermc.geyser.registry.type.EnchantmentData;
 import org.geysermc.geyser.registry.type.ItemMappings;
 import org.geysermc.geyser.registry.type.ParticleMapping;
@@ -59,10 +58,7 @@ import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
 import org.geysermc.geyser.translator.sound.SoundInteractionTranslator;
 import org.geysermc.geyser.translator.sound.SoundTranslator;
 
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 /**
  * Holds all the common registries in Geyser.
@@ -140,6 +136,11 @@ public final class Registries {
      */
     public static final SimpleRegistry<Set<PotionMixData>> POTION_MIXES;
 
+    /**
+     * A registry holding all the
+     */
+    public static final SimpleMappedRegistry<Class<?>, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new);
+
     /**
      * A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value.
      */
diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
index 290f2991b..449f6574e 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
@@ -25,21 +25,32 @@
 
 package org.geysermc.geyser.registry.loader;
 
-import org.geysermc.geyser.registry.provider.AbstractProvider;
+import org.geysermc.geyser.api.command.Command;
+import org.geysermc.geyser.api.command.CommandSource;
+import org.geysermc.geyser.api.item.custom.CustomItemData;
+import org.geysermc.geyser.api.item.custom.CustomItemOptions;
+import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData;
+import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.item.GeyserCustomItemData;
+import org.geysermc.geyser.item.GeyserCustomItemOptions;
+import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData;
 import org.geysermc.geyser.registry.provider.ProviderSupplier;
 
-import java.util.IdentityHashMap;
 import java.util.Map;
 
 /**
  * Registers the provider data from the provider.
  */
-public class ProviderRegistryLoader implements RegistryLoader<AbstractProvider, Map<Class<?>, ProviderSupplier>> {
+public class ProviderRegistryLoader implements RegistryLoader<Map<Class<?>, ProviderSupplier>, Map<Class<?>, ProviderSupplier>> {
 
+    @SuppressWarnings("unchecked")
     @Override
-    public Map<Class<?>, ProviderSupplier> load(AbstractProvider input) {
-        Map<Class<?>, ProviderSupplier> providers = new IdentityHashMap<>();
-        input.registerProviders(providers);
+    public Map<Class<?>, ProviderSupplier> load(Map<Class<?>, ProviderSupplier> providers) {
+        providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Class<? extends CommandSource>) args[0]));
+        providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.CustomItemDataBuilder());
+        providers.put(CustomItemOptions.Builder.class, args -> new GeyserCustomItemOptions.CustomItemOptionsBuilder());
+        providers.put(NonVanillaCustomItemData.Builder.class, args -> new GeyserNonVanillaCustomItemData.NonVanillaCustomItemDataBuilder());
+
         return providers;
     }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/registry/provider/AbstractProvider.java b/core/src/main/java/org/geysermc/geyser/registry/provider/AbstractProvider.java
deleted file mode 100644
index 8c5201c23..000000000
--- a/core/src/main/java/org/geysermc/geyser/registry/provider/AbstractProvider.java
+++ /dev/null
@@ -1,39 +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.registry.provider;
-
-import lombok.RequiredArgsConstructor;
-import org.geysermc.geyser.api.provider.Provider;
-import org.geysermc.geyser.registry.SimpleMappedRegistry;
-
-import java.util.Map;
-
-@RequiredArgsConstructor
-public abstract class AbstractProvider implements Provider {
-    public abstract void registerProviders(Map<Class<?>, ProviderSupplier> providers);
-
-    public abstract SimpleMappedRegistry<Class<?>, ProviderSupplier> providerRegistry();
-}
diff --git a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java b/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java
deleted file mode 100644
index af289bcda..000000000
--- a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserBuilderProvider.java
+++ /dev/null
@@ -1,70 +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.registry.provider;
-
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.command.CommandSource;
-import org.geysermc.geyser.api.item.custom.CustomItemData;
-import org.geysermc.geyser.api.item.custom.CustomItemOptions;
-import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData;
-import org.geysermc.geyser.api.provider.BuilderProvider;
-import org.geysermc.geyser.command.GeyserCommandManager;
-import org.geysermc.geyser.item.GeyserCustomItemData;
-import org.geysermc.geyser.item.GeyserCustomItemOptions;
-import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData;
-import org.geysermc.geyser.registry.ProviderRegistries;
-import org.geysermc.geyser.registry.SimpleMappedRegistry;
-
-import java.util.Map;
-
-public class GeyserBuilderProvider extends AbstractProvider implements BuilderProvider {
-    public static GeyserBuilderProvider INSTANCE = new GeyserBuilderProvider();
-
-    private GeyserBuilderProvider() {
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public void registerProviders(Map<Class<?>, ProviderSupplier> providers) {
-        providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Class<? extends CommandSource>) args[0]));
-        providers.put(CustomItemData.Builder.class, args -> new GeyserCustomItemData.CustomItemDataBuilder());
-        providers.put(CustomItemOptions.Builder.class, args -> new GeyserCustomItemOptions.CustomItemOptionsBuilder());
-        providers.put(NonVanillaCustomItemData.Builder.class, args -> new GeyserNonVanillaCustomItemData.NonVanillaCustomItemDataBuilder());
-    }
-
-    @Override
-    public SimpleMappedRegistry<Class<?>, ProviderSupplier> providerRegistry() {
-        return ProviderRegistries.BUILDERS;
-    }
-
-    @SuppressWarnings("unchecked")
-    @Override
-    public <B extends T, T> @NonNull B provideBuilder(@NonNull Class<T> builderClass, @Nullable Object... args) {
-        return (B) this.providerRegistry().get(builderClass).create(args);
-    }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserProviderManager.java b/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserProviderManager.java
deleted file mode 100644
index 208981d7a..000000000
--- a/core/src/main/java/org/geysermc/geyser/registry/provider/GeyserProviderManager.java
+++ /dev/null
@@ -1,36 +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.registry.provider;
-
-import org.geysermc.geyser.api.provider.ProviderManager;
-
-public class GeyserProviderManager implements ProviderManager {
-
-    @Override
-    public GeyserBuilderProvider builderProvider() {
-        return GeyserBuilderProvider.INSTANCE;
-    }
-}
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 99e29dd21..53d22edbe 100644
--- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
+++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java
@@ -581,7 +581,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
             disconnect(message);
         });
 
-        this.remoteServer = geyser.getRemoteServer();
+        this.remoteServer = geyser.defaultRemoteServer();
     }
 
     /**
@@ -1457,7 +1457,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
         startGamePacket.setFromWorldTemplate(false);
         startGamePacket.setWorldTemplateOptionLocked(false);
 
-        String serverName = geyser.getConfig().getBedrock().getServerName();
+        String serverName = geyser.getConfig().getBedrock().serverName();
         startGamePacket.setLevelId(serverName);
         startGamePacket.setLevelName(serverName);
 
diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java
index 38d57dc01..992835a2b 100644
--- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java
+++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java
@@ -286,7 +286,7 @@ public class SkinManager {
 
             String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
             String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
-            if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserImpl.getInstance().getConfig().getRemote().getAuthType() != AuthType.ONLINE) {
+            if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
                 GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
 
                 if (session != null) {

From 83ba6b5ab53f869bd3e15739a33f61db8c5dc65f Mon Sep 17 00:00:00 2001
From: Camotoy <20743703+Camotoy@users.noreply.github.com>
Date: Sun, 10 Jul 2022 20:58:48 -0400
Subject: [PATCH 2/2] Extensions have their own command

---
 .../downstream/ServerDefineCommandsEvent.java |  4 +-
 .../lifecycle/GeyserDefineCommandsEvent.java  | 42 -----------
 .../bungeecord/GeyserBungeePlugin.java        |  7 +-
 .../command/GeyserBungeeCommandExecutor.java  |  7 +-
 .../platform/spigot/GeyserSpigotInjector.java |  4 +-
 .../platform/spigot/GeyserSpigotPlugin.java   | 13 ++--
 .../command/GeyserSpigotCommandExecutor.java  |  5 +-
 .../spigot/src/main/resources/plugin.yml      |  5 +-
 .../platform/sponge/GeyserSpongePlugin.java   |  7 +-
 .../command/GeyserSpongeCommandExecutor.java  | 10 +--
 .../velocity/GeyserVelocityPlugin.java        |  7 +-
 .../GeyserVelocityCommandExecutor.java        |  6 +-
 .../geyser/command/GeyserCommandExecutor.java |  6 +-
 .../geyser/command/GeyserCommandManager.java  | 75 +++++++++++--------
 .../geyser/command/defaults/HelpCommand.java  | 12 ++-
 .../BedrockCommandRequestTranslator.java      |  7 +-
 16 files changed, 101 insertions(+), 116 deletions(-)
 delete mode 100644 api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java

diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java
index 06412eb4c..2ab1b9611 100644
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/event/downstream/ServerDefineCommandsEvent.java
@@ -33,7 +33,9 @@ import org.geysermc.geyser.api.event.connection.ConnectionEvent;
 import java.util.Set;
 
 /**
- * Called when the downstream server defines the commands available on the server.
+ * Called when the Java server defines the commands available on the server.
+ * <br>
+ * This event is mapped to the existence of Brigadier on the server.
  */
 public class ServerDefineCommandsEvent extends ConnectionEvent implements Cancellable {
     private final Set<? extends CommandInfo> commands;
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java b/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java
deleted file mode 100644
index e506c0ca0..000000000
--- a/api/geyser/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java
+++ /dev/null
@@ -1,42 +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.api.event.lifecycle;
-
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.command.CommandManager;
-import org.geysermc.geyser.api.event.Event;
-
-import java.util.Map;
-
-/**
- * Called when commands are defined within Geyser.
- *
- * @param commandManager the command manager
- * @param commands an immutable view of the default commands
- */
-public record GeyserDefineCommandsEvent(@NonNull CommandManager commandManager, @NonNull Map<String, Command> commands) implements Event {
-}
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 d98349eac..7b937ac6b 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
@@ -86,7 +86,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
             InetSocketAddress javaAddr = listener.getHost();
 
             // By default this should be localhost but may need to be changed in some circumstances
-            if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) {
+            if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) {
                 this.geyserConfig.setAutoconfiguredRemote(true);
                 // Don't use localhost if not listening on all interfaces
                 if (!javaAddr.getHostString().equals("0.0.0.0") && !javaAddr.getHostString().equals("")) {
@@ -109,7 +109,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
             return;
         }
 
-        if (geyserConfig.getRemote().getAuthType() == AuthType.FLOODGATE && getProxy().getPluginManager().getPlugin("floodgate") == null) {
+        if (geyserConfig.getRemote().authType() == AuthType.FLOODGATE && getProxy().getPluginManager().getPlugin("floodgate") == null) {
             geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
             return;
         } else if (geyserConfig.isAutoconfiguredRemote() && getProxy().getPluginManager().getPlugin("floodgate") != null) {
@@ -134,7 +134,8 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
             this.geyserBungeePingPassthrough = new GeyserBungeePingPassthrough(getProxy());
         }
 
-        this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor(geyser));
+        this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor("geyser", geyser, geyserCommandManager.getCommands()));
+        this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor("geyserext", geyser, geyserCommandManager.commands()));
     }
 
     @Override
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java
index d022074fd..6575f047c 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java
@@ -37,14 +37,15 @@ import org.geysermc.geyser.text.GeyserLocale;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Map;
 
 public class GeyserBungeeCommandExecutor extends Command implements TabExecutor {
     private final GeyserCommandExecutor commandExecutor;
 
-    public GeyserBungeeCommandExecutor(GeyserImpl geyser) {
-        super("geyser");
+    public GeyserBungeeCommandExecutor(String name, GeyserImpl geyser, Map<String, org.geysermc.geyser.api.command.Command> commands) {
+        super(name);
 
-        this.commandExecutor = new GeyserCommandExecutor(geyser);
+        this.commandExecutor = new GeyserCommandExecutor(geyser, commands);
     }
 
     @Override
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
index 0fd8d849b..c1d3b6871 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
@@ -170,8 +170,8 @@ public class GeyserSpigotInjector extends GeyserInjector {
      */
     private void workAroundWeirdBug(GeyserBootstrap bootstrap) {
         MinecraftProtocol protocol = new MinecraftProtocol();
-        LocalSession session = new LocalSession(bootstrap.getGeyserConfig().getRemote().getAddress(),
-                bootstrap.getGeyserConfig().getRemote().getPort(), this.serverSocketAddress,
+        LocalSession session = new LocalSession(bootstrap.getGeyserConfig().getRemote().address(),
+                bootstrap.getGeyserConfig().getRemote().port(), this.serverSocketAddress,
                 InetAddress.getLoopbackAddress().getHostAddress(), protocol, protocol.createHelper());
         session.connect();
     }
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 fed5dd6b9..6e36b817d 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
@@ -43,7 +43,6 @@ import org.geysermc.geyser.GeyserImpl;
 import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
 import org.geysermc.geyser.api.command.Command;
 import org.geysermc.geyser.api.network.AuthType;
-import org.geysermc.geyser.command.GeyserCommand;
 import org.geysermc.geyser.command.GeyserCommandManager;
 import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.dump.BootstrapDumpInfo;
@@ -125,7 +124,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
         }
 
         // By default this should be localhost but may need to be changed in some circumstances
-        if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) {
+        if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) {
             geyserConfig.setAutoconfiguredRemote(true);
             // Don't use localhost if not listening on all interfaces
             if (!Bukkit.getIp().equals("0.0.0.0") && !Bukkit.getIp().equals("")) {
@@ -148,7 +147,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
             return;
         }
 
-        if (geyserConfig.getRemote().getAuthType() == AuthType.FLOODGATE && Bukkit.getPluginManager().getPlugin("floodgate") == null) {
+        if (geyserConfig.getRemote().authType() == AuthType.FLOODGATE && Bukkit.getPluginManager().getPlugin("floodgate") == null) {
             geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
             this.getPluginLoader().disablePlugin(this);
             return;
@@ -249,8 +248,10 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
             geyserLogger.debug("Using default world manager: " + this.geyserWorldManager.getClass());
         }
 
-        PluginCommand pluginCommand = this.getCommand("geyser");
-        pluginCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser));
+        PluginCommand geyserCommand = this.getCommand("geyser");
+        geyserCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands()));
+        PluginCommand geyserExtCommand = this.getCommand("geyserext");
+        geyserExtCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands()));
 
         if (!INITIALIZED) {
             // Register permissions so they appear in, for example, LuckPerms' UI
@@ -277,7 +278,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
         boolean brigadierSupported = CommodoreProvider.isSupported();
         geyserLogger.debug("Brigadier supported? " + brigadierSupported);
         if (brigadierSupported) {
-            GeyserBrigadierSupport.loadBrigadier(this, pluginCommand);
+            GeyserBrigadierSupport.loadBrigadier(this, geyserCommand);
         }
 
         // Check to ensure the current setup can support the protocol version Geyser uses
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java
index b5a6ee887..52779db23 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java
@@ -38,11 +38,12 @@ import org.geysermc.geyser.text.GeyserLocale;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 public class GeyserSpigotCommandExecutor extends GeyserCommandExecutor implements TabExecutor {
 
-    public GeyserSpigotCommandExecutor(GeyserImpl geyser) {
-        super(geyser);
+    public GeyserSpigotCommandExecutor(GeyserImpl geyser, Map<String, org.geysermc.geyser.api.command.Command> commands) {
+        super(geyser, commands);
     }
 
     @Override
diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml
index e28b8981d..0f6398b49 100644
--- a/bootstrap/spigot/src/main/resources/plugin.yml
+++ b/bootstrap/spigot/src/main/resources/plugin.yml
@@ -8,4 +8,7 @@ api-version: 1.13
 commands:
   geyser:
     description: The main command for Geyser.
-    usage: /geyser <subcommand>
\ No newline at end of file
+    usage: /geyser <subcommand>
+  geyserext:
+    description: The command any extensions can register to.
+    usage: /geyserext <subcommand>
\ No newline at end of file
diff --git a/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/GeyserSpongePlugin.java b/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/GeyserSpongePlugin.java
index 4e088161d..312dfb087 100644
--- a/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/GeyserSpongePlugin.java
+++ b/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/GeyserSpongePlugin.java
@@ -99,14 +99,14 @@ public class GeyserSpongePlugin implements GeyserBootstrap {
 
             // Don't change the ip if its listening on all interfaces
             // By default this should be 127.0.0.1 but may need to be changed in some circumstances
-            if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) {
+            if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) {
                 this.geyserConfig.setAutoconfiguredRemote(true);
                 geyserConfig.getRemote().setPort(javaAddr.getPort());
             }
         }
 
         if (geyserConfig.getBedrock().isCloneRemotePort()) {
-            geyserConfig.getBedrock().setPort(geyserConfig.getRemote().getPort());
+            geyserConfig.getBedrock().setPort(geyserConfig.getRemote().port());
         }
 
         this.geyserLogger = new GeyserSpongeLogger(logger, geyserConfig.isDebugMode());
@@ -121,7 +121,8 @@ public class GeyserSpongePlugin implements GeyserBootstrap {
 
         this.geyserCommandManager = new GeyserSpongeCommandManager(Sponge.getCommandManager(), geyser);
         this.geyserCommandManager.init();
-        Sponge.getCommandManager().register(this, new GeyserSpongeCommandExecutor(geyser), "geyser");
+        Sponge.getCommandManager().register(this, new GeyserSpongeCommandExecutor(geyser, geyserCommandManager.getCommands()), "geyser");
+        Sponge.getCommandManager().register(this, new GeyserSpongeCommandExecutor(geyser, geyserCommandManager.commands()), "geyserext");
     }
 
     @Override
diff --git a/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/command/GeyserSpongeCommandExecutor.java b/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/command/GeyserSpongeCommandExecutor.java
index 485c06c41..3598ea8c2 100644
--- a/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/command/GeyserSpongeCommandExecutor.java
+++ b/bootstrap/sponge/src/main/java/org/geysermc/geyser/platform/sponge/command/GeyserSpongeCommandExecutor.java
@@ -26,6 +26,7 @@
 package org.geysermc.geyser.platform.sponge.command;
 
 import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.command.Command;
 import org.geysermc.geyser.command.GeyserCommand;
 import org.geysermc.geyser.command.GeyserCommandExecutor;
 import org.geysermc.geyser.command.GeyserCommandSource;
@@ -40,15 +41,12 @@ import org.spongepowered.api.world.Location;
 import org.spongepowered.api.world.World;
 
 import javax.annotation.Nullable;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
+import java.util.*;
 
 public class GeyserSpongeCommandExecutor extends GeyserCommandExecutor implements CommandCallable {
 
-    public GeyserSpongeCommandExecutor(GeyserImpl geyser) {
-        super(geyser);
+    public GeyserSpongeCommandExecutor(GeyserImpl geyser, Map<String, Command> commands) {
+        super(geyser, commands);
     }
 
     @Override
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 0b228941b..739e99d43 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
@@ -102,7 +102,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
         InetSocketAddress javaAddr = proxyServer.getBoundAddress();
 
         // By default this should be localhost but may need to be changed in some circumstances
-        if (this.geyserConfig.getRemote().getAddress().equalsIgnoreCase("auto")) {
+        if (this.geyserConfig.getRemote().address().equalsIgnoreCase("auto")) {
             this.geyserConfig.setAutoconfiguredRemote(true);
             // Don't use localhost if not listening on all interfaces
             if (!javaAddr.getHostString().equals("0.0.0.0") && !javaAddr.getHostString().equals("")) {
@@ -128,7 +128,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
         } catch (ClassNotFoundException ignored) {
         }
 
-        if (geyserConfig.getRemote().getAuthType() == AuthType.FLOODGATE && proxyServer.getPluginManager().getPlugin("floodgate").isEmpty()) {
+        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"));
             return;
@@ -148,7 +148,8 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
         this.geyserCommandManager = new GeyserVelocityCommandManager(geyser);
         this.geyserCommandManager.init();
 
-        this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(geyser));
+        this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(geyser, geyserCommandManager.getCommands()));
+        this.commandManager.register("geyserext", new GeyserVelocityCommandExecutor(geyser, geyserCommandManager.commands()));
         if (geyserConfig.isLegacyPingPassthrough()) {
             this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
         } else {
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java
index b3c4221df..c77a3daef 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java
@@ -27,6 +27,7 @@ package org.geysermc.geyser.platform.velocity.command;
 
 import com.velocitypowered.api.command.SimpleCommand;
 import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.command.Command;
 import org.geysermc.geyser.command.GeyserCommand;
 import org.geysermc.geyser.command.GeyserCommandExecutor;
 import org.geysermc.geyser.command.GeyserCommandSource;
@@ -37,11 +38,12 @@ import org.geysermc.geyser.text.GeyserLocale;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 public class GeyserVelocityCommandExecutor extends GeyserCommandExecutor implements SimpleCommand {
 
-    public GeyserVelocityCommandExecutor(GeyserImpl geyser) {
-        super(geyser);
+    public GeyserVelocityCommandExecutor(GeyserImpl geyser, Map<String, Command> commands) {
+        super(geyser, commands);
     }
 
     @Override
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java
index 3d08600d1..a9b1c734f 100644
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java
+++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java
@@ -37,15 +37,16 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * Represents helper functions for listening to {@code /geyser} commands.
+ * Represents helper functions for listening to {@code /geyser} or {@code /geyserext} commands.
  */
 @AllArgsConstructor
 public class GeyserCommandExecutor {
 
     protected final GeyserImpl geyser;
+    private final Map<String, Command> commands;
 
     public GeyserCommand getCommand(String label) {
-        return (GeyserCommand) geyser.commandManager().commands().get(label);
+        return (GeyserCommand) commands.get(label);
     }
 
     @Nullable
@@ -78,7 +79,6 @@ public class GeyserCommandExecutor {
         }
 
         List<String> availableCommands = new ArrayList<>();
-        Map<String, Command> commands = geyser.commandManager().getCommands();
 
         // Only show commands they have permission to use
         for (Map.Entry<String, Command> entry : commands.entrySet()) {
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
index c6b9cbdd2..4fd5ba411 100644
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
+++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
@@ -25,6 +25,7 @@
 
 package org.geysermc.geyser.command;
 
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import org.checkerframework.checker.nullness.qual.NonNull;
@@ -34,7 +35,6 @@ import org.geysermc.geyser.api.command.Command;
 import org.geysermc.geyser.api.command.CommandExecutor;
 import org.geysermc.geyser.api.command.CommandManager;
 import org.geysermc.geyser.api.command.CommandSource;
-import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent;
 import org.geysermc.geyser.command.defaults.*;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.text.GeyserLocale;
@@ -42,91 +42,104 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 @RequiredArgsConstructor
 public abstract class GeyserCommandManager extends CommandManager {
 
     @Getter
-    private final Map<String, Command> commands = new HashMap<>();
+    private final Map<String, Command> commands = new Object2ObjectOpenHashMap<>(12);
+    private final Map<String, Command> extensionCommands = new Object2ObjectOpenHashMap<>(0);
 
     private final GeyserImpl geyser;
 
     public void init() {
-        register(new HelpCommand(geyser, "help", "geyser.commands.help.desc", "geyser.command.help"));
-        register(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list"));
-        register(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload"));
-        register(new OffhandCommand(geyser, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand"));
-        register(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump"));
-        register(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version"));
-        register(new SettingsCommand(geyser, "settings", "geyser.commands.settings.desc", "geyser.command.settings"));
-        register(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics"));
-        register(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements"));
-        register(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips"));
-        register(new ExtensionsCommand(geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions"));
+        registerBuiltInCommand(new HelpCommand(geyser, "help", "geyser.commands.help.desc", "geyser.command.help", "geyser", commands));
+        registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list"));
+        registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload"));
+        registerBuiltInCommand(new OffhandCommand(geyser, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand"));
+        registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump"));
+        registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version"));
+        registerBuiltInCommand(new SettingsCommand(geyser, "settings", "geyser.commands.settings.desc", "geyser.command.settings"));
+        registerBuiltInCommand(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics"));
+        registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements"));
+        registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips"));
         if (GeyserImpl.getInstance().getPlatformType() == PlatformType.STANDALONE) {
-            register(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
+            registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
         }
 
-        this.geyser.eventBus().fire(new GeyserDefineCommandsEvent(this, this.commands()));
+        register(new HelpCommand(geyser, "help", "geyser.commands.exthelp.desc", "geyser.command.exthelp", "geyserext", extensionCommands));
+    }
+
+    /**
+     * For internal Geyser commands
+     */
+    public void registerBuiltInCommand(GeyserCommand command) {
+        register(command, this.commands);
     }
 
     @Override
     public void register(@NonNull Command command) {
-        this.commands.put(command.name(), command);
-        this.geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", command.name()));
+        register(command, this.extensionCommands);
+    }
+
+    private void register(Command command, Map<String, Command> commands) {
+        commands.put(command.name(), command);
+        geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", command.name()));
 
         if (command.aliases().isEmpty()) {
             return;
         }
 
         for (String alias : command.aliases()) {
-            this.commands.put(alias, command);
+            commands.put(alias, command);
         }
     }
 
     @Override
     public void unregister(@NonNull Command command) {
-        this.commands.remove(command.name(), command);
+        this.extensionCommands.remove(command.name(), command);
 
         if (command.aliases().isEmpty()) {
             return;
         }
 
         for (String alias : command.aliases()) {
-            this.commands.remove(alias, command);
+            this.extensionCommands.remove(alias, command);
         }
     }
 
     @NotNull
     @Override
     public Map<String, Command> commands() {
-        return Collections.unmodifiableMap(this.commands);
+        return Collections.unmodifiableMap(this.extensionCommands);
     }
 
-    public void runCommand(GeyserCommandSource sender, String command) {
-        if (!command.startsWith("geyser "))
-            return;
+    public boolean runCommand(GeyserCommandSource sender, String command) {
+        boolean extensionCommand = command.startsWith("geyserext ");
+        if (!command.startsWith("geyser ") && !extensionCommand) {
+            return false;
+        }
 
-        command = command.trim().replace("geyser ", "");
+        command = command.trim().replace(extensionCommand ? "geyserext " : "geyser ", "");
         String label;
         String[] args;
 
         if (!command.contains(" ")) {
-            label = command.toLowerCase();
+            label = command.toLowerCase(Locale.ROOT);
             args = new String[0];
         } else {
-            label = command.substring(0, command.indexOf(" ")).toLowerCase();
+            label = command.substring(0, command.indexOf(" ")).toLowerCase(Locale.ROOT);
             String argLine = command.substring(command.indexOf(" ") + 1);
             args = argLine.contains(" ") ? argLine.split(" ") : new String[] { argLine };
         }
 
-        Command cmd = commands.get(label);
+        Command cmd = (extensionCommand ? this.extensionCommands : this.commands).get(label);
         if (cmd == null) {
             geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.invalid"));
-            return;
+            return false;
         }
 
         if (cmd instanceof GeyserCommand) {
@@ -140,6 +153,8 @@ public abstract class GeyserCommandManager extends CommandManager {
                 }
             }
         }
+
+        return true;
     }
 
     /**
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
index 84a0730b8..81f34b759 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
@@ -39,10 +39,15 @@ import java.util.Map;
 
 public class HelpCommand extends GeyserCommand {
     private final GeyserImpl geyser;
+    private final String baseCommand;
+    private final Map<String, Command> commands;
 
-    public HelpCommand(GeyserImpl geyser, String name, String description, String permission) {
+    public HelpCommand(GeyserImpl geyser, String name, String description, String permission,
+                       String baseCommand, Map<String, Command> commands) {
         super(name, description, permission);
         this.geyser = geyser;
+        this.baseCommand = baseCommand;
+        this.commands = commands;
 
         this.setAliases(Collections.singletonList("?"));
     }
@@ -61,8 +66,7 @@ public class HelpCommand extends GeyserCommand {
         String header = GeyserLocale.getPlayerLocaleString("geyser.commands.help.header", sender.locale(), page, maxPage);
         sender.sendMessage(header);
 
-        Map<String, Command> cmds = geyser.commandManager().getCommands();
-        for (Map.Entry<String, Command> entry : cmds.entrySet()) {
+        for (Map.Entry<String, Command> entry : commands.entrySet()) {
             Command cmd = entry.getValue();
 
             // Standalone hack-in since it doesn't have a concept of permissions
@@ -72,7 +76,7 @@ public class HelpCommand extends GeyserCommand {
                     continue;
                 }
 
-                sender.sendMessage(ChatColor.YELLOW + "/geyser " + entry.getKey() + ChatColor.WHITE + ": " +
+                sender.sendMessage(ChatColor.YELLOW + "/" + baseCommand + " " + entry.getKey() + ChatColor.WHITE + ": " +
                         GeyserLocale.getPlayerLocaleString(cmd.description(), sender.locale()));
             }
         }
diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java
index e5840ba0d..3301f7b9f 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java
+++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/bedrock/BedrockCommandRequestTranslator.java
@@ -28,7 +28,6 @@ package org.geysermc.geyser.translator.protocol.bedrock;
 import com.nukkitx.protocol.bedrock.packet.CommandRequestPacket;
 import org.geysermc.common.PlatformType;
 import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.command.GeyserCommandManager;
 import org.geysermc.geyser.session.GeyserSession;
 import org.geysermc.geyser.translator.protocol.PacketTranslator;
 import org.geysermc.geyser.translator.protocol.Translator;
@@ -40,10 +39,8 @@ public class BedrockCommandRequestTranslator extends PacketTranslator<CommandReq
     @Override
     public void translate(GeyserSession session, CommandRequestPacket packet) {
         String command = packet.getCommand().replace("/", "");
-        GeyserCommandManager commandManager = GeyserImpl.getInstance().commandManager();
-        if (session.getGeyser().getPlatformType() == PlatformType.STANDALONE && command.trim().startsWith("geyser ") && commandManager.getCommands().containsKey(command.split(" ")[1])) {
-            commandManager.runCommand(session, command);
-        } else {
+        if (!(session.getGeyser().getPlatformType() == PlatformType.STANDALONE
+                && GeyserImpl.getInstance().commandManager().runCommand(session, command))) {
             String message = packet.getCommand().trim();
 
             if (MessageTranslator.isTooLong(message, session)) {