From 2da096645213fa8115c2477536b7a1f955e842a4 Mon Sep 17 00:00:00 2001
From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
Date: Sat, 22 Apr 2023 13:44:40 -0400
Subject: [PATCH] Resolve Plugin Dependency Issues, Improve PluginLoading
 Compat, Small Loading Issues (#9129)

---
 patches/server/Paper-Plugins.patch | 952 ++++++++++++++++++-----------
 1 file changed, 609 insertions(+), 343 deletions(-)

diff --git a/patches/server/Paper-Plugins.patch b/patches/server/Paper-Plugins.patch
index dd0d6902df..6bf32f50ea 100644
--- a/patches/server/Paper-Plugins.patch
+++ b/patches/server/Paper-Plugins.patch
@@ -256,6 +256,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.command.subcommands;
 +
++import com.google.common.graph.GraphBuilder;
 +import com.google.gson.JsonArray;
 +import com.google.gson.JsonElement;
 +import com.google.gson.JsonObject;
@@ -270,8 +271,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.plugin.entrypoint.classloader.group.SimpleListPluginClassLoaderGroup;
 +import io.papermc.paper.plugin.entrypoint.classloader.group.SpigotPluginClassLoaderGroup;
 +import io.papermc.paper.plugin.entrypoint.classloader.group.StaticPluginClassLoaderGroup;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
-+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
 +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
 +import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
 +import io.papermc.paper.plugin.provider.PluginProvider;
@@ -394,7 +397,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    return false;
 +                }
 +            });
-+            modernPluginLoadingStrategy.loadProviders(pluginProviders);
++            modernPluginLoadingStrategy.loadProviders(pluginProviders, new MetaDependencyTree(GraphBuilder.directed().build()));
 +
 +            rootProviders.add(entry.getKey().getDebugName(), entrypoint);
 +        }
@@ -1677,72 +1680,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.plugin.entrypoint.dependency;
 +
-+import com.google.common.graph.MutableGraph;
 +import io.papermc.paper.plugin.configuration.PluginMeta;
-+import io.papermc.paper.plugin.provider.PluginProvider;
-+import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
-+import org.bukkit.plugin.PluginDescriptionFile;
-+import org.jetbrains.annotations.NotNull;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
 +
 +import java.util.ArrayList;
 +import java.util.List;
-+import java.util.Map;
-+import java.util.function.Predicate;
 +
 +@SuppressWarnings("UnstableApiUsage")
 +public class DependencyUtil {
 +
-+    @NotNull
-+    public static MutableGraph<String> buildDependencyGraph(@NotNull MutableGraph<String> dependencyGraph, @NotNull PluginMeta configuration) {
-+        List<String> dependencies = new ArrayList<>();
-+        dependencies.addAll(configuration.getPluginDependencies());
-+        dependencies.addAll(configuration.getPluginSoftDependencies());
-+
-+        return buildDependencyGraph(dependencyGraph, configuration.getName(), dependencies);
-+    }
-+
-+    @NotNull
-+    public static MutableGraph<String> buildDependencyGraph(@NotNull MutableGraph<String> dependencyGraph, String identifier, @NotNull Iterable<String> depends) {
-+        for (String dependency : depends) {
-+            dependencyGraph.putEdge(identifier, dependency);
-+        }
-+
-+        dependencyGraph.addNode(identifier); // Make sure dependencies at least have a node
-+        return dependencyGraph;
-+    }
-+
-+    @NotNull
-+    public static MutableGraph<String> buildLoadGraph(@NotNull MutableGraph<String> dependencyGraph, @NotNull LoadOrderConfiguration configuration, Predicate<String> validator) {
-+        String identifier = configuration.getMeta().getName();
-+        for (String dependency : configuration.getLoadAfter()) {
-+            if (validator.test(dependency)) {
-+                dependencyGraph.putEdge(identifier, dependency);
-+            }
-+        }
-+
-+        for (String loadBeforeTarget : configuration.getLoadBefore()) {
-+            if (validator.test(loadBeforeTarget)) {
-+                dependencyGraph.putEdge(loadBeforeTarget, identifier);
-+            }
-+        }
-+
-+        dependencyGraph.addNode(identifier); // Make sure dependencies at least have a node
-+        return dependencyGraph;
-+    }
-+
-+    // This adds a provided plugin to another plugin, basically making it seem like a "dependency"
-+    // in order to have plugins that need the provided plugin to load after the specified plugin name
-+    @NotNull
-+    public static MutableGraph<String> addProvidedPlugin(@NotNull MutableGraph<String> dependencyGraph, @NotNull String pluginName, @NotNull String providedName) {
-+        dependencyGraph.putEdge(pluginName, providedName);
-+
-+        return dependencyGraph;
-+    }
-+
-+    public static List<String> validateSimple(PluginMeta meta, Map<String, PluginProvider<?>> toLoad) {
++    public static List<String> validateSimple(PluginMeta meta, DependencyContext dependencyContext) {
 +        List<String> missingDependencies = new ArrayList<>();
 +        for (String hardDependency : meta.getPluginDependencies()) {
-+            if (!toLoad.containsKey(hardDependency)) {
++            if (!dependencyContext.hasDependency(hardDependency)) {
 +                missingDependencies.add(hardDependency);
 +            }
 +        }
@@ -1760,6 +1710,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +import com.google.common.graph.Graph;
 +import com.google.common.graph.Graphs;
++import com.google.common.graph.MutableGraph;
 +import io.papermc.paper.plugin.configuration.PluginMeta;
 +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
 +
@@ -1768,9 +1719,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +@SuppressWarnings("UnstableApiUsage")
 +public class GraphDependencyContext implements DependencyContext {
 +
-+    private final Graph<String> dependencyGraph;
++    private final MutableGraph<String> dependencyGraph;
 +
-+    public GraphDependencyContext(Graph<String> dependencyGraph) {
++    public GraphDependencyContext(MutableGraph<String> dependencyGraph) {
 +        this.dependencyGraph = dependencyGraph;
 +    }
 +
@@ -1798,6 +1749,134 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.dependencyGraph.nodes().contains(pluginIdentifier);
 +    }
 +
++    public MutableGraph<String> getDependencyGraph() {
++        return dependencyGraph;
++    }
++
++    @Override
++    public String toString() {
++        return "GraphDependencyContext{" +
++            "dependencyGraph=" + this.dependencyGraph +
++            '}';
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/MetaDependencyTree.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.dependency;
++
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.Graphs;
++import com.google.common.graph.MutableGraph;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
++import org.jetbrains.annotations.NotNull;
++
++import java.util.HashSet;
++import java.util.Set;
++
++public class MetaDependencyTree implements DependencyContext {
++
++    private final MutableGraph<String> graph;
++
++    // We need to upkeep a separate collection since when populating
++    // a graph it adds nodes even if they are not present
++    private final Set<String> dependencies = new HashSet<>();
++
++    public MetaDependencyTree() {
++        this(GraphBuilder.directed().build());
++    }
++
++    public MetaDependencyTree(MutableGraph<String> graph) {
++        this.graph = graph;
++    }
++
++    public void add(PluginProvider<?> provider) {
++        add(provider.getMeta());
++    }
++
++    public void remove(PluginProvider<?> provider) {
++        remove(provider.getMeta());
++    }
++
++    public void add(PluginMeta configuration) {
++        String identifier = configuration.getName();
++        // Build a validated provider's dependencies into the graph
++        for (String dependency : configuration.getPluginDependencies()) {
++            this.graph.putEdge(identifier, dependency);
++        }
++        for (String dependency : configuration.getPluginSoftDependencies()) {
++            this.graph.putEdge(identifier, dependency);
++        }
++
++        this.graph.addNode(identifier); // Make sure dependencies at least have a node
++
++        // Add the provided plugins to the graph as well
++        for (String provides : configuration.getProvidedPlugins()) {
++            this.graph.putEdge(identifier, provides);
++            this.dependencies.add(provides);
++        }
++        this.dependencies.add(identifier);
++    }
++
++    public void remove(PluginMeta configuration) {
++        String identifier = configuration.getName();
++        // Remove a validated provider's dependencies into the graph
++        for (String dependency : configuration.getPluginDependencies()) {
++            this.graph.removeEdge(identifier, dependency);
++        }
++        for (String dependency : configuration.getPluginSoftDependencies()) {
++            this.graph.removeEdge(identifier, dependency);
++        }
++
++        this.graph.removeNode(identifier); // Remove root node
++
++        // Remove the provided plugins to the graph as well
++        for (String provides : configuration.getProvidedPlugins()) {
++            this.graph.removeEdge(identifier, provides);
++            this.dependencies.remove(provides);
++        }
++        this.dependencies.remove(identifier);
++    }
++
++    @Override
++    public boolean isTransitiveDependency(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
++        String pluginIdentifier = plugin.getName();
++
++        if (this.graph.nodes().contains(pluginIdentifier)) {
++            Set<String> reachableNodes = Graphs.reachableNodes(this.graph, pluginIdentifier);
++            if (reachableNodes.contains(depend.getName())) {
++                return true;
++            }
++            for (String provided : depend.getProvidedPlugins()) {
++                if (reachableNodes.contains(provided)) {
++                    return true;
++                }
++            }
++        }
++
++        return false;
++    }
++
++    @Override
++    public boolean hasDependency(@NotNull String pluginIdentifier) {
++        return this.dependencies.contains(pluginIdentifier);
++    }
++
++    @Override
++    public String toString() {
++        return "ProviderDependencyTree{" +
++            "graph=" + this.graph +
++            '}';
++    }
++
++    public MutableGraph<String> getGraph() {
++        return graph;
++    }
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
 new file mode 100644
@@ -2171,6 +2250,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.google.common.graph.MutableGraph;
 +import io.papermc.paper.plugin.configuration.PluginMeta;
 +import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
 +import org.bukkit.plugin.UnknownDependencyException;
@@ -2198,10 +2278,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    @Override
-+    public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> providers) {
++    public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> providers, MetaDependencyTree dependencyTree) {
 +        List<ProviderPair<T>> javapluginsLoaded = new ArrayList<>();
-+        MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
-+        GraphDependencyContext dependencyContext = new GraphDependencyContext(dependencyGraph);
++        MutableGraph<String> dependencyGraph = dependencyTree.getGraph();
 +
 +        Map<String, PluginProvider<T>> providersToLoad = new HashMap<>();
 +        Set<String> loadedPlugins = new HashSet<>();
@@ -2363,7 +2442,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    missingDependency = false;
 +
 +                    try {
-+                        this.configuration.applyContext(file, dependencyContext);
++                        this.configuration.applyContext(file, dependencyTree);
 +                        T loadedPlugin = file.createInstance();
 +                        this.warnIfPaperPlugin(file);
 +
@@ -2395,7 +2474,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                        providerIterator.remove();
 +
 +                        try {
-+                            this.configuration.applyContext(file, dependencyContext);
++                            this.configuration.applyContext(file, dependencyTree);
 +                            T loadedPlugin = file.createInstance();
 +                            this.warnIfPaperPlugin(file);
 +
@@ -2434,223 +2513,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +}
-diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java
-new file mode 100644
-index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
---- /dev/null
-+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java
-@@ -0,0 +0,0 @@
-+package io.papermc.paper.plugin.entrypoint.strategy;
-+
-+import com.google.common.collect.Lists;
-+import com.google.common.collect.Maps;
-+import com.google.common.graph.GraphBuilder;
-+import com.google.common.graph.MutableGraph;
-+import com.mojang.logging.LogUtils;
-+import io.papermc.paper.plugin.configuration.PluginMeta;
-+import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
-+import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
-+import io.papermc.paper.plugin.provider.PluginProvider;
-+import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
-+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
-+import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
-+import java.util.HashSet;
-+import java.util.Set;
-+import org.bukkit.plugin.UnknownDependencyException;
-+import org.slf4j.Logger;
-+
-+import java.util.ArrayList;
-+import java.util.HashMap;
-+import java.util.List;
-+import java.util.Map;
-+
-+@SuppressWarnings("UnstableApiUsage")
-+public class ModernPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
-+
-+    private static final Logger LOGGER = LogUtils.getClassLogger();
-+    private final ProviderConfiguration<T> configuration;
-+
-+    public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
-+        this.configuration = onLoad;
-+    }
-+
-+    @Override
-+    public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> pluginProviders) {
-+        Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>();
-+        Map<String, PluginProvider<?>> providerMapMirror = Maps.transformValues(providerMap, (entry) -> entry.provider);
-+        List<PluginProvider<T>> validatedProviders = new ArrayList<>();
-+
-+        // Populate provider map
-+        for (PluginProvider<T> provider : pluginProviders) {
-+            PluginMeta providerConfig = provider.getMeta();
-+            PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider);
-+
-+            PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry);
-+            if (replacedProvider != null) {
-+                LOGGER.error(String.format(
-+                    "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'",
-+                    providerConfig.getName(),
-+                    provider.getSource(),
-+                    replacedProvider.provider.getSource(),
-+                    replacedProvider.provider.getParentSource()
-+                ));
-+            }
-+
-+            for (String extra : providerConfig.getProvidedPlugins()) {
-+                PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry);
-+                if (replacedExtraProvider != null) {
-+                    LOGGER.warn(String.format(
-+                        "`%s' is provided by both `%s' and `%s'",
-+                        extra,
-+                        providerConfig.getName(),
-+                        replacedExtraProvider.provider.getMeta().getName()
-+                    ));
-+                }
-+            }
-+        }
-+
-+        // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid
-+        for (PluginProvider<T> provider : pluginProviders) {
-+            PluginMeta configuration = provider.getMeta();
-+
-+            // Populate missing dependencies to capture if there are multiple missing ones.
-+            List<String> missingDependencies = provider.validateDependencies(providerMapMirror);
-+
-+            if (missingDependencies.isEmpty()) {
-+                validatedProviders.add(provider);
-+            } else {
-+                LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
-+                // Because the validator is invalid, remove it from the provider map
-+                providerMap.remove(configuration.getName());
-+            }
-+        }
-+
-+        MutableGraph<String> loadOrderGraph = GraphBuilder.directed().build();
-+        MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
-+        for (PluginProvider<?> validated : validatedProviders) {
-+            PluginMeta configuration = validated.getMeta();
-+            LoadOrderConfiguration loadOrderConfiguration = validated.createConfiguration(providerMapMirror);
-+
-+            // Build a validated provider's load order changes
-+            DependencyUtil.buildLoadGraph(loadOrderGraph, loadOrderConfiguration, providerMap::containsKey);
-+
-+            // Build a validated provider's dependencies into the graph
-+            DependencyUtil.buildDependencyGraph(dependencyGraph, configuration);
-+
-+            // Add the provided plugins to the graph as well
-+            for (String provides : configuration.getProvidedPlugins()) {
-+                String name = configuration.getName();
-+                DependencyUtil.addProvidedPlugin(loadOrderGraph, name, provides);
-+                DependencyUtil.addProvidedPlugin(dependencyGraph, name, provides);
-+            }
-+        }
-+
-+        // Reverse the topographic search to let us see which providers we can load first.
-+        List<String> reversedTopographicSort;
-+        try {
-+            reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(loadOrderGraph));
-+        } catch (TopographicGraphSorter.GraphCycleException exception) {
-+            List<List<String>> cycles = new JohnsonSimpleCycles<>(loadOrderGraph).findAndRemoveSimpleCycles();
-+
-+            // Only log an error if at least non-Spigot plugin is present in the cycle
-+            // Due to Spigot plugin metadata making no distinction between load order and dependencies (= class loader access), cycles are an unfortunate reality we have to deal with
-+            Set<String> cyclingPlugins = new HashSet<>();
-+            cycles.forEach(cyclingPlugins::addAll);
-+            if (cyclingPlugins.stream().anyMatch(plugin -> {
-+                PluginProvider<?> pluginProvider = providerMapMirror.get(plugin);
-+                return pluginProvider != null && !(pluginProvider instanceof SpigotPluginProvider);
-+            })) {
-+                logCycleError(cycles, providerMapMirror);
-+            }
-+
-+            // Try again after hopefully having removed all cycles
-+            try {
-+                reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(loadOrderGraph));
-+            } catch (TopographicGraphSorter.GraphCycleException e) {
-+                throw new PluginGraphCycleException(cycles);
-+            }
-+        }
-+
-+        GraphDependencyContext graphDependencyContext = new GraphDependencyContext(dependencyGraph);
-+        List<ProviderPair<T>> loadedPlugins = new ArrayList<>();
-+        for (String providerIdentifier : reversedTopographicSort) {
-+            // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist.
-+            // The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just
-+            // nicer to do it here TBH.
-+            PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier);
-+            if (retrievedProviderEntry == null || retrievedProviderEntry.provided) {
-+                // OR if this was already provided (most likely from a plugin that already "provides" that dependency)
-+                // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways
-+                continue; // Skip provider that doesn't exist....
-+            }
-+            retrievedProviderEntry.provided = true;
-+            PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider;
-+            try {
-+                this.configuration.applyContext(retrievedProvider, graphDependencyContext);
-+
-+                if (this.configuration.preloadProvider(retrievedProvider)) {
-+                    T instance = retrievedProvider.createInstance();
-+                    if (this.configuration.load(retrievedProvider, instance)) {
-+                        loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance));
-+                    }
-+                }
-+            } catch (Throwable ex) {
-+                LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
-+            }
-+        }
-+
-+        return loadedPlugins;
-+    }
-+
-+    private void logCycleError(List<List<String>> cycles, Map<String, PluginProvider<?>> providerMapMirror) {
-+        LOGGER.error("=================================");
-+        LOGGER.error("Circular plugin loading detected:");
-+        for (int i = 0; i < cycles.size(); i++) {
-+            List<String> cycle = cycles.get(i);
-+            LOGGER.error("{}) {} -> {}", i + 1, String.join(" -> ", cycle), cycle.get(0));
-+            for (String pluginName : cycle) {
-+                PluginProvider<?> pluginProvider = providerMapMirror.get(pluginName);
-+                if (pluginProvider == null) {
-+                    return;
-+                }
-+
-+                logPluginInfo(pluginProvider.getMeta());
-+            }
-+        }
-+
-+        LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help.");
-+        LOGGER.error("=================================");
-+    }
-+
-+    private void logPluginInfo(PluginMeta meta) {
-+        if (!meta.getLoadBeforePlugins().isEmpty()) {
-+            LOGGER.error("   {} loadbefore: {}", meta.getName(), meta.getLoadBeforePlugins());
-+        }
-+
-+        if (meta instanceof PaperPluginMeta paperPluginMeta) {
-+            if (!paperPluginMeta.getLoadAfterPlugins().isEmpty()) {
-+                LOGGER.error("   {} loadafter: {}", meta.getName(), paperPluginMeta.getLoadAfterPlugins());
-+            }
-+        } else {
-+            List<String> dependencies = new ArrayList<>();
-+            dependencies.addAll(meta.getPluginDependencies());
-+            dependencies.addAll(meta.getPluginSoftDependencies());
-+            if (!dependencies.isEmpty()) {
-+                LOGGER.error("   {} depend/softdepend: {}", meta.getName(), dependencies);
-+            }
-+        }
-+    }
-+
-+    private static class PluginProviderEntry<T> {
-+
-+        private final PluginProvider<T> provider;
-+        private boolean provided;
-+
-+        private PluginProviderEntry(PluginProvider<T> provider) {
-+            this.provider = provider;
-+        }
-+    }
-+}
 diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -2702,6 +2564,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return true;
 +    }
 +
++    default void onGenericError(PluginProvider<T> provider) {
++
++    }
++
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
 new file mode 100644
@@ -2711,6 +2577,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.plugin.entrypoint.strategy;
 +
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +
 +import java.util.List;
@@ -2719,11 +2586,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 + * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
 + * <p>
 + * Returns providers loaded.
++ *
 + * @param <P> provider type
 + */
 +public interface ProviderLoadingStrategy<P> {
 +
-+    List<ProviderPair<P>> loadProviders(List<PluginProvider<P>> providers);
++    List<ProviderPair<P>> loadProviders(List<PluginProvider<P>> providers, MetaDependencyTree dependencyTree);
 +
 +    record ProviderPair<P>(PluginProvider<P> provider, P provided) {
 +
@@ -2793,6 +2661,278 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +    }
 +}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy.modern;
++
++import com.google.common.collect.Lists;
++import com.google.common.graph.MutableGraph;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
++import io.papermc.paper.plugin.entrypoint.strategy.JohnsonSimpleCycles;
++import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.TopographicGraphSorter;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
++import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++
++class LoadOrderTree {
++
++    private static final Logger LOGGER = LogUtils.getClassLogger();
++
++    private final Map<String, PluginProvider<?>> providerMap;
++    private final MutableGraph<String> graph;
++
++    public LoadOrderTree(Map<String, PluginProvider<?>> providerMapMirror, MutableGraph<String> graph) {
++        this.providerMap = providerMapMirror;
++        this.graph = graph;
++    }
++
++    public void add(PluginProvider<?> provider) {
++        LoadOrderConfiguration configuration = provider.createConfiguration(this.providerMap);
++
++        // Build a validated provider's load order changes
++        String identifier = configuration.getMeta().getName();
++        for (String dependency : configuration.getLoadAfter()) {
++            if (this.providerMap.containsKey(dependency)) {
++                this.graph.putEdge(identifier, dependency);
++            }
++        }
++
++        for (String loadBeforeTarget : configuration.getLoadBefore()) {
++            if (this.providerMap.containsKey(loadBeforeTarget)) {
++                this.graph.putEdge(loadBeforeTarget, identifier);
++            }
++        }
++
++        this.graph.addNode(identifier); // Make sure load order has at least one node
++    }
++
++    public List<String> getLoadOrder() throws PluginGraphCycleException {
++        List<String> reversedTopographicSort;
++        try {
++            reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
++        } catch (TopographicGraphSorter.GraphCycleException exception) {
++            List<List<String>> cycles = new JohnsonSimpleCycles<>(this.graph).findAndRemoveSimpleCycles();
++
++            // Only log an error if at least non-Spigot plugin is present in the cycle
++            // Due to Spigot plugin metadata making no distinction between load order and dependencies (= class loader access), cycles are an unfortunate reality we have to deal with
++            Set<String> cyclingPlugins = new HashSet<>();
++            cycles.forEach(cyclingPlugins::addAll);
++            if (cyclingPlugins.stream().anyMatch(plugin -> {
++                PluginProvider<?> pluginProvider = this.providerMap.get(plugin);
++                return pluginProvider != null && !(pluginProvider instanceof SpigotPluginProvider);
++            })) {
++                logCycleError(cycles, this.providerMap);
++            }
++
++            // Try again after hopefully having removed all cycles
++            try {
++                reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
++            } catch (TopographicGraphSorter.GraphCycleException e) {
++                throw new PluginGraphCycleException(cycles);
++            }
++        }
++
++        return reversedTopographicSort;
++    }
++
++    private void logCycleError(List<List<String>> cycles, Map<String, PluginProvider<?>> providerMapMirror) {
++        LOGGER.error("=================================");
++        LOGGER.error("Circular plugin loading detected:");
++        for (int i = 0; i < cycles.size(); i++) {
++            List<String> cycle = cycles.get(i);
++            LOGGER.error("{}) {} -> {}", i + 1, String.join(" -> ", cycle), cycle.get(0));
++            for (String pluginName : cycle) {
++                PluginProvider<?> pluginProvider = providerMapMirror.get(pluginName);
++                if (pluginProvider == null) {
++                    return;
++                }
++
++                logPluginInfo(pluginProvider.getMeta());
++            }
++        }
++
++        LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help.");
++        LOGGER.error("=================================");
++    }
++
++    private void logPluginInfo(PluginMeta meta) {
++        if (!meta.getLoadBeforePlugins().isEmpty()) {
++            LOGGER.error("   {} loadbefore: {}", meta.getName(), meta.getLoadBeforePlugins());
++        }
++
++        if (meta instanceof PaperPluginMeta paperPluginMeta) {
++            if (!paperPluginMeta.getLoadAfterPlugins().isEmpty()) {
++                LOGGER.error("   {} loadafter: {}", meta.getName(), paperPluginMeta.getLoadAfterPlugins());
++            }
++        } else {
++            List<String> dependencies = new ArrayList<>();
++            dependencies.addAll(meta.getPluginDependencies());
++            dependencies.addAll(meta.getPluginSoftDependencies());
++            if (!dependencies.isEmpty()) {
++                LOGGER.error("   {} depend/softdepend: {}", meta.getName(), dependencies);
++            }
++        }
++    }
++}
+diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin.entrypoint.strategy.modern;
++
++import com.google.common.collect.Maps;
++import com.google.common.graph.GraphBuilder;
++import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
++import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
++import io.papermc.paper.plugin.provider.PluginProvider;
++import org.bukkit.plugin.UnknownDependencyException;
++import org.slf4j.Logger;
++
++import java.util.ArrayList;
++import java.util.HashMap;
++import java.util.HashSet;
++import java.util.List;
++import java.util.Map;
++
++@SuppressWarnings("UnstableApiUsage")
++public class ModernPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
++
++    private static final Logger LOGGER = LogUtils.getClassLogger();
++    private final ProviderConfiguration<T> configuration;
++
++    public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
++        this.configuration = onLoad;
++    }
++
++    @Override
++    public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> pluginProviders, MetaDependencyTree dependencyTree) {
++        Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>();
++        Map<String, PluginProvider<?>> providerMapMirror = Maps.transformValues(providerMap, (entry) -> entry.provider);
++        List<PluginProvider<T>> validatedProviders = new ArrayList<>();
++
++        // Populate provider map
++        for (PluginProvider<T> provider : pluginProviders) {
++            PluginMeta providerConfig = provider.getMeta();
++            PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider);
++
++            PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry);
++            if (replacedProvider != null) {
++                LOGGER.error(String.format(
++                    "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'",
++                    providerConfig.getName(),
++                    provider.getSource(),
++                    replacedProvider.provider.getSource(),
++                    replacedProvider.provider.getParentSource()
++                ));
++                this.configuration.onGenericError(replacedProvider.provider);
++            }
++
++            for (String extra : providerConfig.getProvidedPlugins()) {
++                PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry);
++                if (replacedExtraProvider != null) {
++                    LOGGER.warn(String.format(
++                        "`%s' is provided by both `%s' and `%s'",
++                        extra,
++                        providerConfig.getName(),
++                        replacedExtraProvider.provider.getMeta().getName()
++                    ));
++                }
++            }
++        }
++
++        // Populate dependency tree
++        for (PluginProvider<?> validated : pluginProviders) {
++            dependencyTree.add(validated);
++        }
++
++        // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid
++        for (PluginProvider<T> provider : pluginProviders) {
++            PluginMeta configuration = provider.getMeta();
++
++            // Populate missing dependencies to capture if there are multiple missing ones.
++            List<String> missingDependencies = provider.validateDependencies(dependencyTree);
++
++            if (missingDependencies.isEmpty()) {
++                validatedProviders.add(provider);
++            } else {
++                LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
++                // Because the validator is invalid, remove it from the provider map
++                providerMap.remove(configuration.getName());
++                // Cleanup plugins that failed to load
++                dependencyTree.remove(provider);
++                this.configuration.onGenericError(provider);
++            }
++        }
++
++        LoadOrderTree loadOrderTree = new LoadOrderTree(providerMapMirror, GraphBuilder.directed().build());
++        // Populate load order tree
++        for (PluginProvider<?> validated : validatedProviders) {
++            loadOrderTree.add(validated);
++        }
++
++        // Reverse the topographic search to let us see which providers we can load first.
++        List<String> reversedTopographicSort = loadOrderTree.getLoadOrder();
++        List<ProviderPair<T>> loadedPlugins = new ArrayList<>();
++        for (String providerIdentifier : reversedTopographicSort) {
++            // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist.
++            // The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just
++            // nicer to do it here TBH.
++            PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier);
++            if (retrievedProviderEntry == null || retrievedProviderEntry.provided) {
++                // OR if this was already provided (most likely from a plugin that already "provides" that dependency)
++                // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways
++                continue; // Skip provider that doesn't exist....
++            }
++            retrievedProviderEntry.provided = true;
++            PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider;
++            try {
++                this.configuration.applyContext(retrievedProvider, dependencyTree);
++
++                if (this.configuration.preloadProvider(retrievedProvider)) {
++                    T instance = retrievedProvider.createInstance();
++                    if (this.configuration.load(retrievedProvider, instance)) {
++                        loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance));
++                    }
++                }
++            } catch (Throwable ex) {
++                LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
++            }
++        }
++
++        return loadedPlugins;
++    }
++
++    private static class PluginProviderEntry<T> {
++
++        private final PluginProvider<T> provider;
++        private boolean provided;
++
++        private PluginProviderEntry(PluginProvider<T> provider) {
++            this.provider = provider;
++        }
++    }
++}
 diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -2898,6 +3038,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.plugin.manager;
 +
++import io.papermc.paper.plugin.configuration.PluginMeta;
++import io.papermc.paper.plugin.provider.type.PluginFileType;
 +import org.bukkit.Bukkit;
 +import org.bukkit.event.Event;
 +import org.bukkit.event.Listener;
@@ -2912,8 +3054,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import org.jetbrains.annotations.NotNull;
 +
 +import java.io.File;
++import java.io.FileNotFoundException;
++import java.io.IOException;
 +import java.util.Map;
 +import java.util.Set;
++import java.util.jar.JarFile;
 +import java.util.regex.Pattern;
 +
 +/**
@@ -2923,19 +3068,39 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +@ApiStatus.Internal
 +public class DummyBukkitPluginLoader implements PluginLoader {
 +
++    private static final Pattern[] PATTERNS = new Pattern[0];
++
 +    @Override
 +    public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
-+        throw new UnsupportedOperationException();
++        try {
++            return PaperPluginManagerImpl.getInstance().loadPlugin(file);
++        } catch (InvalidDescriptionException e) {
++            throw new InvalidPluginException(e);
++        }
 +    }
 +
 +    @Override
 +    public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException {
-+        throw new UnsupportedOperationException();
++        try (JarFile jar = new JarFile(file)) {
++            PluginFileType<?, ?> type = PluginFileType.guessType(jar);
++            if (type == null) {
++                throw new InvalidDescriptionException(new FileNotFoundException("Jar does not contain plugin.yml"));
++            }
++
++            PluginMeta meta = type.getConfig(jar);
++            if (meta instanceof PluginDescriptionFile pluginDescriptionFile) {
++                return pluginDescriptionFile;
++            } else {
++                throw new InvalidDescriptionException("Plugin type does not use plugin.yml. Cannot read file description.");
++            }
++        } catch (Exception e) {
++            throw new InvalidDescriptionException(e);
++        }
 +    }
 +
 +    @Override
 +    public @NotNull Pattern[] getPluginFileFilters() {
-+        throw new UnsupportedOperationException();
++        return PATTERNS;
 +    }
 +
 +    @Override
@@ -2964,6 +3129,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.mojang.logging.LogUtils;
 +import io.papermc.paper.plugin.entrypoint.Entrypoint;
 +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
 +import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
@@ -2978,6 +3145,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private static final Logger LOGGER = LogUtils.getClassLogger();
 +    private final List<JavaPlugin> provided = new ArrayList<>();
 +
++    private final MetaDependencyTree dependencyTree;
++
++    MultiRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
++        this.dependencyTree = dependencyTree;
++    }
++
 +    @Override
 +    public void register(PluginProvider<JavaPlugin> provider) {
 +        if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
@@ -3007,6 +3180,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        return this.provided;
 +    }
 +
++    @Override
++    public MetaDependencyTree getDependencyTree() {
++        return this.dependencyTree;
++    }
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
 new file mode 100644
@@ -3477,12 +3654,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.google.common.graph.MutableGraph;
 +import io.papermc.paper.plugin.configuration.PluginMeta;
 +import io.papermc.paper.plugin.entrypoint.Entrypoint;
-+import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
-+import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
 +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
 +import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
-+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
 +import io.papermc.paper.plugin.provider.source.DirectoryProviderSource;
 +import io.papermc.paper.plugin.provider.source.FileProviderSource;
 +import org.bukkit.Bukkit;
@@ -3529,8 +3704,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private final CommandMap commandMap;
 +    private final Server server;
 +
-+    private final MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
-+    private final DependencyContext context = new GraphDependencyContext(this.dependencyGraph);
++    private final MetaDependencyTree dependencyTree = new MetaDependencyTree(GraphBuilder.directed().build());
 +
 +    public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
 +        this.commandMap = commandMap;
@@ -3569,12 +3743,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
 +        }
 +
-+        DependencyUtil.buildDependencyGraph(this.dependencyGraph, configuration);
++        this.dependencyTree.add(configuration);
 +    }
 +
 +    // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception.
 +    public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException {
-+        RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage());
++        RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage(this.dependencyTree));
 +
 +        try {
 +            FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
@@ -3603,7 +3777,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
 +        Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
 +
-+        RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage());
++        RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree));
 +        try {
 +            DIRECTORY_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, directory);
 +            runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
@@ -3761,7 +3935,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
-+        return this.context.isTransitiveDependency(plugin, depend);
++        return this.dependencyTree.isTransitiveDependency(plugin, depend);
 +    }
 +
 +    public boolean hasDependency(String pluginIdentifier) {
@@ -3771,7 +3945,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    // Debug only
 +    @ApiStatus.Internal
 +    public MutableGraph<String> getDependencyGraph() {
-+        return this.dependencyGraph;
++        return this.dependencyTree.getGraph();
 +    }
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
@@ -4085,6 +4259,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import com.destroystokyo.paper.util.SneakyThrow;
 +import io.papermc.paper.plugin.entrypoint.Entrypoint;
 +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
 +import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
@@ -4103,9 +4279,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 + */
 +class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage {
 +
++    private final MetaDependencyTree dependencyTree;
 +    private PluginProvider<JavaPlugin> lastProvider;
 +    private JavaPlugin singleLoaded;
 +
++    SingularRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
++        this.dependencyTree = dependencyTree;
++    }
++
 +    @Override
 +    public void register(PluginProvider<JavaPlugin> provider) {
 +        super.register(provider);
@@ -4128,19 +4309,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            return;
 +        }
 +
-+        // Manually validate dependencies, LEGACY BEHAVIOR.
-+        // Normally it is logged, but manually adding one plugin will cause it to actually throw exceptions.
-+        PluginDescriptionFile descriptionFile = (PluginDescriptionFile) provider.getMeta();
-+        List<String> missingDependencies = new ArrayList<>();
-+        for (String dependency : descriptionFile.getDepend()) {
-+            if (!PaperPluginManagerImpl.getInstance().isPluginEnabled(dependency)) {
-+                missingDependencies.add(dependency);
-+            }
-+        }
-+        if (!missingDependencies.isEmpty()) {
-+            throw new UnknownDependencyException(missingDependencies, provider.getFileName().toString());
-+        }
-+
 +        // Go through normal plugin loading logic
 +        super.enter();
 +    }
@@ -4159,6 +4327,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public Optional<JavaPlugin> getSingleLoaded() {
 +        return Optional.ofNullable(this.singleLoaded);
 +    }
++
++    @Override
++    public MetaDependencyTree getDependencyTree() {
++        return this.dependencyTree;
++    }
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
 new file mode 100644
@@ -4218,6 +4391,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +import io.papermc.paper.plugin.configuration.PluginMeta;
 +import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
 +import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
 +import org.jetbrains.annotations.ApiStatus;
 +import org.jetbrains.annotations.NotNull;
@@ -4266,7 +4440,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    LoadOrderConfiguration createConfiguration(@NotNull Map<String, PluginProvider<?>> toLoad);
 +
 +    // Returns a list of missing dependencies
-+    List<String> validateDependencies(@NotNull Map<String, PluginProvider<?>> toLoad);
++    List<String> validateDependencies(@NotNull DependencyContext context);
 +
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
@@ -4988,9 +5162,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
 +import org.slf4j.Logger;
 +
++import java.nio.file.FileVisitOption;
 +import java.nio.file.Files;
 +import java.nio.file.Path;
-+import java.util.logging.Level;
 +
 +/**
 + * Loads all plugin providers in the given directory.
@@ -5001,7 +5175,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    private static final Logger LOGGER = LogUtils.getClassLogger();
 +
 +    public DirectoryProviderSource() {
-+        super("File '%s'"::formatted);
++        super("Directory '%s'"::formatted);
 +    }
 +
 +    @Override
@@ -5011,15 +5185,22 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            Files.createDirectories(context);
 +        }
 +
-+        Files.walk(context, 1).filter(Files::isRegularFile).forEach((path) -> {
-+            try {
-+                super.registerProviders(entrypointHandler, path);
-+            } catch (IllegalArgumentException ignored) {
-+                // Ignore initial argument exceptions
-+            } catch (Exception e) {
-+                LOGGER.error("Error loading plugin: " + e.getMessage(), e);
-+            }
-+        });
++        Files.walk(context, 1, FileVisitOption.FOLLOW_LINKS)
++            .filter(this::isValidFile)
++            .forEach((path) -> {
++                try {
++                    super.registerProviders(entrypointHandler, path);
++                } catch (IllegalArgumentException ignored) {
++                    // Ignore initial argument exceptions
++                } catch (Exception e) {
++                    LOGGER.error("Error loading plugin: " + e.getMessage(), e);
++                }
++            });
++    }
++
++    public boolean isValidFile(Path path) {
++        // Avoid loading plugins that start with a dot
++        return Files.isRegularFile(path) && !path.startsWith(".");
 +    }
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
@@ -5081,7 +5262,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +            JarFile file = new JarFile(context.toFile(), true, JarFile.OPEN_READ, JarFile.runtimeVersion());
 +            PluginFileType<?, ?> type = PluginFileType.guessType(file);
 +            if (type == null) {
-+                throw new IllegalArgumentException(source + " is not a valid plugin file, cannot load a plugin from it!");
++                throw new IllegalArgumentException(source + " does not contain a " + String.join(" or ", PluginFileType.getConfigTypes()) + "! Could not determine plugin type, cannot load a plugin from it!");
 +            }
 +
 +            type.register(entrypointHandler, file, context);
@@ -5131,7 +5312,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        try (JarFile file = new JarFile(path.toFile())) {
 +            PluginFileType<?, ?> type = PluginFileType.guessType(file);
 +            if (type == null) {
-+                throw new IllegalArgumentException(path + " is not a valid plugin file, cannot load a plugin from it!");
++                throw new IllegalArgumentException(path + " does not contain a " + String.join(" or ", PluginFileType.getConfigTypes()) + "! Could not determine plugin type, cannot load a plugin from it!");
 +            }
 +
 +            return type.getConfig(file).getName();
@@ -5258,6 +5439,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import org.jetbrains.annotations.Nullable;
 +
 +import java.nio.file.Path;
++import java.util.ArrayList;
 +import java.util.List;
 +import java.util.jar.JarEntry;
 +import java.util.jar.JarFile;
@@ -5269,6 +5451,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 + */
 +public abstract class PluginFileType<T, C extends PluginMeta> {
 +
++    private static final List<String> CONFIG_TYPES = new ArrayList<>();
++    
 +    public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) {
 +        @Override
 +        protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) {
@@ -5296,6 +5480,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    PluginFileType(String config, PluginTypeFactory<T, C> factory) {
 +        this.config = config;
 +        this.factory = factory;
++        CONFIG_TYPES.add(config);
 +    }
 +
 +    @Nullable
@@ -5322,6 +5507,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    protected abstract void register(EntrypointHandler entrypointHandler, T provider);
++
++    public static List<String> getConfigTypes() {
++        return CONFIG_TYPES;
++    }
 +}
 diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
 new file mode 100644
@@ -5557,11 +5746,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        @Override
-+        public List<String> validateDependencies(@NotNull Map<String, PluginProvider<?>> toLoad) {
++        public List<String> validateDependencies(@NotNull DependencyContext context) {
 +            List<String> missingDependencies = new ArrayList<>();
 +            for (DependencyConfiguration configuration : this.getMeta().getDependencies()) {
 +                String dependency = configuration.name();
-+                if (configuration.required() && configuration.bootstrap() && !toLoad.containsKey(dependency)) {
++                if (configuration.required() && configuration.bootstrap() && !context.hasDependency(dependency)) {
 +                    missingDependencies.add(dependency);
 +                }
 +            }
@@ -5663,8 +5852,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +
 +        @Override
-+        public List<String> validateDependencies(@NotNull Map<String, PluginProvider<?>> toLoad) {
-+            return DependencyUtil.validateSimple(this.getMeta(), toLoad);
++        public List<String> validateDependencies(@NotNull DependencyContext context) {
++            return DependencyUtil.validateSimple(this.getMeta(), context);
 +        }
 +
 +        @Override
@@ -6022,8 +6211,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    @Override
-+    public List<String> validateDependencies(@NotNull Map<String, PluginProvider<?>> toLoad) {
-+        return DependencyUtil.validateSimple(this.getMeta(), toLoad);
++    public List<String> validateDependencies(@NotNull DependencyContext context) {
++        return DependencyUtil.validateSimple(this.getMeta(), context);
 +    }
 +
 +    @Override
@@ -6118,19 +6307,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
 +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
 +import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
-+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
-+import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
 +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +import io.papermc.paper.plugin.provider.ProviderStatus;
 +import io.papermc.paper.plugin.provider.ProviderStatusHolder;
-+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
-+import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
 +import org.slf4j.Logger;
 +
-+import java.util.ArrayList;
-+import java.util.List;
-+
 +public class BootstrapProviderStorage extends SimpleProviderStorage<PluginBootstrap> {
 +
 +    private static final Logger LOGGER = LogUtils.getClassLogger();
@@ -6150,7 +6333,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    PluginProviderContext context = PluginProviderContextImpl.of(provider, PluginInitializerManager.instance().pluginDirectoryPath());
 +                    provided.bootstrap(context);
 +                    return true;
-+                } catch (Exception e) {
++                } catch (Throwable e) {
 +                    LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e);
 +                    if (provider instanceof ProviderStatusHolder statusHolder) {
 +                        statusHolder.setStatus(ProviderStatus.ERRORED);
@@ -6158,6 +6341,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +                    return false;
 +                }
 +            }
++
++            @Override
++            public void onGenericError(PluginProvider<PluginBootstrap> provider) {
++                if (provider instanceof ProviderStatusHolder statusHolder) {
++                    statusHolder.setStatus(ProviderStatus.ERRORED);
++                }
++            }
 +        }));
 +    }
 +
@@ -6175,7 +6365,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +package io.papermc.paper.plugin.storage;
 +
 +import io.papermc.paper.plugin.entrypoint.strategy.LegacyPluginLoadingStrategy;
-+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
 +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
 +
 +public abstract class ConfiguredProviderStorage<T> extends SimpleProviderStorage<T> {
@@ -6297,7 +6487,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.plugin.storage;
 +
++import com.google.common.graph.GraphBuilder;
++import com.google.common.graph.MutableGraph;
 +import com.mojang.logging.LogUtils;
++import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
 +import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
 +import io.papermc.paper.plugin.provider.PluginProvider;
@@ -6329,7 +6523,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.filterLoadingProviders(providerList);
 +
 +        try {
-+            for (ProviderLoadingStrategy.ProviderPair<T> providerPair : this.strategy.loadProviders(providerList)) {
++            for (ProviderLoadingStrategy.ProviderPair<T> providerPair : this.strategy.loadProviders(providerList, this.getDependencyTree())) {
 +                this.processProvided(providerPair.provider(), providerPair.provided());
 +            }
 +        } catch (PluginGraphCycleException exception) {
@@ -6337,6 +6531,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        }
 +    }
 +
++    public MetaDependencyTree getDependencyTree() {
++        return new MetaDependencyTree(GraphBuilder.directed().build());
++    }
++
 +    @Override
 +    public Iterable<PluginProvider<T>> getRegisteredProviders() {
 +        return this.providers;
@@ -6823,6 +7021,71 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        throw new UnsupportedOperationException("Not supported.");
 +    }
 +}
+diff --git a/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java b/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/test/java/io/papermc/paper/plugin/PluginDependencyValidationTest.java
+@@ -0,0 +0,0 @@
++package io.papermc.paper.plugin;
++
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
++import org.junit.Test;
++
++import java.util.List;
++
++import static org.hamcrest.MatcherAssert.assertThat;
++
++public class PluginDependencyValidationTest {
++
++    private static final TestPluginMeta MAIN;
++    private static final TestPluginMeta HARD_DEPENDENCY_1;
++    private static final TestPluginMeta SOFT_DEPENDENCY_1;
++
++    public static final String ROOT_NAME = "main";
++
++    public static final String REGISTERED_HARD_DEPEND = "hard1";
++    public static final String REGISTERED_SOFT_DEPEND = "soft1";
++    public static final String UNREGISTERED_HARD_DEPEND = "hard2";
++    public static final String UNREGISTERED_SOFT_DEPEND = "soft2";
++
++    static {
++        MAIN = new TestPluginMeta(ROOT_NAME);
++        MAIN.setSoftDependencies(List.of(REGISTERED_SOFT_DEPEND, UNREGISTERED_SOFT_DEPEND));
++        MAIN.setHardDependencies(List.of(REGISTERED_HARD_DEPEND, UNREGISTERED_HARD_DEPEND));
++
++        HARD_DEPENDENCY_1 = new TestPluginMeta(REGISTERED_HARD_DEPEND);
++        SOFT_DEPENDENCY_1 = new TestPluginMeta(REGISTERED_SOFT_DEPEND);
++    }
++
++    @Test
++    public void testDependencyTree() {
++        MetaDependencyTree tree = new MetaDependencyTree();
++        tree.add(MAIN);
++        tree.add(HARD_DEPENDENCY_1);
++        tree.add(SOFT_DEPENDENCY_1);
++
++        // Test simple transitive dependencies
++        assertThat("%s was not a transitive dependency of %s".formatted(ROOT_NAME, REGISTERED_SOFT_DEPEND), tree.isTransitiveDependency(MAIN, SOFT_DEPENDENCY_1));
++        assertThat("%s was not a transitive dependency of %s".formatted(ROOT_NAME, REGISTERED_HARD_DEPEND), tree.isTransitiveDependency(MAIN, HARD_DEPENDENCY_1));
++
++        assertThat("%s was a transitive dependency of %s".formatted(REGISTERED_SOFT_DEPEND, ROOT_NAME), !tree.isTransitiveDependency(SOFT_DEPENDENCY_1, MAIN));
++        assertThat("%s was a transitive dependency of %s".formatted(REGISTERED_HARD_DEPEND, ROOT_NAME), !tree.isTransitiveDependency(HARD_DEPENDENCY_1, MAIN));
++
++        // Test to ensure that registered dependencies exist
++        assertThat("tree did not contain dependency %s".formatted(ROOT_NAME), tree.hasDependency(ROOT_NAME));
++        assertThat("tree did not contain dependency %s".formatted(REGISTERED_HARD_DEPEND), tree.hasDependency(REGISTERED_HARD_DEPEND));
++        assertThat("tree did not contain dependency %s".formatted(REGISTERED_SOFT_DEPEND), tree.hasDependency(REGISTERED_SOFT_DEPEND));
++
++        // Test to ensure unregistered dependencies don't exist
++        assertThat("tree contained dependency %s".formatted(UNREGISTERED_HARD_DEPEND), !tree.hasDependency(UNREGISTERED_HARD_DEPEND));
++        assertThat("tree contained dependency %s".formatted(UNREGISTERED_SOFT_DEPEND), !tree.hasDependency(UNREGISTERED_SOFT_DEPEND));
++
++        // Test removal
++        tree.remove(HARD_DEPENDENCY_1);
++        assertThat("tree contained dependency %s".formatted(REGISTERED_HARD_DEPEND), !tree.hasDependency(REGISTERED_HARD_DEPEND));
++    }
++}
 diff --git a/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java b/src/test/java/io/papermc/paper/plugin/PluginLoadOrderTest.java
 new file mode 100644
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
@@ -6831,8 +7094,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 @@ -0,0 +0,0 @@
 +package io.papermc.paper.plugin;
 +
++import com.google.common.graph.GraphBuilder;
++import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
 +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
-+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
++import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy;
 +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +import org.junit.Assert;
@@ -6934,7 +7199,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +
 +        });
 +
-+        modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS);
++        modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS, new MetaDependencyTree(GraphBuilder.directed().build()));
 +    }
 +
 +    @Test
@@ -7178,6 +7443,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
 +import io.papermc.paper.plugin.provider.PluginProvider;
 +import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
++import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
 +import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
 +import org.jetbrains.annotations.NotNull;
 +
@@ -7244,8 +7510,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +
 +    @Override
-+    public List<String> validateDependencies(@NotNull Map<String, PluginProvider<?>> toLoad) {
-+        return DependencyUtil.validateSimple(this.getMeta(), toLoad);
++    public List<String> validateDependencies(@NotNull DependencyContext context) {
++        return DependencyUtil.validateSimple(this.getMeta(), context);
 +    }
 +}
 diff --git a/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java