graph) {
+ super(graph);
+ }
+
+ @Override
+ protected void registerDependencies(final String identifier, final PluginMeta meta) {
+ for (String dependency : meta.getPluginDependencies()) {
+ this.graph.putEdge(identifier, dependency);
+ }
+ for (String dependency : meta.getPluginSoftDependencies()) {
+ this.graph.putEdge(identifier, dependency);
+ }
+ }
+
+ @Override
+ protected void unregisterDependencies(final String identifier, final PluginMeta meta) {
+ for (String dependency : meta.getPluginDependencies()) {
+ this.graph.removeEdge(identifier, dependency);
+ }
+ for (String dependency : meta.getPluginSoftDependencies()) {
+ this.graph.removeEdge(identifier, dependency);
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9bca905eba67972e4d1b07b1d243272b62fec66
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
@@ -0,0 +1,354 @@
+/*
+ * (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors.
+ *
+ * JGraphT : a free Java graph-theory library
+ *
+ * See the CONTRIBUTORS.md file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the
+ * GNU Lesser General Public License v2.1 or later
+ * which is available at
+ * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
+ */
+
+// MODIFICATIONS:
+// - Modified to use a guava graph directly
+
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.base.Preconditions;
+import com.google.common.graph.Graph;
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import com.mojang.datafixers.util.Pair;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/**
+ * Find all simple cycles of a directed graph using the Johnson's algorithm.
+ *
+ *
+ * See:
+ * D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975),
+ * pp. 77-84.
+ *
+ * @param the vertex type.
+ *
+ * @author Nikolay Ognyanov
+ */
+public class JohnsonSimpleCycles
+{
+ // The graph.
+ private Graph graph;
+
+ // The main state of the algorithm.
+ private Consumer> cycleConsumer = null;
+ private BiConsumer cycleVertexSuccessorConsumer = null; // Paper
+ private V[] iToV = null;
+ private Map vToI = null;
+ private Set blocked = null;
+ private Map> bSets = null;
+ private ArrayDeque stack = null;
+
+ // The state of the embedded Tarjan SCC algorithm.
+ private List> foundSCCs = null;
+ private int index = 0;
+ private Map vIndex = null;
+ private Map vLowlink = null;
+ private ArrayDeque path = null;
+ private Set pathSet = null;
+
+ /**
+ * Create a simple cycle finder for the specified graph.
+ *
+ * @param graph - the DirectedGraph in which to find cycles.
+ *
+ * @throws IllegalArgumentException if the graph argument is
+ * null
.
+ */
+ public JohnsonSimpleCycles(Graph graph)
+ {
+ Preconditions.checkState(graph.isDirected(), "Graph must be directed");
+ this.graph = graph;
+ }
+
+ /**
+ * Find the simple cycles of the graph.
+ *
+ * @return The list of all simple cycles. Possibly empty but never null
.
+ */
+ public List> findAndRemoveSimpleCycles()
+ {
+ List> result = new ArrayList<>();
+ findSimpleCycles(result::add, (v, s) -> ((MutableGraph) graph).removeEdge(v, s)); // Paper
+ return result;
+ }
+
+ /**
+ * Find the simple cycles of the graph.
+ *
+ * @param consumer Consumer that will be called with each cycle found.
+ */
+ public void findSimpleCycles(Consumer> consumer, BiConsumer vertexSuccessorConsumer) // Paper
+ {
+ if (graph == null) {
+ throw new IllegalArgumentException("Null graph.");
+ }
+ cycleVertexSuccessorConsumer = vertexSuccessorConsumer; // Paper
+ initState(consumer);
+
+ int startIndex = 0;
+ int size = graph.nodes().size();
+ while (startIndex < size) {
+ Pair, Integer> minSCCGResult = findMinSCSG(startIndex);
+ if (minSCCGResult != null) {
+ startIndex = minSCCGResult.getSecond();
+ Graph scg = minSCCGResult.getFirst();
+ V startV = toV(startIndex);
+ for (V v : scg.successors(startV)) {
+ blocked.remove(v);
+ getBSet(v).clear();
+ }
+ findCyclesInSCG(startIndex, startIndex, scg);
+ startIndex++;
+ } else {
+ break;
+ }
+ }
+
+ clearState();
+ }
+
+ private Pair, Integer> findMinSCSG(int startIndex)
+ {
+ /*
+ * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph
+ * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected
+ * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum
+ * (among those SCCs) vertex index. We return that index together with the graph.
+ */
+ initMinSCGState();
+
+ List> foundSCCs = findSCCS(startIndex);
+
+ // find the SCC with the minimum index
+ int minIndexFound = Integer.MAX_VALUE;
+ Set minSCC = null;
+ for (Set scc : foundSCCs) {
+ for (V v : scc) {
+ int t = toI(v);
+ if (t < minIndexFound) {
+ minIndexFound = t;
+ minSCC = scc;
+ }
+ }
+ }
+ if (minSCC == null) {
+ return null;
+ }
+
+ // build a graph for the SCC found
+ MutableGraph dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build();
+
+ for (V v : minSCC) {
+ for (V w : minSCC) {
+ if (graph.hasEdgeConnecting(v, w)) {
+ dependencyGraph.putEdge(v, w);
+ }
+ }
+ }
+
+ Pair, Integer> result = Pair.of(dependencyGraph, minIndexFound);
+ clearMinSCCState();
+ return result;
+ }
+
+ private List> findSCCS(int startIndex)
+ {
+ // Find SCCs in the subgraph induced
+ // by vertices startIndex and beyond.
+ // A call to StrongConnectivityAlgorithm
+ // would be too expensive because of the
+ // need to materialize the subgraph.
+ // So - do a local search by the Tarjan's
+ // algorithm and pretend that vertices
+ // with an index smaller than startIndex
+ // do not exist.
+ for (V v : graph.nodes()) {
+ int vI = toI(v);
+ if (vI < startIndex) {
+ continue;
+ }
+ if (!vIndex.containsKey(v)) {
+ getSCCs(startIndex, vI);
+ }
+ }
+ List> result = foundSCCs;
+ foundSCCs = null;
+ return result;
+ }
+
+ private void getSCCs(int startIndex, int vertexIndex)
+ {
+ V vertex = toV(vertexIndex);
+ vIndex.put(vertex, index);
+ vLowlink.put(vertex, index);
+ index++;
+ path.push(vertex);
+ pathSet.add(vertex);
+
+ Set edges = graph.successors(vertex);
+ for (V successor : edges) {
+ int successorIndex = toI(successor);
+ if (successorIndex < startIndex) {
+ continue;
+ }
+ if (!vIndex.containsKey(successor)) {
+ getSCCs(startIndex, successorIndex);
+ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor)));
+ } else if (pathSet.contains(successor)) {
+ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor)));
+ }
+ }
+ if (vLowlink.get(vertex).equals(vIndex.get(vertex))) {
+ Set result = new HashSet<>();
+ V temp;
+ do {
+ temp = path.pop();
+ pathSet.remove(temp);
+ result.add(temp);
+ } while (!vertex.equals(temp));
+ if (result.size() == 1) {
+ V v = result.iterator().next();
+ if (graph.edges().contains(vertex)) {
+ foundSCCs.add(result);
+ }
+ } else {
+ foundSCCs.add(result);
+ }
+ }
+ }
+
+ private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph scg)
+ {
+ /*
+ * Find cycles in a strongly connected graph per Johnson.
+ */
+ boolean foundCycle = false;
+ V vertex = toV(vertexIndex);
+ stack.push(vertex);
+ blocked.add(vertex);
+
+ for (V successor : scg.successors(vertex)) {
+ int successorIndex = toI(successor);
+ if (successorIndex == startIndex) {
+ List cycle = new ArrayList<>(stack.size());
+ stack.descendingIterator().forEachRemaining(cycle::add);
+ cycleConsumer.accept(cycle);
+ cycleVertexSuccessorConsumer.accept(vertex, successor); // Paper
+ //foundCycle = true; // Paper
+ } else if (!blocked.contains(successor)) {
+ boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg);
+ foundCycle = foundCycle || gotCycle;
+ }
+ }
+ if (foundCycle) {
+ unblock(vertex);
+ } else {
+ for (V w : scg.successors(vertex)) {
+ Set bSet = getBSet(w);
+ bSet.add(vertex);
+ }
+ }
+ stack.pop();
+ return foundCycle;
+ }
+
+ private void unblock(V vertex)
+ {
+ blocked.remove(vertex);
+ Set bSet = getBSet(vertex);
+ while (bSet.size() > 0) {
+ V w = bSet.iterator().next();
+ bSet.remove(w);
+ if (blocked.contains(w)) {
+ unblock(w);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void initState(Consumer> consumer)
+ {
+ cycleConsumer = consumer;
+ iToV = (V[]) graph.nodes().toArray();
+ vToI = new HashMap<>();
+ blocked = new HashSet<>();
+ bSets = new HashMap<>();
+ stack = new ArrayDeque<>();
+
+ for (int i = 0; i < iToV.length; i++) {
+ vToI.put(iToV[i], i);
+ }
+ }
+
+ private void clearState()
+ {
+ cycleConsumer = null;
+ iToV = null;
+ vToI = null;
+ blocked = null;
+ bSets = null;
+ stack = null;
+ }
+
+ private void initMinSCGState()
+ {
+ index = 0;
+ foundSCCs = new ArrayList<>();
+ vIndex = new HashMap<>();
+ vLowlink = new HashMap<>();
+ path = new ArrayDeque<>();
+ pathSet = new HashSet<>();
+ }
+
+ private void clearMinSCCState()
+ {
+ index = 0;
+ foundSCCs = null;
+ vIndex = null;
+ vLowlink = null;
+ path = null;
+ pathSet = null;
+ }
+
+ private Integer toI(V vertex)
+ {
+ return vToI.get(vertex);
+ }
+
+ private V toV(Integer i)
+ {
+ return iToV[i];
+ }
+
+ private Set getBSet(V v)
+ {
+ // B sets typically not all needed,
+ // so instantiate lazily.
+ return bSets.computeIfAbsent(v, k -> new HashSet<>());
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..f59f48654eaa299bcac862991b1e2e622264639b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
@@ -0,0 +1,271 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
+import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
+import org.bukkit.plugin.UnknownDependencyException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+@SuppressWarnings("UnstableApiUsage")
+public class LegacyPluginLoadingStrategy implements ProviderLoadingStrategy {
+
+ private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy");
+ private final ProviderConfiguration configuration;
+
+ public LegacyPluginLoadingStrategy(ProviderConfiguration onLoad) {
+ this.configuration = onLoad;
+ }
+
+ @Override
+ public List> loadProviders(List> providers, MetaDependencyTree dependencyTree) {
+ List> javapluginsLoaded = new ArrayList<>();
+ MutableGraph dependencyGraph = dependencyTree.getGraph();
+
+ Map> providersToLoad = new HashMap<>();
+ Set loadedPlugins = new HashSet<>();
+ Map pluginsProvided = new HashMap<>();
+ Map> dependencies = new HashMap<>();
+ Map> softDependencies = new HashMap<>();
+
+ for (PluginProvider provider : providers) {
+ PluginMeta configuration = provider.getMeta();
+
+ PluginProvider replacedProvider = providersToLoad.put(configuration.getName(), provider);
+ dependencyTree.addDirectDependency(configuration.getName()); // add to dependency tree
+ if (replacedProvider != null) {
+ LOGGER.severe(String.format(
+ "Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'",
+ configuration.getName(),
+ provider.getSource(),
+ replacedProvider.getSource(),
+ replacedProvider.getParentSource()
+ ));
+ }
+
+ String removedProvided = pluginsProvided.remove(configuration.getName());
+ if (removedProvided != null) {
+ LOGGER.warning(String.format(
+ "Ambiguous plugin name `%s'. It is also provided by `%s'",
+ configuration.getName(),
+ removedProvided
+ ));
+ }
+
+ for (String provided : configuration.getProvidedPlugins()) {
+ PluginProvider pluginProvider = providersToLoad.get(provided);
+
+ if (pluginProvider != null) {
+ LOGGER.warning(String.format(
+ "`%s provides `%s' while this is also the name of `%s' in `%s'",
+ provider.getSource(),
+ provided,
+ pluginProvider.getSource(),
+ provider.getParentSource()
+ ));
+ } else {
+ String replacedPlugin = pluginsProvided.put(provided, configuration.getName());
+ dependencyTree.addDirectDependency(provided); // add to dependency tree
+ if (replacedPlugin != null) {
+ LOGGER.warning(String.format(
+ "`%s' is provided by both `%s' and `%s'",
+ provided,
+ configuration.getName(),
+ replacedPlugin
+ ));
+ }
+ }
+ }
+
+ Collection softDependencySet = provider.getMeta().getPluginSoftDependencies();
+ if (softDependencySet != null && !softDependencySet.isEmpty()) {
+ if (softDependencies.containsKey(configuration.getName())) {
+ // Duplicates do not matter, they will be removed together if applicable
+ softDependencies.get(configuration.getName()).addAll(softDependencySet);
+ } else {
+ softDependencies.put(configuration.getName(), new LinkedList(softDependencySet));
+ }
+
+ for (String depend : softDependencySet) {
+ dependencyGraph.putEdge(configuration.getName(), depend);
+ }
+ }
+
+ Collection dependencySet = provider.getMeta().getPluginDependencies();
+ if (dependencySet != null && !dependencySet.isEmpty()) {
+ dependencies.put(configuration.getName(), new LinkedList(dependencySet));
+
+ for (String depend : dependencySet) {
+ dependencyGraph.putEdge(configuration.getName(), depend);
+ }
+ }
+
+ Collection loadBeforeSet = provider.getMeta().getLoadBeforePlugins();
+ if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) {
+ for (String loadBeforeTarget : loadBeforeSet) {
+ if (softDependencies.containsKey(loadBeforeTarget)) {
+ softDependencies.get(loadBeforeTarget).add(configuration.getName());
+ } else {
+ // softDependencies is never iterated, so 'ghost' plugins aren't an issue
+ Collection shortSoftDependency = new LinkedList();
+ shortSoftDependency.add(configuration.getName());
+ softDependencies.put(loadBeforeTarget, shortSoftDependency);
+ }
+
+ dependencyGraph.putEdge(loadBeforeTarget, configuration.getName());
+ }
+ }
+ }
+
+ while (!providersToLoad.isEmpty()) {
+ boolean missingDependency = true;
+ Iterator>> providerIterator = providersToLoad.entrySet().iterator();
+
+ while (providerIterator.hasNext()) {
+ Map.Entry> entry = providerIterator.next();
+ String providerIdentifier = entry.getKey();
+
+ if (dependencies.containsKey(providerIdentifier)) {
+ Iterator dependencyIterator = dependencies.get(providerIdentifier).iterator();
+ final Set missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends
+
+ while (dependencyIterator.hasNext()) {
+ String dependency = dependencyIterator.next();
+
+ // Dependency loaded
+ if (loadedPlugins.contains(dependency)) {
+ dependencyIterator.remove();
+
+ // We have a dependency not found
+ } else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) {
+ // Paper start
+ missingHardDependencies.add(dependency);
+ }
+ }
+ if (!missingHardDependencies.isEmpty()) {
+ // Paper end
+ missingDependency = false;
+ providerIterator.remove();
+ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
+ softDependencies.remove(providerIdentifier);
+ dependencies.remove(providerIdentifier);
+
+ LOGGER.log(
+ Level.SEVERE,
+ "Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper
+ new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper
+ }
+
+ if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) {
+ dependencies.remove(providerIdentifier);
+ }
+ }
+ if (softDependencies.containsKey(providerIdentifier)) {
+ Iterator softDependencyIterator = softDependencies.get(providerIdentifier).iterator();
+
+ while (softDependencyIterator.hasNext()) {
+ String softDependency = softDependencyIterator.next();
+
+ // Soft depend is no longer around
+ if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) {
+ softDependencyIterator.remove();
+ }
+ }
+
+ if (softDependencies.get(providerIdentifier).isEmpty()) {
+ softDependencies.remove(providerIdentifier);
+ }
+ }
+ if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) {
+ // We're clear to load, no more soft or hard dependencies left
+ PluginProvider file = providersToLoad.get(providerIdentifier);
+ providerIterator.remove();
+ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
+ missingDependency = false;
+
+ try {
+ this.configuration.applyContext(file, dependencyTree);
+ T loadedPlugin = file.createInstance();
+ this.warnIfPaperPlugin(file);
+
+ if (this.configuration.load(file, loadedPlugin)) {
+ loadedPlugins.add(file.getMeta().getName());
+ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
+ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
+ }
+
+ } catch (Throwable ex) {
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
+ }
+ }
+ }
+
+ if (missingDependency) {
+ // We now iterate over plugins until something loads
+ // This loop will ignore soft dependencies
+ providerIterator = providersToLoad.entrySet().iterator();
+
+ while (providerIterator.hasNext()) {
+ Map.Entry> entry = providerIterator.next();
+ String plugin = entry.getKey();
+
+ if (!dependencies.containsKey(plugin)) {
+ softDependencies.remove(plugin);
+ missingDependency = false;
+ PluginProvider file = entry.getValue();
+ providerIterator.remove();
+
+ try {
+ this.configuration.applyContext(file, dependencyTree);
+ T loadedPlugin = file.createInstance();
+ this.warnIfPaperPlugin(file);
+
+ if (this.configuration.load(file, loadedPlugin)) {
+ loadedPlugins.add(file.getMeta().getName());
+ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
+ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
+ }
+ break;
+ } catch (Throwable ex) {
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
+ }
+ }
+ }
+ // We have no plugins left without a depend
+ if (missingDependency) {
+ softDependencies.clear();
+ dependencies.clear();
+ Iterator> failedPluginIterator = providersToLoad.values().iterator();
+
+ while (failedPluginIterator.hasNext()) {
+ PluginProvider file = failedPluginIterator.next();
+ failedPluginIterator.remove();
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper
+ }
+ }
+ }
+ }
+
+ return javapluginsLoaded;
+ }
+
+ private void warnIfPaperPlugin(PluginProvider provider) {
+ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
+ provider.getLogger().warn("Loading Paper plugin in the legacy plugin loading logic. This is not recommended and may introduce some differences into load order. It's highly recommended you move away from this if you are wanting to use Paper plugins.");
+ }
+ }
+}
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..2ea978ac957849260e7ca69c9ff56588d0ccc41b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
@@ -0,0 +1,19 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import java.util.List;
+
+/**
+ * Indicates a dependency cycle within a provider loading sequence.
+ */
+public class PluginGraphCycleException extends RuntimeException {
+
+ private final List> cycles;
+
+ public PluginGraphCycleException(List> cycles) {
+ this.cycles = cycles;
+ }
+
+ public List> getCycles() {
+ return this.cycles;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..67c4ef672ee509deba2b4bcaac42d9db24d4c89a
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
@@ -0,0 +1,25 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+
+/**
+ * Used to share code with the modern and legacy plugin load strategy.
+ *
+ * @param
+ */
+public interface ProviderConfiguration {
+
+ void applyContext(PluginProvider provider, DependencyContext dependencyContext);
+
+ boolean load(PluginProvider provider, T provided);
+
+ default boolean preloadProvider(PluginProvider provider) {
+ return true;
+ }
+
+ default void onGenericError(PluginProvider provider) {
+
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..a79eb9e2c8c42ecf823aecbd859576415e9981dc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
@@ -0,0 +1,22 @@
+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;
+
+/**
+ * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
+ *
+ * Returns providers loaded.
+ *
+ * @param
provider type
+ */
+public interface ProviderLoadingStrategy
{
+
+ List> loadProviders(List> providers, MetaDependencyTree dependencyTree);
+
+ record ProviderPair(PluginProvider
provider, P provided) {
+
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
new file mode 100644
index 0000000000000000000000000000000000000000..52a110044611c8a0ace6d49549e8acc16cbbe83d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
@@ -0,0 +1,58 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.graph.Graph;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+public final class TopographicGraphSorter {
+
+ // Topographically sort dependencies
+ public static List sortGraph(Graph graph) throws PluginGraphCycleException {
+ List sorted = new ArrayList<>();
+ Deque roots = new ArrayDeque<>();
+ Object2IntMap nonRoots = new Object2IntOpenHashMap<>();
+
+ for (N node : graph.nodes()) {
+ // Is a node being referred to by any other nodes?
+ int degree = graph.inDegree(node);
+ if (degree == 0) {
+ // Is a root
+ roots.add(node);
+ } else {
+ // Isn't a root, the number represents how many nodes connect to it.
+ nonRoots.put(node, degree);
+ }
+ }
+
+ // Pick from nodes that aren't referred to anywhere else
+ N next;
+ while ((next = roots.poll()) != null) {
+ for (N successor : graph.successors(next)) {
+ // Traverse through, moving down a degree
+ int newInDegree = nonRoots.removeInt(successor) - 1;
+
+ if (newInDegree == 0) {
+ roots.add(successor);
+ } else {
+ nonRoots.put(successor, newInDegree);
+ }
+
+ }
+ sorted.add(next);
+ }
+
+ if (!nonRoots.isEmpty()) {
+ throw new GraphCycleException();
+ }
+
+ return sorted;
+ }
+
+ public static final class GraphCycleException extends RuntimeException {
+
+ }
+}
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..e3f01ec40a704acb46f7ac31d500e9d0185e3db9
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/LoadOrderTree.java
@@ -0,0 +1,121 @@
+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.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> providerMap;
+ private final MutableGraph graph;
+
+ public LoadOrderTree(Map> providerMapMirror, MutableGraph 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 getLoadOrder() throws PluginGraphCycleException {
+ List reversedTopographicSort;
+ try {
+ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
+ } catch (TopographicGraphSorter.GraphCycleException exception) {
+ List> 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 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> cycles, Map> providerMapMirror) {
+ LOGGER.error("=================================");
+ LOGGER.error("Circular plugin loading detected:");
+ for (int i = 0; i < cycles.size(); i++) {
+ List 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 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..9af388a8e56806ab44f8c3ef4f97086ce38ef3b4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/modern/ModernPluginLoadingStrategy.java
@@ -0,0 +1,138 @@
+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 implements ProviderLoadingStrategy {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+ private final ProviderConfiguration configuration;
+
+ public ModernPluginLoadingStrategy(ProviderConfiguration onLoad) {
+ this.configuration = onLoad;
+ }
+
+ @Override
+ public List> loadProviders(List> pluginProviders, MetaDependencyTree dependencyTree) {
+ Map> providerMap = new HashMap<>();
+ Map> providerMapMirror = Maps.transformValues(providerMap, (entry) -> entry.provider);
+ List> validatedProviders = new ArrayList<>();
+
+ // Populate provider map
+ for (PluginProvider provider : pluginProviders) {
+ PluginMeta providerConfig = provider.getMeta();
+ PluginProviderEntry entry = new PluginProviderEntry<>(provider);
+
+ PluginProviderEntry 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 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 provider : pluginProviders) {
+ PluginMeta configuration = provider.getMeta();
+
+ // Populate missing dependencies to capture if there are multiple missing ones.
+ List 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 reversedTopographicSort = loadOrderTree.getLoadOrder();
+ List> 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>, but we would have to check if each dependency exists there... just
+ // nicer to do it here TBH.
+ PluginProviderEntry 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 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 {
+
+ private final PluginProvider provider;
+ private boolean provided;
+
+ private PluginProviderEntry(PluginProvider 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..f38ecd7f65dc24e4a3f0bc675e3730287ac353f1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
@@ -0,0 +1,64 @@
+package io.papermc.paper.plugin.loader;
+
+import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+import io.papermc.paper.plugin.loader.library.ClassPathLibrary;
+import io.papermc.paper.plugin.loader.library.PaperLibraryStore;
+import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+
+public class PaperClasspathBuilder implements PluginClasspathBuilder {
+
+ private final List libraries = new ArrayList<>();
+
+ private final PluginProviderContext context;
+
+ public PaperClasspathBuilder(PluginProviderContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public @NotNull PluginProviderContext getContext() {
+ return this.context;
+ }
+
+ @Override
+ public @NotNull PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary) {
+ this.libraries.add(classPathLibrary);
+ return this;
+ }
+
+ public PaperPluginClassLoader buildClassLoader(Logger logger, Path source, JarFile jarFile, PaperPluginMeta configuration) {
+ PaperLibraryStore paperLibraryStore = new PaperLibraryStore();
+ for (ClassPathLibrary library : this.libraries) {
+ library.register(paperLibraryStore);
+ }
+
+ List paths = paperLibraryStore.getPaths();
+ URL[] urls = new URL[paths.size()];
+ for (int i = 0; i < paths.size(); i++) {
+ Path path = paperLibraryStore.getPaths().get(i);
+ try {
+ urls[i] = path.toUri().toURL();
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ try {
+ return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader()));
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
new file mode 100644
index 0000000000000000000000000000000000000000..5fcce65009f715d46dd3013f1f92ec8393d66e15
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
@@ -0,0 +1,21 @@
+package io.papermc.paper.plugin.loader.library;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PaperLibraryStore implements LibraryStore {
+
+ private final List paths = new ArrayList<>();
+
+ @Override
+ public void addLibrary(@NotNull Path library) {
+ this.paths.add(library);
+ }
+
+ public List getPaths() {
+ return this.paths;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..aef19b44075a3b2e8696315baa89117dd8ebb513
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java
@@ -0,0 +1,82 @@
+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;
+import org.bukkit.plugin.InvalidDescriptionException;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginLoader;
+import org.bukkit.plugin.RegisteredListener;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.regex.Pattern;
+
+/**
+ * A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation
+ * of papers new plugin system.
+ */
+@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 {
+ try {
+ return PaperPluginManagerImpl.getInstance().loadPlugin(file);
+ } catch (InvalidDescriptionException e) {
+ throw new InvalidPluginException(e);
+ }
+ }
+
+ @Override
+ public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException {
+ 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() {
+ return PATTERNS;
+ }
+
+ @Override
+ public @NotNull Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) {
+ return PaperPluginManagerImpl.getInstance().paperEventManager.createRegisteredListeners(listener, plugin);
+ }
+
+ @Override
+ public void enablePlugin(@NotNull Plugin plugin) {
+ Bukkit.getPluginManager().enablePlugin(plugin);
+ }
+
+ @Override
+ public void disablePlugin(@NotNull Plugin plugin) {
+ Bukkit.getPluginManager().disablePlugin(plugin);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..d681222f355af5c4c26f35aaba484a393aee41c6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
@@ -0,0 +1,61 @@
+package io.papermc.paper.plugin.manager;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.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;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage {
+
+ private static final Logger LOGGER = LogUtils.getClassLogger();
+ private final List provided = new ArrayList<>();
+
+ private final MetaDependencyTree dependencyTree;
+
+ MultiRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
+ this.dependencyTree = dependencyTree;
+ }
+
+ @Override
+ public void register(PluginProvider provider) {
+ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
+ LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager.");
+ return;
+ }
+ super.register(provider);
+ /*
+ Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage,
+ as putting it into the entrypoint handlers doesn't make much sense.
+ */
+ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
+ }
+
+ @Override
+ public void processProvided(PluginProvider provider, JavaPlugin provided) {
+ super.processProvided(provider, provided);
+ this.provided.add(provided);
+ }
+
+ @Override
+ public boolean throwOnCycle() {
+ return false;
+ }
+
+ public List getLoaded() {
+ return this.provided;
+ }
+
+ @Override
+ public MetaDependencyTree createDependencyTree() {
+ 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
index 0000000000000000000000000000000000000000..6f6aaab295018017565ba27d6958a1f5c7b69bc8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
@@ -0,0 +1,43 @@
+package io.papermc.paper.plugin.manager;
+
+import org.bukkit.permissions.Permissible;
+import org.bukkit.permissions.Permission;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+class NormalPaperPermissionManager extends PaperPermissionManager {
+
+ private final Map permissions = new HashMap<>();
+ private final Map> defaultPerms = new LinkedHashMap<>();
+ private final Map> permSubs = new HashMap<>();
+ private final Map> defSubs = new HashMap<>();
+
+ public NormalPaperPermissionManager() {
+ this.defaultPerms().put(true, new LinkedHashSet<>());
+ this.defaultPerms().put(false, new LinkedHashSet<>());
+ }
+
+ @Override
+ public Map permissions() {
+ return this.permissions;
+ }
+
+ @Override
+ public Map> defaultPerms() {
+ return this.defaultPerms;
+ }
+
+ @Override
+ public Map> permSubs() {
+ return this.permSubs;
+ }
+
+ @Override
+ public Map> defSubs() {
+ return this.defSubs;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
@@ -0,0 +1,194 @@
+package io.papermc.paper.plugin.manager;
+
+import co.aikar.timings.TimedEventExecutor;
+import com.destroystokyo.paper.event.server.ServerExceptionEvent;
+import com.destroystokyo.paper.exception.ServerEventException;
+import com.google.common.collect.Sets;
+import org.bukkit.Server;
+import org.bukkit.Warning;
+import org.bukkit.event.Event;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.Listener;
+import org.bukkit.plugin.AuthorNagException;
+import org.bukkit.plugin.EventExecutor;
+import org.bukkit.plugin.IllegalPluginAccessException;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.RegisteredListener;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+
+class PaperEventManager {
+
+ private final Server server;
+
+ public PaperEventManager(Server server) {
+ this.server = server;
+ }
+
+ // SimplePluginManager
+ public void callEvent(@NotNull Event event) {
+ if (event.isAsynchronous() && this.server.isPrimaryThread()) {
+ throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
+ } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) {
+ throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
+ }
+
+ HandlerList handlers = event.getHandlers();
+ RegisteredListener[] listeners = handlers.getRegisteredListeners();
+
+ for (RegisteredListener registration : listeners) {
+ if (!registration.getPlugin().isEnabled()) {
+ continue;
+ }
+
+ try {
+ registration.callEvent(event);
+ } catch (AuthorNagException ex) {
+ Plugin plugin = registration.getPlugin();
+
+ if (plugin.isNaggable()) {
+ plugin.setNaggable(false);
+
+ this.server.getLogger().log(Level.SEVERE, String.format(
+ "Nag author(s): '%s' of '%s' about the following: %s",
+ plugin.getPluginMeta().getAuthors(),
+ plugin.getPluginMeta().getDisplayName(),
+ ex.getMessage()
+ ));
+ }
+ } catch (Throwable ex) {
+ String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getPluginMeta().getDisplayName();
+ this.server.getLogger().log(Level.SEVERE, msg, ex);
+ if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop
+ this.callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event)));
+ }
+ }
+ }
+ }
+
+ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
+ if (!plugin.isEnabled()) {
+ throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled");
+ }
+
+ for (Map.Entry, Set> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
+ this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
+ }
+
+ }
+
+ public void registerEvent(@NotNull Class extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
+ this.registerEvent(event, listener, priority, executor, plugin, false);
+ }
+
+ public void registerEvent(@NotNull Class extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
+ if (!plugin.isEnabled()) {
+ throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
+ }
+
+ executor = new TimedEventExecutor(executor, plugin, null, event);
+ this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
+ }
+
+ @NotNull
+ private HandlerList getEventListeners(@NotNull Class extends Event> type) {
+ try {
+ Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList");
+ method.setAccessible(true);
+ return (HandlerList) method.invoke(null);
+ } catch (Exception e) {
+ throw new IllegalPluginAccessException(e.toString());
+ }
+ }
+
+ @NotNull
+ private Class extends Event> getRegistrationClass(@NotNull Class extends Event> clazz) {
+ try {
+ clazz.getDeclaredMethod("getHandlerList");
+ return clazz;
+ } catch (NoSuchMethodException e) {
+ if (clazz.getSuperclass() != null
+ && !clazz.getSuperclass().equals(Event.class)
+ && Event.class.isAssignableFrom(clazz.getSuperclass())) {
+ return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
+ } else {
+ throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!");
+ }
+ }
+ }
+
+ // JavaPluginLoader
+ @NotNull
+ public Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) {
+ Map, Set> ret = new HashMap<>();
+
+ Set methods;
+ try {
+ Class> listenerClazz = listener.getClass();
+ methods = Sets.union(
+ Set.of(listenerClazz.getMethods()),
+ Set.of(listenerClazz.getDeclaredMethods())
+ );
+ } catch (NoClassDefFoundError e) {
+ plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
+ return ret;
+ }
+
+ for (final Method method : methods) {
+ final EventHandler eh = method.getAnnotation(EventHandler.class);
+ if (eh == null) continue;
+ // Do not register bridge or synthetic methods to avoid event duplication
+ // Fixes SPIGOT-893
+ if (method.isBridge() || method.isSynthetic()) {
+ continue;
+ }
+ final Class> checkClass;
+ if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
+ plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
+ continue;
+ }
+ final Class extends Event> eventClass = checkClass.asSubclass(Event.class);
+ method.setAccessible(true);
+ Set eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>());
+
+ for (Class> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
+ // This loop checks for extending deprecated events
+ if (clazz.getAnnotation(Deprecated.class) != null) {
+ Warning warning = clazz.getAnnotation(Warning.class);
+ Warning.WarningState warningState = this.server.getWarningState();
+ if (!warningState.printFor(warning)) {
+ break;
+ }
+ plugin.getLogger().log(
+ Level.WARNING,
+ String.format(
+ "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.",
+ plugin.getPluginMeta().getDisplayName(),
+ clazz.getName(),
+ method.toGenericString(),
+ (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
+ Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())),
+ warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null);
+ break;
+ }
+ }
+
+ EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass);
+ eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
+ }
+ return ret;
+ }
+
+ public void clearEvents() {
+ HandlerList.unregisterAll();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..92a69677f21b2c1c035119d8e5a6af63fa19b801
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
@@ -0,0 +1,201 @@
+package io.papermc.paper.plugin.manager;
+
+import com.google.common.collect.ImmutableSet;
+import io.papermc.paper.plugin.PermissionManager;
+import org.bukkit.permissions.Permissible;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * See
+ * {@link StupidSPMPermissionManagerWrapper}
+ */
+abstract class PaperPermissionManager implements PermissionManager {
+
+ public abstract Map permissions();
+
+ public abstract Map> defaultPerms();
+
+ public abstract Map> permSubs();
+
+ public abstract Map> defSubs();
+
+ @Override
+ @Nullable
+ public Permission getPermission(@NotNull String name) {
+ return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH));
+ }
+
+ @Override
+ public void addPermission(@NotNull Permission perm) {
+ this.addPermission(perm, true);
+ }
+
+ @Override
+ public void addPermissions(@NotNull List permissions) {
+ for (Permission permission : permissions) {
+ this.addPermission(permission, false);
+ }
+ this.dirtyPermissibles();
+ }
+
+ // Allow suppressing permission default calculations
+ private void addPermission(@NotNull Permission perm, boolean dirty) {
+ String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH);
+
+ if (this.permissions().containsKey(name)) {
+ throw new IllegalArgumentException("The permission " + name + " is already defined!");
+ }
+
+ this.permissions().put(name, perm);
+ this.calculatePermissionDefault(perm, dirty);
+ }
+
+ @Override
+ @NotNull
+ public Set getDefaultPermissions(boolean op) {
+ return ImmutableSet.copyOf(this.defaultPerms().get(op));
+ }
+
+
+ @Override
+ public void removePermission(@NotNull Permission perm) {
+ this.removePermission(perm.getName());
+ }
+
+
+ @Override
+ public void removePermission(@NotNull String name) {
+ this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH));
+ }
+
+ @Override
+ public void recalculatePermissionDefaults(@NotNull Permission perm) {
+ // we need a null check here because some plugins for some unknown reason pass null into this?
+ if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ENGLISH))) {
+ this.defaultPerms().get(true).remove(perm);
+ this.defaultPerms().get(false).remove(perm);
+
+ this.calculatePermissionDefault(perm, true);
+ }
+ }
+
+ private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) {
+ if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
+ this.defaultPerms().get(true).add(perm);
+ if (dirty) {
+ this.dirtyPermissibles(true);
+ }
+ }
+ if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
+ this.defaultPerms().get(false).add(perm);
+ if (dirty) {
+ this.dirtyPermissibles(false);
+ }
+ }
+ }
+
+
+ @Override
+ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+ Map map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>());
+
+ map.put(permissible, true);
+ }
+
+ @Override
+ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+ Map map = this.permSubs().get(name);
+
+ if (map != null) {
+ map.remove(permissible);
+
+ if (map.isEmpty()) {
+ this.permSubs().remove(name);
+ }
+ }
+ }
+
+ @Override
+ @NotNull
+ public Set getPermissionSubscriptions(@NotNull String permission) {
+ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+ Map map = this.permSubs().get(name);
+
+ if (map == null) {
+ return ImmutableSet.of();
+ } else {
+ return ImmutableSet.copyOf(map.keySet());
+ }
+ }
+
+ @Override
+ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ Map map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>());
+
+ map.put(permissible, true);
+ }
+
+ @Override
+ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ Map map = this.defSubs().get(op);
+
+ if (map != null) {
+ map.remove(permissible);
+
+ if (map.isEmpty()) {
+ this.defSubs().remove(op);
+ }
+ }
+ }
+
+ @Override
+ @NotNull
+ public Set getDefaultPermSubscriptions(boolean op) {
+ Map map = this.defSubs().get(op);
+
+ if (map == null) {
+ return ImmutableSet.of();
+ } else {
+ return ImmutableSet.copyOf(map.keySet());
+ }
+ }
+
+ @Override
+ @NotNull
+ public Set getPermissions() {
+ return new HashSet<>(this.permissions().values());
+ }
+
+ @Override
+ public void clearPermissions() {
+ this.permissions().clear();
+ this.defaultPerms().get(true).clear();
+ this.defaultPerms().get(false).clear();
+ }
+
+
+ void dirtyPermissibles(boolean op) {
+ Set permissibles = this.getDefaultPermSubscriptions(op);
+
+ for (Permissible p : permissibles) {
+ p.recalculatePermissions();
+ }
+ }
+
+ void dirtyPermissibles() {
+ this.dirtyPermissibles(true);
+ this.dirtyPermissibles(false);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..08b1aab5d37a56dc42542ce15ba1f7ccd1b08400
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
@@ -0,0 +1,302 @@
+package io.papermc.paper.plugin.manager;
+
+import com.google.common.base.Preconditions;
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
+import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
+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.source.DirectoryProviderSource;
+import io.papermc.paper.plugin.provider.source.FileProviderSource;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.World;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandMap;
+import org.bukkit.command.PluginCommandYamlParser;
+import org.bukkit.craftbukkit.util.CraftMagicNumbers;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.server.PluginDisableEvent;
+import org.bukkit.event.server.PluginEnableEvent;
+import org.bukkit.plugin.InvalidDescriptionException;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+@SuppressWarnings("UnstableApiUsage")
+class PaperPluginInstanceManager {
+
+ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted);
+ private static final DirectoryProviderSource DIRECTORY_PROVIDER_SOURCE = new DirectoryProviderSource();
+
+ private final List plugins = new ArrayList<>();
+ private final Map lookupNames = new HashMap<>();
+
+ private final PluginManager pluginManager;
+ private final CommandMap commandMap;
+ private final Server server;
+
+ private final MetaDependencyTree dependencyTree = new SimpleMetaDependencyTree(GraphBuilder.directed().build());
+
+ public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
+ this.commandMap = commandMap;
+ this.server = server;
+ this.pluginManager = pluginManager;
+ }
+
+ public @Nullable Plugin getPlugin(@NotNull String name) {
+ return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
+ }
+
+ public @NotNull Plugin[] getPlugins() {
+ return this.plugins.toArray(new Plugin[0]);
+ }
+
+ public boolean isPluginEnabled(@NotNull String name) {
+ Plugin plugin = this.getPlugin(name);
+
+ return this.isPluginEnabled(plugin);
+ }
+
+ public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) {
+ if ((plugin != null) && (this.plugins.contains(plugin))) {
+ return plugin.isEnabled();
+ } else {
+ return false;
+ }
+ }
+
+ public void loadPlugin(Plugin provided) {
+ PluginMeta configuration = provided.getPluginMeta();
+
+ this.plugins.add(provided);
+ this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided);
+ for (String providedPlugin : configuration.getProvidedPlugins()) {
+ this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
+ }
+
+ 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 runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage(this.dependencyTree));
+
+ try {
+ FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
+ } catch (IllegalArgumentException exception) {
+ return null; // Return null when the plugin file is not valid / plugin type is unknown
+ } catch (PluginGraphCycleException exception) {
+ throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!");
+ } catch (SerializationException |
+ InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception
+ throw new InvalidPluginException(ex);
+ } catch (Exception e) {
+ throw new InvalidPluginException(e);
+ }
+
+ try {
+ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
+ } catch (Throwable e) {
+ throw new InvalidPluginException(e);
+ }
+
+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded()
+ .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?"));
+ }
+
+ // The behavior of this is that all errors are logged instead of being thrown
+ public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
+ Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
+
+ RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree));
+ try {
+ DIRECTORY_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, directory);
+ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
+ } catch (Exception e) {
+ // This should never happen, any errors that occur in this provider should instead be logged.
+ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
+ }
+
+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
+ }
+
+ // Plugins are disabled in order like this inorder to "rougly" prevent
+ // their dependencies unloading first. But, eh.
+ public void disablePlugins() {
+ Plugin[] plugins = this.getPlugins();
+ for (int i = plugins.length - 1; i >= 0; i--) {
+ this.disablePlugin(plugins[i]);
+ }
+ }
+
+ public void clearPlugins() {
+ synchronized (this) {
+ this.disablePlugins();
+ this.plugins.clear();
+ this.lookupNames.clear();
+ }
+ }
+
+ public synchronized void enablePlugin(@NotNull Plugin plugin) {
+ if (plugin.isEnabled()) {
+ return;
+ }
+
+ if (plugin.getPluginMeta() instanceof PluginDescriptionFile) {
+ List bukkitCommands = PluginCommandYamlParser.parse(plugin);
+
+ if (!bukkitCommands.isEmpty()) {
+ this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands);
+ }
+ }
+
+ try {
+ String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName();
+ if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) {
+ enableMsg += "*";
+ }
+ plugin.getLogger().info(enableMsg);
+
+ JavaPlugin jPlugin = (JavaPlugin) plugin;
+
+ if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper
+ if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) {
+ this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName());
+ }
+ } // Paper
+
+ try {
+ jPlugin.setEnabled(true);
+ } catch (Throwable ex) {
+ this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex);
+ // Paper start - Disable plugins that fail to load
+ this.server.getPluginManager().disablePlugin(jPlugin);
+ return;
+ // Paper end
+ }
+
+ // Perhaps abort here, rather than continue going, but as it stands,
+ // an abort is not possible the way it's currently written
+ this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while enabling "
+ + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin);
+ }
+
+ HandlerList.bakeAll();
+ }
+
+ public synchronized void disablePlugin(@NotNull Plugin plugin) {
+ if (!(plugin instanceof JavaPlugin javaPlugin)) {
+ throw new IllegalArgumentException("Only expects java plugins.");
+ }
+ if (!plugin.isEnabled()) {
+ return;
+ }
+
+ String pluginName = plugin.getPluginMeta().getDisplayName();
+
+ try {
+ plugin.getLogger().info("Disabling %s".formatted(pluginName));
+
+ this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
+
+ javaPlugin.setEnabled(false);
+
+ ClassLoader classLoader = plugin.getClass().getClassLoader();
+ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ try {
+ configuredPluginClassLoader.close();
+ } catch (IOException ex) {
+ this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception
+ }
+ // Remove from the classloader pool inorder to prevent plugins from trying
+ // to access classes
+ PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader);
+ }
+
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while disabling "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getScheduler().cancelTasks(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getServicesManager().unregisterAll(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ HandlerList.unregisterAll(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getMessenger().unregisterIncomingPluginChannel(plugin);
+ this.server.getMessenger().unregisterOutgoingPluginChannel(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ for (World world : this.server.getWorlds()) {
+ world.removePluginChunkTickets(plugin);
+ }
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ }
+
+ // TODO: Implement event part in future patch (paper patch move up, this patch is lower)
+ private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
+ Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex);
+ this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
+ }
+
+ public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
+ return this.dependencyTree.isTransitiveDependency(plugin, depend);
+ }
+
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.getPlugin(pluginIdentifier) != null;
+ }
+
+ // Debug only
+ @ApiStatus.Internal
+ public MutableGraph getDependencyGraph() {
+ 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
new file mode 100644
index 0000000000000000000000000000000000000000..dab211c458311869c61779305580a1c7da830f71
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
@@ -0,0 +1,241 @@
+package io.papermc.paper.plugin.manager;
+
+import com.google.common.graph.MutableGraph;
+import io.papermc.paper.plugin.PermissionManager;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.command.CommandMap;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.event.Event;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.permissions.Permissible;
+import org.bukkit.permissions.Permission;
+import org.bukkit.plugin.EventExecutor;
+import org.bukkit.plugin.InvalidDescriptionException;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginLoader;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.plugin.SimplePluginManager;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+public class PaperPluginManagerImpl implements PluginManager, DependencyContext {
+
+ final PaperPluginInstanceManager instanceManager;
+ final PaperEventManager paperEventManager;
+ PermissionManager permissionManager;
+
+ public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) {
+ this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server);
+ this.paperEventManager = new PaperEventManager(server);
+
+ if (permissionManager == null) {
+ this.permissionManager = new NormalPaperPermissionManager();
+ } else {
+ this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed
+ }
+ }
+
+ // REMOVE THIS WHEN SimplePluginManager is removed.
+ // Just cast and use Bukkit.getServer().getPluginManager()
+ public static PaperPluginManagerImpl getInstance() {
+ return ((CraftServer) (Bukkit.getServer())).paperPluginManager;
+ }
+
+ // Plugin Manipulation
+
+ @Override
+ public @Nullable Plugin getPlugin(@NotNull String name) {
+ return this.instanceManager.getPlugin(name);
+ }
+
+ @Override
+ public @NotNull Plugin[] getPlugins() {
+ return this.instanceManager.getPlugins();
+ }
+
+ @Override
+ public boolean isPluginEnabled(@NotNull String name) {
+ return this.instanceManager.isPluginEnabled(name);
+ }
+
+ @Override
+ public boolean isPluginEnabled(@Nullable Plugin plugin) {
+ return this.instanceManager.isPluginEnabled(plugin);
+ }
+
+ public void loadPlugin(Plugin plugin) {
+ this.instanceManager.loadPlugin(plugin);
+ }
+
+ @Override
+ public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException {
+ return this.instanceManager.loadPlugin(file.toPath());
+ }
+
+ @Override
+ public @NotNull Plugin[] loadPlugins(@NotNull File directory) {
+ return this.instanceManager.loadPlugins(directory.toPath());
+ }
+
+ @Override
+ public void disablePlugins() {
+ this.instanceManager.disablePlugins();
+ }
+
+ @Override
+ public synchronized void clearPlugins() {
+ this.instanceManager.clearPlugins();
+ this.permissionManager.clearPermissions();
+ this.paperEventManager.clearEvents();
+ }
+
+ @Override
+ public void enablePlugin(@NotNull Plugin plugin) {
+ this.instanceManager.enablePlugin(plugin);
+ }
+
+ @Override
+ public void disablePlugin(@NotNull Plugin plugin) {
+ this.instanceManager.disablePlugin(plugin);
+ }
+
+ @Override
+ public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) {
+ return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig);
+ }
+
+ @Override
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.instanceManager.hasDependency(pluginIdentifier);
+ }
+
+ // Event manipulation
+
+ @Override
+ public void callEvent(@NotNull Event event) throws IllegalStateException {
+ this.paperEventManager.callEvent(event);
+ }
+
+ @Override
+ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
+ this.paperEventManager.registerEvents(listener, plugin);
+ }
+
+ @Override
+ public void registerEvent(@NotNull Class extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
+ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin);
+ }
+
+ @Override
+ public void registerEvent(@NotNull Class extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
+ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled);
+ }
+
+ // Permission manipulation
+
+ @Override
+ public @Nullable Permission getPermission(@NotNull String name) {
+ return this.permissionManager.getPermission(name);
+ }
+
+ @Override
+ public void addPermission(@NotNull Permission perm) {
+ this.permissionManager.addPermission(perm);
+ }
+
+ @Override
+ public void removePermission(@NotNull Permission perm) {
+ this.permissionManager.removePermission(perm);
+ }
+
+ @Override
+ public void removePermission(@NotNull String name) {
+ this.permissionManager.removePermission(name);
+ }
+
+ @Override
+ public @NotNull Set getDefaultPermissions(boolean op) {
+ return this.permissionManager.getDefaultPermissions(op);
+ }
+
+ @Override
+ public void recalculatePermissionDefaults(@NotNull Permission perm) {
+ this.permissionManager.recalculatePermissionDefaults(perm);
+ }
+
+ @Override
+ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ this.permissionManager.subscribeToPermission(permission, permissible);
+ }
+
+ @Override
+ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ this.permissionManager.unsubscribeFromPermission(permission, permissible);
+ }
+
+ @Override
+ public @NotNull Set getPermissionSubscriptions(@NotNull String permission) {
+ return this.permissionManager.getPermissionSubscriptions(permission);
+ }
+
+ @Override
+ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ this.permissionManager.subscribeToDefaultPerms(op, permissible);
+ }
+
+ @Override
+ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ this.permissionManager.unsubscribeFromDefaultPerms(op, permissible);
+ }
+
+ @Override
+ public @NotNull Set getDefaultPermSubscriptions(boolean op) {
+ return this.permissionManager.getDefaultPermSubscriptions(op);
+ }
+
+ @Override
+ public @NotNull Set getPermissions() {
+ return this.permissionManager.getPermissions();
+ }
+
+ @Override
+ public void addPermissions(@NotNull List perm) {
+ this.permissionManager.addPermissions(perm);
+ }
+
+ @Override
+ public void clearPermissions() {
+ this.permissionManager.clearPermissions();
+ }
+
+ @Override
+ public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) {
+ this.permissionManager = permissionManager;
+ }
+
+ // Etc
+
+ @Override
+ public boolean useTimings() {
+ return co.aikar.timings.Timings.isTimingsEnabled();
+ }
+
+ @Override
+ public void registerInterface(@NotNull Class extends PluginLoader> loader) throws IllegalArgumentException {
+ throw new UnsupportedOperationException();
+ }
+
+ public MutableGraph getInstanceManagerGraph() {
+ return instanceManager.getDependencyGraph();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d50d1d312388e979c0e1cd53a6bf5977ca6e549
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
@@ -0,0 +1,47 @@
+package io.papermc.paper.plugin.manager;
+
+import com.destroystokyo.paper.util.SneakyThrow;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.storage.ProviderStorage;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Used for loading plugins during runtime, only supporting providers that are plugins.
+ * This is only used for the plugin manager, as it only allows plugins to be
+ * registered to a provider storage.
+ */
+class RuntimePluginEntrypointHandler> implements EntrypointHandler {
+
+ private final T providerStorage;
+
+ RuntimePluginEntrypointHandler(T providerStorage) {
+ this.providerStorage = providerStorage;
+ }
+
+ @Override
+ public void register(Entrypoint entrypoint, PluginProvider provider) {
+ if (!entrypoint.equals(Entrypoint.PLUGIN)) {
+ SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint)));
+ // We have to throw an invalid plugin exception for legacy reasons
+ }
+
+ this.providerStorage.register((PluginProvider) provider);
+ }
+
+ @Override
+ public void enter(Entrypoint> entrypoint) {
+ if (entrypoint != Entrypoint.PLUGIN) {
+ throw new IllegalArgumentException("Only plugin entrypoint supported");
+ }
+ this.providerStorage.enter();
+ }
+
+ @NotNull
+ public T getPluginProviderStorage() {
+ return this.providerStorage;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..b0e723bcda9b1fc01e6aa5e53e57c09ea4f1a1c8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
@@ -0,0 +1,79 @@
+package io.papermc.paper.plugin.manager;
+
+import com.destroystokyo.paper.util.SneakyThrow;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.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;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Used for registering a single plugin provider.
+ * This has special behavior in that some errors are thrown instead of logged.
+ */
+class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage {
+
+ private final MetaDependencyTree dependencyTree;
+ private PluginProvider lastProvider;
+ private JavaPlugin singleLoaded;
+
+ SingularRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
+ this.dependencyTree = dependencyTree;
+ }
+
+ @Override
+ public void register(PluginProvider provider) {
+ super.register(provider);
+ if (this.lastProvider != null) {
+ SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins"));
+ }
+ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
+ throw new IllegalStateException("Cannot register paper plugins during runtime!");
+ }
+ this.lastProvider = provider;
+ // Register the provider into the server entrypoint, this allows it to show in /plugins correctly.
+ // Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense.
+ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
+ }
+
+ @Override
+ public void enter() {
+ PluginProvider