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 7fbb782ccf17e581a759429ffac6db608c08e3cf..1f4053ce50673585c27494a3c23f6acd119875db 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -52,7 +52,7 @@ dependencies {
     implementation("org.ow2.asm:asm-commons:9.5")
     // Paper end
 
-    compileOnly("org.apache.maven:maven-resolver-provider:3.9.6")
+    api("org.apache.maven:maven-resolver-provider:3.9.6") // Paper - make API dependency for Paper Plugins
     compileOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18")
     compileOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18")
 
@@ -138,6 +138,7 @@ tasks.withType<Javadoc> {
         "https://jd.advntr.dev/text-serializer-plain/$adventureVersion/",
         "https://jd.advntr.dev/text-logger-slf4j/$adventureVersion/",
         // Paper end
+        "https://javadoc.io/doc/org.apache.maven.resolver/maven-resolver-api/1.7.3", // Paper
     )
     options.tags("apiNote:a:API Note:")
 
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..cdbc93b317b3bab47bf6552c29cfbb2c27846933
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/PermissionManager.java
@@ -0,0 +1,171 @@
+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/BootstrapContext.java b/src/main/java/io/papermc/paper/plugin/bootstrap/BootstrapContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..08f2050356acaf74e3210416760e3873c2dafd2c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/bootstrap/BootstrapContext.java
@@ -0,0 +1,14 @@
+package io.papermc.paper.plugin.bootstrap;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin
+ * instantiation 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.Experimental
+@ApiStatus.NonExtendable
+public interface BootstrapContext extends PluginProviderContext {
+}
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..288c078da3d3ca78d02caa4e3565ac7cf89f9f9f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginBootstrap.java
@@ -0,0 +1,41 @@
+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 BootstrapContext 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..2c14693155de3654d5ca011c63e13e4a1abf2080
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContext.java
@@ -0,0 +1,52 @@
+package io.papermc.paper.plugin.bootstrap;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+
+/**
+ * Represents the context provided to a {@link PluginBootstrap} during both the bootstrapping and plugin
+ * instantiation 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
+    ComponentLogger getLogger();
+
+    /**
+     * Provides the path to the originating source of the plugin, such as the plugin's JAR file.
+     *
+     * @return the previously described path
+     */
+    @NotNull
+    Path getPluginSource();
+
+}
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..ef393f1f93ca48264fc1b6e3a27787f6a9152e1b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/configuration/PluginMeta.java
@@ -0,0 +1,203 @@
+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..ddb3076124365d0d1a5caa32d4dcb1f4314dd7ae
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/configuration/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * 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..28cbc09b7c1ded1f4515969cef4a669adac85703
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/PluginClasspathBuilder.java
@@ -0,0 +1,38 @@
+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..c9e31f78ff6ff969436c6d99755845786c4d383f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/PluginLoader.java
@@ -0,0 +1,30 @@
+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..1347b535d90c2c281c184d0459e7ac59c0350c9f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/library/ClassPathLibrary.java
@@ -0,0 +1,20 @@
+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..79ba423a364b50588f3ee87fdc69155cb8e64ad0
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryLoadingException.java
@@ -0,0 +1,15 @@
+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..0546fa1e9dcd7155086a8650806a8c086b6fc458
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/library/LibraryStore.java
@@ -0,0 +1,26 @@
+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..e3da0d67cab01e1233dccde1a12ff25961ee79fb
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/library/impl/JarLibrary.java
@@ -0,0 +1,45 @@
+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 the file does not exist 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..9af07d168beadaa77e4965819200eeb94fe3e092
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/library/impl/MavenLibraryResolver.java
@@ -0,0 +1,132 @@
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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 = LoggerFactory.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.info("Downloading {}", 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..64e46fdfa4d404cb08c67a456e5990b729296b98
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/ClassLoaderAccess.java
@@ -0,0 +1,34 @@
+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..a21bdc57564aef7caf43dde3b2bcb2fc7f30461c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/ConfiguredPluginClassLoader.java
@@ -0,0 +1,71 @@
+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;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * 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);
+
+    /**
+     * Gets the plugin held by this class loader.
+     *
+     * @return the plugin or null if it doesn't exist yet
+     */
+    @Nullable JavaPlugin getPlugin();
+
+    /**
+     * Get the plugin classloader group
+     * that is used by the underlying classloader
+     * @return classloader
+     */
+    @Nullable
+    PluginClassLoaderGroup getGroup();
+}
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..fefc87d5ffd36b848a7adb326a3a741e9edb28df
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorage.java
@@ -0,0 +1,92 @@
+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..2c0e5ba6f8eba7a632180491843071b8a8558e56
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PaperClassLoaderStorageAccess.java
@@ -0,0 +1,17 @@
+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..885151cb932d9b8c09a7887edc879e154225f416
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/classloader/PluginClassLoaderGroup.java
@@ -0,0 +1,65 @@
+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..44d630c3eb2670c36134b9907519dc986b3761b4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/entrypoint/DependencyContext.java
@@ -0,0 +1,48 @@
+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/ProviderUtil.java b/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bf3d212a6156ad9ab0e82d1ca0a04f83f6e4b83
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/util/ProviderUtil.java
@@ -0,0 +1,78 @@
+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 a3ee6f2f588bc9c87e49378359f450820b9af1b9..0743592273cc1c5f4ab6935349941bf25b438448 100644
--- a/src/main/java/org/bukkit/UnsafeValues.java
+++ b/src/main/java/org/bukkit/UnsafeValues.java
@@ -109,4 +109,14 @@ public interface UnsafeValues {
      * @return an internal potion data
      */
     PotionType.InternalPotionData getInternalPotionData(NamespacedKey key);
+
+    // 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 1dbbc244309043b18c1d71707c4fb066c0d0e02d..551c5af6a7bfa2268cbc63be8e70d129bccaa912 100644
--- a/src/main/java/org/bukkit/command/PluginCommand.java
+++ b/src/main/java/org/bukkit/command/PluginCommand.java
@@ -14,7 +14,7 @@ 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 e195e74c48c69047aa825b75fad95419c505b41f..53f28c9e6843991486a576d41b6641c170589807 100644
--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java
+++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java
@@ -34,7 +34,7 @@ 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/command/defaults/PluginsCommand.java b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
index bcb576a4271b1ec7b1cfe6f83cf161b7d89ed2e5..4d849e1283fdf6b0872a0f2183964cc93748c116 100644
--- a/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
+++ b/src/main/java/org/bukkit/command/defaults/PluginsCommand.java
@@ -9,6 +9,7 @@ import org.bukkit.command.CommandSender;
 import org.bukkit.plugin.Plugin;
 import org.jetbrains.annotations.NotNull;
 
+@Deprecated(forRemoval = true) // Paper
 public class PluginsCommand extends BukkitCommand {
     public PluginsCommand(@NotNull String name) {
         super(name);
diff --git a/src/main/java/org/bukkit/plugin/Plugin.java b/src/main/java/org/bukkit/plugin/Plugin.java
index b37938745f916b5f0111b07b1a1c97527f026e9d..8c76716249e44ed8bf6be94c1f5c7b6d9bb35be2 100644
--- a/src/main/java/org/bukkit/plugin/Plugin.java
+++ b/src/main/java/org/bukkit/plugin/Plugin.java
@@ -30,10 +30,21 @@ 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"
@@ -94,6 +105,7 @@ 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 94f8ceb965cecb5669a84a0ec61c0f706c2a2673..e773db6da357ad210eb24d4c389af2dc84ce450a 100644
--- a/src/main/java/org/bukkit/plugin/PluginBase.java
+++ b/src/main/java/org/bukkit/plugin/PluginBase.java
@@ -31,6 +31,6 @@ 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 f48bdeb628a82416d93f84a0a10141447482bf83..c0691a849831f99268fdcb7b0f471f80a1a2a70e 100644
--- a/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
+++ b/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java
@@ -198,7 +198,7 @@ import org.yaml.snakeyaml.representer.Representer;
  *      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
@@ -259,6 +259,70 @@ 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 a88733f1cd1ddb5d85ab1b0e6af4fd5b80bbc1c6..cb530369e667c426c842da356c31304bb5c3ecfa 100644
--- a/src/main/java/org/bukkit/plugin/PluginLoader.java
+++ b/src/main/java/org/bukkit/plugin/PluginLoader.java
@@ -12,6 +12,7 @@ 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 41e26451fe12d8e6e0ef73c85731b24b4e3f200c..03213fde8315384ec56c16031cfc606ade2e8091 100644
--- a/src/main/java/org/bukkit/plugin/PluginManager.java
+++ b/src/main/java/org/bukkit/plugin/PluginManager.java
@@ -14,7 +14,7 @@ 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
@@ -23,6 +23,7 @@ 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;
 
     /**
@@ -303,4 +304,17 @@ 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 a69c5d5cad6168aeaae41e8adc319dc8c976b1e2..77f9ebbe675cf1b6a17e98d98e7666711998eb4e 100644
--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java
+++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java
@@ -43,6 +43,8 @@ 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>();
@@ -51,10 +53,13 @@ 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) {
@@ -111,6 +116,11 @@ 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");
 
@@ -378,6 +388,15 @@ 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);
 
@@ -428,12 +447,14 @@ 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()]);
     }
 
@@ -447,6 +468,7 @@ 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);
@@ -460,6 +482,7 @@ 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 {
@@ -469,6 +492,7 @@ 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);
 
@@ -488,6 +512,7 @@ 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]);
@@ -496,6 +521,7 @@ 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);
@@ -540,6 +566,7 @@ public final class SimplePluginManager implements PluginManager {
 
     @Override
     public void clearPlugins() {
+        if (true) {this.paperPluginManager.clearPlugins(); return;} // Paper
         synchronized (this) {
             disablePlugins();
             plugins.clear();
@@ -560,6 +587,7 @@ 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.");
@@ -608,6 +636,7 @@ 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");
         }
@@ -641,6 +670,7 @@ 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, ignoreCancelled); return;} // Paper
 
         if (!plugin.isEnabled()) {
             throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
@@ -688,16 +718,19 @@ 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)) {
@@ -711,21 +744,25 @@ 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);
@@ -765,6 +802,7 @@ 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);
 
@@ -778,6 +816,7 @@ 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);
 
@@ -793,6 +832,7 @@ 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);
 
@@ -805,6 +845,7 @@ 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) {
@@ -817,6 +858,7 @@ 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) {
@@ -831,6 +873,7 @@ 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) {
@@ -843,6 +886,7 @@ 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());
     }
 
@@ -866,6 +910,7 @@ public final class SimplePluginManager implements PluginManager {
 
     @Override
     public boolean useTimings() {
+        if (true) {return this.paperPluginManager.useTimings();} // Paper
         return useTimings;
     }
 
@@ -877,4 +922,28 @@ 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 a80251eff75430863b37db1c131e22593f3fcd5e..310c4041963a3f1e0a26e39a6da12a9bfdb51edc 100644
--- a/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
+++ b/src/main/java/org/bukkit/plugin/UnknownDependencyException.java
@@ -43,4 +43,16 @@ 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 ee100b7ad89ce1eccef0c3bc55885cd78aadd1ec..f594913e6b94f77b26a4a758c447a42d8a25b6ff 100644
--- a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java
+++ b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java
@@ -40,6 +40,7 @@ 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;
@@ -48,13 +49,16 @@ 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) {
@@ -79,9 +83,12 @@ 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;
     }
@@ -122,13 +129,20 @@ 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;
     }
 
+    @NotNull
+    public final io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
+        return this.pluginMeta;
+    }
+
     @NotNull
     @Override
     public FileConfiguration getConfig() {
@@ -258,7 +272,8 @@ 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;
 
@@ -270,9 +285,18 @@ 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
+    private static class DummyPluginLoaderImplHolder {
+        private static final PluginLoader INSTANCE =  net.kyori.adventure.util.Services.service(PluginLoader.class)
+            .orElseThrow();
+    }
+    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 = DummyPluginLoaderImplHolder.INSTANCE; // Paper
         this.server = server;
         this.file = file;
         this.description = description;
@@ -280,6 +304,7 @@ 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
     }
 
     /**
@@ -396,10 +421,10 @@ public abstract class JavaPlugin extends PluginBase {
             throw new IllegalArgumentException(clazz + " does not extend " + JavaPlugin.class);
         }
         final ClassLoader cl = clazz.getClassLoader();
-        if (!(cl instanceof PluginClassLoader)) {
-            throw new IllegalArgumentException(clazz + " is not initialized by " + PluginClassLoader.class);
+        if (!(cl instanceof io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader configuredPluginClassLoader)) { // Paper
+            throw new IllegalArgumentException(clazz + " is not initialized by a " + io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader.class); // Paper
         }
-        JavaPlugin plugin = ((PluginClassLoader) cl).plugin;
+        JavaPlugin plugin = configuredPluginClassLoader.getPlugin(); // Paper
         if (plugin == null) {
             throw new IllegalStateException("Cannot get plugin for " + clazz + " from a static initializer");
         }
@@ -422,10 +447,10 @@ public abstract class JavaPlugin extends PluginBase {
     public static JavaPlugin getProvidingPlugin(@NotNull Class<?> clazz) {
         Preconditions.checkArgument(clazz != null, "Null class cannot have a plugin");
         final ClassLoader cl = clazz.getClassLoader();
-        if (!(cl instanceof PluginClassLoader)) {
-            throw new IllegalArgumentException(clazz + " is not provided by " + PluginClassLoader.class);
+        if (!(cl instanceof io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader configuredPluginClassLoader)) { // Paper
+            throw new IllegalArgumentException(clazz + " is not provided by a " + io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader.class); // Paper
         }
-        JavaPlugin plugin = ((PluginClassLoader) cl).plugin;
+        JavaPlugin plugin = configuredPluginClassLoader.getPlugin(); // Paper
         if (plugin == null) {
             throw new IllegalStateException("Cannot get plugin for " + clazz + " from a static initializer");
         }
diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
index 047c0304fd617cec990f80815b43916c6ef5a94c..ab04ffe4cd05315a2ee0f64c553b4c674740eb7f 100644
--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
@@ -49,6 +49,7 @@ 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$")};
@@ -79,6 +80,7 @@ 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()) {
@@ -142,7 +144,7 @@ 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, 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 6d634b0ea813ccb19f1562a7d0e5a59cea4eab21..e4b6f278a811acbb0070e311c5c3bdaff7b00474 100644
--- a/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/LibraryLoader.java
@@ -36,7 +36,10 @@ 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;
@@ -79,7 +82,7 @@ class LibraryLoader
         }
         logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[]
         {
-            desc.getName(), desc.getLibraries().size()
+            java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), desc.getLibraries().size() // Paper - use configured log prefix
         } );
 
         List<Dependency> dependencies = new ArrayList<>();
@@ -117,7 +120,7 @@ class LibraryLoader
             jarFiles.add( url );
             logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[]
             {
-                desc.getName(), file
+                java.util.Objects.requireNonNullElseGet(desc.getPrefix(), desc::getName), file // Paper - use configured log prefix
             } );
         }
 
diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
index 64a294aeb6fb548794708b38c3707f9dd882b2ff..74b6581a97a092c44a0876e7981ff8a8e6153100 100644
--- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java
@@ -31,7 +31,8 @@ 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;
@@ -45,24 +46,32 @@ final class PluginClassLoader extends URLClassLoader {
     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, JarFile jarFile, io.papermc.paper.plugin.provider.entrypoint.DependencyContext dependencyContext) throws IOException, InvalidPluginException, MalformedURLException { // Paper // Paper - use JarFile provided by SpigotPluginProvider
         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;
-        this.jar = new JarFile(file);
+        this.jar = jarFile; // Paper - use JarFile provided by SpigotPluginProvider
         this.manifest = jar.getManifest();
         this.url = file.toURI().toURL();
         this.libraryLoader = libraryLoader;
 
+        // Paper start
+        this.dependencyContext = dependencyContext;
+        this.classLoaderGroup = io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage.instance().registerSpigotGroup(this);
+        // Paper end
+
         Class<?> jarClass;
         try {
             jarClass = Class.forName(description.getMain(), true, this);
@@ -107,6 +116,27 @@ 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);
+    }
+
+    @Override
+    public JavaPlugin getPlugin() {
+        return this.plugin;
+    }
+    // Paper end
+
     @Override
     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
         return loadClass0(name, resolve, true, true);
@@ -132,26 +162,11 @@ 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.
-                if (result.getClassLoader() instanceof PluginClassLoader) {
-                    PluginDescriptionFile provider = ((PluginClassLoader) result.getClassLoader()).description;
-
-                    if (provider != description
-                            && !seenIllegalAccess.contains(provider.getName())
-                            && !((SimplePluginManager) loader.server.getPluginManager()).isTransitiveDepend(description, provider)) {
-
-                        seenIllegalAccess.add(provider.getName());
-                        if (plugin != null) {
-                            plugin.getLogger().log(Level.WARNING, "Loaded class {0} from {1} which is not a depend or softdepend of this plugin.", new Object[]{name, provider.getFullName()});
-                        } else {
-                            // 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()});
-                        }
-                    }
-                }
+                // Paper - Totally delete the illegal access logic, we are never going to enforce it anyways here.
 
                 return result;
             }
@@ -180,7 +195,7 @@ 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) {
@@ -210,8 +225,8 @@ final class PluginClassLoader extends URLClassLoader {
                 result = super.findClass(name);
             }
 
-            loader.setClass(name, result);
             classes.put(name, result);
+            this.setClass(name, result); // Paper
         }
 
         return result;
@@ -220,6 +235,12 @@ 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();
@@ -231,7 +252,7 @@ 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) {
@@ -241,6 +262,38 @@ 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);
+        }
     }
+
+    @Override
+    public @Nullable io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup getGroup() {
+        return this.classLoaderGroup;
+    }
+
+    // Paper end
 }
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 40a086f2883c4419d2bf0bd44285f7c55562ba3e..0000000000000000000000000000000000000000
--- a/src/test/java/org/bukkit/event/SyntheticEventTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.bukkit.event;
-
-import static org.junit.jupiter.api.Assertions.*;
-import org.bukkit.Bukkit;
-import org.bukkit.plugin.PluginLoader;
-import org.bukkit.plugin.SimplePluginManager;
-import org.bukkit.plugin.TestPlugin;
-import org.bukkit.plugin.java.JavaPluginLoader;
-import org.bukkit.support.AbstractTestingBase;
-import org.junit.jupiter.api.Test;
-
-public class SyntheticEventTest extends AbstractTestingBase {
-    @SuppressWarnings("deprecation")
-    @Test
-    public void test() {
-        final JavaPluginLoader loader = new JavaPluginLoader(Bukkit.getServer());
-        TestPlugin plugin = new TestPlugin(getClass().getName()) {
-            @Override
-            public PluginLoader getPluginLoader() {
-                return loader;
-            }
-        };
-        SimplePluginManager pluginManager = new SimplePluginManager(Bukkit.getServer(), null);
-
-        TestEvent event = new TestEvent(false);
-        Impl impl = new Impl();
-
-        pluginManager.registerEvents(impl, plugin);
-        pluginManager.callEvent(event);
-
-        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 03b08e47e91e8b56c1992fcd749a62eb9e7d4d68..0000000000000000000000000000000000000000
--- a/src/test/java/org/bukkit/plugin/PluginManagerTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-package org.bukkit.plugin;
-
-import static org.bukkit.support.MatcherAssert.*;
-import static org.hamcrest.Matchers.*;
-import org.bukkit.Bukkit;
-import org.bukkit.event.Event;
-import org.bukkit.event.TestEvent;
-import org.bukkit.permissions.Permission;
-import org.bukkit.support.AbstractTestingBase;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Test;
-
-public class PluginManagerTest extends AbstractTestingBase {
-    private class MutableObject {
-        volatile Object value = null;
-    }
-
-    private static final PluginManager pm = Bukkit.getServer().getPluginManager();
-
-    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(pm.getPermission(name), is(perm), "Permission \"" + name + "\" was not added");
-        pm.removePermission(name);
-        assertThat(pm.getPermission(name), is(nullValue()), "Permission \"" + name + "\" was not removed");
-    }
-
-    private void testRemovePermissionByPermission(final String name) {
-        final Permission perm = new Permission(name);
-        pm.addPermission(perm);
-        assertThat(pm.getPermission(name), is(perm), "Permission \"" + name + "\" was not added");
-        pm.removePermission(perm);
-        assertThat(pm.getPermission(name), is(nullValue()), "Permission \"" + name + "\" was not removed");
-    }
-
-    @AfterEach
-    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 a8be3e23e3e280ad301d9530de50028515612966..43b58e920e739bb949ac0673e9ef73ba7b500dc9 100644
--- a/src/test/java/org/bukkit/plugin/TestPlugin.java
+++ b/src/test/java/org/bukkit/plugin/TestPlugin.java
@@ -32,6 +32,12 @@ 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/src/test/java/org/bukkit/support/TestServer.java b/src/test/java/org/bukkit/support/TestServer.java
index 73ec679ac0d1f398b417bd174b47f9af93351e27..b208150297a23c0b4acb79135416809718f5650e 100644
--- a/src/test/java/org/bukkit/support/TestServer.java
+++ b/src/test/java/org/bukkit/support/TestServer.java
@@ -25,8 +25,7 @@ public final class TestServer {
         Thread creatingThread = Thread.currentThread();
         when(instance.isPrimaryThread()).then(mock -> Thread.currentThread().equals(creatingThread));
 
-        PluginManager pluginManager = new SimplePluginManager(instance, new SimpleCommandMap(instance));
-        when(instance.getPluginManager()).thenReturn(pluginManager);
+        // Paper - remove plugin manager for Paper Plugins
 
         Logger logger = Logger.getLogger(TestServer.class.getCanonicalName());
         when(instance.getLogger()).thenReturn(logger);