diff --git a/build.gradle.kts b/build.gradle.kts
index 0f7fd3dbda..d3c530db0b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -39,7 +39,7 @@ subprojects {
             events(TestLogEvent.STANDARD_OUT)
         }
         minHeapSize = "2g"
-        maxHeapSize = "2g"
+        maxHeapSize = "4g"
     }
 
     repositories {
diff --git a/patches/api/Add-Raw-Byte-ItemStack-Serialization.patch b/patches/api/Add-Raw-Byte-ItemStack-Serialization.patch
index bb89536915..d30beb0b77 100644
--- a/patches/api/Add-Raw-Byte-ItemStack-Serialization.patch
+++ b/patches/api/Add-Raw-Byte-ItemStack-Serialization.patch
@@ -10,8 +10,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/org/bukkit/UnsafeValues.java
 +++ b/src/main/java/org/bukkit/UnsafeValues.java
 @@ -0,0 +0,0 @@ public interface UnsafeValues {
-     static boolean isLegacyPlugin(org.bukkit.plugin.Plugin plugin) {
-         return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion());
+     default com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
+         return new com.destroystokyo.paper.util.VersionFetcher.DummyVersionFetcher();
      }
 +
 +    byte[] serializeItem(ItemStack item);
diff --git a/patches/api/Add-an-asterisk-to-legacy-API-plugins.patch b/patches/api/Add-an-asterisk-to-legacy-API-plugins.patch
deleted file mode 100644
index 0f936372dd..0000000000
--- a/patches/api/Add-an-asterisk-to-legacy-API-plugins.patch
+++ /dev/null
@@ -1,66 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Phoenix616 <max@themoep.de>
-Date: Tue, 1 Dec 2020 14:57:02 +0100
-Subject: [PATCH] Add an asterisk to legacy API plugins
-
-Not here to name and shame, only so server admins can be aware of which
-plugins have and haven't been updated.
-
-diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/UnsafeValues.java
-+++ b/src/main/java/org/bukkit/UnsafeValues.java
-@@ -0,0 +0,0 @@ public interface UnsafeValues {
-     default com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
-         return new com.destroystokyo.paper.util.VersionFetcher.DummyVersionFetcher();
-     }
-+
-+    boolean isSupportedApiVersion(String apiVersion);
-+
-+    static boolean isLegacyPlugin(org.bukkit.plugin.Plugin plugin) {
-+        return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion());
-+    }
-     // Paper end
- }
-diff --git a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
-+++ b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
-@@ -0,0 +0,0 @@ public class PluginsCommand extends BukkitCommand {
-             }
- 
-             Plugin plugin = entry.getValue();
--            
-+
-             pluginList.append(plugin.isEnabled() ? ChatColor.GREEN : ChatColor.RED);
--            pluginList.append(plugin.getDescription().getName());
-+            // Paper start - Add an asterisk to legacy plugins (so admins are aware)
-+            String pluginName = plugin.getDescription().getName();
-+            if (org.bukkit.UnsafeValues.isLegacyPlugin(plugin)) {
-+                pluginName += "*";
-+            }
-+            pluginList.append(pluginName);
-+            // Paper end
- 
-             if (plugin.getDescription().getProvides().size() > 0) {
-                 pluginList.append(" (").append(String.join(", ", plugin.getDescription().getProvides())).append(")");
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
-         Preconditions.checkArgument(plugin instanceof JavaPlugin, "Plugin is not associated with this PluginLoader");
- 
-         if (!plugin.isEnabled()) {
--            plugin.getLogger().info("Enabling " + plugin.getDescription().getFullName());
-+            // Paper start - Add an asterisk to legacy plugins (so admins are aware)
-+            String enableMsg = "Enabling " + plugin.getDescription().getFullName();
-+            if (org.bukkit.UnsafeValues.isLegacyPlugin(plugin)) {
-+                enableMsg += "*";
-+            }
-+
-+            plugin.getLogger().info(enableMsg);
-+            // Paper end
- 
-             JavaPlugin jPlugin = (JavaPlugin) plugin;
- 
diff --git a/patches/api/Add-command-line-option-to-load-extra-plugin-jars-no.patch b/patches/api/Add-command-line-option-to-load-extra-plugin-jars-no.patch
index 0d057f5045..b7f88bd3af 100644
--- a/patches/api/Add-command-line-option-to-load-extra-plugin-jars-no.patch
+++ b/patches/api/Add-command-line-option-to-load-extra-plugin-jars-no.patch
@@ -68,104 +68,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    @NotNull
 +    public Plugin[] loadPlugins(final @NotNull File directory, final @NotNull List<File> extraPluginJars) {
 +        // Paper end
+         if (true) {
+             List<Plugin> pluginList = new ArrayList<>();
+             java.util.Collections.addAll(pluginList, this.paperPluginManager.loadPlugins(directory));
++            for (File file : extraPluginJars) {
++                try {
++                    pluginList.add(this.paperPluginManager.loadPlugin(file));
++                } catch (Exception e) {
++                    this.server.getLogger().log(Level.SEVERE, "Plugin loading error!", e);
++                }
++            }
+             return pluginList.toArray(new Plugin[0]);
+         }
          Preconditions.checkArgument(directory != null, "Directory cannot be null");
-         Preconditions.checkArgument(directory.isDirectory(), "Directory must be a directory");
- 
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-         Map<String, Collection<String>> softDependencies = new HashMap<String, Collection<String>>();
- 
-         // This is where it figures out all possible plugins
--        for (File file : directory.listFiles()) {
-+        // Paper start - extra jars
-+        final List<File> pluginJars = new ArrayList<>(java.util.Arrays.asList(directory.listFiles()));
-+        pluginJars.addAll(extraPluginJars);
-+        for (File file : pluginJars) {
-+            // Paper end
-             PluginLoader loader = null;
-             for (Pattern filter : filters) {
-                 Matcher match = filter.matcher(file.getName());
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                 description = loader.getPluginDescription(file);
-                 String name = description.getName();
-                 if (name.equalsIgnoreCase("bukkit") || name.equalsIgnoreCase("minecraft") || name.equalsIgnoreCase("mojang")) {
--                    server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "': Restricted Name");
-+                    server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "': Restricted Name"); // Paper
-                     continue;
-                 } else if (description.rawName.indexOf(' ') != -1) {
--                    server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "': uses the space-character (0x20) in its name");
-+                    server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "': uses the space-character (0x20) in its name"); // Paper
-                     continue;
-                 }
-             } catch (InvalidDescriptionException ex) {
--                server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "'", ex);
-+                server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "'", ex); // Paper
-                 continue;
-             }
- 
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                     description.getName(),
-                     file.getPath(),
-                     replacedFile.getPath(),
--                    directory.getPath()
-+                    file.getParentFile().getPath() // Paper
-                 ));
-             }
- 
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                             file.getPath(),
-                             provided,
-                             pluginFile.getPath(),
--                            directory.getPath()
-+                            file.getParentFile().getPath() // Paper
-                     ));
-                 } else {
-                     String replacedPlugin = pluginsProvided.put(provided, description.getName());
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
- 
-                             server.getLogger().log(
-                                 Level.SEVERE,
--                                "Could not load '" + entry.getValue().getPath() + "' in folder '" + directory.getPath() + "'",
-+                                "Could not load '" + entry.getValue().getPath() + "' in folder '" + entry.getValue().getParentFile().getPath() + "'", // Paper
-                                 new UnknownDependencyException("Unknown dependency " + dependency + ". Please download and install " + dependency + " to run this plugin."));
-                             break;
-                         }
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                             loadedPlugins.add(loadedPlugin.getName());
-                             loadedPlugins.addAll(loadedPlugin.getDescription().getProvides());
-                         } else {
--                            server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "'");
-+                            server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "'"); // Paper
-                         }
-                         continue;
-                     } catch (InvalidPluginException ex) {
--                        server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "'", ex);
-+                        server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "'", ex); // Paper
-                     }
-                 }
-             }
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                                 loadedPlugins.add(loadedPlugin.getName());
-                                 loadedPlugins.addAll(loadedPlugin.getDescription().getProvides());
-                             } else {
--                                server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "'");
-+                                server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "'"); // Paper
-                             }
-                             break;
-                         } catch (InvalidPluginException ex) {
--                            server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "'", ex);
-+                            server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "'", ex); // Paper
-                         }
-                     }
-                 }
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                     while (failedPluginIterator.hasNext()) {
-                         File file = failedPluginIterator.next();
-                         failedPluginIterator.remove();
--                        server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + directory.getPath() + "': circular dependency detected");
-+                        server.getLogger().log(Level.SEVERE, "Could not load '" + file.getPath() + "' in folder '" + file.getParentFile().getPath() + "': circular dependency detected"); // Paper
-                     }
-                 }
-             }
 diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
diff --git a/patches/api/Add-command-to-reload-permissions.yml-and-require-co.patch b/patches/api/Add-command-to-reload-permissions.yml-and-require-co.patch
index d74beb5762..7addea4a6c 100644
--- a/patches/api/Add-command-to-reload-permissions.yml-and-require-co.patch
+++ b/patches/api/Add-command-to-reload-permissions.yml-and-require-co.patch
@@ -84,21 +84,3 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return java.util.Collections.singletonList("permissions"); // Paper
      }
  }
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-     public void useTimings(boolean use) {
-         co.aikar.timings.Timings.setTimingsEnabled(use); // Paper
-     }
-+
-+    // Paper start
-+    public void clearPermissions() {
-+        permissions.clear();
-+        defaultPerms.get(true).clear();
-+        defaultPerms.get(false).clear();
-+    }
-+    // Paper end
-+
- }
diff --git a/patches/api/Add-exception-reporting-event.patch b/patches/api/Add-exception-reporting-event.patch
index 47a6b652ec..89475e4f38 100644
--- a/patches/api/Add-exception-reporting-event.patch
+++ b/patches/api/Add-exception-reporting-event.patch
@@ -562,7 +562,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
      @Override
      public void clearPlugins() {
-         synchronized (this) {
+         if (true) {this.paperPluginManager.clearPlugins(); return;} // Paper
 @@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
                              ));
                  }
diff --git a/patches/api/Add-system-property-to-print-stacktrace-on-bad-plugi.patch b/patches/api/Add-system-property-to-print-stacktrace-on-bad-plugi.patch
index b724857c38..c9371793dd 100644
--- a/patches/api/Add-system-property-to-print-stacktrace-on-bad-plugi.patch
+++ b/patches/api/Add-system-property-to-print-stacktrace-on-bad-plugi.patch
@@ -9,7 +9,7 @@ diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/m
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
+@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
                              // In case the bad access occurs on construction
                              loader.server.getLogger().log(Level.WARNING, "[{0}] Loaded class {1} from {2} which is not a depend or softdepend of this plugin.", new Object[]{description.getName(), name, provider.getFullName()});
                          }
diff --git a/patches/api/Add-workaround-for-plugins-modifying-the-parent-of-t.patch b/patches/api/Add-workaround-for-plugins-modifying-the-parent-of-t.patch
index 2406cc6426..feb529d95d 100644
--- a/patches/api/Add-workaround-for-plugins-modifying-the-parent-of-t.patch
+++ b/patches/api/Add-workaround-for-plugins-modifying-the-parent-of-t.patch
@@ -20,6 +20,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package com.destroystokyo.paper.utils;
 +
++import io.papermc.paper.plugin.configuration.PluginMeta;
 +import org.bukkit.plugin.PluginDescriptionFile;
 +
 +import java.util.logging.Level;
@@ -32,20 +33,26 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 + */
 +public class PaperPluginLogger extends Logger {
 +
++    @Deprecated(forRemoval = true)
 +    @NotNull
 +    public static Logger getLogger(@NotNull PluginDescriptionFile description) {
-+        Logger logger = new PaperPluginLogger(description);
++        return getLogger((PluginMeta) description);
++    }
++
++    @NotNull
++    public static Logger getLogger(@NotNull PluginMeta meta) {
++        Logger logger = new PaperPluginLogger(meta);
 +        if (!LogManager.getLogManager().addLogger(logger)) {
 +            // Disable this if it's going to happen across reloads anyways...
 +            //logger.log(Level.WARNING, "Could not insert plugin logger - one was already found: {}", LogManager.getLogManager().getLogger(this.getName()));
-+            logger = LogManager.getLogManager().getLogger(description.getPrefix() != null ? description.getPrefix() : description.getName());
++            logger = LogManager.getLogManager().getLogger(meta.getLoggerPrefix() != null ? meta.getLoggerPrefix() : meta.getName());
 +        }
 +
 +        return logger;
 +    }
 +
-+    private PaperPluginLogger(@NotNull PluginDescriptionFile description) {
-+        super(description.getPrefix() != null ? description.getPrefix() : description.getName(), null);
++    private PaperPluginLogger(@NotNull PluginMeta meta) {
++        super(meta.getLoggerPrefix() != null ? meta.getLoggerPrefix() : meta.getName(), null);
 +    }
 +
 +    @Override
@@ -68,16 +75,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private FileConfiguration newConfig = null;
      private File configFile = null;
 -    private Logger logger = null; // Paper - PluginLogger -> Logger
-+    Logger logger = null; // Paper - PluginLogger -> Logger, package-private
++    public Logger logger = null; // Paper - PluginLogger -> Logger, public
  
      public JavaPlugin() {
-         final ClassLoader classLoader = this.getClass().getClassLoader();
+         // Paper start
 @@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
-         this.dataFolder = dataFolder;
          this.classLoader = classLoader;
          this.configFile = new File(dataFolder, "config.yml");
--        // Paper - Handle plugin prefix in implementation
--        this.logger = Logger.getLogger(description.getPrefix() != null ? description.getPrefix() : description.getName());
+         this.pluginMeta = configuration; // Paper
+-        this.logger = Logger.getLogger(description.getPrefix() != null ? description.getPrefix() : description.getName()); // Paper - Handle plugin prefix in implementation
 +        // Paper start
 +        if (this.logger == null) {
 +            this.logger = com.destroystokyo.paper.utils.PaperPluginLogger.getLogger(this.description);
@@ -90,28 +96,20 @@ diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/m
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
-     private JavaPlugin pluginInit;
-     private IllegalStateException pluginState;
-     private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
-+    private java.util.logging.Logger logger; // Paper - add field
- 
-     static {
-         ClassLoader.registerAsParallelCapable();
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
+@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
          this.url = file.toURI().toURL();
          this.libraryLoader = libraryLoader;
  
+-
 +        this.logger = com.destroystokyo.paper.utils.PaperPluginLogger.getLogger(description); // Paper - Register logger early
-+
-         try {
-             Class<?> jarClass;
-             try {
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
+         // Paper start
+         this.classLoaderGroup = io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage.instance().registerSpigotGroup(this); // Paper
+         this.dependencyContext = dependencyContext;
+@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
          pluginState = new IllegalStateException("Initial initialization");
          this.pluginInit = javaPlugin;
  
 +        javaPlugin.logger = this.logger; // Paper - set logger
-         javaPlugin.init(loader, loader.server, description, dataFolder, file, this);
+         javaPlugin.init(null, org.bukkit.Bukkit.getServer(), description, dataFolder, file, this); // Paper
      }
- }
+ 
diff --git a/patches/api/Adventure.patch b/patches/api/Adventure.patch
index 175246c58c..26af193a6c 100644
--- a/patches/api/Adventure.patch
+++ b/patches/api/Adventure.patch
@@ -1569,11 +1569,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      protected String usageMessage;
      private String permission;
 -    private String permissionMessage;
--    public org.spigotmc.CustomTimingsHandler timings; // Spigot
 +    private net.kyori.adventure.text.Component permissionMessage; // Paper
+     public org.spigotmc.CustomTimingsHandler timings; // Spigot
  
      protected Command(@NotNull String name) {
-         this(name, "", "/" + name, new ArrayList<String>());
 @@ -0,0 +0,0 @@ public abstract class Command {
  
          if (permissionMessage == null) {
diff --git a/patches/api/Also-load-resources-from-LibraryLoader.patch b/patches/api/Also-load-resources-from-LibraryLoader.patch
index 8457d39fe3..bc1cd06028 100644
--- a/patches/api/Also-load-resources-from-LibraryLoader.patch
+++ b/patches/api/Also-load-resources-from-LibraryLoader.patch
@@ -8,7 +8,7 @@ diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/m
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
+@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
  
      @Override
      public URL getResource(String name) {
@@ -43,6 +43,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end
 +
+     // Paper start
      @Override
-     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
-         return loadClass0(name, resolve, true, true);
+     public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException {
diff --git a/patches/api/Automatically-disable-plugins-that-fail-to-load.patch b/patches/api/Automatically-disable-plugins-that-fail-to-load.patch
deleted file mode 100644
index f957932d0a..0000000000
--- a/patches/api/Automatically-disable-plugins-that-fail-to-load.patch
+++ /dev/null
@@ -1,21 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Mon, 29 Feb 2016 19:45:21 -0600
-Subject: [PATCH] Automatically disable plugins that fail to load
-
-
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
-                 jPlugin.setEnabled(true);
-             } catch (Throwable ex) {
-                 server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
-+                // Paper start - Disable plugins that fail to load
-+                this.server.getPluginManager().disablePlugin(jPlugin);
-+                return;
-+                // Paper end
-             }
- 
-             // Perhaps abort here, rather than continue going, but as it stands,
diff --git a/patches/api/Close-Plugin-Class-Loaders-on-Disable.patch b/patches/api/Close-Plugin-Class-Loaders-on-Disable.patch
deleted file mode 100644
index accef0b3c3..0000000000
--- a/patches/api/Close-Plugin-Class-Loaders-on-Disable.patch
+++ /dev/null
@@ -1,105 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Tue, 1 May 2018 21:33:35 -0400
-Subject: [PATCH] Close Plugin Class Loaders on Disable
-
-This should close more memory leaks from /reload and disabling plugins,
-by closing the class loader and the jar file.
-
-Note: This patch is no longer necessary as upstream now also closes
-PluginClassLoaders on disable. This patch is now only to keep around
-API methods it previously added, and to add back the log message when a
-PluginClassLoader fails to disable, as upstream decided to ignore the
-exception.
-
-diff --git a/src/main/java/org/bukkit/plugin/PluginLoader.java b/src/main/java/org/bukkit/plugin/PluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/PluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/PluginLoader.java
-@@ -0,0 +0,0 @@ public interface PluginLoader {
-      * @param plugin Plugin to disable
-      */
-     public void disablePlugin(@NotNull Plugin plugin);
-+
-+    // Paper start - close Classloader on disable
-+    /**
-+     * This method is no longer useful as upstream has
-+     * made it so plugin classloaders are always closed on disable.
-+     * Use {@link #disablePlugin(Plugin)} instead.
-+     *
-+     * @param plugin Plugin to disable
-+     * @param closeClassloader unused
-+     * @deprecated Classloader is always closed by upstream now.
-+     */
-+    @Deprecated(forRemoval = true)
-+    // provide default to allow other PluginLoader implementations to work
-+    default public void disablePlugin(@NotNull Plugin plugin, boolean closeClassloader) {
-+        disablePlugin(plugin);
-+    }
-+    // Paper end - close Classloader on disable
- }
-diff --git a/src/main/java/org/bukkit/plugin/PluginManager.java b/src/main/java/org/bukkit/plugin/PluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/PluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/PluginManager.java
-@@ -0,0 +0,0 @@ public interface PluginManager {
-      */
-     public void disablePlugin(@NotNull Plugin plugin);
- 
-+    // Paper start - close Classloader on disable
-+    /**
-+     * This method is no longer useful as upstream has
-+     * made it so plugin classloaders are always closed on disable.
-+     * Use {@link #disablePlugin(Plugin)} instead.
-+     *
-+     * @param plugin Plugin to disable
-+     * @param closeClassloader unused
-+     * @deprecated Classloader is always closed by upstream now.
-+     */
-+    @Deprecated(forRemoval = true)
-+    public default void disablePlugin(@NotNull Plugin plugin, boolean closeClassloader) {
-+        this.disablePlugin(plugin);
-+    }
-+    // Paper end - close Classloader on disable
-+
-     /**
-      * Gets a {@link Permission} from its fully qualified name
-      *
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-         }
-     }
- 
-+    // Paper start
-+    /**
-+     * This method is no longer useful as upstream has
-+     * made it so plugin classloaders are always closed on disable.
-+     * Use {@link #disablePlugins()} instead.
-+     *
-+     * @param closeClassloaders unused
-+     * @deprecated Classloader is always closed by upstream now.
-+     */
-+    @Deprecated(forRemoval = true)
-+    public void disablePlugins(boolean closeClassloaders) {
-+        this.disablePlugins();
-+    }
-+    // Paper end
-+
-     @Override
-     public void disablePlugin(@NotNull final Plugin plugin) {
-         if (plugin.isEnabled()) {
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
-                     loader.close();
-                 } catch (IOException ex) {
-                     //
-+                    this.server.getLogger().log(Level.WARNING, "Error closing the PluginClassLoader for '" + plugin.getDescription().getFullName() + "'", ex); // Paper - log exception
-                 }
-             }
-         }
diff --git a/patches/api/Disable-Sync-Events-firing-Async-errors-during-shutd.patch b/patches/api/Disable-Sync-Events-firing-Async-errors-during-shutd.patch
deleted file mode 100644
index 56f396dc00..0000000000
--- a/patches/api/Disable-Sync-Events-firing-Async-errors-during-shutd.patch
+++ /dev/null
@@ -1,25 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Sat, 11 Apr 2020 21:38:59 -0400
-Subject: [PATCH] Disable Sync Events firing Async errors during shutdown
-
-This is how it use to behave on Paper, and this is totally destroying
-the ability to try to shut the server down gracefully during the
-shutdown process as events firing on the watchdog thread are throwing
-errors.
-
-This isn't an issue on Spigot
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-         // Paper - replace callEvent by merging to below method
-         if (event.isAsynchronous() && server.isPrimaryThread()) {
-             throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
--        } else if (!event.isAsynchronous() && !server.isPrimaryThread()) {
-+        } else if (!event.isAsynchronous() && !server.isPrimaryThread() && !server.isStopping() ) {
-             throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
-         }
- 
diff --git a/patches/api/Don-t-load-plugins-prefixed-with-a-dot.patch b/patches/api/Don-t-load-plugins-prefixed-with-a-dot.patch
deleted file mode 100644
index faf72711b0..0000000000
--- a/patches/api/Don-t-load-plugins-prefixed-with-a-dot.patch
+++ /dev/null
@@ -1,18 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Noah van der Aa <ndvdaa@gmail.com>
-Date: Sat, 22 Jan 2022 16:35:44 +0100
-Subject: [PATCH] Don't load plugins prefixed with a dot
-
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-         final List<File> pluginJars = new ArrayList<>(java.util.Arrays.asList(directory.listFiles()));
-         pluginJars.addAll(extraPluginJars);
-         for (File file : pluginJars) {
-+            if (file.getName().startsWith(".") && !extraPluginJars.contains(file)) continue; // Don't load plugin if the file name starts with a dot, except if it's a extra plugin jar.
-             // Paper end
-             PluginLoader loader = null;
-             for (Pattern filter : filters) {
diff --git a/patches/api/Enable-multi-release-plugin-jars.patch b/patches/api/Enable-multi-release-plugin-jars.patch
index a9230e6211..21890c1e89 100644
--- a/patches/api/Enable-multi-release-plugin-jars.patch
+++ b/patches/api/Enable-multi-release-plugin-jars.patch
@@ -8,7 +8,7 @@ diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/m
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
+@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
          this.description = description;
          this.dataFolder = dataFolder;
          this.file = file;
diff --git a/patches/api/Fix-plugin-provides-load-order.patch b/patches/api/Fix-plugin-provides-load-order.patch
deleted file mode 100644
index 8068cedb1c..0000000000
--- a/patches/api/Fix-plugin-provides-load-order.patch
+++ /dev/null
@@ -1,27 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Nassim Jahnke <nassim@njahnke.dev>
-Date: Fri, 1 Oct 2021 09:47:00 +0200
-Subject: [PATCH] Fix plugin provides load order
-
-Fixes https://hub.spigotmc.org/jira/browse/SPIGOT-6740
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                             // Paper end
-                             missingDependency = false;
-                             pluginIterator.remove();
-+                            pluginsProvided.values().removeIf(s -> s.equals(plugin)); // Paper - remove provided plugins
-                             softDependencies.remove(plugin);
-                             dependencies.remove(plugin);
- 
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                     // We're clear to load, no more soft or hard dependencies left
-                     File file = plugins.get(plugin);
-                     pluginIterator.remove();
-+                    pluginsProvided.values().removeIf(s -> s.equals(plugin)); // Paper - remove provided plugins
-                     missingDependency = false;
- 
-                     try {
diff --git a/patches/api/Future-API-Plans.patch b/patches/api/Future-API-Plans.patch
deleted file mode 100644
index 1fae1cab9c..0000000000
--- a/patches/api/Future-API-Plans.patch
+++ /dev/null
@@ -1,67 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
-Date: Wed, 7 Dec 2022 19:12:54 -0500
-Subject: [PATCH] Future API Plans
-
-
-diff --git a/src/main/java/org/bukkit/plugin/Plugin.java b/src/main/java/org/bukkit/plugin/Plugin.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/Plugin.java
-+++ b/src/main/java/org/bukkit/plugin/Plugin.java
-@@ -0,0 +0,0 @@ public interface Plugin extends TabExecutor {
-      *
-      * @return PluginLoader that controls this plugin
-      */
-+    @Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future
-     @NotNull
-     public PluginLoader getPluginLoader();
- 
-diff --git a/src/main/java/org/bukkit/plugin/PluginLoader.java b/src/main/java/org/bukkit/plugin/PluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/PluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/PluginLoader.java
-@@ -0,0 +0,0 @@ import org.jetbrains.annotations.NotNull;
-  * Represents a plugin loader, which handles direct access to specific types
-  * of plugins
-  */
-+@Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future
- public interface PluginLoader {
- 
-     /**
-diff --git a/src/main/java/org/bukkit/plugin/PluginManager.java b/src/main/java/org/bukkit/plugin/PluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/PluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/PluginManager.java
-@@ -0,0 +0,0 @@ public interface PluginManager {
-      * @throws IllegalArgumentException Thrown when the given Class is not a
-      *     valid PluginLoader
-      */
-+    @Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future
-     public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException;
- 
-     /**
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ import org.jetbrains.annotations.Nullable;
- /**
-  * Handles all plugin management from the Server
-  */
-+@Deprecated(forRemoval = true) // Paper - This implementation may be replaced in a future version of Paper.
-+// Plugins may still reflect into this class to modify permission logic for the time being.
- public final class SimplePluginManager implements PluginManager {
-     private final Server server;
-     private final Map<Pattern, PluginLoader> fileAssociations = new HashMap<Pattern, PluginLoader>();
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ import org.yaml.snakeyaml.error.YAMLException;
- /**
-  * Represents a Java plugin loader, allowing plugins in the form of .jar
-  */
-+@Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future. This implementation will be moved.
- public final class JavaPluginLoader implements PluginLoader {
-     final Server server;
-     private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization"); // Paper
diff --git a/patches/api/Handle-plugin-prefixes-in-implementation-logging-con.patch b/patches/api/Handle-plugin-prefixes-in-implementation-logging-con.patch
index 423278a7a5..1877c6a79c 100644
--- a/patches/api/Handle-plugin-prefixes-in-implementation-logging-con.patch
+++ b/patches/api/Handle-plugin-prefixes-in-implementation-logging-con.patch
@@ -28,14 +28,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private Logger logger = null; // Paper - PluginLogger -> Logger
  
      public JavaPlugin() {
-         final ClassLoader classLoader = this.getClass().getClassLoader();
+         // Paper start
 @@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
          this.dataFolder = dataFolder;
          this.classLoader = classLoader;
          this.configFile = new File(dataFolder, "config.yml");
 -        this.logger = new PluginLogger(this);
-+        // Paper - Handle plugin prefix in implementation
-+        this.logger = Logger.getLogger(description.getPrefix() != null ? description.getPrefix() : description.getName());
+         this.pluginMeta = configuration; // Paper
++        this.logger = Logger.getLogger(description.getPrefix() != null ? description.getPrefix() : description.getName()); // Paper - Handle plugin prefix in implementation
      }
  
      /**
diff --git a/patches/api/List-all-missing-hard-depends-not-just-first.patch b/patches/api/List-all-missing-hard-depends-not-just-first.patch
deleted file mode 100644
index 622b6742e0..0000000000
--- a/patches/api/List-all-missing-hard-depends-not-just-first.patch
+++ /dev/null
@@ -1,91 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Jake Potrebic <jake.m.potrebic@gmail.com>
-Date: Tue, 18 May 2021 10:38:10 -0700
-Subject: [PATCH] List all missing hard depends not just first
-
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
- 
-                 if (dependencies.containsKey(plugin)) {
-                     Iterator<String> dependencyIterator = dependencies.get(plugin).iterator();
-+                    final Set<String> missingHardDependencies = new HashSet<>(dependencies.get(plugin).size()); // Paper - list all missing hard depends
- 
-                     while (dependencyIterator.hasNext()) {
-                         String dependency = dependencyIterator.next();
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
- 
-                         // We have a dependency not found
-                         } else if (!plugins.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) {
-+                            // Paper start
-+                            missingHardDependencies.add(dependency);
-+                        }
-+                    }
-+                    if (!missingHardDependencies.isEmpty()) {
-+                            // Paper end
-                             missingDependency = false;
-                             pluginIterator.remove();
-                             softDependencies.remove(plugin);
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-                             server.getLogger().log(
-                                 Level.SEVERE,
-                                 "Could not load '" + entry.getValue().getPath() + "' in folder '" + entry.getValue().getParentFile().getPath() + "'", // Paper
--                                new UnknownDependencyException("Unknown dependency " + dependency + ". Please download and install " + dependency + " to run this plugin."));
--                            break;
--                        }
-+                                new UnknownDependencyException(missingHardDependencies, plugin)); // Paper
-                     }
- 
-                     if (dependencies.containsKey(plugin) && dependencies.get(plugin).isEmpty()) {
-diff --git a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
-+++ b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
-@@ -0,0 +0,0 @@ public class UnknownDependencyException extends RuntimeException {
-         super(message);
-     }
- 
-+    // Paper start
-+    /**
-+     * Create a new {@link UnknownDependencyException} with a message informing
-+     * about which dependencies are missing for what plugin.
-+     *
-+     * @param missingDependencies missing dependencies
-+     * @param pluginName plugin which is missing said dependencies
-+     */
-+    public UnknownDependencyException(final @org.jetbrains.annotations.NotNull java.util.Collection<String> missingDependencies, final @org.jetbrains.annotations.NotNull String pluginName) {
-+        this("Unknown/missing dependency plugins: [" + String.join(", ", missingDependencies) + "]. Please download and install these plugins to run '" + pluginName + "'.");
-+    }
-+    // Paper end
-+
-     /**
-      * Constructs a new UnknownDependencyException based on the given
-      * Exception
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
-             ));
-         }
- 
-+        Set<String> missingHardDependencies = new HashSet<>(description.getDepend().size()); // Paper - list all missing hard depends
-         for (final String pluginName : description.getDepend()) {
-             Plugin current = server.getPluginManager().getPlugin(pluginName);
- 
-             if (current == null) {
--                throw new UnknownDependencyException("Unknown dependency " + pluginName + ". Please download and install " + pluginName + " to run this plugin.");
-+                missingHardDependencies.add(pluginName); // Paper - list all missing hard depends
-             }
-         }
-+        // Paper start - list all missing hard depends
-+        if (!missingHardDependencies.isEmpty()) {
-+            throw new UnknownDependencyException(missingHardDependencies, description.getFullName());
-+        }
-+        // Paper end
- 
-         server.getUnsafe().checkSupported(description);
- 
diff --git a/patches/api/Make-JavaPluginLoader-thread-safe.patch b/patches/api/Make-JavaPluginLoader-thread-safe.patch
deleted file mode 100644
index 33c26d2809..0000000000
--- a/patches/api/Make-JavaPluginLoader-thread-safe.patch
+++ /dev/null
@@ -1,53 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Trigary <trigary0@gmail.com>
-Date: Wed, 15 Apr 2020 01:24:55 -0400
-Subject: [PATCH] Make JavaPluginLoader thread-safe
-
-
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ import org.yaml.snakeyaml.error.YAMLException;
- public final class JavaPluginLoader implements PluginLoader {
-     final Server server;
-     private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")};
-+    private final Map<String, java.util.concurrent.locks.ReentrantReadWriteLock> classLoadLock = new java.util.HashMap<String, java.util.concurrent.locks.ReentrantReadWriteLock>(); // Paper
-+    private final Map<String, Integer> classLoadLockCount = new java.util.HashMap<String, Integer>(); // Paper
-     private final List<PluginClassLoader> loaders = new CopyOnWriteArrayList<PluginClassLoader>();
-     private final LibraryLoader libraryLoader;
- 
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
- 
-     @Nullable
-     Class<?> getClassByName(final String name, boolean resolve, PluginDescriptionFile description) {
-+        // Paper start - make MT safe
-+        java.util.concurrent.locks.ReentrantReadWriteLock lock;
-+        synchronized (classLoadLock) {
-+            lock = classLoadLock.computeIfAbsent(name, (x) -> new java.util.concurrent.locks.ReentrantReadWriteLock());
-+            classLoadLockCount.compute(name, (x, prev) -> prev != null ? prev + 1 : 1);
-+        }
-+        lock.writeLock().lock();try {
-+        // Paper end
-         for (PluginClassLoader loader : loaders) {
-             try {
-                 return loader.loadClass0(name, resolve, false, ((SimplePluginManager) server.getPluginManager()).isTransitiveDepend(description, loader.plugin.getDescription()));
-             } catch (ClassNotFoundException cnfe) {
-             }
-         }
-+        // Paper start - make MT safe
-+        } finally {
-+            synchronized (classLoadLock) {
-+                lock.writeLock().unlock();
-+                if (classLoadLockCount.get(name) == 1) {
-+                    classLoadLock.remove(name);
-+                    classLoadLockCount.remove(name);
-+                } else {
-+                    classLoadLockCount.compute(name, (x, prev) -> prev - 1);
-+                }
-+            }
-+        }
-+        // Paper end
-         return null;
-     }
- 
diff --git a/patches/api/Make-plugins-list-alphabetical.patch b/patches/api/Make-plugins-list-alphabetical.patch
deleted file mode 100644
index b714733ba0..0000000000
--- a/patches/api/Make-plugins-list-alphabetical.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: BillyGalbreath <Blake.Galbreath@GMail.com>
-Date: Mon, 31 Jul 2017 02:08:55 -0500
-Subject: [PATCH] Make /plugins list alphabetical
-
-
-diff --git a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
-+++ b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
-@@ -0,0 +0,0 @@ package org.bukkit.command.defaults;
- import java.util.Arrays;
- import java.util.Collections;
- import java.util.List;
-+import java.util.Map;
-+import java.util.TreeMap;
-+
- import org.bukkit.Bukkit;
- import org.bukkit.ChatColor;
- import org.bukkit.command.CommandSender;
-@@ -0,0 +0,0 @@ public class PluginsCommand extends BukkitCommand {
- 
-     @NotNull
-     private String getPluginList() {
--        StringBuilder pluginList = new StringBuilder();
--        Plugin[] plugins = Bukkit.getPluginManager().getPlugins();
-+        // Paper start
-+        TreeMap<String, Plugin> plugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
-+
-+        for (Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
-+            plugins.put(plugin.getDescription().getName(), plugin);
-+        }
- 
--        for (Plugin plugin : plugins) {
-+        StringBuilder pluginList = new StringBuilder();
-+        for (Map.Entry<String, Plugin> entry : plugins.entrySet()) {
-             if (pluginList.length() > 0) {
-                 pluginList.append(ChatColor.WHITE);
-                 pluginList.append(", ");
-             }
- 
-+            Plugin plugin = entry.getValue();
-+            
-             pluginList.append(plugin.isEnabled() ? ChatColor.GREEN : ChatColor.RED);
-             pluginList.append(plugin.getDescription().getName());
- 
-@@ -0,0 +0,0 @@ public class PluginsCommand extends BukkitCommand {
-             }
-         }
- 
--        return "(" + plugins.length + "): " + pluginList.toString();
-+        return "(" + plugins.size() + "): " + pluginList.toString();
-+        // Paper end
-     }
-+
- }
diff --git a/patches/api/Paper-Plugins.patch b/patches/api/Paper-Plugins.patch
new file mode 100644
index 0000000000..33542a2054
--- /dev/null
+++ b/patches/api/Paper-Plugins.patch
@@ -0,0 +1,2494 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
+Date: Wed, 6 Jul 2022 23:00:36 -0400
+Subject: [PATCH] Paper Plugins
+
+
+diff --git a/build.gradle.kts b/build.gradle.kts
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -0,0 +0,0 @@ dependencies {
+     implementation("org.ow2.asm:asm-commons:9.2")
+     // Paper end
+ 
+-    compileOnly("org.apache.maven:maven-resolver-provider:3.8.5")
++    api("org.apache.maven:maven-resolver-provider:3.8.5") // Paper, expose
+     compileOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.7.3")
+     compileOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.7.3")
+     compileOnly("com.google.code.findbugs:jsr305:1.3.9") // Paper
+diff --git a/src/main/java/io/papermc/paper/plugin/PermissionManager.java b/src/main/java/io/papermc/paper/plugin/PermissionManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/PermissionManager.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.List;
++import java.util.Set;
++
++/**
++ * A permission manager implementation to keep backwards compatibility partially alive with existing plugins that used
++ * the bukkit one before.
++ */
++@ApiStatus.Experimental
++public interface PermissionManager {
++
++    /**
++     * Gets a {@link Permission} from its fully qualified name
++     *
++     * @param name Name of the permission
++     * @return Permission, or null if none
++     */
++    @Nullable
++    Permission getPermission(@NotNull String name);
++
++    /**
++     * Adds a {@link Permission} to this plugin manager.
++     * <p>
++     * If a permission is already defined with the given name of the new
++     * permission, an exception will be thrown.
++     *
++     * @param perm Permission to add
++     * @throws IllegalArgumentException Thrown when a permission with the same
++     *                                  name already exists
++     */
++    void addPermission(@NotNull Permission perm);
++
++    /**
++     * Removes a {@link Permission} registration from this plugin manager.
++     * <p>
++     * If the specified permission does not exist in this plugin manager,
++     * nothing will happen.
++     * <p>
++     * Removing a permission registration will <b>not</b> remove the
++     * permission from any {@link Permissible}s that have it.
++     *
++     * @param perm Permission to remove
++     */
++    void removePermission(@NotNull Permission perm);
++
++    /**
++     * Removes a {@link Permission} registration from this plugin manager.
++     * <p>
++     * If the specified permission does not exist in this plugin manager,
++     * nothing will happen.
++     * <p>
++     * Removing a permission registration will <b>not</b> remove the
++     * permission from any {@link Permissible}s that have it.
++     *
++     * @param name Permission to remove
++     */
++    void removePermission(@NotNull String name);
++
++    /**
++     * Gets the default permissions for the given op status
++     *
++     * @param op Which set of default permissions to get
++     * @return The default permissions
++     */
++    @NotNull
++    Set<Permission> getDefaultPermissions(boolean op);
++
++    /**
++     * Recalculates the defaults for the given {@link Permission}.
++     * <p>
++     * This will have no effect if the specified permission is not registered
++     * here.
++     *
++     * @param perm Permission to recalculate
++     */
++    void recalculatePermissionDefaults(@NotNull Permission perm);
++
++    /**
++     * Subscribes the given Permissible for information about the requested
++     * Permission, by name.
++     * <p>
++     * If the specified Permission changes in any form, the Permissible will
++     * be asked to recalculate.
++     *
++     * @param permission  Permission to subscribe to
++     * @param permissible Permissible subscribing
++     */
++    void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible);
++
++    /**
++     * Unsubscribes the given Permissible for information about the requested
++     * Permission, by name.
++     *
++     * @param permission  Permission to unsubscribe from
++     * @param permissible Permissible subscribing
++     */
++    void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible);
++
++    /**
++     * Gets a set containing all subscribed {@link Permissible}s to the given
++     * permission, by name
++     *
++     * @param permission Permission to query for
++     * @return Set containing all subscribed permissions
++     */
++    @NotNull
++    Set<Permissible> getPermissionSubscriptions(@NotNull String permission);
++
++    /**
++     * Subscribes to the given Default permissions by operator status
++     * <p>
++     * If the specified defaults change in any form, the Permissible will be
++     * asked to recalculate.
++     *
++     * @param op          Default list to subscribe to
++     * @param permissible Permissible subscribing
++     */
++    void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible);
++
++    /**
++     * Unsubscribes from the given Default permissions by operator status
++     *
++     * @param op          Default list to unsubscribe from
++     * @param permissible Permissible subscribing
++     */
++    void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible);
++
++    /**
++     * Gets a set containing all subscribed {@link Permissible}s to the given
++     * default list, by op status
++     *
++     * @param op Default list to query for
++     * @return Set containing all subscribed permissions
++     */
++    @NotNull
++    Set<Permissible> getDefaultPermSubscriptions(boolean op);
++
++    /**
++     * Gets a set of all registered permissions.
++     * <p>
++     * This set is a copy and will not be modified live.
++     *
++     * @return Set containing all current registered permissions
++     */
++    @NotNull
++    Set<Permission> getPermissions();
++
++    /**
++     * Adds a list of permissions.
++     * <p>
++     * This is meant as an optimization for adding multiple permissions without recalculating each permission.
++     *
++     * @param perm permission
++     */
++    void addPermissions(@NotNull List<Permission> perm);
++
++    /**
++     * Clears the current registered permissinos.
++     * <p>
++     * This is used for reloading.
++     */
++    void clearPermissions();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.bootstrap;
++
++import io.papermc.paper.plugin.provider.util.ProviderUtil;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * A plugin boostrap is meant for loading certain parts of the plugin before the server is loaded.
++ * <p>
++ * Plugin bootstrapping allows values to be initialized in certain parts of the server that might not be allowed
++ * when the server is running.
++ * <p>
++ * Your bootstrap class will be on the same classloader as your JavaPlugin.
++ * <p>
++ * <b>All calls to Bukkit may throw a NullPointerExceptions or return null unexpectedly. You should only call api methods that are explicitly documented to work in the bootstrapper</b>
++ */
++@ApiStatus.OverrideOnly
++@ApiStatus.Experimental
++public interface PluginBootstrap {
++
++    /**
++     * Called by the server, allowing you to bootstrap the plugin with a context that provides things like a logger and your shared plugin configuration file.
++     *
++     * @param context the server provided context
++     */
++    void bootstrap(@NotNull PluginProviderContext context);
++
++    /**
++     * Called by the server to instantiate your main class.
++     * Plugins may override this logic to define custom creation logic for said instance, like passing addition
++     * constructor arguments.
++     *
++     * @param context the server created bootstrap object
++     * @return the server requested instance of the plugins main class.
++     */
++    @NotNull
++    default JavaPlugin createPlugin(@NotNull PluginProviderContext context) {
++        return ProviderUtil.loadClass(context.getConfiguration().getMainClass(), JavaPlugin.class, this.getClass().getClassLoader());
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.bootstrap;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.logging.Logger;
++
++/**
++ * Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin
++ * instanciation logic.
++ * A boostrap context may be used to access data or logic usually provided to {@link org.bukkit.plugin.Plugin} instances
++ * like the plugin's configuration or logger during the plugins bootstrap.
++ */
++@ApiStatus.NonExtendable
++@ApiStatus.Experimental
++public interface PluginProviderContext {
++
++    /**
++     * Provides the plugin's configuration.
++     *
++     * @return the plugin's configuration
++     */
++    @NotNull
++    PluginMeta getConfiguration();
++
++    /**
++     * Provides the path to the data directory of the plugin.
++     *
++     * @return the previously described path
++     */
++    @NotNull
++    Path getDataDirectory();
++
++    /**
++     * Provides the logger used for this plugin.
++     *
++     * @return the logger instance
++     */
++    @NotNull
++    Logger getLogger();
++
++    /**
++     * Provides the SLF4J logger assigned to this plugin.
++     *
++     * @return SLF4J logger
++     */
++    @NotNull
++    default org.slf4j.Logger getSLF4JLogger() {
++        return org.slf4j.LoggerFactory.getLogger(this.getLogger().getName());
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java b/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.configuration;
++
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.bukkit.plugin.PluginLoadOrder;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.List;
++
++/**
++ * This class acts as an abstraction for a plugin configuration.
++ */
++@ApiStatus.NonExtendable
++@ApiStatus.Experimental // Subject to change!
++public interface PluginMeta {
++
++    /**
++     * Provides the name of the plugin. This name uniquely identifies the plugin amongst all loaded plugins on the
++     * server.
++     * <ul>
++     * <li>Will only contain alphanumeric characters, underscores, hyphens,
++     *     and periods: [a-zA-Z0-9_\-\.].
++     * <li>Typically used for identifying the plugin data folder.
++     * <li>The name also acts as the token referenced in {@link #getPluginDependencies()},
++     * {@link #getPluginSoftDependencies()}, and {@link #getLoadBeforePlugins()}.
++     * </ul>
++     * <p>
++     * In the plugin.yml, this entry is named <code>name</code>.
++     * <p>
++     * Example:<blockquote><pre>name: MyPlugin</pre></blockquote>
++     *
++     * @return the name of the plugin
++     */
++    @NotNull
++    String getName();
++
++    /**
++     * Returns the display name of the plugin, including the version.
++     *
++     * @return a descriptive name of the plugin and respective version
++     */
++    @NotNull
++    default String getDisplayName() {
++        return this.getName() + " v" + this.getVersion();
++    }
++
++    /**
++     * Provides the fully qualified class name of the main class for the plugin.
++     * A subtype of {@link JavaPlugin} is expected at this location.
++     *
++     * @return the fully qualified class name of the plugin's main class.
++     */
++    @NotNull
++    String getMainClass();
++
++    /**
++     * Returns the phase of the server startup logic that the plugin should be loaded.
++     *
++     * @return the plugin load order
++     * @see PluginLoadOrder for further details regards the available load orders.
++     */
++    @NotNull
++    PluginLoadOrder getLoadOrder();
++
++    /**
++     * Provides the version of this plugin as defined by the plugin.
++     * There is no inherit format defined/enforced for the version of a plugin, however a common approach
++     * might be schematic versioning.
++     *
++     * @return the string representation of the plugin's version
++     */
++    @NotNull
++    String getVersion();
++
++    /**
++     * Provides the prefix that should be used for the plugin logger.
++     * The logger prefix allows plugins to overwrite the usual default of the logger prefix, which is the name of the
++     * plugin.
++     *
++     * @return the specific overwrite of the logger prefix as defined by the plugin. If the plugin did not define a
++     *     custom logger prefix, this method will return null
++     */
++    @Nullable
++    String getLoggerPrefix();
++
++    /**
++     * Provides a list of dependencies that are required for this plugin to load.
++     * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, of the
++     * dependencies.
++     * <p>
++     * If any of the dependencies defined by this list are not installed on the server, this plugin will fail to load.
++     *
++     * @return an immutable list of required dependency names
++     */
++    @NotNull
++    List<String> getPluginDependencies();
++
++    /**
++     * Provides a list of dependencies that are used but not required by this plugin.
++     * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, of the soft
++     * dependencies.
++     * <p>
++     * If these dependencies are installed on the server, they will be loaded first and supplied as dependencies to this
++     * plugin, however the plugin will load even if these dependencies are not installed.
++     *
++     * @return immutable list of soft dependencies
++     */
++    @NotNull
++    List<String> getPluginSoftDependencies();
++
++    /**
++     * Provides a list of plugins that should be loaded before this plugin is loaded.
++     * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, of the
++     * plugins that should be loaded before the plugin described by this plugin meta.
++     * <p>
++     * The plugins referenced in the list provided by this method are not considered dependencies of this plugin and
++     * are hence not available to the plugin at runtime. They merely load before this plugin.
++     *
++     * @return immutable list of plugins to load before this plugin
++     */
++    @NotNull
++    List<String> getLoadBeforePlugins();
++
++    /**
++     * Returns the list of plugins/dependencies that this plugin provides.
++     * The list holds the unique identifiers, following the constraints laid out in {@link #getName()}, for each plugin
++     * it provides the expected classes for.
++     *
++     * @return immutable list of provided plugins/dependencies
++     */
++    @NotNull
++    List<String> getProvidedPlugins();
++
++    /**
++     * Provides the list of authors that are credited with creating this plugin.
++     * The author names are in no particular format.
++     *
++     * @return an immutable list of the plugin's authors
++     */
++    @NotNull
++    List<String> getAuthors();
++
++    /**
++     * Provides a list of contributors that contributed to the plugin but are not considered authors.
++     * The names of the contributors are in no particular format.
++     *
++     * @return an immutable list of the plugin's contributors
++     */
++    @NotNull
++    List<String> getContributors();
++
++    /**
++     * Gives a human-friendly description of the functionality the plugin
++     * provides.
++     *
++     * @return description or null if the plugin did not define a human readable description.
++     */
++    @Nullable
++    String getDescription();
++
++    /**
++     * Provides the website for the plugin or the plugin's author.
++     * The defined string value is <b>not guaranteed</b> to be in the form of a url.
++     *
++     * @return a string representation of the website that serves as the main hub for this plugin/its author.
++     */
++    @Nullable
++    String getWebsite();
++
++    /**
++     * Provides the list of permissions that are defined via the plugin meta instance.
++     *
++     * @return an immutable list of permissions
++     */
++    // TODO: Do we even want this? Why not just use the bootstrapper
++    @NotNull
++    List<Permission> getPermissions();
++
++    /**
++     * Provides the default values that apply to the permissions defined in this plugin meta.
++     *
++     * @return the bukkit permission default container.
++     * @see #getPermissions()
++     */
++    // TODO: Do we even want this? Why not just use the bootstrapper
++    @NotNull
++    PermissionDefault getPermissionDefault();
++
++    /**
++     * Gets the api version that this plugin supports.
++     * Nullable if this version is not specified, and should be
++     * considered legacy (spigot plugins only)
++     *
++     * @return the version string made up of the major and minor version (e.g. 1.18 or 1.19). Minor versions like 1.18.2
++     * are unified to their major release version (in this example 1.18)
++     */
++    @Nullable
++    String getAPIVersion();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/configuration/package-info.java b/src/main/java/io/papermc/paper/plugin/configuration/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/configuration/package-info.java
+@@ -0,0 +0,0 @@
++/**
++ * The paper configuration package contains the new java representation of a plugins configuration file.
++ * While most values are described in detail on {@link io.papermc.paper.plugin.configuration.PluginMeta}, a full
++ * entry on the paper contains a full and extensive example of possible configurations of the paper-plugin.yml.
++ * @see <a href="https://docs.papermc.io/paper">Extensive documentation and examples of the paper-plugin.yml</a>
++ * <!--TODO update the documentation link once documentation for this exists and is deployed-->
++ */
++package io.papermc.paper.plugin.configuration;
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader;
++
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
++import io.papermc.paper.plugin.loader.library.LibraryStore;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Contract;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * A mutable builder that may be used to collect and register all {@link ClassPathLibrary} instances a
++ * {@link PluginLoader} aims to provide to its plugin at runtime.
++ */
++@ApiStatus.NonExtendable
++@ApiStatus.Experimental
++public interface PluginClasspathBuilder {
++
++    /**
++     * Adds a new classpath library to this classpath builder.
++     * <p>
++     * As a builder, this method does not invoke {@link ClassPathLibrary#register(LibraryStore)} and
++     * may hence be run without invoking potential IO performed by a {@link ClassPathLibrary} during resolution.
++     * <p>
++     * The paper api provides pre implemented {@link ClassPathLibrary} types that allow easy inclusion of existing
++     * libraries on disk or on remote maven repositories.
++     *
++     * @param classPathLibrary the library instance to add to this builder
++     * @return self
++     * @see io.papermc.paper.plugin.loader.library.impl.JarLibrary
++     * @see io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver
++     */
++    @NotNull
++    @Contract("_ -> this")
++    PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary);
++
++    @NotNull
++    PluginProviderContext getContext();
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java b/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * A plugin loader is responsible for creating certain aspects of a plugin before it is created.
++ * <p>
++ * The goal of the plugin loader is the creation of an expected/dynamic environment for the plugin to load into.
++ * This, as of right now, only applies to creating the expected classpath for the plugin, e.g. supplying external
++ * libraries to the plugin.
++ * <p>
++ * It should be noted that this class will be called from a different classloader, this will cause any static values
++ * set in this class/any other classes loaded not to persist when the plugin loads.
++ */
++@ApiStatus.OverrideOnly
++@ApiStatus.Experimental
++public interface PluginLoader {
++
++    /**
++     * Called by the server to allows plugins to configure the runtime classpath that the plugin is run on.
++     * This allows plugin loaders to configure dependencies for the plugin where jars can be downloaded or
++     * provided during runtime.
++     *
++     * @param classpathBuilder a mutable classpath builder that may be used to register custom runtime dependencies
++     *                         for the plugin the loader was registered for.
++     */
++    void classloader(@NotNull PluginClasspathBuilder classpathBuilder);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java b/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader.library;
++
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * The classpath library interface represents libraries that are capable of registering themselves via
++ * {@link #register(LibraryStore)} on any given {@link LibraryStore}.
++ */
++public interface ClassPathLibrary {
++
++    /**
++     * Called to register the library this class path library represents into the passed library store.
++     * This method may either be implemented by the plugins themselves if they need complex logic, or existing
++     * API exposed implementations of this interface may be used.
++     *
++     * @param store the library store instance to register this library into
++     * @throws LibraryLoadingException if library loading failed for this classpath library
++     */
++    void register(@NotNull LibraryStore store) throws LibraryLoadingException;
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader.library;
++
++/**
++ * Indicates that an exception has occured while loading a library.
++ */
++public class LibraryLoadingException extends RuntimeException {
++
++    public LibraryLoadingException(String s) {
++        super(s);
++    }
++
++    public LibraryLoadingException(String s, Exception e) {
++        super(s, e);
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader.library;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++
++/**
++ * Represents a storage that stores library jars.
++ * <p>
++ * The library store api allows plugins to register specific dependencies into their runtime classloader when their
++ * {@link io.papermc.paper.plugin.loader.PluginLoader} is processed.
++ *
++ * @see io.papermc.paper.plugin.loader.PluginLoader
++ */
++@ApiStatus.Internal
++public interface LibraryStore {
++
++    /**
++     * Adds the provided library path to this library store.
++     *
++     * @param library path to the libraries jar file on the disk
++     */
++    void addLibrary(@NotNull Path library);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java b/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader.library.impl;
++
++import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
++import io.papermc.paper.plugin.loader.library.LibraryLoadingException;
++import io.papermc.paper.plugin.loader.library.LibraryStore;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Files;
++import java.nio.file.Path;
++
++/**
++ * A simple jar library implementation of the {@link ClassPathLibrary} that allows {@link io.papermc.paper.plugin.loader.PluginLoader}s to
++ * append a jar stored on the local file system into their runtime classloader.
++ * <p>
++ * An example creation of the jar library type may look like this:
++ * <pre>{@code
++ *   final JarLibrary customLibrary = new JarLibrary(Path.of("libs/custom-library-1.24.jar"));
++ * }</pre>
++ * resulting in a jar library that provides the jar at {@code libs/custom-library-1.24.jar} to the plugins classloader
++ * at runtime.
++ * <p>
++ * The jar library implementation will error if file exists at the specified path.
++ */
++public class JarLibrary implements ClassPathLibrary {
++
++    private final Path path;
++
++    /**
++     * Creates a new jar library that references the jar file found at the provided path.
++     *
++     * @param path the path, relative to the JVMs start directory.
++     */
++    public JarLibrary(@NotNull Path path) {
++        this.path = path;
++    }
++
++    @Override
++    public void register(@NotNull LibraryStore store) throws LibraryLoadingException {
++        if (Files.notExists(this.path)) {
++            throw new LibraryLoadingException("Could not find library at " + this.path);
++        }
++
++        store.addLibrary(this.path);
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java b/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader.library.impl;
++
++import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
++import io.papermc.paper.plugin.loader.library.LibraryLoadingException;
++import io.papermc.paper.plugin.loader.library.LibraryStore;
++import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
++import org.eclipse.aether.DefaultRepositorySystemSession;
++import org.eclipse.aether.RepositorySystem;
++import org.eclipse.aether.collection.CollectRequest;
++import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
++import org.eclipse.aether.graph.Dependency;
++import org.eclipse.aether.impl.DefaultServiceLocator;
++import org.eclipse.aether.repository.LocalRepository;
++import org.eclipse.aether.repository.RemoteRepository;
++import org.eclipse.aether.repository.RepositoryPolicy;
++import org.eclipse.aether.resolution.ArtifactResult;
++import org.eclipse.aether.resolution.DependencyRequest;
++import org.eclipse.aether.resolution.DependencyResolutionException;
++import org.eclipse.aether.resolution.DependencyResult;
++import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
++import org.eclipse.aether.spi.connector.transport.TransporterFactory;
++import org.eclipse.aether.transfer.AbstractTransferListener;
++import org.eclipse.aether.transfer.TransferCancelledException;
++import org.eclipse.aether.transfer.TransferEvent;
++import org.eclipse.aether.transport.http.HttpTransporterFactory;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.File;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++
++/**
++ * The maven library resolver acts as a resolver for yet to be resolved jar libraries that may be pulled from a
++ * remote maven repository.
++ * <p>
++ * Plugins may create and configure a {@link MavenLibraryResolver} by creating a new one and registering both
++ * a dependency artifact that should be resolved to a library at runtime and the repository it is found in.
++ * An example of this would be the inclusion of the jooq library for typesafe SQL queries:
++ * <pre>{@code
++ * MavenLibraryResolver resolver = new MavenLibraryResolver();
++ * resolver.addDependency(new Dependency(new DefaultArtifact("org.jooq:jooq:3.17.7"), null));
++ * resolver.addRepository(new RemoteRepository.Builder(
++ *     "central", "default", "https://repo1.maven.org/maven2/"
++ * ).build());
++ * }</pre>
++ *
++ * Plugins may create and register a {@link MavenLibraryResolver} after configuring it.
++ */
++public class MavenLibraryResolver implements ClassPathLibrary {
++
++    private static final Logger logger = Logger.getLogger("MavenLibraryResolver");
++
++    private final RepositorySystem repository;
++    private final DefaultRepositorySystemSession session;
++    private final List<RemoteRepository> repositories = new ArrayList<>();
++    private final List<Dependency> dependencies = new ArrayList<>();
++
++    /**
++     * Creates a new maven library resolver instance.
++     * <p>
++     * The created instance will use the servers {@code libraries} folder to cache fetched libraries in.
++     * Notably, the resolver is created without any repository, not even maven central.
++     * It is hence crucial that plugins which aim to use this api register all required repositories before
++     * submitting the {@link MavenLibraryResolver} to the {@link io.papermc.paper.plugin.loader.PluginClasspathBuilder}.
++     */
++    public MavenLibraryResolver() {
++        DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
++        locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
++        locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
++
++        this.repository = locator.getService(RepositorySystem.class);
++        this.session = MavenRepositorySystemUtils.newSession();
++
++        this.session.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_FAIL);
++        this.session.setLocalRepositoryManager(this.repository.newLocalRepositoryManager(this.session, new LocalRepository("libraries")));
++        this.session.setTransferListener(new AbstractTransferListener() {
++            @Override
++            public void transferInitiated(@NotNull TransferEvent event) throws TransferCancelledException {
++                logger.log(Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName());
++            }
++        });
++        this.session.setReadOnly();
++    }
++
++    /**
++     * Adds the provided dependency to the library resolver.
++     * The artifact from the first valid repository matching the passed dependency will be chosen.
++     *
++     * @param dependency the definition of the dependency the maven library resolver should resolve when running
++     * @see MavenLibraryResolver#addRepository(RemoteRepository)
++     */
++    public void addDependency(@NotNull Dependency dependency) {
++        this.dependencies.add(dependency);
++    }
++
++    /**
++     * Adds the provided repository to the library resolver.
++     * The order in which these are added does matter, as dependency resolving will start at the first added
++     * repository.
++     *
++     * @param remoteRepository the configuration that defines the maven repository this library resolver should fetch
++     *                         dependencies from
++     */
++    public void addRepository(@NotNull RemoteRepository remoteRepository) {
++        this.repositories.add(remoteRepository);
++    }
++
++    /**
++     * Resolves the provided dependencies and adds them to the library store.
++     *
++     * @param store the library store the then resolved and downloaded dependencies are registered into
++     * @throws LibraryLoadingException if resolving a dependency failed
++     */
++    @Override
++    public void register(@NotNull LibraryStore store) throws LibraryLoadingException {
++        List<RemoteRepository> repos = this.repository.newResolutionRepositories(this.session, this.repositories);
++
++        DependencyResult result;
++        try {
++            result = this.repository.resolveDependencies(this.session, new DependencyRequest(new CollectRequest((Dependency) null, this.dependencies, repos), null));
++        } catch (DependencyResolutionException ex) {
++            throw new LibraryLoadingException("Error resolving libraries", ex);
++        }
++
++        for (ArtifactResult artifact : result.getArtifactResults()) {
++            File file = artifact.getArtifact().getFile();
++            store.addLibrary(file.toPath());
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.classloader;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * The class loader access interface is an <b>internal</b> representation of a class accesses' ability to see types
++ * from other {@link ConfiguredPluginClassLoader}.
++ * <p>
++ * An example of this would be a class loader access representing a plugin. The class loader access in that case would
++ * only return {@code true} on calls for {@link #canAccess(ConfiguredPluginClassLoader)} if the passed class loader
++ * is owned by a direct or transitive dependency of the plugin, preventing the plugin for accidentally discovering and
++ * using class types that are supplied by plugins/libraries the plugin did not actively define as a dependency.
++ */
++@ApiStatus.Internal
++public interface ClassLoaderAccess {
++
++    /**
++     * Evaluates if this class loader access is allowed to access types provided by the passed {@link
++     * ConfiguredPluginClassLoader}.
++     * <p>
++     * This interface method does not offer any further contracts on the interface level, as the logic to determine
++     * what class loaders this class loader access is allowed to retrieve types from depends heavily on the type of
++     * access.
++     * Legacy spigot types for example may access any class loader available on the server, while modern paper plugins
++     * are properly limited to their dependency tree.
++     *
++     * @param classLoader the class loader for which access should be evaluated
++     * @return a plain boolean flag, {@code true} indicating that this class loader access is allowed to access types
++     * from the passed configured plugin class loader, {@code false} indicating otherwise.
++     */
++    boolean canAccess(ConfiguredPluginClassLoader classLoader);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.Closeable;
++
++/**
++ * The configured plugin class loader represents an <b>internal</b> abstraction over the classloaders used by the server
++ * to load and access a plugins classes during runtime.
++ * <p>
++ * It implements {@link Closeable} to define the ability to shutdown and close the classloader that implements this
++ * interface.
++ */
++@ApiStatus.Internal
++public interface ConfiguredPluginClassLoader extends Closeable {
++
++    /**
++     * Provides the configuration of the plugin that this plugin classloader provides type access to.
++     *
++     * @return the plugin meta instance, holding all meta information about the plugin instance.
++     */
++    PluginMeta getConfiguration();
++
++    /**
++     * Attempts to load a class from this plugin class loader using the passed fully qualified name.
++     * This lookup logic can be configured through the following parameters to define how wide or how narrow the
++     * class lookup should be.
++     *
++     * @param name           the fully qualified name of the class to load
++     * @param resolve        whether the class should be resolved if needed or not
++     * @param checkGlobal    whether this lookup should check transitive dependencies, including either the legacy spigot
++     *                       global class loader or the paper {@link PluginClassLoaderGroup}
++     * @param checkLibraries whether the defined libraries should be checked for the class or not
++     * @return the class found at the fully qualified class name passed under the passed restrictions
++     * @throws ClassNotFoundException if the class could not be found considering the passed restrictions
++     * @see ClassLoader#loadClass(String)
++     * @see Class#forName(String, boolean, ClassLoader)
++     */
++    Class<?> loadClass(@NotNull String name,
++                       boolean resolve,
++                       boolean checkGlobal,
++                       boolean checkLibraries) throws ClassNotFoundException;
++
++    /**
++     * Initializes both this configured plugin class loader and the java plugin passed to link to each other.
++     * This logic is to be called exactly once when the initial setup between the class loader and the instantiated
++     * {@link JavaPlugin} is loaded.
++     *
++     * @param plugin the {@link JavaPlugin} that should be interlinked with this class loader.
++     */
++    void init(JavaPlugin plugin);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.classloader;
++
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * The plugin classloader storage is an <b>internal</b> type that is used to manage existing classloaders on the server.
++ * <p>
++ * The paper classloader storage is also responsible for storing added {@link ConfiguredPluginClassLoader}s into
++ * {@link PluginClassLoaderGroup}s, via {@link #registerOpenGroup(ConfiguredPluginClassLoader)},
++ * {@link #registerSpigotGroup(PluginClassLoader)} and {@link
++ * #registerAccessBackedGroup(ConfiguredPluginClassLoader, ClassLoaderAccess)}.
++ * <p>
++ * Groups are differentiated into the global group or plugin owned groups.
++ * <ul>
++ * <li>The global group holds all registered class loaders and merely exists to maintain backwards compatibility with
++ * spigots legacy classloader handling.</li>
++ * <li>The plugin groups only contains the classloaders that each plugin has access to and hence serves to properly
++ * separates unrelated classloaders.</li>
++ * </ul>
++ */
++@ApiStatus.Internal
++public interface PaperClassLoaderStorage {
++
++    /**
++     * Access to the shared instance of the {@link PaperClassLoaderStorageAccess}.
++     *
++     * @return the singleton instance of the {@link PaperClassLoaderStorage} used throughout the server
++     */
++    static PaperClassLoaderStorage instance() {
++        return PaperClassLoaderStorageAccess.INSTANCE;
++    }
++
++    /**
++     * Registers a legacy spigot {@link PluginClassLoader} into the loader storage, creating a group wrapping
++     * the single plugin class loader with transitive access to the global group.
++     *
++     * @param pluginClassLoader the legacy spigot plugin class loader to register
++     * @return the group the plugin class loader was placed into
++     */
++    PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader);
++
++    /**
++     * Registers a paper configured plugin classloader into a new open group, with full access to the global
++     * plugin class loader group.
++     * <p>
++     * This method hence allows the configured plugin class loader to access all other class loaders registered in this
++     * storage.
++     *
++     * @param classLoader the configured plugin class loader to register
++     * @return the group the plugin class loader was placed into
++     */
++    PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader);
++
++    /**
++     * Registers a paper configured classloader into a new, access backed group.
++     * The access backed classloader group, different from an open group, only has access to the classloaders
++     * the passed {@link ClassLoaderAccess} grants access to.
++     *
++     * @param classLoader the configured plugin class loader to register
++     * @param access      the class loader access that defines what other classloaders the passed plugin class loader
++     *                    should be granted access to.
++     * @return the group the plugin class loader was placed into.
++     */
++    PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access);
++
++    /**
++     * Unregisters a configured class loader from this storage.
++     * This removes the passed class loaders from any group it may have been a part of, including the global group.
++     * <p>
++     * Note: this method is <b>highly</b> discouraged from being used, as mutation of the classloaders at runtime
++     * is not encouraged
++     *
++     * @param configuredPluginClassLoader the class loader to remove from this storage.
++     */
++    void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader);
++
++    /**
++     * Registers a configured plugin class loader directly into the global group without adding it to
++     * any existing groups.
++     * <p>
++     * Note: this method unsafely injects the plugin classloader directly into the global group, which bypasses the
++     * group structure paper's plugin API introduced. This method should hence be used with caution.
++     *
++     * @param pluginLoader the configured plugin classloader instance that should be registered directly into the global
++     *                     group.
++     * @return a simple boolean flag, {@code true} if the classloader was registered or {@code false} if the classloader
++     * was already part of the global group.
++     */
++    boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.classloader;
++
++import net.kyori.adventure.util.Services;
++
++/**
++ * The paper classloader storage access acts as the holder for the server provided implementation of the
++ * {@link PaperClassLoaderStorage} interface.
++ */
++class PaperClassLoaderStorageAccess {
++
++    /**
++     * The shared instance of the {@link PaperClassLoaderStorage}, supplied through the {@link java.util.ServiceLoader}
++     * by the server.
++     */
++    static final PaperClassLoaderStorage INSTANCE = Services.service(PaperClassLoaderStorage.class).orElseThrow();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.classloader;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Contract;
++import org.jetbrains.annotations.Nullable;
++
++/**
++ * A plugin classloader group represents a group of classloaders that a plugins classloader may access.
++ * <p>
++ * An example of this would be a classloader group that holds all direct and transitive dependencies a plugin declared,
++ * allowing a plugins classloader to access classes included in these dependencies via this group.
++ */
++@ApiStatus.Internal
++public interface PluginClassLoaderGroup {
++
++    /**
++     * Attempts to find/load a class from this plugin class loader group using the passed fully qualified name
++     * in any of the classloaders that are part of this group.
++     * <p>
++     * The lookup order across the contained loaders is not defined on the API level and depends purely on the
++     * implementation.
++     *
++     * @param name      the fully qualified name of the class to load
++     * @param resolve   whether the class should be resolved if needed or not
++     * @param requester plugin classloader that is requesting the class from this loader group
++     * @return the class found at the fully qualified class name passed. If the class could not be found, {@code null}
++     * will be returned.
++     * @see ConfiguredPluginClassLoader#loadClass(String, boolean, boolean, boolean)
++     */
++    @Nullable
++    Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester);
++
++    /**
++     * Removes a configured plugin classloader from this class loader group.
++     * If the classloader is not currently in the list, this method will simply do nothing.
++     *
++     * @param configuredPluginClassLoader the plugin classloader to remove from the group
++     */
++    @Contract(mutates = "this")
++    void remove(ConfiguredPluginClassLoader configuredPluginClassLoader);
++
++    /**
++     * Adds the passed plugin classloader to this group, allowing this group to use it during
++     * {@link #getClassByName(String, boolean, ConfiguredPluginClassLoader)} lookups.
++     * <p>
++     * This method does <b>not</b> query the {@link ClassLoaderAccess} (exposed via {@link #getAccess()}) to ensure
++     * if this group has access to the class loader passed.
++     *
++     * @param configuredPluginClassLoader the plugin classloader to add to this group.
++     */
++    @Contract(mutates = "this")
++    void add(ConfiguredPluginClassLoader configuredPluginClassLoader);
++
++    /**
++     * Provides the class loader access that guards and defines the content of this classloader group.
++     * While not guaranteed contractually (see {@link #add(ConfiguredPluginClassLoader)}), the access generally is
++     * responsible for defining which {@link ConfiguredPluginClassLoader}s should be part of this group and which ones
++     * should not.
++     *
++     * @return the classloader access governing which classloaders should be part of this group and which ones should
++     * not.
++     */
++    ClassLoaderAccess getAccess();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java b/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.entrypoint;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * A dependency context is a read-only abstraction of a type/concept that can resolve dependencies between plugins.
++ * <p>
++ * This may for example be the server wide plugin manager itself, capable of validating if a dependency exists between
++ * two {@link PluginMeta} instances, however the implementation is not limited to such a concrete use-case.
++ */
++@ApiStatus.Internal
++public interface DependencyContext {
++
++    /**
++     * Computes if the passed {@link PluginMeta} defined the passed dependency as a transitive dependency.
++     * A transitive dependency, as implied by its name, may not have been configured directly by the passed plugin
++     * but could also simply be a dependency of a dependency.
++     * <p>
++     * A simple example of this method would be
++     * <pre>{@code
++     * dependencyContext.isTransitiveDependency(pluginMetaA, pluginMetaC);
++     * }</pre>
++     * which would return {@code true} if {@code pluginMetaA} directly or indirectly depends on {@code pluginMetaC}.
++     *
++     * @param plugin the plugin meta this computation should consider the requester of the dependency status for the
++     *               passed potential dependency.
++     * @param depend the potential transitive dependency of the {@code plugin} parameter.
++     * @return a simple boolean flag indicating if {@code plugin} considers {@code depend} as a transitive dependency.
++     */
++    boolean isTransitiveDependency(@NotNull PluginMeta plugin, @NotNull PluginMeta depend);
++
++    /**
++     * Computes if this dependency context is aware of a dependency that provides/matches the passed identifier.
++     * <p>
++     * A dependency in this methods context is any dependable artefact. It does not matter if anything actually depends
++     * on said artefact, its mere existence as a potential dependency is enough for this method to consider it a
++     * dependency. If this dependency context is hence aware of an artefact with the matching identifier, this
++     * method returns {@code true}.
++     *
++     * @param pluginIdentifier the unique identifier of the dependency with which to probe this dependency context.
++     * @return a plain boolean flag indicating if this dependency context is aware of a potential dependency with the
++     * passed identifier.
++     */
++    boolean hasDependency(@NotNull String pluginIdentifier);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/util/DummyBukkitPluginLoader.java b/src/main/java/io/papermc/paper/plugin/provider/util/DummyBukkitPluginLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/util/DummyBukkitPluginLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.util;
++
++import org.bukkit.Bukkit;
++import org.bukkit.event.Event;
++import org.bukkit.event.Listener;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.PluginLoader;
++import org.bukkit.plugin.RegisteredListener;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.File;
++import java.util.Map;
++import java.util.Set;
++import java.util.regex.Pattern;
++
++/**
++ * A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation
++ * of papers new plugin system.
++ *
++ * @param plugin the loaded plugin that should be wrapped by this NOOP implementation
++ */
++@ApiStatus.Internal
++public record DummyBukkitPluginLoader(Plugin plugin) implements PluginLoader {
++
++
++    @Override
++    public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
++        throw new UnsupportedOperationException();
++    }
++
++    @Override
++    public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException {
++        throw new UnsupportedOperationException();
++    }
++
++    @Override
++    public @NotNull Pattern[] getPluginFileFilters() {
++        throw new UnsupportedOperationException();
++    }
++
++    @Override
++    public @NotNull Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) {
++        throw new UnsupportedOperationException();
++    }
++
++    @Override
++    public void enablePlugin(@NotNull Plugin plugin) {
++        Bukkit.getPluginManager().enablePlugin(plugin);
++    }
++
++    @Override
++    public void disablePlugin(@NotNull Plugin plugin) {
++        Bukkit.getPluginManager().disablePlugin(plugin);
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java b/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.util;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++/**
++ * An <b>internal</b> utility type that holds logic for loading a provider-like type from a classloaders.
++ * Provides, at least in the context of this utility, define themselves as implementations of a specific parent
++ * interface/type, e.g. {@link org.bukkit.plugin.java.JavaPlugin} and implement a no-args constructor.
++ */
++@ApiStatus.Internal
++public class ProviderUtil {
++
++    /**
++     * Loads the class found at the provided fully qualified class name from the passed classloader, creates a new
++     * instance of it using the no-args constructor, that should exist as per this method contract, and casts it to the
++     * provided parent type.
++     *
++     * @param clazz     the fully qualified name of the class to load
++     * @param classType the parent type that the created object found at the {@code clazz} name should be cast to
++     * @param loader    the loader from which the class should be loaded
++     * @param <T>       the generic type of the parent class the created object will be cast to
++     * @return the object instantiated from the class found at the provided FQN, cast to the parent type
++     */
++    @NotNull
++    public static <T> T loadClass(@NotNull String clazz, @NotNull Class<T> classType, @NotNull ClassLoader loader) {
++        return loadClass(clazz, classType, loader, null);
++    }
++
++    /**
++     * Loads the class found at the provided fully qualified class name from the passed classloader, creates a new
++     * instance of it using the no-args constructor, that should exist as per this method contract, and casts it to the
++     * provided parent type.
++     *
++     * @param clazz     the fully qualified name of the class to load
++     * @param classType the parent type that the created object found at the {@code clazz} name should be cast to
++     * @param loader    the loader from which the class should be loaded
++     * @param onError   a runnable that is executed before any unknown exception is raised through a sneaky throw.
++     * @param <T>       the generic type of the parent class the created object will be cast to
++     * @return the object instantiated from the class found at the provided fully qualified class name, cast to the
++     * parent type
++     */
++    @NotNull
++    public static <T> T loadClass(@NotNull String clazz, @NotNull Class<T> classType, @NotNull ClassLoader loader, @Nullable Runnable onError) {
++        try {
++            T clazzInstance;
++
++            try {
++                Class<?> jarClass = Class.forName(clazz, true, loader);
++
++                Class<? extends T> pluginClass;
++                try {
++                    pluginClass = jarClass.asSubclass(classType);
++                } catch (ClassCastException ex) {
++                    throw new ClassCastException("class '%s' does not extend '%s'".formatted(clazz, classType));
++                }
++
++                clazzInstance = pluginClass.getDeclaredConstructor().newInstance();
++            } catch (IllegalAccessException exception) {
++                throw new RuntimeException("No public constructor");
++            } catch (InstantiationException exception) {
++                throw new RuntimeException("Abnormal class instantiation", exception);
++            }
++
++            return clazzInstance;
++        } catch (Throwable e) {
++            if (onError != null) {
++                onError.run();
++            }
++            SneakyThrow.sneaky(e);
++        }
++
++        throw new AssertionError(); // Shouldn't happen
++    }
++
++}
+diff --git a/src/main/java/org/bukkit/UnsafeValues.java b/src/main/java/org/bukkit/UnsafeValues.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/UnsafeValues.java
++++ b/src/main/java/org/bukkit/UnsafeValues.java
+@@ -0,0 +0,0 @@ public interface UnsafeValues {
+     String getTranslationKey(EntityType entityType);
+ 
+     String getTranslationKey(ItemStack itemStack);
++
++    // Paper start
++    @Deprecated(forRemoval = true)
++    boolean isSupportedApiVersion(String apiVersion);
++
++    @Deprecated(forRemoval = true)
++    static boolean isLegacyPlugin(org.bukkit.plugin.Plugin plugin) {
++        return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion());
++    }
++    // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/command/PluginCommand.java b/src/main/java/org/bukkit/command/PluginCommand.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/command/PluginCommand.java
++++ b/src/main/java/org/bukkit/command/PluginCommand.java
+@@ -0,0 +0,0 @@ public final class PluginCommand extends Command implements PluginIdentifiableCo
+     private CommandExecutor executor;
+     private TabCompleter completer;
+ 
+-    protected PluginCommand(@NotNull String name, @NotNull Plugin owner) {
++    PluginCommand(@NotNull String name, @NotNull Plugin owner) {
+         super(name);
+         this.executor = owner;
+         this.owningPlugin = owner;
+diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java
++++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java
+@@ -0,0 +0,0 @@ public class SimpleCommandMap implements CommandMap {
+     private void setDefaultCommands() {
+         register("bukkit", new VersionCommand("version"));
+         register("bukkit", new ReloadCommand("reload"));
+-        register("bukkit", new PluginsCommand("plugins"));
++        //register("bukkit", new PluginsCommand("plugins")); // Paper
+         register("bukkit", new TimingsCommand("timings"));
+     }
+ 
+diff --git a/src/main/java/org/bukkit/plugin/Plugin.java b/src/main/java/org/bukkit/plugin/Plugin.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/Plugin.java
++++ b/src/main/java/org/bukkit/plugin/Plugin.java
+@@ -0,0 +0,0 @@ public interface Plugin extends TabExecutor {
+      * Returns the plugin.yaml file containing the details for this plugin
+      *
+      * @return Contents of the plugin.yaml file
++     * @deprecated May be inaccurate due to different plugin implementations.
++     * @see Plugin#getPluginMeta()
+      */
++    @Deprecated // Paper
+     @NotNull
+     public PluginDescriptionFile getDescription();
+ 
++    // Paper start
++    /**
++     * Gets the plugin meta for this plugin.
++     * @return configuration
++     */
++    @NotNull
++    io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta();
++    // Paper end
+     /**
+      * Gets a {@link FileConfiguration} for this plugin, read through
+      * "config.yml"
+@@ -0,0 +0,0 @@ public interface Plugin extends TabExecutor {
+      *
+      * @return PluginLoader that controls this plugin
+      */
++    @Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future
+     @NotNull
+     public PluginLoader getPluginLoader();
+ 
+diff --git a/src/main/java/org/bukkit/plugin/PluginBase.java b/src/main/java/org/bukkit/plugin/PluginBase.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/PluginBase.java
++++ b/src/main/java/org/bukkit/plugin/PluginBase.java
+@@ -0,0 +0,0 @@ public abstract class PluginBase implements Plugin {
+     @Override
+     @NotNull
+     public final String getName() {
+-        return getDescription().getName();
++        return getPluginMeta().getName(); // Paper
+     }
+ }
+diff --git a/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java b/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
++++ b/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
+@@ -0,0 +0,0 @@ import org.yaml.snakeyaml.nodes.Tag;
+  *      inferno.burningdeaths: true
+  *</pre></blockquote>
+  */
+-public final class PluginDescriptionFile {
++public final class PluginDescriptionFile implements io.papermc.paper.plugin.configuration.PluginMeta { // Paper
+     private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z0-9 _.-]+$");
+     private static final ThreadLocal<Yaml> YAML = new ThreadLocal<Yaml>() {
+         @Override
+@@ -0,0 +0,0 @@ public final class PluginDescriptionFile {
+     private Set<PluginAwareness> awareness = ImmutableSet.of();
+     private String apiVersion = null;
+     private List<String> libraries = ImmutableList.of();
++    // Paper start - oh my goddddd
++    /**
++     * Don't use this.
++     */
++    @org.jetbrains.annotations.ApiStatus.Internal
++    public PluginDescriptionFile(String rawName, String name, List<String> provides, String main, String classLoaderOf, List<String> depend, List<String> softDepend, List<String> loadBefore, String version, Map<String, Map<String, Object>> commands, String description, List<String> authors, List<String> contributors, String website, String prefix, PluginLoadOrder order, List<Permission> permissions, PermissionDefault defaultPerm, Set<PluginAwareness> awareness, String apiVersion, List<String> libraries) {
++        this.rawName = rawName;
++        this.name = name;
++        this.provides = provides;
++        this.main = main;
++        this.classLoaderOf = classLoaderOf;
++        this.depend = depend;
++        this.softDepend = softDepend;
++        this.loadBefore = loadBefore;
++        this.version = version;
++        this.commands = commands;
++        this.description = description;
++        this.authors = authors;
++        this.contributors = contributors;
++        this.website = website;
++        this.prefix = prefix;
++        this.order = order;
++        this.permissions = permissions;
++        this.defaultPerm = defaultPerm;
++        this.awareness = awareness;
++        this.apiVersion = apiVersion;
++        this.libraries = libraries;
++    }
++
++    @Override
++    public @NotNull String getMainClass() {
++        return this.main;
++    }
++
++    @Override
++    public @NotNull PluginLoadOrder getLoadOrder() {
++        return this.order;
++    }
++
++    @Override
++    public @Nullable String getLoggerPrefix() {
++        return this.prefix;
++    }
++
++    @Override
++    public @NotNull List<String> getPluginDependencies() {
++        return this.depend;
++    }
++
++    @Override
++    public @NotNull List<String> getPluginSoftDependencies() {
++        return this.softDepend;
++    }
++
++    @Override
++    public @NotNull List<String> getLoadBeforePlugins() {
++        return this.loadBefore;
++    }
++
++    @Override
++    public @NotNull List<String> getProvidedPlugins() {
++        return this.provides;
++    }
++    // Paper end
+ 
+     public PluginDescriptionFile(@NotNull final InputStream stream) throws InvalidDescriptionException {
+         loadMap(asMap(YAML.get().load(stream)));
+diff --git a/src/main/java/org/bukkit/plugin/PluginLoader.java b/src/main/java/org/bukkit/plugin/PluginLoader.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/PluginLoader.java
++++ b/src/main/java/org/bukkit/plugin/PluginLoader.java
+@@ -0,0 +0,0 @@ import org.jetbrains.annotations.NotNull;
+  * Represents a plugin loader, which handles direct access to specific types
+  * of plugins
+  */
++@Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future
+ public interface PluginLoader {
+ 
+     /**
+diff --git a/src/main/java/org/bukkit/plugin/PluginManager.java b/src/main/java/org/bukkit/plugin/PluginManager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/PluginManager.java
++++ b/src/main/java/org/bukkit/plugin/PluginManager.java
+@@ -0,0 +0,0 @@ import org.jetbrains.annotations.Nullable;
+ /**
+  * Handles all plugin management from the Server
+  */
+-public interface PluginManager {
++public interface PluginManager extends io.papermc.paper.plugin.PermissionManager { // Paper
+ 
+     /**
+      * Registers the specified plugin loader
+@@ -0,0 +0,0 @@ public interface PluginManager {
+      * @throws IllegalArgumentException Thrown when the given Class is not a
+      *     valid PluginLoader
+      */
++    @Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future
+     public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException;
+ 
+     /**
+@@ -0,0 +0,0 @@ public interface PluginManager {
+      * @return True if event timings are to be used
+      */
+     public boolean useTimings();
++
++    // Paper start
++    @org.jetbrains.annotations.ApiStatus.Internal
++    boolean isTransitiveDependency(io.papermc.paper.plugin.configuration.PluginMeta pluginMeta, io.papermc.paper.plugin.configuration.PluginMeta dependencyConfig);
++
++    /**
++     * Sets the permission manager to be used for this server.
++     *
++     * @param permissionManager permission manager
++     */
++    @org.jetbrains.annotations.ApiStatus.Experimental
++    void overridePermissionManager(@NotNull Plugin plugin, @Nullable io.papermc.paper.plugin.PermissionManager permissionManager);
++    // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
+@@ -0,0 +0,0 @@ import org.jetbrains.annotations.Nullable;
+ /**
+  * Handles all plugin management from the Server
+  */
++@Deprecated(forRemoval = true) // Paper - This implementation may be replaced in a future version of Paper.
++// Plugins may still reflect into this class to modify permission logic for the time being.
+ public final class SimplePluginManager implements PluginManager {
+     private final Server server;
+     private final Map<Pattern, PluginLoader> fileAssociations = new HashMap<Pattern, PluginLoader>();
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     private MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
+     private File updateDirectory;
+     private final SimpleCommandMap commandMap;
+-    private final Map<String, Permission> permissions = new HashMap<String, Permission>();
+-    private final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<Boolean, Set<Permission>>();
+-    private final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<String, Map<Permissible, Boolean>>();
+-    private final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<Boolean, Map<Permissible, Boolean>>();
++    // Paper start
++    public final Map<String, Permission> permissions = new HashMap<String, Permission>();
++    public final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<Boolean, Set<Permission>>();
++    public final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<String, Map<Permissible, Boolean>>();
++    public final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<Boolean, Map<Permissible, Boolean>>();
++    public PluginManager paperPluginManager;
++    // Paper end
+     private boolean useTimings = false;
+ 
+     public SimplePluginManager(@NotNull Server instance, @NotNull SimpleCommandMap commandMap) {
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @NotNull
+     public Plugin[] loadPlugins(@NotNull File directory) {
++        if (true) {
++            List<Plugin> pluginList = new ArrayList<>();
++            java.util.Collections.addAll(pluginList, this.paperPluginManager.loadPlugins(directory));
++            return pluginList.toArray(new Plugin[0]);
++        }
+         Preconditions.checkArgument(directory != null, "Directory cannot be null");
+         Preconditions.checkArgument(directory.isDirectory(), "Directory must be a directory");
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Nullable
+     public synchronized Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
+         Preconditions.checkArgument(file != null, "File cannot be null");
++        // Paper start
++        if (true) {
++            try {
++                return this.paperPluginManager.loadPlugin(file);
++            } catch (org.bukkit.plugin.InvalidDescriptionException ignored) {
++                return null;
++            }
++        }
++        // Paper end
+ 
+         checkUpdate(file);
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @Nullable
+     public synchronized Plugin getPlugin(@NotNull String name) {
++        if (true) {return this.paperPluginManager.getPlugin(name);} // Paper
+         return lookupNames.get(name.replace(' ', '_'));
+     }
+ 
+     @Override
+     @NotNull
+     public synchronized Plugin[] getPlugins() {
++        if (true) {return this.paperPluginManager.getPlugins();} // Paper
+         return plugins.toArray(new Plugin[plugins.size()]);
+     }
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+      */
+     @Override
+     public boolean isPluginEnabled(@NotNull String name) {
++        if (true) {return this.paperPluginManager.isPluginEnabled(name);} // Paper
+         Plugin plugin = getPlugin(name);
+ 
+         return isPluginEnabled(plugin);
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+      */
+     @Override
+     public boolean isPluginEnabled(@Nullable Plugin plugin) {
++        if (true) {return this.paperPluginManager.isPluginEnabled(plugin);} // Paper
+         if ((plugin != null) && (plugins.contains(plugin))) {
+             return plugin.isEnabled();
+         } else {
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void enablePlugin(@NotNull final Plugin plugin) {
++        if (true) {this.paperPluginManager.enablePlugin(plugin); return;} // Paper
+         if (!plugin.isEnabled()) {
+             List<Command> pluginCommands = PluginCommandYamlParser.parse(plugin);
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void disablePlugins() {
++        if (true) {this.paperPluginManager.disablePlugins(); return;} // Paper
+         Plugin[] plugins = getPlugins();
+         for (int i = plugins.length - 1; i >= 0; i--) {
+             disablePlugin(plugins[i]);
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void disablePlugin(@NotNull final Plugin plugin) {
++        if (true) {this.paperPluginManager.disablePlugin(plugin); return;} // Paper
+         if (plugin.isEnabled()) {
+             try {
+                 plugin.getPluginLoader().disablePlugin(plugin);
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void clearPlugins() {
++        if (true) {this.paperPluginManager.clearPlugins(); return;} // Paper
+         synchronized (this) {
+             disablePlugins();
+             plugins.clear();
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+      */
+     @Override
+     public void callEvent(@NotNull Event event) {
++        if (true) {this.paperPluginManager.callEvent(event); return;} // Paper
+         if (event.isAsynchronous()) {
+             if (Thread.holdsLock(this)) {
+                 throw new IllegalStateException(event.getEventName() + " cannot be triggered asynchronously from inside synchronized code.");
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
++        if (true) {this.paperPluginManager.registerEvents(listener, plugin); return;} // Paper
+         if (!plugin.isEnabled()) {
+             throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled");
+         }
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+         Preconditions.checkArgument(priority != null, "Priority cannot be null");
+         Preconditions.checkArgument(executor != null, "Executor cannot be null");
+         Preconditions.checkArgument(plugin != null, "Plugin cannot be null");
++        if (true) {this.paperPluginManager.registerEvent(event, listener, priority, executor, plugin); return;} // Paper
+ 
+         if (!plugin.isEnabled()) {
+             throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @Nullable
+     public Permission getPermission(@NotNull String name) {
++        if (true) {return this.paperPluginManager.getPermission(name);} // Paper
+         return permissions.get(name.toLowerCase(java.util.Locale.ENGLISH));
+     }
+ 
+     @Override
+     public void addPermission(@NotNull Permission perm) {
++        if (true) {this.paperPluginManager.addPermission(perm); return;} // Paper
+         addPermission(perm, true);
+     }
+ 
+     @Deprecated
+     public void addPermission(@NotNull Permission perm, boolean dirty) {
++        if (true) {this.paperPluginManager.addPermission(perm); return;} // Paper - This just has a performance implication, use the better api to avoid this.
+         String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH);
+ 
+         if (permissions.containsKey(name)) {
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @NotNull
+     public Set<Permission> getDefaultPermissions(boolean op) {
++        if (true) {return this.paperPluginManager.getDefaultPermissions(op);} // Paper
+         return ImmutableSet.copyOf(defaultPerms.get(op));
+     }
+ 
+     @Override
+     public void removePermission(@NotNull Permission perm) {
++        if (true) {this.paperPluginManager.removePermission(perm); return;} // Paper
+         removePermission(perm.getName());
+     }
+ 
+     @Override
+     public void removePermission(@NotNull String name) {
++        if (true) {this.paperPluginManager.removePermission(name); return;} // Paper
+         permissions.remove(name.toLowerCase(java.util.Locale.ENGLISH));
+     }
+ 
+     @Override
+     public void recalculatePermissionDefaults(@NotNull Permission perm) {
++        if (true) {this.paperPluginManager.recalculatePermissionDefaults(perm); return;} // Paper
+         if (perm != null && permissions.containsKey(perm.getName().toLowerCase(java.util.Locale.ENGLISH))) {
+             defaultPerms.get(true).remove(perm);
+             defaultPerms.get(false).remove(perm);
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
++        if (true) {this.paperPluginManager.subscribeToPermission(permission, permissible); return;} // Paper
+         String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+         Map<Permissible, Boolean> map = permSubs.get(name);
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
++        if (true) {this.paperPluginManager.unsubscribeFromPermission(permission, permissible); return;} // Paper
+         String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+         Map<Permissible, Boolean> map = permSubs.get(name);
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @NotNull
+     public Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
++        if (true) {return this.paperPluginManager.getPermissionSubscriptions(permission);} // Paper
+         String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+         Map<Permissible, Boolean> map = permSubs.get(name);
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
++        if (true) {this.paperPluginManager.subscribeToDefaultPerms(op, permissible); return;} // Paper
+         Map<Permissible, Boolean> map = defSubs.get(op);
+ 
+         if (map == null) {
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
++        if (true) {this.paperPluginManager.unsubscribeFromDefaultPerms(op, permissible); return;} // Paper
+         Map<Permissible, Boolean> map = defSubs.get(op);
+ 
+         if (map != null) {
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @NotNull
+     public Set<Permissible> getDefaultPermSubscriptions(boolean op) {
++        if (true) {return this.paperPluginManager.getDefaultPermSubscriptions(op);} // Paper
+         Map<Permissible, Boolean> map = defSubs.get(op);
+ 
+         if (map == null) {
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     @Override
+     @NotNull
+     public Set<Permission> getPermissions() {
++        if (true) {return this.paperPluginManager.getPermissions();} // Paper
+         return new HashSet<Permission>(permissions.values());
+     }
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+ 
+     @Override
+     public boolean useTimings() {
++        if (true) {return this.paperPluginManager.useTimings();} // Paper
+         return useTimings;
+     }
+ 
+@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
+     public void useTimings(boolean use) {
+         useTimings = use;
+     }
++
++    // Paper start
++    public void clearPermissions() {
++        if (true) {this.paperPluginManager.clearPermissions(); return;} // Paper
++        permissions.clear();
++        defaultPerms.get(true).clear();
++        defaultPerms.get(false).clear();
++    }
++
++    @Override
++    public boolean isTransitiveDependency(io.papermc.paper.plugin.configuration.PluginMeta pluginMeta, io.papermc.paper.plugin.configuration.PluginMeta dependencyConfig) {
++        return this.paperPluginManager.isTransitiveDependency(pluginMeta, dependencyConfig);
++    }
++
++    @Override
++    public void overridePermissionManager(@NotNull Plugin plugin, @Nullable io.papermc.paper.plugin.PermissionManager permissionManager) {
++        this.paperPluginManager.overridePermissionManager(plugin, permissionManager);
++    }
++
++    @Override
++    public void addPermissions(@NotNull List<Permission> perm) {
++        this.paperPluginManager.addPermissions(perm);
++    }
++    // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
++++ b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
+@@ -0,0 +0,0 @@ public class UnknownDependencyException extends RuntimeException {
+     public UnknownDependencyException() {
+ 
+     }
++    // Paper start
++    /**
++     * Create a new {@link UnknownDependencyException} with a message informing
++     * about which dependencies are missing for what plugin.
++     *
++     * @param missingDependencies missing dependencies
++     * @param pluginName plugin which is missing said dependencies
++     */
++    public UnknownDependencyException(final @org.jetbrains.annotations.NotNull java.util.Collection<String> missingDependencies, final @org.jetbrains.annotations.NotNull String pluginName) {
++        this("Unknown/missing dependency plugins: [" + String.join(", ", missingDependencies) + "]. Please download and install these plugins to run '" + pluginName + "'.");
++    }
++    // Paper end
+ }
+diff --git a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java
++++ b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+     private Server server = null;
+     private File file = null;
+     private PluginDescriptionFile description = null;
++    private io.papermc.paper.plugin.configuration.PluginMeta pluginMeta = null; // Paper
+     private File dataFolder = null;
+     private ClassLoader classLoader = null;
+     private boolean naggable = true;
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+     private PluginLogger logger = null;
+ 
+     public JavaPlugin() {
+-        final ClassLoader classLoader = this.getClass().getClassLoader();
+-        if (!(classLoader instanceof PluginClassLoader)) {
+-            throw new IllegalStateException("JavaPlugin requires " + PluginClassLoader.class.getName());
++        // Paper start
++        if (this.getClass().getClassLoader() instanceof io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader configuredPluginClassLoader) {
++            configuredPluginClassLoader.init(this);
++        } else {
++            throw new IllegalStateException("JavaPlugin requires to be created by a valid classloader.");
+         }
+-        ((PluginClassLoader) classLoader).initialize(this);
++        // Paper end
+     }
+ 
++    @Deprecated(forRemoval = true) // Paper
+     protected JavaPlugin(@NotNull final JavaPluginLoader loader, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file) {
+         final ClassLoader classLoader = this.getClass().getClassLoader();
+         if (classLoader instanceof PluginClassLoader) {
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+      * Gets the associated PluginLoader responsible for this plugin
+      *
+      * @return PluginLoader that controls this plugin
++     * @deprecated Plugin loading now occurs at a point which makes it impossible to expose this
++     * behavior. This instance will only throw unsupported operation exceptions.
+      */
+     @NotNull
+     @Override
++    @Deprecated(forRemoval = true) // Paper
+     public final PluginLoader getPluginLoader() {
+         return loader;
+     }
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+      * Returns the plugin.yaml file containing the details for this plugin
+      *
+      * @return Contents of the plugin.yaml file
++     * @deprecated No longer applicable to all types of plugins
+      */
+     @NotNull
+     @Override
++    @Deprecated
+     public final PluginDescriptionFile getDescription() {
+         return description;
+     }
+ 
++    @Nullable
++    public final io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
++        return this.pluginMeta;
++    }
++
+     @NotNull
+     @Override
+     public FileConfiguration getConfig() {
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+      *
+      * @param enabled true if enabled, otherwise false
+      */
+-    protected final void setEnabled(final boolean enabled) {
++    @org.jetbrains.annotations.ApiStatus.Internal // Paper
++    public final void setEnabled(final boolean enabled) { // Paper
+         if (isEnabled != enabled) {
+             isEnabled = enabled;
+ 
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+         }
+     }
+ 
+-
+-    final void init(@NotNull PluginLoader loader, @NotNull Server server, @NotNull PluginDescriptionFile description, @NotNull File dataFolder, @NotNull File file, @NotNull ClassLoader classLoader) {
+-        this.loader = loader;
++    // Paper start
++    public final void init(@NotNull PluginLoader loader, @NotNull Server server, @NotNull PluginDescriptionFile description, @NotNull File dataFolder, @NotNull File file, @NotNull ClassLoader classLoader) {
++        init(server, description, dataFolder, file, classLoader, description);
++        this.pluginMeta = description;
++    }
++    public final void init(@NotNull Server server, @NotNull PluginDescriptionFile description, @NotNull File dataFolder, @NotNull File file, @NotNull ClassLoader classLoader, @Nullable io.papermc.paper.plugin.configuration.PluginMeta configuration) {
++    // Paper end
++        this.loader = new io.papermc.paper.plugin.provider.util.DummyBukkitPluginLoader(this);
+         this.server = server;
+         this.file = file;
+         this.description = description;
+@@ -0,0 +0,0 @@ public abstract class JavaPlugin extends PluginBase {
+         this.classLoader = classLoader;
+         this.configFile = new File(dataFolder, "config.yml");
+         this.logger = new PluginLogger(this);
++        this.pluginMeta = configuration; // Paper
+     }
+ 
+     /**
+diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+@@ -0,0 +0,0 @@ import org.yaml.snakeyaml.error.YAMLException;
+ /**
+  * Represents a Java plugin loader, allowing plugins in the form of .jar
+  */
++@Deprecated(forRemoval = true) // Paper - The PluginLoader system will not function in the near future. This implementation will be moved.
+ public final class JavaPluginLoader implements PluginLoader {
+     final Server server;
+     private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")};
+@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
+     @Override
+     @NotNull
+     public Plugin loadPlugin(@NotNull final File file) throws InvalidPluginException {
++        if (true) throw new UnsupportedOperationException(); // Paper
+         Preconditions.checkArgument(file != null, "File cannot be null");
+ 
+         if (!file.exists()) {
+@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
+ 
+         final PluginClassLoader loader;
+         try {
+-            loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file, (libraryLoader != null) ? libraryLoader.createLoader(description) : null);
++            loader = new PluginClassLoader(getClass().getClassLoader(), description, dataFolder, file, (libraryLoader != null) ? libraryLoader.createLoader(description) : null, null); // Paper
+         } catch (InvalidPluginException ex) {
+             throw ex;
+         } catch (Throwable ex) {
+diff --git a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
++++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+@@ -0,0 +0,0 @@ import org.eclipse.aether.transport.http.HttpTransporterFactory;
+ import org.jetbrains.annotations.NotNull;
+ import org.jetbrains.annotations.Nullable;
+ 
+-class LibraryLoader
++// Paper start
++@org.jetbrains.annotations.ApiStatus.Internal
++public class LibraryLoader
++// Paper end
+ {
+ 
+     private final Logger logger;
+diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
++++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
+@@ -0,0 +0,0 @@ import org.jetbrains.annotations.Nullable;
+ /**
+  * A ClassLoader for plugins, to allow shared classes across multiple plugins
+  */
+-final class PluginClassLoader extends URLClassLoader {
++@org.jetbrains.annotations.ApiStatus.Internal // Paper
++public final class PluginClassLoader extends URLClassLoader implements io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader { // Paper
+     private final JavaPluginLoader loader;
+     private final Map<String, Class<?>> classes = new ConcurrentHashMap<String, Class<?>>();
+     private final PluginDescriptionFile description;
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+     private final Manifest manifest;
+     private final URL url;
+     private final ClassLoader libraryLoader;
+-    final JavaPlugin plugin;
++    public final JavaPlugin plugin; // Paper
+     private JavaPlugin pluginInit;
+     private IllegalStateException pluginState;
+     private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
++    private java.util.logging.Logger logger; // Paper - add field
++    private io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup classLoaderGroup; // Paper
++    public io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext; // Paper
+ 
+     static {
+         ClassLoader.registerAsParallelCapable();
+     }
+ 
+-    PluginClassLoader(@NotNull final JavaPluginLoader loader, @Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader) throws IOException, InvalidPluginException, MalformedURLException {
++    @org.jetbrains.annotations.ApiStatus.Internal // Paper
++    public PluginClassLoader(@Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader, io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext) throws IOException, InvalidPluginException, MalformedURLException { // Paper
+         super(new URL[] {file.toURI().toURL()}, parent);
+-        Preconditions.checkArgument(loader != null, "Loader cannot be null");
++        this.loader = null; // Paper - pass null into loader field
+ 
+-        this.loader = loader;
+         this.description = description;
+         this.dataFolder = dataFolder;
+         this.file = file;
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+         this.url = file.toURI().toURL();
+         this.libraryLoader = libraryLoader;
+ 
++
++        // Paper start
++        this.classLoaderGroup = io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage.instance().registerSpigotGroup(this); // Paper
++        this.dependencyContext = dependencyContext;
++        // Paper end
+         try {
+             Class<?> jarClass;
+             try {
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+         return findResources(name);
+     }
+ 
++    // Paper start
++    @Override
++    public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException {
++        return this.loadClass0(name, resolve, checkGlobal, checkLibraries);
++    }
++    @Override
++    public io.papermc.paper.plugin.configuration.PluginMeta getConfiguration() {
++        return this.description;
++    }
++
++    @Override
++    public void init(JavaPlugin plugin) {
++        this.initialize(plugin);
++    }
++    // Paper end
++
+     @Override
+     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+         return loadClass0(name, resolve, true, true);
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+ 
+         if (checkGlobal) {
+             // This ignores the libraries of other plugins, unless they are transitive dependencies.
+-            Class<?> result = loader.getClassByName(name, resolve, description);
++            Class<?> result = this.classLoaderGroup.getClassByName(name, resolve, this); // Paper
+ 
+             if (result != null) {
+                 // If the class was loaded from a library instead of a PluginClassLoader, we can assume that its associated plugin is a transitive dependency and can therefore skip this check.
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+ 
+                     if (provider != description
+                             && !seenIllegalAccess.contains(provider.getName())
+-                            && !((SimplePluginManager) loader.server.getPluginManager()).isTransitiveDepend(description, provider)) {
++                        && !this.dependencyContext.isTransitiveDependency(description, provider)) { // Paper
+ 
+                         seenIllegalAccess.add(provider.getName());
+                         if (plugin != null) {
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+                     throw new ClassNotFoundException(name, ex);
+                 }
+ 
+-                classBytes = loader.server.getUnsafe().processClass(description, path, classBytes);
++                classBytes = org.bukkit.Bukkit.getServer().getUnsafe().processClass(description, path, classBytes); // Paper
+ 
+                 int dot = name.lastIndexOf('.');
+                 if (dot != -1) {
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+                 result = super.findClass(name);
+             }
+ 
+-            loader.setClass(name, result);
+             classes.put(name, result);
++            this.setClass(name, result); // Paper
+         }
+ 
+         return result;
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+     @Override
+     public void close() throws IOException {
+         try {
++            // Paper start
++            Collection<Class<?>> classes = getClasses();
++            for (Class<?> clazz : classes) {
++                removeClass(clazz);
++            }
++            // Paper end
+             super.close();
+         } finally {
+             jar.close();
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+         return classes.values();
+     }
+ 
+-    synchronized void initialize(@NotNull JavaPlugin javaPlugin) {
++    public synchronized void initialize(@NotNull JavaPlugin javaPlugin) { // Paper
+         Preconditions.checkArgument(javaPlugin != null, "Initializing plugin cannot be null");
+         Preconditions.checkArgument(javaPlugin.getClass().getClassLoader() == this, "Cannot initialize plugin outside of this class loader");
+         if (this.plugin != null || this.pluginInit != null) {
+@@ -0,0 +0,0 @@ final class PluginClassLoader extends URLClassLoader {
+         pluginState = new IllegalStateException("Initial initialization");
+         this.pluginInit = javaPlugin;
+ 
+-        javaPlugin.init(loader, loader.server, description, dataFolder, file, this);
++        javaPlugin.init(null, org.bukkit.Bukkit.getServer(), description, dataFolder, file, this); // Paper
++    }
++
++    // Paper start
++    @Override
++    public String toString() {
++        JavaPlugin currPlugin = plugin != null ? plugin : pluginInit;
++        return "PluginClassLoader{" +
++                   "plugin=" + currPlugin +
++                   ", pluginEnabled=" + (currPlugin == null ? "uninitialized" : currPlugin.isEnabled()) +
++                   ", url=" + file +
++                   '}';
++    }
++
++    void setClass(@NotNull final String name, @NotNull final Class<?> clazz) {
++        if (org.bukkit.configuration.serialization.ConfigurationSerializable.class.isAssignableFrom(clazz)) {
++            Class<? extends org.bukkit.configuration.serialization.ConfigurationSerializable> serializable = clazz.asSubclass(org.bukkit.configuration.serialization.ConfigurationSerializable.class);
++            org.bukkit.configuration.serialization.ConfigurationSerialization.registerClass(serializable);
++        }
++    }
++
++    private void removeClass(@NotNull Class<?> clazz) {
++        if (org.bukkit.configuration.serialization.ConfigurationSerializable.class.isAssignableFrom(clazz)) {
++            Class<? extends org.bukkit.configuration.serialization.ConfigurationSerializable> serializable = clazz.asSubclass(org.bukkit.configuration.serialization.ConfigurationSerializable.class);
++            org.bukkit.configuration.serialization.ConfigurationSerialization.unregisterClass(serializable);
++        }
+     }
++    // Paper end
+ }
+diff --git a/src/test/java/io/papermc/paper/testing/TestServer.java b/src/test/java/io/papermc/paper/testing/TestServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/test/java/io/papermc/paper/testing/TestServer.java
++++ b/src/test/java/io/papermc/paper/testing/TestServer.java
+@@ -0,0 +0,0 @@ public final class TestServer {
+ 
+         when(dummyServer.getTag(anyString(), any(NamespacedKey.class), any())).thenAnswer(ignored -> new EmptyTag());
+ 
+-        final PluginManager pluginManager = new SimplePluginManager(dummyServer, new SimpleCommandMap(dummyServer));
+-        when(dummyServer.getPluginManager()).thenReturn(pluginManager);
+-
+         when(dummyServer.getRegistry(any())).thenAnswer(ignored -> new EmptyRegistry());
+         when(dummyServer.getScoreboardCriteria(anyString())).thenReturn(null);
+ 
+diff --git a/src/test/java/org/bukkit/event/SyntheticEventTest.java b/src/test/java/org/bukkit/event/SyntheticEventTest.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/test/java/org/bukkit/event/SyntheticEventTest.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package org.bukkit.event;
+-
+-import org.bukkit.plugin.PluginLoader;
+-import org.bukkit.plugin.SimplePluginManager;
+-import org.bukkit.plugin.TestPlugin;
+-import org.bukkit.plugin.java.JavaPluginLoader;
+-import org.junit.Assert;
+-import org.junit.Test;
+-
+-public class SyntheticEventTest {
+-    @SuppressWarnings("deprecation")
+-    @Test
+-    public void test() {
+-        io.papermc.paper.testing.TestServer.setup(); // Paper
+-        final JavaPluginLoader loader = new JavaPluginLoader(org.bukkit.Bukkit.getServer()); // Paper
+-        TestPlugin plugin = new TestPlugin(getClass().getName()) {
+-            @Override
+-            public PluginLoader getPluginLoader() {
+-                return loader;
+-            }
+-        };
+-        SimplePluginManager pluginManager = new SimplePluginManager(org.bukkit.Bukkit.getServer(), null); // Paper
+-
+-        TestEvent event = new TestEvent(false);
+-        Impl impl = new Impl();
+-
+-        pluginManager.registerEvents(impl, plugin);
+-        pluginManager.callEvent(event);
+-
+-        Assert.assertEquals(1, impl.callCount);
+-    }
+-
+-    public abstract static class Base<E extends Event> implements Listener {
+-        int callCount = 0;
+-
+-        public void accept(E evt) {
+-            callCount++;
+-        }
+-    }
+-
+-    public static class Impl extends Base<TestEvent> {
+-        @Override
+-        @EventHandler
+-        public void accept(TestEvent evt) {
+-            super.accept(evt);
+-        }
+-    }
+-}
+diff --git a/src/test/java/org/bukkit/plugin/PluginManagerTest.java b/src/test/java/org/bukkit/plugin/PluginManagerTest.java
+deleted file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- a/src/test/java/org/bukkit/plugin/PluginManagerTest.java
++++ /dev/null
+@@ -0,0 +0,0 @@
+-package org.bukkit.plugin;
+-
+-import static org.hamcrest.Matchers.*;
+-import static org.junit.Assert.*;
+-import org.bukkit.event.Event;
+-import org.bukkit.event.TestEvent;
+-import org.bukkit.permissions.Permission;
+-import org.junit.After;
+-import org.junit.Test;
+-
+-public class PluginManagerTest {
+-    private class MutableObject {
+-        volatile Object value = null;
+-    }
+-
+-    private static final PluginManager pm = org.bukkit.Bukkit.getServer().getPluginManager(); // Paper
+-
+-    private final MutableObject store = new MutableObject();
+-
+-    @Test
+-    public void testAsyncSameThread() {
+-        final Event event = new TestEvent(true);
+-        try {
+-            pm.callEvent(event);
+-        } catch (IllegalStateException ex) {
+-            assertThat(event.getEventName() + " cannot be triggered asynchronously from primary server thread.", is(ex.getMessage()));
+-            return;
+-        }
+-        throw new IllegalStateException("No exception thrown");
+-    }
+-
+-    @Test
+-    public void testSyncSameThread() {
+-        final Event event = new TestEvent(false);
+-        pm.callEvent(event);
+-    }
+-
+-    @Test
+-    public void testAsyncLocked() throws InterruptedException {
+-        final Event event = new TestEvent(true);
+-        Thread secondThread = new Thread(
+-            new Runnable() {
+-                @Override
+-                public void run() {
+-                    try {
+-                        synchronized (pm) {
+-                            pm.callEvent(event);
+-                        }
+-                    } catch (Throwable ex) {
+-                        store.value = ex;
+-                    }
+-                }
+-            }
+-        );
+-        secondThread.start();
+-        secondThread.join();
+-        assertThat(store.value, is(instanceOf(IllegalStateException.class)));
+-        assertThat(event.getEventName() + " cannot be triggered asynchronously from inside synchronized code.", is(((Throwable) store.value).getMessage()));
+-    }
+-
+-    @Test
+-    public void testAsyncUnlocked() throws InterruptedException {
+-        final Event event = new TestEvent(true);
+-        Thread secondThread = new Thread(
+-            new Runnable() {
+-                @Override
+-                public void run() {
+-                    try {
+-                        pm.callEvent(event);
+-                    } catch (Throwable ex) {
+-                        store.value = ex;
+-                    }
+-                }
+-            }
+-        );
+-        secondThread.start();
+-        secondThread.join();
+-        if (store.value != null) {
+-            throw new RuntimeException((Throwable) store.value);
+-        }
+-    }
+-
+-    @Test
+-    public void testSyncUnlocked() throws InterruptedException {
+-        final Event event = new TestEvent(false);
+-        Thread secondThread = new Thread(
+-            new Runnable() {
+-                @Override
+-                public void run() {
+-                    try {
+-                        pm.callEvent(event);
+-                    } catch (Throwable ex) {
+-                        store.value = ex;
+-                        assertThat(event.getEventName() + " cannot be triggered asynchronously from another thread.", is(ex.getMessage()));
+-                        return;
+-                    }
+-                }
+-            }
+-        );
+-        secondThread.start();
+-        secondThread.join();
+-        if (store.value == null) {
+-            throw new IllegalStateException("No exception thrown");
+-        }
+-    }
+-
+-    @Test
+-    public void testSyncLocked() throws InterruptedException {
+-        final Event event = new TestEvent(false);
+-        Thread secondThread = new Thread(
+-            new Runnable() {
+-                @Override
+-                public void run() {
+-                    try {
+-                        synchronized (pm) {
+-                            pm.callEvent(event);
+-                        }
+-                    } catch (Throwable ex) {
+-                        store.value = ex;
+-                        assertThat(event.getEventName() + " cannot be triggered asynchronously from another thread.", is(ex.getMessage()));
+-                        return;
+-                    }
+-                }
+-            }
+-        );
+-        secondThread.start();
+-        secondThread.join();
+-        if (store.value == null) {
+-            throw new IllegalStateException("No exception thrown");
+-        }
+-    }
+-
+-    @Test
+-    public void testRemovePermissionByNameLower() {
+-        this.testRemovePermissionByName("lower");
+-    }
+-
+-    @Test
+-    public void testRemovePermissionByNameUpper() {
+-        this.testRemovePermissionByName("UPPER");
+-    }
+-
+-    @Test
+-    public void testRemovePermissionByNameCamel() {
+-        this.testRemovePermissionByName("CaMeL");
+-    }
+-
+-    public void testRemovePermissionByPermissionLower() {
+-        this.testRemovePermissionByPermission("lower");
+-    }
+-
+-    @Test
+-    public void testRemovePermissionByPermissionUpper() {
+-        this.testRemovePermissionByPermission("UPPER");
+-    }
+-
+-    @Test
+-    public void testRemovePermissionByPermissionCamel() {
+-        this.testRemovePermissionByPermission("CaMeL");
+-    }
+-
+-    private void testRemovePermissionByName(final String name) {
+-        final Permission perm = new Permission(name);
+-        pm.addPermission(perm);
+-        assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
+-        pm.removePermission(name);
+-        assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
+-    }
+-
+-    private void testRemovePermissionByPermission(final String name) {
+-        final Permission perm = new Permission(name);
+-        pm.addPermission(perm);
+-        assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
+-        pm.removePermission(perm);
+-        assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
+-    }
+-
+-    @After
+-    public void tearDown() {
+-        pm.clearPlugins();
+-        assertThat(pm.getPermissions(), is(empty()));
+-    }
+-}
+diff --git a/src/test/java/org/bukkit/plugin/TestPlugin.java b/src/test/java/org/bukkit/plugin/TestPlugin.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/test/java/org/bukkit/plugin/TestPlugin.java
++++ b/src/test/java/org/bukkit/plugin/TestPlugin.java
+@@ -0,0 +0,0 @@ public class TestPlugin extends PluginBase {
+     public PluginDescriptionFile getDescription() {
+         return new PluginDescriptionFile(pluginName, "1.0", "test.test");
+     }
++    // Paper start
++    @Override
++    public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
++        return getDescription();
++    }
++    // Paper end
+ 
+     @Override
+     public FileConfiguration getConfig() {
diff --git a/patches/api/Prioritise-own-classes-where-possible.patch b/patches/api/Prioritise-own-classes-where-possible.patch
deleted file mode 100644
index 9f75bf62cc..0000000000
--- a/patches/api/Prioritise-own-classes-where-possible.patch
+++ /dev/null
@@ -1,86 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Mariell Hoversholm <proximyst@proximyst.com>
-Date: Mon, 27 Apr 2020 12:31:59 +0200
-Subject: [PATCH] Prioritise own classes where possible
-
-This adds the server property `Paper.DisableClassPrioritization` to disable
-prioritization of own classes for plugins' classloaders.
-
-This value is by default not present, and this will therefore break any
-plugins which abuse behaviour related to not using their own classes
-while still loading their own. This is often an issue with failing to
-relocate or shade properly, such as when shading plugin APIs like Vault.
-
-A plugin's classloader will first look in the same jar as it is loading
-in for a requested class, then load it. It does not re-use other
-plugins' classes if it has the chance to avoid doing so.
-
-If a class is not found in the same jar as it is loading for and it does
-find it elsewhere, it will still choose the class elsewhere. This is
-intended behaviour, as it will only prioritise classes it has in its own
-jar, no other plugins' classes will be prioritised in any other order
-than the one they were registered in.
-
-The patch in general terms just loads the class in the plugin's jar
-before it starts looking elsewhere for it.
-
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ import org.yaml.snakeyaml.error.YAMLException;
-  */
- public final class JavaPluginLoader implements PluginLoader {
-     final Server server;
-+    private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization"); // Paper
-     private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")};
-     private final Map<String, java.util.concurrent.locks.ReentrantReadWriteLock> classLoadLock = new java.util.HashMap<String, java.util.concurrent.locks.ReentrantReadWriteLock>(); // Paper
-     private final Map<String, Integer> classLoadLockCount = new java.util.HashMap<String, Integer>(); // Paper
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
- 
-     @Nullable
-     Class<?> getClassByName(final String name, boolean resolve, PluginDescriptionFile description) {
-+        // Paper start - prioritize self
-+        return getClassByName(name, resolve, description, null);
-+    }
-+    Class<?> getClassByName(final String name, boolean resolve, PluginDescriptionFile description, PluginClassLoader requester) {
-+        // Paper end
-         // Paper start - make MT safe
-         java.util.concurrent.locks.ReentrantReadWriteLock lock;
-         synchronized (classLoadLock) {
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
-             classLoadLockCount.compute(name, (x, prev) -> prev != null ? prev + 1 : 1);
-         }
-         lock.writeLock().lock();try {
-+            // Paper start - prioritize self
-+            if (!DISABLE_CLASS_PRIORITIZATION && requester != null) {
-+                try {
-+                return requester.loadClass0(name, false, false, ((SimplePluginManager) server.getPluginManager()).isTransitiveDepend(description, requester.getDescription()));
-+                } catch (ClassNotFoundException cnfe) {}
-+            }
-+            // Paper end
-         // Paper end
-         for (PluginClassLoader loader : loaders) {
-             try {
-diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
-     public JavaPlugin getPlugin() { return plugin; } // Spigot
-     private final JavaPluginLoader loader;
-     private final Map<String, Class<?>> classes = new ConcurrentHashMap<String, Class<?>>();
--    private final PluginDescriptionFile description;
-+    private final PluginDescriptionFile description; PluginDescriptionFile getDescription() { return description; } // Paper
-     private final File dataFolder;
-     private final File file;
-     private final JarFile jar;
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
- 
-         if (checkGlobal) {
-             // This ignores the libraries of other plugins, unless they are transitive dependencies.
--            Class<?> result = loader.getClassByName(name, resolve, description);
-+            Class<?> result = loader.getClassByName(name, resolve, description, this);  // Paper - prioritize self
- 
-             if (result != null) {
-                 // If the class was loaded from a library instead of a PluginClassLoader, we can assume that its associated plugin is a transitive dependency and can therefore skip this check.
diff --git a/patches/api/Provide-a-useful-PluginClassLoader-toString.patch b/patches/api/Provide-a-useful-PluginClassLoader-toString.patch
deleted file mode 100644
index 6380e62aaf..0000000000
--- a/patches/api/Provide-a-useful-PluginClassLoader-toString.patch
+++ /dev/null
@@ -1,30 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Shane Freeder <theboyetronic@gmail.com>
-Date: Sun, 31 May 2020 15:26:17 +0100
-Subject: [PATCH] Provide a useful PluginClassLoader#toString
-
-There are several cases where the plugin classloader may be dumped to the logs,
-however, this provides no indication of the owner of the classloader, making
-these messages effectively useless, this patch rectifies this
-
-diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
-         javaPlugin.logger = this.logger; // Paper - set logger
-         javaPlugin.init(loader, loader.server, description, dataFolder, file, this);
-     }
-+
-+    // Paper start
-+    @Override
-+    public String toString() {
-+        JavaPlugin currPlugin = plugin != null ? plugin : pluginInit;
-+        return "PluginClassLoader{" +
-+                   "plugin=" + currPlugin +
-+                   ", pluginEnabled=" + (currPlugin == null ? "uninitialized" : currPlugin.isEnabled()) +
-+                   ", url=" + file +
-+                   '}';
-+    }
-+    // Paper end
- }
diff --git a/patches/api/Remove-deadlock-risk-in-firing-async-events.patch b/patches/api/Remove-deadlock-risk-in-firing-async-events.patch
deleted file mode 100644
index b41013e8f4..0000000000
--- a/patches/api/Remove-deadlock-risk-in-firing-async-events.patch
+++ /dev/null
@@ -1,124 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Aikar <aikar@aikar.co>
-Date: Sun, 9 Sep 2018 00:32:05 -0400
-Subject: [PATCH] Remove deadlock risk in firing async events
-
-The PluginManager incorrectly used synchronization on firing any event
-that was marked as synchronous.
-
-This synchronized did not even protect any concurrency risk as
-handlers were already thread safe in terms of mutations during event
-dispatch.
-
-The way it was used, has commonly led to deadlocks on the server,
-which results in a hard crash.
-
-This change removes the synchronize and adds some protection around enable/disable
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-      * @return true if the plugin is enabled, otherwise false
-      */
-     @Override
--    public boolean isPluginEnabled(@Nullable Plugin plugin) {
-+    public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) { // Paper - synchronize
-         if ((plugin != null) && (plugins.contains(plugin))) {
-             return plugin.isEnabled();
-         } else {
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-     }
- 
-     @Override
--    public void enablePlugin(@NotNull final Plugin plugin) {
-+    public synchronized void enablePlugin(@NotNull final Plugin plugin) { // Paper - synchronize
-         if (!plugin.isEnabled()) {
-             List<Command> pluginCommands = PluginCommandYamlParser.parse(plugin);
- 
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-     // Paper end
- 
-     @Override
--    public void disablePlugin(@NotNull final Plugin plugin) {
-+    public synchronized void disablePlugin(@NotNull final Plugin plugin) { // Paper - synchronize
-         if (plugin.isEnabled()) {
-             try {
-                 plugin.getPluginLoader().disablePlugin(plugin);
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-             defaultPerms.get(false).clear();
-         }
-     }
-+    private void fireEvent(Event event) { callEvent(event); } // Paper - support old method incase plugin uses reflection
- 
-     /**
-      * Calls an event with the given details.
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-      */
-     @Override
-     public void callEvent(@NotNull Event event) {
--        if (event.isAsynchronous()) {
--            if (Thread.holdsLock(this)) {
--                throw new IllegalStateException(event.getEventName() + " cannot be triggered asynchronously from inside synchronized code.");
--            }
--            if (server.isPrimaryThread()) {
--                throw new IllegalStateException(event.getEventName() + " cannot be triggered asynchronously from primary server thread.");
--            }
--        } else {
--            if (!server.isPrimaryThread()) {
--                throw new IllegalStateException(event.getEventName() + " cannot be triggered asynchronously from another thread.");
--            }
-+        // Paper - replace callEvent by merging to below method
-+        if (event.isAsynchronous() && server.isPrimaryThread()) {
-+            throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
-+        } else if (!event.isAsynchronous() && !server.isPrimaryThread()) {
-+            throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
-         }
- 
--        fireEvent(event);
--    }
--
--    private void fireEvent(@NotNull Event event) {
-         HandlerList handlers = event.getHandlers();
-         RegisteredListener[] listeners = handlers.getRegisteredListeners();
- 
-diff --git a/src/test/java/org/bukkit/plugin/PluginManagerTest.java b/src/test/java/org/bukkit/plugin/PluginManagerTest.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/test/java/org/bukkit/plugin/PluginManagerTest.java
-+++ b/src/test/java/org/bukkit/plugin/PluginManagerTest.java
-@@ -0,0 +0,0 @@ public class PluginManagerTest {
-     private static final PluginManager pm = org.bukkit.Bukkit.getServer().getPluginManager(); // Paper
- 
-     private final MutableObject store = new MutableObject();
--
-+/* // Paper start - remove unneeded test
-     @Test
-     public void testAsyncSameThread() {
-         final Event event = new TestEvent(true);
-@@ -0,0 +0,0 @@ public class PluginManagerTest {
-             return;
-         }
-         throw new IllegalStateException("No exception thrown");
--    }
-+    }*/ // Paper end
- 
-     @Test
-     public void testSyncSameThread() {
-         final Event event = new TestEvent(false);
-         pm.callEvent(event);
-     }
--
-+/* // Paper start - remove unneeded test
-     @Test
-     public void testAsyncLocked() throws InterruptedException {
-         final Event event = new TestEvent(true);
-@@ -0,0 +0,0 @@ public class PluginManagerTest {
-         if (store.value == null) {
-             throw new IllegalStateException("No exception thrown");
-         }
--    }
-+    } */ // Paper
- 
-     @Test
-     public void testRemovePermissionByNameLower() {
diff --git a/patches/api/Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch b/patches/api/Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch
index e13e260a58..f790f54550 100644
--- a/patches/api/Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch
+++ b/patches/api/Rewrite-LogEvents-to-contain-the-source-jars-in-stac.patch
@@ -8,12 +8,12 @@ diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/m
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
-@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot
-     }
+@@ -0,0 +0,0 @@ public final class PluginClassLoader extends URLClassLoader implements io.paperm
  
-     PluginClassLoader(@NotNull final JavaPluginLoader loader, @Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader) throws IOException, InvalidPluginException, MalformedURLException {
+     @org.jetbrains.annotations.ApiStatus.Internal // Paper
+     public PluginClassLoader(@Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader, io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext) throws IOException, InvalidPluginException, MalformedURLException { // Paper
 -        super(new URL[] {file.toURI().toURL()}, parent);
-+        super(file.getName(), new URL[] {file.toURI().toURL()}, parent); // Paper - rewrite LogEvents to contain source jar info
-         Preconditions.checkArgument(loader != null, "Loader cannot be null");
++        super(file.getName(), new URL[] {file.toURI().toURL()}, parent);
+         this.loader = null; // Paper - pass null into loader field
  
-         this.loader = loader;
+         this.description = description;
diff --git a/patches/api/Test-changes.patch b/patches/api/Test-changes.patch
index b7039e17b4..362859abbb 100644
--- a/patches/api/Test-changes.patch
+++ b/patches/api/Test-changes.patch
@@ -230,6 +230,18 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      private static void collectClasses(@NotNull File from, @NotNull Map<String, ClassNode> to) throws IOException {
 @@ -0,0 +0,0 @@ public class AnnotationTest {
+             // Exceptions are excluded
+             return false;
+         }
++        // Paper start
++        if (isInternal(clazz.invisibleAnnotations)) {
++            return false;
++        }
++        // Paper end
+ 
+         for (String excludedClass : EXCLUDED_CLASSES) {
+             if (excludedClass.equals(clazz.name)) {
+@@ -0,0 +0,0 @@ public class AnnotationTest {
  
      private static boolean isMethodIncluded(@NotNull ClassNode clazz, @NotNull MethodNode method, @NotNull Map<String, ClassNode> allClasses) {
          // Exclude private, synthetic and deprecated methods
@@ -239,8 +251,31 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
  
 @@ -0,0 +0,0 @@ public class AnnotationTest {
+         if ("<init>".equals(method.name) && isAnonymous(clazz)) {
+             return false;
+         }
++        // Paper start
++        if (isInternal(method.invisibleAnnotations)) {
++            return false;
++        }
++        // Paper end
+ 
          return true;
      }
++    // Paper start
++    private static boolean isInternal(List<? extends AnnotationNode> annotations) {
++        if (annotations == null) {
++            return false;
++        }
++        for (AnnotationNode node : annotations) {
++            if (node.desc.equals("Lorg/jetbrains/annotations/ApiStatus$Internal;")) {
++                return true;
++            }
++        }
++
++        return false;
++    }
++    // Paper end
  
 -    private static boolean isWellAnnotated(@Nullable List<AnnotationNode> annotations) {
 +    private static boolean isWellAnnotated(@Nullable List<? extends AnnotationNode> annotations) { // Paper
diff --git a/patches/api/Timings-v2.patch b/patches/api/Timings-v2.patch
index ae392c11ef..ad8e63cc03 100644
--- a/patches/api/Timings-v2.patch
+++ b/patches/api/Timings-v2.patch
@@ -2846,9 +2846,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      Material fromLegacy(Material material);
 @@ -0,0 +0,0 @@ public interface UnsafeValues {
-     String getTranslationKey(EntityType entityType);
- 
-     String getTranslationKey(ItemStack itemStack);
+         return !Bukkit.getUnsafe().isSupportedApiVersion(plugin.getDescription().getAPIVersion());
+     }
+     // Paper end
 +
 +    // Paper start
 +    /**
@@ -2893,6 +2893,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      protected String usageMessage;
      private String permission;
      private net.kyori.adventure.text.Component permissionMessage; // Paper
+-    public org.spigotmc.CustomTimingsHandler timings; // Spigot
 +    public co.aikar.timings.Timing timings; // Paper
 +    @NotNull public String getTimingName() {return getName();} // Paper
  
@@ -3093,7 +3094,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@ public class SimpleCommandMap implements CommandMap {
          register("bukkit", new VersionCommand("version"));
          register("bukkit", new ReloadCommand("reload"));
-         register("bukkit", new PluginsCommand("plugins"));
+         //register("bukkit", new PluginsCommand("plugins")); // Paper
 -        register("bukkit", new TimingsCommand("timings"));
 +        register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Paper
      }
@@ -3434,9 +3435,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
  
 @@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-     @Override
      @Nullable
      public synchronized Plugin getPlugin(@NotNull String name) {
+         if (true) {return this.paperPluginManager.getPlugin(name);} // Paper
 -        return lookupNames.get(name.replace(' ', '_'));
 +        return lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
      }
@@ -3453,9 +3454,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          } else {
              getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
 @@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
- 
      @Override
      public boolean useTimings() {
+         if (true) {return this.paperPluginManager.useTimings();} // Paper
 -        return useTimings;
 +        return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot
      }
@@ -3468,7 +3469,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        useTimings = use;
 +        co.aikar.timings.Timings.setTimingsEnabled(use); // Paper
      }
- }
+ 
+     // Paper start
 diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
@@ -3517,11 +3519,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 +++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
 @@ -0,0 +0,0 @@ import org.jetbrains.annotations.Nullable;
- /**
-  * A ClassLoader for plugins, to allow shared classes across multiple plugins
   */
--final class PluginClassLoader extends URLClassLoader {
-+public final class PluginClassLoader extends URLClassLoader { // Spigot
+ @org.jetbrains.annotations.ApiStatus.Internal // Paper
+ public final class PluginClassLoader extends URLClassLoader implements io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader { // Paper
 +    public JavaPlugin getPlugin() { return plugin; } // Spigot
      private final JavaPluginLoader loader;
      private final Map<String, Class<?>> classes = new ConcurrentHashMap<String, Class<?>>();
diff --git a/patches/api/Update-Folder-Uses-Plugin-Name.patch b/patches/api/Update-Folder-Uses-Plugin-Name.patch
deleted file mode 100644
index 3f59b529ec..0000000000
--- a/patches/api/Update-Folder-Uses-Plugin-Name.patch
+++ /dev/null
@@ -1,86 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: Xemorr <31805746+Xemorr@users.noreply.github.com>
-Date: Fri, 1 Apr 2022 19:57:40 +0100
-Subject: [PATCH] Update Folder Uses Plugin Name
-
-
-diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-     public synchronized Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
-         Preconditions.checkArgument(file != null, "File cannot be null");
- 
--        checkUpdate(file);
-+        file = checkUpdate(file); // Paper - update the reference in case checkUpdate renamed it
- 
-         Set<Pattern> filters = fileAssociations.keySet();
-         Plugin result = null;
-@@ -0,0 +0,0 @@ public final class SimplePluginManager implements PluginManager {
-         return result;
-     }
- 
--    private void checkUpdate(@NotNull File file) {
-+    // Paper start - Update Folder Uses Plugin Name to replace
-+    /**
-+     * Replaces a plugin with a plugin of the same plugin name in the update folder.
-+     * @param file
-+     * @throws InvalidPluginException
-+     */
-+    private File checkUpdate(@NotNull File file) throws InvalidPluginException {
-         if (updateDirectory == null || !updateDirectory.isDirectory()) {
--            return;
-+            return file;
-+        }
-+        PluginLoader pluginLoader = getPluginLoader(file);
-+        try {
-+            String pluginName = pluginLoader.getPluginDescription(file).getName();
-+            for (File updateFile : updateDirectory.listFiles()) {
-+                if (!updateFile.isFile()) continue;
-+                PluginLoader updatePluginLoader = getPluginLoader(updateFile);
-+                if (updatePluginLoader == null) continue;
-+                String updatePluginName;
-+                try {
-+                     updatePluginName = updatePluginLoader.getPluginDescription(updateFile).getName();
-+                     // We failed to load this data for some reason, so, we'll skip over this
-+                } catch (InvalidDescriptionException ex) {
-+                    continue;
-+                }
-+                if (!pluginName.equals(updatePluginName)) continue;
-+                try {
-+                    java.nio.file.Files.copy(updateFile.toPath(), file.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
-+                } catch (java.io.IOException exception) {
-+                    server.getLogger().log(Level.SEVERE, "Could not copy '" + updateFile.getPath() + "' to '" + file.getPath() + "' in update plugin process", exception);
-+                    continue;
-+                }
-+                File newName = new File(file.getParentFile(), updateFile.getName());
-+                file.renameTo(newName);
-+                updateFile.delete();
-+                return newName;
-+            }
-         }
-+        catch (InvalidDescriptionException e) {
-+            throw new InvalidPluginException(e);
-+        }
-+        return file;
-+    }
- 
--        File updateFile = new File(updateDirectory, file.getName());
--        if (updateFile.isFile() && FileUtil.copy(updateFile, file)) {
--            updateFile.delete();
-+    @Nullable
-+    private PluginLoader getPluginLoader(File file) {
-+        Set<Pattern> filters = fileAssociations.keySet();
-+        for (Pattern filter : filters) {
-+            Matcher match = filter.matcher(file.getName());
-+            if (match.find()) {
-+                return fileAssociations.get(filter);
-+            }
-         }
-+        return null;
-     }
-+    // Paper end
- 
-     /**
-      * Checks if the given plugin is loaded and returns it when applicable
diff --git a/patches/api/Use-ASM-for-event-executors.patch b/patches/api/Use-ASM-for-event-executors.patch
index 830aa0c5fb..245cbad56f 100644
--- a/patches/api/Use-ASM-for-event-executors.patch
+++ b/patches/api/Use-ASM-for-event-executors.patch
@@ -368,30 +368,3 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end
  }
-diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
-@@ -0,0 +0,0 @@ public final class JavaPluginLoader implements PluginLoader {
-                 }
-             }
- 
--            EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Paper
--                @Override
--                public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { // Paper
--                    try {
--                        if (!eventClass.isAssignableFrom(event.getClass())) {
--                            return;
--                        }
--                        method.invoke(listener, event);
--                    } catch (InvocationTargetException ex) {
--                        throw new EventException(ex.getCause());
--                    } catch (Throwable t) {
--                        throw new EventException(t);
--                    }
--                }
--            }, plugin, method, eventClass); // Paper
-+            EventExecutor executor = new co.aikar.timings.TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); // Paper // Paper (Yes.) - Use factory method `EventExecutor.create()`
-             if (false) { // Spigot - RL handles useTimings check now
-                 eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
-             } else {
diff --git a/patches/server/Add-CraftMagicNumbers.isSupportedApiVersion.patch b/patches/server/Add-CraftMagicNumbers.isSupportedApiVersion.patch
deleted file mode 100644
index 39ef9e26a9..0000000000
--- a/patches/server/Add-CraftMagicNumbers.isSupportedApiVersion.patch
+++ /dev/null
@@ -1,22 +0,0 @@
-From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
-From: BlackHole <black-hole@live.com>
-Date: Sun, 15 Dec 2019 19:12:39 +0100
-Subject: [PATCH] Add CraftMagicNumbers.isSupportedApiVersion()
-
-
-diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
-+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
-@@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
-     public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
-         return new com.destroystokyo.paper.PaperVersionFetcher();
-     }
-+
-+    @Override
-+    public boolean isSupportedApiVersion(String apiVersion) {
-+        return apiVersion != null && SUPPORTED_API.contains(apiVersion);
-+    }
-     // Paper end
- 
-     /**
diff --git a/patches/server/Add-Raw-Byte-ItemStack-Serialization.patch b/patches/server/Add-Raw-Byte-ItemStack-Serialization.patch
index e02614e295..ef47ed6e92 100644
--- a/patches/server/Add-Raw-Byte-ItemStack-Serialization.patch
+++ b/patches/server/Add-Raw-Byte-ItemStack-Serialization.patch
@@ -10,8 +10,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
 +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
 @@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
-     public boolean isSupportedApiVersion(String apiVersion) {
-         return apiVersion != null && SUPPORTED_API.contains(apiVersion);
+     public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() {
+         return new com.destroystokyo.paper.PaperVersionFetcher();
      }
 +
 +    @Override
diff --git a/patches/server/Add-command-line-option-to-load-extra-plugin-jars-no.patch b/patches/server/Add-command-line-option-to-load-extra-plugin-jars-no.patch
index a847b68040..22811394ef 100644
--- a/patches/server/Add-command-line-option-to-load-extra-plugin-jars-no.patch
+++ b/patches/server/Add-command-line-option-to-load-extra-plugin-jars-no.patch
@@ -11,26 +11,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
 +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
 @@ -0,0 +0,0 @@ public final class CraftServer implements Server {
-     public void loadPlugins() {
-         this.pluginManager.registerInterface(JavaPluginLoader.class);
- 
--        File pluginFolder = (File) console.options.valueOf("plugins");
-+        File pluginFolder = this.getPluginsFolder(); // Paper
- 
--        if (pluginFolder.exists()) {
--            Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder);
-+        // Paper start
-+        if (true || pluginFolder.exists()) {
-+            if (!pluginFolder.exists()) {
-+                pluginFolder.mkdirs();
-+            }
-+            Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder, this.extraPluginJars());
-+            // Paper end
-             for (Plugin plugin : plugins) {
-                 try {
-                     String message = String.format("Loading %s", plugin.getDescription().getFullName());
-@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
-         }
+         io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation
      }
  
 +    // Paper start
diff --git a/patches/server/Add-debug-for-sync-chunk-loads.patch b/patches/server/Add-debug-for-sync-chunk-loads.patch
index 4075a8f74e..93cb7ce941 100644
--- a/patches/server/Add-debug-for-sync-chunk-loads.patch
+++ b/patches/server/Add-debug-for-sync-chunk-loads.patch
@@ -203,10 +203,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import io.papermc.paper.command.subcommands.ReloadCommand;
 +import io.papermc.paper.command.subcommands.SyncLoadInfoCommand;
  import io.papermc.paper.command.subcommands.VersionCommand;
+ import io.papermc.paper.command.subcommands.DumpPluginsCommand;
  import it.unimi.dsi.fastutil.Pair;
- import java.util.ArrayList;
 @@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
-         commands.put(Set.of("version"), new VersionCommand());
+         commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
          commands.put(Set.of("fixlight"), new FixLightCommand());
          commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
 +        commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand());
diff --git a/patches/server/Flat-bedrock-generator-settings.patch b/patches/server/Flat-bedrock-generator-settings.patch
index 052e452869..4ea5971df7 100644
--- a/patches/server/Flat-bedrock-generator-settings.patch
+++ b/patches/server/Flat-bedrock-generator-settings.patch
@@ -19,24 +19,6 @@ public net.minecraft.world.level.levelgen.SurfaceSystem getOrCreateRandomFactory
 
 Co-authored-by: Noah van der Aa <ndvdaa@gmail.com>
 
-diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
---- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
-+++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
-@@ -0,0 +0,0 @@ public class BuiltInRegistries {
-     }
- 
-     public static void bootStrap() {
-+        // Paper start
-+        bootStrap(() -> {});
-+    }
-+    public static void bootStrap(Runnable runnable) {
-+        // Paper end
-         createContents();
-+        runnable.run(); // Paper
-         freeze();
-         validate(REGISTRY);
-     }
 diff --git a/src/main/java/net/minecraft/data/worldgen/SurfaceRuleData.java b/src/main/java/net/minecraft/data/worldgen/SurfaceRuleData.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/data/worldgen/SurfaceRuleData.java
@@ -136,18 +118,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/net/minecraft/server/Bootstrap.java
 +++ b/src/main/java/net/minecraft/server/Bootstrap.java
 @@ -0,0 +0,0 @@ public class Bootstrap {
-                     EntitySelectorOptions.bootStrap();
-                     DispenseItemBehavior.bootStrap();
                      CauldronInteraction.bootStrap();
--                    BuiltInRegistries.bootStrap();
-+                    // Paper start - register custom flat bedrock
-+                    BuiltInRegistries.bootStrap(() -> {
+                     // Paper start
+                     BuiltInRegistries.bootStrap(() -> {
 +                        net.minecraft.core.Registry.register(net.minecraft.core.registries.BuiltInRegistries.MATERIAL_CONDITION, new net.minecraft.resources.ResourceLocation("paper", "bedrock_condition_source"), net.minecraft.data.worldgen.SurfaceRuleData.PaperBedrockConditionSource.CODEC.codec());
-+                    });
-+                    // Paper end
-                     Bootstrap.wrapStreams();
-                 }
-                 // CraftBukkit start - easier than fixing the decompile
+                         io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping
+                     });
+                     // Paper end
 diff --git a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java b/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java
diff --git a/patches/server/Paper-Plugins.patch b/patches/server/Paper-Plugins.patch
new file mode 100644
index 0000000000..cf53fdf829
--- /dev/null
+++ b/patches/server/Paper-Plugins.patch
@@ -0,0 +1,6780 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
+Date: Wed, 6 Jul 2022 23:00:31 -0400
+Subject: [PATCH] Paper Plugins
+
+
+diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
++++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
+@@ -0,0 +0,0 @@ import io.papermc.paper.command.subcommands.EntityCommand;
+ import io.papermc.paper.command.subcommands.HeapDumpCommand;
+ import io.papermc.paper.command.subcommands.ReloadCommand;
+ import io.papermc.paper.command.subcommands.VersionCommand;
++import io.papermc.paper.command.subcommands.DumpPluginsCommand;
+ import it.unimi.dsi.fastutil.Pair;
+ import java.util.ArrayList;
+ import java.util.Arrays;
+@@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
+         commands.put(Set.of("entity"), new EntityCommand());
+         commands.put(Set.of("reload"), new ReloadCommand());
+         commands.put(Set.of("version"), new VersionCommand());
++        commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
+ 
+         return commands.entrySet().stream()
+             .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
+diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/command/PaperCommands.java
++++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
+@@ -0,0 +0,0 @@ public final class PaperCommands {
+         COMMANDS.forEach((s, command) -> {
+             server.server.getCommandMap().register(s, "Paper", command);
+         });
++        server.server.getCommandMap().register("bukkit", new PaperPluginsCommand());
+     }
+ }
+diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.command;
++
++import com.google.common.collect.Lists;
++import io.leangen.geantyref.GenericTypeReflector;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import net.kyori.adventure.text.Component;
++import net.kyori.adventure.text.JoinConfiguration;
++import net.kyori.adventure.text.TextComponent;
++import net.kyori.adventure.text.event.ClickEvent;
++import net.kyori.adventure.text.format.NamedTextColor;
++import net.kyori.adventure.text.format.TextColor;
++import org.bukkit.Bukkit;
++import org.bukkit.command.CommandSender;
++import org.bukkit.command.defaults.BukkitCommand;
++import org.bukkit.craftbukkit.util.CraftMagicNumbers;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++import java.lang.reflect.Type;
++import java.util.ArrayList;
++import java.util.Arrays;
++import java.util.Collections;
++import java.util.List;
++import java.util.TreeMap;
++
++public class PaperPluginsCommand extends BukkitCommand {
++
++    private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218);
++
++    // TODO: LINK?
++    private static final Component SERVER_PLUGIN_INFO = Component.text("ℹ What is a server plugin?", INFO_COLOR)
++        .append(asPlainComponents("""
++            Server plugins can add new behavior to your server!
++            You can find new plugins on Paper's plugin repository, Hangar.
++                        
++            <link to hangar>
++            """));
++
++    private static final Component SERVER_INITIALIZER_INFO = Component.text("ℹ What is a server initializer?", INFO_COLOR)
++        .append(asPlainComponents("""
++            Server initializers are ran before your server
++            starts and are provided by paper plugins.
++            """));
++
++    private static final Component LEGACY_PLUGIN_INFO = Component.text("ℹ What is a legacy plugin?", INFO_COLOR)
++        .append(asPlainComponents("""
++            A legacy plugin is a plugin that was made on
++            very old unsupported versions of the game.
++                        
++            It is encouraged that you replace this plugin,
++            as they might not work in the future and may cause
++            performance issues.
++            """));
++
++    private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO);
++    private static final Component INFO_ICON_START = Component.text("ℹ ", INFO_COLOR);
++    private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209));
++    private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6));
++    private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY);
++    private static final Component PLUGIN_TICK_EMPTY = Component.text(" ");
++
++    private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken<PluginProvider<JavaPlugin>>() {}.getType();
++
++    public PaperPluginsCommand() {
++        super("plugins");
++        this.description = "Gets a list of plugins running on the server";
++        this.usageMessage = "/plugins";
++        this.setPermission("bukkit.command.plugins");
++        this.setAliases(Arrays.asList("pl"));
++    }
++
++    private static <T> List<Component> formatProviders(TreeMap<String, PluginProvider<T>> plugins) {
++        List<Component> components = new ArrayList<>(plugins.size());
++        for (PluginProvider<T> entry : plugins.values()) {
++            components.add(formatProvider(entry));
++        }
++
++        boolean isFirst = true;
++        List<Component> formattedSublists = new ArrayList<>();
++        /*
++        Split up the plugin list for each 10 plugins to get size down
++
++        Plugin List:
++        - Plugin 1, Plugin 2, .... Plugin 10,
++          Plugin 11, Plugin 12 ... Plugin 20,
++         */
++        for (List<Component> componentSublist : Lists.partition(components, 10)) {
++            Component component = Component.space();
++            if (isFirst) {
++                component = component.append(PLUGIN_TICK);
++                isFirst = false;
++            } else {
++                component = PLUGIN_TICK_EMPTY;
++                //formattedSublists.add(Component.empty()); // Add an empty line, the auto chat wrapping and this makes it quite jarring.
++            }
++
++            formattedSublists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist)));
++        }
++
++        return formattedSublists;
++    }
++
++    private static Component formatProvider(PluginProvider<?> provider) {
++        TextComponent.Builder builder = Component.text();
++        if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) {
++            builder.append(LEGACY_PLUGIN_STAR);
++        }
++
++        String name = provider.getMeta().getName();
++        Component pluginName = Component.text(name, fromStatus(provider))
++            .clickEvent(ClickEvent.runCommand("/version " + name));
++
++        builder.append(pluginName);
++
++        return builder.build();
++    }
++
++    private static Component asPlainComponents(String strings) {
++        net.kyori.adventure.text.TextComponent.Builder builder = Component.text();
++        for (String string : strings.split("\n")) {
++            builder.append(Component.newline());
++            builder.append(Component.text(string, NamedTextColor.WHITE));
++        }
++
++        return builder.build();
++    }
++
++    private static TextColor fromStatus(PluginProvider<?> provider) {
++        if (provider instanceof ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) {
++            ProviderStatus status = statusHolder.getLastProvidedStatus();
++
++            // Handle enabled/disabled game plugins
++            if (status == ProviderStatus.INITIALIZED && GenericTypeReflector.isSuperType(JAVA_PLUGIN_PROVIDER_TYPE, provider.getClass())) {
++                Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName());
++                // Plugin doesn't exist? Could be due to it being removed.
++                if (plugin == null) {
++                    return NamedTextColor.RED;
++                }
++
++                return plugin.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED;
++            }
++
++            return switch (status) {
++                case INITIALIZED -> NamedTextColor.GREEN;
++                case ERRORED -> NamedTextColor.RED;
++            };
++        } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) {
++            // Paper plugins will be skipped if their provider is skipped due to their initializer failing.
++            // Show them as red
++            return NamedTextColor.RED;
++        } else {
++            // Separated for future logic choice, but this indicated a provider that failed to load due to
++            // dependency issues or what not.
++            return NamedTextColor.RED;
++        }
++    }
++
++    @Override
++    public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) {
++        if (!this.testPermission(sender)) return true;
++
++        TreeMap<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
++        TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
++
++
++        for (PluginProvider<JavaPlugin> provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) {
++            PluginMeta configuration = provider.getMeta();
++
++            if (provider instanceof SpigotPluginProvider) {
++                spigotPlugins.put(configuration.getDisplayName(), provider);
++            } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++                paperPlugins.put(configuration.getDisplayName(), provider);
++            }
++        }
++
++        Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE);
++            //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs
++
++        sender.sendMessage(infoMessage);
++        sender.sendMessage(PAPER_HEADER);
++        for (Component component : formatProviders(paperPlugins)) {
++            sender.sendMessage(component);
++        }
++        sender.sendMessage(BUKKIT_HEADER);
++        for (Component component : formatProviders(spigotPlugins)) {
++            sender.sendMessage(component);
++        }
++
++        return true;
++    }
++
++    @NotNull
++    @Override
++    public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
++        return Collections.emptyList();
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.command.subcommands;
++
++import com.google.gson.JsonArray;
++import com.google.gson.JsonElement;
++import com.google.gson.JsonObject;
++import com.google.gson.JsonPrimitive;
++import com.google.gson.internal.Streams;
++import com.google.gson.stream.JsonWriter;
++import io.papermc.paper.command.PaperSubcommand;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.entrypoint.classloader.group.LockingClassLoaderGroup;
++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
++import io.papermc.paper.plugin.entrypoint.classloader.group.SimpleListPluginClassLoaderGroup;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import io.papermc.paper.plugin.storage.ConfiguredProviderStorage;
++import io.papermc.paper.plugin.storage.ProviderStorage;
++import net.minecraft.server.MinecraftServer;
++import org.bukkit.command.CommandSender;
++import org.bukkit.plugin.Plugin;
++import org.checkerframework.checker.nullness.qual.NonNull;
++import org.checkerframework.framework.qual.DefaultQualifier;
++
++import java.io.PrintStream;
++import java.io.StringWriter;
++import java.nio.charset.StandardCharsets;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.time.LocalDateTime;
++import java.time.format.DateTimeFormatter;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++
++import static net.kyori.adventure.text.Component.text;
++import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
++import static net.kyori.adventure.text.format.NamedTextColor.RED;
++
++@DefaultQualifier(NonNull.class)
++public final class DumpPluginsCommand implements PaperSubcommand {
++    @Override
++    public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
++        this.dumpPlugins(sender, args);
++        return true;
++    }
++
++    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
++
++    private void dumpPlugins(final CommandSender sender, final String[] args) {
++        Path parent = Path.of("debug");
++        Path path = parent.resolve("plugin-info" + FORMATTER.format(LocalDateTime.now()) + ".txt");
++        try {
++            Files.createDirectories(parent);
++            Files.createFile(path);
++            sender.sendMessage(text("Writing plugin information to " + path, GREEN));
++
++            final JsonObject data = this.writeDebug();
++
++            StringWriter stringWriter = new StringWriter();
++            JsonWriter jsonWriter = new JsonWriter(stringWriter);
++            jsonWriter.setIndent(" ");
++            jsonWriter.setLenient(false);
++            Streams.write(data, jsonWriter);
++
++            try (PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8)) {
++                out.print(stringWriter);
++            }
++            sender.sendMessage(text("Successfully written plugin debug information!", GREEN));
++        } catch (Throwable e) {
++            sender.sendMessage(text("Failed to write plugin information! See the console for more info.", RED));
++            MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", e);
++        }
++    }
++
++    private JsonObject writeDebug() {
++        JsonObject root = new JsonObject();
++        if (ConfiguredProviderStorage.LEGACY_PLUGIN_LOADING) {
++            root.addProperty("legacy-loading-strategy", true);
++        }
++
++        this.writeProviders(root);
++        this.writePlugins(root);
++        this.writeClassloaders(root);
++
++        return root;
++    }
++
++    private void writeProviders(JsonObject root) {
++        JsonObject rootProviders = new JsonObject();
++        root.add("providers", rootProviders);
++
++        for (Map.Entry<Entrypoint<?>, ProviderStorage<?>> entry : LaunchEntryPointHandler.INSTANCE.getStorage().entrySet()) {
++            JsonObject entrypoint = new JsonObject();
++
++            JsonArray providers = new JsonArray();
++            entrypoint.add("providers", providers);
++
++            List<PluginProvider<Object>> pluginProviders = new ArrayList<>();
++            for (PluginProvider<?> provider : entry.getValue().getRegisteredProviders()) {
++                JsonObject providerObj = new JsonObject();
++                providerObj.addProperty("name", provider.getMeta().getName());
++                providerObj.addProperty("version", provider.getMeta().getVersion());
++                providerObj.addProperty("dependencies", provider.getMeta().getPluginDependencies().toString());
++                providerObj.addProperty("soft-dependencies", provider.getMeta().getPluginSoftDependencies().toString());
++                providerObj.addProperty("load-before", provider.getMeta().getLoadBeforePlugins().toString());
++
++
++                providers.add(providerObj);
++                pluginProviders.add((PluginProvider<Object>) provider);
++            }
++
++            JsonArray loadOrder = new JsonArray();
++            entrypoint.add("load-order", loadOrder);
++
++            ModernPluginLoadingStrategy<Object> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
++                @Override
++                public void applyContext(PluginProvider<Object> provider, DependencyContext dependencyContext) {
++                }
++
++                @Override
++                public boolean load(PluginProvider<Object> provider, Object provided) {
++                    loadOrder.add(provider.getMeta().getName());
++                    return false;
++                }
++
++                @Override
++                public List<String> requiredDependencies(PluginProvider<Object> provider) {
++                    return provider.getMeta().getPluginDependencies();
++                }
++
++                @Override
++                public List<String> optionalDependencies(PluginProvider<Object> provider) {
++                    return provider.getMeta().getPluginSoftDependencies();
++                }
++
++                @Override
++                public List<String> loadBeforeDependencies(PluginProvider<Object> provider) {
++                    return provider.getMeta().getLoadBeforePlugins();
++                }
++            });
++            modernPluginLoadingStrategy.loadProviders(pluginProviders);
++
++            rootProviders.add(entry.getKey().getDebugName(), entrypoint);
++        }
++    }
++
++    private void writePlugins(JsonObject root) {
++        JsonArray rootPlugins = new JsonArray();
++        root.add("plugins", rootPlugins);
++
++        for (Plugin plugin : PaperPluginManagerImpl.getInstance().getPlugins()) {
++            rootPlugins.add(plugin.toString());
++        }
++    }
++
++    private void writeClassloaders(JsonObject root) {
++        JsonObject classLoadersRoot = new JsonObject();
++        root.add("classloaders", classLoadersRoot);
++
++        PaperPluginClassLoaderStorage storage = (PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance();
++        classLoadersRoot.addProperty("global", storage.getGlobalGroup().toString());
++        classLoadersRoot.addProperty("dependency_graph", PaperPluginManagerImpl.getInstance().getInstanceManagerGraph().toString());
++
++        JsonArray array = new JsonArray();
++        classLoadersRoot.add("children", array);
++        for (PluginClassLoaderGroup group : storage.getGroups()) {
++            array.add(this.writeClassloader(group));
++        }
++    }
++
++    private JsonObject writeClassloader(PluginClassLoaderGroup group) {
++        JsonObject classLoadersRoot = new JsonObject();
++        if (group instanceof SimpleListPluginClassLoaderGroup listGroup) {
++            JsonArray array = new JsonArray();
++            classLoadersRoot.addProperty("main", listGroup.toString());
++            classLoadersRoot.add("children", array);
++            for (ConfiguredPluginClassLoader innerGroup : listGroup.getClassLoaders()) {
++                array.add(this.writeClassloader(innerGroup));
++            }
++
++        } else if (group instanceof LockingClassLoaderGroup locking) {
++            // Unwrap
++            return this.writeClassloader(locking.getParent());
++        } else {
++            classLoadersRoot.addProperty("raw", group.toString());
++        }
++
++        return classLoadersRoot;
++    }
++
++    private JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) {
++        return new JsonPrimitive(innerGroup.toString());
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import com.mojang.logging.LogUtils;
++import joptsimple.OptionSet;
++import org.bukkit.configuration.file.YamlConfiguration;
++import org.jetbrains.annotations.NotNull;
++import org.slf4j.Logger;
++
++import java.io.File;
++import java.nio.file.Path;
++
++public class PluginInitializerManager {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++    private static PluginInitializerManager impl;
++    private final Path pluginDirectory;
++    private final Path updateDirectory;
++
++    PluginInitializerManager(@NotNull OptionSet minecraftOptionSet) {
++        // We have to load the bukkit configuration inorder to get the update folder location.
++        File configFileLocationBukkit = (File) minecraftOptionSet.valueOf("bukkit-settings");
++        this.pluginDirectory = ((File) minecraftOptionSet.valueOf("plugins")).toPath();
++        this.updateDirectory = this.pluginDirectory.resolve(YamlConfiguration.loadConfiguration(configFileLocationBukkit).getString("settings.update-folder", "update"));
++    }
++
++    public static PluginInitializerManager init(OptionSet optionSet) {
++        impl = new PluginInitializerManager(optionSet);
++        return impl;
++    }
++
++    public static PluginInitializerManager instance() {
++        return impl;
++    }
++
++    @NotNull
++    public Path pluginDirectoryPath() {
++        return pluginDirectory;
++    }
++
++    @NotNull
++    public Path pluginUpdatePath() {
++        return updateDirectory;
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.bootstrap;
++
++import io.papermc.paper.plugin.PluginInitializerManager;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.logging.Logger;
++
++public record PluginProviderContextImpl(PluginMeta config, Path dataFolder,
++                                        Logger logger) implements PluginProviderContext {
++
++    public static PluginProviderContextImpl of(PluginMeta config, Logger logger) {
++        Path dataFolder = PluginInitializerManager.instance().pluginDirectoryPath().resolve(config.getDisplayName());
++
++        return new PluginProviderContextImpl(config, dataFolder, logger);
++    }
++
++    public static PluginProviderContextImpl of(PluginProvider<?> provider, Path pluginFolder) {
++        Path dataFolder = pluginFolder.resolve(provider.getMeta().getDisplayName());
++
++        return new PluginProviderContextImpl(provider.getMeta(), dataFolder, provider.getLogger());
++    }
++
++    @Override
++    public @NotNull PluginMeta getConfiguration() {
++        return this.config;
++    }
++
++    @Override
++    public @NotNull Path getDataDirectory() {
++        return this.dataFolder;
++    }
++
++    @Override
++    public @NotNull Logger getLogger() {
++        return this.logger;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint;
++
++import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
++import org.bukkit.plugin.java.JavaPlugin;
++
++/**
++ * Used to mark a certain place that {@link EntrypointHandler} will register {@link io.papermc.paper.plugin.provider.PluginProvider} under.
++ * Used for loading only certain providers at a certain time.
++ * @param <T> provider type
++ */
++public final class Entrypoint<T> {
++
++    public static final Entrypoint<PluginBootstrap> BOOTSTRAPPER = new Entrypoint<>("bootstrapper");
++    public static final Entrypoint<JavaPlugin> PLUGIN = new Entrypoint<>("plugin");
++
++    private final String debugName;
++
++    private Entrypoint(String debugName) {
++        this.debugName = debugName;
++    }
++
++    public String getDebugName() {
++        return debugName;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++/**
++ * Represents a register that will register providers at a certain {@link Entrypoint},
++ * where then when the given {@link Entrypoint} is registered those will be loaded.
++ */
++public interface EntrypointHandler {
++
++    <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider);
++
++    void enter(Entrypoint<?> entrypoint);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.storage.BootstrapProviderStorage;
++import io.papermc.paper.plugin.storage.ProviderStorage;
++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.HashMap;
++import java.util.Map;
++
++/**
++ * Used by the server to register/load plugin bootstrappers and plugins.
++ */
++public class LaunchEntryPointHandler implements EntrypointHandler {
++
++    public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler();
++    private final Map<Entrypoint<?>, ProviderStorage<?>> storage = new HashMap<>();
++
++    LaunchEntryPointHandler() {
++        this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
++        this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage());
++    }
++
++    // Utility
++    public static void enterBootstrappers() {
++        LaunchEntryPointHandler.INSTANCE.enter(Entrypoint.BOOTSTRAPPER);
++    }
++
++    @Override
++    public void enter(Entrypoint<?> entrypoint) {
++        ProviderStorage<?> storage = this.storage.get(entrypoint);
++        if (storage == null) {
++            throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
++        }
++
++        storage.enter();
++    }
++
++    @Override
++    public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
++        ProviderStorage<T> storage = this.get(entrypoint);
++        if (storage == null) {
++            throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
++        }
++
++        storage.register(provider);
++    }
++
++    @SuppressWarnings("unchecked")
++    public <T> ProviderStorage<T> get(Entrypoint<T> entrypoint) {
++        return (ProviderStorage<T>) this.storage.get(entrypoint);
++    }
++
++    // Debug only
++    @ApiStatus.Internal
++    public Map<Entrypoint<?>, ProviderStorage<?>> getStorage() {
++        return storage;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import net.kyori.adventure.util.Services;
++import org.jetbrains.annotations.ApiStatus;
++
++@ApiStatus.Internal
++public interface ClassloaderBytecodeModifier {
++
++    static ClassloaderBytecodeModifier bytecodeModifier() {
++        return Provider.INSTANCE;
++    }
++
++    byte[] modify(PluginMeta config, byte[] bytecode);
++
++    class Provider {
++
++        private static final ClassloaderBytecodeModifier INSTANCE = Services.service(ClassloaderBytecodeModifier.class).orElseThrow();
++
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++
++// Stub, implement in future.
++public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
++
++    @Override
++    public byte[] modify(PluginMeta configuration, byte[] bytecode) {
++        return bytecode;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import org.bukkit.Bukkit;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.File;
++import java.io.IOException;
++import java.net.URL;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.Collections;
++import java.util.Enumeration;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.concurrent.ConcurrentHashMap;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++/**
++ * This is similar to a {@link org.bukkit.plugin.java.PluginClassLoader} but is completely kept hidden from the api.
++ * This is only used with Paper plugins.
++ *
++ * @see PaperPluginClassLoaderStorage
++ */
++public class PaperPluginClassLoader extends PaperSimplePluginClassLoader implements ConfiguredPluginClassLoader {
++
++    static {
++        registerAsParallelCapable();
++    }
++
++    private final ClassLoader libraryLoader;
++    private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
++    private final Logger logger;
++    @Nullable
++    private JavaPlugin loadedJavaPlugin;
++    @Nullable
++    private PluginClassLoaderGroup group;
++
++    public PaperPluginClassLoader(Logger logger, Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader, ClassLoader libraryLoader) throws IOException {
++        super(source, file, configuration, parentLoader);
++        this.libraryLoader = libraryLoader;
++
++        this.logger = logger;
++        if (this.configuration.hasOpenClassloader()) {
++            this.group = PaperClassLoaderStorage.instance().registerOpenGroup(this);
++        }
++    }
++
++    public void refreshClassloaderDependencyTree(DependencyContext dependencyContext) {
++         if (this.configuration.hasOpenClassloader()) {
++             return;
++         }
++         if (this.group != null) {
++             // We need to unregister the classloader inorder to allow for dependencies
++             // to be recalculated
++             PaperClassLoaderStorage.instance().unregisterClassloader(this);
++         }
++
++        this.group = PaperClassLoaderStorage.instance().registerAccessBackedGroup(this, (classLoader) -> {
++            return dependencyContext.isTransitiveDependency(PaperPluginClassLoader.this.configuration, classLoader.getConfiguration());
++        });
++    }
++
++    @Override
++    public URL getResource(String name) {
++        URL resource = findResource(name);
++        if (resource == null && this.libraryLoader != null) {
++            return this.libraryLoader.getResource(name);
++        }
++        return resource;
++    }
++
++    @Override
++    public Enumeration<URL> getResources(String name) throws IOException {
++        List<URL> resources = new ArrayList<>();
++        this.addEnumeration(resources, this.findResources(name));
++        if (this.libraryLoader != null) {
++            addEnumeration(resources, this.libraryLoader.getResources(name));
++        }
++        return Collections.enumeration(resources);
++    }
++
++    private <T> void addEnumeration(List<T> list, Enumeration<T> enumeration) {
++        while (enumeration.hasMoreElements()) {
++            list.add(enumeration.nextElement());
++        }
++    }
++
++    @Override
++    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
++        return this.loadClass(name, resolve, true, true);
++    }
++
++    @Override
++    public PluginMeta getConfiguration() {
++        return this.configuration;
++    }
++
++    @Override
++    public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGroup, boolean checkLibraries) throws ClassNotFoundException {
++        try {
++            Class<?> result = super.loadClass(name, resolve);
++
++            // SPIGOT-6749: Library classes will appear in the above, but we don't want to return them to other plugins
++            if (checkGroup || result.getClassLoader() == this) {
++                return result;
++            }
++        } catch (ClassNotFoundException ignored) {
++        }
++
++        if (checkLibraries) {
++            try {
++                return this.libraryLoader.loadClass(name);
++            } catch (ClassNotFoundException ignored) {
++            }
++        }
++
++        if (checkGroup) {
++            // This ignores the libraries of other plugins, unless they are transitive dependencies.
++            if (this.group == null) {
++                throw new IllegalStateException("Tried to resolve class while group was not yet initialized");
++            }
++
++            Class<?> clazz = this.group.getClassByName(name, resolve, this);
++            if (clazz != null) {
++                return clazz;
++            }
++        }
++
++        throw new ClassNotFoundException(name);
++    }
++
++    @Override
++    public void init(JavaPlugin plugin) {
++        PluginMeta config = this.configuration;
++        PluginDescriptionFile pluginDescriptionFile = new PluginDescriptionFile(
++            config.getName(),
++            config.getDisplayName(),
++            config.getProvidedPlugins(),
++            config.getMainClass(),
++            "", // Classloader load order api
++            List.of(), // Dependencies
++            List.of(), // Soft Depends
++            List.of(), // Load Before
++            config.getVersion(),
++            Map.of(), // Commands, we use a separate system
++            config.getDescription(),
++            config.getAuthors(),
++            config.getContributors(),
++            config.getWebsite(),
++            config.getLoggerPrefix(),
++            config.getLoadOrder(),
++            config.getPermissions(),
++            config.getPermissionDefault(),
++            Set.of(), // Aware api
++            config.getAPIVersion(),
++            List.of() // Libraries
++        );
++
++        File dataFolder = new File(Bukkit.getPluginsFolder(), pluginDescriptionFile.getName());
++
++        plugin.init(Bukkit.getServer(), pluginDescriptionFile, dataFolder, this.source.toFile(), this, config);
++        plugin.logger = this.logger;
++
++        this.loadedJavaPlugin = plugin;
++    }
++
++    @Nullable
++    public JavaPlugin getLoadedJavaPlugin() {
++        return this.loadedJavaPlugin;
++    }
++
++    @Override
++    public String toString() {
++        return "PaperPluginClassLoader{" +
++            "libraryLoader=" + this.libraryLoader +
++            ", seenIllegalAccess=" + this.seenIllegalAccess +
++            ", loadedJavaPlugin=" + this.loadedJavaPlugin +
++            ", group=" + this.group +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader;
++
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.util.NamespaceChecker;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.io.IOException;
++import java.io.InputStream;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.nio.file.Path;
++import java.security.CodeSigner;
++import java.security.CodeSource;
++import java.util.Enumeration;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++import java.util.jar.Manifest;
++
++/**
++ * Represents a simple classloader used for paper plugin bootstrappers.
++ */
++@ApiStatus.Internal
++public class PaperSimplePluginClassLoader extends URLClassLoader {
++
++    static {
++        ClassLoader.registerAsParallelCapable();
++    }
++
++    protected final PaperPluginMeta configuration;
++    protected final Path source;
++    protected final Manifest jarManifest;
++    protected final URL jarUrl;
++    protected final JarFile jar;
++
++    public PaperSimplePluginClassLoader(Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader) throws IOException {
++        super(source.getFileName().toString(), new URL[]{source.toUri().toURL()}, parentLoader);
++
++        this.source = source;
++        this.jarManifest = file.getManifest();
++        this.jarUrl = source.toUri().toURL();
++        this.configuration = configuration;
++        this.jar = file;
++    }
++
++    @Override
++    public URL getResource(String name) {
++        return this.findResource(name);
++    }
++
++    @Override
++    public Enumeration<URL> getResources(String name) throws IOException {
++        return this.findResources(name);
++    }
++
++    // Bytecode modification supported loader
++    @Override
++    protected Class<?> findClass(String name) throws ClassNotFoundException {
++        NamespaceChecker.validateNameSpaceForClassloading(name);
++
++        // See UrlClassLoader#findClass(String)
++        String path = name.replace('.', '/').concat(".class");
++        JarEntry entry = this.jar.getJarEntry(path);
++        if (entry == null) {
++            throw new ClassNotFoundException();
++        }
++
++        // See URLClassLoader#defineClass(String, Resource)
++        byte[] classBytes;
++
++        try (InputStream is = this.jar.getInputStream(entry)) {
++            classBytes = is.readAllBytes();
++        } catch (IOException ex) {
++            throw new ClassNotFoundException(name, ex);
++        }
++
++        classBytes = ClassloaderBytecodeModifier.bytecodeModifier().modify(this.configuration, classBytes);
++
++        int dot = name.lastIndexOf('.');
++        if (dot != -1) {
++            String pkgName = name.substring(0, dot);
++            // Get defined package does not correctly handle sealed packages.
++            if (this.getDefinedPackage(pkgName) == null) {
++                try {
++                    if (this.jarManifest != null) {
++                        this.definePackage(pkgName, this.jarManifest, this.jarUrl);
++                    } else {
++                        this.definePackage(pkgName, null, null, null, null, null, null, null);
++                    }
++                } catch (IllegalArgumentException ex) {
++                    // parallel-capable class loaders: re-verify in case of a
++                    // race condition
++                    if (this.getDefinedPackage(pkgName) == null) {
++                        // Should never happen
++                        throw new IllegalStateException("Cannot find package " + pkgName);
++                    }
++                }
++            }
++        }
++
++        CodeSigner[] signers = entry.getCodeSigners();
++        CodeSource source = new CodeSource(this.jarUrl, signers);
++
++        return this.defineClass(name, classBytes, 0, classBytes.length, source);
++    }
++
++    @Override
++    public String toString() {
++        return "PaperSimplePluginClassLoader{" +
++            "configuration=" + this.configuration +
++            ", source=" + this.source +
++            ", jarManifest=" + this.jarManifest +
++            ", jarUrl=" + this.jarUrl +
++            ", jar=" + this.jar +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.ArrayList;
++
++@ApiStatus.Internal
++public class DependencyBasedPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++    private final GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup;
++    private final ClassLoaderAccess access;
++
++    public DependencyBasedPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, ClassLoaderAccess access) {
++        super(new ArrayList<>());
++        this.access = access;
++        this.globalPluginClassLoaderGroup = globalPluginClassLoaderGroup;
++    }
++
++    /**
++     * This will refresh the dependencies of the current classloader.
++     */
++    public void populateDependencies() {
++        this.classloaders.clear();
++        for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalPluginClassLoaderGroup.getClassLoaders()) {
++            if (this.access.canAccess(configuredPluginClassLoader)) {
++                this.classloaders.add(configuredPluginClassLoader);
++            }
++        }
++
++    }
++
++    @Override
++    public ClassLoaderAccess getAccess() {
++        return this.access;
++    }
++
++    @Override
++    public String toString() {
++        return "DependencyBasedPluginClassLoaderGroup{" +
++            "globalPluginClassLoaderGroup=" + this.globalPluginClassLoaderGroup +
++            ", access=" + this.access +
++            ", classloaders=" + this.classloaders +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import org.jetbrains.annotations.ApiStatus;
++
++@ApiStatus.Internal
++public class GlobalPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++    @Override
++    public ClassLoaderAccess getAccess() {
++        return (v) -> true;
++    }
++
++    @Override
++    public String toString() {
++        return super.toString();
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.HashMap;
++import java.util.Map;
++import java.util.concurrent.atomic.AtomicInteger;
++import java.util.concurrent.locks.ReentrantReadWriteLock;
++
++@ApiStatus.Internal
++public class LockingClassLoaderGroup implements PluginClassLoaderGroup {
++
++    private final PluginClassLoaderGroup parent;
++    private final Map<String, ClassLockEntry> classLoadLock = new HashMap<>();
++
++    public LockingClassLoaderGroup(PluginClassLoaderGroup parent) {
++        this.parent = parent;
++    }
++
++    @Override
++    public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
++        // make MT safe
++        ClassLockEntry lock;
++        synchronized (this.classLoadLock) {
++            lock = this.classLoadLock.computeIfAbsent(name, (x) -> new ClassLockEntry(new AtomicInteger(0), new java.util.concurrent.locks.ReentrantReadWriteLock()));
++            lock.count.incrementAndGet();
++        }
++        lock.reentrantReadWriteLock.writeLock().lock();
++        try {
++            return parent.getClassByName(name, resolve, requester);
++        } finally {
++            synchronized (this.classLoadLock) {
++                lock.reentrantReadWriteLock.writeLock().unlock();
++                if (lock.count.get() == 1) {
++                    this.classLoadLock.remove(name);
++                } else {
++                    lock.count.decrementAndGet();
++                }
++            }
++        }
++    }
++
++    @Override
++    public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++        this.parent.remove(configuredPluginClassLoader);
++    }
++
++    @Override
++    public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++        this.parent.add(configuredPluginClassLoader);
++    }
++
++    @Override
++    public ClassLoaderAccess getAccess() {
++        return this.parent.getAccess();
++    }
++
++    public PluginClassLoaderGroup getParent() {
++        return parent;
++    }
++
++    record ClassLockEntry(AtomicInteger count, ReentrantReadWriteLock reentrantReadWriteLock) {
++    }
++
++    @Override
++    public String toString() {
++        return "LockingClassLoaderGroup{" +
++            "parent=" + this.parent +
++            ", classLoadLock=" + this.classLoadLock +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.bukkit.Bukkit;
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.concurrent.CopyOnWriteArrayList;
++
++/**
++ * This is used for connecting multiple classloaders.
++ */
++public final class PaperPluginClassLoaderStorage implements PaperClassLoaderStorage {
++
++    private final GlobalPluginClassLoaderGroup globalGroup = new GlobalPluginClassLoaderGroup();
++    private final List<PluginClassLoaderGroup> groups = new CopyOnWriteArrayList<>();
++
++    public PaperPluginClassLoaderStorage() {
++        this.groups.add(this.globalGroup);
++    }
++
++    @Override
++    public PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader) {
++        return this.registerGroup(pluginClassLoader, new SpigotPluginClassLoaderGroup(this.globalGroup, (library) -> {
++            return Bukkit.getServer().getPluginManager().isTransitiveDependency(pluginClassLoader.getConfiguration(), library.getConfiguration());
++        }));
++    }
++
++    @Override
++    public PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader) {
++        return this.registerGroup(classLoader, this.globalGroup);
++    }
++
++    @Override
++    public PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access) {
++        List<ConfiguredPluginClassLoader> allowedLoaders = new ArrayList<>();
++        for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalGroup.getClassLoaders()) {
++            if (access.canAccess(configuredPluginClassLoader)) {
++                allowedLoaders.add(configuredPluginClassLoader);
++            }
++        }
++
++        return this.registerGroup(classLoader, new StaticPluginClassLoaderGroup(allowedLoaders, access));
++    }
++
++    private PluginClassLoaderGroup registerGroup(ConfiguredPluginClassLoader classLoader, PluginClassLoaderGroup group) {
++        // Now add this classloader to any groups that allows it (includes global)
++        for (PluginClassLoaderGroup loaderGroup : this.groups) {
++            if (loaderGroup.getAccess().canAccess(classLoader)) {
++                loaderGroup.add(classLoader);
++            }
++        }
++
++        group = new LockingClassLoaderGroup(group);
++        this.groups.add(group);
++        return group;
++    }
++
++    @Override
++    public void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++        this.globalGroup.remove(configuredPluginClassLoader);
++        for (PluginClassLoaderGroup group : this.groups) {
++            group.remove(configuredPluginClassLoader);
++        }
++    }
++
++    @Override
++    public boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader) {
++        if (this.globalGroup.getClassLoaders().contains(pluginLoader)) {
++            return false;
++        } else {
++            this.globalGroup.getClassLoaders().add(pluginLoader);
++            return true;
++        }
++    }
++
++    // Debug only
++    @ApiStatus.Internal
++    public GlobalPluginClassLoaderGroup getGlobalGroup() {
++        return this.globalGroup;
++    }
++
++    // Debug only
++    @ApiStatus.Internal
++    public List<PluginClassLoaderGroup> getGroups() {
++        return this.groups;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.List;
++import java.util.concurrent.CopyOnWriteArrayList;
++
++@ApiStatus.Internal
++public abstract class SimpleListPluginClassLoaderGroup implements PluginClassLoaderGroup {
++
++    private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization");
++
++    protected final List<ConfiguredPluginClassLoader> classloaders;
++
++    protected SimpleListPluginClassLoaderGroup() {
++        this(new CopyOnWriteArrayList<>());
++    }
++
++    protected SimpleListPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders) {
++        this.classloaders = classloaders;
++    }
++
++    @Override
++    public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
++        if (!DISABLE_CLASS_PRIORITIZATION) {
++            try {
++                return this.lookupClass(name, false, requester); // First check the requester
++            } catch (ClassNotFoundException ignored) {
++            }
++        }
++
++        for (ConfiguredPluginClassLoader loader : this.classloaders) {
++            try {
++                return this.lookupClass(name, resolve, loader);
++            } catch (ClassNotFoundException ignored) {
++            }
++        }
++
++        return null;
++    }
++
++    protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
++        return current.loadClass(name, resolve, false, true);
++    }
++
++    @Override
++    public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++        this.classloaders.remove(configuredPluginClassLoader);
++    }
++
++    @Override
++    public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++        this.classloaders.add(configuredPluginClassLoader);
++    }
++
++    public List<ConfiguredPluginClassLoader> getClassLoaders() {
++        return classloaders;
++    }
++
++    @Override
++    public String toString() {
++        return "SimpleListPluginClassLoaderGroup{" +
++            "classloaders=" + this.classloaders +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.Nullable;
++
++@ApiStatus.Internal
++public class SingletonPluginClassLoaderGroup implements PluginClassLoaderGroup {
++
++    private final ConfiguredPluginClassLoader configuredPluginClassLoader;
++    private final Access access;
++
++    public SingletonPluginClassLoaderGroup(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++        this.configuredPluginClassLoader = configuredPluginClassLoader;
++        this.access = new Access();
++    }
++
++    @Override
++    public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
++        try {
++            return this.configuredPluginClassLoader.loadClass(name, resolve, false, true);
++        } catch (ClassNotFoundException ignored) {
++        }
++
++        return null;
++    }
++
++    @Override
++    public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++    }
++
++    @Override
++    public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++    }
++
++    @Override
++    public ClassLoaderAccess getAccess() {
++        return this.access;
++    }
++
++    @ApiStatus.Internal
++    private class Access implements ClassLoaderAccess {
++
++        @Override
++        public boolean canAccess(ConfiguredPluginClassLoader classLoader) {
++            return SingletonPluginClassLoaderGroup.this.configuredPluginClassLoader == classLoader;
++        }
++
++    }
++
++    @Override
++    public String toString() {
++        return "SingletonPluginClassLoaderGroup{" +
++            "configuredPluginClassLoader=" + this.configuredPluginClassLoader +
++            ", access=" + this.access +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.function.Predicate;
++
++/**
++ * Spigot classloaders have the ability to see everything.
++ * However, libraries are ONLY shared depending on their dependencies.
++ */
++@ApiStatus.Internal
++public class SpigotPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++    private final Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate;
++
++    public SpigotPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate) {
++        super(globalPluginClassLoaderGroup.getClassLoaders());
++        this.libraryClassloaderPredicate = libraryClassloaderPredicate;
++    }
++
++    // Mirrors global list
++    @Override
++    public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++    }
++
++    @Override
++    public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
++    }
++
++    @Override
++    protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
++        return current.loadClass(name, resolve, false, this.libraryClassloaderPredicate.test(current));
++    }
++
++    @Override
++    public ClassLoaderAccess getAccess() {
++        return v -> true;
++    }
++
++    @Override
++    public String toString() {
++        return "SpigotPluginClassLoaderGroup{" +
++            "libraryClassloaderPredicate=" + this.libraryClassloaderPredicate +
++            ", classloaders=" + this.classloaders +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.classloader.group;
++
++import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import org.jetbrains.annotations.ApiStatus;
++
++import java.util.List;
++
++@ApiStatus.Internal
++public class StaticPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
++
++    private final ClassLoaderAccess access;
++
++    public StaticPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders, ClassLoaderAccess access) {
++        super(classloaders);
++        this.access = access;
++    }
++
++    @Override
++    public ClassLoaderAccess getAccess() {
++        return this.access;
++    }
++
++    @Override
++    public String toString() {
++        return "StaticPluginClassLoaderGroup{" +
++            "access=" + this.access +
++            ", classloaders=" + this.classloaders +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++
++public interface DependencyContextHolder {
++
++    void setContext(DependencyContext context);
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.ArrayList;
++import java.util.List;
++
++@SuppressWarnings("UnstableApiUsage")
++public class DependencyUtil {
++
++    @NotNull
++    public static MutableGraph<String> buildDependencyGraph(@NotNull MutableGraph<String> dependencyGraph, @NotNull PluginMeta configuration) {
++        List<String> dependencies = new ArrayList<>();
++        dependencies.addAll(configuration.getPluginDependencies());
++        dependencies.addAll(configuration.getPluginSoftDependencies());
++
++        return buildDependencyGraph(dependencyGraph, configuration.getName(), dependencies, configuration.getLoadBeforePlugins());
++    }
++
++    @NotNull
++    public static MutableGraph<String> buildDependencyGraph(@NotNull MutableGraph<String> dependencyGraph, String identifier, @NotNull Iterable<String> depends, @NotNull Iterable<String> loadBefore) {
++        for (String dependency : depends) {
++            dependencyGraph.putEdge(identifier, dependency);
++        }
++
++        for (String loadBeforeTarget : loadBefore) {
++            dependencyGraph.putEdge(loadBeforeTarget, identifier);
++        }
++
++        dependencyGraph.addNode(identifier); // Make sure dependencies at least have a node
++        return dependencyGraph;
++    }
++
++    // This adds a provided plugin to another plugin, basically making it seem like a "dependency"
++    // in order to have plugins that need the provided plugin to load after the specified plugin name
++    @NotNull
++    public static MutableGraph<String> addProvidedPlugin(@NotNull MutableGraph<String> dependencyGraph, @NotNull String pluginName, @NotNull String providedName) {
++        dependencyGraph.putEdge(pluginName, providedName);
++
++        return dependencyGraph;
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.Graph;
++import com.google.common.graph.Graphs;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++
++import java.util.Set;
++
++@SuppressWarnings("UnstableApiUsage")
++public class GraphDependencyContext implements DependencyContext {
++
++    private final Graph<String> dependencyGraph;
++
++    public GraphDependencyContext(Graph<String> dependencyGraph) {
++        this.dependencyGraph = dependencyGraph;
++    }
++
++    @Override
++    public boolean isTransitiveDependency(PluginMeta plugin, PluginMeta depend) {
++        String pluginIdentifier = plugin.getName();
++
++        if (this.dependencyGraph.nodes().contains(pluginIdentifier)) {
++            Set<String> reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier);
++            if (reachableNodes.contains(depend.getName())) {
++                return true;
++            }
++            for (String provided : depend.getProvidedPlugins()) {
++                if (reachableNodes.contains(provided)) {
++                    return true;
++                }
++            }
++        }
++
++        return false;
++    }
++
++    @Override
++    public boolean hasDependency(String pluginIdentifier) {
++        return this.dependencyGraph.nodes().contains(pluginIdentifier);
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
+@@ -0,0 +0,0 @@
++/*
++ * (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors.
++ *
++ * JGraphT : a free Java graph-theory library
++ *
++ * See the CONTRIBUTORS.md file distributed with this work for additional
++ * information regarding copyright ownership.
++ *
++ * This program and the accompanying materials are made available under the
++ * terms of the Eclipse Public License 2.0 which is available at
++ * http://www.eclipse.org/legal/epl-2.0, or the
++ * GNU Lesser General Public License v2.1 or later
++ * which is available at
++ * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
++ *
++ * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
++ */
++
++// MODIFICATIONS:
++// - Modified to use a guava graph directly
++
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.base.Preconditions;
++import com.google.common.graph.Graph;
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import com.mojang.datafixers.util.Pair;
++
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.function.Consumer;
++
++/**
++ * Find all simple cycles of a directed graph using the Johnson's algorithm.
++ *
++ * <p>
++ * See:<br>
++ * D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975),
++ * pp. 77-84.
++ *
++ * @param <V> the vertex type.
++ *
++ * @author Nikolay Ognyanov
++ */
++public class JohnsonSimpleCycles<V>
++{
++    // The graph.
++    private Graph<V> graph;
++
++    // The main state of the algorithm.
++    private Consumer<List<V>> cycleConsumer = null;
++    private V[] iToV = null;
++    private Map<V, Integer> vToI = null;
++    private Set<V> blocked = null;
++    private Map<V, Set<V>> bSets = null;
++    private ArrayDeque<V> stack = null;
++
++    // The state of the embedded Tarjan SCC algorithm.
++    private List<Set<V>> foundSCCs = null;
++    private int index = 0;
++    private Map<V, Integer> vIndex = null;
++    private Map<V, Integer> vLowlink = null;
++    private ArrayDeque<V> path = null;
++    private Set<V> pathSet = null;
++
++    /**
++     * Create a simple cycle finder for the specified graph.
++     *
++     * @param graph - the DirectedGraph in which to find cycles.
++     *
++     * @throws IllegalArgumentException if the graph argument is <code>
++     * null</code>.
++     */
++    public JohnsonSimpleCycles(Graph<V> graph)
++    {
++        Preconditions.checkState(graph.isDirected(), "Graph must be directed");
++        this.graph = graph;
++    }
++
++    /**
++     * Find the simple cycles of the graph.
++     *
++     * @return The list of all simple cycles. Possibly empty but never <code>null</code>.
++     */
++    public List<List<V>> findSimpleCycles()
++    {
++        List<List<V>> result = new ArrayList<>();
++        findSimpleCycles(result::add);
++        return result;
++    }
++
++    /**
++     * Find the simple cycles of the graph.
++     *
++     * @param consumer Consumer that will be called with each cycle found.
++     */
++    public void findSimpleCycles(Consumer<List<V>> consumer)
++    {
++        if (graph == null) {
++            throw new IllegalArgumentException("Null graph.");
++        }
++        initState(consumer);
++
++        int startIndex = 0;
++        int size = graph.nodes().size();
++        while (startIndex < size) {
++            Pair<Graph<V>, Integer> minSCCGResult = findMinSCSG(startIndex);
++            if (minSCCGResult != null) {
++                startIndex = minSCCGResult.getSecond();
++                Graph<V> scg = minSCCGResult.getFirst();
++                V startV = toV(startIndex);
++                for (V v : scg.successors(startV)) {
++                    blocked.remove(v);
++                    getBSet(v).clear();
++                }
++                findCyclesInSCG(startIndex, startIndex, scg);
++                startIndex++;
++            } else {
++                break;
++            }
++        }
++
++        clearState();
++    }
++
++    private Pair<Graph<V>, Integer> findMinSCSG(int startIndex)
++    {
++        /*
++         * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph
++         * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected
++         * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum
++         * (among those SCCs) vertex index. We return that index together with the graph.
++         */
++        initMinSCGState();
++
++        List<Set<V>> foundSCCs = findSCCS(startIndex);
++
++        // find the SCC with the minimum index
++        int minIndexFound = Integer.MAX_VALUE;
++        Set<V> minSCC = null;
++        for (Set<V> scc : foundSCCs) {
++            for (V v : scc) {
++                int t = toI(v);
++                if (t < minIndexFound) {
++                    minIndexFound = t;
++                    minSCC = scc;
++                }
++            }
++        }
++        if (minSCC == null) {
++            return null;
++        }
++
++        // build a graph for the SCC found
++        MutableGraph<V> dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build();
++
++        for (V v : minSCC) {
++            for (V w : minSCC) {
++                if (graph.hasEdgeConnecting(v, w)) {
++                    dependencyGraph.putEdge(v, w);
++                }
++            }
++        }
++
++        Pair<Graph<V>, Integer> result = Pair.of(dependencyGraph, minIndexFound);
++        clearMinSCCState();
++        return result;
++    }
++
++    private List<Set<V>> findSCCS(int startIndex)
++    {
++        // Find SCCs in the subgraph induced
++        // by vertices startIndex and beyond.
++        // A call to StrongConnectivityAlgorithm
++        // would be too expensive because of the
++        // need to materialize the subgraph.
++        // So - do a local search by the Tarjan's
++        // algorithm and pretend that vertices
++        // with an index smaller than startIndex
++        // do not exist.
++        for (V v : graph.nodes()) {
++            int vI = toI(v);
++            if (vI < startIndex) {
++                continue;
++            }
++            if (!vIndex.containsKey(v)) {
++                getSCCs(startIndex, vI);
++            }
++        }
++        List<Set<V>> result = foundSCCs;
++        foundSCCs = null;
++        return result;
++    }
++
++    private void getSCCs(int startIndex, int vertexIndex)
++    {
++        V vertex = toV(vertexIndex);
++        vIndex.put(vertex, index);
++        vLowlink.put(vertex, index);
++        index++;
++        path.push(vertex);
++        pathSet.add(vertex);
++
++        Set<V> edges = graph.successors(vertex);
++        for (V successor : edges) {
++            int successorIndex = toI(successor);
++            if (successorIndex < startIndex) {
++                continue;
++            }
++            if (!vIndex.containsKey(successor)) {
++                getSCCs(startIndex, successorIndex);
++                vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor)));
++            } else if (pathSet.contains(successor)) {
++                vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor)));
++            }
++        }
++        if (vLowlink.get(vertex).equals(vIndex.get(vertex))) {
++            Set<V> result = new HashSet<>();
++            V temp;
++            do {
++                temp = path.pop();
++                pathSet.remove(temp);
++                result.add(temp);
++            } while (!vertex.equals(temp));
++            if (result.size() == 1) {
++                V v = result.iterator().next();
++                if (graph.edges().contains(vertex)) {
++                    foundSCCs.add(result);
++                }
++            } else {
++                foundSCCs.add(result);
++            }
++        }
++    }
++
++    private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph<V> scg)
++    {
++        /*
++         * Find cycles in a strongly connected graph per Johnson.
++         */
++        boolean foundCycle = false;
++        V vertex = toV(vertexIndex);
++        stack.push(vertex);
++        blocked.add(vertex);
++
++        for (V successor : scg.successors(vertex)) {
++            int successorIndex = toI(successor);
++            if (successorIndex == startIndex) {
++                List<V> cycle = new ArrayList<>(stack.size());
++                stack.descendingIterator().forEachRemaining(cycle::add);
++                cycleConsumer.accept(cycle);
++                foundCycle = true;
++            } else if (!blocked.contains(successor)) {
++                boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg);
++                foundCycle = foundCycle || gotCycle;
++            }
++        }
++        if (foundCycle) {
++            unblock(vertex);
++        } else {
++            for (V w : scg.successors(vertex)) {
++                Set<V> bSet = getBSet(w);
++                bSet.add(vertex);
++            }
++        }
++        stack.pop();
++        return foundCycle;
++    }
++
++    private void unblock(V vertex)
++    {
++        blocked.remove(vertex);
++        Set<V> bSet = getBSet(vertex);
++        while (bSet.size() > 0) {
++            V w = bSet.iterator().next();
++            bSet.remove(w);
++            if (blocked.contains(w)) {
++                unblock(w);
++            }
++        }
++    }
++
++    @SuppressWarnings("unchecked")
++    private void initState(Consumer<List<V>> consumer)
++    {
++        cycleConsumer = consumer;
++        iToV = (V[]) graph.nodes().toArray();
++        vToI = new HashMap<>();
++        blocked = new HashSet<>();
++        bSets = new HashMap<>();
++        stack = new ArrayDeque<>();
++
++        for (int i = 0; i < iToV.length; i++) {
++            vToI.put(iToV[i], i);
++        }
++    }
++
++    private void clearState()
++    {
++        cycleConsumer = null;
++        iToV = null;
++        vToI = null;
++        blocked = null;
++        bSets = null;
++        stack = null;
++    }
++
++    private void initMinSCGState()
++    {
++        index = 0;
++        foundSCCs = new ArrayList<>();
++        vIndex = new HashMap<>();
++        vLowlink = new HashMap<>();
++        path = new ArrayDeque<>();
++        pathSet = new HashSet<>();
++    }
++
++    private void clearMinSCCState()
++    {
++        index = 0;
++        foundSCCs = null;
++        vIndex = null;
++        vLowlink = null;
++        path = null;
++        pathSet = null;
++    }
++
++    private Integer toI(V vertex)
++    {
++        return vToI.get(vertex);
++    }
++
++    private V toV(Integer i)
++    {
++        return iToV[i];
++    }
++
++    private Set<V> getBSet(V v)
++    {
++        // B sets typically not all needed,
++        // so instantiate lazily.
++        return bSets.computeIfAbsent(v, k -> new HashSet<>());
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.bukkit.plugin.UnknownDependencyException;
++
++import java.util.ArrayList;
++import java.util.Collection;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Iterator;
++import java.util.LinkedList;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++
++@SuppressWarnings("UnstableApiUsage")
++public class LegacyPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
++
++    private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy");
++    private final ProviderConfiguration<T> configuration;
++
++    public LegacyPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
++        this.configuration = onLoad;
++    }
++
++    @Override
++    public List<T> loadProviders(List<PluginProvider<T>> providers) {
++        List<T> javapluginsLoaded = new ArrayList<>();
++        MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
++        GraphDependencyContext dependencyContext = new GraphDependencyContext(dependencyGraph);
++
++        Map<String, PluginProvider<T>> providersToLoad = new HashMap<>();
++        Set<String> loadedPlugins = new HashSet<>();
++        Map<String, String> pluginsProvided = new HashMap<>();
++        Map<String, Collection<String>> dependencies = new HashMap<>();
++        Map<String, Collection<String>> softDependencies = new HashMap<>();
++
++        for (PluginProvider<T> provider : providers) {
++            PluginMeta configuration = provider.getMeta();
++
++            PluginProvider<T> replacedProvider = providersToLoad.put(configuration.getName(), provider);
++            if (replacedProvider != null) {
++                LOGGER.severe(String.format(
++                    "Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'",
++                    configuration.getName(),
++                    provider.getSource(),
++                    replacedProvider.getSource(),
++                    replacedProvider.getParentSource()
++                ));
++            }
++
++            String removedProvided = pluginsProvided.remove(configuration.getName());
++            if (removedProvided != null) {
++                LOGGER.warning(String.format(
++                    "Ambiguous plugin name `%s'. It is also provided by `%s'",
++                    configuration.getName(),
++                    removedProvided
++                ));
++            }
++
++            for (String provided : configuration.getProvidedPlugins()) {
++                PluginProvider<T> pluginProvider = providersToLoad.get(provided);
++
++                if (pluginProvider != null) {
++                    LOGGER.warning(String.format(
++                        "`%s provides `%s' while this is also the name of `%s' in `%s'",
++                        provider.getSource(),
++                        provided,
++                        pluginProvider.getSource(),
++                        provider.getParentSource()
++                    ));
++                } else {
++                    String replacedPlugin = pluginsProvided.put(provided, configuration.getName());
++                    if (replacedPlugin != null) {
++                        LOGGER.warning(String.format(
++                            "`%s' is provided by both `%s' and `%s'",
++                            provided,
++                            configuration.getName(),
++                            replacedPlugin
++                        ));
++                    }
++                }
++            }
++
++            Collection<String> softDependencySet = this.configuration.optionalDependencies(provider);
++            if (softDependencySet != null && !softDependencySet.isEmpty()) {
++                if (softDependencies.containsKey(configuration.getName())) {
++                    // Duplicates do not matter, they will be removed together if applicable
++                    softDependencies.get(configuration.getName()).addAll(softDependencySet);
++                } else {
++                    softDependencies.put(configuration.getName(), new LinkedList<String>(softDependencySet));
++                }
++
++                for (String depend : softDependencySet) {
++                    dependencyGraph.putEdge(configuration.getName(), depend);
++                }
++            }
++
++            Collection<String> dependencySet = this.configuration.requiredDependencies(provider);
++            if (dependencySet != null && !dependencySet.isEmpty()) {
++                dependencies.put(configuration.getName(), new LinkedList<String>(dependencySet));
++
++                for (String depend : dependencySet) {
++                    dependencyGraph.putEdge(configuration.getName(), depend);
++                }
++            }
++
++            Collection<String> loadBeforeSet = this.configuration.loadBeforeDependencies(provider);
++            if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) {
++                for (String loadBeforeTarget : loadBeforeSet) {
++                    if (softDependencies.containsKey(loadBeforeTarget)) {
++                        softDependencies.get(loadBeforeTarget).add(configuration.getName());
++                    } else {
++                        // softDependencies is never iterated, so 'ghost' plugins aren't an issue
++                        Collection<String> shortSoftDependency = new LinkedList<String>();
++                        shortSoftDependency.add(configuration.getName());
++                        softDependencies.put(loadBeforeTarget, shortSoftDependency);
++                    }
++
++                    dependencyGraph.putEdge(loadBeforeTarget, configuration.getName());
++                }
++            }
++        }
++
++        while (!providersToLoad.isEmpty()) {
++            boolean missingDependency = true;
++            Iterator<Map.Entry<String, PluginProvider<T>>> providerIterator = providersToLoad.entrySet().iterator();
++
++            while (providerIterator.hasNext()) {
++                Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
++                String providerIdentifier = entry.getKey();
++
++                if (dependencies.containsKey(providerIdentifier)) {
++                    Iterator<String> dependencyIterator = dependencies.get(providerIdentifier).iterator();
++                    final Set<String> missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends
++
++                    while (dependencyIterator.hasNext()) {
++                        String dependency = dependencyIterator.next();
++
++                        // Dependency loaded
++                        if (loadedPlugins.contains(dependency)) {
++                            dependencyIterator.remove();
++
++                            // We have a dependency not found
++                        } else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) {
++                            // Paper start
++                            missingHardDependencies.add(dependency);
++                        }
++                    }
++                    if (!missingHardDependencies.isEmpty()) {
++                        // Paper end
++                        missingDependency = false;
++                        providerIterator.remove();
++                        pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
++                        softDependencies.remove(providerIdentifier);
++                        dependencies.remove(providerIdentifier);
++
++                        LOGGER.log(
++                            Level.SEVERE,
++                            "Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper
++                            new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper
++                    }
++
++                    if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) {
++                        dependencies.remove(providerIdentifier);
++                    }
++                }
++                if (softDependencies.containsKey(providerIdentifier)) {
++                    Iterator<String> softDependencyIterator = softDependencies.get(providerIdentifier).iterator();
++
++                    while (softDependencyIterator.hasNext()) {
++                        String softDependency = softDependencyIterator.next();
++
++                        // Soft depend is no longer around
++                        if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) {
++                            softDependencyIterator.remove();
++                        }
++                    }
++
++                    if (softDependencies.get(providerIdentifier).isEmpty()) {
++                        softDependencies.remove(providerIdentifier);
++                    }
++                }
++                if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) {
++                    // We're clear to load, no more soft or hard dependencies left
++                    PluginProvider<T> file = providersToLoad.get(providerIdentifier);
++                    providerIterator.remove();
++                    pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
++                    missingDependency = false;
++
++                    try {
++                        this.configuration.applyContext(file, dependencyContext);
++                        T loadedPlugin = file.createInstance();
++
++                        if (this.configuration.load(file, loadedPlugin)) {
++                            loadedPlugins.add(file.getMeta().getName());
++                            loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
++                            javapluginsLoaded.add(loadedPlugin);
++                        }
++
++                    } catch (Exception ex) {
++                        LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
++                    }
++                }
++            }
++
++            if (missingDependency) {
++                // We now iterate over plugins until something loads
++                // This loop will ignore soft dependencies
++                providerIterator = providersToLoad.entrySet().iterator();
++
++                while (providerIterator.hasNext()) {
++                    Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
++                    String plugin = entry.getKey();
++
++                    if (!dependencies.containsKey(plugin)) {
++                        softDependencies.remove(plugin);
++                        missingDependency = false;
++                        PluginProvider<T> file = entry.getValue();
++                        providerIterator.remove();
++
++                        try {
++                            this.configuration.applyContext(file, dependencyContext);
++                            T loadedPlugin = file.createInstance();
++
++                            if (this.configuration.load(file, loadedPlugin)) {
++                                loadedPlugins.add(file.getMeta().getName());
++                                loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
++                                javapluginsLoaded.add(loadedPlugin);
++                            }
++                            break;
++                        } catch (Exception ex) {
++                            LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
++                        }
++                    }
++                }
++                // We have no plugins left without a depend
++                if (missingDependency) {
++                    softDependencies.clear();
++                    dependencies.clear();
++                    Iterator<PluginProvider<T>> failedPluginIterator = providersToLoad.values().iterator();
++
++                    while (failedPluginIterator.hasNext()) {
++                        PluginProvider<T> file = failedPluginIterator.next();
++                        failedPluginIterator.remove();
++                        LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper
++                    }
++                }
++            }
++        }
++
++        return javapluginsLoaded;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.collect.Lists;
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++
++@SuppressWarnings("UnstableApiUsage")
++public class ModernPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++    private final ProviderConfiguration<T> configuration;
++
++    public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
++        this.configuration = onLoad;
++    }
++
++    @Override
++    public List<T> loadProviders(List<PluginProvider<T>> pluginProviders) {
++        MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
++        Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>();
++        List<PluginProvider<T>> validatedProviders = new ArrayList<>();
++
++        // Populate provider map
++        for (PluginProvider<T> provider : pluginProviders) {
++            PluginMeta providerConfig = provider.getMeta();
++            PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider);
++
++            PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry);
++            if (replacedProvider != null) {
++                LOGGER.error(String.format(
++                    "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'",
++                    providerConfig.getName(),
++                    provider.getSource(),
++                    replacedProvider.provider.getSource(),
++                    replacedProvider.provider.getParentSource()
++                ));
++            }
++
++            for (String extra : providerConfig.getProvidedPlugins()) {
++                PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry);
++                if (replacedExtraProvider != null) {
++                    LOGGER.warn(String.format(
++                        "`%s' is provided by both `%s' and `%s'",
++                        extra,
++                        providerConfig.getName(),
++                        replacedExtraProvider.provider.getMeta().getName()
++                    ));
++                }
++            }
++        }
++
++        // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid
++        for (PluginProvider<T> provider : pluginProviders) {
++            PluginMeta configuration = provider.getMeta();
++
++            // Populate missing dependencies to capture if there are multiple missing ones.
++            List<String> missingDependencies = new ArrayList<>();
++            for (String hardDependency : this.configuration.requiredDependencies(provider)) {
++                if (!providerMap.containsKey(hardDependency)) {
++                    missingDependencies.add(hardDependency);
++                }
++            }
++
++            if (missingDependencies.isEmpty()) {
++                validatedProviders.add(provider);
++            } else {
++                LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
++                // Because the validator is invalid, remove it from the provider map
++                providerMap.remove(configuration.getName());
++            }
++        }
++
++        for (PluginProvider<?> validated : validatedProviders) {
++            PluginMeta configuration = validated.getMeta();
++
++            // Build a validated provider's dependencies into the graph
++            DependencyUtil.buildDependencyGraph(dependencyGraph, configuration);
++
++            // Add the provided plugins to the graph as well
++            for (String provides : configuration.getProvidedPlugins()) {
++                DependencyUtil.addProvidedPlugin(dependencyGraph, configuration.getName(), provides);
++            }
++        }
++
++        // Reverse the topographic search to let us see which providers we can load first.
++        List<String> reversedTopographicSort;
++        try {
++            reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(dependencyGraph));
++        } catch (TopographicGraphSorter.GraphCycleException exception) {
++            throw new PluginGraphCycleException(new JohnsonSimpleCycles<>(dependencyGraph).findSimpleCycles());
++        }
++
++        GraphDependencyContext graphDependencyContext = new GraphDependencyContext(dependencyGraph);
++        List<T> loadedPlugins = new ArrayList<>();
++        for (String providerIdentifier : reversedTopographicSort) {
++            // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist.
++            // The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just
++            // nicer to do it here TBH.
++            PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier);
++            if (retrievedProviderEntry == null || retrievedProviderEntry.provided) {
++                // OR if this was already provided (most likely from a plugin that already "provides" that dependency)
++                // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways
++                continue; // Skip provider that doesn't exist....
++            }
++            retrievedProviderEntry.provided = true;
++            PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider;
++            try {
++                this.configuration.applyContext(retrievedProvider, graphDependencyContext);
++
++                T instance = retrievedProvider.createInstance();
++                if (this.configuration.load(retrievedProvider, instance)) {
++                    loadedPlugins.add(instance);
++                }
++            } catch (Exception ex) {
++                LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
++            }
++        }
++
++        return loadedPlugins;
++    }
++
++    private static class PluginProviderEntry<T> {
++
++        private final PluginProvider<T> provider;
++        private boolean provided;
++
++        private PluginProviderEntry(PluginProvider<T> provider) {
++            this.provider = provider;
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import java.util.List;
++
++/**
++ * Indicates a dependency cycle within a provider loading sequence.
++ */
++public class PluginGraphCycleException extends RuntimeException {
++
++    private final List<List<String>> cycles;
++
++    public PluginGraphCycleException(List<List<String>> cycles) {
++        this.cycles = cycles;
++    }
++
++    public List<List<String>> getCycles() {
++        return this.cycles;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++
++import java.util.List;
++
++/**
++ * Used to share code with the modern and legacy plugin load strategy.
++ *
++ * @param <T>
++ */
++public interface ProviderConfiguration<T> {
++
++    void applyContext(PluginProvider<T> provider, DependencyContext dependencyContext);
++
++    boolean load(PluginProvider<T> provider, T provided);
++
++    List<String> requiredDependencies(PluginProvider<T> provider);
++
++    List<String> optionalDependencies(PluginProvider<T> provider);
++
++    List<String> loadBeforeDependencies(PluginProvider<T> provider);
++
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++import java.util.List;
++
++/**
++ * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
++ * <p>
++ * Returns providers loaded.
++ * @param <P> provider type
++ */
++public interface ProviderLoadingStrategy<P> {
++
++    List<P> loadProviders(List<PluginProvider<P>> providers);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy;
++
++import com.google.common.graph.Graph;
++
++import java.util.ArrayDeque;
++import java.util.ArrayList;
++import java.util.Deque;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++
++public class TopographicGraphSorter {
++
++    // Topographically sort dependencies
++    public static <N> List<N> sortGraph(Graph<N> graph) throws PluginGraphCycleException {
++        List<N> sorted = new ArrayList<>();
++        Deque<N> roots = new ArrayDeque<>();
++        Map<N, Integer> nonRoots = new HashMap<>();
++
++        for (N node : graph.nodes()) {
++            // Is a node being referred to by any other nodes?
++            int degree = graph.inDegree(node);
++            if (degree == 0) {
++                // Is a root
++                roots.add(node);
++            } else {
++                // Isn't a root, the number represents how many nodes connect to it.
++                nonRoots.put(node, degree);
++            }
++        }
++
++        // Pick from nodes that aren't referred to anywhere else
++        while (!roots.isEmpty()) {
++            N next = roots.remove();
++
++            for (N successor : graph.successors(next)) {
++                // Traverse through, moving down a degree
++                int newInDegree = nonRoots.get(successor) - 1;
++
++                if (newInDegree == 0) {
++                    nonRoots.remove(successor);
++                    roots.add(successor);
++                } else {
++                    nonRoots.put(successor, newInDegree);
++                }
++
++            }
++            sorted.add(next);
++        }
++
++        if (!nonRoots.isEmpty()) {
++            throw new GraphCycleException();
++        }
++
++        return sorted;
++    }
++
++    public static class GraphCycleException extends RuntimeException {
++
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader;
++
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
++import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.IOException;
++import java.net.MalformedURLException;
++import java.net.URL;
++import java.net.URLClassLoader;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++public class PaperClasspathBuilder implements PluginClasspathBuilder {
++
++    private final List<ClassPathLibrary> libraries = new ArrayList<>();
++
++    private final PluginProviderContext context;
++
++    public PaperClasspathBuilder(PluginProviderContext context) {
++        this.context = context;
++    }
++
++    @Override
++    public @NotNull PluginProviderContext getContext() {
++        return this.context;
++    }
++
++    @Override
++    public @NotNull PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary) {
++        this.libraries.add(classPathLibrary);
++        return this;
++    }
++
++    public PaperPluginClassLoader buildClassLoader(Logger logger, Path source, JarFile jarFile, PaperPluginMeta configuration) {
++        PaperLibraryStore paperLibraryStore = new PaperLibraryStore();
++        for (ClassPathLibrary library : this.libraries) {
++            library.register(paperLibraryStore);
++        }
++
++        List<Path> paths = paperLibraryStore.getPaths();
++        URL[] urls = new URL[paths.size()];
++        for (int i = 0; i < paths.size(); i++) {
++            Path path = paperLibraryStore.getPaths().get(i);
++            try {
++                urls[i] = path.toUri().toURL();
++            } catch (MalformedURLException e) {
++                throw new AssertionError(e);
++            }
++        }
++
++        try {
++            return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls));
++        } catch (IOException exception) {
++            throw new RuntimeException(exception);
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.loader.library;
++
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.List;
++
++public class PaperLibraryStore implements LibraryStore {
++
++    private final List<Path> paths = new ArrayList<>();
++
++    @Override
++    public void addLibrary(@NotNull Path library) {
++        this.paths.add(library);
++    }
++
++    public List<Path> getPaths() {
++        return this.paths;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++    private final List<JavaPlugin> provided = new ArrayList<>();
++
++    @Override
++    public void register(PluginProvider<JavaPlugin> provider) {
++        if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++            LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager.");
++            return;
++        }
++        super.register(provider);
++        /*
++        Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage,
++         as putting it into the entrypoint handlers doesn't make much sense.
++         */
++        LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
++    }
++
++    @Override
++    public void processProvided(JavaPlugin provided) {
++        super.processProvided(provided);
++        this.provided.add(provided);
++    }
++
++    @Override
++    public boolean exitOnCycleDependencies() {
++        return false;
++    }
++
++    public List<JavaPlugin> getLoaded() {
++        return this.provided;
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++
++import java.util.HashMap;
++import java.util.LinkedHashMap;
++import java.util.LinkedHashSet;
++import java.util.Map;
++import java.util.Set;
++
++class NormalPaperPermissionManager extends PaperPermissionManager {
++
++    private final Map<String, Permission> permissions = new HashMap<>();
++    private final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<>();
++    private final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<>();
++    private final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<>();
++
++    public NormalPaperPermissionManager() {
++        this.defaultPerms().put(true, new LinkedHashSet<>());
++        this.defaultPerms().put(false, new LinkedHashSet<>());
++    }
++
++    @Override
++    public Map<String, Permission> permissions() {
++        return this.permissions;
++    }
++
++    @Override
++    public Map<Boolean, Set<Permission>> defaultPerms() {
++        return this.defaultPerms;
++    }
++
++    @Override
++    public Map<String, Map<Permissible, Boolean>> permSubs() {
++        return this.permSubs;
++    }
++
++    @Override
++    public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
++        return this.defSubs;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import co.aikar.timings.TimedEventExecutor;
++import com.destroystokyo.paper.event.server.ServerExceptionEvent;
++import com.destroystokyo.paper.exception.ServerEventException;
++import com.google.common.collect.Sets;
++import org.bukkit.Server;
++import org.bukkit.Warning;
++import org.bukkit.event.Event;
++import org.bukkit.event.EventHandler;
++import org.bukkit.event.EventPriority;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.Listener;
++import org.bukkit.plugin.AuthorNagException;
++import org.bukkit.plugin.EventExecutor;
++import org.bukkit.plugin.IllegalPluginAccessException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.RegisteredListener;
++import org.jetbrains.annotations.NotNull;
++
++import java.lang.reflect.Method;
++import java.util.Arrays;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.Map;
++import java.util.Set;
++import java.util.logging.Level;
++
++class PaperEventManager {
++
++    private final Server server;
++
++    public PaperEventManager(Server server) {
++        this.server = server;
++    }
++
++    // SimplePluginManager
++    public void callEvent(@NotNull Event event) {
++        if (event.isAsynchronous() && this.server.isPrimaryThread()) {
++            throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
++        } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) {
++            throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
++        }
++
++        HandlerList handlers = event.getHandlers();
++        RegisteredListener[] listeners = handlers.getRegisteredListeners();
++
++        for (RegisteredListener registration : listeners) {
++            if (!registration.getPlugin().isEnabled()) {
++                continue;
++            }
++
++            try {
++                registration.callEvent(event);
++            } catch (AuthorNagException ex) {
++                Plugin plugin = registration.getPlugin();
++
++                if (plugin.isNaggable()) {
++                    plugin.setNaggable(false);
++
++                    this.server.getLogger().log(Level.SEVERE, String.format(
++                        "Nag author(s): '%s' of '%s' about the following: %s",
++                        plugin.getPluginMeta().getAuthors(),
++                        plugin.getPluginMeta().getDisplayName(),
++                        ex.getMessage()
++                    ));
++                }
++            } catch (Throwable ex) {
++                String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getPluginMeta().getDisplayName();
++                this.server.getLogger().log(Level.SEVERE, msg, ex);
++                if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop
++                    this.callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event)));
++                }
++            }
++        }
++    }
++
++    public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
++        if (!plugin.isEnabled()) {
++            throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled");
++        }
++
++        for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
++            this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
++        }
++
++    }
++
++    public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
++        this.registerEvent(event, listener, priority, executor, plugin, false);
++    }
++
++    public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
++        if (!plugin.isEnabled()) {
++            throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
++        }
++
++        executor = new TimedEventExecutor(executor, plugin, null, event);
++        this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
++    }
++
++    @NotNull
++    private HandlerList getEventListeners(@NotNull Class<? extends Event> type) {
++        try {
++            Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList");
++            method.setAccessible(true);
++            return (HandlerList) method.invoke(null);
++        } catch (Exception e) {
++            throw new IllegalPluginAccessException(e.toString());
++        }
++    }
++
++    @NotNull
++    private Class<? extends Event> getRegistrationClass(@NotNull Class<? extends Event> clazz) {
++        try {
++            clazz.getDeclaredMethod("getHandlerList");
++            return clazz;
++        } catch (NoSuchMethodException e) {
++            if (clazz.getSuperclass() != null
++                && !clazz.getSuperclass().equals(Event.class)
++                && Event.class.isAssignableFrom(clazz.getSuperclass())) {
++                return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
++            } else {
++                throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!");
++            }
++        }
++    }
++
++    // JavaPluginLoader
++    @NotNull
++    public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) {
++        Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<>();
++
++        Set<Method> methods;
++        try {
++            Class<?> listenerClazz = listener.getClass();
++            methods = Sets.union(
++                Set.of(listenerClazz.getMethods()),
++                Set.of(listenerClazz.getDeclaredMethods())
++            );
++        } catch (NoClassDefFoundError e) {
++            plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
++            return ret;
++        }
++
++        for (final Method method : methods) {
++            final EventHandler eh = method.getAnnotation(EventHandler.class);
++            if (eh == null) continue;
++            // Do not register bridge or synthetic methods to avoid event duplication
++            // Fixes SPIGOT-893
++            if (method.isBridge() || method.isSynthetic()) {
++                continue;
++            }
++            final Class<?> checkClass;
++            if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
++                plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
++                continue;
++            }
++            final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class);
++            method.setAccessible(true);
++            Set<RegisteredListener> eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>());
++
++            for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
++                // This loop checks for extending deprecated events
++                if (clazz.getAnnotation(Deprecated.class) != null) {
++                    Warning warning = clazz.getAnnotation(Warning.class);
++                    Warning.WarningState warningState = this.server.getWarningState();
++                    if (!warningState.printFor(warning)) {
++                        break;
++                    }
++                    plugin.getLogger().log(
++                        Level.WARNING,
++                        String.format(
++                            "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.",
++                            plugin.getPluginMeta().getDisplayName(),
++                            clazz.getName(),
++                            method.toGenericString(),
++                            (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
++                            Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())),
++                        warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null);
++                    break;
++                }
++            }
++
++            EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass);
++            eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
++        }
++        return ret;
++    }
++
++    public void clearEvents() {
++        HandlerList.unregisterAll();
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import com.google.common.collect.ImmutableSet;
++import io.papermc.paper.plugin.PermissionManager;
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.HashSet;
++import java.util.List;
++import java.util.Locale;
++import java.util.Map;
++import java.util.Set;
++import java.util.WeakHashMap;
++
++/**
++ * See
++ * {@link StupidSPMPermissionManagerWrapper}
++ */
++abstract class PaperPermissionManager implements PermissionManager {
++
++    public abstract Map<String, Permission> permissions();
++
++    public abstract Map<Boolean, Set<Permission>> defaultPerms();
++
++    public abstract Map<String, Map<Permissible, Boolean>> permSubs();
++
++    public abstract Map<Boolean, Map<Permissible, Boolean>> defSubs();
++
++    @Override
++    @Nullable
++    public Permission getPermission(@NotNull String name) {
++        return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH));
++    }
++
++    @Override
++    public void addPermission(@NotNull Permission perm) {
++        this.addPermission(perm, true);
++    }
++
++    @Override
++    public void addPermissions(@NotNull List<Permission> permissions) {
++        for (Permission permission : permissions) {
++            this.addPermission(permission, false);
++        }
++        this.dirtyPermissibles();
++    }
++
++    // Allow suppressing permission default calculations
++    private void addPermission(@NotNull Permission perm, boolean dirty) {
++        String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH);
++
++        if (this.permissions().containsKey(name)) {
++            throw new IllegalArgumentException("The permission " + name + " is already defined!");
++        }
++
++        this.permissions().put(name, perm);
++        this.calculatePermissionDefault(perm, dirty);
++    }
++
++    @Override
++    @NotNull
++    public Set<Permission> getDefaultPermissions(boolean op) {
++        return ImmutableSet.copyOf(this.defaultPerms().get(op));
++    }
++
++
++    @Override
++    public void removePermission(@NotNull Permission perm) {
++        this.removePermission(perm.getName());
++    }
++
++
++    @Override
++    public void removePermission(@NotNull String name) {
++        this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH));
++    }
++
++    @Override
++    public void recalculatePermissionDefaults(@NotNull Permission perm) {
++        // we need a null check here because some plugins for some unknown reason pass null into this?
++        if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ENGLISH))) {
++            this.defaultPerms().get(true).remove(perm);
++            this.defaultPerms().get(false).remove(perm);
++
++            this.calculatePermissionDefault(perm, true);
++        }
++    }
++
++    private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) {
++        if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
++            this.defaultPerms().get(true).add(perm);
++            if (dirty) {
++                this.dirtyPermissibles(true);
++            }
++        }
++        if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
++            this.defaultPerms().get(false).add(perm);
++            if (dirty) {
++                this.dirtyPermissibles(false);
++            }
++        }
++    }
++
++
++    @Override
++    public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
++        String name = permission.toLowerCase(java.util.Locale.ENGLISH);
++        Map<Permissible, Boolean> map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>());
++
++        map.put(permissible, true);
++    }
++
++    @Override
++    public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
++        String name = permission.toLowerCase(java.util.Locale.ENGLISH);
++        Map<Permissible, Boolean> map = this.permSubs().get(name);
++
++        if (map != null) {
++            map.remove(permissible);
++
++            if (map.isEmpty()) {
++                this.permSubs().remove(name);
++            }
++        }
++    }
++
++    @Override
++    @NotNull
++    public Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
++        String name = permission.toLowerCase(java.util.Locale.ENGLISH);
++        Map<Permissible, Boolean> map = this.permSubs().get(name);
++
++        if (map == null) {
++            return ImmutableSet.of();
++        } else {
++            return ImmutableSet.copyOf(map.keySet());
++        }
++    }
++
++    @Override
++    public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
++        Map<Permissible, Boolean> map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>());
++
++        map.put(permissible, true);
++    }
++
++    @Override
++    public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
++        Map<Permissible, Boolean> map = this.defSubs().get(op);
++
++        if (map != null) {
++            map.remove(permissible);
++
++            if (map.isEmpty()) {
++                this.defSubs().remove(op);
++            }
++        }
++    }
++
++    @Override
++    @NotNull
++    public Set<Permissible> getDefaultPermSubscriptions(boolean op) {
++        Map<Permissible, Boolean> map = this.defSubs().get(op);
++
++        if (map == null) {
++            return ImmutableSet.of();
++        } else {
++            return ImmutableSet.copyOf(map.keySet());
++        }
++    }
++
++    @Override
++    @NotNull
++    public Set<Permission> getPermissions() {
++        return new HashSet<>(this.permissions().values());
++    }
++
++    @Override
++    public void clearPermissions() {
++        this.permissions().clear();
++        this.defaultPerms().get(true).clear();
++        this.defaultPerms().get(false).clear();
++    }
++
++
++    void dirtyPermissibles(boolean op) {
++        Set<Permissible> permissibles = this.getDefaultPermSubscriptions(op);
++
++        for (Permissible p : permissibles) {
++            p.recalculatePermissions();
++        }
++    }
++
++    void dirtyPermissibles() {
++        this.dirtyPermissibles(true);
++        this.dirtyPermissibles(false);
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import com.google.common.base.Preconditions;
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
++import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.provider.source.DirectoryProviderSource;
++import io.papermc.paper.plugin.provider.source.FileProviderSource;
++import org.bukkit.Bukkit;
++import org.bukkit.Server;
++import org.bukkit.World;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandMap;
++import org.bukkit.command.PluginCommandYamlParser;
++import org.bukkit.craftbukkit.util.CraftMagicNumbers;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.server.PluginDisableEvent;
++import org.bukkit.event.server.PluginEnableEvent;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.PluginManager;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.io.IOException;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.logging.Level;
++
++@SuppressWarnings("UnstableApiUsage")
++class PaperPluginInstanceManager {
++
++    private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted);
++    private static final DirectoryProviderSource DIRECTORY_PROVIDER_SOURCE = new DirectoryProviderSource();
++
++    private final List<Plugin> plugins = new ArrayList<>();
++    private final Map<String, Plugin> lookupNames = new HashMap<>();
++
++    private final PluginManager pluginManager;
++    private final CommandMap commandMap;
++    private final Server server;
++
++    private final MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
++    private final DependencyContext context = new GraphDependencyContext(this.dependencyGraph);
++
++    public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
++        this.commandMap = commandMap;
++        this.server = server;
++        this.pluginManager = pluginManager;
++    }
++
++    public @Nullable Plugin getPlugin(@NotNull String name) {
++        return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
++    }
++
++    public @NotNull Plugin[] getPlugins() {
++        return this.plugins.toArray(new Plugin[0]);
++    }
++
++    public boolean isPluginEnabled(@NotNull String name) {
++        Plugin plugin = this.getPlugin(name);
++
++        return this.isPluginEnabled(plugin);
++    }
++
++    public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) {
++        if ((plugin != null) && (this.plugins.contains(plugin))) {
++            return plugin.isEnabled();
++        } else {
++            return false;
++        }
++    }
++
++    public void loadPlugin(Plugin provided) {
++        PluginMeta configuration = provided.getPluginMeta();
++
++        this.plugins.add(provided);
++        this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided);
++        for (String providedPlugin : configuration.getProvidedPlugins()) {
++            this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
++        }
++
++        DependencyUtil.buildDependencyGraph(this.dependencyGraph, configuration);
++    }
++
++    // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception.
++    public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException {
++        RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage());
++
++        try {
++            FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
++        } catch (IllegalArgumentException exception) {
++            return null; // Return null when the plugin file is not valid / plugin type is unknown
++        } catch (PluginGraphCycleException exception) {
++            throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!");
++        } catch (SerializationException |
++                 InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception
++            throw new InvalidPluginException(ex);
++        } catch (Exception e) {
++            throw new InvalidPluginException(e);
++        }
++
++        try {
++            runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
++        } catch (Throwable e) {
++            throw new InvalidPluginException(e);
++        }
++
++        return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded()
++            .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?"));
++    }
++
++    // The behavior of this is that all errors are logged instead of being thrown
++    public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
++        Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
++
++        RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage());
++        try {
++            DIRECTORY_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, directory);
++            runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
++        } catch (Exception e) {
++            // This should never happen, any errors that occur in this provider should instead be logged.
++            this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
++        }
++
++        return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
++    }
++
++    // Plugins are disabled in order like this inorder to "rougly" prevent
++    // their dependencies unloading first. But, eh.
++    public void disablePlugins() {
++        Plugin[] plugins = this.getPlugins();
++        for (int i = plugins.length - 1; i >= 0; i--) {
++            this.disablePlugin(plugins[i]);
++        }
++    }
++
++    public void clearPlugins() {
++        synchronized (this) {
++            this.disablePlugins();
++            this.plugins.clear();
++            this.lookupNames.clear();
++        }
++    }
++
++    public synchronized void enablePlugin(@NotNull Plugin plugin) {
++        if (plugin.isEnabled()) {
++            return;
++        }
++
++        if (plugin.getPluginMeta() instanceof PluginDescriptionFile) {
++            List<Command> bukkitCommands = PluginCommandYamlParser.parse(plugin);
++
++            if (!bukkitCommands.isEmpty()) {
++                this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands);
++            }
++        }
++
++        try {
++            String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName();
++            if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) {
++                enableMsg += "*";
++            }
++            plugin.getLogger().info(enableMsg);
++
++            JavaPlugin jPlugin = (JavaPlugin) plugin;
++
++            if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper
++                if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) {
++                    this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName());
++                }
++            } // Paper
++
++            try {
++                jPlugin.setEnabled(true);
++            } catch (Throwable ex) {
++                this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex);
++                // Paper start - Disable plugins that fail to load
++                this.server.getPluginManager().disablePlugin(jPlugin);
++                return;
++                // Paper end
++            }
++
++            // Perhaps abort here, rather than continue going, but as it stands,
++            // an abort is not possible the way it's currently written
++            this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while enabling "
++                + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin);
++        }
++
++        HandlerList.bakeAll();
++    }
++
++    public synchronized void disablePlugin(@NotNull Plugin plugin) {
++        if (!(plugin instanceof JavaPlugin javaPlugin)) {
++            throw new IllegalArgumentException("Only expects java plugins.");
++        }
++        if (!plugin.isEnabled()) {
++            return;
++        }
++
++        String pluginName = plugin.getPluginMeta().getDisplayName();
++
++        try {
++            plugin.getLogger().info("Disabling %s".formatted(pluginName));
++
++            this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
++
++            javaPlugin.setEnabled(false);
++
++            ClassLoader classLoader = plugin.getClass().getClassLoader();
++            if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
++                try {
++                    configuredPluginClassLoader.close();
++                } catch (IOException ex) {
++                    this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception
++                }
++                // Remove from the classloader pool inorder to prevent plugins from trying
++                // to access classes
++                PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader);
++            }
++
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while disabling "
++                + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++        }
++
++        try {
++            this.server.getScheduler().cancelTasks(plugin);
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
++                + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++        }
++
++        try {
++            this.server.getServicesManager().unregisterAll(plugin);
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
++                + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++        }
++
++        try {
++            HandlerList.unregisterAll(plugin);
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
++                + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++        }
++
++        try {
++            this.server.getMessenger().unregisterIncomingPluginChannel(plugin);
++            this.server.getMessenger().unregisterOutgoingPluginChannel(plugin);
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
++                + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++        }
++
++        try {
++            for (World world : this.server.getWorlds()) {
++                world.removePluginChunkTickets(plugin);
++            }
++        } catch (Throwable ex) {
++            this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper
++        }
++
++    }
++
++    // TODO: Implement event part in future patch (paper patch move up, this patch is lower)
++    private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
++        Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex);
++        this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
++    }
++
++    public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
++        return this.context.isTransitiveDependency(plugin, depend);
++    }
++
++    public boolean hasDependency(String pluginIdentifier) {
++        return this.getPlugin(pluginIdentifier) != null;
++    }
++
++    // Debug only
++    @ApiStatus.Internal
++    public MutableGraph<String> getDependencyGraph() {
++        return this.dependencyGraph;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.PermissionManager;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import org.bukkit.Bukkit;
++import org.bukkit.Server;
++import org.bukkit.command.CommandMap;
++import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.event.Event;
++import org.bukkit.event.EventPriority;
++import org.bukkit.event.Listener;
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.bukkit.plugin.EventExecutor;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.PluginLoader;
++import org.bukkit.plugin.PluginManager;
++import org.bukkit.plugin.SimplePluginManager;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.File;
++import java.util.List;
++import java.util.Set;
++
++public class PaperPluginManagerImpl implements PluginManager, DependencyContext {
++
++    private final PaperPluginInstanceManager instanceManager;
++    private final PaperEventManager paperEventManager;
++    private PermissionManager permissionManager;
++
++    public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) {
++        this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server);
++        this.paperEventManager = new PaperEventManager(server);
++
++        if (permissionManager == null) {
++            this.permissionManager = new NormalPaperPermissionManager();
++        } else {
++            this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed
++        }
++    }
++
++    // REMOVE THIS WHEN SimplePluginManager is removed.
++    // Just cast and use Bukkit.getServer().getPluginManager()
++    public static PaperPluginManagerImpl getInstance() {
++        return ((CraftServer) (Bukkit.getServer())).paperPluginManager;
++    }
++
++    // Plugin Manipulation
++
++    @Override
++    public @Nullable Plugin getPlugin(@NotNull String name) {
++        return this.instanceManager.getPlugin(name);
++    }
++
++    @Override
++    public @NotNull Plugin[] getPlugins() {
++        return this.instanceManager.getPlugins();
++    }
++
++    @Override
++    public boolean isPluginEnabled(@NotNull String name) {
++        return this.instanceManager.isPluginEnabled(name);
++    }
++
++    @Override
++    public boolean isPluginEnabled(@Nullable Plugin plugin) {
++        return this.instanceManager.isPluginEnabled(plugin);
++    }
++
++    public void loadPlugin(Plugin plugin) {
++        this.instanceManager.loadPlugin(plugin);
++    }
++
++    @Override
++    public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException {
++        return this.instanceManager.loadPlugin(file.toPath());
++    }
++
++    @Override
++    public @NotNull Plugin[] loadPlugins(@NotNull File directory) {
++        return this.instanceManager.loadPlugins(directory.toPath());
++    }
++
++    @Override
++    public void disablePlugins() {
++        this.instanceManager.disablePlugins();
++    }
++
++    @Override
++    public synchronized void clearPlugins() {
++        this.instanceManager.clearPlugins();
++        this.permissionManager.clearPermissions();
++        this.paperEventManager.clearEvents();
++    }
++
++    @Override
++    public void enablePlugin(@NotNull Plugin plugin) {
++        this.instanceManager.enablePlugin(plugin);
++    }
++
++    @Override
++    public void disablePlugin(@NotNull Plugin plugin) {
++        this.instanceManager.disablePlugin(plugin);
++    }
++
++    @Override
++    public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) {
++        return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig);
++    }
++
++    @Override
++    public boolean hasDependency(String pluginIdentifier) {
++        return this.instanceManager.hasDependency(pluginIdentifier);
++    }
++
++    // Event manipulation
++
++    @Override
++    public void callEvent(@NotNull Event event) throws IllegalStateException {
++        this.paperEventManager.callEvent(event);
++    }
++
++    @Override
++    public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
++        this.paperEventManager.registerEvents(listener, plugin);
++    }
++
++    @Override
++    public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
++        this.paperEventManager.registerEvent(event, listener, priority, executor, plugin);
++    }
++
++    @Override
++    public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
++        this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled);
++    }
++
++    // Permission manipulation
++
++    @Override
++    public @Nullable Permission getPermission(@NotNull String name) {
++        return this.permissionManager.getPermission(name);
++    }
++
++    @Override
++    public void addPermission(@NotNull Permission perm) {
++        this.permissionManager.addPermission(perm);
++    }
++
++    @Override
++    public void removePermission(@NotNull Permission perm) {
++        this.permissionManager.removePermission(perm);
++    }
++
++    @Override
++    public void removePermission(@NotNull String name) {
++        this.permissionManager.removePermission(name);
++    }
++
++    @Override
++    public @NotNull Set<Permission> getDefaultPermissions(boolean op) {
++        return this.permissionManager.getDefaultPermissions(op);
++    }
++
++    @Override
++    public void recalculatePermissionDefaults(@NotNull Permission perm) {
++        this.permissionManager.recalculatePermissionDefaults(perm);
++    }
++
++    @Override
++    public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
++        this.permissionManager.subscribeToPermission(permission, permissible);
++    }
++
++    @Override
++    public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
++        this.permissionManager.unsubscribeFromPermission(permission, permissible);
++    }
++
++    @Override
++    public @NotNull Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
++        return this.permissionManager.getPermissionSubscriptions(permission);
++    }
++
++    @Override
++    public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
++        this.permissionManager.subscribeToDefaultPerms(op, permissible);
++    }
++
++    @Override
++    public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
++        this.permissionManager.unsubscribeFromDefaultPerms(op, permissible);
++    }
++
++    @Override
++    public @NotNull Set<Permissible> getDefaultPermSubscriptions(boolean op) {
++        return this.permissionManager.getDefaultPermSubscriptions(op);
++    }
++
++    @Override
++    public @NotNull Set<Permission> getPermissions() {
++        return this.permissionManager.getPermissions();
++    }
++
++    @Override
++    public void addPermissions(@NotNull List<Permission> perm) {
++        this.permissionManager.addPermissions(perm);
++    }
++
++    @Override
++    public void clearPermissions() {
++        this.permissionManager.clearPermissions();
++    }
++
++    @Override
++    public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) {
++        this.permissionManager = permissionManager;
++    }
++
++    // Etc
++
++    @Override
++    public boolean useTimings() {
++        return co.aikar.timings.Timings.isTimingsEnabled();
++    }
++
++    @Override
++    public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException {
++        throw new UnsupportedOperationException();
++    }
++
++    public MutableGraph<String> getInstanceManagerGraph() {
++        return instanceManager.getDependencyGraph();
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.storage.ProviderStorage;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++/**
++ * Used for loading plugins during runtime, only supporting providers that are plugins.
++ * This is only used for the plugin manager, as it only allows plugins to be
++ * registered to a provider storage.
++ */
++class RuntimePluginEntrypointHandler<T extends ProviderStorage<JavaPlugin>> implements EntrypointHandler {
++
++    private final T providerStorage;
++
++    RuntimePluginEntrypointHandler(T providerStorage) {
++        this.providerStorage = providerStorage;
++    }
++
++    @Override
++    public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
++        if (!entrypoint.equals(Entrypoint.PLUGIN)) {
++            SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint)));
++            // We have to throw an invalid plugin exception for legacy reasons
++        }
++
++        this.providerStorage.register((PluginProvider<JavaPlugin>) provider);
++    }
++
++    @Override
++    public void enter(Entrypoint<?> entrypoint) {
++        if (entrypoint != Entrypoint.PLUGIN) {
++            throw new IllegalArgumentException("Only plugin entrypoint supported");
++        }
++        this.providerStorage.enter();
++    }
++
++    @NotNull
++    public T getPluginProviderStorage() {
++        return this.providerStorage;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.bukkit.plugin.java.JavaPlugin;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Optional;
++
++/**
++ * Used for registering a single plugin provider.
++ * This has special behavior in that some errors are thrown instead of logged.
++ */
++class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage {
++
++    private PluginProvider<JavaPlugin> lastProvider;
++    private JavaPlugin singleLoaded;
++
++    @Override
++    public void register(PluginProvider<JavaPlugin> provider) {
++        super.register(provider);
++        if (this.lastProvider != null) {
++            SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins"));
++        }
++        if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
++            throw new IllegalStateException("Cannot register paper plugins during runtime!");
++        }
++        this.lastProvider = provider;
++        // Register the provider into the server entrypoint, this allows it to show in /plugins correctly.
++        // Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense.
++        LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
++    }
++
++    @Override
++    public void enter() {
++        PluginProvider<JavaPlugin> provider = this.lastProvider;
++        if (provider == null) {
++            return;
++        }
++
++        // Manually validate dependencies, LEGACY BEHAVIOR.
++        // Normally it is logged, but manually adding one plugin will cause it to actually throw exceptions.
++        PluginDescriptionFile descriptionFile = (PluginDescriptionFile) provider.getMeta();
++        List<String> missingDependencies = new ArrayList<>();
++        for (String dependency : descriptionFile.getDepend()) {
++            if (!PaperPluginManagerImpl.getInstance().isPluginEnabled(dependency)) {
++                missingDependencies.add(dependency);
++            }
++        }
++        if (!missingDependencies.isEmpty()) {
++            throw new UnknownDependencyException(missingDependencies, provider.getFileName().toString());
++        }
++
++        // Go through normal plugin loading logic
++        super.enter();
++    }
++
++    @Override
++    public void processProvided(JavaPlugin provided) {
++        super.processProvided(provided);
++        this.singleLoaded = provided;
++    }
++
++    @Override
++    public boolean exitOnCycleDependencies() {
++        return false;
++    }
++
++    public Optional<JavaPlugin> getSingleLoaded() {
++        return Optional.ofNullable(this.singleLoaded);
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.manager;
++
++import org.bukkit.permissions.Permissible;
++import org.bukkit.permissions.Permission;
++import org.bukkit.plugin.SimplePluginManager;
++
++import java.util.Map;
++import java.util.Set;
++
++/*
++This is actually so cursed I hate it.
++We need to wrap these in fields as people override the fields, so we need to access them lazily at all times.
++// TODO: When SimplePluginManager is GONE remove this and cleanup the PaperPermissionManager to use actual fields.
++ */
++class StupidSPMPermissionManagerWrapper extends PaperPermissionManager {
++
++    private final SimplePluginManager simplePluginManager;
++
++    public StupidSPMPermissionManagerWrapper(SimplePluginManager simplePluginManager) {
++        this.simplePluginManager = simplePluginManager;
++    }
++
++    @Override
++    public Map<String, Permission> permissions() {
++        return this.simplePluginManager.permissions;
++    }
++
++    @Override
++    public Map<Boolean, Set<Permission>> defaultPerms() {
++        return this.simplePluginManager.defaultPerms;
++    }
++
++    @Override
++    public Map<String, Map<Permissible, Boolean>> permSubs() {
++        return this.simplePluginManager.permSubs;
++    }
++
++    @Override
++    public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
++        return this.simplePluginManager.defSubs;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++/**
++ * PluginProviders are created by a {@link io.papermc.paper.plugin.provider.source.ProviderSource},
++ * which is loaded into an {@link io.papermc.paper.plugin.entrypoint.EntrypointHandler}.
++ * <p>
++ * A PluginProvider is responsible for providing part of a plugin, whether it's a Bootstrapper or Server Plugin.
++ * The point of this class is to be able to create the actual instance later, as at the time this is created the server
++ * may be missing some key parts. For example, the Bukkit singleton will not be initialized yet, therefor we need to
++ * have a PluginServerProvider load the server plugin later.
++ * <p>
++ * Plugin providers are currently not exposed in any way of the api. It is preferred that this stays this way,
++ * as providers are only needed for initialization.
++ *
++ * @param <T> provider type
++ */
++@ApiStatus.Internal
++public interface PluginProvider<T> {
++
++    @NotNull
++    Path getSource();
++
++    default Path getFileName() {
++        return this.getSource().getFileName();
++    }
++
++    default Path getParentSource() {
++        return this.getSource().getParent();
++    }
++
++    JarFile file();
++
++    T createInstance();
++
++    PluginMeta getMeta();
++
++    Logger getLogger();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider;
++
++import org.jetbrains.annotations.ApiStatus;
++
++/**
++ * This is used for the /plugins command, where it will look in the {@link io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler} and
++ * use the provider statuses to determine the color.
++ */
++@ApiStatus.Internal
++public enum ProviderStatus {
++    INITIALIZED,
++    ERRORED,
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider;
++
++/**
++ * This is used to mark that a plugin provider is able to hold a status for the /plugins command.
++ */
++public interface ProviderStatusHolder {
++
++    ProviderStatus getLastProvidedStatus();
++
++    void setStatus(ProviderStatus status);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration;
++
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
++
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.AnnotatedElement;
++
++@Retention(RetentionPolicy.RUNTIME)
++@Target(ElementType.FIELD)
++public @interface FlattenedResolver {
++
++    final class Factory implements NodeResolver.Factory {
++
++        @Override
++        public @Nullable NodeResolver make(String name, AnnotatedElement element) {
++            if (element.isAnnotationPresent(FlattenedResolver.class)) {
++                return (node) -> node;
++            } else {
++                return null;
++            }
++        }
++    }
++
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration;
++
++import com.google.common.collect.ImmutableList;
++import io.leangen.geantyref.TypeToken;
++import io.papermc.paper.configuration.constraint.Constraint;
++import io.papermc.paper.configuration.serializer.ComponentSerializer;
++import io.papermc.paper.configuration.serializer.EnumValueSerializer;
++import io.papermc.paper.configuration.serializer.collections.MapSerializer;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.configuration.serializer.ImmutableListSerializer;
++import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer;
++import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
++import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.bukkit.plugin.PluginLoadOrder;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++import org.spongepowered.configurate.CommentedConfigurationNode;
++import org.spongepowered.configurate.ConfigurateException;
++import org.spongepowered.configurate.loader.HeaderMode;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.ObjectMapper;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++import org.spongepowered.configurate.yaml.NodeStyle;
++import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
++
++import java.io.BufferedReader;
++import java.util.List;
++
++@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
++@ConfigSerializable
++public class PaperPluginMeta implements PluginMeta {
++
++    @PluginConfigConstraints.PluginName
++    @Required
++    private String name;
++    @Required
++    @PluginConfigConstraints.PluginNameSpace
++    private String main;
++    @PluginConfigConstraints.PluginNameSpace
++    private String bootstrapper;
++    @PluginConfigConstraints.PluginNameSpace
++    private String loader;
++    private List<DependencyConfiguration> dependencies = List.of();
++    private List<String> loadBefore = List.of();
++    private List<String> provides = List.of();
++    private boolean hasOpenClassloader = false;
++    @Required
++    private String version;
++    private String description;
++    private List<String> authors = List.of();
++    private List<String> contributors = List.of();
++    private String website;
++    private String prefix;
++    private PluginLoadOrder load = PluginLoadOrder.POSTWORLD;
++    @FlattenedResolver
++    private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of());
++    @Required
++    @PluginConfigConstraints.PluginVersion
++    private String apiVersion;
++
++    private transient String displayName;
++
++    public PaperPluginMeta() {
++    }
++
++    public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException {
++        YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
++            .indent(2)
++            .nodeStyle(NodeStyle.BLOCK)
++            .headerMode(HeaderMode.NONE)
++            .source(() -> reader)
++            .defaultOptions((options) -> {
++
++                return options.serializers((serializers) -> {
++                    serializers
++                        .register(new EnumValueSerializer())
++                        .register(MapSerializer.TYPE, new MapSerializer(false))
++                        .register(new TypeToken<>() {
++                        }, new ImmutableListSerializer())
++                        .register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER)
++                        .register(new ComponentSerializer())
++                        .registerAnnotatedObjects(
++                            ObjectMapper.factoryBuilder()
++                                .addConstraint(Constraint.class, new Constraint.Factory())
++                                .addConstraint(PluginConfigConstraints.PluginName.class, String.class, new PluginConfigConstraints.PluginName.Factory())
++                                .addConstraint(PluginConfigConstraints.PluginVersion.class, String.class, new PluginConfigConstraints.PluginVersion.Factory())
++                                .addConstraint(PluginConfigConstraints.PluginNameSpace.class, String.class, new PluginConfigConstraints.PluginNameSpace.Factory())
++                                .addNodeResolver(new FlattenedResolver.Factory())
++                                .build()
++                        );
++
++                });
++            })
++            .build();
++        CommentedConfigurationNode node = loader.load();
++        PaperPluginMeta pluginConfiguration = node.require(PaperPluginMeta.class);
++
++        if (!node.node("author").virtual()) {
++            pluginConfiguration.authors = ImmutableList.<String>builder()
++                .addAll(pluginConfiguration.authors)
++                .add(node.node("author").getString())
++                .build();
++        }
++
++        pluginConfiguration.displayName = pluginConfiguration.name.replace('_', ' ');
++
++        return pluginConfiguration;
++    }
++
++    @Override
++    public @NotNull String getName() {
++        return this.name;
++    }
++
++    @Override
++    public @NotNull String getMainClass() {
++        return this.main;
++    }
++
++    @Override
++    public @NotNull String getVersion() {
++        return this.version;
++    }
++
++    @Override
++    public @NotNull String getDisplayName() {
++        return this.displayName;
++    }
++
++    @Override
++    public @Nullable String getLoggerPrefix() {
++        return this.prefix;
++    }
++
++    @Override
++    public @NotNull List<String> getPluginDependencies() {
++        return this.dependencies.stream().filter((dependency) -> dependency.required() && !dependency.bootstrap()).map(DependencyConfiguration::name).toList();
++    }
++
++    @Override
++    public @NotNull List<String> getPluginSoftDependencies() {
++        return this.dependencies.stream().filter((dependency) -> !dependency.required() && !dependency.bootstrap()).map(DependencyConfiguration::name).toList();
++    }
++
++    @Override
++    public @NotNull List<String> getLoadBeforePlugins() {
++        return this.loadBefore;
++    }
++
++    @Override
++    public @NotNull PluginLoadOrder getLoadOrder() {
++        return this.load;
++    }
++
++    @Override
++    public @NotNull String getDescription() {
++        return this.description;
++    }
++
++    @Override
++    public @NotNull List<String> getAuthors() {
++        return this.authors;
++    }
++
++    @Override
++    public @NotNull List<String> getContributors() {
++        return this.contributors;
++    }
++
++    @Override
++    public String getWebsite() {
++        return this.website;
++    }
++
++    @Override
++    public @NotNull List<Permission> getPermissions() {
++        return this.permissionConfiguration.permissions();
++    }
++
++    @Override
++    public @NotNull PermissionDefault getPermissionDefault() {
++        return this.permissionConfiguration.defaultPerm();
++    }
++
++    @Override
++    public @NotNull String getAPIVersion() {
++        return this.apiVersion;
++    }
++
++    @Override
++    public @NotNull List<String> getProvidedPlugins() {
++        return this.provides;
++    }
++
++    public String getBootstrapper() {
++        return this.bootstrapper;
++    }
++
++    public String getLoader() {
++        return this.loader;
++    }
++
++    public boolean hasOpenClassloader() {
++        return this.hasOpenClassloader;
++    }
++
++    public List<DependencyConfiguration> getDependencies() {
++        return dependencies;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration.serializer;
++
++import com.google.common.collect.ImmutableCollection;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.ConfigurationOptions;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++import org.spongepowered.configurate.util.CheckedConsumer;
++
++import java.lang.reflect.Type;
++import java.util.Collection;
++import java.util.Collections;
++import java.util.List;
++
++@SuppressWarnings("unchecked")
++public abstract class ImmutableCollectionSerializer<B extends ImmutableCollection.Builder<?>, T extends Collection<?>> implements TypeSerializer<T> {
++
++    protected ImmutableCollectionSerializer() {
++    }
++
++    @Override
++    public final T deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
++        final Type entryType = this.elementType(type);
++        final @Nullable TypeSerializer<?> entrySerial = node.options().serializers().get(entryType);
++        if (entrySerial == null) {
++            throw new SerializationException(node, entryType, "No applicable type serializer for type");
++        }
++
++        if (node.isList()) {
++            final List<? extends ConfigurationNode> values = node.childrenList();
++            final B builder = this.createNew(values.size());
++            for (ConfigurationNode value : values) {
++                try {
++                    this.deserializeSingle(builder, entrySerial.deserialize(entryType, value));
++                } catch (final SerializationException ex) {
++                    ex.initPath(value::path);
++                    throw ex;
++                }
++            }
++            return (T) builder.build();
++        } else {
++            final @Nullable Object unwrappedVal = node.raw();
++            if (unwrappedVal != null) {
++                final B builder = this.createNew(1);
++                this.deserializeSingle(builder, entrySerial.deserialize(entryType, node));
++                return (T) builder.build();
++            }
++        }
++        return this.emptyValue(type, null);
++    }
++
++    @SuppressWarnings({"unchecked", "rawtypes"})
++    @Override
++    public final void serialize(final Type type, final @Nullable T obj, final ConfigurationNode node) throws SerializationException {
++        final Type entryType = this.elementType(type);
++        final @Nullable TypeSerializer entrySerial = node.options().serializers().get(entryType);
++        if (entrySerial == null) {
++            throw new SerializationException(node, entryType, "No applicable type serializer for type");
++        }
++
++        node.raw(Collections.emptyList());
++        if (obj != null) {
++            this.forEachElement(obj, el -> {
++                final ConfigurationNode child = node.appendListNode();
++                try {
++                    entrySerial.serialize(entryType, el, child);
++                } catch (final SerializationException ex) {
++                    ex.initPath(child::path);
++                    throw ex;
++                }
++            });
++        }
++    }
++
++    @SuppressWarnings({"unchecked"})
++    @Override
++    public @Nullable T emptyValue(final Type specificType, final ConfigurationOptions options) {
++        return (T) this.createNew(0).build();
++    }
++
++    protected abstract Type elementType(Type containerType) throws SerializationException;
++
++    protected abstract B createNew(int size);
++
++    protected abstract void forEachElement(T collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException;
++
++    protected abstract void deserializeSingle(B builder, @Nullable Object deserialized) throws SerializationException;
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration.serializer;
++
++import com.google.common.collect.ImmutableList;
++import org.checkerframework.checker.nullness.qual.Nullable;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.util.CheckedConsumer;
++
++import java.lang.reflect.ParameterizedType;
++import java.lang.reflect.Type;
++import java.util.List;
++
++public class ImmutableListSerializer extends ImmutableCollectionSerializer<ImmutableList.Builder<?>, List<?>> {
++
++    @Override
++    protected Type elementType(Type containerType) throws SerializationException {
++        if (!(containerType instanceof ParameterizedType)) {
++            throw new SerializationException(containerType, "Raw types are not supported for collections");
++        }
++        return ((ParameterizedType) containerType).getActualTypeArguments()[0];
++    }
++
++    @Override
++    protected ImmutableList.Builder<?> createNew(int size) {
++        return ImmutableList.builderWithExpectedSize(size);
++    }
++
++    @Override
++    protected void forEachElement(List<?> collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException {
++        for (Object obj : collection) {
++            action.accept(obj);
++        }
++    }
++
++    @SuppressWarnings({"unchecked", "rawtypes"})
++    @Override
++    protected void deserializeSingle(ImmutableList.Builder<?> builder, @Nullable Object deserialized) throws SerializationException {
++        if (deserialized == null) {
++            return;
++        }
++
++        ((ImmutableList.Builder) builder).add(deserialized);
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration.serializer;
++
++import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.spongepowered.configurate.ConfigurationNode;
++import org.spongepowered.configurate.serialize.SerializationException;
++import org.spongepowered.configurate.serialize.TypeSerializer;
++
++import java.lang.reflect.Type;
++import java.util.ArrayList;
++import java.util.List;
++import java.util.Map;
++
++public class PermissionConfigurationSerializer {
++
++    public static final Serializer SERIALIZER = new Serializer();
++
++    private static final class Serializer implements TypeSerializer<PermissionConfiguration> {
++        private Serializer() {
++            super();
++        }
++
++        @Override
++        public PermissionConfiguration deserialize(Type type, ConfigurationNode node) throws SerializationException {
++            Map<?, ?> map = (Map<?, ?>) node.node("permissions").raw();
++
++            PermissionDefault permissionDefault;
++            ConfigurationNode permNode = node.node("defaultPerm");
++            if (permNode.virtual()) {
++                permissionDefault = PermissionDefault.OP;
++            } else {
++                permissionDefault = PermissionDefault.getByName(permNode.getString());
++            }
++
++            List<Permission> result = new ArrayList<>();
++            if (map != null) {
++                for (Map.Entry<?, ?> entry : map.entrySet()) {
++                    try {
++                        result.add(Permission.loadPermission(entry.getKey().toString(), (Map<?, ?>) entry.getValue(), permissionDefault, result));
++                    } catch (Throwable ex) {
++                        throw new SerializationException(null, "Error loading permission %s".formatted(entry.getKey()), ex);
++                    }
++                }
++            }
++
++            return new PermissionConfiguration(permissionDefault, List.copyOf(result));
++        }
++
++        @Override
++        public void serialize(Type type, @org.checkerframework.checker.nullness.qual.Nullable PermissionConfiguration obj, ConfigurationNode node) throws SerializationException {
++
++        }
++
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration.serializer.constraints;
++
++import io.papermc.paper.plugin.util.NamespaceChecker;
++import org.spongepowered.configurate.objectmapping.meta.Constraint;
++import org.spongepowered.configurate.serialize.SerializationException;
++
++import java.lang.annotation.Documented;
++import java.lang.annotation.ElementType;
++import java.lang.annotation.Retention;
++import java.lang.annotation.RetentionPolicy;
++import java.lang.annotation.Target;
++import java.lang.reflect.Type;
++import java.util.Locale;
++import java.util.Set;
++import java.util.regex.Pattern;
++
++public final class PluginConfigConstraints {
++
++    public static final Set<String> RESERVED_KEYS = Set.of("bukkit", "minecraft", "mojang", "spigot", "paper");
++    public static final Set<String> VALID_PAPER_VERSIONS = Set.of("1.19");
++
++    @Documented
++    @Retention(RetentionPolicy.RUNTIME)
++    @Target(ElementType.FIELD)
++    public @interface PluginName {
++
++        final class Factory implements Constraint.Factory<PluginName, String> {
++
++            private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z\\d _.-]+$");
++
++            @Override
++            public Constraint<String> make(PluginName data, Type type) {
++                return value -> {
++                    if (value != null) {
++                        if (RESERVED_KEYS.contains(value.toLowerCase(Locale.ROOT))) {
++                            throw new SerializationException("Restricted name, cannot use '%s' as a plugin name.".formatted(data));
++                        } else if (value.indexOf(' ') != -1) {
++                            // For legacy reasons, the space condition has a separate exception message.
++                            throw new SerializationException("Restricted name, cannot use 0x20 (space character) in a plugin name.");
++                        }
++
++                        if (!VALID_NAME.matcher(value).matches()) {
++                            throw new SerializationException("name '" + value + "' contains invalid characters.");
++                        }
++                    }
++                };
++            }
++        }
++    }
++
++    @Documented
++    @Retention(RetentionPolicy.RUNTIME)
++    @Target(ElementType.FIELD)
++    public @interface PluginNameSpace {
++
++        final class Factory implements Constraint.Factory<PluginNameSpace, String> {
++
++            @Override
++            public Constraint<String> make(PluginNameSpace data, Type type) {
++                return value -> {
++                    if (value != null && !NamespaceChecker.isValidNameSpace(value)) {
++                        throw new SerializationException("provided class '%s' is in an invalid namespace.".formatted(value));
++                    }
++                };
++            }
++        }
++    }
++
++    @Documented
++    @Retention(RetentionPolicy.RUNTIME)
++    @Target(ElementType.FIELD)
++    public @interface PluginVersion {
++
++        final class Factory implements Constraint.Factory<PluginVersion, String> {
++
++            @Override
++            public Constraint<String> make(PluginVersion data, Type type) {
++                return value -> {
++                    if (value != null && !VALID_PAPER_VERSIONS.contains(value)) {
++                        throw new SerializationException("Provided plugin's version (%s) is not supported on this version.".formatted(value));
++                    }
++                };
++            }
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration.type;
++
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++import org.spongepowered.configurate.objectmapping.meta.Required;
++
++@ConfigSerializable
++public record DependencyConfiguration(
++    @Required String name,
++    boolean required,
++    boolean bootstrap
++) {
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.configuration.type;
++
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.spongepowered.configurate.objectmapping.ConfigSerializable;
++
++import java.util.List;
++
++// Record components used for deserialization!!!!
++@ConfigSerializable
++public record PermissionConfiguration(
++    PermissionDefault defaultPerm,
++    List<Permission> permissions) {
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.source;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import org.slf4j.Logger;
++
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.util.logging.Level;
++
++/**
++ * Loads all plugin providers in the given directory.
++ */
++public class DirectoryProviderSource extends FileProviderSource {
++
++    public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource();
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    public DirectoryProviderSource() {
++        super("Directory '%s'"::formatted);
++    }
++
++    @Override
++    public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception {
++        // Sym link happy, create file if missing.
++        if (!Files.isDirectory(context)) {
++            Files.createDirectories(context);
++        }
++
++        Files.walk(context, 1).filter(Files::isRegularFile).forEach((path) -> {
++            try {
++                super.registerProviders(entrypointHandler, path);
++            } catch (IllegalArgumentException ignored) {
++                // Ignore initial argument exceptions
++            } catch (Exception e) {
++                LOGGER.error("Error loading plugin: " + e.getMessage(), e);
++            }
++        });
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.source;
++
++import io.papermc.paper.plugin.PluginInitializerManager;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import io.papermc.paper.plugin.provider.type.PluginFileType;
++import org.bukkit.plugin.InvalidPluginException;
++import org.jetbrains.annotations.Nullable;
++
++import java.io.File;
++import java.io.IOException;
++import java.nio.file.FileVisitResult;
++import java.nio.file.FileVisitor;
++import java.nio.file.Files;
++import java.nio.file.Path;
++import java.nio.file.StandardCopyOption;
++import java.nio.file.attribute.BasicFileAttributes;
++import java.util.Set;
++import java.util.function.Function;
++import java.util.jar.JarFile;
++
++/**
++ * Loads a plugin provider at the given plugin jar file path.
++ */
++public class FileProviderSource implements ProviderSource<Path> {
++
++    private final Function<Path, String> contextChecker;
++
++    public FileProviderSource(Function<Path, String> contextChecker) {
++        this.contextChecker = contextChecker;
++    }
++
++    @Override
++    public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception {
++        String source = this.contextChecker.apply(context);
++
++        if (Files.notExists(context)) {
++            throw new IllegalArgumentException(source + " does not exist, cannot load a plugin from it!");
++        }
++
++        if (!Files.isRegularFile(context)) {
++            throw new IllegalArgumentException(source + " is not a file, cannot load a plugin from it!");
++        }
++
++        if (!context.getFileName().toString().endsWith(".jar")) {
++            throw new IllegalArgumentException(source + " is not a jar file, cannot load a plugin from it!");
++        }
++
++        try {
++            this.checkUpdate(context);
++
++            JarFile file = new JarFile(context.toFile());
++            PluginFileType<?,?> type = PluginFileType.guessType(file);
++            if (type == null) {
++                throw new IllegalArgumentException(source + " is not a valid plugin file, cannot load a plugin from it!");
++            }
++
++            type.register(entrypointHandler, file, context);
++        } catch (Exception exception) {
++            throw new RuntimeException(source + " failed to load!", exception);
++        }
++    }
++
++    /**
++     * Replaces a plugin with a plugin of the same plugin name in the update folder.
++     *
++     * @param file
++     */
++    private Path checkUpdate(Path file) throws Exception {
++        PluginInitializerManager pluginSystem = PluginInitializerManager.instance();
++        if (!Files.isDirectory(pluginSystem.pluginUpdatePath())) {
++            return file;
++        }
++
++        try {
++            String pluginName = this.getPluginName(file);
++            UpdateFileVisitor visitor = new UpdateFileVisitor(pluginName);
++            Files.walkFileTree(pluginSystem.pluginUpdatePath(), Set.of(), 1, visitor);
++            if (visitor.getValidPlugin() != null) {
++                Path updateLocation = visitor.getValidPlugin();
++
++                try {
++                    Files.copy(updateLocation, file, StandardCopyOption.REPLACE_EXISTING);
++                } catch (IOException exception) {
++                    throw new RuntimeException("Could not copy '" + updateLocation + "' to '" + file + "' in update plugin process", exception);
++                }
++
++                // Idk what this is about, TODO
++                File newName = new File(file.toFile().getParentFile(), updateLocation.toFile().getName());
++                file.toFile().renameTo(newName);
++                updateLocation.toFile().delete();
++            }
++        } catch (Exception e) {
++            throw new InvalidPluginException(e);
++        }
++        return file;
++    }
++
++    private String getPluginName(Path path) throws Exception {
++        JarFile file = new JarFile(path.toFile());
++        PluginFileType<?, ?> type = PluginFileType.guessType(file);
++        if (type == null) {
++            throw new IllegalArgumentException(path + " is not a valid plugin file, cannot load a plugin from it!");
++        }
++
++        return type.getConfig(file).getName();
++    }
++
++    private class UpdateFileVisitor implements FileVisitor<Path> {
++
++        private final String targetName;
++        @Nullable
++        private Path validPlugin;
++
++        private UpdateFileVisitor(String targetName) {
++            this.targetName = targetName;
++        }
++
++        @Override
++        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
++            return FileVisitResult.CONTINUE;
++        }
++
++        @Override
++        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
++            try {
++                String updatePluginName = FileProviderSource.this.getPluginName(file);
++                if (this.targetName.equals(updatePluginName)) {
++                    this.validPlugin = file;
++                    return FileVisitResult.TERMINATE;
++                }
++            } catch (Exception e) {
++                // We failed to load this data for some reason, so, we'll skip over this
++            }
++
++
++            return FileVisitResult.CONTINUE;
++        }
++
++        @Override
++        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
++            return FileVisitResult.CONTINUE;
++        }
++
++        @Override
++        public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
++            return FileVisitResult.CONTINUE;
++        }
++
++        @Nullable
++        public Path getValidPlugin() {
++            return validPlugin;
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.source;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import org.slf4j.Logger;
++
++import java.io.File;
++import java.util.List;
++
++/**
++ * Registers providers at the provided files in the add-plugin argument.
++ */
++public class PluginFlagProviderSource implements ProviderSource<List<File>> {
++
++    public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource();
++    private static final Logger LOGGER = LogUtils.getLogger();
++    private final FileProviderSource providerSource = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted);
++
++    @Override
++    public void registerProviders(EntrypointHandler entrypointHandler, List<File> context) {
++        for (File file : context) {
++            try {
++                this.providerSource.registerProviders(entrypointHandler, file.toPath());
++            } catch (Exception e) {
++                LOGGER.error("Error loading plugin: " + e.getMessage(), e);
++            }
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.source;
++
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++
++/**
++ * A provider source is responsible for giving PluginTypes an EntrypointHandler for
++ * registering providers at.
++ *
++ * @param <C> context
++ */
++public interface ProviderSource<C> {
++
++    void registerProviders(EntrypointHandler entrypointHandler, C context) throws Throwable;
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.type;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.Entrypoint;
++import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.jetbrains.annotations.Nullable;
++
++import java.nio.file.Path;
++import java.util.List;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++
++/**
++ * This is where spigot/paper plugins are registered.
++ * This will get the jar and find a certain config file, create an object
++ * then registering it into a {@link EntrypointHandler} at a certain {@link Entrypoint}.
++ */
++public abstract class PluginFileType<T, C extends PluginMeta> {
++
++    public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) {
++        @Override
++        protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) {
++            PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null;
++            if (parent.shouldCreateBootstrap()) {
++                bootstrapPluginProvider = parent.createBootstrapProvider();
++                entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider);
++            }
++
++            entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider));
++        }
++    };
++    public static final PluginFileType<SpigotPluginProvider, PluginDescriptionFile> SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) {
++        @Override
++        protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) {
++            entrypointHandler.register(Entrypoint.PLUGIN, provider);
++        }
++    };
++
++    private static final List<PluginFileType<?, ?>> VALUES = List.of(PAPER, SPIGOT);
++
++    private final String config;
++    private final PluginTypeFactory<T, C> factory;
++
++    PluginFileType(String config, PluginTypeFactory<T, C> factory) {
++        this.config = config;
++        this.factory = factory;
++    }
++
++    @Nullable
++    public static PluginFileType<?, ?> guessType(JarFile file) {
++        for (PluginFileType<?, ?> type : VALUES) {
++            JarEntry entry = file.getJarEntry(type.config);
++            if (entry != null) {
++                return type;
++            }
++        }
++
++        return null;
++    }
++
++    public T register(EntrypointHandler entrypointHandler, JarFile file, Path context) throws Exception {
++        C config = this.getConfig(file);
++        T provider = this.factory.build(file, config, context);
++        this.register(entrypointHandler, provider);
++        return provider;
++    }
++
++    public C getConfig(JarFile file) throws Exception {
++        return this.factory.create(file, file.getJarEntry(this.config));
++    }
++
++    protected abstract void register(EntrypointHandler entrypointHandler, T provider);
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.type;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++
++import java.nio.file.Path;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++
++/**
++ * A plugin type factory is responsible for building an object
++ * and config for a certain plugin type.
++ *
++ * @param <T> plugin provider type (may not be a plugin provider)
++ * @param <C> config type
++ */
++public interface PluginTypeFactory<T, C extends PluginMeta> {
++
++    T build(JarFile file, C configuration, Path source) throws Exception;
++
++    C create(JarFile file, JarEntry config) throws Exception;
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.type.paper;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import io.papermc.paper.plugin.provider.util.ProviderUtil;
++import org.bukkit.Bukkit;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++public class PaperPluginParent {
++
++    public static final PluginTypeFactory<PaperPluginParent, PaperPluginMeta> FACTORY = new PaperPluginProviderFactory();
++    private final Path path;
++    private final JarFile jarFile;
++    private final PaperPluginMeta description;
++    private final PaperPluginClassLoader classLoader;
++    private final PluginProviderContext context;
++    private final Logger logger;
++
++    public PaperPluginParent(Path path, JarFile jarFile, PaperPluginMeta description, PaperPluginClassLoader classLoader, PluginProviderContext context) {
++        this.path = path;
++        this.jarFile = jarFile;
++        this.description = description;
++        this.classLoader = classLoader;
++        this.context = context;
++        this.logger = context.getLogger();
++    }
++
++    public boolean shouldCreateBootstrap() {
++        return this.description.getBootstrapper() != null;
++    }
++
++    public PaperBootstrapProvider createBootstrapProvider() {
++        return new PaperBootstrapProvider();
++    }
++
++    public PaperServerPluginProvider createPluginProvider(PaperBootstrapProvider provider) {
++        return new PaperServerPluginProvider(provider);
++    }
++
++    public class PaperBootstrapProvider implements PluginProvider<PluginBootstrap>, ProviderStatusHolder, DependencyContextHolder {
++
++        private ProviderStatus status;
++        private PluginBootstrap lastProvided;
++
++        @Override
++        public @NotNull Path getSource() {
++            return PaperPluginParent.this.path;
++        }
++
++        @Override
++        public JarFile file() {
++            return PaperPluginParent.this.jarFile;
++        }
++
++        @Override
++        public PluginBootstrap createInstance() {
++            PluginBootstrap bootstrap = ProviderUtil.loadClass(PaperPluginParent.this.description.getBootstrapper(),
++                PluginBootstrap.class, PaperPluginParent.this.classLoader, () -> this.status = ProviderStatus.ERRORED);
++            this.status = ProviderStatus.INITIALIZED;
++            this.lastProvided = bootstrap;
++            return bootstrap;
++        }
++
++        @Override
++        public PaperPluginMeta getMeta() {
++            return PaperPluginParent.this.description;
++        }
++
++        @Override
++        public Logger getLogger() {
++            return PaperPluginParent.this.logger;
++        }
++
++        @Override
++        public ProviderStatus getLastProvidedStatus() {
++            return this.status;
++        }
++
++        @Override
++        public void setStatus(ProviderStatus status) {
++            this.status = status;
++        }
++
++        public PluginBootstrap getLastProvided() {
++            return this.lastProvided;
++        }
++
++        @Override
++        public void setContext(DependencyContext context) {
++            PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context);
++        }
++
++        @Override
++        public String toString() {
++            return "PaperBootstrapProvider{" +
++                "parent=" + PaperPluginParent.this +
++                "status=" + status +
++                ", lastProvided=" + lastProvided +
++                '}';
++        }
++    }
++
++    public class PaperServerPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder {
++
++        private final PaperBootstrapProvider bootstrapProvider;
++
++        private ProviderStatus status;
++
++        PaperServerPluginProvider(PaperBootstrapProvider bootstrapProvider) {
++            this.bootstrapProvider = bootstrapProvider;
++        }
++
++        @Override
++        public @NotNull Path getSource() {
++            return PaperPluginParent.this.path;
++        }
++
++        @Override
++        public JarFile file() {
++            return PaperPluginParent.this.jarFile;
++        }
++
++        @Override
++        public JavaPlugin createInstance() {
++            PluginBootstrap bootstrap = null;
++            if (this.bootstrapProvider != null && this.bootstrapProvider.getLastProvided() != null) {
++                bootstrap = this.bootstrapProvider.getLastProvided();
++            }
++
++            try {
++                JavaPlugin plugin;
++                if (bootstrap == null) {
++                    plugin = ProviderUtil.loadClass(PaperPluginParent.this.description.getMainClass(), JavaPlugin.class, PaperPluginParent.this.classLoader);
++                } else {
++                    plugin = bootstrap.createPlugin(PaperPluginParent.this.context);
++                }
++
++                // Don't allow plugins to load plugins other than the one defined in main. This restriction might not be necessary.
++                if (!plugin.getClass().isAssignableFrom(Class.forName(PaperPluginParent.this.description.getMainClass(), true, plugin.getClass().getClassLoader()))) {
++                    throw new IllegalArgumentException("Plugin provided must be the same type as main defined in plugin configuration!");
++                }
++
++                this.status = ProviderStatus.INITIALIZED;
++                return plugin;
++            } catch (Throwable throwable) {
++                this.status = ProviderStatus.ERRORED;
++                SneakyThrow.sneaky(throwable);
++            }
++
++            throw new AssertionError(); // Impossible
++        }
++
++        @Override
++        public PaperPluginMeta getMeta() {
++            return PaperPluginParent.this.description;
++        }
++
++        @Override
++        public Logger getLogger() {
++            return PaperPluginParent.this.logger;
++        }
++
++        @Override
++        public ProviderStatus getLastProvidedStatus() {
++            return this.status;
++        }
++
++        @Override
++        public void setStatus(ProviderStatus status) {
++            this.status = status;
++        }
++
++        public boolean shouldSkipCreation() {
++            if (this.bootstrapProvider == null) {
++                return false;
++            }
++
++            return this.bootstrapProvider.getLastProvidedStatus() == ProviderStatus.ERRORED;
++        }
++
++        /*
++        The plugin has to reuse the classloader in order to share the bootstrapper.
++        However, a plugin may have totally separate dependencies during bootstrapping.
++        This is a bit yuck, but in general we have to treat bootstrapping and normal game as connected.
++         */
++        @Override
++        public void setContext(DependencyContext context) {
++            PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context);
++        }
++
++        @Override
++        public String toString() {
++            return "PaperServerPluginProvider{" +
++                "parent=" + PaperPluginParent.this +
++                "bootstrapProvider=" + bootstrapProvider +
++                ", status=" + status +
++                '}';
++        }
++    }
++
++
++    @Override
++    public String toString() {
++        return "PaperPluginParent{" +
++            "path=" + path +
++            ", jarFile=" + jarFile +
++            ", description=" + description +
++            ", classLoader=" + classLoader +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.type.paper;
++
++import com.destroystokyo.paper.utils.PaperPluginLogger;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
++import io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader;
++import io.papermc.paper.plugin.loader.PaperClasspathBuilder;
++import io.papermc.paper.plugin.loader.PluginLoader;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import io.papermc.paper.plugin.provider.util.ProviderUtil;
++
++import java.io.BufferedReader;
++import java.io.IOException;
++import java.io.InputStreamReader;
++import java.nio.file.Path;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++class PaperPluginProviderFactory implements PluginTypeFactory<PaperPluginParent, PaperPluginMeta> {
++
++    @Override
++    public PaperPluginParent build(JarFile file, PaperPluginMeta configuration, Path source) throws Exception {
++        Logger logger = PaperPluginLogger.getLogger(configuration);
++        PluginProviderContext context = PluginProviderContextImpl.of(configuration, logger);
++
++        PaperClasspathBuilder builder = new PaperClasspathBuilder(context);
++
++        if (configuration.getLoader() != null) {
++            try (
++                PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(source, file, configuration, this.getClass().getClassLoader())
++            ) {
++                PluginLoader loader = ProviderUtil.loadClass(configuration.getLoader(), PluginLoader.class, simplePluginClassLoader);
++                loader.classloader(builder);
++            } catch (IOException e) {
++                throw new RuntimeException(e);
++            }
++        }
++
++        PaperPluginClassLoader classLoader = builder.buildClassLoader(logger, source, file, configuration);
++        return new PaperPluginParent(source, file, configuration, classLoader, context);
++    }
++
++    @Override
++    public PaperPluginMeta create(JarFile file, JarEntry config) throws Exception {
++        PaperPluginMeta configuration;
++        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(config)))) {
++            configuration = PaperPluginMeta.create(bufferedReader);
++        }
++        return configuration;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.type.spigot;
++
++import com.destroystokyo.paper.util.SneakyThrow;
++import com.destroystokyo.paper.utils.PaperPluginLogger;
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import org.bukkit.Bukkit;
++import org.bukkit.Server;
++import org.bukkit.plugin.InvalidPluginException;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.bukkit.plugin.java.LibraryLoader;
++import org.bukkit.plugin.java.PluginClassLoader;
++import org.jetbrains.annotations.NotNull;
++
++import java.io.File;
++import java.nio.file.Path;
++import java.util.HashSet;
++import java.util.Set;
++import java.util.jar.JarFile;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++
++public class SpigotPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder {
++
++    public static final PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> FACTORY = new SpigotPluginProviderFactory();
++    private static final LibraryLoader LIBRARY_LOADER = new LibraryLoader(Logger.getLogger("SpigotLibraryLoader"));
++    private final Path path;
++    private final PluginDescriptionFile description;
++    private final JarFile jarFile;
++    private final Logger logger;
++    private ProviderStatus status;
++    private DependencyContext dependencyContext;
++
++    SpigotPluginProvider(Path path, JarFile file, PluginDescriptionFile description) {
++        this.path = path;
++        this.jarFile = file;
++        this.description = description;
++        this.logger = PaperPluginLogger.getLogger(description);
++    }
++
++    @Override
++    public @NotNull Path getSource() {
++        return this.path;
++    }
++
++    @Override
++    public JarFile file() {
++        return this.jarFile;
++    }
++
++    @Override
++    public JavaPlugin createInstance() {
++        Server server = Bukkit.getServer();
++        try {
++
++            final File parentFile = server.getPluginsFolder(); // Paper
++            final File dataFolder = new File(parentFile, this.description.getName());
++            @SuppressWarnings("deprecation") final File oldDataFolder = new File(parentFile, this.description.getRawName());
++
++            // Found old data folder
++            if (dataFolder.equals(oldDataFolder)) {
++                // They are equal -- nothing needs to be done!
++            } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) {
++                server.getLogger().warning(String.format(
++                    "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'",
++                    this.description.getFullName(),
++                    this.path,
++                    oldDataFolder,
++                    dataFolder
++                ));
++            } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) {
++                if (!oldDataFolder.renameTo(dataFolder)) {
++                    throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'");
++                }
++                server.getLogger().log(Level.INFO, String.format(
++                    "While loading %s (%s) renamed data folder: `%s' to `%s'",
++                    this.description.getFullName(),
++                    this.path,
++                    oldDataFolder,
++                    dataFolder
++                ));
++            }
++
++            if (dataFolder.exists() && !dataFolder.isDirectory()) {
++                throw new InvalidPluginException(String.format(
++                    "Projected datafolder: `%s' for %s (%s) exists and is not a directory",
++                    dataFolder,
++                    this.description.getFullName(),
++                    this.path
++                ));
++            }
++
++            Set<String> missingHardDependencies = new HashSet<>(this.description.getDepend().size()); // Paper - list all missing hard depends
++            for (final String pluginName : this.description.getDepend()) {
++                if (!this.dependencyContext.hasDependency(pluginName)) {
++                    missingHardDependencies.add(pluginName); // Paper - list all missing hard depends
++                }
++            }
++            // Paper start - list all missing hard depends
++            if (!missingHardDependencies.isEmpty()) {
++                throw new UnknownDependencyException(missingHardDependencies, this.description.getFullName());
++            }
++            // Paper end
++
++            server.getUnsafe().checkSupported(this.description);
++
++            final PluginClassLoader loader;
++            try {
++                loader = new PluginClassLoader(this.getClass().getClassLoader(), this.description, dataFolder, this.path.toFile(), LIBRARY_LOADER.createLoader(this.description), this.dependencyContext); // Paper
++            } catch (InvalidPluginException ex) {
++                throw ex;
++            } catch (Throwable ex) {
++                throw new InvalidPluginException(ex);
++            }
++
++            // Override dependency context.
++            // We must provide a temporary context in order to properly handle dependencies on the plugin classloader constructor.
++            loader.dependencyContext = PaperPluginManagerImpl.getInstance();
++
++            this.status = ProviderStatus.INITIALIZED;
++            return loader.plugin;
++        } catch (Throwable ex) {
++            this.status = ProviderStatus.ERRORED;
++            SneakyThrow.sneaky(ex);
++        }
++
++        throw new AssertionError(); // Shouldn't happen
++    }
++
++    @Override
++    public PluginDescriptionFile getMeta() {
++        return this.description;
++    }
++
++    @Override
++    public Logger getLogger() {
++        return this.logger;
++    }
++
++    @Override
++    public ProviderStatus getLastProvidedStatus() {
++        return this.status;
++    }
++
++    @Override
++    public void setStatus(ProviderStatus status) {
++        this.status = status;
++    }
++
++    @Override
++    public void setContext(DependencyContext context) {
++        this.dependencyContext = context;
++    }
++
++    @Override
++    public String toString() {
++        return "SpigotPluginProvider{" +
++            "path=" + path +
++            ", description=" + description +
++            ", jarFile=" + jarFile +
++            ", status=" + status +
++            ", dependencyContext=" + dependencyContext +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.provider.type.spigot;
++
++import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
++import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
++import org.bukkit.plugin.InvalidDescriptionException;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.yaml.snakeyaml.error.YAMLException;
++
++import java.io.IOException;
++import java.io.InputStream;
++import java.nio.file.Path;
++import java.util.Locale;
++import java.util.jar.JarEntry;
++import java.util.jar.JarFile;
++
++class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
++
++    @Override
++    public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws Exception {
++        // Copied from SimplePluginManager#loadPlugins
++        // Spigot doesn't validate the name when the config is created, and instead when the plugin is loaded.
++        // Paper plugin configuration will do these checks in config serializer instead of when this is created.
++        String name = configuration.getRawName();
++        if (PluginConfigConstraints.RESERVED_KEYS.contains(name.toLowerCase(Locale.ROOT))) {
++            throw new InvalidDescriptionException("Restricted name, cannot use %s as a plugin name.".formatted(name));
++        } else if (name.indexOf(' ') != -1) {
++            throw new InvalidDescriptionException("Restricted name, cannot use 0x20 (space character) in a plugin name.");
++        }
++
++        return new SpigotPluginProvider(source, file, configuration);
++    }
++
++    @Override
++    public PluginDescriptionFile create(JarFile file, JarEntry config) throws Exception {
++        PluginDescriptionFile descriptionFile;
++        try (InputStream inputStream = file.getInputStream(config)) {
++            descriptionFile = new PluginDescriptionFile(inputStream);
++        } catch (IOException | YAMLException ex) {
++            throw new InvalidDescriptionException(ex);
++        }
++
++        return descriptionFile;
++    }
++}
++
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.storage;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.PluginInitializerManager;
++import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
++import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public class BootstrapProviderStorage extends SimpleProviderStorage<PluginBootstrap> {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    public BootstrapProviderStorage() {
++        super(new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
++            @Override
++            public void applyContext(PluginProvider<PluginBootstrap> provider, DependencyContext dependencyContext) {
++                if (provider instanceof DependencyContextHolder contextHolder) {
++                    contextHolder.setContext(dependencyContext);
++                }
++            }
++
++            @Override
++            public boolean load(PluginProvider<PluginBootstrap> provider, PluginBootstrap provided) {
++                try {
++                    PluginProviderContext context = PluginProviderContextImpl.of(provider, PluginInitializerManager.instance().pluginDirectoryPath());
++                    provided.bootstrap(context);
++                    return true;
++                } catch (Exception e) {
++                    LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e);
++                    if (provider instanceof ProviderStatusHolder statusHolder) {
++                        statusHolder.setStatus(ProviderStatus.ERRORED);
++                    }
++                    return false;
++                }
++            }
++
++            @Override
++            public List<String> requiredDependencies(PluginProvider<PluginBootstrap> provider) {
++                List<String> dependencies = new ArrayList<>();
++                if (provider.getMeta() instanceof PaperPluginMeta paperPluginMeta) {
++                    for (DependencyConfiguration configuration : paperPluginMeta.getDependencies()) {
++                        if (configuration.required() && configuration.bootstrap()) {
++                            dependencies.add(configuration.name());
++                        }
++                    }
++
++                    return dependencies;
++                }
++
++                throw new IllegalStateException();
++            }
++
++            @Override
++            public List<String> optionalDependencies(PluginProvider<PluginBootstrap> provider) {
++                List<String> dependencies = new ArrayList<>();
++                if (provider.getMeta() instanceof PaperPluginMeta paperPluginMeta) {
++                    for (DependencyConfiguration configuration : paperPluginMeta.getDependencies()) {
++                        if (!configuration.required() && configuration.bootstrap()) {
++                            dependencies.add(configuration.name());
++                        }
++                    }
++
++                    return dependencies;
++                }
++
++                throw new IllegalStateException();
++            }
++
++            @Override
++            public List<String> loadBeforeDependencies(PluginProvider<PluginBootstrap> provider) {
++                return provider.getMeta().getLoadBeforePlugins();
++            }
++        }));
++    }
++
++    @Override
++    protected void handleCycle(PluginGraphCycleException exception) {
++        List<String> logMessages = new ArrayList<>();
++        for (List<String> list : exception.getCycles()) {
++            // CoolPlugin depends on Dependency depends on CoolPlugin...
++            logMessages.add(String.join(" depends on ", list) + " depends on " + list.get(0) + "...");
++        }
++
++        LOGGER.error("Circular dependencies detected!");
++        LOGGER.error("You have a plugin that is depending on a plugin which refers back to that plugin. Your server will shut down until these are resolved, or the strategy is changed.");
++        LOGGER.error("Circular dependencies:");
++        for (String message : logMessages) {
++            LOGGER.error(message);
++        }
++        LOGGER.error("If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true");
++
++        System.exit(-1);
++    }
++
++    @Override
++    public String toString() {
++        return "BOOTSTRAP:" + super.toString();
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.storage;
++
++import io.papermc.paper.plugin.entrypoint.strategy.LegacyPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++
++import java.util.ArrayList;
++import java.util.List;
++import java.util.logging.Level;
++import java.util.logging.Logger;
++
++public abstract class ConfiguredProviderStorage<T> extends SimpleProviderStorage<T> {
++
++    private static final Logger LOGGER = Logger.getLogger("ConfiguredOrderedProviderStorage");
++    public static final boolean LEGACY_PLUGIN_LOADING = Boolean.getBoolean("paper.useLegacyPluginLoading");
++
++    protected ConfiguredProviderStorage(ProviderConfiguration<T> onLoad) {
++        // This doesn't work with reloading.
++        // Should we care?
++        super(LEGACY_PLUGIN_LOADING ? new LegacyPluginLoadingStrategy<>(onLoad) : new ModernPluginLoadingStrategy<>(onLoad));
++    }
++
++    @Override
++    protected void handleCycle(PluginGraphCycleException exception) {
++        List<String> logMessages = new ArrayList<>();
++        for (List<String> list : exception.getCycles()) {
++            // CoolPlugin depends on Dependency depends on CoolPlugin...
++            logMessages.add(String.join(" depends on ", list) + " depends on " + list.get(0) + "...");
++        }
++
++        LOGGER.log(Level.SEVERE, "Circular dependencies detected!");
++        LOGGER.log(Level.SEVERE, "You have a plugin that is depending on a plugin which refers back to that plugin. Your server will shut down until these are resolved, or the strategy is changed.");
++        LOGGER.log(Level.SEVERE, "Circular dependencies:");
++        for (String message : logMessages) {
++            LOGGER.log(Level.SEVERE, message);
++        }
++        LOGGER.log(Level.SEVERE, "If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true");
++
++        if (this.exitOnCycleDependencies()) {
++            System.exit(-1);
++        }
++    }
++
++    public boolean exitOnCycleDependencies() {
++        return true;
++    }
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.storage;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++/**
++ * A provider storage is meant to be a singleton that stores providers.
++ *
++ * @param <T> provider type
++ */
++public interface ProviderStorage<T> {
++
++    void register(PluginProvider<T> provider);
++
++    void enter();
++
++    Iterable<PluginProvider<T>> getRegisteredProviders();
++
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.storage;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.ProviderStatus;
++import io.papermc.paper.plugin.provider.ProviderStatusHolder;
++import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
++import org.bukkit.plugin.Plugin;
++import org.bukkit.plugin.java.JavaPlugin;
++import org.slf4j.Logger;
++
++import java.util.List;
++
++public class ServerPluginProviderStorage extends ConfiguredProviderStorage<JavaPlugin> {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    public ServerPluginProviderStorage() {
++        super(new ProviderConfiguration<>() {
++            @Override
++            public void applyContext(PluginProvider<JavaPlugin> provider, DependencyContext dependencyContext) {
++                Plugin alreadyLoadedPlugin = PaperPluginManagerImpl.getInstance().getPlugin(provider.getMeta().getName());
++                if (alreadyLoadedPlugin != null) {
++                    throw new IllegalStateException("Provider " + provider + " attempted to add duplicate plugin identifier " + alreadyLoadedPlugin + " THIS WILL CREATE BUGS!!!");
++                }
++
++                if (provider instanceof DependencyContextHolder contextHolder) {
++                    contextHolder.setContext(dependencyContext);
++                }
++            }
++
++            @Override
++            public boolean load(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
++                try {
++                    provided.getLogger().info(String.format("Loading server plugin %s", provided.getPluginMeta().getDisplayName()));
++                    PaperPluginManagerImpl.getInstance().loadPlugin(provided); // We have to add it to the map before the plugin is loaded
++                    provided.onLoad();
++                    return true;
++                } catch (Throwable ex) {
++                    if (provider instanceof ProviderStatusHolder statusHolder) {
++                        statusHolder.setStatus(ProviderStatus.ERRORED);
++                    }
++                    LOGGER.error("Could not load server plugin '%s' in folder '%s' (Is it up to date?)".formatted(provider.getFileName(), provider.getParentSource()), ex);
++                    return false;
++                }
++            }
++
++            @Override
++            public List<String> requiredDependencies(PluginProvider<JavaPlugin> provider) {
++                return provider.getMeta().getPluginDependencies();
++            }
++
++            @Override
++            public List<String> optionalDependencies(PluginProvider<JavaPlugin> provider) {
++                return provider.getMeta().getPluginSoftDependencies();
++            }
++
++            @Override
++            public List<String> loadBeforeDependencies(PluginProvider<JavaPlugin> provider) {
++                return provider.getMeta().getLoadBeforePlugins();
++            }
++        });
++    }
++
++    @Override
++    protected void filterLoadingProviders(List<PluginProvider<JavaPlugin>> pluginProviders) {
++         /*
++        Have to do this to prevent loading plugin providers that have failed initializers.
++        This is a hack and a better solution here would be to store failed plugin providers elsewhere.
++         */
++        pluginProviders.removeIf((provider) -> (provider instanceof PaperPluginParent.PaperServerPluginProvider pluginProvider && pluginProvider.shouldSkipCreation()));
++    }
++
++    @Override
++    public String toString() {
++        return "PLUGIN:" + super.toString();
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.storage;
++
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
++import io.papermc.paper.plugin.provider.PluginProvider;
++
++import java.util.ArrayList;
++import java.util.List;
++
++public abstract class SimpleProviderStorage<T> implements ProviderStorage<T> {
++
++    protected final List<PluginProvider<T>> providers = new ArrayList<>();
++    protected ProviderLoadingStrategy<T> strategy;
++
++    protected SimpleProviderStorage(ProviderLoadingStrategy<T> strategy) {
++        this.strategy = strategy;
++    }
++
++    @Override
++    public void register(PluginProvider<T> provider) {
++        this.providers.add(provider);
++    }
++
++    @Override
++    public void enter() {
++        List<PluginProvider<T>> providerList = new ArrayList<>(this.providers);
++        this.filterLoadingProviders(providerList);
++
++        try {
++            for (T plugin : this.strategy.loadProviders(providerList)) {
++                this.processProvided(plugin);
++            }
++        } catch (PluginGraphCycleException exception) {
++            this.handleCycle(exception);
++        }
++    }
++
++    @Override
++    public Iterable<PluginProvider<T>> getRegisteredProviders() {
++        return this.providers;
++    }
++
++    public void processProvided(T provided) {}
++
++    // Mutable enter
++    protected void filterLoadingProviders(List<PluginProvider<T>> providers) {}
++
++    protected abstract void handleCycle(PluginGraphCycleException exception);
++
++    @Override
++    public String toString() {
++        return "SimpleProviderStorage{" +
++            "providers=" + this.providers +
++            ", strategy=" + this.strategy +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/storage/package-info.java b/src/main/java/io/papermc/paper/plugin/storage/package-info.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/storage/package-info.java
+@@ -0,0 +0,0 @@
++/**
++ * Classes in this package are supposed to connect components of {@link io.papermc.paper.plugin.entrypoint} and {@link io.papermc.paper.plugin.provider} packages.
++ * @see io.papermc.paper.plugin.entrypoint.Entrypoint
++ */
++package io.papermc.paper.plugin.storage;
+diff --git a/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.util;
++
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.provider.source.ProviderSource;
++import org.slf4j.Logger;
++
++public class EntrypointUtil {
++
++    private static final Logger LOGGER = LogUtils.getLogger();
++
++    public static <C> void registerProvidersFromSource(ProviderSource<C> source, C context) {
++        try {
++            source.registerProviders(LaunchEntryPointHandler.INSTANCE, context);
++        } catch (Throwable e) {
++            LOGGER.error(e.getMessage(), e);
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.util;
++
++import org.jetbrains.annotations.ApiStatus;
++import org.jetbrains.annotations.NotNull;
++
++@ApiStatus.Internal
++public class NamespaceChecker {
++
++    private static final String[] QUICK_INVALID_NAMESPACES = {
++        "net.minecraft.",
++        "org.bukkit.",
++        "io.papermc.paper.",
++        "com.destroystokoyo.paper."
++    };
++
++    /**
++     * Used for a variety of namespaces that shouldn't be resolved and should instead be moved to
++     * other classloaders. We can assume this because only plugins should be using this classloader.
++     *
++     * @param name namespace
++     */
++    public static void validateNameSpaceForClassloading(@NotNull String name) throws ClassNotFoundException {
++        if (!isValidNameSpace(name)) {
++            throw new ClassNotFoundException(name);
++        }
++    }
++
++    public static boolean isValidNameSpace(@NotNull String name) {
++        for (String string : QUICK_INVALID_NAMESPACES) {
++            if (name.startsWith(string)) {
++                return false;
++            }
++        }
++
++        return true;
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
++++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+@@ -0,0 +0,0 @@
+ package io.papermc.paper.util;
+ 
++import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+ import org.bukkit.plugin.java.JavaPlugin;
+ import org.bukkit.plugin.java.PluginClassLoader;
+ import org.jetbrains.annotations.Nullable;
+ 
++import java.util.Objects;
+ import java.util.Optional;
+ 
+ public class StackWalkerUtil {
+@@ -0,0 +0,0 @@ public class StackWalkerUtil {
+     public static JavaPlugin getFirstPluginCaller() {
+         Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+             .walk(stream -> stream
+-                .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader)
+                 .map((frame) -> {
+-                    PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader();
+-                    return classLoader.getPlugin();
++                    ClassLoader classLoader =  frame.getDeclaringClass().getClassLoader();
++                    JavaPlugin plugin;
++                    if (classLoader instanceof PaperPluginClassLoader pluginClassLoader) {
++                        plugin = pluginClassLoader.getLoadedJavaPlugin();
++                    } else if (classLoader instanceof PluginClassLoader spigotClassloader) {
++                        plugin = spigotClassloader.getPlugin();
++                    } else {
++                        plugin = null;
++                    }
++
++                    return plugin;
+                 })
++                .filter(Objects::nonNull)
+                 .findFirst());
+ 
+         return foundFrame.orElse(null);
+diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -0,0 +0,0 @@ public class BuiltInRegistries {
+     }
+ 
+     public static void bootStrap() {
++        // Paper start
++        bootStrap(() -> {});
++    }
++    public static void bootStrap(Runnable runnable) {
++        // Paper end
+         createContents();
++        runnable.run(); // Paper
+         freeze();
+         validate(REGISTRY);
+     }
+diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/Bootstrap.java
++++ b/src/main/java/net/minecraft/server/Bootstrap.java
+@@ -0,0 +0,0 @@ public class Bootstrap {
+                     EntitySelectorOptions.bootStrap();
+                     DispenseItemBehavior.bootStrap();
+                     CauldronInteraction.bootStrap();
+-                    BuiltInRegistries.bootStrap();
++                    // Paper start
++                    BuiltInRegistries.bootStrap(() -> {
++                        io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping
++                    });
++                    // Paper end
+                     Bootstrap.wrapStreams();
+                 }
+                 // CraftBukkit start - easier than fixing the decompile
+diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/Main.java
++++ b/src/main/java/net/minecraft/server/Main.java
+@@ -0,0 +0,0 @@ public class Main {
+                 JvmProfiler.INSTANCE.start(Environment.SERVER);
+             }
+ 
++            // Paper start
++
++            // We have to load the bukkit configuration inorder to get the update folder location.
++            io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionset);
++            // Register the default plugin directory
++            io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath());
++            @SuppressWarnings("unchecked")
++            java.util.List<File> files = (java.util.List<File>) optionset.valuesOf("add-plugin");
++            // Register plugins from the flag
++            io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files);
++            // Paper end
+             Bootstrap.bootStrap();
+             Bootstrap.validate();
+             Util.startTimerHackThread();
+diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
+     private final CraftCommandMap commandMap = new CraftCommandMap(this);
+     private final SimpleHelpMap helpMap = new SimpleHelpMap(this);
+     private final StandardMessenger messenger = new StandardMessenger();
+-    private final SimplePluginManager pluginManager = new SimplePluginManager(this, this.commandMap);
++    private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap);
++    public final io.papermc.paper.plugin.manager.PaperPluginManagerImpl paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); {this.pluginManager.paperPluginManager = this.paperPluginManager;} // Paper
+     private final StructureManager structureManager;
+     protected final DedicatedServer console;
+     protected final DedicatedPlayerList playerList;
+@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
+     }
+ 
+     public void loadPlugins() {
+-        this.pluginManager.registerInterface(JavaPluginLoader.class);
+-
+-        File pluginFolder = (File) console.options.valueOf("plugins");
+-
+-        if (pluginFolder.exists()) {
+-            Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder);
+-            for (Plugin plugin : plugins) {
+-                try {
+-                    String message = String.format("Loading %s", plugin.getDescription().getFullName());
+-                    plugin.getLogger().info(message);
+-                    plugin.onLoad();
+-                } catch (Throwable ex) {
+-                    Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
+-                }
+-            }
+-        } else {
+-            pluginFolder.mkdir();
+-        }
++        io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation
+     }
+ 
+     public void enablePlugins(PluginLoadOrder type) {
+@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
+     private void enablePlugin(Plugin plugin) {
+         try {
+             List<Permission> perms = plugin.getDescription().getPermissions();
+-
++            List<Permission> permsToLoad = new ArrayList<>(); // Paper
+             for (Permission perm : perms) {
+-                try {
+-                    this.pluginManager.addPermission(perm, false);
+-                } catch (IllegalArgumentException ex) {
+-                    this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
++                // Paper start
++                if (this.paperPluginManager.getPermission(perm.getName()) == null) {
++                    permsToLoad.add(perm);
++                } else {
++                    this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered");
++                // Paper end
+                 }
+             }
+-            this.pluginManager.dirtyPermissibles();
++            this.paperPluginManager.addPermissions(permsToLoad); // Paper
+ 
+             this.pluginManager.enablePlugin(plugin);
+         } catch (Throwable ex) {
+diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
++++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
+@@ -0,0 +0,0 @@ public class MinecraftInternalPlugin extends PluginBase {
+     public PluginDescriptionFile getDescription() {
+         return pdf;
+     }
++    // Paper start
++    @Override
++    public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
++        return pdf;
++    }
++    // Paper end
+ 
+     @Override
+     public FileConfiguration getConfig() {
+diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+@@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
+         net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
+         return nmsItemStack.getItem().getDescriptionId(nmsItemStack);
+     }
++    // Paper start
++    @Override
++    public boolean isSupportedApiVersion(String apiVersion) {
++        return apiVersion != null && SUPPORTED_API.contains(apiVersion);
++    }
++    // Paper end
+ 
+     /**
+      * This helper class represents the different NBT Tags.
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier
+@@ -0,0 +1 @@
++io.papermc.paper.plugin.entrypoint.classloader.PaperClassloaderBytecodeModifier
+diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage
+@@ -0,0 +1 @@
++io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage
+diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.bukkit.Server;
++import org.bukkit.command.Command;
++import org.bukkit.command.CommandSender;
++import org.bukkit.configuration.file.FileConfiguration;
++import org.bukkit.generator.BiomeProvider;
++import org.bukkit.generator.ChunkGenerator;
++import org.bukkit.plugin.PluginBase;
++import org.bukkit.plugin.PluginDescriptionFile;
++import org.bukkit.plugin.PluginLoader;
++import org.bukkit.plugin.PluginLogger;
++
++import java.io.File;
++import java.io.InputStream;
++import java.util.List;
++
++public class PaperTestPlugin extends PluginBase {
++    private final String pluginName;
++    private boolean enabled = true;
++    private final PluginMeta configuration;
++
++    public PaperTestPlugin(String pluginName) {
++        this.pluginName = pluginName;
++        this.configuration = new TestPluginMeta(pluginName);
++    }
++
++    public PaperTestPlugin(PluginMeta configuration) {
++        this.configuration = configuration;
++        this.pluginName = configuration.getName();
++    }
++
++    @Override
++    public File getDataFolder() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public PluginDescriptionFile getDescription() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public PluginMeta getPluginMeta() {
++        return this.configuration;
++    }
++
++    @Override
++    public FileConfiguration getConfig() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public InputStream getResource(String filename) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public void saveConfig() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public void saveDefaultConfig() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public void saveResource(String resourcePath, boolean replace) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public void reloadConfig() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public PluginLogger getLogger() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public PluginLoader getPluginLoader() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public Server getServer() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public boolean isEnabled() {
++        return enabled;
++    }
++
++    public void setEnabled(boolean enabled) {
++        this.enabled = enabled;
++    }
++
++    @Override
++    public void onDisable() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public void onLoad() {
++    }
++
++    @Override
++    public void onEnable() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public boolean isNaggable() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public void setNaggable(boolean canNag) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public BiomeProvider getDefaultBiomeProvider(String worldName, String id) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    @Override
++    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java b/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.junit.Assert;
++import org.junit.Before;
++import org.junit.Test;
++
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.concurrent.atomic.AtomicInteger;
++
++public class PluginDependencyLoadingTest {
++
++    private static List<PluginProvider<PaperTestPlugin>> REGISTERED_PROVIDERS = new ArrayList<>();
++    private static Map<String, Integer> LOAD_ORDER = new HashMap<>();
++
++    static {
++        setup();
++    }
++
++    private static TestJavaPluginProvider setup(String identifier, String[] hard, String[] soft, String[] before) {
++        TestPluginMeta configuration = new TestPluginMeta(identifier);
++        configuration.setHardDependencies(List.of(hard));
++        configuration.setSoftDependencies(List.of(soft));
++        configuration.setLoadBefore(List.of(before));
++
++        TestJavaPluginProvider provider = new TestJavaPluginProvider(configuration);
++        REGISTERED_PROVIDERS.add(provider);
++        return provider;
++    }
++
++    /**
++     * Obfuscated plugin names, this uses a real dependency tree...
++     */
++    private static void setup() {
++        setup("RedAir", new String[]{}, new String[]{"NightShovel", "EmeraldFire"}, new String[]{"GreenShovel", "IronSpork", "BrightBlueShovel", "WireDoor"});
++        setup("BigGrass", new String[]{}, new String[]{"IronEarth", "RedAir"}, new String[]{"BlueFire"});
++        setup("BlueFire", new String[]{}, new String[]{}, new String[]{});
++        setup("BigPaper", new String[]{}, new String[]{"BlueFire"}, new String[]{});
++        setup("EmeraldSpork", new String[]{}, new String[]{}, new String[]{"GoldPaper", "YellowSnow"});
++        setup("GreenShovel", new String[]{}, new String[]{}, new String[]{});
++        setup("BrightBlueGrass", new String[]{"BigPaper"}, new String[]{"DarkSpork"}, new String[]{});
++        setup("GoldPaper", new String[]{}, new String[]{"BlueFire"}, new String[]{});
++        setup("GreenGlass", new String[]{}, new String[]{}, new String[]{});
++        setup("GoldNeptune", new String[]{}, new String[]{"GreenShovel", "GoldNeptuneVersioning"}, new String[]{});
++        setup("RedPaper", new String[]{}, new String[]{"GoldPaper", "GoldFire", "EmeraldGrass", "BlueFire", "CopperSpork", "YellowDoor", "OrangeClam", "BlueSponge", "GoldNeptune", "BrightBlueGrass", "DarkSpoon", "BigShovel", "GreenGlass", "IronGlass"}, new String[]{"IronPaper", "YellowFire"});
++        setup("YellowGrass", new String[]{}, new String[]{"RedAir"}, new String[]{});
++        setup("WireFire", new String[]{}, new String[]{"RedPaper", "WireGrass", "YellowSpork", "NightAir"}, new String[]{});
++        setup("OrangeNeptune", new String[]{}, new String[]{}, new String[]{});
++        setup("BigSpoon", new String[]{"YellowGrass", "GreenShovel"}, new String[]{"RedAir", "GoldNeptune", "BrightBlueGrass", "LightDoor", "LightSpork", "LightEarth", "NightDoor", "OrangeSpoon", "GoldSponge", "GoldDoor", "DarkPaper", "RedPaper", "GreenGlass", "IronGlass", "NightGlass", "BigGrass", "BlueFire", "YellowSpoon", "DiamondGrass", "DiamondShovel", "DarkSnow", "EmeraldGlass", "EmeraldSpoon", "LightFire", "WireGrass", "RedEarth", "WireFire"}, new String[]{});
++        setup("CopperSnow", new String[]{}, new String[]{"RedSnow", "OrangeFire", "WireAir", "GreenGlass", "NightSpork", "EmeraldPaper"}, new String[]{"BlueGrass"});
++        setup("BrightBluePaper", new String[]{}, new String[]{"GoldEarth", "BrightBlueSpoon", "CopperGlass", "LightSporkChat", "DarkAir", "LightEarth", "DiamondDoor", "YellowShovel", "BlueAir", "DarkShovel", "GoldPaper", "BlueFire", "GreenGlass", "YellowSpork", "BigGrass", "OrangePaper", "DarkPaper"}, new String[]{"WireShovel"});
++        setup("LightSponge", new String[]{}, new String[]{}, new String[]{});
++        setup("OrangeShovel", new String[]{}, new String[]{}, new String[]{});
++        setup("GoldGrass", new String[]{}, new String[]{"GreenGlass", "BlueFire"}, new String[]{});
++        setup("IronSponge", new String[]{}, new String[]{"DiamondEarth"}, new String[]{});
++        setup("EmeraldSnow", new String[]{}, new String[]{}, new String[]{});
++        setup("BlueSpoon", new String[]{"BigGrass"}, new String[]{"GreenGlass", "GoldPaper", "GreenShovel", "YellowClam"}, new String[]{});
++        setup("BigSpork", new String[]{}, new String[]{"BigPaper"}, new String[]{});
++        setup("BluePaper", new String[]{}, new String[]{"BigClam", "RedSpoon", "GreenFire", "WireSnow", "OrangeSnow", "BlueFire", "BrightBlueGrass", "YellowSpork", "GreenGlass"}, new String[]{});
++        setup("OrangeSpork", new String[]{}, new String[]{}, new String[]{});
++        setup("DiamondNeptune", new String[]{}, new String[]{"GreenGlass", "GreenShovel", "YellowNeptune"}, new String[]{});
++        setup("BigFire", new String[]{}, new String[]{"BlueFire", "BrightBlueDoor", "GreenGlass"}, new String[]{});
++        setup("NightNeptune", new String[]{}, new String[]{"BlueFire", "DarkGlass", "GoldPaper", "YellowNeptune", "BlueShovel"}, new String[]{});
++        setup("YellowEarth", new String[]{"RedAir"}, new String[]{}, new String[]{});
++        setup("DiamondClam", new String[]{}, new String[]{}, new String[]{});
++        setup("CopperAir", new String[]{}, new String[]{"BigPaper"}, new String[]{});
++        setup("NightSpoon", new String[]{"OrangeNeptune"}, new String[]{"BlueFire", "GreenGlass", "RedSpork", "GoldPaper", "BigShovel", "YellowSponge", "EmeraldSpork"}, new String[]{});
++        setup("GreenClam", new String[]{}, new String[]{"GreenShovel", "BrightBlueEarth", "BigSpoon", "RedPaper", "BlueFire", "GreenGlass", "WireFire", "GreenSnow"}, new String[]{});
++        setup("YellowPaper", new String[]{}, new String[]{}, new String[]{});
++        setup("WireGlass", new String[]{"YellowGrass"}, new String[]{"YellowGlass", "BigSpoon", "CopperSnow", "GreenGlass", "BlueEarth"}, new String[]{});
++        setup("BlueSpork", new String[]{}, new String[]{"BrightBlueGrass"}, new String[]{});
++        setup("CopperShovel", new String[]{}, new String[]{"GreenGlass"}, new String[]{});
++        setup("RedClam", new String[]{}, new String[]{}, new String[]{});
++        setup("EmeraldClam", new String[]{}, new String[]{"BlueFire"}, new String[]{});
++        setup("DarkClam", new String[]{}, new String[]{"GoldAir", "LightGlass"}, new String[]{});
++        setup("WireSpoon", new String[]{}, new String[]{"GoldPaper", "LightSnow"}, new String[]{});
++        setup("CopperNeptune", new String[]{}, new String[]{"GreenGlass", "BigGrass"}, new String[]{});
++        setup("RedNeptune", new String[]{}, new String[]{}, new String[]{});
++        setup("GreenAir", new String[]{}, new String[]{}, new String[]{});
++        setup("RedFire", new String[]{"BrightBlueGrass", "BigPaper"}, new String[]{"BlueFire", "GreenGlass", "BigGrass"}, new String[]{});
++    }
++
++    @Before
++    public void loadProviders() {
++        AtomicInteger currentLoad = new AtomicInteger();
++        ModernPluginLoadingStrategy<PaperTestPlugin> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
++            @Override
++            public void applyContext(PluginProvider<PaperTestPlugin> provider, DependencyContext dependencyContext) {
++            }
++
++            @Override
++            public boolean load(PluginProvider<PaperTestPlugin> provider, PaperTestPlugin provided) {
++                LOAD_ORDER.put(provider.getMeta().getName(), currentLoad.getAndIncrement());
++                return false;
++            }
++
++            @Override
++            public List<String> requiredDependencies(PluginProvider<PaperTestPlugin> provider) {
++                return provider.getMeta().getPluginDependencies();
++            }
++
++            @Override
++            public List<String> optionalDependencies(PluginProvider<PaperTestPlugin> provider) {
++                return provider.getMeta().getPluginSoftDependencies();
++            }
++
++            @Override
++            public List<String> loadBeforeDependencies(PluginProvider<PaperTestPlugin> provider) {
++                return provider.getMeta().getLoadBeforePlugins();
++            }
++        });
++
++        modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS);
++    }
++
++    @Test
++    public void testDependencies() {
++        for (PluginProvider<PaperTestPlugin> provider : REGISTERED_PROVIDERS) {
++            TestPluginMeta pluginMeta = (TestPluginMeta) provider.getMeta();
++            String identifier = pluginMeta.getName();
++            Assert.assertTrue("Provider wasn't loaded! (%s)".formatted(identifier), LOAD_ORDER.containsKey(identifier));
++
++            int index = LOAD_ORDER.get(identifier);
++
++            // Hard dependencies should be loaded BEFORE
++            for (String hardDependency : pluginMeta.getPluginDependencies()) {
++                Assert.assertTrue("Plugin (%s) is missing hard dependency (%s)".formatted(identifier, hardDependency), LOAD_ORDER.containsKey(hardDependency));
++
++                int dependencyIndex = LOAD_ORDER.get(hardDependency);
++                Assert.assertTrue("Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, hardDependency), index > dependencyIndex);
++            }
++
++            for (String softDependency : pluginMeta.getPluginSoftDependencies()) {
++                if (!LOAD_ORDER.containsKey(softDependency)) {
++                    continue;
++                }
++
++                int dependencyIndex = LOAD_ORDER.get(softDependency);
++
++                Assert.assertTrue("Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, softDependency), index > dependencyIndex);
++            }
++
++            for (String loadBefore : pluginMeta.getLoadBeforePlugins()) {
++                if (!LOAD_ORDER.containsKey(loadBefore)) {
++                    continue;
++                }
++
++                int dependencyIndex = LOAD_ORDER.get(loadBefore);
++                Assert.assertTrue("Plugin (%s) was NOT loaded BEFORE loadbefore dependency. (%s)".formatted(identifier, loadBefore), index < dependencyIndex);
++            }
++        }
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginLoadingTest.java b/src/test/java/io/papermc/paper/plugin/PluginLoadingTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import org.bukkit.Bukkit;
++import org.bukkit.event.Event;
++import org.bukkit.permissions.Permission;
++import org.bukkit.plugin.PluginManager;
++import org.bukkit.support.AbstractTestingBase;
++import org.junit.After;
++import org.junit.Test;
++
++import static org.hamcrest.MatcherAssert.assertThat;
++import static org.hamcrest.Matchers.*;
++
++public class PluginManagerTest extends AbstractTestingBase {
++
++    private static final PluginManager pm = Bukkit.getPluginManager();
++
++    @Test
++    public void testSyncSameThread() {
++        final Event event = new TestEvent(false);
++        pm.callEvent(event);
++    }
++
++    @Test
++    public void testRemovePermissionByNameLower() {
++        this.testRemovePermissionByName("lower");
++    }
++
++    @Test
++    public void testRemovePermissionByNameUpper() {
++        this.testRemovePermissionByName("UPPER");
++    }
++
++    @Test
++    public void testRemovePermissionByNameCamel() {
++        this.testRemovePermissionByName("CaMeL");
++    }
++
++    @Test
++    public void testRemovePermissionByPermissionLower() {
++        this.testRemovePermissionByPermission("lower");
++    }
++
++    @Test
++    public void testRemovePermissionByPermissionUpper() {
++        this.testRemovePermissionByPermission("UPPER");
++    }
++
++    @Test
++    public void testRemovePermissionByPermissionCamel() {
++        this.testRemovePermissionByPermission("CaMeL");
++    }
++
++    private void testRemovePermissionByName(final String name) {
++        final Permission perm = new Permission(name);
++        pm.addPermission(perm);
++        assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
++        pm.removePermission(name);
++        assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
++    }
++
++    private void testRemovePermissionByPermission(final String name) {
++        final Permission perm = new Permission(name);
++        pm.addPermission(perm);
++        assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
++        pm.removePermission(perm);
++        assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
++    }
++
++    @After
++    public void tearDown() {
++        pm.clearPlugins();
++        assertThat(pm.getPermissions(), is(empty()));
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
++import org.bukkit.Bukkit;
++import org.bukkit.event.Event;
++import org.bukkit.event.EventHandler;
++import org.bukkit.event.Listener;
++import org.junit.Assert;
++import org.junit.Test;
++
++public class SyntheticEventTest {
++
++    @Test
++    public void test() {
++        PaperTestPlugin paperTestPlugin = new PaperTestPlugin("synthetictest");
++        PaperPluginManagerImpl paperPluginManager = new PaperPluginManagerImpl(Bukkit.getServer(), null, null);
++
++        TestEvent event = new TestEvent(false);
++        Impl impl = new Impl();
++
++        paperPluginManager.registerEvents(impl, paperTestPlugin);
++        paperPluginManager.callEvent(event);
++
++        Assert.assertEquals(1, impl.callCount);
++    }
++
++    public abstract static class Base<E extends Event> implements Listener {
++        int callCount = 0;
++
++        public void accept(E evt) {
++            callCount++;
++        }
++    }
++
++    public static class Impl extends Base<TestEvent> {
++        @Override
++        @EventHandler
++        public void accept(TestEvent evt) {
++            super.accept(evt);
++        }
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/TestEvent.java b/src/test/java/io/papermc/paper/plugin/TestEvent.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/TestEvent.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++
++import org.bukkit.event.Event;
++import org.bukkit.event.HandlerList;
++
++public class TestEvent extends Event {
++    private static final HandlerList handlers = new HandlerList();
++
++    public TestEvent(boolean async) {
++        super(async);
++    }
++
++    @Override
++    public HandlerList getHandlers() {
++        return handlers;
++    }
++
++    public static HandlerList getHandlerList() {
++        return handlers;
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.jetbrains.annotations.NotNull;
++
++import java.nio.file.Path;
++import java.util.jar.JarFile;
++import java.util.logging.Logger;
++
++public class TestJavaPluginProvider implements PluginProvider<PaperTestPlugin> {
++
++    private final TestPluginMeta testPluginConfiguration;
++
++    public TestJavaPluginProvider(TestPluginMeta testPluginConfiguration) {
++        this.testPluginConfiguration = testPluginConfiguration;
++    }
++
++    @Override
++    public @NotNull Path getSource() {
++        return Path.of("dummy");
++    }
++
++    @Override
++    public JarFile file() {
++        throw new UnsupportedOperationException();
++    }
++
++    @Override
++    public PaperTestPlugin createInstance() {
++        return new PaperTestPlugin(this.testPluginConfiguration);
++    }
++
++    @Override
++    public TestPluginMeta getMeta() {
++        return this.testPluginConfiguration;
++    }
++
++    @Override
++    public Logger getLogger() {
++        return Logger.getLogger("TestPlugin");
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import org.bukkit.permissions.Permission;
++import org.bukkit.permissions.PermissionDefault;
++import org.bukkit.plugin.PluginLoadOrder;
++import org.jetbrains.annotations.NotNull;
++import org.jetbrains.annotations.Nullable;
++
++import java.util.List;
++
++public class TestPluginMeta implements PluginMeta {
++
++    private final String identifier;
++    private List<String> hardDependencies = List.of();
++    private List<String> softDependencies = List.of();
++    private List<String> loadBefore = List.of();
++
++    public TestPluginMeta(String identifier) {
++        this.identifier = identifier;
++    }
++
++    @Override
++    public @NotNull String getName() {
++        return this.identifier;
++    }
++
++    @Override
++    public @NotNull String getMainClass() {
++        return "null";
++    }
++
++    @Override
++    public @NotNull PluginLoadOrder getLoadOrder() {
++        return PluginLoadOrder.POSTWORLD;
++    }
++
++    @Override
++    public @NotNull String getVersion() {
++        return "1.0";
++    }
++
++    @Override
++    public @Nullable String getLoggerPrefix() {
++        return this.identifier;
++    }
++
++    public void setHardDependencies(List<String> hardDependencies) {
++        this.hardDependencies = hardDependencies;
++    }
++
++    @Override
++    public @NotNull List<String> getPluginDependencies() {
++        return this.hardDependencies;
++    }
++
++    public void setSoftDependencies(List<String> softDependencies) {
++        this.softDependencies = softDependencies;
++    }
++
++    @Override
++    public @NotNull List<String> getPluginSoftDependencies() {
++        return this.softDependencies;
++    }
++
++    public void setLoadBefore(List<String> loadBefore) {
++        this.loadBefore = loadBefore;
++    }
++
++    @Override
++    public @NotNull List<String> getLoadBeforePlugins() {
++        return this.loadBefore;
++    }
++
++    @Override
++    public @NotNull List<String> getProvidedPlugins() {
++        return List.of();
++    }
++
++    @Override
++    public @NotNull List<String> getAuthors() {
++        return List.of();
++    }
++
++    @Override
++    public @NotNull List<String> getContributors() {
++        return List.of();
++    }
++
++    @Override
++    public @Nullable String getDescription() {
++        return "null";
++    }
++
++    @Override
++    public @Nullable String getWebsite() {
++        return "null";
++    }
++
++    @Override
++    public @NotNull List<Permission> getPermissions() {
++        return List.of();
++    }
++
++    @Override
++    public @NotNull PermissionDefault getPermissionDefault() {
++        return PermissionDefault.TRUE;
++    }
++
++    @Override
++    public @NotNull String getAPIVersion() {
++        return "null";
++    }
++}
+diff --git a/src/test/java/io/papermc/paper/testing/DummyServer.java b/src/test/java/io/papermc/paper/testing/DummyServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/test/java/io/papermc/paper/testing/DummyServer.java
++++ b/src/test/java/io/papermc/paper/testing/DummyServer.java
+@@ -0,0 +0,0 @@ public final class DummyServer {
+             return new LazyRegistry(() -> CraftRegistry.createRegistry(invocation.getArgument(0, Class.class), AbstractTestingBase.REGISTRY_CUSTOM));
+         });
+ 
+-        final PluginManager pluginManager = new SimplePluginManager(dummyServer, new SimpleCommandMap(dummyServer));
++        final PluginManager pluginManager = new  io.papermc.paper.plugin.manager.PaperPluginManagerImpl(dummyServer, new SimpleCommandMap(dummyServer), null);
+         when(dummyServer.getPluginManager()).thenReturn(pluginManager);
+ 
+         Bukkit.setServer(dummyServer);
diff --git a/patches/server/Rewrite-chunk-system.patch b/patches/server/Rewrite-chunk-system.patch
index be0d647ef3..1923c0e788 100644
--- a/patches/server/Rewrite-chunk-system.patch
+++ b/patches/server/Rewrite-chunk-system.patch
@@ -11566,8 +11566,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import io.papermc.paper.command.subcommands.FixLightCommand;
  import io.papermc.paper.command.subcommands.HeapDumpCommand;
 @@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
-         commands.put(Set.of("reload"), new ReloadCommand());
          commands.put(Set.of("version"), new VersionCommand());
+         commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
          commands.put(Set.of("fixlight"), new FixLightCommand());
 +        commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand());
  
diff --git a/patches/server/Starlight.patch b/patches/server/Starlight.patch
index 41e8c1ff94..5d8c63684b 100644
--- a/patches/server/Starlight.patch
+++ b/patches/server/Starlight.patch
@@ -4343,9 +4343,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  import io.papermc.paper.command.subcommands.ReloadCommand;
  import io.papermc.paper.command.subcommands.VersionCommand;
 @@ -0,0 +0,0 @@ public final class PaperCommand extends Command {
-         commands.put(Set.of("entity"), new EntityCommand());
          commands.put(Set.of("reload"), new ReloadCommand());
          commands.put(Set.of("version"), new VersionCommand());
+         commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
 +        commands.put(Set.of("fixlight"), new FixLightCommand());
  
          return commands.entrySet().stream()
diff --git a/patches/server/Timings-v2.patch b/patches/server/Timings-v2.patch
index f1aa454792..5193963dc0 100644
--- a/patches/server/Timings-v2.patch
+++ b/patches/server/Timings-v2.patch
@@ -2038,8 +2038,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public static byte toLegacyData(BlockState data) {
          return CraftLegacy.toLegacyData(data);
 @@ -0,0 +0,0 @@ public final class CraftMagicNumbers implements UnsafeValues {
-         return nmsItemStack.getItem().getDescriptionId(nmsItemStack);
      }
+     // Paper end
  
 +    // Paper start
 +    @Override
diff --git a/test-plugin/build.gradle.kts b/test-plugin/build.gradle.kts
index 6bcdb356c5..e86b933408 100644
--- a/test-plugin/build.gradle.kts
+++ b/test-plugin/build.gradle.kts
@@ -13,7 +13,7 @@ tasks.processResources {
         "apiversion" to apiVersion,
     )
     inputs.properties(props)
-    filesMatching("plugin.yml") {
+    filesMatching("paper-plugin.yml") {
         expand(props)
     }
 }
diff --git a/test-plugin/src/main/java/io/papermc/paper/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
similarity index 88%
rename from test-plugin/src/main/java/io/papermc/paper/testplugin/TestPlugin.java
rename to test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
index a0c78b9d68..4e68423bb7 100644
--- a/test-plugin/src/main/java/io/papermc/paper/testplugin/TestPlugin.java
+++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java
@@ -1,9 +1,10 @@
-package io.papermc.paper.testplugin;
+package io.papermc.testplugin;
 
 import org.bukkit.event.Listener;
 import org.bukkit.plugin.java.JavaPlugin;
 
 public final class TestPlugin extends JavaPlugin implements Listener {
+
     @Override
     public void onEnable() {
         this.getServer().getPluginManager().registerEvents(this, this);
diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java
new file mode 100644
index 0000000000..e464dac8ae
--- /dev/null
+++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginBootstrap.java
@@ -0,0 +1,13 @@
+package io.papermc.testplugin;
+
+import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+import org.jetbrains.annotations.NotNull;
+
+public class TestPluginBootstrap implements PluginBootstrap {
+
+    @Override
+    public void bootstrap(@NotNull PluginProviderContext context) {
+    }
+
+}
diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPluginLoader.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginLoader.java
new file mode 100644
index 0000000000..084899a9fe
--- /dev/null
+++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPluginLoader.java
@@ -0,0 +1,11 @@
+package io.papermc.testplugin;
+
+import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
+import io.papermc.paper.plugin.loader.PluginLoader;
+import org.jetbrains.annotations.NotNull;
+
+public class TestPluginLoader implements PluginLoader {
+    @Override
+    public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) {
+    }
+}
diff --git a/test-plugin/src/main/resources/paper-plugin.yml b/test-plugin/src/main/resources/paper-plugin.yml
new file mode 100644
index 0000000000..459345d794
--- /dev/null
+++ b/test-plugin/src/main/resources/paper-plugin.yml
@@ -0,0 +1,12 @@
+name: Paper-Test-Plugin
+version: ${version}
+main: io.papermc.testplugin.TestPlugin
+description: Paper Test Plugin
+author: PaperMC
+api-version: ${apiversion}
+load: STARTUP
+bootstrapper: io.papermc.testplugin.TestPluginBootstrap
+loader: io.papermc.testplugin.TestPluginLoader
+defaultPerm: FALSE
+permissions:
+dependencies:
diff --git a/test-plugin/src/main/resources/plugin.yml b/test-plugin/src/main/resources/plugin.yml
deleted file mode 100644
index 1e6adb9ff6..0000000000
--- a/test-plugin/src/main/resources/plugin.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-name: Paper-Test-Plugin
-version: ${version}
-main: io.papermc.paper.testplugin.TestPlugin
-description: Paper Test Plugin
-author: PaperMC
-api-version: ${apiversion}
-load: STARTUP