diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java
new file mode 100644
index 000000000..487df3926
--- /dev/null
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java
@@ -0,0 +1,68 @@
+/*
+ * 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.extension;
+
+import java.util.List;
+
+/**
+ * This is the Geyser extension description
+ */
+public interface ExtensionDescription {
+    /**
+     * Gets the extension's name
+     *
+     * @return the extension's name
+     */
+    String name();
+
+    /**
+     * Gets the extension's main class
+     *
+     * @return the extension's main class
+     */
+    String main();
+
+    /**
+     * Gets the extension's api version
+     *
+     * @return the extension's api version
+     */
+    String apiVersion();
+
+    /**
+     * Gets the extension's description
+     *
+     * @return the extension's description
+     */
+    String version();
+
+    /**
+     * Gets the extension's authors
+     *
+     * @return the extension's authors
+     */
+    List<String> authors();
+}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLoader.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLoader.java
new file mode 100644
index 000000000..1301493d5
--- /dev/null
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLoader.java
@@ -0,0 +1,76 @@
+/*
+ * 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.extension;
+
+import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
+import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
+import java.io.File;
+
+/**
+ * The extension loader is responsible for loading, unloading, enabling and disabling extensions
+ */
+public interface ExtensionLoader {
+    /**
+     * Loads an extension from a given file
+     *
+     * @param file the file to load the extension from
+     * @return the loaded extension
+     * @throws InvalidExtensionException
+     */
+    GeyserExtension loadExtension(File file) throws InvalidExtensionException;
+
+    /**
+     * Gets an extension's description from a given file
+     *
+     * @param file the file to get the description from
+     * @return the extension's description
+     * @throws InvalidDescriptionException
+     */
+    ExtensionDescription extensionDescription(File file) throws InvalidDescriptionException;
+
+    /**
+     * Gets a class by its name from the extension's classloader
+     *
+     * @param name the name of the class
+     * @return the class
+     * @throws ClassNotFoundException
+     */
+    Class<?> classByName(final String name) throws ClassNotFoundException;
+
+    /**
+     * Enables an extension
+     *
+     * @param extension the extension to enable
+     */
+    void enableExtension(GeyserExtension extension);
+
+    /**
+     * Disables an extension
+     *
+     * @param extension the extension to disable
+     */
+    void disableExtension(GeyserExtension extension);
+}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLogger.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLogger.java
new file mode 100644
index 000000000..17e108455
--- /dev/null
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLogger.java
@@ -0,0 +1,94 @@
+/*
+ * 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.extension;
+
+/**
+ * This is the Geyser extension logger
+ */
+public interface ExtensionLogger {
+    /**
+     * Get the logger prefix
+     *
+     * @return the logger prefix
+     */
+    String prefix();
+
+    /**
+     * Logs a severe message to console
+     *
+     * @param message the message to log
+     */
+    void severe(String message);
+
+    /**
+     * Logs a severe message and an exception to console
+     *
+     * @param message the message to log
+     * @param error the error to throw
+     */
+    void severe(String message, Throwable error);
+
+    /**
+     * Logs an error message to console
+     *
+     * @param message the message to log
+     */
+    void error(String message);
+
+    /**
+     * Logs an error message and an exception to console
+     *
+     * @param message the message to log
+     * @param error the error to throw
+     */
+    void error(String message, Throwable error);
+
+    /**
+     * Logs a warning message to console
+     *
+     * @param message the message to log
+     */
+    void warning(String message);
+
+    /**
+     * Logs an info message to console
+     *
+     * @param message the message to log
+     */
+    void info(String message);
+
+    /**
+     * Logs a debug message to console
+     *
+     * @param message the message to log
+     */
+    void debug(String message);
+
+    /**
+     * If debug is enabled for this logger
+     */
+    boolean isDebug();
+}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/GeyserExtension.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/GeyserExtension.java
new file mode 100644
index 000000000..bd53bafd3
--- /dev/null
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/GeyserExtension.java
@@ -0,0 +1,235 @@
+/*
+ * 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.extension;
+
+import org.geysermc.api.GeyserApiBase;
+import java.io.*;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * This class is to be extended by a Geyser extension
+ */
+public class GeyserExtension {
+    private boolean initialized = false;
+    private boolean enabled = false;
+    private File file = null;
+    private File dataFolder = null;
+    private ClassLoader classLoader = null;
+    private ExtensionLoader loader = null;
+    private ExtensionLogger logger = null;
+    private ExtensionDescription description = null;
+    private GeyserApiBase api = null;
+
+    /**
+     * Called when the extension is loaded
+     */
+    public void onLoad() {
+
+    }
+
+    /**
+     * Called when the extension is enabled
+     */
+    public void onEnable() {
+
+    }
+
+    /**
+     * Called when the extension is disabled
+     */
+    public void onDisable() {
+
+    }
+
+    /**
+     * Gets if the extension is enabled
+     *
+     * @return true if the extension is enabled
+     */
+    public boolean isEnabled() {
+        return this.enabled;
+    }
+
+    /**
+     * Enables or disables the extension
+     */
+    public void setEnabled(boolean value) {
+        if (this.enabled != value) {
+            this.enabled = value;
+            if (this.enabled) {
+                onEnable();
+            } else {
+                onDisable();
+            }
+        }
+    }
+
+    /**
+     * Gets the extension's data folder
+     *
+     * @return the extension's data folder
+     */
+    public File dataFolder() {
+        return this.dataFolder;
+    }
+
+    /**
+     * Gets the extension's description
+     *
+     * @return the extension's description
+     */
+    public ExtensionDescription description() {
+        return this.description;
+    }
+
+    /**
+     * Gets the extension's name
+     *
+     * @return the extension's name
+     */
+    public String name() {
+        return this.description.name();
+    }
+
+    public void init(GeyserApiBase api, ExtensionLoader loader, ExtensionLogger logger, ExtensionDescription description, File dataFolder, File file) {
+        if (!this.initialized) {
+            this.initialized = true;
+            this.file = file;
+            this.dataFolder = dataFolder;
+            this.classLoader = this.getClass().getClassLoader();
+            this.loader = loader;
+            this.logger = logger;
+            this.description = description;
+            this.api = api;
+        }
+    }
+
+    /**
+     * Gets a resource from the extension jar file
+     *
+     * @param filename the file name
+     * @return the input stream
+     */
+    public InputStream getResource(String filename) {
+        if (filename == null) {
+            throw new IllegalArgumentException("Filename cannot be null");
+        }
+
+        try {
+            URL url = this.classLoader.getResource(filename);
+
+            if (url == null) {
+                return null;
+            }
+
+            URLConnection connection = url.openConnection();
+            connection.setUseCaches(false);
+            return connection.getInputStream();
+        } catch (IOException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Saves a resource from the extension jar file to the extension's data folder
+     *
+     * @param filename the file name
+     * @param replace whether to replace the file if it already exists
+     */
+    public void saveResource(String filename, boolean replace) {
+        if (filename == null || filename.equals("")) {
+            throw new IllegalArgumentException("ResourcePath cannot be null or empty");
+        }
+
+        filename = filename.replace('\\', '/');
+        InputStream in = getResource(filename);
+        if (in == null) {
+            throw new IllegalArgumentException("The embedded resource '" + filename + "' cannot be found in " + file);
+        }
+
+        File outFile = new File(dataFolder, filename);
+        int lastIndex = filename.lastIndexOf('/');
+        File outDir = new File(dataFolder, filename.substring(0, Math.max(lastIndex, 0)));
+
+        if (!outDir.exists()) {
+            outDir.mkdirs();
+        }
+
+        try {
+            if (!outFile.exists() || replace) {
+                OutputStream out = new FileOutputStream(outFile);
+                byte[] buf = new byte[1024];
+                int len;
+                while ((len = in.read(buf)) > 0) {
+                    out.write(buf, 0, len);
+                }
+                out.close();
+                in.close();
+            } else {
+                this.logger.warning("Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists.");
+            }
+        } catch (IOException ex) {
+            this.logger.severe("Could not save " + outFile.getName() + " to " + outFile, ex);
+        }
+    }
+
+    /**
+     * Gets the extension's class loader
+     *
+     * @return the extension's class loader
+     */
+    public ClassLoader classLoader() {
+        return this.classLoader;
+    }
+
+    /**
+     * Gets the extension's loader
+     *
+     * @return the extension's loader
+     */
+    public ExtensionLoader extensionLoader() {
+        return this.loader;
+    }
+
+    /**
+     * Gets the extension's logger
+     *
+     * @return the extension's logger
+     */
+    public ExtensionLogger logger() {
+        return this.logger;
+    }
+
+    /**
+     * Gets the {@link GeyserApiBase} instance
+     *
+     * @return the {@link GeyserApiBase} instance
+     */
+    public GeyserApiBase geyserApi() {
+        return this.api;
+    }
+}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java
new file mode 100644
index 000000000..1fe88e9e9
--- /dev/null
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.extension.exception;
+
+/**
+ * Thrown when an extension's description is invalid.
+ */
+public class InvalidDescriptionException extends Exception {
+    public InvalidDescriptionException(Throwable cause) {
+        super(cause);
+    }
+
+    public InvalidDescriptionException(String message) {
+        super(message);
+    }
+
+    public InvalidDescriptionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java
new file mode 100644
index 000000000..7fb6b6922
--- /dev/null
+++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java
@@ -0,0 +1,43 @@
+/*
+ * 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.extension.exception;
+
+/**
+ * Thrown when an extension is invalid.
+ */
+public class InvalidExtensionException extends Exception {
+    public InvalidExtensionException(Throwable cause) {
+        super(cause);
+    }
+
+    public InvalidExtensionException(String message) {
+        super(message);
+    }
+
+    public InvalidExtensionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index f63e222cc..2fbcbaddd 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -52,6 +52,7 @@ import org.geysermc.geyser.api.GeyserApi;
 import org.geysermc.geyser.command.CommandManager;
 import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.entity.EntityDefinitions;
+import org.geysermc.geyser.extension.GeyserExtensionManager;
 import org.geysermc.geyser.level.WorldManager;
 import org.geysermc.geyser.network.ConnectorServerEventHandler;
 import org.geysermc.geyser.pack.ResourcePack;
@@ -122,6 +123,8 @@ public class GeyserImpl implements GeyserApi {
     private final PlatformType platformType;
     private final GeyserBootstrap bootstrap;
 
+    private final GeyserExtensionManager extensionManager;
+
     private Metrics metrics;
 
     private static GeyserImpl instance;
@@ -154,6 +157,9 @@ public class GeyserImpl implements GeyserApi {
         MessageTranslator.init();
         MinecraftLocale.init();
 
+        extensionManager = new GeyserExtensionManager();
+        extensionManager.init();
+
         start();
 
         GeyserConfiguration config = bootstrap.getGeyserConfig();
@@ -197,6 +203,8 @@ public class GeyserImpl implements GeyserApi {
 
         ResourcePack.loadPacks();
 
+        extensionManager.enableExtensions();
+
         if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) {
             // Set the remote address to localhost since that is where we are always connecting
             try {
@@ -457,6 +465,8 @@ public class GeyserImpl implements GeyserApi {
 
         ResourcePack.PACKS.clear();
 
+        extensionManager.disableExtensions();
+
         bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done"));
     }
 
@@ -508,6 +518,10 @@ public class GeyserImpl implements GeyserApi {
         return bootstrap.getWorldManager();
     }
 
+    public GeyserExtensionManager getExtensionManager() {
+        return extensionManager;
+    }
+
     public static GeyserImpl getInstance() {
         return instance;
     }
diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
index 2734c7443..3377f7ee5 100644
--- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
+++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
@@ -36,6 +36,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.extension.GeyserExtension;
+import org.geysermc.geyser.extension.GeyserExtensionManager;
 import org.geysermc.geyser.text.AsteriskSerializer;
 import org.geysermc.geyser.configuration.GeyserConfiguration;
 import org.geysermc.geyser.network.MinecraftProtocol;
@@ -54,10 +56,7 @@ import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.UnknownHostException;
 import java.nio.file.Paths;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Getter
@@ -76,6 +75,7 @@ public class DumpInfo {
     private LogsInfo logsInfo;
     private final BootstrapDumpInfo bootstrapInfo;
     private final FlagsInfo flagsInfo;
+    private final List<ExtensionInfo> extensionInfo;
 
     public DumpInfo(boolean addLog) {
         this.versionInfo = new VersionInfo();
@@ -125,6 +125,11 @@ public class DumpInfo {
         this.bootstrapInfo = GeyserImpl.getInstance().getBootstrap().getDumpInfo();
 
         this.flagsInfo = new FlagsInfo();
+
+        this.extensionInfo = new ArrayList<>();
+        for (GeyserExtension extension : GeyserImpl.getInstance().getExtensionManager().getExtensions().values()) {
+            this.extensionInfo.add(new ExtensionInfo(extension.isEnabled(), extension.name(), extension.description().version(), extension.description().apiVersion(), extension.description().main(), extension.description().authors()));
+        }
     }
 
     @Getter
@@ -277,4 +282,15 @@ public class DumpInfo {
             this.flags = ManagementFactory.getRuntimeMXBean().getInputArguments();
         }
     }
+
+    @Getter
+    @AllArgsConstructor
+    public static class ExtensionInfo {
+        public boolean enabled;
+        public String name;
+        public String version;
+        public String apiVersion;
+        public String main;
+        public List<String> authors;
+    }
 }
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
new file mode 100644
index 000000000..67363a40f
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java
@@ -0,0 +1,99 @@
+/*
+ * 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.extension;
+
+import org.geysermc.geyser.api.extension.ExtensionDescription;
+import org.geysermc.geyser.api.extension.GeyserExtension;
+import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class GeyserExtensionClassLoader extends URLClassLoader {
+    private final GeyserExtensionLoader loader;
+    private final Map<String, Class> classes = new HashMap<>();
+    public GeyserExtension extension;
+
+    public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, ExtensionDescription description, File file) throws InvalidExtensionException, MalformedURLException {
+        super(new URL[] { file.toURI().toURL() }, parent);
+        this.loader = loader;
+
+        try {
+            Class<?> jarClass;
+            try {
+                jarClass = Class.forName(description.main(), true, this);
+            } catch (ClassNotFoundException ex) {
+                throw new InvalidExtensionException("Class " + description.main() + " not found, extension cannot be loaded", ex);
+            }
+
+            Class<? extends GeyserExtension> extensionClass;
+            try {
+                extensionClass = jarClass.asSubclass(GeyserExtension.class);
+            } catch (ClassCastException ex) {
+                throw new InvalidExtensionException("Main class " + description.main() + " should extends GeyserExtension, but extends " + jarClass.getSuperclass().getSimpleName(), ex);
+            }
+
+            extension = extensionClass.newInstance();
+        } catch (IllegalAccessException ex) {
+            throw new InvalidExtensionException("No public constructor", ex);
+        } catch (InstantiationException ex) {
+            throw new InvalidExtensionException("Abnormal extension type", ex);
+        }
+    }
+
+    @Override
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+        return this.findClass(name, true);
+    }
+
+    protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFoundException {
+        if (name.startsWith("org.geysermc.geyser.") || name.startsWith("org.geysermc.connector.") || name.startsWith("org.geysermc.platform.") || name.startsWith("org.geysermc.floodgate.") || name.startsWith("org.geysermc.api.") || name.startsWith("org.geysermc.processor.") || name.startsWith("net.minecraft.")) {
+            throw new ClassNotFoundException(name);
+        }
+        Class<?> result = classes.get(name);
+        if (result == null) {
+            if (checkGlobal) {
+                result = loader.classByName(name);
+            }
+            if (result == null) {
+                result = super.findClass(name);
+                if (result != null) {
+                    loader.setClass(name, result);
+                }
+            }
+            classes.put(name, result);
+        }
+        return result;
+    }
+
+    Set<String> getClasses() {
+        return classes.keySet();
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
new file mode 100644
index 000000000..f8a3d9bbe
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
@@ -0,0 +1,102 @@
+/*
+ * 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.extension;
+
+import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import java.io.InputStream;
+import java.util.*;
+
+public class GeyserExtensionDescription implements org.geysermc.geyser.api.extension.ExtensionDescription {
+    private String name;
+    private String main;
+    private String api;
+    private String version;
+    private final List<String> authors = new ArrayList<>();
+
+    public GeyserExtensionDescription(InputStream inputStream) throws InvalidDescriptionException {
+        DumperOptions dumperOptions = new DumperOptions();
+        dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+        Yaml yaml = new Yaml(dumperOptions);
+        this.loadMap(yaml.loadAs(inputStream, LinkedHashMap.class));
+    }
+
+    private void loadMap(Map<String, Object> yamlMap) throws InvalidDescriptionException {
+        this.name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", "");
+        if (this.name.equals("")) {
+            throw new InvalidDescriptionException("Invalid extension name, cannot be empty");
+        }
+        this.name = this.name.replace(" ", "_");
+        this.version = String.valueOf(yamlMap.get("version"));
+        this.main = (String) yamlMap.get("main");
+
+        Object api = yamlMap.get("api");
+        if (api instanceof String) {
+            this.api = (String) api;
+        } else {
+            this.api = "0.0.0";
+            throw new InvalidDescriptionException("Invalid api version format, should be a string: major.minor.patch");
+        }
+
+        if (yamlMap.containsKey("author")) {
+            this.authors.add((String) yamlMap.get("author"));
+        }
+
+        if (yamlMap.containsKey("authors")) {
+            try {
+                this.authors.addAll((Collection<? extends String>) yamlMap.get("authors"));
+            } catch (Exception e) {
+                throw new InvalidDescriptionException("Invalid authors format, should be a list of strings", e);
+            }
+        }
+    }
+
+    @Override
+    public String name() {
+        return this.name;
+    }
+
+    @Override
+    public String main() {
+        return this.main;
+    }
+
+    @Override
+    public String apiVersion() {
+        return api;
+    }
+
+    @Override
+    public String version() {
+        return this.version;
+    }
+
+    @Override
+    public List<String> authors() {
+        return this.authors;
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
new file mode 100644
index 000000000..f029eb797
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
@@ -0,0 +1,165 @@
+/*
+ * 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.extension;
+
+import org.geysermc.api.Geyser;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.extension.ExtensionLoader;
+import org.geysermc.geyser.api.extension.GeyserExtension;
+import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
+import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
+import org.geysermc.geyser.text.GeyserLocale;
+import java.io.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Pattern;
+
+public class GeyserExtensionLoader implements ExtensionLoader {
+    private final Map<String, Class> classes = new HashMap<>();
+    private final Map<String, GeyserExtensionClassLoader> classLoaders = new HashMap<>();
+
+    @Override
+    public GeyserExtension loadExtension(File file) throws InvalidExtensionException {
+        if (file == null) {
+            throw new InvalidExtensionException("File is null");
+        }
+
+        if (!file.exists()) {
+            throw new InvalidExtensionException(new FileNotFoundException(file.getPath()) + " does not exist");
+        }
+
+        final GeyserExtensionDescription description;
+        try {
+            description = extensionDescription(file);
+        } catch (InvalidDescriptionException e) {
+            throw new InvalidExtensionException(e);
+        }
+
+        final File parentFile = file.getParentFile();
+        final File dataFolder = new File(parentFile, description.name());
+        if (dataFolder.exists() && !dataFolder.isDirectory()) {
+            throw new InvalidExtensionException("The folder " + dataFolder.getPath() + " is not a directory and is the data folder for the extension " + description.name() + "!");
+        }
+
+        final GeyserExtensionClassLoader loader;
+        try {
+            loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), description, file);
+        } catch (Throwable e) {
+            throw new InvalidExtensionException(e);
+        }
+        classLoaders.put(description.name(), loader);
+
+        setup(loader.extension, description, dataFolder, file);
+        return loader.extension;
+    }
+
+    private void setup(GeyserExtension extension, GeyserExtensionDescription description, File dataFolder, File file) {
+        GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name());
+        extension.init(Geyser.api(), this, logger, description, dataFolder, file);
+        extension.onLoad();
+    }
+
+    @Override
+    public GeyserExtensionDescription extensionDescription(File file) throws InvalidDescriptionException {
+        JarFile jarFile = null;
+        InputStream stream = null;
+
+        try {
+            jarFile = new JarFile(file);
+
+            JarEntry descriptionEntry = jarFile.getJarEntry("extension.yml");
+            if (descriptionEntry == null) {
+                throw new InvalidDescriptionException(new FileNotFoundException("extension.yml") + " does not exist in the jar file!");
+            }
+
+            stream = jarFile.getInputStream(descriptionEntry);
+            return new GeyserExtensionDescription(stream);
+        } catch (IOException e) {
+            throw new InvalidDescriptionException(e);
+        } finally {
+            if (jarFile != null) {
+                try {
+                    jarFile.close();
+                } catch (IOException e) {
+                }
+            }
+            if (stream != null) {
+                try {
+                    stream.close();
+                } catch (IOException e) {
+                }
+            }
+        }
+    }
+
+    public Pattern[] extensionFilters() {
+        return new Pattern[] { Pattern.compile("^.+\\.jar$") };
+    }
+
+    @Override
+    public Class<?> classByName(final String name) throws ClassNotFoundException{
+        Class<?> clazz = classes.get(name);
+        try {
+            for(GeyserExtensionClassLoader loader : classLoaders.values()) {
+                try {
+                    clazz = loader.findClass(name,false);
+                } catch(NullPointerException e) {
+                }
+            }
+            return clazz;
+        } catch(NullPointerException s) {
+            return null;
+        }
+    }
+
+    void setClass(String name, final Class<?> clazz) {
+        if (!classes.containsKey(name)) {
+            classes.put(name,clazz);
+        }
+    }
+
+    void removeClass(String name) {
+        classes.remove(name);
+    }
+
+    @Override
+    public void enableExtension(GeyserExtension extension) {
+        if (!extension.isEnabled()) {
+            GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.description().name()));
+            extension.setEnabled(true);
+        }
+    }
+
+    @Override
+    public void disableExtension(GeyserExtension extension) {
+        if (extension.isEnabled()) {
+            GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.description().name()));
+            extension.setEnabled(false);
+        }
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLogger.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLogger.java
new file mode 100644
index 000000000..fe23417f8
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLogger.java
@@ -0,0 +1,88 @@
+/*
+ * 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.extension;
+
+import org.geysermc.geyser.GeyserLogger;
+import org.geysermc.geyser.api.extension.ExtensionLogger;
+
+public class GeyserExtensionLogger implements ExtensionLogger {
+    private final GeyserLogger logger;
+    private final String loggerPrefix;
+
+    public GeyserExtensionLogger(GeyserLogger logger, String prefix) {
+        this.logger = logger;
+        this.loggerPrefix = prefix;
+    }
+
+    @Override
+    public String prefix() {
+        return this.loggerPrefix;
+    }
+
+    private String addPrefix(String message) {
+        return "[" + this.loggerPrefix + "] " + message;
+    }
+
+    @Override
+    public void severe(String message) {
+        this.logger.severe(this.addPrefix(message));
+    }
+
+    @Override
+    public void severe(String message, Throwable error) {
+        this.logger.severe(this.addPrefix(message), error);
+    }
+
+    @Override
+    public void error(String message) {
+        this.logger.error(this.addPrefix(message));
+    }
+
+    @Override
+    public void error(String message, Throwable error) {
+        this.logger.error(this.addPrefix(message), error);
+    }
+
+    @Override
+    public void warning(String message) {
+        this.logger.warning(this.addPrefix(message));
+    }
+
+    @Override
+    public void info(String message) {
+        this.logger.info(this.addPrefix(message));
+    }
+
+    @Override
+    public void debug(String message) {
+        this.logger.debug(this.addPrefix(message));
+    }
+
+    @Override
+    public boolean isDebug() {
+        return this.logger.isDebug();
+    }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java
new file mode 100644
index 000000000..169053182
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java
@@ -0,0 +1,229 @@
+/*
+ * 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.extension;
+
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.extension.ExtensionDescription;
+import org.geysermc.geyser.api.extension.GeyserExtension;
+import org.geysermc.geyser.text.GeyserLocale;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.*;
+import java.util.regex.Pattern;
+
+public class GeyserExtensionManager {
+    protected Map<String, GeyserExtension> extensions = new LinkedHashMap<>();
+    protected Map<Pattern, GeyserExtensionLoader> fileAssociations = new HashMap<>();
+
+    public void init() {
+        GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.loading"));
+
+        this.registerInterface(GeyserExtensionLoader.class);
+        this.loadExtensions(new File("extensions"));
+
+        GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.done", this.extensions.size()));
+    }
+
+    public GeyserExtension getExtension(String name) {
+        if (this.extensions.containsKey(name)) {
+            return this.extensions.get(name);
+        }
+        return null;
+    }
+
+    public Map<String, GeyserExtension> getExtensions() {
+        return this.extensions;
+    }
+
+    public void registerInterface(Class<? extends GeyserExtensionLoader> loader) {
+        GeyserExtensionLoader instance;
+
+        if (GeyserExtensionLoader.class.isAssignableFrom(loader)) {
+            Constructor<? extends GeyserExtensionLoader> constructor;
+
+            try {
+                constructor = loader.getConstructor();
+                instance = constructor.newInstance();
+            } catch (NoSuchMethodException ex) { // This should never happen
+                String className = loader.getName();
+
+                throw new IllegalArgumentException("Class " + className + " does not have a public constructor", ex);
+            } catch (Exception ex) { // This should never happen
+                throw new IllegalArgumentException("Unexpected exception " + ex.getClass().getName() + " while attempting to construct a new instance of " + loader.getName(), ex);
+            }
+        } else {
+            throw new IllegalArgumentException("Class " + loader.getName() + " does not implement interface ExtensionLoader");
+        }
+
+        Pattern[] patterns = instance.extensionFilters();
+
+        synchronized (this) {
+            for (Pattern pattern : patterns) {
+                fileAssociations.put(pattern, instance);
+            }
+        }
+    }
+
+    public GeyserExtension loadExtension(File file, Map<Pattern, GeyserExtensionLoader> loaders) {
+        for (GeyserExtensionLoader loader : (loaders == null ? this.fileAssociations : loaders).values()) {
+            for (Pattern pattern : loader.extensionFilters()) {
+                if (pattern.matcher(file.getName()).matches()) {
+                    try {
+                        ExtensionDescription description = loader.extensionDescription(file);
+                        if (description != null) {
+                            GeyserExtension extension = loader.loadExtension(file);
+
+                            if (extension != null) {
+                                this.extensions.put(extension.description().name(), extension);
+
+                                return extension;
+                            }
+                        }
+                    } catch (Exception e) {
+                        GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed"), e);
+                        return null;
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public Map<String, GeyserExtension> loadExtensions(File dictionary) {
+        if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) { // If your IDE says this is always true, ignore it, it isn't.
+            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_dev_environment"));
+            return new HashMap<>();
+        }
+        if (!GeyserImpl.VERSION.contains(".")) {
+            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_version_number"));
+            return new HashMap<>();
+        }
+
+        String[] apiVersion = GeyserImpl.VERSION.split("\\.");
+
+        if (!dictionary.exists()) {
+            dictionary.mkdir();
+        }
+        if (!dictionary.isDirectory()) {
+            return new HashMap<>();
+        }
+
+        Map<String, File> extensions = new LinkedHashMap<>();
+        Map<String, GeyserExtension> loadedExtensions = new LinkedHashMap<>();
+
+        for (final GeyserExtensionLoader loader : this.fileAssociations.values()) {
+            for (File file : dictionary.listFiles((dir, name) -> {
+                for (Pattern pattern : loader.extensionFilters()) {
+                    if (pattern.matcher(name).matches()) {
+                        return true;
+                    }
+                }
+                return false;
+            })) {
+                if (file.isDirectory()) {
+                    continue;
+                }
+
+                try {
+                    ExtensionDescription description = loader.extensionDescription(file);
+                    if (description != null) {
+                        String name = description.name();
+
+                        if (extensions.containsKey(name) || this.getExtension(name) != null) {
+                            GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, file.getName()));
+                            continue;
+                        }
+
+                        try {
+                            //Check the format: majorVersion.minorVersion.patch
+                            if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", description.apiVersion())) {
+                                throw new IllegalArgumentException();
+                            }
+                        } catch (NullPointerException | IllegalArgumentException e) {
+                            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion[0] + "." + apiVersion[1]));
+                            continue;
+                        }
+
+                        String[] versionArray = description.apiVersion().split("\\.");
+
+                        //Completely different API version
+                        if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
+                            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1]));
+                            continue;
+                        }
+
+                        //If the extension requires new API features, being backwards compatible
+                        if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
+                            GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1]));
+                            continue;
+                        }
+
+                        extensions.put(name, file);
+                        loadedExtensions.put(name, this.loadExtension(file, this.fileAssociations));
+                    }
+                } catch (Exception e) {
+                    GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", file.getName(), dictionary.getAbsolutePath()), e);
+                }
+            }
+        }
+
+        return loadedExtensions;
+    }
+
+    public void enableExtension(GeyserExtension extension) {
+        if (!extension.isEnabled()) {
+            try {
+                extension.extensionLoader().enableExtension(extension);
+            } catch (Exception e) {
+                GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.failed", extension.name()), e);
+                this.disableExtension(extension);
+            }
+        }
+    }
+
+    public void disableExtension(GeyserExtension extension) {
+        if (extension.isEnabled()) {
+            try {
+                extension.extensionLoader().disableExtension(extension);
+            } catch (Exception e) {
+                GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.failed", extension.name()), e);
+            }
+        }
+    }
+
+    public void enableExtensions() {
+        for (GeyserExtension extension : this.getExtensions().values()) {
+            this.enableExtension(extension);
+        }
+    }
+
+    public void disableExtensions() {
+        for (GeyserExtension extension : this.getExtensions().values()) {
+            this.disableExtension(extension);
+        }
+    }
+}
diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages
index bdee0d0f3..94c185193 160000
--- a/core/src/main/resources/languages
+++ b/core/src/main/resources/languages
@@ -1 +1 @@
-Subproject commit bdee0d0f3f8a1271cd001f0bd0d672d0010be1db
+Subproject commit 94c1851931f2319a7e7f42c2fe9066b78235bc39