reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier);
+ if (reachableNodes.contains(depend.getName())) {
+ return true;
+ }
+ for (String provided : depend.getProvidedPlugins()) {
+ if (reachableNodes.contains(provided)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.dependencyGraph.nodes().contains(pluginIdentifier);
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
new file mode 100644
index 0000000000000000000000000000000000000000..22189a1c42459c00d3e8bdeb980d15a69b720805
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
@@ -0,0 +1,350 @@
+/*
+ * (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors.
+ *
+ * JGraphT : a free Java graph-theory library
+ *
+ * See the CONTRIBUTORS.md file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the
+ * GNU Lesser General Public License v2.1 or later
+ * which is available at
+ * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
+ */
+
+// MODIFICATIONS:
+// - Modified to use a guava graph directly
+
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.base.Preconditions;
+import com.google.common.graph.Graph;
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import com.mojang.datafixers.util.Pair;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+/**
+ * Find all simple cycles of a directed graph using the Johnson's algorithm.
+ *
+ *
+ * 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 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> findSimpleCycles()
+ {
+ List> result = new ArrayList<>();
+ findSimpleCycles(result::add);
+ return result;
+ }
+
+ /**
+ * Find the simple cycles of the graph.
+ *
+ * @param consumer Consumer that will be called with each cycle found.
+ */
+ public void findSimpleCycles(Consumer> consumer)
+ {
+ if (graph == null) {
+ throw new IllegalArgumentException("Null graph.");
+ }
+ 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);
+ foundCycle = true;
+ } else if (!blocked.contains(successor)) {
+ boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg);
+ foundCycle = foundCycle || gotCycle;
+ }
+ }
+ if (foundCycle) {
+ unblock(vertex);
+ } else {
+ for (V w : scg.successors(vertex)) {
+ Set 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..c337852883f38f6c2d8d5afbed0c848b047701d9
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
@@ -0,0 +1,260 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import org.bukkit.plugin.UnknownDependencyException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+@SuppressWarnings("UnstableApiUsage")
+public class LegacyPluginLoadingStrategy 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) {
+ List> javapluginsLoaded = new ArrayList<>();
+ MutableGraph dependencyGraph = GraphBuilder.directed().build();
+ GraphDependencyContext dependencyContext = new GraphDependencyContext(dependencyGraph);
+
+ 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);
+ 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());
+ if (replacedPlugin != null) {
+ LOGGER.warning(String.format(
+ "`%s' is provided by both `%s' and `%s'",
+ provided,
+ configuration.getName(),
+ replacedPlugin
+ ));
+ }
+ }
+ }
+
+ Collection softDependencySet = this.configuration.optionalDependencies(provider);
+ if (softDependencySet != null && !softDependencySet.isEmpty()) {
+ if (softDependencies.containsKey(configuration.getName())) {
+ // Duplicates do not matter, they will be removed together if applicable
+ softDependencies.get(configuration.getName()).addAll(softDependencySet);
+ } else {
+ softDependencies.put(configuration.getName(), new LinkedList(softDependencySet));
+ }
+
+ for (String depend : softDependencySet) {
+ dependencyGraph.putEdge(configuration.getName(), depend);
+ }
+ }
+
+ Collection dependencySet = this.configuration.requiredDependencies(provider);
+ if (dependencySet != null && !dependencySet.isEmpty()) {
+ dependencies.put(configuration.getName(), new LinkedList(dependencySet));
+
+ for (String depend : dependencySet) {
+ dependencyGraph.putEdge(configuration.getName(), depend);
+ }
+ }
+
+ Collection loadBeforeSet = this.configuration.loadBeforeDependencies(provider);
+ if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) {
+ for (String loadBeforeTarget : loadBeforeSet) {
+ if (softDependencies.containsKey(loadBeforeTarget)) {
+ softDependencies.get(loadBeforeTarget).add(configuration.getName());
+ } else {
+ // softDependencies is never iterated, so 'ghost' plugins aren't an issue
+ Collection 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, dependencyContext);
+ T loadedPlugin = file.createInstance();
+
+ if (this.configuration.load(file, loadedPlugin)) {
+ loadedPlugins.add(file.getMeta().getName());
+ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
+ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
+ }
+
+ } catch (Exception ex) {
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
+ }
+ }
+ }
+
+ if (missingDependency) {
+ // We now iterate over plugins until something loads
+ // This loop will ignore soft dependencies
+ providerIterator = providersToLoad.entrySet().iterator();
+
+ while (providerIterator.hasNext()) {
+ Map.Entry> 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, dependencyContext);
+ T loadedPlugin = file.createInstance();
+
+ if (this.configuration.load(file, loadedPlugin)) {
+ loadedPlugins.add(file.getMeta().getName());
+ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
+ javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
+ }
+ break;
+ } catch (Exception ex) {
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
+ }
+ }
+ }
+ // We have no plugins left without a depend
+ if (missingDependency) {
+ softDependencies.clear();
+ dependencies.clear();
+ Iterator> 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;
+ }
+}
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..30e56289cccc09456b3421f7960d8647b6610639
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java
@@ -0,0 +1,143 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.collect.Lists;
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
+import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("UnstableApiUsage")
+public class ModernPluginLoadingStrategy implements ProviderLoadingStrategy {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final ProviderConfiguration configuration;
+
+ public ModernPluginLoadingStrategy(ProviderConfiguration onLoad) {
+ this.configuration = onLoad;
+ }
+
+ @Override
+ public List> loadProviders(List> pluginProviders) {
+ MutableGraph dependencyGraph = GraphBuilder.directed().build();
+ Map> providerMap = new HashMap<>();
+ 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()
+ ));
+ }
+
+ 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()
+ ));
+ }
+ }
+ }
+
+ // 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 = new ArrayList<>();
+ for (String hardDependency : this.configuration.requiredDependencies(provider)) {
+ if (!providerMap.containsKey(hardDependency)) {
+ missingDependencies.add(hardDependency);
+ }
+ }
+
+ if (missingDependencies.isEmpty()) {
+ validatedProviders.add(provider);
+ } else {
+ LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
+ // Because the validator is invalid, remove it from the provider map
+ providerMap.remove(configuration.getName());
+ }
+ }
+
+ for (PluginProvider> validated : validatedProviders) {
+ PluginMeta configuration = validated.getMeta();
+
+ // Build a validated provider's dependencies into the graph
+ DependencyUtil.buildDependencyGraph(dependencyGraph, configuration);
+
+ // Add the provided plugins to the graph as well
+ for (String provides : configuration.getProvidedPlugins()) {
+ DependencyUtil.addProvidedPlugin(dependencyGraph, configuration.getName(), provides);
+ }
+ }
+
+ // Reverse the topographic search to let us see which providers we can load first.
+ List reversedTopographicSort;
+ try {
+ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(dependencyGraph));
+ } catch (TopographicGraphSorter.GraphCycleException exception) {
+ throw new PluginGraphCycleException(new JohnsonSimpleCycles<>(dependencyGraph).findSimpleCycles());
+ }
+
+ GraphDependencyContext graphDependencyContext = new GraphDependencyContext(dependencyGraph);
+ List> 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, graphDependencyContext);
+
+ T instance = retrievedProvider.createInstance();
+ if (this.configuration.load(retrievedProvider, instance)) {
+ loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance));
+ }
+ } catch (Exception ex) {
+ LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
+ }
+ }
+
+ return loadedPlugins;
+ }
+
+ private static class PluginProviderEntry {
+
+ private final PluginProvider provider;
+ private boolean provided;
+
+ private PluginProviderEntry(PluginProvider 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..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..5e18616160014d70df2b539d7e65bb003ac7a4b7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
@@ -0,0 +1,26 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+
+import java.util.List;
+
+/**
+ * Used to share code with the modern and legacy plugin load strategy.
+ *
+ * @param
+ */
+public interface ProviderConfiguration {
+
+ void applyContext(PluginProvider provider, DependencyContext dependencyContext);
+
+ boolean load(PluginProvider provider, T provided);
+
+ List requiredDependencies(PluginProvider provider);
+
+ List optionalDependencies(PluginProvider provider);
+
+ List loadBeforeDependencies(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..dee83e821dcc9baf3a3e5ca8325b03ed2d5eb81c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
@@ -0,0 +1,20 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+
+import java.util.List;
+
+/**
+ * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
+ *
+ * Returns providers loaded.
+ * @param
provider type
+ */
+public interface ProviderLoadingStrategy
{
+
+ List> loadProviders(List> providers);
+
+ 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..0720af0d48b39ca46e7d3aba08d7b359ed053461
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
@@ -0,0 +1,61 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import com.google.common.graph.Graph;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class TopographicGraphSorter {
+
+ // Topographically sort dependencies
+ public static List sortGraph(Graph graph) throws PluginGraphCycleException {
+ List sorted = new ArrayList<>();
+ Deque roots = new ArrayDeque<>();
+ Map nonRoots = new HashMap<>();
+
+ for (N node : graph.nodes()) {
+ // Is a node being referred to by any other nodes?
+ int degree = graph.inDegree(node);
+ if (degree == 0) {
+ // Is a root
+ roots.add(node);
+ } else {
+ // Isn't a root, the number represents how many nodes connect to it.
+ nonRoots.put(node, degree);
+ }
+ }
+
+ // Pick from nodes that aren't referred to anywhere else
+ while (!roots.isEmpty()) {
+ N next = roots.remove();
+
+ for (N successor : graph.successors(next)) {
+ // Traverse through, moving down a degree
+ int newInDegree = nonRoots.get(successor) - 1;
+
+ if (newInDegree == 0) {
+ nonRoots.remove(successor);
+ roots.add(successor);
+ } else {
+ nonRoots.put(successor, newInDegree);
+ }
+
+ }
+ sorted.add(next);
+ }
+
+ if (!nonRoots.isEmpty()) {
+ throw new GraphCycleException();
+ }
+
+ return sorted;
+ }
+
+ public static class GraphCycleException extends RuntimeException {
+
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..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..ea37ace14849ef4589a4f97287e6dcd64351370f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java
@@ -0,0 +1,57 @@
+package io.papermc.paper.plugin.manager;
+
+import org.bukkit.Bukkit;
+import org.bukkit.event.Event;
+import org.bukkit.event.Listener;
+import org.bukkit.plugin.InvalidDescriptionException;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginLoader;
+import org.bukkit.plugin.RegisteredListener;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation
+ * of papers new plugin system.
+ */
+@ApiStatus.Internal
+public class DummyBukkitPluginLoader implements PluginLoader {
+
+ @Override
+ public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public @NotNull Pattern[] getPluginFileFilters() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public @NotNull Map, 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..b532e35505d5fffd4c525109f273012e2652f777
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
@@ -0,0 +1,49 @@
+package io.papermc.paper.plugin.manager;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
+import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final List provided = new ArrayList<>();
+
+ @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 exitOnCycleDependencies() {
+ return false;
+ }
+
+ public List getLoaded() {
+ return this.provided;
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..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..c0e896343c22badd97c774c4ed1daa4e274f5d44
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
@@ -0,0 +1,304 @@
+package io.papermc.paper.plugin.manager;
+
+import com.google.common.base.Preconditions;
+import com.google.common.graph.GraphBuilder;
+import com.google.common.graph.MutableGraph;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil;
+import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
+import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.provider.source.DirectoryProviderSource;
+import io.papermc.paper.plugin.provider.source.FileProviderSource;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.World;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandMap;
+import org.bukkit.command.PluginCommandYamlParser;
+import org.bukkit.craftbukkit.util.CraftMagicNumbers;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.server.PluginDisableEvent;
+import org.bukkit.event.server.PluginEnableEvent;
+import org.bukkit.plugin.InvalidDescriptionException;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+@SuppressWarnings("UnstableApiUsage")
+class PaperPluginInstanceManager {
+
+ private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted);
+ private static final DirectoryProviderSource DIRECTORY_PROVIDER_SOURCE = new DirectoryProviderSource();
+
+ private final List plugins = new ArrayList<>();
+ private final Map lookupNames = new HashMap<>();
+
+ private final PluginManager pluginManager;
+ private final CommandMap commandMap;
+ private final Server server;
+
+ private final MutableGraph dependencyGraph = GraphBuilder.directed().build();
+ private final DependencyContext context = new GraphDependencyContext(this.dependencyGraph);
+
+ public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
+ this.commandMap = commandMap;
+ this.server = server;
+ this.pluginManager = pluginManager;
+ }
+
+ public @Nullable Plugin getPlugin(@NotNull String name) {
+ return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
+ }
+
+ public @NotNull Plugin[] getPlugins() {
+ return this.plugins.toArray(new Plugin[0]);
+ }
+
+ public boolean isPluginEnabled(@NotNull String name) {
+ Plugin plugin = this.getPlugin(name);
+
+ return this.isPluginEnabled(plugin);
+ }
+
+ public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) {
+ if ((plugin != null) && (this.plugins.contains(plugin))) {
+ return plugin.isEnabled();
+ } else {
+ return false;
+ }
+ }
+
+ public void loadPlugin(Plugin provided) {
+ PluginMeta configuration = provided.getPluginMeta();
+
+ this.plugins.add(provided);
+ this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided);
+ for (String providedPlugin : configuration.getProvidedPlugins()) {
+ this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
+ }
+
+ DependencyUtil.buildDependencyGraph(this.dependencyGraph, configuration);
+ }
+
+ // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception.
+ public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException {
+ RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage());
+
+ try {
+ FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
+ } catch (IllegalArgumentException exception) {
+ return null; // Return null when the plugin file is not valid / plugin type is unknown
+ } catch (PluginGraphCycleException exception) {
+ throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!");
+ } catch (SerializationException |
+ InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception
+ throw new InvalidPluginException(ex);
+ } catch (Exception e) {
+ throw new InvalidPluginException(e);
+ }
+
+ try {
+ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
+ } catch (Throwable e) {
+ throw new InvalidPluginException(e);
+ }
+
+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded()
+ .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?"));
+ }
+
+ // The behavior of this is that all errors are logged instead of being thrown
+ public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
+ Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
+
+ RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage());
+ try {
+ DIRECTORY_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, directory);
+ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
+ } catch (Exception e) {
+ // This should never happen, any errors that occur in this provider should instead be logged.
+ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
+ }
+
+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
+ }
+
+ // Plugins are disabled in order like this inorder to "rougly" prevent
+ // their dependencies unloading first. But, eh.
+ public void disablePlugins() {
+ Plugin[] plugins = this.getPlugins();
+ for (int i = plugins.length - 1; i >= 0; i--) {
+ this.disablePlugin(plugins[i]);
+ }
+ }
+
+ public void clearPlugins() {
+ synchronized (this) {
+ this.disablePlugins();
+ this.plugins.clear();
+ this.lookupNames.clear();
+ }
+ }
+
+ public synchronized void enablePlugin(@NotNull Plugin plugin) {
+ if (plugin.isEnabled()) {
+ return;
+ }
+
+ if (plugin.getPluginMeta() instanceof PluginDescriptionFile) {
+ List bukkitCommands = PluginCommandYamlParser.parse(plugin);
+
+ if (!bukkitCommands.isEmpty()) {
+ this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands);
+ }
+ }
+
+ try {
+ String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName();
+ if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) {
+ enableMsg += "*";
+ }
+ plugin.getLogger().info(enableMsg);
+
+ JavaPlugin jPlugin = (JavaPlugin) plugin;
+
+ if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper
+ if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) {
+ this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName());
+ }
+ } // Paper
+
+ try {
+ jPlugin.setEnabled(true);
+ } catch (Throwable ex) {
+ this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex);
+ // Paper start - Disable plugins that fail to load
+ this.server.getPluginManager().disablePlugin(jPlugin);
+ return;
+ // Paper end
+ }
+
+ // Perhaps abort here, rather than continue going, but as it stands,
+ // an abort is not possible the way it's currently written
+ this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while enabling "
+ + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin);
+ }
+
+ HandlerList.bakeAll();
+ }
+
+ public synchronized void disablePlugin(@NotNull Plugin plugin) {
+ if (!(plugin instanceof JavaPlugin javaPlugin)) {
+ throw new IllegalArgumentException("Only expects java plugins.");
+ }
+ if (!plugin.isEnabled()) {
+ return;
+ }
+
+ String pluginName = plugin.getPluginMeta().getDisplayName();
+
+ try {
+ plugin.getLogger().info("Disabling %s".formatted(pluginName));
+
+ this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
+
+ javaPlugin.setEnabled(false);
+
+ ClassLoader classLoader = plugin.getClass().getClassLoader();
+ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ try {
+ configuredPluginClassLoader.close();
+ } catch (IOException ex) {
+ this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception
+ }
+ // Remove from the classloader pool inorder to prevent plugins from trying
+ // to access classes
+ PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader);
+ }
+
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while disabling "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getScheduler().cancelTasks(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getServicesManager().unregisterAll(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ HandlerList.unregisterAll(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getMessenger().unregisterIncomingPluginChannel(plugin);
+ this.server.getMessenger().unregisterOutgoingPluginChannel(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ for (World world : this.server.getWorlds()) {
+ world.removePluginChunkTickets(plugin);
+ }
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ }
+
+ // TODO: Implement event part in future patch (paper patch move up, this patch is lower)
+ private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
+ Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex);
+ this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
+ }
+
+ public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
+ return this.context.isTransitiveDependency(plugin, depend);
+ }
+
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.getPlugin(pluginIdentifier) != null;
+ }
+
+ // Debug only
+ @ApiStatus.Internal
+ public MutableGraph getDependencyGraph() {
+ return this.dependencyGraph;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..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..194f1439e322cb6b3433a72f80a8a4c7624b20d5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
@@ -0,0 +1,80 @@
+package io.papermc.paper.plugin.manager;
+
+import com.destroystokyo.paper.util.SneakyThrow;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
+import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Used for registering a single plugin provider.
+ * This has special behavior in that some errors are thrown instead of logged.
+ */
+class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage {
+
+ private PluginProvider lastProvider;
+ private JavaPlugin singleLoaded;
+
+ @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 provider = this.lastProvider;
+ if (provider == null) {
+ return;
+ }
+
+ // Manually validate dependencies, LEGACY BEHAVIOR.
+ // Normally it is logged, but manually adding one plugin will cause it to actually throw exceptions.
+ PluginDescriptionFile descriptionFile = (PluginDescriptionFile) provider.getMeta();
+ List missingDependencies = new ArrayList<>();
+ for (String dependency : descriptionFile.getDepend()) {
+ if (!PaperPluginManagerImpl.getInstance().isPluginEnabled(dependency)) {
+ missingDependencies.add(dependency);
+ }
+ }
+ if (!missingDependencies.isEmpty()) {
+ throw new UnknownDependencyException(missingDependencies, provider.getFileName().toString());
+ }
+
+ // Go through normal plugin loading logic
+ super.enter();
+ }
+
+ @Override
+ public void processProvided(PluginProvider provider, JavaPlugin provided) {
+ super.processProvided(provider, provided);
+ this.singleLoaded = provided;
+ }
+
+ @Override
+ public boolean exitOnCycleDependencies() {
+ return false;
+ }
+
+ public Optional getSingleLoaded() {
+ return Optional.ofNullable(this.singleLoaded);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea8cf22c35242eb9f3914b95df00e20504aef5c1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
@@ -0,0 +1,42 @@
+package io.papermc.paper.plugin.manager;
+
+import org.bukkit.permissions.Permissible;
+import org.bukkit.permissions.Permission;
+import org.bukkit.plugin.SimplePluginManager;
+
+import java.util.Map;
+import java.util.Set;
+
+/*
+This is actually so cursed I hate it.
+We need to wrap these in fields as people override the fields, so we need to access them lazily at all times.
+// TODO: When SimplePluginManager is GONE remove this and cleanup the PaperPermissionManager to use actual fields.
+ */
+class StupidSPMPermissionManagerWrapper extends PaperPermissionManager {
+
+ private final SimplePluginManager simplePluginManager;
+
+ public StupidSPMPermissionManagerWrapper(SimplePluginManager simplePluginManager) {
+ this.simplePluginManager = simplePluginManager;
+ }
+
+ @Override
+ public Map permissions() {
+ return this.simplePluginManager.permissions;
+ }
+
+ @Override
+ public Map> defaultPerms() {
+ return this.simplePluginManager.defaultPerms;
+ }
+
+ @Override
+ public Map> permSubs() {
+ return this.simplePluginManager.permSubs;
+ }
+
+ @Override
+ public Map