3
0
Mirror von https://github.com/PaperMC/Paper.git synchronisiert 2024-11-15 12:30:06 +01:00
Paper/patches/server/0013-Paper-Plugins.patch
Nassim Jahnke 8c4e81184b
Update circular dependencies warning, properly crash
Also fixes logging errors
2023-02-20 15:20:49 +01:00

6818 Zeilen
276 KiB
Diff
Originalformat Blame Verlauf

Diese Datei enthält mehrdeutige Unicode-Zeichen

Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com>
Date: Wed, 6 Jul 2022 23:00:31 -0400
Subject: [PATCH] Paper Plugins
diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java
index b3a58bf4b654e336826dc04da9e2f80ff8b9a9a7..156334e3876d966fedc91d18da29f562395ab182 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommand.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -4,6 +4,7 @@ import io.papermc.paper.command.subcommands.EntityCommand;
import io.papermc.paper.command.subcommands.HeapDumpCommand;
import io.papermc.paper.command.subcommands.ReloadCommand;
import io.papermc.paper.command.subcommands.VersionCommand;
+import io.papermc.paper.command.subcommands.DumpPluginsCommand;
import it.unimi.dsi.fastutil.Pair;
import java.util.ArrayList;
import java.util.Arrays;
@@ -40,6 +41,7 @@ public final class PaperCommand extends Command {
commands.put(Set.of("entity"), new EntityCommand());
commands.put(Set.of("reload"), new ReloadCommand());
commands.put(Set.of("version"), new VersionCommand());
+ commands.put(Set.of("dumpplugins"), new DumpPluginsCommand());
return commands.entrySet().stream()
.flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue())))
diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java
index 6a00f3d38da8107825ab1d405f337fd077b09f72..d44d0074446c1c54e87dc8078dff7fef1d92f343 100644
--- a/src/main/java/io/papermc/paper/command/PaperCommands.java
+++ b/src/main/java/io/papermc/paper/command/PaperCommands.java
@@ -23,5 +23,6 @@ public final class PaperCommands {
COMMANDS.forEach((s, command) -> {
server.server.getCommandMap().register(s, "Paper", command);
});
+ server.server.getCommandMap().register("bukkit", new PaperPluginsCommand());
}
}
diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..72096a66a4046633de73a12f5a043ac6dff169b1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java
@@ -0,0 +1,207 @@
+package io.papermc.paper.command;
+
+import com.google.common.collect.Lists;
+import io.leangen.geantyref.GenericTypeReflector;
+import io.leangen.geantyref.TypeToken;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.ProviderStatus;
+import io.papermc.paper.plugin.provider.ProviderStatusHolder;
+import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
+import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.JoinConfiguration;
+import net.kyori.adventure.text.TextComponent;
+import net.kyori.adventure.text.event.ClickEvent;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextColor;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.defaults.BukkitCommand;
+import org.bukkit.craftbukkit.util.CraftMagicNumbers;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeMap;
+
+public class PaperPluginsCommand extends BukkitCommand {
+
+ private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218);
+
+ // TODO: LINK?
+ private static final Component SERVER_PLUGIN_INFO = Component.text(" What is a server plugin?", INFO_COLOR)
+ .append(asPlainComponents("""
+ Server plugins can add new behavior to your server!
+ You can find new plugins on Paper's plugin repository, Hangar.
+
+ <link to hangar>
+ """));
+
+ private static final Component SERVER_INITIALIZER_INFO = Component.text(" What is a server initializer?", INFO_COLOR)
+ .append(asPlainComponents("""
+ Server initializers are ran before your server
+ starts and are provided by paper plugins.
+ """));
+
+ private static final Component LEGACY_PLUGIN_INFO = Component.text(" What is a legacy plugin?", INFO_COLOR)
+ .append(asPlainComponents("""
+ A legacy plugin is a plugin that was made on
+ very old unsupported versions of the game.
+
+ It is encouraged that you replace this plugin,
+ as they might not work in the future and may cause
+ performance issues.
+ """));
+
+ private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO);
+ private static final Component INFO_ICON_START = Component.text(" ", INFO_COLOR);
+ private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209));
+ private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6));
+ private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY);
+ private static final Component PLUGIN_TICK_EMPTY = Component.text(" ");
+
+ private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken<PluginProvider<JavaPlugin>>() {}.getType();
+
+ public PaperPluginsCommand() {
+ super("plugins");
+ this.description = "Gets a list of plugins running on the server";
+ this.usageMessage = "/plugins";
+ this.setPermission("bukkit.command.plugins");
+ this.setAliases(Arrays.asList("pl"));
+ }
+
+ private static <T> List<Component> formatProviders(TreeMap<String, PluginProvider<T>> plugins) {
+ List<Component> components = new ArrayList<>(plugins.size());
+ for (PluginProvider<T> entry : plugins.values()) {
+ components.add(formatProvider(entry));
+ }
+
+ boolean isFirst = true;
+ List<Component> formattedSublists = new ArrayList<>();
+ /*
+ Split up the plugin list for each 10 plugins to get size down
+
+ Plugin List:
+ - Plugin 1, Plugin 2, .... Plugin 10,
+ Plugin 11, Plugin 12 ... Plugin 20,
+ */
+ for (List<Component> componentSublist : Lists.partition(components, 10)) {
+ Component component = Component.space();
+ if (isFirst) {
+ component = component.append(PLUGIN_TICK);
+ isFirst = false;
+ } else {
+ component = PLUGIN_TICK_EMPTY;
+ //formattedSublists.add(Component.empty()); // Add an empty line, the auto chat wrapping and this makes it quite jarring.
+ }
+
+ formattedSublists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist)));
+ }
+
+ return formattedSublists;
+ }
+
+ private static Component formatProvider(PluginProvider<?> provider) {
+ TextComponent.Builder builder = Component.text();
+ if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) {
+ builder.append(LEGACY_PLUGIN_STAR);
+ }
+
+ String name = provider.getMeta().getName();
+ Component pluginName = Component.text(name, fromStatus(provider))
+ .clickEvent(ClickEvent.runCommand("/version " + name));
+
+ builder.append(pluginName);
+
+ return builder.build();
+ }
+
+ private static Component asPlainComponents(String strings) {
+ net.kyori.adventure.text.TextComponent.Builder builder = Component.text();
+ for (String string : strings.split("\n")) {
+ builder.append(Component.newline());
+ builder.append(Component.text(string, NamedTextColor.WHITE));
+ }
+
+ return builder.build();
+ }
+
+ private static TextColor fromStatus(PluginProvider<?> provider) {
+ if (provider instanceof ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) {
+ ProviderStatus status = statusHolder.getLastProvidedStatus();
+
+ // Handle enabled/disabled game plugins
+ if (status == ProviderStatus.INITIALIZED && GenericTypeReflector.isSuperType(JAVA_PLUGIN_PROVIDER_TYPE, provider.getClass())) {
+ Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName());
+ // Plugin doesn't exist? Could be due to it being removed.
+ if (plugin == null) {
+ return NamedTextColor.RED;
+ }
+
+ return plugin.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED;
+ }
+
+ return switch (status) {
+ case INITIALIZED -> NamedTextColor.GREEN;
+ case ERRORED -> NamedTextColor.RED;
+ };
+ } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) {
+ // Paper plugins will be skipped if their provider is skipped due to their initializer failing.
+ // Show them as red
+ return NamedTextColor.RED;
+ } else {
+ // Separated for future logic choice, but this indicated a provider that failed to load due to
+ // dependency issues or what not.
+ return NamedTextColor.RED;
+ }
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) {
+ if (!this.testPermission(sender)) return true;
+
+ TreeMap<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+
+ for (PluginProvider<JavaPlugin> provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) {
+ PluginMeta configuration = provider.getMeta();
+
+ if (provider instanceof SpigotPluginProvider) {
+ spigotPlugins.put(configuration.getDisplayName(), provider);
+ } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
+ paperPlugins.put(configuration.getDisplayName(), provider);
+ }
+ }
+
+ Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE);
+ //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs
+
+ sender.sendMessage(infoMessage);
+ sender.sendMessage(PAPER_HEADER);
+ for (Component component : formatProviders(paperPlugins)) {
+ sender.sendMessage(component);
+ }
+ sender.sendMessage(BUKKIT_HEADER);
+ for (Component component : formatProviders(spigotPlugins)) {
+ sender.sendMessage(component);
+ }
+
+ return true;
+ }
+
+ @NotNull
+ @Override
+ public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
+ return Collections.emptyList();
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..254854646b748e5bb47657625315ced51b22887f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java
@@ -0,0 +1,201 @@
+package io.papermc.paper.command.subcommands;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.internal.Streams;
+import com.google.gson.stream.JsonWriter;
+import io.papermc.paper.command.PaperSubcommand;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.entrypoint.classloader.group.LockingClassLoaderGroup;
+import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
+import io.papermc.paper.plugin.entrypoint.classloader.group.SimpleListPluginClassLoaderGroup;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
+import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
+import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
+import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
+import io.papermc.paper.plugin.storage.ConfiguredProviderStorage;
+import io.papermc.paper.plugin.storage.ProviderStorage;
+import net.minecraft.server.MinecraftServer;
+import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.Plugin;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+import java.io.PrintStream;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static net.kyori.adventure.text.Component.text;
+import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
+import static net.kyori.adventure.text.format.NamedTextColor.RED;
+
+@DefaultQualifier(NonNull.class)
+public final class DumpPluginsCommand implements PaperSubcommand {
+ @Override
+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) {
+ this.dumpPlugins(sender, args);
+ return true;
+ }
+
+ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss");
+
+ private void dumpPlugins(final CommandSender sender, final String[] args) {
+ Path parent = Path.of("debug");
+ Path path = parent.resolve("plugin-info" + FORMATTER.format(LocalDateTime.now()) + ".txt");
+ try {
+ Files.createDirectories(parent);
+ Files.createFile(path);
+ sender.sendMessage(text("Writing plugin information to " + path, GREEN));
+
+ final JsonObject data = this.writeDebug();
+
+ StringWriter stringWriter = new StringWriter();
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(" ");
+ jsonWriter.setLenient(false);
+ Streams.write(data, jsonWriter);
+
+ try (PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8)) {
+ out.print(stringWriter);
+ }
+ sender.sendMessage(text("Successfully written plugin debug information!", GREEN));
+ } catch (Throwable e) {
+ sender.sendMessage(text("Failed to write plugin information! See the console for more info.", RED));
+ MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", e);
+ }
+ }
+
+ private JsonObject writeDebug() {
+ JsonObject root = new JsonObject();
+ if (ConfiguredProviderStorage.LEGACY_PLUGIN_LOADING) {
+ root.addProperty("legacy-loading-strategy", true);
+ }
+
+ this.writeProviders(root);
+ this.writePlugins(root);
+ this.writeClassloaders(root);
+
+ return root;
+ }
+
+ private void writeProviders(JsonObject root) {
+ JsonObject rootProviders = new JsonObject();
+ root.add("providers", rootProviders);
+
+ for (Map.Entry<Entrypoint<?>, ProviderStorage<?>> entry : LaunchEntryPointHandler.INSTANCE.getStorage().entrySet()) {
+ JsonObject entrypoint = new JsonObject();
+
+ JsonArray providers = new JsonArray();
+ entrypoint.add("providers", providers);
+
+ List<PluginProvider<Object>> pluginProviders = new ArrayList<>();
+ for (PluginProvider<?> provider : entry.getValue().getRegisteredProviders()) {
+ JsonObject providerObj = new JsonObject();
+ providerObj.addProperty("name", provider.getMeta().getName());
+ providerObj.addProperty("version", provider.getMeta().getVersion());
+ providerObj.addProperty("dependencies", provider.getMeta().getPluginDependencies().toString());
+ providerObj.addProperty("soft-dependencies", provider.getMeta().getPluginSoftDependencies().toString());
+ providerObj.addProperty("load-before", provider.getMeta().getLoadBeforePlugins().toString());
+
+
+ providers.add(providerObj);
+ pluginProviders.add((PluginProvider<Object>) provider);
+ }
+
+ JsonArray loadOrder = new JsonArray();
+ entrypoint.add("load-order", loadOrder);
+
+ ModernPluginLoadingStrategy<Object> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
+ @Override
+ public void applyContext(PluginProvider<Object> provider, DependencyContext dependencyContext) {
+ }
+
+ @Override
+ public boolean load(PluginProvider<Object> provider, Object provided) {
+ loadOrder.add(provider.getMeta().getName());
+ return false;
+ }
+
+ @Override
+ public List<String> requiredDependencies(PluginProvider<Object> provider) {
+ return provider.getMeta().getPluginDependencies();
+ }
+
+ @Override
+ public List<String> optionalDependencies(PluginProvider<Object> provider) {
+ return provider.getMeta().getPluginSoftDependencies();
+ }
+
+ @Override
+ public List<String> loadBeforeDependencies(PluginProvider<Object> provider) {
+ return provider.getMeta().getLoadBeforePlugins();
+ }
+ });
+ modernPluginLoadingStrategy.loadProviders(pluginProviders);
+
+ rootProviders.add(entry.getKey().getDebugName(), entrypoint);
+ }
+ }
+
+ private void writePlugins(JsonObject root) {
+ JsonArray rootPlugins = new JsonArray();
+ root.add("plugins", rootPlugins);
+
+ for (Plugin plugin : PaperPluginManagerImpl.getInstance().getPlugins()) {
+ rootPlugins.add(plugin.toString());
+ }
+ }
+
+ private void writeClassloaders(JsonObject root) {
+ JsonObject classLoadersRoot = new JsonObject();
+ root.add("classloaders", classLoadersRoot);
+
+ PaperPluginClassLoaderStorage storage = (PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance();
+ classLoadersRoot.addProperty("global", storage.getGlobalGroup().toString());
+ classLoadersRoot.addProperty("dependency_graph", PaperPluginManagerImpl.getInstance().getInstanceManagerGraph().toString());
+
+ JsonArray array = new JsonArray();
+ classLoadersRoot.add("children", array);
+ for (PluginClassLoaderGroup group : storage.getGroups()) {
+ array.add(this.writeClassloader(group));
+ }
+ }
+
+ private JsonObject writeClassloader(PluginClassLoaderGroup group) {
+ JsonObject classLoadersRoot = new JsonObject();
+ if (group instanceof SimpleListPluginClassLoaderGroup listGroup) {
+ JsonArray array = new JsonArray();
+ classLoadersRoot.addProperty("main", listGroup.toString());
+ classLoadersRoot.add("children", array);
+ for (ConfiguredPluginClassLoader innerGroup : listGroup.getClassLoaders()) {
+ array.add(this.writeClassloader(innerGroup));
+ }
+
+ } else if (group instanceof LockingClassLoaderGroup locking) {
+ // Unwrap
+ return this.writeClassloader(locking.getParent());
+ } else {
+ classLoadersRoot.addProperty("raw", group.toString());
+ }
+
+ return classLoadersRoot;
+ }
+
+ private JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) {
+ return new JsonPrimitive(innerGroup.toString());
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..85fa762d7d3476510886a7cc7b3bc79acfce4425
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java
@@ -0,0 +1,78 @@
+package io.papermc.paper.plugin;
+
+import com.mojang.logging.LogUtils;
+import joptsimple.OptionSet;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class PluginInitializerManager {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static PluginInitializerManager impl;
+ private final Path pluginDirectory;
+ private final Path updateDirectory;
+
+ PluginInitializerManager(@NotNull OptionSet minecraftOptionSet) {
+ // We have to load the bukkit configuration inorder to get the update folder location.
+ File configFileLocationBukkit = (File) minecraftOptionSet.valueOf("bukkit-settings");
+ this.pluginDirectory = ((File) minecraftOptionSet.valueOf("plugins")).toPath();
+
+ String updateDirectory = YamlConfiguration.loadConfiguration(configFileLocationBukkit).getString("settings.update-folder", "update");
+ if (updateDirectory.isBlank()) {
+ this.updateDirectory = null;
+ } else {
+ Path resolvedUpdateDirectory = this.pluginDirectory.resolve(updateDirectory);
+ if (!Files.isDirectory(resolvedUpdateDirectory)) {
+ this.updateDirectory = null;
+ return;
+ }
+
+ boolean isSameFile = true;
+ try {
+ isSameFile = Files.isSameFile(resolvedUpdateDirectory, this.pluginDirectory);
+ } catch (IOException e) {
+ LOGGER.error("Misconfigured update directory!");
+ LOGGER.error("Failed to compare update/plugin directory", e);
+ }
+
+ if (isSameFile) {
+ LOGGER.error("Misconfigured update directory!");
+ LOGGER.error(("Your configured update directory (%s) in bukkit.yml is pointing to the same location as the plugin directory (%s). " +
+ "Disabling auto updating functionality.").formatted(resolvedUpdateDirectory, this.pluginDirectory));
+
+ this.updateDirectory = null;
+ } else {
+ this.updateDirectory = resolvedUpdateDirectory;
+ }
+
+ }
+
+ }
+
+ public static PluginInitializerManager init(OptionSet optionSet) {
+ impl = new PluginInitializerManager(optionSet);
+ return impl;
+ }
+
+ public static PluginInitializerManager instance() {
+ return impl;
+ }
+
+ @NotNull
+ public Path pluginDirectoryPath() {
+ return pluginDirectory;
+ }
+
+ @Nullable
+ public Path pluginUpdatePath() {
+ return updateDirectory;
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..8249a7024537fccd99735b92abb1368e6647b5ae
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java
@@ -0,0 +1,40 @@
+package io.papermc.paper.plugin.bootstrap;
+
+import io.papermc.paper.plugin.PluginInitializerManager;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+import java.util.logging.Logger;
+
+public record PluginProviderContextImpl(PluginMeta config, Path dataFolder,
+ Logger logger) implements PluginProviderContext {
+
+ public static PluginProviderContextImpl of(PluginMeta config, Logger logger) {
+ Path dataFolder = PluginInitializerManager.instance().pluginDirectoryPath().resolve(config.getDisplayName());
+
+ return new PluginProviderContextImpl(config, dataFolder, logger);
+ }
+
+ public static PluginProviderContextImpl of(PluginProvider<?> provider, Path pluginFolder) {
+ Path dataFolder = pluginFolder.resolve(provider.getMeta().getDisplayName());
+
+ return new PluginProviderContextImpl(provider.getMeta(), dataFolder, provider.getLogger());
+ }
+
+ @Override
+ public @NotNull PluginMeta getConfiguration() {
+ return this.config;
+ }
+
+ @Override
+ public @NotNull Path getDataDirectory() {
+ return this.dataFolder;
+ }
+
+ @Override
+ public @NotNull Logger getLogger() {
+ return this.logger;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java
new file mode 100644
index 0000000000000000000000000000000000000000..125008ac7db8b9f3fb57c49f8e4facc4ad4bb136
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java
@@ -0,0 +1,25 @@
+package io.papermc.paper.plugin.entrypoint;
+
+import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
+import org.bukkit.plugin.java.JavaPlugin;
+
+/**
+ * Used to mark a certain place that {@link EntrypointHandler} will register {@link io.papermc.paper.plugin.provider.PluginProvider} under.
+ * Used for loading only certain providers at a certain time.
+ * @param <T> provider type
+ */
+public final class Entrypoint<T> {
+
+ public static final Entrypoint<PluginBootstrap> BOOTSTRAPPER = new Entrypoint<>("bootstrapper");
+ public static final Entrypoint<JavaPlugin> PLUGIN = new Entrypoint<>("plugin");
+
+ private final String debugName;
+
+ private Entrypoint(String debugName) {
+ this.debugName = debugName;
+ }
+
+ public String getDebugName() {
+ return debugName;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..b38e1e0f3d3055086f51bb191fd4b60ecf32d016
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java
@@ -0,0 +1,14 @@
+package io.papermc.paper.plugin.entrypoint;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+
+/**
+ * Represents a register that will register providers at a certain {@link Entrypoint},
+ * where then when the given {@link Entrypoint} is registered those will be loaded.
+ */
+public interface EntrypointHandler {
+
+ <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider);
+
+ void enter(Entrypoint<?> entrypoint);
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..61a67971a41527c0e3b614bf48d2bc8eabd443b5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.plugin.entrypoint;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.storage.BootstrapProviderStorage;
+import io.papermc.paper.plugin.storage.ProviderStorage;
+import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Used by the server to register/load plugin bootstrappers and plugins.
+ */
+public class LaunchEntryPointHandler implements EntrypointHandler {
+
+ public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler();
+ private final Map<Entrypoint<?>, ProviderStorage<?>> storage = new HashMap<>();
+
+ LaunchEntryPointHandler() {
+ this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
+ this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage());
+ }
+
+ // Utility
+ public static void enterBootstrappers() {
+ LaunchEntryPointHandler.INSTANCE.enter(Entrypoint.BOOTSTRAPPER);
+ }
+
+ @Override
+ public void enter(Entrypoint<?> entrypoint) {
+ ProviderStorage<?> storage = this.storage.get(entrypoint);
+ if (storage == null) {
+ throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
+ }
+
+ storage.enter();
+ }
+
+ @Override
+ public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
+ ProviderStorage<T> storage = this.get(entrypoint);
+ if (storage == null) {
+ throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
+ }
+
+ storage.register(provider);
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> ProviderStorage<T> get(Entrypoint<T> entrypoint) {
+ return (ProviderStorage<T>) this.storage.get(entrypoint);
+ }
+
+ // Debug only
+ @ApiStatus.Internal
+ public Map<Entrypoint<?>, ProviderStorage<?>> getStorage() {
+ return storage;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..93b5196a960f3efbe0d28f5527ea2752426213ce
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java
@@ -0,0 +1,22 @@
+package io.papermc.paper.plugin.entrypoint.classloader;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import net.kyori.adventure.util.Services;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Internal
+public interface ClassloaderBytecodeModifier {
+
+ static ClassloaderBytecodeModifier bytecodeModifier() {
+ return Provider.INSTANCE;
+ }
+
+ byte[] modify(PluginMeta config, byte[] bytecode);
+
+ class Provider {
+
+ private static final ClassloaderBytecodeModifier INSTANCE = Services.service(ClassloaderBytecodeModifier.class).orElseThrow();
+
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..f9a2c55a354c877749db3f92956de802ae575788
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java
@@ -0,0 +1,12 @@
+package io.papermc.paper.plugin.entrypoint.classloader;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+
+// Stub, implement in future.
+public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
+
+ @Override
+ public byte[] modify(PluginMeta configuration, byte[] bytecode) {
+ return bytecode;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..f1a2d25079c841daff19f41f2b24fb8e97d2c7d3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java
@@ -0,0 +1,193 @@
+package io.papermc.paper.plugin.entrypoint.classloader;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
+import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
+import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+
+/**
+ * This is similar to a {@link org.bukkit.plugin.java.PluginClassLoader} but is completely kept hidden from the api.
+ * This is only used with Paper plugins.
+ *
+ * @see PaperPluginClassLoaderStorage
+ */
+public class PaperPluginClassLoader extends PaperSimplePluginClassLoader implements ConfiguredPluginClassLoader {
+
+ static {
+ registerAsParallelCapable();
+ }
+
+ private final ClassLoader libraryLoader;
+ private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private final Logger logger;
+ @Nullable
+ private JavaPlugin loadedJavaPlugin;
+ @Nullable
+ private PluginClassLoaderGroup group;
+
+ public PaperPluginClassLoader(Logger logger, Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader, ClassLoader libraryLoader) throws IOException {
+ super(source, file, configuration, parentLoader);
+ this.libraryLoader = libraryLoader;
+
+ this.logger = logger;
+ if (this.configuration.hasOpenClassloader()) {
+ this.group = PaperClassLoaderStorage.instance().registerOpenGroup(this);
+ }
+ }
+
+ public void refreshClassloaderDependencyTree(DependencyContext dependencyContext) {
+ if (this.configuration.hasOpenClassloader()) {
+ return;
+ }
+ if (this.group != null) {
+ // We need to unregister the classloader inorder to allow for dependencies
+ // to be recalculated
+ PaperClassLoaderStorage.instance().unregisterClassloader(this);
+ }
+
+ this.group = PaperClassLoaderStorage.instance().registerAccessBackedGroup(this, (classLoader) -> {
+ return dependencyContext.isTransitiveDependency(PaperPluginClassLoader.this.configuration, classLoader.getConfiguration());
+ });
+ }
+
+ @Override
+ public URL getResource(String name) {
+ URL resource = findResource(name);
+ if (resource == null && this.libraryLoader != null) {
+ return this.libraryLoader.getResource(name);
+ }
+ return resource;
+ }
+
+ @Override
+ public Enumeration<URL> getResources(String name) throws IOException {
+ List<URL> resources = new ArrayList<>();
+ this.addEnumeration(resources, this.findResources(name));
+ if (this.libraryLoader != null) {
+ addEnumeration(resources, this.libraryLoader.getResources(name));
+ }
+ return Collections.enumeration(resources);
+ }
+
+ private <T> void addEnumeration(List<T> list, Enumeration<T> enumeration) {
+ while (enumeration.hasMoreElements()) {
+ list.add(enumeration.nextElement());
+ }
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ return this.loadClass(name, resolve, true, true);
+ }
+
+ @Override
+ public PluginMeta getConfiguration() {
+ return this.configuration;
+ }
+
+ @Override
+ public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGroup, boolean checkLibraries) throws ClassNotFoundException {
+ try {
+ Class<?> result = super.loadClass(name, resolve);
+
+ // SPIGOT-6749: Library classes will appear in the above, but we don't want to return them to other plugins
+ if (checkGroup || result.getClassLoader() == this) {
+ return result;
+ }
+ } catch (ClassNotFoundException ignored) {
+ }
+
+ if (checkLibraries) {
+ try {
+ return this.libraryLoader.loadClass(name);
+ } catch (ClassNotFoundException ignored) {
+ }
+ }
+
+ if (checkGroup) {
+ // This ignores the libraries of other plugins, unless they are transitive dependencies.
+ if (this.group == null) {
+ throw new IllegalStateException("Tried to resolve class while group was not yet initialized");
+ }
+
+ Class<?> clazz = this.group.getClassByName(name, resolve, this);
+ if (clazz != null) {
+ return clazz;
+ }
+ }
+
+ throw new ClassNotFoundException(name);
+ }
+
+ @Override
+ public void init(JavaPlugin plugin) {
+ PluginMeta config = this.configuration;
+ PluginDescriptionFile pluginDescriptionFile = new PluginDescriptionFile(
+ config.getName(),
+ config.getDisplayName(),
+ config.getProvidedPlugins(),
+ config.getMainClass(),
+ "", // Classloader load order api
+ List.of(), // Dependencies
+ List.of(), // Soft Depends
+ List.of(), // Load Before
+ config.getVersion(),
+ Map.of(), // Commands, we use a separate system
+ config.getDescription(),
+ config.getAuthors(),
+ config.getContributors(),
+ config.getWebsite(),
+ config.getLoggerPrefix(),
+ config.getLoadOrder(),
+ config.getPermissions(),
+ config.getPermissionDefault(),
+ Set.of(), // Aware api
+ config.getAPIVersion(),
+ List.of() // Libraries
+ );
+
+ File dataFolder = new File(Bukkit.getPluginsFolder(), pluginDescriptionFile.getName());
+
+ plugin.init(Bukkit.getServer(), pluginDescriptionFile, dataFolder, this.source.toFile(), this, config);
+ plugin.logger = this.logger;
+
+ this.loadedJavaPlugin = plugin;
+ }
+
+ @Nullable
+ public JavaPlugin getLoadedJavaPlugin() {
+ return this.loadedJavaPlugin;
+ }
+
+ @Override
+ public String toString() {
+ return "PaperPluginClassLoader{" +
+ "libraryLoader=" + this.libraryLoader +
+ ", seenIllegalAccess=" + this.seenIllegalAccess +
+ ", loadedJavaPlugin=" + this.loadedJavaPlugin +
+ ", group=" + this.group +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..6688f2af4a32c2f79785f162e2eac0330d439ac1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java
@@ -0,0 +1,116 @@
+package io.papermc.paper.plugin.entrypoint.classloader;
+
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import io.papermc.paper.plugin.util.NamespaceChecker;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.security.CodeSigner;
+import java.security.CodeSource;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+/**
+ * Represents a simple classloader used for paper plugin bootstrappers.
+ */
+@ApiStatus.Internal
+public class PaperSimplePluginClassLoader extends URLClassLoader {
+
+ static {
+ ClassLoader.registerAsParallelCapable();
+ }
+
+ protected final PaperPluginMeta configuration;
+ protected final Path source;
+ protected final Manifest jarManifest;
+ protected final URL jarUrl;
+ protected final JarFile jar;
+
+ public PaperSimplePluginClassLoader(Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader) throws IOException {
+ super(source.getFileName().toString(), new URL[]{source.toUri().toURL()}, parentLoader);
+
+ this.source = source;
+ this.jarManifest = file.getManifest();
+ this.jarUrl = source.toUri().toURL();
+ this.configuration = configuration;
+ this.jar = file;
+ }
+
+ @Override
+ public URL getResource(String name) {
+ return this.findResource(name);
+ }
+
+ @Override
+ public Enumeration<URL> getResources(String name) throws IOException {
+ return this.findResources(name);
+ }
+
+ // Bytecode modification supported loader
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ NamespaceChecker.validateNameSpaceForClassloading(name);
+
+ // See UrlClassLoader#findClass(String)
+ String path = name.replace('.', '/').concat(".class");
+ JarEntry entry = this.jar.getJarEntry(path);
+ if (entry == null) {
+ throw new ClassNotFoundException();
+ }
+
+ // See URLClassLoader#defineClass(String, Resource)
+ byte[] classBytes;
+
+ try (InputStream is = this.jar.getInputStream(entry)) {
+ classBytes = is.readAllBytes();
+ } catch (IOException ex) {
+ throw new ClassNotFoundException(name, ex);
+ }
+
+ classBytes = ClassloaderBytecodeModifier.bytecodeModifier().modify(this.configuration, classBytes);
+
+ int dot = name.lastIndexOf('.');
+ if (dot != -1) {
+ String pkgName = name.substring(0, dot);
+ // Get defined package does not correctly handle sealed packages.
+ if (this.getDefinedPackage(pkgName) == null) {
+ try {
+ if (this.jarManifest != null) {
+ this.definePackage(pkgName, this.jarManifest, this.jarUrl);
+ } else {
+ this.definePackage(pkgName, null, null, null, null, null, null, null);
+ }
+ } catch (IllegalArgumentException ex) {
+ // parallel-capable class loaders: re-verify in case of a
+ // race condition
+ if (this.getDefinedPackage(pkgName) == null) {
+ // Should never happen
+ throw new IllegalStateException("Cannot find package " + pkgName);
+ }
+ }
+ }
+ }
+
+ CodeSigner[] signers = entry.getCodeSigners();
+ CodeSource source = new CodeSource(this.jarUrl, signers);
+
+ return this.defineClass(name, classBytes, 0, classBytes.length, source);
+ }
+
+ @Override
+ public String toString() {
+ return "PaperSimplePluginClassLoader{" +
+ "configuration=" + this.configuration +
+ ", source=" + this.source +
+ ", jarManifest=" + this.jarManifest +
+ ", jarUrl=" + this.jarUrl +
+ ", jar=" + this.jar +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..eaf5c794cbe8d6138c9d60eaae20f5fc7711f541
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java
@@ -0,0 +1,47 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.ArrayList;
+
+@ApiStatus.Internal
+public class DependencyBasedPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
+
+ private final GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup;
+ private final ClassLoaderAccess access;
+
+ public DependencyBasedPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, ClassLoaderAccess access) {
+ super(new ArrayList<>());
+ this.access = access;
+ this.globalPluginClassLoaderGroup = globalPluginClassLoaderGroup;
+ }
+
+ /**
+ * This will refresh the dependencies of the current classloader.
+ */
+ public void populateDependencies() {
+ this.classloaders.clear();
+ for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalPluginClassLoaderGroup.getClassLoaders()) {
+ if (this.access.canAccess(configuredPluginClassLoader)) {
+ this.classloaders.add(configuredPluginClassLoader);
+ }
+ }
+
+ }
+
+ @Override
+ public ClassLoaderAccess getAccess() {
+ return this.access;
+ }
+
+ @Override
+ public String toString() {
+ return "DependencyBasedPluginClassLoaderGroup{" +
+ "globalPluginClassLoaderGroup=" + this.globalPluginClassLoaderGroup +
+ ", access=" + this.access +
+ ", classloaders=" + this.classloaders +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..2b7eef787f83e5a32896cb30c215406b6f652786
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java
@@ -0,0 +1,18 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Internal
+public class GlobalPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
+
+ @Override
+ public ClassLoaderAccess getAccess() {
+ return (v) -> true;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..aae50ebba6ba1579b75af5370c8b020d2a927b2c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java
@@ -0,0 +1,76 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+@ApiStatus.Internal
+public class LockingClassLoaderGroup implements PluginClassLoaderGroup {
+
+ private final PluginClassLoaderGroup parent;
+ private final Map<String, ClassLockEntry> classLoadLock = new HashMap<>();
+
+ public LockingClassLoaderGroup(PluginClassLoaderGroup parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
+ // make MT safe
+ ClassLockEntry lock;
+ synchronized (this.classLoadLock) {
+ lock = this.classLoadLock.computeIfAbsent(name, (x) -> new ClassLockEntry(new AtomicInteger(0), new java.util.concurrent.locks.ReentrantReadWriteLock()));
+ lock.count.incrementAndGet();
+ }
+ lock.reentrantReadWriteLock.writeLock().lock();
+ try {
+ return parent.getClassByName(name, resolve, requester);
+ } finally {
+ synchronized (this.classLoadLock) {
+ lock.reentrantReadWriteLock.writeLock().unlock();
+ if (lock.count.get() == 1) {
+ this.classLoadLock.remove(name);
+ } else {
+ lock.count.decrementAndGet();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ this.parent.remove(configuredPluginClassLoader);
+ }
+
+ @Override
+ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ this.parent.add(configuredPluginClassLoader);
+ }
+
+ @Override
+ public ClassLoaderAccess getAccess() {
+ return this.parent.getAccess();
+ }
+
+ public PluginClassLoaderGroup getParent() {
+ return parent;
+ }
+
+ record ClassLockEntry(AtomicInteger count, ReentrantReadWriteLock reentrantReadWriteLock) {
+ }
+
+ @Override
+ public String toString() {
+ return "LockingClassLoaderGroup{" +
+ "parent=" + this.parent +
+ ", classLoadLock=" + this.classLoadLock +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c906e2c7d972b221a41acd614e00d0fbc1227c6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java
@@ -0,0 +1,93 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
+import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.java.PluginClassLoader;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * This is used for connecting multiple classloaders.
+ */
+public final class PaperPluginClassLoaderStorage implements PaperClassLoaderStorage {
+
+ private final GlobalPluginClassLoaderGroup globalGroup = new GlobalPluginClassLoaderGroup();
+ private final List<PluginClassLoaderGroup> groups = new CopyOnWriteArrayList<>();
+
+ public PaperPluginClassLoaderStorage() {
+ this.groups.add(this.globalGroup);
+ }
+
+ @Override
+ public PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader) {
+ return this.registerGroup(pluginClassLoader, new SpigotPluginClassLoaderGroup(this.globalGroup, (library) -> {
+ return Bukkit.getServer().getPluginManager().isTransitiveDependency(pluginClassLoader.getConfiguration(), library.getConfiguration());
+ }));
+ }
+
+ @Override
+ public PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader) {
+ return this.registerGroup(classLoader, this.globalGroup);
+ }
+
+ @Override
+ public PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access) {
+ List<ConfiguredPluginClassLoader> allowedLoaders = new ArrayList<>();
+ for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalGroup.getClassLoaders()) {
+ if (access.canAccess(configuredPluginClassLoader)) {
+ allowedLoaders.add(configuredPluginClassLoader);
+ }
+ }
+
+ return this.registerGroup(classLoader, new StaticPluginClassLoaderGroup(allowedLoaders, access));
+ }
+
+ private PluginClassLoaderGroup registerGroup(ConfiguredPluginClassLoader classLoader, PluginClassLoaderGroup group) {
+ // Now add this classloader to any groups that allows it (includes global)
+ for (PluginClassLoaderGroup loaderGroup : this.groups) {
+ if (loaderGroup.getAccess().canAccess(classLoader)) {
+ loaderGroup.add(classLoader);
+ }
+ }
+
+ group = new LockingClassLoaderGroup(group);
+ this.groups.add(group);
+ return group;
+ }
+
+ @Override
+ public void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ this.globalGroup.remove(configuredPluginClassLoader);
+ for (PluginClassLoaderGroup group : this.groups) {
+ group.remove(configuredPluginClassLoader);
+ }
+ }
+
+ @Override
+ public boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader) {
+ if (this.globalGroup.getClassLoaders().contains(pluginLoader)) {
+ return false;
+ } else {
+ this.globalGroup.getClassLoaders().add(pluginLoader);
+ return true;
+ }
+ }
+
+ // Debug only
+ @ApiStatus.Internal
+ public GlobalPluginClassLoaderGroup getGlobalGroup() {
+ return this.globalGroup;
+ }
+
+ // Debug only
+ @ApiStatus.Internal
+ public List<PluginClassLoaderGroup> getGroups() {
+ return this.groups;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..23b6cb297f46c9c2b2944a3ab4031c31414620ad
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java
@@ -0,0 +1,69 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@ApiStatus.Internal
+public abstract class SimpleListPluginClassLoaderGroup implements PluginClassLoaderGroup {
+
+ private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization");
+
+ protected final List<ConfiguredPluginClassLoader> classloaders;
+
+ protected SimpleListPluginClassLoaderGroup() {
+ this(new CopyOnWriteArrayList<>());
+ }
+
+ protected SimpleListPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders) {
+ this.classloaders = classloaders;
+ }
+
+ @Override
+ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
+ if (!DISABLE_CLASS_PRIORITIZATION) {
+ try {
+ return this.lookupClass(name, false, requester); // First check the requester
+ } catch (ClassNotFoundException ignored) {
+ }
+ }
+
+ for (ConfiguredPluginClassLoader loader : this.classloaders) {
+ try {
+ return this.lookupClass(name, resolve, loader);
+ } catch (ClassNotFoundException ignored) {
+ }
+ }
+
+ return null;
+ }
+
+ protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
+ return current.loadClass(name, resolve, false, true);
+ }
+
+ @Override
+ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ this.classloaders.remove(configuredPluginClassLoader);
+ }
+
+ @Override
+ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ this.classloaders.add(configuredPluginClassLoader);
+ }
+
+ public List<ConfiguredPluginClassLoader> getClassLoaders() {
+ return classloaders;
+ }
+
+ @Override
+ public String toString() {
+ return "SimpleListPluginClassLoaderGroup{" +
+ "classloaders=" + this.classloaders +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b670bd6b35ae7f56488a9b50df54709a0b28901
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java
@@ -0,0 +1,60 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+@ApiStatus.Internal
+public class SingletonPluginClassLoaderGroup implements PluginClassLoaderGroup {
+
+ private final ConfiguredPluginClassLoader configuredPluginClassLoader;
+ private final Access access;
+
+ public SingletonPluginClassLoaderGroup(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ this.configuredPluginClassLoader = configuredPluginClassLoader;
+ this.access = new Access();
+ }
+
+ @Override
+ public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
+ try {
+ return this.configuredPluginClassLoader.loadClass(name, resolve, false, true);
+ } catch (ClassNotFoundException ignored) {
+ }
+
+ return null;
+ }
+
+ @Override
+ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ }
+
+ @Override
+ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ }
+
+ @Override
+ public ClassLoaderAccess getAccess() {
+ return this.access;
+ }
+
+ @ApiStatus.Internal
+ private class Access implements ClassLoaderAccess {
+
+ @Override
+ public boolean canAccess(ConfiguredPluginClassLoader classLoader) {
+ return SingletonPluginClassLoaderGroup.this.configuredPluginClassLoader == classLoader;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return "SingletonPluginClassLoaderGroup{" +
+ "configuredPluginClassLoader=" + this.configuredPluginClassLoader +
+ ", access=" + this.access +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d26367524389388be163ae3120c1d2bf55cfef7
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java
@@ -0,0 +1,49 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.function.Predicate;
+
+/**
+ * Spigot classloaders have the ability to see everything.
+ * However, libraries are ONLY shared depending on their dependencies.
+ */
+@ApiStatus.Internal
+public class SpigotPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
+
+ private final Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate;
+
+ public SpigotPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate) {
+ super(globalPluginClassLoaderGroup.getClassLoaders());
+ this.libraryClassloaderPredicate = libraryClassloaderPredicate;
+ }
+
+ // Mirrors global list
+ @Override
+ public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ }
+
+ @Override
+ public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ }
+
+ @Override
+ protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
+ return current.loadClass(name, resolve, false, this.libraryClassloaderPredicate.test(current));
+ }
+
+ @Override
+ public ClassLoaderAccess getAccess() {
+ return v -> true;
+ }
+
+ @Override
+ public String toString() {
+ return "SpigotPluginClassLoaderGroup{" +
+ "libraryClassloaderPredicate=" + this.libraryClassloaderPredicate +
+ ", classloaders=" + this.classloaders +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f7085b06271adf8a37485f4c9c9b8af605dd27d
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java
@@ -0,0 +1,31 @@
+package io.papermc.paper.plugin.entrypoint.classloader.group;
+
+import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
+import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.List;
+
+@ApiStatus.Internal
+public class StaticPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
+
+ private final ClassLoaderAccess access;
+
+ public StaticPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders, ClassLoaderAccess access) {
+ super(classloaders);
+ this.access = access;
+ }
+
+ @Override
+ public ClassLoaderAccess getAccess() {
+ return this.access;
+ }
+
+ @Override
+ public String toString() {
+ return "StaticPluginClassLoaderGroup{" +
+ "access=" + this.access +
+ ", classloaders=" + this.classloaders +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..f43295fdeaa587cf30c35a1d545167071d58ce4b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java
@@ -0,0 +1,9 @@
+package io.papermc.paper.plugin.entrypoint.dependency;
+
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+
+public interface DependencyContextHolder {
+
+ void setContext(DependencyContext context);
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..1af3c3434eb1f3b00857b17a07f42e51086c1e2b
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java
@@ -0,0 +1,45 @@
+package io.papermc.paper.plugin.entrypoint.dependency;
+
+import com.google.common.graph.MutableGraph;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("UnstableApiUsage")
+public class DependencyUtil {
+
+ @NotNull
+ public static MutableGraph<String> buildDependencyGraph(@NotNull MutableGraph<String> dependencyGraph, @NotNull PluginMeta configuration) {
+ List<String> dependencies = new ArrayList<>();
+ dependencies.addAll(configuration.getPluginDependencies());
+ dependencies.addAll(configuration.getPluginSoftDependencies());
+
+ return buildDependencyGraph(dependencyGraph, configuration.getName(), dependencies, configuration.getLoadBeforePlugins());
+ }
+
+ @NotNull
+ public static MutableGraph<String> buildDependencyGraph(@NotNull MutableGraph<String> dependencyGraph, String identifier, @NotNull Iterable<String> depends, @NotNull Iterable<String> loadBefore) {
+ for (String dependency : depends) {
+ dependencyGraph.putEdge(identifier, dependency);
+ }
+
+ for (String loadBeforeTarget : loadBefore) {
+ dependencyGraph.putEdge(loadBeforeTarget, identifier);
+ }
+
+ dependencyGraph.addNode(identifier); // Make sure dependencies at least have a node
+ return dependencyGraph;
+ }
+
+ // This adds a provided plugin to another plugin, basically making it seem like a "dependency"
+ // in order to have plugins that need the provided plugin to load after the specified plugin name
+ @NotNull
+ public static MutableGraph<String> addProvidedPlugin(@NotNull MutableGraph<String> dependencyGraph, @NotNull String pluginName, @NotNull String providedName) {
+ dependencyGraph.putEdge(pluginName, providedName);
+
+ return dependencyGraph;
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java
new file mode 100644
index 0000000000000000000000000000000000000000..6f201a8131ca9631ac4af62c75e6f2e889cb5eae
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java
@@ -0,0 +1,43 @@
+package io.papermc.paper.plugin.entrypoint.dependency;
+
+import com.google.common.graph.Graph;
+import com.google.common.graph.Graphs;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+
+import java.util.Set;
+
+@SuppressWarnings("UnstableApiUsage")
+public class GraphDependencyContext implements DependencyContext {
+
+ private final Graph<String> dependencyGraph;
+
+ public GraphDependencyContext(Graph<String> dependencyGraph) {
+ this.dependencyGraph = dependencyGraph;
+ }
+
+ @Override
+ public boolean isTransitiveDependency(PluginMeta plugin, PluginMeta depend) {
+ String pluginIdentifier = plugin.getName();
+
+ if (this.dependencyGraph.nodes().contains(pluginIdentifier)) {
+ Set<String> reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier);
+ if (reachableNodes.contains(depend.getName())) {
+ return true;
+ }
+ for (String provided : depend.getProvidedPlugins()) {
+ if (reachableNodes.contains(provided)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.dependencyGraph.nodes().contains(pluginIdentifier);
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java
new file mode 100644
index 0000000000000000000000000000000000000000..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.
+ *
+ * <p>
+ * See:<br>
+ * D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975),
+ * pp. 77-84.
+ *
+ * @param <V> the vertex type.
+ *
+ * @author Nikolay Ognyanov
+ */
+public class JohnsonSimpleCycles<V>
+{
+ // The graph.
+ private Graph<V> graph;
+
+ // The main state of the algorithm.
+ private Consumer<List<V>> cycleConsumer = null;
+ private V[] iToV = null;
+ private Map<V, Integer> vToI = null;
+ private Set<V> blocked = null;
+ private Map<V, Set<V>> bSets = null;
+ private ArrayDeque<V> stack = null;
+
+ // The state of the embedded Tarjan SCC algorithm.
+ private List<Set<V>> foundSCCs = null;
+ private int index = 0;
+ private Map<V, Integer> vIndex = null;
+ private Map<V, Integer> vLowlink = null;
+ private ArrayDeque<V> path = null;
+ private Set<V> pathSet = null;
+
+ /**
+ * Create a simple cycle finder for the specified graph.
+ *
+ * @param graph - the DirectedGraph in which to find cycles.
+ *
+ * @throws IllegalArgumentException if the graph argument is <code>
+ * null</code>.
+ */
+ public JohnsonSimpleCycles(Graph<V> graph)
+ {
+ Preconditions.checkState(graph.isDirected(), "Graph must be directed");
+ this.graph = graph;
+ }
+
+ /**
+ * Find the simple cycles of the graph.
+ *
+ * @return The list of all simple cycles. Possibly empty but never <code>null</code>.
+ */
+ public List<List<V>> findSimpleCycles()
+ {
+ List<List<V>> result = new ArrayList<>();
+ findSimpleCycles(result::add);
+ return result;
+ }
+
+ /**
+ * Find the simple cycles of the graph.
+ *
+ * @param consumer Consumer that will be called with each cycle found.
+ */
+ public void findSimpleCycles(Consumer<List<V>> consumer)
+ {
+ if (graph == null) {
+ throw new IllegalArgumentException("Null graph.");
+ }
+ initState(consumer);
+
+ int startIndex = 0;
+ int size = graph.nodes().size();
+ while (startIndex < size) {
+ Pair<Graph<V>, Integer> minSCCGResult = findMinSCSG(startIndex);
+ if (minSCCGResult != null) {
+ startIndex = minSCCGResult.getSecond();
+ Graph<V> scg = minSCCGResult.getFirst();
+ V startV = toV(startIndex);
+ for (V v : scg.successors(startV)) {
+ blocked.remove(v);
+ getBSet(v).clear();
+ }
+ findCyclesInSCG(startIndex, startIndex, scg);
+ startIndex++;
+ } else {
+ break;
+ }
+ }
+
+ clearState();
+ }
+
+ private Pair<Graph<V>, Integer> findMinSCSG(int startIndex)
+ {
+ /*
+ * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph
+ * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected
+ * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum
+ * (among those SCCs) vertex index. We return that index together with the graph.
+ */
+ initMinSCGState();
+
+ List<Set<V>> foundSCCs = findSCCS(startIndex);
+
+ // find the SCC with the minimum index
+ int minIndexFound = Integer.MAX_VALUE;
+ Set<V> minSCC = null;
+ for (Set<V> scc : foundSCCs) {
+ for (V v : scc) {
+ int t = toI(v);
+ if (t < minIndexFound) {
+ minIndexFound = t;
+ minSCC = scc;
+ }
+ }
+ }
+ if (minSCC == null) {
+ return null;
+ }
+
+ // build a graph for the SCC found
+ MutableGraph<V> dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build();
+
+ for (V v : minSCC) {
+ for (V w : minSCC) {
+ if (graph.hasEdgeConnecting(v, w)) {
+ dependencyGraph.putEdge(v, w);
+ }
+ }
+ }
+
+ Pair<Graph<V>, Integer> result = Pair.of(dependencyGraph, minIndexFound);
+ clearMinSCCState();
+ return result;
+ }
+
+ private List<Set<V>> findSCCS(int startIndex)
+ {
+ // Find SCCs in the subgraph induced
+ // by vertices startIndex and beyond.
+ // A call to StrongConnectivityAlgorithm
+ // would be too expensive because of the
+ // need to materialize the subgraph.
+ // So - do a local search by the Tarjan's
+ // algorithm and pretend that vertices
+ // with an index smaller than startIndex
+ // do not exist.
+ for (V v : graph.nodes()) {
+ int vI = toI(v);
+ if (vI < startIndex) {
+ continue;
+ }
+ if (!vIndex.containsKey(v)) {
+ getSCCs(startIndex, vI);
+ }
+ }
+ List<Set<V>> result = foundSCCs;
+ foundSCCs = null;
+ return result;
+ }
+
+ private void getSCCs(int startIndex, int vertexIndex)
+ {
+ V vertex = toV(vertexIndex);
+ vIndex.put(vertex, index);
+ vLowlink.put(vertex, index);
+ index++;
+ path.push(vertex);
+ pathSet.add(vertex);
+
+ Set<V> edges = graph.successors(vertex);
+ for (V successor : edges) {
+ int successorIndex = toI(successor);
+ if (successorIndex < startIndex) {
+ continue;
+ }
+ if (!vIndex.containsKey(successor)) {
+ getSCCs(startIndex, successorIndex);
+ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor)));
+ } else if (pathSet.contains(successor)) {
+ vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor)));
+ }
+ }
+ if (vLowlink.get(vertex).equals(vIndex.get(vertex))) {
+ Set<V> result = new HashSet<>();
+ V temp;
+ do {
+ temp = path.pop();
+ pathSet.remove(temp);
+ result.add(temp);
+ } while (!vertex.equals(temp));
+ if (result.size() == 1) {
+ V v = result.iterator().next();
+ if (graph.edges().contains(vertex)) {
+ foundSCCs.add(result);
+ }
+ } else {
+ foundSCCs.add(result);
+ }
+ }
+ }
+
+ private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph<V> scg)
+ {
+ /*
+ * Find cycles in a strongly connected graph per Johnson.
+ */
+ boolean foundCycle = false;
+ V vertex = toV(vertexIndex);
+ stack.push(vertex);
+ blocked.add(vertex);
+
+ for (V successor : scg.successors(vertex)) {
+ int successorIndex = toI(successor);
+ if (successorIndex == startIndex) {
+ List<V> cycle = new ArrayList<>(stack.size());
+ stack.descendingIterator().forEachRemaining(cycle::add);
+ cycleConsumer.accept(cycle);
+ foundCycle = true;
+ } else if (!blocked.contains(successor)) {
+ boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg);
+ foundCycle = foundCycle || gotCycle;
+ }
+ }
+ if (foundCycle) {
+ unblock(vertex);
+ } else {
+ for (V w : scg.successors(vertex)) {
+ Set<V> bSet = getBSet(w);
+ bSet.add(vertex);
+ }
+ }
+ stack.pop();
+ return foundCycle;
+ }
+
+ private void unblock(V vertex)
+ {
+ blocked.remove(vertex);
+ Set<V> bSet = getBSet(vertex);
+ while (bSet.size() > 0) {
+ V w = bSet.iterator().next();
+ bSet.remove(w);
+ if (blocked.contains(w)) {
+ unblock(w);
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void initState(Consumer<List<V>> consumer)
+ {
+ cycleConsumer = consumer;
+ iToV = (V[]) graph.nodes().toArray();
+ vToI = new HashMap<>();
+ blocked = new HashSet<>();
+ bSets = new HashMap<>();
+ stack = new ArrayDeque<>();
+
+ for (int i = 0; i < iToV.length; i++) {
+ vToI.put(iToV[i], i);
+ }
+ }
+
+ private void clearState()
+ {
+ cycleConsumer = null;
+ iToV = null;
+ vToI = null;
+ blocked = null;
+ bSets = null;
+ stack = null;
+ }
+
+ private void initMinSCGState()
+ {
+ index = 0;
+ foundSCCs = new ArrayList<>();
+ vIndex = new HashMap<>();
+ vLowlink = new HashMap<>();
+ path = new ArrayDeque<>();
+ pathSet = new HashSet<>();
+ }
+
+ private void clearMinSCCState()
+ {
+ index = 0;
+ foundSCCs = null;
+ vIndex = null;
+ vLowlink = null;
+ path = null;
+ pathSet = null;
+ }
+
+ private Integer toI(V vertex)
+ {
+ return vToI.get(vertex);
+ }
+
+ private V toV(Integer i)
+ {
+ return iToV[i];
+ }
+
+ private Set<V> getBSet(V v)
+ {
+ // B sets typically not all needed,
+ // so instantiate lazily.
+ return bSets.computeIfAbsent(v, k -> new HashSet<>());
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..8bc11d7e9341a31382c1b055565cc8d5fd3203ea
--- /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<T> implements ProviderLoadingStrategy<T> {
+
+ private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy");
+ private final ProviderConfiguration<T> configuration;
+
+ public LegacyPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
+ this.configuration = onLoad;
+ }
+
+ @Override
+ public List<T> loadProviders(List<PluginProvider<T>> providers) {
+ List<T> javapluginsLoaded = new ArrayList<>();
+ MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
+ GraphDependencyContext dependencyContext = new GraphDependencyContext(dependencyGraph);
+
+ Map<String, PluginProvider<T>> providersToLoad = new HashMap<>();
+ Set<String> loadedPlugins = new HashSet<>();
+ Map<String, String> pluginsProvided = new HashMap<>();
+ Map<String, Collection<String>> dependencies = new HashMap<>();
+ Map<String, Collection<String>> softDependencies = new HashMap<>();
+
+ for (PluginProvider<T> provider : providers) {
+ PluginMeta configuration = provider.getMeta();
+
+ PluginProvider<T> replacedProvider = providersToLoad.put(configuration.getName(), provider);
+ if (replacedProvider != null) {
+ LOGGER.severe(String.format(
+ "Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'",
+ configuration.getName(),
+ provider.getSource(),
+ replacedProvider.getSource(),
+ replacedProvider.getParentSource()
+ ));
+ }
+
+ String removedProvided = pluginsProvided.remove(configuration.getName());
+ if (removedProvided != null) {
+ LOGGER.warning(String.format(
+ "Ambiguous plugin name `%s'. It is also provided by `%s'",
+ configuration.getName(),
+ removedProvided
+ ));
+ }
+
+ for (String provided : configuration.getProvidedPlugins()) {
+ PluginProvider<T> pluginProvider = providersToLoad.get(provided);
+
+ if (pluginProvider != null) {
+ LOGGER.warning(String.format(
+ "`%s provides `%s' while this is also the name of `%s' in `%s'",
+ provider.getSource(),
+ provided,
+ pluginProvider.getSource(),
+ provider.getParentSource()
+ ));
+ } else {
+ String replacedPlugin = pluginsProvided.put(provided, configuration.getName());
+ if (replacedPlugin != null) {
+ LOGGER.warning(String.format(
+ "`%s' is provided by both `%s' and `%s'",
+ provided,
+ configuration.getName(),
+ replacedPlugin
+ ));
+ }
+ }
+ }
+
+ Collection<String> softDependencySet = this.configuration.optionalDependencies(provider);
+ if (softDependencySet != null && !softDependencySet.isEmpty()) {
+ if (softDependencies.containsKey(configuration.getName())) {
+ // Duplicates do not matter, they will be removed together if applicable
+ softDependencies.get(configuration.getName()).addAll(softDependencySet);
+ } else {
+ softDependencies.put(configuration.getName(), new LinkedList<String>(softDependencySet));
+ }
+
+ for (String depend : softDependencySet) {
+ dependencyGraph.putEdge(configuration.getName(), depend);
+ }
+ }
+
+ Collection<String> dependencySet = this.configuration.requiredDependencies(provider);
+ if (dependencySet != null && !dependencySet.isEmpty()) {
+ dependencies.put(configuration.getName(), new LinkedList<String>(dependencySet));
+
+ for (String depend : dependencySet) {
+ dependencyGraph.putEdge(configuration.getName(), depend);
+ }
+ }
+
+ Collection<String> loadBeforeSet = this.configuration.loadBeforeDependencies(provider);
+ if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) {
+ for (String loadBeforeTarget : loadBeforeSet) {
+ if (softDependencies.containsKey(loadBeforeTarget)) {
+ softDependencies.get(loadBeforeTarget).add(configuration.getName());
+ } else {
+ // softDependencies is never iterated, so 'ghost' plugins aren't an issue
+ Collection<String> shortSoftDependency = new LinkedList<String>();
+ shortSoftDependency.add(configuration.getName());
+ softDependencies.put(loadBeforeTarget, shortSoftDependency);
+ }
+
+ dependencyGraph.putEdge(loadBeforeTarget, configuration.getName());
+ }
+ }
+ }
+
+ while (!providersToLoad.isEmpty()) {
+ boolean missingDependency = true;
+ Iterator<Map.Entry<String, PluginProvider<T>>> providerIterator = providersToLoad.entrySet().iterator();
+
+ while (providerIterator.hasNext()) {
+ Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
+ String providerIdentifier = entry.getKey();
+
+ if (dependencies.containsKey(providerIdentifier)) {
+ Iterator<String> dependencyIterator = dependencies.get(providerIdentifier).iterator();
+ final Set<String> missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends
+
+ while (dependencyIterator.hasNext()) {
+ String dependency = dependencyIterator.next();
+
+ // Dependency loaded
+ if (loadedPlugins.contains(dependency)) {
+ dependencyIterator.remove();
+
+ // We have a dependency not found
+ } else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) {
+ // Paper start
+ missingHardDependencies.add(dependency);
+ }
+ }
+ if (!missingHardDependencies.isEmpty()) {
+ // Paper end
+ missingDependency = false;
+ providerIterator.remove();
+ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
+ softDependencies.remove(providerIdentifier);
+ dependencies.remove(providerIdentifier);
+
+ LOGGER.log(
+ Level.SEVERE,
+ "Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper
+ new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper
+ }
+
+ if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) {
+ dependencies.remove(providerIdentifier);
+ }
+ }
+ if (softDependencies.containsKey(providerIdentifier)) {
+ Iterator<String> softDependencyIterator = softDependencies.get(providerIdentifier).iterator();
+
+ while (softDependencyIterator.hasNext()) {
+ String softDependency = softDependencyIterator.next();
+
+ // Soft depend is no longer around
+ if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) {
+ softDependencyIterator.remove();
+ }
+ }
+
+ if (softDependencies.get(providerIdentifier).isEmpty()) {
+ softDependencies.remove(providerIdentifier);
+ }
+ }
+ if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) {
+ // We're clear to load, no more soft or hard dependencies left
+ PluginProvider<T> file = providersToLoad.get(providerIdentifier);
+ providerIterator.remove();
+ pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
+ missingDependency = false;
+
+ try {
+ this.configuration.applyContext(file, dependencyContext);
+ T loadedPlugin = file.createInstance();
+
+ if (this.configuration.load(file, loadedPlugin)) {
+ loadedPlugins.add(file.getMeta().getName());
+ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
+ javapluginsLoaded.add(loadedPlugin);
+ }
+
+ } catch (Exception ex) {
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
+ }
+ }
+ }
+
+ if (missingDependency) {
+ // We now iterate over plugins until something loads
+ // This loop will ignore soft dependencies
+ providerIterator = providersToLoad.entrySet().iterator();
+
+ while (providerIterator.hasNext()) {
+ Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
+ String plugin = entry.getKey();
+
+ if (!dependencies.containsKey(plugin)) {
+ softDependencies.remove(plugin);
+ missingDependency = false;
+ PluginProvider<T> file = entry.getValue();
+ providerIterator.remove();
+
+ try {
+ this.configuration.applyContext(file, dependencyContext);
+ T loadedPlugin = file.createInstance();
+
+ if (this.configuration.load(file, loadedPlugin)) {
+ loadedPlugins.add(file.getMeta().getName());
+ loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
+ javapluginsLoaded.add(loadedPlugin);
+ }
+ break;
+ } catch (Exception ex) {
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
+ }
+ }
+ }
+ // We have no plugins left without a depend
+ if (missingDependency) {
+ softDependencies.clear();
+ dependencies.clear();
+ Iterator<PluginProvider<T>> failedPluginIterator = providersToLoad.values().iterator();
+
+ while (failedPluginIterator.hasNext()) {
+ PluginProvider<T> file = failedPluginIterator.next();
+ failedPluginIterator.remove();
+ LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper
+ }
+ }
+ }
+ }
+
+ return javapluginsLoaded;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0363af157131a2b42c00028a260e9c3d3c543de
--- /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<T> implements ProviderLoadingStrategy<T> {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final ProviderConfiguration<T> configuration;
+
+ public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
+ this.configuration = onLoad;
+ }
+
+ @Override
+ public List<T> loadProviders(List<PluginProvider<T>> pluginProviders) {
+ MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
+ Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>();
+ List<PluginProvider<T>> validatedProviders = new ArrayList<>();
+
+ // Populate provider map
+ for (PluginProvider<T> provider : pluginProviders) {
+ PluginMeta providerConfig = provider.getMeta();
+ PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider);
+
+ PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry);
+ if (replacedProvider != null) {
+ LOGGER.error(String.format(
+ "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'",
+ providerConfig.getName(),
+ provider.getSource(),
+ replacedProvider.provider.getSource(),
+ replacedProvider.provider.getParentSource()
+ ));
+ }
+
+ for (String extra : providerConfig.getProvidedPlugins()) {
+ PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry);
+ if (replacedExtraProvider != null) {
+ LOGGER.warn(String.format(
+ "`%s' is provided by both `%s' and `%s'",
+ extra,
+ providerConfig.getName(),
+ replacedExtraProvider.provider.getMeta().getName()
+ ));
+ }
+ }
+ }
+
+ // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid
+ for (PluginProvider<T> provider : pluginProviders) {
+ PluginMeta configuration = provider.getMeta();
+
+ // Populate missing dependencies to capture if there are multiple missing ones.
+ List<String> missingDependencies = new ArrayList<>();
+ for (String hardDependency : this.configuration.requiredDependencies(provider)) {
+ if (!providerMap.containsKey(hardDependency)) {
+ missingDependencies.add(hardDependency);
+ }
+ }
+
+ if (missingDependencies.isEmpty()) {
+ validatedProviders.add(provider);
+ } else {
+ LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
+ // Because the validator is invalid, remove it from the provider map
+ providerMap.remove(configuration.getName());
+ }
+ }
+
+ for (PluginProvider<?> validated : validatedProviders) {
+ PluginMeta configuration = validated.getMeta();
+
+ // Build a validated provider's dependencies into the graph
+ DependencyUtil.buildDependencyGraph(dependencyGraph, configuration);
+
+ // Add the provided plugins to the graph as well
+ for (String provides : configuration.getProvidedPlugins()) {
+ DependencyUtil.addProvidedPlugin(dependencyGraph, configuration.getName(), provides);
+ }
+ }
+
+ // Reverse the topographic search to let us see which providers we can load first.
+ List<String> reversedTopographicSort;
+ try {
+ reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(dependencyGraph));
+ } catch (TopographicGraphSorter.GraphCycleException exception) {
+ throw new PluginGraphCycleException(new JohnsonSimpleCycles<>(dependencyGraph).findSimpleCycles());
+ }
+
+ GraphDependencyContext graphDependencyContext = new GraphDependencyContext(dependencyGraph);
+ List<T> loadedPlugins = new ArrayList<>();
+ for (String providerIdentifier : reversedTopographicSort) {
+ // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist.
+ // The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just
+ // nicer to do it here TBH.
+ PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier);
+ if (retrievedProviderEntry == null || retrievedProviderEntry.provided) {
+ // OR if this was already provided (most likely from a plugin that already "provides" that dependency)
+ // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways
+ continue; // Skip provider that doesn't exist....
+ }
+ retrievedProviderEntry.provided = true;
+ PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider;
+ try {
+ this.configuration.applyContext(retrievedProvider, graphDependencyContext);
+
+ T instance = retrievedProvider.createInstance();
+ if (this.configuration.load(retrievedProvider, instance)) {
+ loadedPlugins.add(instance);
+ }
+ } catch (Exception ex) {
+ LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
+ }
+ }
+
+ return loadedPlugins;
+ }
+
+ private static class PluginProviderEntry<T> {
+
+ private final PluginProvider<T> provider;
+ private boolean provided;
+
+ private PluginProviderEntry(PluginProvider<T> provider) {
+ this.provider = provider;
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<List<String>> cycles;
+
+ public PluginGraphCycleException(List<List<String>> cycles) {
+ this.cycles = cycles;
+ }
+
+ public List<List<String>> getCycles() {
+ return this.cycles;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..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 <T>
+ */
+public interface ProviderConfiguration<T> {
+
+ void applyContext(PluginProvider<T> provider, DependencyContext dependencyContext);
+
+ boolean load(PluginProvider<T> provider, T provided);
+
+ List<String> requiredDependencies(PluginProvider<T> provider);
+
+ List<String> optionalDependencies(PluginProvider<T> provider);
+
+ List<String> loadBeforeDependencies(PluginProvider<T> provider);
+
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8d5ce5f137a960e353c6722e1f20f9b342d4ba5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java
@@ -0,0 +1,16 @@
+package io.papermc.paper.plugin.entrypoint.strategy;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+
+import java.util.List;
+
+/**
+ * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
+ * <p>
+ * Returns providers loaded.
+ * @param <P> provider type
+ */
+public interface ProviderLoadingStrategy<P> {
+
+ List<P> loadProviders(List<PluginProvider<P>> providers);
+}
diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java
new file mode 100644
index 0000000000000000000000000000000000000000..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 <N> List<N> sortGraph(Graph<N> graph) throws PluginGraphCycleException {
+ List<N> sorted = new ArrayList<>();
+ Deque<N> roots = new ArrayDeque<>();
+ Map<N, Integer> nonRoots = new HashMap<>();
+
+ for (N node : graph.nodes()) {
+ // Is a node being referred to by any other nodes?
+ int degree = graph.inDegree(node);
+ if (degree == 0) {
+ // Is a root
+ roots.add(node);
+ } else {
+ // Isn't a root, the number represents how many nodes connect to it.
+ nonRoots.put(node, degree);
+ }
+ }
+
+ // Pick from nodes that aren't referred to anywhere else
+ while (!roots.isEmpty()) {
+ N next = roots.remove();
+
+ for (N successor : graph.successors(next)) {
+ // Traverse through, moving down a degree
+ int newInDegree = nonRoots.get(successor) - 1;
+
+ if (newInDegree == 0) {
+ nonRoots.remove(successor);
+ roots.add(successor);
+ } else {
+ nonRoots.put(successor, newInDegree);
+ }
+
+ }
+ sorted.add(next);
+ }
+
+ if (!nonRoots.isEmpty()) {
+ throw new GraphCycleException();
+ }
+
+ return sorted;
+ }
+
+ public static class GraphCycleException extends RuntimeException {
+
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java
new file mode 100644
index 0000000000000000000000000000000000000000..d49dd7d17ce7ac17efb19b33de163015787213f7
--- /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<ClassPathLibrary> libraries = new ArrayList<>();
+
+ private final PluginProviderContext context;
+
+ public PaperClasspathBuilder(PluginProviderContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public @NotNull PluginProviderContext getContext() {
+ return this.context;
+ }
+
+ @Override
+ public @NotNull PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary) {
+ this.libraries.add(classPathLibrary);
+ return this;
+ }
+
+ public PaperPluginClassLoader buildClassLoader(Logger logger, Path source, JarFile jarFile, PaperPluginMeta configuration) {
+ PaperLibraryStore paperLibraryStore = new PaperLibraryStore();
+ for (ClassPathLibrary library : this.libraries) {
+ library.register(paperLibraryStore);
+ }
+
+ List<Path> paths = paperLibraryStore.getPaths();
+ URL[] urls = new URL[paths.size()];
+ for (int i = 0; i < paths.size(); i++) {
+ Path path = paperLibraryStore.getPaths().get(i);
+ try {
+ urls[i] = path.toUri().toURL();
+ } catch (MalformedURLException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ try {
+ return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls));
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<Path> paths = new ArrayList<>();
+
+ @Override
+ public void addLibrary(@NotNull Path library) {
+ this.paths.add(library);
+ }
+
+ public List<Path> getPaths() {
+ return this.paths;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..a47c0c1dedf5f8cd7006b170639676429c17d74d
--- /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<JavaPlugin> provided = new ArrayList<>();
+
+ @Override
+ public void register(PluginProvider<JavaPlugin> provider) {
+ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
+ LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager.");
+ return;
+ }
+ super.register(provider);
+ /*
+ Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage,
+ as putting it into the entrypoint handlers doesn't make much sense.
+ */
+ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
+ }
+
+ @Override
+ public void processProvided(JavaPlugin provided) {
+ super.processProvided(provided);
+ this.provided.add(provided);
+ }
+
+ @Override
+ public boolean exitOnCycleDependencies() {
+ return false;
+ }
+
+ public List<JavaPlugin> getLoaded() {
+ return this.provided;
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<String, Permission> permissions = new HashMap<>();
+ private final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<>();
+ private final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<>();
+ private final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<>();
+
+ public NormalPaperPermissionManager() {
+ this.defaultPerms().put(true, new LinkedHashSet<>());
+ this.defaultPerms().put(false, new LinkedHashSet<>());
+ }
+
+ @Override
+ public Map<String, Permission> permissions() {
+ return this.permissions;
+ }
+
+ @Override
+ public Map<Boolean, Set<Permission>> defaultPerms() {
+ return this.defaultPerms;
+ }
+
+ @Override
+ public Map<String, Map<Permissible, Boolean>> permSubs() {
+ return this.permSubs;
+ }
+
+ @Override
+ public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
+ return this.defSubs;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
+ this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
+ }
+
+ }
+
+ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
+ this.registerEvent(event, listener, priority, executor, plugin, false);
+ }
+
+ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
+ if (!plugin.isEnabled()) {
+ throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
+ }
+
+ executor = new TimedEventExecutor(executor, plugin, null, event);
+ this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
+ }
+
+ @NotNull
+ private HandlerList getEventListeners(@NotNull Class<? extends Event> type) {
+ try {
+ Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList");
+ method.setAccessible(true);
+ return (HandlerList) method.invoke(null);
+ } catch (Exception e) {
+ throw new IllegalPluginAccessException(e.toString());
+ }
+ }
+
+ @NotNull
+ private Class<? extends Event> getRegistrationClass(@NotNull Class<? extends Event> clazz) {
+ try {
+ clazz.getDeclaredMethod("getHandlerList");
+ return clazz;
+ } catch (NoSuchMethodException e) {
+ if (clazz.getSuperclass() != null
+ && !clazz.getSuperclass().equals(Event.class)
+ && Event.class.isAssignableFrom(clazz.getSuperclass())) {
+ return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
+ } else {
+ throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!");
+ }
+ }
+ }
+
+ // JavaPluginLoader
+ @NotNull
+ public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) {
+ Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<>();
+
+ Set<Method> methods;
+ try {
+ Class<?> listenerClazz = listener.getClass();
+ methods = Sets.union(
+ Set.of(listenerClazz.getMethods()),
+ Set.of(listenerClazz.getDeclaredMethods())
+ );
+ } catch (NoClassDefFoundError e) {
+ plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
+ return ret;
+ }
+
+ for (final Method method : methods) {
+ final EventHandler eh = method.getAnnotation(EventHandler.class);
+ if (eh == null) continue;
+ // Do not register bridge or synthetic methods to avoid event duplication
+ // Fixes SPIGOT-893
+ if (method.isBridge() || method.isSynthetic()) {
+ continue;
+ }
+ final Class<?> checkClass;
+ if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
+ plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
+ continue;
+ }
+ final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class);
+ method.setAccessible(true);
+ Set<RegisteredListener> eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>());
+
+ for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
+ // This loop checks for extending deprecated events
+ if (clazz.getAnnotation(Deprecated.class) != null) {
+ Warning warning = clazz.getAnnotation(Warning.class);
+ Warning.WarningState warningState = this.server.getWarningState();
+ if (!warningState.printFor(warning)) {
+ break;
+ }
+ plugin.getLogger().log(
+ Level.WARNING,
+ String.format(
+ "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.",
+ plugin.getPluginMeta().getDisplayName(),
+ clazz.getName(),
+ method.toGenericString(),
+ (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
+ Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())),
+ warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null);
+ break;
+ }
+ }
+
+ EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass);
+ eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
+ }
+ return ret;
+ }
+
+ public void clearEvents() {
+ HandlerList.unregisterAll();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<String, Permission> permissions();
+
+ public abstract Map<Boolean, Set<Permission>> defaultPerms();
+
+ public abstract Map<String, Map<Permissible, Boolean>> permSubs();
+
+ public abstract Map<Boolean, Map<Permissible, Boolean>> defSubs();
+
+ @Override
+ @Nullable
+ public Permission getPermission(@NotNull String name) {
+ return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH));
+ }
+
+ @Override
+ public void addPermission(@NotNull Permission perm) {
+ this.addPermission(perm, true);
+ }
+
+ @Override
+ public void addPermissions(@NotNull List<Permission> permissions) {
+ for (Permission permission : permissions) {
+ this.addPermission(permission, false);
+ }
+ this.dirtyPermissibles();
+ }
+
+ // Allow suppressing permission default calculations
+ private void addPermission(@NotNull Permission perm, boolean dirty) {
+ String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH);
+
+ if (this.permissions().containsKey(name)) {
+ throw new IllegalArgumentException("The permission " + name + " is already defined!");
+ }
+
+ this.permissions().put(name, perm);
+ this.calculatePermissionDefault(perm, dirty);
+ }
+
+ @Override
+ @NotNull
+ public Set<Permission> getDefaultPermissions(boolean op) {
+ return ImmutableSet.copyOf(this.defaultPerms().get(op));
+ }
+
+
+ @Override
+ public void removePermission(@NotNull Permission perm) {
+ this.removePermission(perm.getName());
+ }
+
+
+ @Override
+ public void removePermission(@NotNull String name) {
+ this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH));
+ }
+
+ @Override
+ public void recalculatePermissionDefaults(@NotNull Permission perm) {
+ // we need a null check here because some plugins for some unknown reason pass null into this?
+ if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ENGLISH))) {
+ this.defaultPerms().get(true).remove(perm);
+ this.defaultPerms().get(false).remove(perm);
+
+ this.calculatePermissionDefault(perm, true);
+ }
+ }
+
+ private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) {
+ if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
+ this.defaultPerms().get(true).add(perm);
+ if (dirty) {
+ this.dirtyPermissibles(true);
+ }
+ }
+ if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
+ this.defaultPerms().get(false).add(perm);
+ if (dirty) {
+ this.dirtyPermissibles(false);
+ }
+ }
+ }
+
+
+ @Override
+ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+ Map<Permissible, Boolean> map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>());
+
+ map.put(permissible, true);
+ }
+
+ @Override
+ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+ Map<Permissible, Boolean> map = this.permSubs().get(name);
+
+ if (map != null) {
+ map.remove(permissible);
+
+ if (map.isEmpty()) {
+ this.permSubs().remove(name);
+ }
+ }
+ }
+
+ @Override
+ @NotNull
+ public Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
+ String name = permission.toLowerCase(java.util.Locale.ENGLISH);
+ Map<Permissible, Boolean> map = this.permSubs().get(name);
+
+ if (map == null) {
+ return ImmutableSet.of();
+ } else {
+ return ImmutableSet.copyOf(map.keySet());
+ }
+ }
+
+ @Override
+ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ Map<Permissible, Boolean> map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>());
+
+ map.put(permissible, true);
+ }
+
+ @Override
+ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ Map<Permissible, Boolean> map = this.defSubs().get(op);
+
+ if (map != null) {
+ map.remove(permissible);
+
+ if (map.isEmpty()) {
+ this.defSubs().remove(op);
+ }
+ }
+ }
+
+ @Override
+ @NotNull
+ public Set<Permissible> getDefaultPermSubscriptions(boolean op) {
+ Map<Permissible, Boolean> map = this.defSubs().get(op);
+
+ if (map == null) {
+ return ImmutableSet.of();
+ } else {
+ return ImmutableSet.copyOf(map.keySet());
+ }
+ }
+
+ @Override
+ @NotNull
+ public Set<Permission> getPermissions() {
+ return new HashSet<>(this.permissions().values());
+ }
+
+ @Override
+ public void clearPermissions() {
+ this.permissions().clear();
+ this.defaultPerms().get(true).clear();
+ this.defaultPerms().get(false).clear();
+ }
+
+
+ void dirtyPermissibles(boolean op) {
+ Set<Permissible> permissibles = this.getDefaultPermSubscriptions(op);
+
+ for (Permissible p : permissibles) {
+ p.recalculatePermissions();
+ }
+ }
+
+ void dirtyPermissibles() {
+ this.dirtyPermissibles(true);
+ this.dirtyPermissibles(false);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<Plugin> plugins = new ArrayList<>();
+ private final Map<String, Plugin> lookupNames = new HashMap<>();
+
+ private final PluginManager pluginManager;
+ private final CommandMap commandMap;
+ private final Server server;
+
+ private final MutableGraph<String> dependencyGraph = GraphBuilder.directed().build();
+ private final DependencyContext context = new GraphDependencyContext(this.dependencyGraph);
+
+ public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
+ this.commandMap = commandMap;
+ this.server = server;
+ this.pluginManager = pluginManager;
+ }
+
+ public @Nullable Plugin getPlugin(@NotNull String name) {
+ return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
+ }
+
+ public @NotNull Plugin[] getPlugins() {
+ return this.plugins.toArray(new Plugin[0]);
+ }
+
+ public boolean isPluginEnabled(@NotNull String name) {
+ Plugin plugin = this.getPlugin(name);
+
+ return this.isPluginEnabled(plugin);
+ }
+
+ public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) {
+ if ((plugin != null) && (this.plugins.contains(plugin))) {
+ return plugin.isEnabled();
+ } else {
+ return false;
+ }
+ }
+
+ public void loadPlugin(Plugin provided) {
+ PluginMeta configuration = provided.getPluginMeta();
+
+ this.plugins.add(provided);
+ this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided);
+ for (String providedPlugin : configuration.getProvidedPlugins()) {
+ this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
+ }
+
+ DependencyUtil.buildDependencyGraph(this.dependencyGraph, configuration);
+ }
+
+ // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception.
+ public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException {
+ RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage());
+
+ try {
+ FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
+ } catch (IllegalArgumentException exception) {
+ return null; // Return null when the plugin file is not valid / plugin type is unknown
+ } catch (PluginGraphCycleException exception) {
+ throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!");
+ } catch (SerializationException |
+ InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception
+ throw new InvalidPluginException(ex);
+ } catch (Exception e) {
+ throw new InvalidPluginException(e);
+ }
+
+ try {
+ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
+ } catch (Throwable e) {
+ throw new InvalidPluginException(e);
+ }
+
+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded()
+ .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?"));
+ }
+
+ // The behavior of this is that all errors are logged instead of being thrown
+ public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
+ Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
+
+ RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage());
+ try {
+ DIRECTORY_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, directory);
+ runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
+ } catch (Exception e) {
+ // This should never happen, any errors that occur in this provider should instead be logged.
+ this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
+ }
+
+ return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
+ }
+
+ // Plugins are disabled in order like this inorder to "rougly" prevent
+ // their dependencies unloading first. But, eh.
+ public void disablePlugins() {
+ Plugin[] plugins = this.getPlugins();
+ for (int i = plugins.length - 1; i >= 0; i--) {
+ this.disablePlugin(plugins[i]);
+ }
+ }
+
+ public void clearPlugins() {
+ synchronized (this) {
+ this.disablePlugins();
+ this.plugins.clear();
+ this.lookupNames.clear();
+ }
+ }
+
+ public synchronized void enablePlugin(@NotNull Plugin plugin) {
+ if (plugin.isEnabled()) {
+ return;
+ }
+
+ if (plugin.getPluginMeta() instanceof PluginDescriptionFile) {
+ List<Command> bukkitCommands = PluginCommandYamlParser.parse(plugin);
+
+ if (!bukkitCommands.isEmpty()) {
+ this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands);
+ }
+ }
+
+ try {
+ String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName();
+ if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) {
+ enableMsg += "*";
+ }
+ plugin.getLogger().info(enableMsg);
+
+ JavaPlugin jPlugin = (JavaPlugin) plugin;
+
+ if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper
+ if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) {
+ this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName());
+ }
+ } // Paper
+
+ try {
+ jPlugin.setEnabled(true);
+ } catch (Throwable ex) {
+ this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex);
+ // Paper start - Disable plugins that fail to load
+ this.server.getPluginManager().disablePlugin(jPlugin);
+ return;
+ // Paper end
+ }
+
+ // Perhaps abort here, rather than continue going, but as it stands,
+ // an abort is not possible the way it's currently written
+ this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while enabling "
+ + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin);
+ }
+
+ HandlerList.bakeAll();
+ }
+
+ public synchronized void disablePlugin(@NotNull Plugin plugin) {
+ if (!(plugin instanceof JavaPlugin javaPlugin)) {
+ throw new IllegalArgumentException("Only expects java plugins.");
+ }
+ if (!plugin.isEnabled()) {
+ return;
+ }
+
+ String pluginName = plugin.getPluginMeta().getDisplayName();
+
+ try {
+ plugin.getLogger().info("Disabling %s".formatted(pluginName));
+
+ this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
+
+ javaPlugin.setEnabled(false);
+
+ ClassLoader classLoader = plugin.getClass().getClassLoader();
+ if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
+ try {
+ configuredPluginClassLoader.close();
+ } catch (IOException ex) {
+ this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception
+ }
+ // Remove from the classloader pool inorder to prevent plugins from trying
+ // to access classes
+ PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader);
+ }
+
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while disabling "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getScheduler().cancelTasks(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getServicesManager().unregisterAll(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ HandlerList.unregisterAll(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ this.server.getMessenger().unregisterIncomingPluginChannel(plugin);
+ this.server.getMessenger().unregisterOutgoingPluginChannel(plugin);
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
+ + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ try {
+ for (World world : this.server.getWorlds()) {
+ world.removePluginChunkTickets(plugin);
+ }
+ } catch (Throwable ex) {
+ this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper
+ }
+
+ }
+
+ // TODO: Implement event part in future patch (paper patch move up, this patch is lower)
+ private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
+ Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex);
+ this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
+ }
+
+ public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
+ return this.context.isTransitiveDependency(plugin, depend);
+ }
+
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.getPlugin(pluginIdentifier) != null;
+ }
+
+ // Debug only
+ @ApiStatus.Internal
+ public MutableGraph<String> getDependencyGraph() {
+ return this.dependencyGraph;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..cdc3f6c5c845fe00478c41a22d39d7b348673559
--- /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 {
+
+ private final PaperPluginInstanceManager instanceManager;
+ private final PaperEventManager paperEventManager;
+ private PermissionManager permissionManager;
+
+ public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) {
+ this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server);
+ this.paperEventManager = new PaperEventManager(server);
+
+ if (permissionManager == null) {
+ this.permissionManager = new NormalPaperPermissionManager();
+ } else {
+ this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed
+ }
+ }
+
+ // REMOVE THIS WHEN SimplePluginManager is removed.
+ // Just cast and use Bukkit.getServer().getPluginManager()
+ public static PaperPluginManagerImpl getInstance() {
+ return ((CraftServer) (Bukkit.getServer())).paperPluginManager;
+ }
+
+ // Plugin Manipulation
+
+ @Override
+ public @Nullable Plugin getPlugin(@NotNull String name) {
+ return this.instanceManager.getPlugin(name);
+ }
+
+ @Override
+ public @NotNull Plugin[] getPlugins() {
+ return this.instanceManager.getPlugins();
+ }
+
+ @Override
+ public boolean isPluginEnabled(@NotNull String name) {
+ return this.instanceManager.isPluginEnabled(name);
+ }
+
+ @Override
+ public boolean isPluginEnabled(@Nullable Plugin plugin) {
+ return this.instanceManager.isPluginEnabled(plugin);
+ }
+
+ public void loadPlugin(Plugin plugin) {
+ this.instanceManager.loadPlugin(plugin);
+ }
+
+ @Override
+ public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException {
+ return this.instanceManager.loadPlugin(file.toPath());
+ }
+
+ @Override
+ public @NotNull Plugin[] loadPlugins(@NotNull File directory) {
+ return this.instanceManager.loadPlugins(directory.toPath());
+ }
+
+ @Override
+ public void disablePlugins() {
+ this.instanceManager.disablePlugins();
+ }
+
+ @Override
+ public synchronized void clearPlugins() {
+ this.instanceManager.clearPlugins();
+ this.permissionManager.clearPermissions();
+ this.paperEventManager.clearEvents();
+ }
+
+ @Override
+ public void enablePlugin(@NotNull Plugin plugin) {
+ this.instanceManager.enablePlugin(plugin);
+ }
+
+ @Override
+ public void disablePlugin(@NotNull Plugin plugin) {
+ this.instanceManager.disablePlugin(plugin);
+ }
+
+ @Override
+ public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) {
+ return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig);
+ }
+
+ @Override
+ public boolean hasDependency(String pluginIdentifier) {
+ return this.instanceManager.hasDependency(pluginIdentifier);
+ }
+
+ // Event manipulation
+
+ @Override
+ public void callEvent(@NotNull Event event) throws IllegalStateException {
+ this.paperEventManager.callEvent(event);
+ }
+
+ @Override
+ public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
+ this.paperEventManager.registerEvents(listener, plugin);
+ }
+
+ @Override
+ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
+ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin);
+ }
+
+ @Override
+ public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
+ this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled);
+ }
+
+ // Permission manipulation
+
+ @Override
+ public @Nullable Permission getPermission(@NotNull String name) {
+ return this.permissionManager.getPermission(name);
+ }
+
+ @Override
+ public void addPermission(@NotNull Permission perm) {
+ this.permissionManager.addPermission(perm);
+ }
+
+ @Override
+ public void removePermission(@NotNull Permission perm) {
+ this.permissionManager.removePermission(perm);
+ }
+
+ @Override
+ public void removePermission(@NotNull String name) {
+ this.permissionManager.removePermission(name);
+ }
+
+ @Override
+ public @NotNull Set<Permission> getDefaultPermissions(boolean op) {
+ return this.permissionManager.getDefaultPermissions(op);
+ }
+
+ @Override
+ public void recalculatePermissionDefaults(@NotNull Permission perm) {
+ this.permissionManager.recalculatePermissionDefaults(perm);
+ }
+
+ @Override
+ public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ this.permissionManager.subscribeToPermission(permission, permissible);
+ }
+
+ @Override
+ public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
+ this.permissionManager.unsubscribeFromPermission(permission, permissible);
+ }
+
+ @Override
+ public @NotNull Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
+ return this.permissionManager.getPermissionSubscriptions(permission);
+ }
+
+ @Override
+ public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ this.permissionManager.subscribeToDefaultPerms(op, permissible);
+ }
+
+ @Override
+ public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
+ this.permissionManager.unsubscribeFromDefaultPerms(op, permissible);
+ }
+
+ @Override
+ public @NotNull Set<Permissible> getDefaultPermSubscriptions(boolean op) {
+ return this.permissionManager.getDefaultPermSubscriptions(op);
+ }
+
+ @Override
+ public @NotNull Set<Permission> getPermissions() {
+ return this.permissionManager.getPermissions();
+ }
+
+ @Override
+ public void addPermissions(@NotNull List<Permission> perm) {
+ this.permissionManager.addPermissions(perm);
+ }
+
+ @Override
+ public void clearPermissions() {
+ this.permissionManager.clearPermissions();
+ }
+
+ @Override
+ public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) {
+ this.permissionManager = permissionManager;
+ }
+
+ // Etc
+
+ @Override
+ public boolean useTimings() {
+ return co.aikar.timings.Timings.isTimingsEnabled();
+ }
+
+ @Override
+ public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException {
+ throw new UnsupportedOperationException();
+ }
+
+ public MutableGraph<String> getInstanceManagerGraph() {
+ return instanceManager.getDependencyGraph();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<T extends ProviderStorage<JavaPlugin>> implements EntrypointHandler {
+
+ private final T providerStorage;
+
+ RuntimePluginEntrypointHandler(T providerStorage) {
+ this.providerStorage = providerStorage;
+ }
+
+ @Override
+ public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
+ if (!entrypoint.equals(Entrypoint.PLUGIN)) {
+ SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint)));
+ // We have to throw an invalid plugin exception for legacy reasons
+ }
+
+ this.providerStorage.register((PluginProvider<JavaPlugin>) provider);
+ }
+
+ @Override
+ public void enter(Entrypoint<?> entrypoint) {
+ if (entrypoint != Entrypoint.PLUGIN) {
+ throw new IllegalArgumentException("Only plugin entrypoint supported");
+ }
+ this.providerStorage.enter();
+ }
+
+ @NotNull
+ public T getPluginProviderStorage() {
+ return this.providerStorage;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..ebc01cbffdc55ca9664897ff10ef14ad6c5ab726
--- /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<JavaPlugin> lastProvider;
+ private JavaPlugin singleLoaded;
+
+ @Override
+ public void register(PluginProvider<JavaPlugin> provider) {
+ super.register(provider);
+ if (this.lastProvider != null) {
+ SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins"));
+ }
+ if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
+ throw new IllegalStateException("Cannot register paper plugins during runtime!");
+ }
+ this.lastProvider = provider;
+ // Register the provider into the server entrypoint, this allows it to show in /plugins correctly.
+ // Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense.
+ LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
+ }
+
+ @Override
+ public void enter() {
+ PluginProvider<JavaPlugin> provider = this.lastProvider;
+ if (provider == null) {
+ return;
+ }
+
+ // Manually validate dependencies, LEGACY BEHAVIOR.
+ // Normally it is logged, but manually adding one plugin will cause it to actually throw exceptions.
+ PluginDescriptionFile descriptionFile = (PluginDescriptionFile) provider.getMeta();
+ List<String> missingDependencies = new ArrayList<>();
+ for (String dependency : descriptionFile.getDepend()) {
+ if (!PaperPluginManagerImpl.getInstance().isPluginEnabled(dependency)) {
+ missingDependencies.add(dependency);
+ }
+ }
+ if (!missingDependencies.isEmpty()) {
+ throw new UnknownDependencyException(missingDependencies, provider.getFileName().toString());
+ }
+
+ // Go through normal plugin loading logic
+ super.enter();
+ }
+
+ @Override
+ public void processProvided(JavaPlugin provided) {
+ super.processProvided(provided);
+ this.singleLoaded = provided;
+ }
+
+ @Override
+ public boolean exitOnCycleDependencies() {
+ return false;
+ }
+
+ public Optional<JavaPlugin> getSingleLoaded() {
+ return Optional.ofNullable(this.singleLoaded);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..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<String, Permission> permissions() {
+ return this.simplePluginManager.permissions;
+ }
+
+ @Override
+ public Map<Boolean, Set<Permission>> defaultPerms() {
+ return this.simplePluginManager.defaultPerms;
+ }
+
+ @Override
+ public Map<String, Map<Permissible, Boolean>> permSubs() {
+ return this.simplePluginManager.permSubs;
+ }
+
+ @Override
+ public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
+ return this.simplePluginManager.defSubs;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..11b6cb377c9b04b63b6359918eef214ba3032d96
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java
@@ -0,0 +1,47 @@
+package io.papermc.paper.plugin.provider;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+
+/**
+ * PluginProviders are created by a {@link io.papermc.paper.plugin.provider.source.ProviderSource},
+ * which is loaded into an {@link io.papermc.paper.plugin.entrypoint.EntrypointHandler}.
+ * <p>
+ * A PluginProvider is responsible for providing part of a plugin, whether it's a Bootstrapper or Server Plugin.
+ * The point of this class is to be able to create the actual instance later, as at the time this is created the server
+ * may be missing some key parts. For example, the Bukkit singleton will not be initialized yet, therefor we need to
+ * have a PluginServerProvider load the server plugin later.
+ * <p>
+ * Plugin providers are currently not exposed in any way of the api. It is preferred that this stays this way,
+ * as providers are only needed for initialization.
+ *
+ * @param <T> provider type
+ */
+@ApiStatus.Internal
+public interface PluginProvider<T> {
+
+ @NotNull
+ Path getSource();
+
+ default Path getFileName() {
+ return this.getSource().getFileName();
+ }
+
+ default Path getParentSource() {
+ return this.getSource().getParent();
+ }
+
+ JarFile file();
+
+ T createInstance();
+
+ PluginMeta getMeta();
+
+ Logger getLogger();
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..6154e864b0ff01cb70acaaeee5ca8c9f4a90a90e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java
@@ -0,0 +1,13 @@
+package io.papermc.paper.plugin.provider;
+
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * This is used for the /plugins command, where it will look in the {@link io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler} and
+ * use the provider statuses to determine the color.
+ */
+@ApiStatus.Internal
+public enum ProviderStatus {
+ INITIALIZED,
+ ERRORED,
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java
new file mode 100644
index 0000000000000000000000000000000000000000..17284d0f61c459dff765c0adae4ad2c641e054c1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java
@@ -0,0 +1,11 @@
+package io.papermc.paper.plugin.provider;
+
+/**
+ * This is used to mark that a plugin provider is able to hold a status for the /plugins command.
+ */
+public interface ProviderStatusHolder {
+
+ ProviderStatus getLastProvidedStatus();
+
+ void setStatus(ProviderStatus status);
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ba3bcc468c0a60c76d6d0f0243bda661c737f2f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java
@@ -0,0 +1,29 @@
+package io.papermc.paper.plugin.provider.configuration;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.objectmapping.meta.NodeResolver;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.AnnotatedElement;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface FlattenedResolver {
+
+ final class Factory implements NodeResolver.Factory {
+
+ @Override
+ public @Nullable NodeResolver make(String name, AnnotatedElement element) {
+ if (element.isAnnotationPresent(FlattenedResolver.class)) {
+ return (node) -> node;
+ } else {
+ return null;
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java
new file mode 100644
index 0000000000000000000000000000000000000000..622a6c5fdfcb6d8cc12054f3de81c73c9af2389f
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java
@@ -0,0 +1,213 @@
+package io.papermc.paper.plugin.provider.configuration;
+
+import com.google.common.collect.ImmutableList;
+import io.leangen.geantyref.TypeToken;
+import io.papermc.paper.configuration.constraint.Constraint;
+import io.papermc.paper.configuration.serializer.ComponentSerializer;
+import io.papermc.paper.configuration.serializer.EnumValueSerializer;
+import io.papermc.paper.configuration.serializer.collections.MapSerializer;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.provider.configuration.serializer.ImmutableListSerializer;
+import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer;
+import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
+import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
+import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.bukkit.plugin.PluginLoadOrder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.configurate.CommentedConfigurationNode;
+import org.spongepowered.configurate.ConfigurateException;
+import org.spongepowered.configurate.loader.HeaderMode;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.ObjectMapper;
+import org.spongepowered.configurate.objectmapping.meta.Required;
+import org.spongepowered.configurate.yaml.NodeStyle;
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
+
+import java.io.BufferedReader;
+import java.util.List;
+
+@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"})
+@ConfigSerializable
+public class PaperPluginMeta implements PluginMeta {
+
+ @PluginConfigConstraints.PluginName
+ @Required
+ private String name;
+ @Required
+ @PluginConfigConstraints.PluginNameSpace
+ private String main;
+ @PluginConfigConstraints.PluginNameSpace
+ private String bootstrapper;
+ @PluginConfigConstraints.PluginNameSpace
+ private String loader;
+ private List<DependencyConfiguration> dependencies = List.of();
+ private List<String> loadBefore = List.of();
+ private List<String> provides = List.of();
+ private boolean hasOpenClassloader = false;
+ @Required
+ private String version;
+ private String description;
+ private List<String> authors = List.of();
+ private List<String> contributors = List.of();
+ private String website;
+ private String prefix;
+ private PluginLoadOrder load = PluginLoadOrder.POSTWORLD;
+ @FlattenedResolver
+ private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of());
+ @Required
+ @PluginConfigConstraints.PluginVersion
+ private String apiVersion;
+
+ private transient String displayName;
+
+ public PaperPluginMeta() {
+ }
+
+ public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException {
+ YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
+ .indent(2)
+ .nodeStyle(NodeStyle.BLOCK)
+ .headerMode(HeaderMode.NONE)
+ .source(() -> reader)
+ .defaultOptions((options) -> {
+
+ return options.serializers((serializers) -> {
+ serializers
+ .register(new EnumValueSerializer())
+ .register(MapSerializer.TYPE, new MapSerializer(false))
+ .register(new TypeToken<>() {
+ }, new ImmutableListSerializer())
+ .register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER)
+ .register(new ComponentSerializer())
+ .registerAnnotatedObjects(
+ ObjectMapper.factoryBuilder()
+ .addConstraint(Constraint.class, new Constraint.Factory())
+ .addConstraint(PluginConfigConstraints.PluginName.class, String.class, new PluginConfigConstraints.PluginName.Factory())
+ .addConstraint(PluginConfigConstraints.PluginVersion.class, String.class, new PluginConfigConstraints.PluginVersion.Factory())
+ .addConstraint(PluginConfigConstraints.PluginNameSpace.class, String.class, new PluginConfigConstraints.PluginNameSpace.Factory())
+ .addNodeResolver(new FlattenedResolver.Factory())
+ .build()
+ );
+
+ });
+ })
+ .build();
+ CommentedConfigurationNode node = loader.load();
+ PaperPluginMeta pluginConfiguration = node.require(PaperPluginMeta.class);
+
+ if (!node.node("author").virtual()) {
+ pluginConfiguration.authors = ImmutableList.<String>builder()
+ .addAll(pluginConfiguration.authors)
+ .add(node.node("author").getString())
+ .build();
+ }
+
+ pluginConfiguration.displayName = pluginConfiguration.name.replace('_', ' ');
+
+ return pluginConfiguration;
+ }
+
+ @Override
+ public @NotNull String getName() {
+ return this.name;
+ }
+
+ @Override
+ public @NotNull String getMainClass() {
+ return this.main;
+ }
+
+ @Override
+ public @NotNull String getVersion() {
+ return this.version;
+ }
+
+ @Override
+ public @NotNull String getDisplayName() {
+ return this.displayName;
+ }
+
+ @Override
+ public @Nullable String getLoggerPrefix() {
+ return this.prefix;
+ }
+
+ @Override
+ public @NotNull List<String> getPluginDependencies() {
+ return this.dependencies.stream().filter((dependency) -> dependency.required() && !dependency.bootstrap()).map(DependencyConfiguration::name).toList();
+ }
+
+ @Override
+ public @NotNull List<String> getPluginSoftDependencies() {
+ return this.dependencies.stream().filter((dependency) -> !dependency.required() && !dependency.bootstrap()).map(DependencyConfiguration::name).toList();
+ }
+
+ @Override
+ public @NotNull List<String> getLoadBeforePlugins() {
+ return this.loadBefore;
+ }
+
+ @Override
+ public @NotNull PluginLoadOrder getLoadOrder() {
+ return this.load;
+ }
+
+ @Override
+ public @NotNull String getDescription() {
+ return this.description;
+ }
+
+ @Override
+ public @NotNull List<String> getAuthors() {
+ return this.authors;
+ }
+
+ @Override
+ public @NotNull List<String> getContributors() {
+ return this.contributors;
+ }
+
+ @Override
+ public String getWebsite() {
+ return this.website;
+ }
+
+ @Override
+ public @NotNull List<Permission> getPermissions() {
+ return this.permissionConfiguration.permissions();
+ }
+
+ @Override
+ public @NotNull PermissionDefault getPermissionDefault() {
+ return this.permissionConfiguration.defaultPerm();
+ }
+
+ @Override
+ public @NotNull String getAPIVersion() {
+ return this.apiVersion;
+ }
+
+ @Override
+ public @NotNull List<String> getProvidedPlugins() {
+ return this.provides;
+ }
+
+ public String getBootstrapper() {
+ return this.bootstrapper;
+ }
+
+ public String getLoader() {
+ return this.loader;
+ }
+
+ public boolean hasOpenClassloader() {
+ return this.hasOpenClassloader;
+ }
+
+ public List<DependencyConfiguration> getDependencies() {
+ return dependencies;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..f0cdb1bab30faaa438aa3e6de6125ade3fae98c2
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java
@@ -0,0 +1,90 @@
+package io.papermc.paper.plugin.provider.configuration.serializer;
+
+import com.google.common.collect.ImmutableCollection;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.ConfigurationOptions;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+import org.spongepowered.configurate.util.CheckedConsumer;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@SuppressWarnings("unchecked")
+public abstract class ImmutableCollectionSerializer<B extends ImmutableCollection.Builder<?>, T extends Collection<?>> implements TypeSerializer<T> {
+
+ protected ImmutableCollectionSerializer() {
+ }
+
+ @Override
+ public final T deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
+ final Type entryType = this.elementType(type);
+ final @Nullable TypeSerializer<?> entrySerial = node.options().serializers().get(entryType);
+ if (entrySerial == null) {
+ throw new SerializationException(node, entryType, "No applicable type serializer for type");
+ }
+
+ if (node.isList()) {
+ final List<? extends ConfigurationNode> values = node.childrenList();
+ final B builder = this.createNew(values.size());
+ for (ConfigurationNode value : values) {
+ try {
+ this.deserializeSingle(builder, entrySerial.deserialize(entryType, value));
+ } catch (final SerializationException ex) {
+ ex.initPath(value::path);
+ throw ex;
+ }
+ }
+ return (T) builder.build();
+ } else {
+ final @Nullable Object unwrappedVal = node.raw();
+ if (unwrappedVal != null) {
+ final B builder = this.createNew(1);
+ this.deserializeSingle(builder, entrySerial.deserialize(entryType, node));
+ return (T) builder.build();
+ }
+ }
+ return this.emptyValue(type, null);
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ @Override
+ public final void serialize(final Type type, final @Nullable T obj, final ConfigurationNode node) throws SerializationException {
+ final Type entryType = this.elementType(type);
+ final @Nullable TypeSerializer entrySerial = node.options().serializers().get(entryType);
+ if (entrySerial == null) {
+ throw new SerializationException(node, entryType, "No applicable type serializer for type");
+ }
+
+ node.raw(Collections.emptyList());
+ if (obj != null) {
+ this.forEachElement(obj, el -> {
+ final ConfigurationNode child = node.appendListNode();
+ try {
+ entrySerial.serialize(entryType, el, child);
+ } catch (final SerializationException ex) {
+ ex.initPath(child::path);
+ throw ex;
+ }
+ });
+ }
+ }
+
+ @SuppressWarnings({"unchecked"})
+ @Override
+ public @Nullable T emptyValue(final Type specificType, final ConfigurationOptions options) {
+ return (T) this.createNew(0).build();
+ }
+
+ protected abstract Type elementType(Type containerType) throws SerializationException;
+
+ protected abstract B createNew(int size);
+
+ protected abstract void forEachElement(T collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException;
+
+ protected abstract void deserializeSingle(B builder, @Nullable Object deserialized) throws SerializationException;
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..7757d7df70e39a6fe4d92d02b6f905a22f80dbf3
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java
@@ -0,0 +1,43 @@
+package io.papermc.paper.plugin.provider.configuration.serializer;
+
+import com.google.common.collect.ImmutableList;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.util.CheckedConsumer;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+
+public class ImmutableListSerializer extends ImmutableCollectionSerializer<ImmutableList.Builder<?>, List<?>> {
+
+ @Override
+ protected Type elementType(Type containerType) throws SerializationException {
+ if (!(containerType instanceof ParameterizedType)) {
+ throw new SerializationException(containerType, "Raw types are not supported for collections");
+ }
+ return ((ParameterizedType) containerType).getActualTypeArguments()[0];
+ }
+
+ @Override
+ protected ImmutableList.Builder<?> createNew(int size) {
+ return ImmutableList.builderWithExpectedSize(size);
+ }
+
+ @Override
+ protected void forEachElement(List<?> collection, CheckedConsumer<Object, SerializationException> action) throws SerializationException {
+ for (Object obj : collection) {
+ action.accept(obj);
+ }
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ @Override
+ protected void deserializeSingle(ImmutableList.Builder<?> builder, @Nullable Object deserialized) throws SerializationException {
+ if (deserialized == null) {
+ return;
+ }
+
+ ((ImmutableList.Builder) builder).add(deserialized);
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..d1088e4b7fa5f8e689f23b150b83645ce1ae5a0e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java
@@ -0,0 +1,56 @@
+package io.papermc.paper.plugin.provider.configuration.serializer;
+
+import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class PermissionConfigurationSerializer {
+
+ public static final Serializer SERIALIZER = new Serializer();
+
+ private static final class Serializer implements TypeSerializer<PermissionConfiguration> {
+ private Serializer() {
+ super();
+ }
+
+ @Override
+ public PermissionConfiguration deserialize(Type type, ConfigurationNode node) throws SerializationException {
+ Map<?, ?> map = (Map<?, ?>) node.node("permissions").raw();
+
+ PermissionDefault permissionDefault;
+ ConfigurationNode permNode = node.node("defaultPerm");
+ if (permNode.virtual()) {
+ permissionDefault = PermissionDefault.OP;
+ } else {
+ permissionDefault = PermissionDefault.getByName(permNode.getString());
+ }
+
+ List<Permission> result = new ArrayList<>();
+ if (map != null) {
+ for (Map.Entry<?, ?> entry : map.entrySet()) {
+ try {
+ result.add(Permission.loadPermission(entry.getKey().toString(), (Map<?, ?>) entry.getValue(), permissionDefault, result));
+ } catch (Throwable ex) {
+ throw new SerializationException(null, "Error loading permission %s".formatted(entry.getKey()), ex);
+ }
+ }
+ }
+
+ return new PermissionConfiguration(permissionDefault, List.copyOf(result));
+ }
+
+ @Override
+ public void serialize(Type type, @org.checkerframework.checker.nullness.qual.Nullable PermissionConfiguration obj, ConfigurationNode node) throws SerializationException {
+
+ }
+
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java
new file mode 100644
index 0000000000000000000000000000000000000000..a0109a388188b0808900405d334a40318ab16ec1
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java
@@ -0,0 +1,86 @@
+package io.papermc.paper.plugin.provider.configuration.serializer.constraints;
+
+import io.papermc.paper.plugin.util.NamespaceChecker;
+import org.spongepowered.configurate.objectmapping.meta.Constraint;
+import org.spongepowered.configurate.serialize.SerializationException;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Type;
+import java.util.Locale;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+public final class PluginConfigConstraints {
+
+ public static final Set<String> RESERVED_KEYS = Set.of("bukkit", "minecraft", "mojang", "spigot", "paper");
+ public static final Set<String> VALID_PAPER_VERSIONS = Set.of("1.19");
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface PluginName {
+
+ final class Factory implements Constraint.Factory<PluginName, String> {
+
+ private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z\\d _.-]+$");
+
+ @Override
+ public Constraint<String> make(PluginName data, Type type) {
+ return value -> {
+ if (value != null) {
+ if (RESERVED_KEYS.contains(value.toLowerCase(Locale.ROOT))) {
+ throw new SerializationException("Restricted name, cannot use '%s' as a plugin name.".formatted(data));
+ } else if (value.indexOf(' ') != -1) {
+ // For legacy reasons, the space condition has a separate exception message.
+ throw new SerializationException("Restricted name, cannot use 0x20 (space character) in a plugin name.");
+ }
+
+ if (!VALID_NAME.matcher(value).matches()) {
+ throw new SerializationException("name '" + value + "' contains invalid characters.");
+ }
+ }
+ };
+ }
+ }
+ }
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface PluginNameSpace {
+
+ final class Factory implements Constraint.Factory<PluginNameSpace, String> {
+
+ @Override
+ public Constraint<String> make(PluginNameSpace data, Type type) {
+ return value -> {
+ if (value != null && !NamespaceChecker.isValidNameSpace(value)) {
+ throw new SerializationException("provided class '%s' is in an invalid namespace.".formatted(value));
+ }
+ };
+ }
+ }
+ }
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface PluginVersion {
+
+ final class Factory implements Constraint.Factory<PluginVersion, String> {
+
+ @Override
+ public Constraint<String> make(PluginVersion data, Type type) {
+ return value -> {
+ if (value != null && !VALID_PAPER_VERSIONS.contains(value)) {
+ throw new SerializationException("Provided plugin's version (%s) is not supported on this version.".formatted(value));
+ }
+ };
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..071bff3f988a4391be424bdf7e98a6c35e6cac67
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java
@@ -0,0 +1,12 @@
+package io.papermc.paper.plugin.provider.configuration.type;
+
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+import org.spongepowered.configurate.objectmapping.meta.Required;
+
+@ConfigSerializable
+public record DependencyConfiguration(
+ @Required String name,
+ boolean required,
+ boolean bootstrap
+) {
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..a180612a1ec395202dbae1ca5b97ec01382097e4
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java
@@ -0,0 +1,14 @@
+package io.papermc.paper.plugin.provider.configuration.type;
+
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.spongepowered.configurate.objectmapping.ConfigSerializable;
+
+import java.util.List;
+
+// Record components used for deserialization!!!!
+@ConfigSerializable
+public record PermissionConfiguration(
+ PermissionDefault defaultPerm,
+ List<Permission> permissions) {
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..1822e076601db51c8a7954036853bee1fb8e3704
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java
@@ -0,0 +1,40 @@
+package io.papermc.paper.plugin.provider.source;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
+import org.slf4j.Logger;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Level;
+
+/**
+ * Loads all plugin providers in the given directory.
+ */
+public class DirectoryProviderSource extends FileProviderSource {
+
+ public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource();
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public DirectoryProviderSource() {
+ super("Directory '%s'"::formatted);
+ }
+
+ @Override
+ public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception {
+ // Sym link happy, create file if missing.
+ if (!Files.isDirectory(context)) {
+ Files.createDirectories(context);
+ }
+
+ Files.walk(context, 1).filter(Files::isRegularFile).forEach((path) -> {
+ try {
+ super.registerProviders(entrypointHandler, path);
+ } catch (IllegalArgumentException ignored) {
+ // Ignore initial argument exceptions
+ } catch (Exception e) {
+ LOGGER.error("Error loading plugin: " + e.getMessage(), e);
+ }
+ });
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..e6a99f422038fad519215abf239135b11edc2bce
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java
@@ -0,0 +1,156 @@
+package io.papermc.paper.plugin.provider.source;
+
+import io.papermc.paper.plugin.PluginInitializerManager;
+import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
+import io.papermc.paper.plugin.provider.type.PluginFileType;
+import org.bukkit.plugin.InvalidPluginException;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.jar.JarFile;
+
+/**
+ * Loads a plugin provider at the given plugin jar file path.
+ */
+public class FileProviderSource implements ProviderSource<Path> {
+
+ private final Function<Path, String> contextChecker;
+
+ public FileProviderSource(Function<Path, String> contextChecker) {
+ this.contextChecker = contextChecker;
+ }
+
+ @Override
+ public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception {
+ String source = this.contextChecker.apply(context);
+
+ if (Files.notExists(context)) {
+ throw new IllegalArgumentException(source + " does not exist, cannot load a plugin from it!");
+ }
+
+ if (!Files.isRegularFile(context)) {
+ throw new IllegalArgumentException(source + " is not a file, cannot load a plugin from it!");
+ }
+
+ if (!context.getFileName().toString().endsWith(".jar")) {
+ throw new IllegalArgumentException(source + " is not a jar file, cannot load a plugin from it!");
+ }
+
+ try {
+ context = this.checkUpdate(context);
+
+ JarFile file = new JarFile(context.toFile());
+ PluginFileType<?, ?> type = PluginFileType.guessType(file);
+ if (type == null) {
+ throw new IllegalArgumentException(source + " is not a valid plugin file, cannot load a plugin from it!");
+ }
+
+ type.register(entrypointHandler, file, context);
+ } catch (Exception exception) {
+ throw new RuntimeException(source + " failed to load!", exception);
+ }
+ }
+
+ /**
+ * Replaces a plugin with a plugin of the same plugin name in the update folder.
+ *
+ * @param file
+ */
+ private Path checkUpdate(Path file) throws Exception {
+ PluginInitializerManager pluginSystem = PluginInitializerManager.instance();
+ Path updateDirectory = pluginSystem.pluginUpdatePath();
+ if (updateDirectory == null || !Files.isDirectory(updateDirectory)) {
+ return file;
+ }
+
+ try {
+ String pluginName = this.getPluginName(file);
+ UpdateFileVisitor visitor = new UpdateFileVisitor(pluginName);
+ Files.walkFileTree(updateDirectory, Set.of(), 1, visitor);
+ if (visitor.getValidPlugin() != null) {
+ Path updateLocation = visitor.getValidPlugin();
+
+ try {
+ Files.copy(updateLocation, file, StandardCopyOption.REPLACE_EXISTING);
+ } catch (IOException exception) {
+ throw new RuntimeException("Could not copy '" + updateLocation + "' to '" + file + "' in update plugin process", exception);
+ }
+
+ // Idk what this is about, TODO
+ File newName = new File(file.toFile().getParentFile(), updateLocation.toFile().getName());
+ file.toFile().renameTo(newName);
+ updateLocation.toFile().delete();
+ return newName.toPath();
+ }
+ } catch (Exception e) {
+ throw new InvalidPluginException(e);
+ }
+ return file;
+ }
+
+ private String getPluginName(Path path) throws Exception {
+ JarFile file = new JarFile(path.toFile());
+ PluginFileType<?, ?> type = PluginFileType.guessType(file);
+ if (type == null) {
+ throw new IllegalArgumentException(path + " is not a valid plugin file, cannot load a plugin from it!");
+ }
+
+ return type.getConfig(file).getName();
+ }
+
+ private class UpdateFileVisitor implements FileVisitor<Path> {
+
+ private final String targetName;
+ @Nullable
+ private Path validPlugin;
+
+ private UpdateFileVisitor(String targetName) {
+ this.targetName = targetName;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ try {
+ String updatePluginName = FileProviderSource.this.getPluginName(file);
+ if (this.targetName.equals(updatePluginName)) {
+ this.validPlugin = file;
+ return FileVisitResult.TERMINATE;
+ }
+ } catch (Exception e) {
+ // We failed to load this data for some reason, so, we'll skip over this
+ }
+
+
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Nullable
+ public Path getValidPlugin() {
+ return validPlugin;
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..e62b476ddbce2bdc66061c116aa0228622f6fd16
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java
@@ -0,0 +1,29 @@
+package io.papermc.paper.plugin.provider.source;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
+import org.slf4j.Logger;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Registers providers at the provided files in the add-plugin argument.
+ */
+public class PluginFlagProviderSource implements ProviderSource<List<File>> {
+
+ public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource();
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private final FileProviderSource providerSource = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted);
+
+ @Override
+ public void registerProviders(EntrypointHandler entrypointHandler, List<File> context) {
+ for (File file : context) {
+ try {
+ this.providerSource.registerProviders(entrypointHandler, file.toPath());
+ } catch (Exception e) {
+ LOGGER.error("Error loading plugin: " + e.getMessage(), e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java
new file mode 100644
index 0000000000000000000000000000000000000000..6d247819ee842eb054a74711a0e5805ac8f0498e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java
@@ -0,0 +1,14 @@
+package io.papermc.paper.plugin.provider.source;
+
+import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
+
+/**
+ * A provider source is responsible for giving PluginTypes an EntrypointHandler for
+ * registering providers at.
+ *
+ * @param <C> context
+ */
+public interface ProviderSource<C> {
+
+ void registerProviders(EntrypointHandler entrypointHandler, C context) throws Throwable;
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
new file mode 100644
index 0000000000000000000000000000000000000000..22c25dc6fdfd336f5074fa52c3a4e8128d433ccc
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java
@@ -0,0 +1,77 @@
+package io.papermc.paper.plugin.provider.type;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import io.papermc.paper.plugin.entrypoint.Entrypoint;
+import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
+import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * This is where spigot/paper plugins are registered.
+ * This will get the jar and find a certain config file, create an object
+ * then registering it into a {@link EntrypointHandler} at a certain {@link Entrypoint}.
+ */
+public abstract class PluginFileType<T, C extends PluginMeta> {
+
+ public static final PluginFileType<PaperPluginParent, PaperPluginMeta> PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) {
+ @Override
+ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) {
+ PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null;
+ if (parent.shouldCreateBootstrap()) {
+ bootstrapPluginProvider = parent.createBootstrapProvider();
+ entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider);
+ }
+
+ entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider));
+ }
+ };
+ public static final PluginFileType<SpigotPluginProvider, PluginDescriptionFile> SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) {
+ @Override
+ protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) {
+ entrypointHandler.register(Entrypoint.PLUGIN, provider);
+ }
+ };
+
+ private static final List<PluginFileType<?, ?>> VALUES = List.of(PAPER, SPIGOT);
+
+ private final String config;
+ private final PluginTypeFactory<T, C> factory;
+
+ PluginFileType(String config, PluginTypeFactory<T, C> factory) {
+ this.config = config;
+ this.factory = factory;
+ }
+
+ @Nullable
+ public static PluginFileType<?, ?> guessType(JarFile file) {
+ for (PluginFileType<?, ?> type : VALUES) {
+ JarEntry entry = file.getJarEntry(type.config);
+ if (entry != null) {
+ return type;
+ }
+ }
+
+ return null;
+ }
+
+ public T register(EntrypointHandler entrypointHandler, JarFile file, Path context) throws Exception {
+ C config = this.getConfig(file);
+ T provider = this.factory.build(file, config, context);
+ this.register(entrypointHandler, provider);
+ return provider;
+ }
+
+ public C getConfig(JarFile file) throws Exception {
+ return this.factory.create(file, file.getJarEntry(this.config));
+ }
+
+ protected abstract void register(EntrypointHandler entrypointHandler, T provider);
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..32f230d66f6953520b59ccbf3079c5a6242ca92c
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java
@@ -0,0 +1,21 @@
+package io.papermc.paper.plugin.provider.type;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+
+import java.nio.file.Path;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * A plugin type factory is responsible for building an object
+ * and config for a certain plugin type.
+ *
+ * @param <T> plugin provider type (may not be a plugin provider)
+ * @param <C> config type
+ */
+public interface PluginTypeFactory<T, C extends PluginMeta> {
+
+ T build(JarFile file, C configuration, Path source) throws Exception;
+
+ C create(JarFile file, JarEntry config) throws Exception;
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java
new file mode 100644
index 0000000000000000000000000000000000000000..46ee1b7f4b4c509932c68a4152e1d8445811d3b8
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java
@@ -0,0 +1,226 @@
+package io.papermc.paper.plugin.provider.type.paper;
+
+import com.destroystokyo.paper.util.SneakyThrow;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
+import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
+import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.ProviderStatus;
+import io.papermc.paper.plugin.provider.ProviderStatusHolder;
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
+import io.papermc.paper.plugin.provider.util.ProviderUtil;
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+
+public class PaperPluginParent {
+
+ public static final PluginTypeFactory<PaperPluginParent, PaperPluginMeta> FACTORY = new PaperPluginProviderFactory();
+ private final Path path;
+ private final JarFile jarFile;
+ private final PaperPluginMeta description;
+ private final PaperPluginClassLoader classLoader;
+ private final PluginProviderContext context;
+ private final Logger logger;
+
+ public PaperPluginParent(Path path, JarFile jarFile, PaperPluginMeta description, PaperPluginClassLoader classLoader, PluginProviderContext context) {
+ this.path = path;
+ this.jarFile = jarFile;
+ this.description = description;
+ this.classLoader = classLoader;
+ this.context = context;
+ this.logger = context.getLogger();
+ }
+
+ public boolean shouldCreateBootstrap() {
+ return this.description.getBootstrapper() != null;
+ }
+
+ public PaperBootstrapProvider createBootstrapProvider() {
+ return new PaperBootstrapProvider();
+ }
+
+ public PaperServerPluginProvider createPluginProvider(PaperBootstrapProvider provider) {
+ return new PaperServerPluginProvider(provider);
+ }
+
+ public class PaperBootstrapProvider implements PluginProvider<PluginBootstrap>, ProviderStatusHolder, DependencyContextHolder {
+
+ private ProviderStatus status;
+ private PluginBootstrap lastProvided;
+
+ @Override
+ public @NotNull Path getSource() {
+ return PaperPluginParent.this.path;
+ }
+
+ @Override
+ public JarFile file() {
+ return PaperPluginParent.this.jarFile;
+ }
+
+ @Override
+ public PluginBootstrap createInstance() {
+ PluginBootstrap bootstrap = ProviderUtil.loadClass(PaperPluginParent.this.description.getBootstrapper(),
+ PluginBootstrap.class, PaperPluginParent.this.classLoader, () -> this.status = ProviderStatus.ERRORED);
+ this.status = ProviderStatus.INITIALIZED;
+ this.lastProvided = bootstrap;
+ return bootstrap;
+ }
+
+ @Override
+ public PaperPluginMeta getMeta() {
+ return PaperPluginParent.this.description;
+ }
+
+ @Override
+ public Logger getLogger() {
+ return PaperPluginParent.this.logger;
+ }
+
+ @Override
+ public ProviderStatus getLastProvidedStatus() {
+ return this.status;
+ }
+
+ @Override
+ public void setStatus(ProviderStatus status) {
+ this.status = status;
+ }
+
+ public PluginBootstrap getLastProvided() {
+ return this.lastProvided;
+ }
+
+ @Override
+ public void setContext(DependencyContext context) {
+ PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context);
+ }
+
+ @Override
+ public String toString() {
+ return "PaperBootstrapProvider{" +
+ "parent=" + PaperPluginParent.this +
+ "status=" + status +
+ ", lastProvided=" + lastProvided +
+ '}';
+ }
+ }
+
+ public class PaperServerPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder {
+
+ private final PaperBootstrapProvider bootstrapProvider;
+
+ private ProviderStatus status;
+
+ PaperServerPluginProvider(PaperBootstrapProvider bootstrapProvider) {
+ this.bootstrapProvider = bootstrapProvider;
+ }
+
+ @Override
+ public @NotNull Path getSource() {
+ return PaperPluginParent.this.path;
+ }
+
+ @Override
+ public JarFile file() {
+ return PaperPluginParent.this.jarFile;
+ }
+
+ @Override
+ public JavaPlugin createInstance() {
+ PluginBootstrap bootstrap = null;
+ if (this.bootstrapProvider != null && this.bootstrapProvider.getLastProvided() != null) {
+ bootstrap = this.bootstrapProvider.getLastProvided();
+ }
+
+ try {
+ JavaPlugin plugin;
+ if (bootstrap == null) {
+ plugin = ProviderUtil.loadClass(PaperPluginParent.this.description.getMainClass(), JavaPlugin.class, PaperPluginParent.this.classLoader);
+ } else {
+ plugin = bootstrap.createPlugin(PaperPluginParent.this.context);
+ }
+
+ // Don't allow plugins to load plugins other than the one defined in main. This restriction might not be necessary.
+ if (!plugin.getClass().isAssignableFrom(Class.forName(PaperPluginParent.this.description.getMainClass(), true, plugin.getClass().getClassLoader()))) {
+ throw new IllegalArgumentException("Plugin provided must be the same type as main defined in plugin configuration!");
+ }
+
+ this.status = ProviderStatus.INITIALIZED;
+ return plugin;
+ } catch (Throwable throwable) {
+ this.status = ProviderStatus.ERRORED;
+ SneakyThrow.sneaky(throwable);
+ }
+
+ throw new AssertionError(); // Impossible
+ }
+
+ @Override
+ public PaperPluginMeta getMeta() {
+ return PaperPluginParent.this.description;
+ }
+
+ @Override
+ public Logger getLogger() {
+ return PaperPluginParent.this.logger;
+ }
+
+ @Override
+ public ProviderStatus getLastProvidedStatus() {
+ return this.status;
+ }
+
+ @Override
+ public void setStatus(ProviderStatus status) {
+ this.status = status;
+ }
+
+ public boolean shouldSkipCreation() {
+ if (this.bootstrapProvider == null) {
+ return false;
+ }
+
+ return this.bootstrapProvider.getLastProvidedStatus() == ProviderStatus.ERRORED;
+ }
+
+ /*
+ The plugin has to reuse the classloader in order to share the bootstrapper.
+ However, a plugin may have totally separate dependencies during bootstrapping.
+ This is a bit yuck, but in general we have to treat bootstrapping and normal game as connected.
+ */
+ @Override
+ public void setContext(DependencyContext context) {
+ PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context);
+ }
+
+ @Override
+ public String toString() {
+ return "PaperServerPluginProvider{" +
+ "parent=" + PaperPluginParent.this +
+ "bootstrapProvider=" + bootstrapProvider +
+ ", status=" + status +
+ '}';
+ }
+ }
+
+
+ @Override
+ public String toString() {
+ return "PaperPluginParent{" +
+ "path=" + path +
+ ", jarFile=" + jarFile +
+ ", description=" + description +
+ ", classLoader=" + classLoader +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..db343a2f482ac375078610f0875692861f412ee2
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java
@@ -0,0 +1,54 @@
+package io.papermc.paper.plugin.provider.type.paper;
+
+import com.destroystokyo.paper.utils.PaperPluginLogger;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
+import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
+import io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader;
+import io.papermc.paper.plugin.loader.PaperClasspathBuilder;
+import io.papermc.paper.plugin.loader.PluginLoader;
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
+import io.papermc.paper.plugin.provider.util.ProviderUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+
+class PaperPluginProviderFactory implements PluginTypeFactory<PaperPluginParent, PaperPluginMeta> {
+
+ @Override
+ public PaperPluginParent build(JarFile file, PaperPluginMeta configuration, Path source) throws Exception {
+ Logger logger = PaperPluginLogger.getLogger(configuration);
+ PluginProviderContext context = PluginProviderContextImpl.of(configuration, logger);
+
+ PaperClasspathBuilder builder = new PaperClasspathBuilder(context);
+
+ if (configuration.getLoader() != null) {
+ try (
+ PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(source, file, configuration, this.getClass().getClassLoader())
+ ) {
+ PluginLoader loader = ProviderUtil.loadClass(configuration.getLoader(), PluginLoader.class, simplePluginClassLoader);
+ loader.classloader(builder);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ PaperPluginClassLoader classLoader = builder.buildClassLoader(logger, source, file, configuration);
+ return new PaperPluginParent(source, file, configuration, classLoader, context);
+ }
+
+ @Override
+ public PaperPluginMeta create(JarFile file, JarEntry config) throws Exception {
+ PaperPluginMeta configuration;
+ try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(config)))) {
+ configuration = PaperPluginMeta.create(bufferedReader);
+ }
+ return configuration;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..309c3dc492cbf469768d6a712485a0e42e73f947
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java
@@ -0,0 +1,172 @@
+package io.papermc.paper.plugin.provider.type.spigot;
+
+import com.destroystokyo.paper.util.SneakyThrow;
+import com.destroystokyo.paper.utils.PaperPluginLogger;
+import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.ProviderStatus;
+import io.papermc.paper.plugin.provider.ProviderStatusHolder;
+import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
+import org.bukkit.Bukkit;
+import org.bukkit.Server;
+import org.bukkit.plugin.InvalidPluginException;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.UnknownDependencyException;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.bukkit.plugin.java.LibraryLoader;
+import org.bukkit.plugin.java.PluginClassLoader;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class SpigotPluginProvider implements PluginProvider<JavaPlugin>, ProviderStatusHolder, DependencyContextHolder {
+
+ public static final PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> FACTORY = new SpigotPluginProviderFactory();
+ private static final LibraryLoader LIBRARY_LOADER = new LibraryLoader(Logger.getLogger("SpigotLibraryLoader"));
+ private final Path path;
+ private final PluginDescriptionFile description;
+ private final JarFile jarFile;
+ private final Logger logger;
+ private ProviderStatus status;
+ private DependencyContext dependencyContext;
+
+ SpigotPluginProvider(Path path, JarFile file, PluginDescriptionFile description) {
+ this.path = path;
+ this.jarFile = file;
+ this.description = description;
+ this.logger = PaperPluginLogger.getLogger(description);
+ }
+
+ @Override
+ public @NotNull Path getSource() {
+ return this.path;
+ }
+
+ @Override
+ public JarFile file() {
+ return this.jarFile;
+ }
+
+ @Override
+ public JavaPlugin createInstance() {
+ Server server = Bukkit.getServer();
+ try {
+
+ final File parentFile = server.getPluginsFolder(); // Paper
+ final File dataFolder = new File(parentFile, this.description.getName());
+ @SuppressWarnings("deprecation") final File oldDataFolder = new File(parentFile, this.description.getRawName());
+
+ // Found old data folder
+ if (dataFolder.equals(oldDataFolder)) {
+ // They are equal -- nothing needs to be done!
+ } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) {
+ server.getLogger().warning(String.format(
+ "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'",
+ this.description.getFullName(),
+ this.path,
+ oldDataFolder,
+ dataFolder
+ ));
+ } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) {
+ if (!oldDataFolder.renameTo(dataFolder)) {
+ throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'");
+ }
+ server.getLogger().log(Level.INFO, String.format(
+ "While loading %s (%s) renamed data folder: `%s' to `%s'",
+ this.description.getFullName(),
+ this.path,
+ oldDataFolder,
+ dataFolder
+ ));
+ }
+
+ if (dataFolder.exists() && !dataFolder.isDirectory()) {
+ throw new InvalidPluginException(String.format(
+ "Projected datafolder: `%s' for %s (%s) exists and is not a directory",
+ dataFolder,
+ this.description.getFullName(),
+ this.path
+ ));
+ }
+
+ Set<String> missingHardDependencies = new HashSet<>(this.description.getDepend().size()); // Paper - list all missing hard depends
+ for (final String pluginName : this.description.getDepend()) {
+ if (!this.dependencyContext.hasDependency(pluginName)) {
+ missingHardDependencies.add(pluginName); // Paper - list all missing hard depends
+ }
+ }
+ // Paper start - list all missing hard depends
+ if (!missingHardDependencies.isEmpty()) {
+ throw new UnknownDependencyException(missingHardDependencies, this.description.getFullName());
+ }
+ // Paper end
+
+ server.getUnsafe().checkSupported(this.description);
+
+ final PluginClassLoader loader;
+ try {
+ loader = new PluginClassLoader(this.getClass().getClassLoader(), this.description, dataFolder, this.path.toFile(), LIBRARY_LOADER.createLoader(this.description), this.dependencyContext); // Paper
+ } catch (InvalidPluginException ex) {
+ throw ex;
+ } catch (Throwable ex) {
+ throw new InvalidPluginException(ex);
+ }
+
+ // Override dependency context.
+ // We must provide a temporary context in order to properly handle dependencies on the plugin classloader constructor.
+ loader.dependencyContext = PaperPluginManagerImpl.getInstance();
+
+ this.status = ProviderStatus.INITIALIZED;
+ return loader.plugin;
+ } catch (Throwable ex) {
+ this.status = ProviderStatus.ERRORED;
+ SneakyThrow.sneaky(ex);
+ }
+
+ throw new AssertionError(); // Shouldn't happen
+ }
+
+ @Override
+ public PluginDescriptionFile getMeta() {
+ return this.description;
+ }
+
+ @Override
+ public Logger getLogger() {
+ return this.logger;
+ }
+
+ @Override
+ public ProviderStatus getLastProvidedStatus() {
+ return this.status;
+ }
+
+ @Override
+ public void setStatus(ProviderStatus status) {
+ this.status = status;
+ }
+
+ @Override
+ public void setContext(DependencyContext context) {
+ this.dependencyContext = context;
+ }
+
+ @Override
+ public String toString() {
+ return "SpigotPluginProvider{" +
+ "path=" + path +
+ ", description=" + description +
+ ", jarFile=" + jarFile +
+ ", status=" + status +
+ ", dependencyContext=" + dependencyContext +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..14ed05945ba5bfeb2b539d4786278b0e04130404
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java
@@ -0,0 +1,45 @@
+package io.papermc.paper.plugin.provider.type.spigot;
+
+import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
+import io.papermc.paper.plugin.provider.type.PluginTypeFactory;
+import org.bukkit.plugin.InvalidDescriptionException;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.yaml.snakeyaml.error.YAMLException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Locale;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+class SpigotPluginProviderFactory implements PluginTypeFactory<SpigotPluginProvider, PluginDescriptionFile> {
+
+ @Override
+ public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws Exception {
+ // Copied from SimplePluginManager#loadPlugins
+ // Spigot doesn't validate the name when the config is created, and instead when the plugin is loaded.
+ // Paper plugin configuration will do these checks in config serializer instead of when this is created.
+ String name = configuration.getRawName();
+ if (PluginConfigConstraints.RESERVED_KEYS.contains(name.toLowerCase(Locale.ROOT))) {
+ throw new InvalidDescriptionException("Restricted name, cannot use %s as a plugin name.".formatted(name));
+ } else if (name.indexOf(' ') != -1) {
+ throw new InvalidDescriptionException("Restricted name, cannot use 0x20 (space character) in a plugin name.");
+ }
+
+ return new SpigotPluginProvider(source, file, configuration);
+ }
+
+ @Override
+ public PluginDescriptionFile create(JarFile file, JarEntry config) throws Exception {
+ PluginDescriptionFile descriptionFile;
+ try (InputStream inputStream = file.getInputStream(config)) {
+ descriptionFile = new PluginDescriptionFile(inputStream);
+ } catch (IOException | YAMLException ex) {
+ throw new InvalidDescriptionException(ex);
+ }
+
+ return descriptionFile;
+ }
+}
+
diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..7af995b941ce83265a93cdc6b5a2de8ad27e4db6
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java
@@ -0,0 +1,113 @@
+package io.papermc.paper.plugin.storage;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.PluginInitializerManager;
+import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
+import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
+import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.ProviderStatus;
+import io.papermc.paper.plugin.provider.ProviderStatusHolder;
+import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
+import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BootstrapProviderStorage extends SimpleProviderStorage<PluginBootstrap> {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public BootstrapProviderStorage() {
+ super(new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
+ @Override
+ public void applyContext(PluginProvider<PluginBootstrap> provider, DependencyContext dependencyContext) {
+ if (provider instanceof DependencyContextHolder contextHolder) {
+ contextHolder.setContext(dependencyContext);
+ }
+ }
+
+ @Override
+ public boolean load(PluginProvider<PluginBootstrap> provider, PluginBootstrap provided) {
+ try {
+ PluginProviderContext context = PluginProviderContextImpl.of(provider, PluginInitializerManager.instance().pluginDirectoryPath());
+ provided.bootstrap(context);
+ return true;
+ } catch (Exception e) {
+ LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e);
+ if (provider instanceof ProviderStatusHolder statusHolder) {
+ statusHolder.setStatus(ProviderStatus.ERRORED);
+ }
+ return false;
+ }
+ }
+
+ @Override
+ public List<String> requiredDependencies(PluginProvider<PluginBootstrap> provider) {
+ List<String> dependencies = new ArrayList<>();
+ if (provider.getMeta() instanceof PaperPluginMeta paperPluginMeta) {
+ for (DependencyConfiguration configuration : paperPluginMeta.getDependencies()) {
+ if (configuration.required() && configuration.bootstrap()) {
+ dependencies.add(configuration.name());
+ }
+ }
+
+ return dependencies;
+ }
+
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public List<String> optionalDependencies(PluginProvider<PluginBootstrap> provider) {
+ List<String> dependencies = new ArrayList<>();
+ if (provider.getMeta() instanceof PaperPluginMeta paperPluginMeta) {
+ for (DependencyConfiguration configuration : paperPluginMeta.getDependencies()) {
+ if (!configuration.required() && configuration.bootstrap()) {
+ dependencies.add(configuration.name());
+ }
+ }
+
+ return dependencies;
+ }
+
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public List<String> loadBeforeDependencies(PluginProvider<PluginBootstrap> provider) {
+ return provider.getMeta().getLoadBeforePlugins();
+ }
+ }));
+ }
+
+ @Override
+ protected void handleCycle(PluginGraphCycleException exception) {
+ List<String> logMessages = new ArrayList<>();
+ for (List<String> list : exception.getCycles()) {
+ // CoolPlugin depends on Dependency depends on CoolPlugin...
+ logMessages.add(String.join(" depends on ", list) + " depends on " + list.get(0) + "...");
+ }
+
+ LOGGER.error("Circular dependencies detected!");
+ LOGGER.error("You have a plugin that is depending on a plugin which refers back to that plugin. Your server will shut down until these are resolved, or the strategy is changed.");
+ LOGGER.error("Circular dependencies:");
+ for (String message : logMessages) {
+ LOGGER.error(message);
+ }
+ LOGGER.error("If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true");
+
+ System.exit(-1);
+ }
+
+ @Override
+ public String toString() {
+ return "BOOTSTRAP:" + super.toString();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..eb4a7e1fbd9f0e853ebf965c6b4f9e0e6061ad74
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java
@@ -0,0 +1,51 @@
+package io.papermc.paper.plugin.storage;
+
+import io.papermc.paper.plugin.entrypoint.strategy.LegacyPluginLoadingStrategy;
+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
+import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
+import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+public abstract class ConfiguredProviderStorage<T> extends SimpleProviderStorage<T> {
+
+ private static final Logger LOGGER = Logger.getLogger("ConfiguredOrderedProviderStorage");
+ public static final boolean LEGACY_PLUGIN_LOADING = Boolean.getBoolean("paper.useLegacyPluginLoading");
+
+ protected ConfiguredProviderStorage(ProviderConfiguration<T> onLoad) {
+ // This doesn't work with reloading.
+ // Should we care?
+ super(LEGACY_PLUGIN_LOADING ? new LegacyPluginLoadingStrategy<>(onLoad) : new ModernPluginLoadingStrategy<>(onLoad));
+ }
+
+ @Override
+ protected void handleCycle(PluginGraphCycleException exception) {
+ List<String> logMessages = new ArrayList<>();
+ for (List<String> list : exception.getCycles()) {
+ logMessages.add(String.join(" -> ", list) + " -> " + list.get(0));
+ }
+
+ LOGGER.log(Level.SEVERE, "Circular dependencies detected! This happens when");
+ LOGGER.log(Level.SEVERE, " i) plugin A has a plugin B in its (soft)depend list, and plugin B has plugin A in its (soft)depend list, or");
+ LOGGER.log(Level.SEVERE, " ii) plugin A has plugin B both in its (soft)depend list and its loadbefore list.");
+ LOGGER.log(Level.SEVERE, "Circular dependencies:");
+ for (String logMessage : logMessages) {
+ LOGGER.log(Level.SEVERE, " " + logMessage);
+ }
+ LOGGER.log(Level.SEVERE, "Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help.");
+ LOGGER.log(Level.SEVERE, "If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true");
+
+ if (this.exitOnCycleDependencies()) {
+ throw new IllegalStateException("Circular plugin dependencies from plugins " + exception.getCycles().stream().map(cycle -> cycle.get(0)).collect(Collectors.joining(", ")));
+ }
+ }
+
+ public boolean exitOnCycleDependencies() {
+ return true;
+ }
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..374e7d3d69fc8603ecf54999f173123d3a9fbf6e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java
@@ -0,0 +1,18 @@
+package io.papermc.paper.plugin.storage;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+
+/**
+ * A provider storage is meant to be a singleton that stores providers.
+ *
+ * @param <T> provider type
+ */
+public interface ProviderStorage<T> {
+
+ void register(PluginProvider<T> provider);
+
+ void enter();
+
+ Iterable<PluginProvider<T>> getRegisteredProviders();
+
+}
diff --git a/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..b049691292fb9d53ac598cb333fa408b043e7bc5
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java
@@ -0,0 +1,82 @@
+package io.papermc.paper.plugin.storage;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder;
+import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
+import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import io.papermc.paper.plugin.provider.ProviderStatus;
+import io.papermc.paper.plugin.provider.ProviderStatusHolder;
+import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.slf4j.Logger;
+
+import java.util.List;
+
+public class ServerPluginProviderStorage extends ConfiguredProviderStorage<JavaPlugin> {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public ServerPluginProviderStorage() {
+ super(new ProviderConfiguration<>() {
+ @Override
+ public void applyContext(PluginProvider<JavaPlugin> provider, DependencyContext dependencyContext) {
+ Plugin alreadyLoadedPlugin = PaperPluginManagerImpl.getInstance().getPlugin(provider.getMeta().getName());
+ if (alreadyLoadedPlugin != null) {
+ throw new IllegalStateException("Provider " + provider + " attempted to add duplicate plugin identifier " + alreadyLoadedPlugin + " THIS WILL CREATE BUGS!!!");
+ }
+
+ if (provider instanceof DependencyContextHolder contextHolder) {
+ contextHolder.setContext(dependencyContext);
+ }
+ }
+
+ @Override
+ public boolean load(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
+ try {
+ provided.getLogger().info(String.format("Loading server plugin %s", provided.getPluginMeta().getDisplayName()));
+ PaperPluginManagerImpl.getInstance().loadPlugin(provided); // We have to add it to the map before the plugin is loaded
+ provided.onLoad();
+ return true;
+ } catch (Throwable ex) {
+ if (provider instanceof ProviderStatusHolder statusHolder) {
+ statusHolder.setStatus(ProviderStatus.ERRORED);
+ }
+ LOGGER.error("Could not load server plugin '%s' in folder '%s' (Is it up to date?)".formatted(provider.getFileName(), provider.getParentSource()), ex);
+ return false;
+ }
+ }
+
+ @Override
+ public List<String> requiredDependencies(PluginProvider<JavaPlugin> provider) {
+ return provider.getMeta().getPluginDependencies();
+ }
+
+ @Override
+ public List<String> optionalDependencies(PluginProvider<JavaPlugin> provider) {
+ return provider.getMeta().getPluginSoftDependencies();
+ }
+
+ @Override
+ public List<String> loadBeforeDependencies(PluginProvider<JavaPlugin> provider) {
+ return provider.getMeta().getLoadBeforePlugins();
+ }
+ });
+ }
+
+ @Override
+ protected void filterLoadingProviders(List<PluginProvider<JavaPlugin>> pluginProviders) {
+ /*
+ Have to do this to prevent loading plugin providers that have failed initializers.
+ This is a hack and a better solution here would be to store failed plugin providers elsewhere.
+ */
+ pluginProviders.removeIf((provider) -> (provider instanceof PaperPluginParent.PaperServerPluginProvider pluginProvider && pluginProvider.shouldSkipCreation()));
+ }
+
+ @Override
+ public String toString() {
+ return "PLUGIN:" + super.toString();
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac263f02a9a79fb3db24c4daa16c405f9b0f7459
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java
@@ -0,0 +1,57 @@
+package io.papermc.paper.plugin.storage;
+
+import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
+import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
+import io.papermc.paper.plugin.provider.PluginProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class SimpleProviderStorage<T> implements ProviderStorage<T> {
+
+ protected final List<PluginProvider<T>> providers = new ArrayList<>();
+ protected ProviderLoadingStrategy<T> strategy;
+
+ protected SimpleProviderStorage(ProviderLoadingStrategy<T> strategy) {
+ this.strategy = strategy;
+ }
+
+ @Override
+ public void register(PluginProvider<T> provider) {
+ this.providers.add(provider);
+ }
+
+ @Override
+ public void enter() {
+ List<PluginProvider<T>> providerList = new ArrayList<>(this.providers);
+ this.filterLoadingProviders(providerList);
+
+ try {
+ for (T plugin : this.strategy.loadProviders(providerList)) {
+ this.processProvided(plugin);
+ }
+ } catch (PluginGraphCycleException exception) {
+ this.handleCycle(exception);
+ }
+ }
+
+ @Override
+ public Iterable<PluginProvider<T>> getRegisteredProviders() {
+ return this.providers;
+ }
+
+ public void processProvided(T provided) {}
+
+ // Mutable enter
+ protected void filterLoadingProviders(List<PluginProvider<T>> providers) {}
+
+ protected abstract void handleCycle(PluginGraphCycleException exception);
+
+ @Override
+ public String toString() {
+ return "SimpleProviderStorage{" +
+ "providers=" + this.providers +
+ ", strategy=" + this.strategy +
+ '}';
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/storage/package-info.java b/src/main/java/io/papermc/paper/plugin/storage/package-info.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1114675137e862ac9682b635bfdbfbc1d7c6e67
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/storage/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Classes in this package are supposed to connect components of {@link io.papermc.paper.plugin.entrypoint} and {@link io.papermc.paper.plugin.provider} packages.
+ * @see io.papermc.paper.plugin.entrypoint.Entrypoint
+ */
+package io.papermc.paper.plugin.storage;
diff --git a/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..98518351e616e536315cd89790b327d3bad33d0e
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java
@@ -0,0 +1,19 @@
+package io.papermc.paper.plugin.util;
+
+import com.mojang.logging.LogUtils;
+import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
+import io.papermc.paper.plugin.provider.source.ProviderSource;
+import org.slf4j.Logger;
+
+public class EntrypointUtil {
+
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public static <C> void registerProvidersFromSource(ProviderSource<C> source, C context) {
+ try {
+ source.registerProviders(LaunchEntryPointHandler.INSTANCE, context);
+ } catch (Throwable e) {
+ LOGGER.error(e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd55fd1d6518ebd1bc2513dd331f072018fd4782
--- /dev/null
+++ b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java
@@ -0,0 +1,37 @@
+package io.papermc.paper.plugin.util;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+@ApiStatus.Internal
+public class NamespaceChecker {
+
+ private static final String[] QUICK_INVALID_NAMESPACES = {
+ "net.minecraft.",
+ "org.bukkit.",
+ "io.papermc.paper.",
+ "com.destroystokoyo.paper."
+ };
+
+ /**
+ * Used for a variety of namespaces that shouldn't be resolved and should instead be moved to
+ * other classloaders. We can assume this because only plugins should be using this classloader.
+ *
+ * @param name namespace
+ */
+ public static void validateNameSpaceForClassloading(@NotNull String name) throws ClassNotFoundException {
+ if (!isValidNameSpace(name)) {
+ throw new ClassNotFoundException(name);
+ }
+ }
+
+ public static boolean isValidNameSpace(@NotNull String name) {
+ for (String string : QUICK_INVALID_NAMESPACES) {
+ if (name.startsWith(string)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
index f7114d5b8f2f93f62883e24da29afaf9f74ee1a6..00e78a495575abb9496b1849822605b613afe3b4 100644
--- a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
+++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java
@@ -1,9 +1,11 @@
package io.papermc.paper.util;
+import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.plugin.java.PluginClassLoader;
import org.jetbrains.annotations.Nullable;
+import java.util.Objects;
import java.util.Optional;
public class StackWalkerUtil {
@@ -12,11 +14,20 @@ public class StackWalkerUtil {
public static JavaPlugin getFirstPluginCaller() {
Optional<JavaPlugin> foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(stream -> stream
- .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader)
.map((frame) -> {
- PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader();
- return classLoader.getPlugin();
+ ClassLoader classLoader = frame.getDeclaringClass().getClassLoader();
+ JavaPlugin plugin;
+ if (classLoader instanceof PaperPluginClassLoader pluginClassLoader) {
+ plugin = pluginClassLoader.getLoadedJavaPlugin();
+ } else if (classLoader instanceof PluginClassLoader spigotClassloader) {
+ plugin = spigotClassloader.getPlugin();
+ } else {
+ plugin = null;
+ }
+
+ return plugin;
})
+ .filter(Objects::nonNull)
.findFirst());
return foundFrame.orElse(null);
diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
index 5b8ecf5b0165ed2cd4397cdee958e97c2e8f18d5..ad802eb211f05f646159d7fc53f8a9427b46cb89 100644
--- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
+++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java
@@ -310,7 +310,13 @@ public class BuiltInRegistries {
}
public static void bootStrap() {
+ // Paper start
+ bootStrap(() -> {});
+ }
+ public static void bootStrap(Runnable runnable) {
+ // Paper end
createContents();
+ runnable.run(); // Paper
freeze();
validate(REGISTRY);
}
diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java
index b5aa358638b9d0638dfe47f7ebac04cca1dd80b9..e43096e69a00f9ea96badd7c966443cfcf3e7b95 100644
--- a/src/main/java/net/minecraft/server/Bootstrap.java
+++ b/src/main/java/net/minecraft/server/Bootstrap.java
@@ -69,7 +69,11 @@ public class Bootstrap {
EntitySelectorOptions.bootStrap();
DispenseItemBehavior.bootStrap();
CauldronInteraction.bootStrap();
- BuiltInRegistries.bootStrap();
+ // Paper start
+ BuiltInRegistries.bootStrap(() -> {
+ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping
+ });
+ // Paper end
Bootstrap.wrapStreams();
}
// CraftBukkit start - easier than fixing the decompile
diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java
index b7399e29094c66c88a6f4c0e996a906bcaa3b4ca..abf4c54eec6881d6e05893983f83f9eb4b249634 100644
--- a/src/main/java/net/minecraft/server/Main.java
+++ b/src/main/java/net/minecraft/server/Main.java
@@ -110,6 +110,17 @@ public class Main {
JvmProfiler.INSTANCE.start(Environment.SERVER);
}
+ // Paper start
+
+ // We have to load the bukkit configuration inorder to get the update folder location.
+ io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionset);
+ // Register the default plugin directory
+ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath());
+ @SuppressWarnings("unchecked")
+ java.util.List<File> files = (java.util.List<File>) optionset.valuesOf("add-plugin");
+ // Register plugins from the flag
+ io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files);
+ // Paper end
Bootstrap.bootStrap();
Bootstrap.validate();
Util.startTimerHackThread();
diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
index 26ca07b5e302cc4cc02e06f5d07f6d9eb541275e..17a6290969a63be85fa780e2cad4ce63790379b1 100644
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -268,7 +268,8 @@ public final class CraftServer implements Server {
private final CraftCommandMap commandMap = new CraftCommandMap(this);
private final SimpleHelpMap helpMap = new SimpleHelpMap(this);
private final StandardMessenger messenger = new StandardMessenger();
- private final SimplePluginManager pluginManager = new SimplePluginManager(this, this.commandMap);
+ private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap);
+ public final io.papermc.paper.plugin.manager.PaperPluginManagerImpl paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); {this.pluginManager.paperPluginManager = this.paperPluginManager;} // Paper
private final StructureManager structureManager;
protected final DedicatedServer console;
protected final DedicatedPlayerList playerList;
@@ -416,24 +417,7 @@ public final class CraftServer implements Server {
}
public void loadPlugins() {
- this.pluginManager.registerInterface(JavaPluginLoader.class);
-
- File pluginFolder = (File) console.options.valueOf("plugins");
-
- if (pluginFolder.exists()) {
- Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder);
- for (Plugin plugin : plugins) {
- try {
- String message = String.format("Loading %s", plugin.getDescription().getFullName());
- plugin.getLogger().info(message);
- plugin.onLoad();
- } catch (Throwable ex) {
- Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
- }
- }
- } else {
- pluginFolder.mkdir();
- }
+ io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation
}
public void enablePlugins(PluginLoadOrder type) {
@@ -522,15 +506,17 @@ public final class CraftServer implements Server {
private void enablePlugin(Plugin plugin) {
try {
List<Permission> perms = plugin.getDescription().getPermissions();
-
+ List<Permission> permsToLoad = new ArrayList<>(); // Paper
for (Permission perm : perms) {
- try {
- this.pluginManager.addPermission(perm, false);
- } catch (IllegalArgumentException ex) {
- this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex);
+ // Paper start
+ if (this.paperPluginManager.getPermission(perm.getName()) == null) {
+ permsToLoad.add(perm);
+ } else {
+ this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered");
+ // Paper end
}
}
- this.pluginManager.dirtyPermissibles();
+ this.paperPluginManager.addPermissions(permsToLoad); // Paper
this.pluginManager.enablePlugin(plugin);
} catch (Throwable ex) {
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
index 909b2c98e7a9117d2f737245e4661792ffafb744..d96399e9bf1a58db5a4a22e58abb99e7660e0694 100644
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
@@ -42,6 +42,12 @@ public class MinecraftInternalPlugin extends PluginBase {
public PluginDescriptionFile getDescription() {
return pdf;
}
+ // Paper start
+ @Override
+ public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
+ return pdf;
+ }
+ // Paper end
@Override
public FileConfiguration getConfig() {
diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
index 750f4b3930278c291f10015c7a8a8df57d04a286..3d2303dbd06a12968302cb100e36be9de28700f0 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java
@@ -436,6 +436,12 @@ public final class CraftMagicNumbers implements UnsafeValues {
net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack);
return nmsItemStack.getItem().getDescriptionId(nmsItemStack);
}
+ // Paper start
+ @Override
+ public boolean isSupportedApiVersion(String apiVersion) {
+ return apiVersion != null && SUPPORTED_API.contains(apiVersion);
+ }
+ // Paper end
/**
* This helper class represents the different NBT Tags.
diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier
new file mode 100644
index 0000000000000000000000000000000000000000..20dbe2775951bfcdb85c5d679ac86c77a93e0847
--- /dev/null
+++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier
@@ -0,0 +1 @@
+io.papermc.paper.plugin.entrypoint.classloader.PaperClassloaderBytecodeModifier
diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage
new file mode 100644
index 0000000000000000000000000000000000000000..a22647244037cd92262b3b5a6582f0a11172fdc8
--- /dev/null
+++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage
@@ -0,0 +1 @@
+io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage
diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..1d14f530ef888102e47eeeaf0d1a6076e51871c4
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java
@@ -0,0 +1,146 @@
+package io.papermc.paper.plugin;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import org.bukkit.Server;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.generator.BiomeProvider;
+import org.bukkit.generator.ChunkGenerator;
+import org.bukkit.plugin.PluginBase;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginLoader;
+import org.bukkit.plugin.PluginLogger;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+public class PaperTestPlugin extends PluginBase {
+ private final String pluginName;
+ private boolean enabled = true;
+ private final PluginMeta configuration;
+
+ public PaperTestPlugin(String pluginName) {
+ this.pluginName = pluginName;
+ this.configuration = new TestPluginMeta(pluginName);
+ }
+
+ public PaperTestPlugin(PluginMeta configuration) {
+ this.configuration = configuration;
+ this.pluginName = configuration.getName();
+ }
+
+ @Override
+ public File getDataFolder() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginDescriptionFile getDescription() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginMeta getPluginMeta() {
+ return this.configuration;
+ }
+
+ @Override
+ public FileConfiguration getConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public InputStream getResource(String filename) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void saveConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void saveDefaultConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void saveResource(String resourcePath, boolean replace) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void reloadConfig() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginLogger getLogger() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public PluginLoader getPluginLoader() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public Server getServer() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public void onDisable() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void onLoad() {
+ }
+
+ @Override
+ public void onEnable() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean isNaggable() {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public void setNaggable(boolean canNag) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public BiomeProvider getDefaultBiomeProvider(String worldName, String id) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+}
diff --git a/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java b/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7665967dc1849dde750c6d24298e76c2c74f8443
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java
@@ -0,0 +1,159 @@
+package io.papermc.paper.plugin;
+
+import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
+import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy;
+import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
+import io.papermc.paper.plugin.provider.PluginProvider;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class PluginDependencyLoadingTest {
+
+ private static List<PluginProvider<PaperTestPlugin>> REGISTERED_PROVIDERS = new ArrayList<>();
+ private static Map<String, Integer> LOAD_ORDER = new HashMap<>();
+
+ static {
+ setup();
+ }
+
+ private static TestJavaPluginProvider setup(String identifier, String[] hard, String[] soft, String[] before) {
+ TestPluginMeta configuration = new TestPluginMeta(identifier);
+ configuration.setHardDependencies(List.of(hard));
+ configuration.setSoftDependencies(List.of(soft));
+ configuration.setLoadBefore(List.of(before));
+
+ TestJavaPluginProvider provider = new TestJavaPluginProvider(configuration);
+ REGISTERED_PROVIDERS.add(provider);
+ return provider;
+ }
+
+ /**
+ * Obfuscated plugin names, this uses a real dependency tree...
+ */
+ private static void setup() {
+ setup("RedAir", new String[]{}, new String[]{"NightShovel", "EmeraldFire"}, new String[]{"GreenShovel", "IronSpork", "BrightBlueShovel", "WireDoor"});
+ setup("BigGrass", new String[]{}, new String[]{"IronEarth", "RedAir"}, new String[]{"BlueFire"});
+ setup("BlueFire", new String[]{}, new String[]{}, new String[]{});
+ setup("BigPaper", new String[]{}, new String[]{"BlueFire"}, new String[]{});
+ setup("EmeraldSpork", new String[]{}, new String[]{}, new String[]{"GoldPaper", "YellowSnow"});
+ setup("GreenShovel", new String[]{}, new String[]{}, new String[]{});
+ setup("BrightBlueGrass", new String[]{"BigPaper"}, new String[]{"DarkSpork"}, new String[]{});
+ setup("GoldPaper", new String[]{}, new String[]{"BlueFire"}, new String[]{});
+ setup("GreenGlass", new String[]{}, new String[]{}, new String[]{});
+ setup("GoldNeptune", new String[]{}, new String[]{"GreenShovel", "GoldNeptuneVersioning"}, new String[]{});
+ setup("RedPaper", new String[]{}, new String[]{"GoldPaper", "GoldFire", "EmeraldGrass", "BlueFire", "CopperSpork", "YellowDoor", "OrangeClam", "BlueSponge", "GoldNeptune", "BrightBlueGrass", "DarkSpoon", "BigShovel", "GreenGlass", "IronGlass"}, new String[]{"IronPaper", "YellowFire"});
+ setup("YellowGrass", new String[]{}, new String[]{"RedAir"}, new String[]{});
+ setup("WireFire", new String[]{}, new String[]{"RedPaper", "WireGrass", "YellowSpork", "NightAir"}, new String[]{});
+ setup("OrangeNeptune", new String[]{}, new String[]{}, new String[]{});
+ setup("BigSpoon", new String[]{"YellowGrass", "GreenShovel"}, new String[]{"RedAir", "GoldNeptune", "BrightBlueGrass", "LightDoor", "LightSpork", "LightEarth", "NightDoor", "OrangeSpoon", "GoldSponge", "GoldDoor", "DarkPaper", "RedPaper", "GreenGlass", "IronGlass", "NightGlass", "BigGrass", "BlueFire", "YellowSpoon", "DiamondGrass", "DiamondShovel", "DarkSnow", "EmeraldGlass", "EmeraldSpoon", "LightFire", "WireGrass", "RedEarth", "WireFire"}, new String[]{});
+ setup("CopperSnow", new String[]{}, new String[]{"RedSnow", "OrangeFire", "WireAir", "GreenGlass", "NightSpork", "EmeraldPaper"}, new String[]{"BlueGrass"});
+ setup("BrightBluePaper", new String[]{}, new String[]{"GoldEarth", "BrightBlueSpoon", "CopperGlass", "LightSporkChat", "DarkAir", "LightEarth", "DiamondDoor", "YellowShovel", "BlueAir", "DarkShovel", "GoldPaper", "BlueFire", "GreenGlass", "YellowSpork", "BigGrass", "OrangePaper", "DarkPaper"}, new String[]{"WireShovel"});
+ setup("LightSponge", new String[]{}, new String[]{}, new String[]{});
+ setup("OrangeShovel", new String[]{}, new String[]{}, new String[]{});
+ setup("GoldGrass", new String[]{}, new String[]{"GreenGlass", "BlueFire"}, new String[]{});
+ setup("IronSponge", new String[]{}, new String[]{"DiamondEarth"}, new String[]{});
+ setup("EmeraldSnow", new String[]{}, new String[]{}, new String[]{});
+ setup("BlueSpoon", new String[]{"BigGrass"}, new String[]{"GreenGlass", "GoldPaper", "GreenShovel", "YellowClam"}, new String[]{});
+ setup("BigSpork", new String[]{}, new String[]{"BigPaper"}, new String[]{});
+ setup("BluePaper", new String[]{}, new String[]{"BigClam", "RedSpoon", "GreenFire", "WireSnow", "OrangeSnow", "BlueFire", "BrightBlueGrass", "YellowSpork", "GreenGlass"}, new String[]{});
+ setup("OrangeSpork", new String[]{}, new String[]{}, new String[]{});
+ setup("DiamondNeptune", new String[]{}, new String[]{"GreenGlass", "GreenShovel", "YellowNeptune"}, new String[]{});
+ setup("BigFire", new String[]{}, new String[]{"BlueFire", "BrightBlueDoor", "GreenGlass"}, new String[]{});
+ setup("NightNeptune", new String[]{}, new String[]{"BlueFire", "DarkGlass", "GoldPaper", "YellowNeptune", "BlueShovel"}, new String[]{});
+ setup("YellowEarth", new String[]{"RedAir"}, new String[]{}, new String[]{});
+ setup("DiamondClam", new String[]{}, new String[]{}, new String[]{});
+ setup("CopperAir", new String[]{}, new String[]{"BigPaper"}, new String[]{});
+ setup("NightSpoon", new String[]{"OrangeNeptune"}, new String[]{"BlueFire", "GreenGlass", "RedSpork", "GoldPaper", "BigShovel", "YellowSponge", "EmeraldSpork"}, new String[]{});
+ setup("GreenClam", new String[]{}, new String[]{"GreenShovel", "BrightBlueEarth", "BigSpoon", "RedPaper", "BlueFire", "GreenGlass", "WireFire", "GreenSnow"}, new String[]{});
+ setup("YellowPaper", new String[]{}, new String[]{}, new String[]{});
+ setup("WireGlass", new String[]{"YellowGrass"}, new String[]{"YellowGlass", "BigSpoon", "CopperSnow", "GreenGlass", "BlueEarth"}, new String[]{});
+ setup("BlueSpork", new String[]{}, new String[]{"BrightBlueGrass"}, new String[]{});
+ setup("CopperShovel", new String[]{}, new String[]{"GreenGlass"}, new String[]{});
+ setup("RedClam", new String[]{}, new String[]{}, new String[]{});
+ setup("EmeraldClam", new String[]{}, new String[]{"BlueFire"}, new String[]{});
+ setup("DarkClam", new String[]{}, new String[]{"GoldAir", "LightGlass"}, new String[]{});
+ setup("WireSpoon", new String[]{}, new String[]{"GoldPaper", "LightSnow"}, new String[]{});
+ setup("CopperNeptune", new String[]{}, new String[]{"GreenGlass", "BigGrass"}, new String[]{});
+ setup("RedNeptune", new String[]{}, new String[]{}, new String[]{});
+ setup("GreenAir", new String[]{}, new String[]{}, new String[]{});
+ setup("RedFire", new String[]{"BrightBlueGrass", "BigPaper"}, new String[]{"BlueFire", "GreenGlass", "BigGrass"}, new String[]{});
+ }
+
+ @Before
+ public void loadProviders() {
+ AtomicInteger currentLoad = new AtomicInteger();
+ ModernPluginLoadingStrategy<PaperTestPlugin> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
+ @Override
+ public void applyContext(PluginProvider<PaperTestPlugin> provider, DependencyContext dependencyContext) {
+ }
+
+ @Override
+ public boolean load(PluginProvider<PaperTestPlugin> provider, PaperTestPlugin provided) {
+ LOAD_ORDER.put(provider.getMeta().getName(), currentLoad.getAndIncrement());
+ return false;
+ }
+
+ @Override
+ public List<String> requiredDependencies(PluginProvider<PaperTestPlugin> provider) {
+ return provider.getMeta().getPluginDependencies();
+ }
+
+ @Override
+ public List<String> optionalDependencies(PluginProvider<PaperTestPlugin> provider) {
+ return provider.getMeta().getPluginSoftDependencies();
+ }
+
+ @Override
+ public List<String> loadBeforeDependencies(PluginProvider<PaperTestPlugin> provider) {
+ return provider.getMeta().getLoadBeforePlugins();
+ }
+ });
+
+ modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS);
+ }
+
+ @Test
+ public void testDependencies() {
+ for (PluginProvider<PaperTestPlugin> provider : REGISTERED_PROVIDERS) {
+ TestPluginMeta pluginMeta = (TestPluginMeta) provider.getMeta();
+ String identifier = pluginMeta.getName();
+ Assert.assertTrue("Provider wasn't loaded! (%s)".formatted(identifier), LOAD_ORDER.containsKey(identifier));
+
+ int index = LOAD_ORDER.get(identifier);
+
+ // Hard dependencies should be loaded BEFORE
+ for (String hardDependency : pluginMeta.getPluginDependencies()) {
+ Assert.assertTrue("Plugin (%s) is missing hard dependency (%s)".formatted(identifier, hardDependency), LOAD_ORDER.containsKey(hardDependency));
+
+ int dependencyIndex = LOAD_ORDER.get(hardDependency);
+ Assert.assertTrue("Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, hardDependency), index > dependencyIndex);
+ }
+
+ for (String softDependency : pluginMeta.getPluginSoftDependencies()) {
+ if (!LOAD_ORDER.containsKey(softDependency)) {
+ continue;
+ }
+
+ int dependencyIndex = LOAD_ORDER.get(softDependency);
+
+ Assert.assertTrue("Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, softDependency), index > dependencyIndex);
+ }
+
+ for (String loadBefore : pluginMeta.getLoadBeforePlugins()) {
+ if (!LOAD_ORDER.containsKey(loadBefore)) {
+ continue;
+ }
+
+ int dependencyIndex = LOAD_ORDER.get(loadBefore);
+ Assert.assertTrue("Plugin (%s) was NOT loaded BEFORE loadbefore dependency. (%s)".formatted(identifier, loadBefore), index < dependencyIndex);
+ }
+ }
+ }
+}
diff --git a/src/test/java/io/papermc/paper/plugin/PluginLoadingTest.java b/src/test/java/io/papermc/paper/plugin/PluginLoadingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..726eba26470e62b0e94a91418512e242464800ae
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java
@@ -0,0 +1,75 @@
+package io.papermc.paper.plugin;
+
+import org.bukkit.Bukkit;
+import org.bukkit.event.Event;
+import org.bukkit.permissions.Permission;
+import org.bukkit.plugin.PluginManager;
+import org.bukkit.support.AbstractTestingBase;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+public class PluginManagerTest extends AbstractTestingBase {
+
+ private static final PluginManager pm = Bukkit.getPluginManager();
+
+ @Test
+ public void testSyncSameThread() {
+ final Event event = new TestEvent(false);
+ pm.callEvent(event);
+ }
+
+ @Test
+ public void testRemovePermissionByNameLower() {
+ this.testRemovePermissionByName("lower");
+ }
+
+ @Test
+ public void testRemovePermissionByNameUpper() {
+ this.testRemovePermissionByName("UPPER");
+ }
+
+ @Test
+ public void testRemovePermissionByNameCamel() {
+ this.testRemovePermissionByName("CaMeL");
+ }
+
+ @Test
+ public void testRemovePermissionByPermissionLower() {
+ this.testRemovePermissionByPermission("lower");
+ }
+
+ @Test
+ public void testRemovePermissionByPermissionUpper() {
+ this.testRemovePermissionByPermission("UPPER");
+ }
+
+ @Test
+ public void testRemovePermissionByPermissionCamel() {
+ this.testRemovePermissionByPermission("CaMeL");
+ }
+
+ private void testRemovePermissionByName(final String name) {
+ final Permission perm = new Permission(name);
+ pm.addPermission(perm);
+ assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
+ pm.removePermission(name);
+ assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
+ }
+
+ private void testRemovePermissionByPermission(final String name) {
+ final Permission perm = new Permission(name);
+ pm.addPermission(perm);
+ assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm));
+ pm.removePermission(perm);
+ assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue()));
+ }
+
+ @After
+ public void tearDown() {
+ pm.clearPlugins();
+ assertThat(pm.getPermissions(), is(empty()));
+ }
+}
diff --git a/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..51c08740edffd152c8e2b6d3676ff7f1ce6090c6
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java
@@ -0,0 +1,42 @@
+package io.papermc.paper.plugin;
+
+import io.papermc.paper.plugin.manager.PaperPluginManagerImpl;
+import org.bukkit.Bukkit;
+import org.bukkit.event.Event;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SyntheticEventTest {
+
+ @Test
+ public void test() {
+ PaperTestPlugin paperTestPlugin = new PaperTestPlugin("synthetictest");
+ PaperPluginManagerImpl paperPluginManager = new PaperPluginManagerImpl(Bukkit.getServer(), null, null);
+
+ TestEvent event = new TestEvent(false);
+ Impl impl = new Impl();
+
+ paperPluginManager.registerEvents(impl, paperTestPlugin);
+ paperPluginManager.callEvent(event);
+
+ Assert.assertEquals(1, impl.callCount);
+ }
+
+ public abstract static class Base<E extends Event> implements Listener {
+ int callCount = 0;
+
+ public void accept(E evt) {
+ callCount++;
+ }
+ }
+
+ public static class Impl extends Base<TestEvent> {
+ @Override
+ @EventHandler
+ public void accept(TestEvent evt) {
+ super.accept(evt);
+ }
+ }
+}
diff --git a/src/test/java/io/papermc/paper/plugin/TestEvent.java b/src/test/java/io/papermc/paper/plugin/TestEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..04903794a8ee4dd73162ae240862ff6dc4cb4e24
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/TestEvent.java
@@ -0,0 +1,22 @@
+package io.papermc.paper.plugin;
+
+
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+
+public class TestEvent extends Event {
+ private static final HandlerList handlers = new HandlerList();
+
+ public TestEvent(boolean async) {
+ super(async);
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+}
diff --git a/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..8837b131dedc41e7d0d58ccf3d1e251168c5b9b8
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java
@@ -0,0 +1,42 @@
+package io.papermc.paper.plugin;
+
+import io.papermc.paper.plugin.provider.PluginProvider;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+import java.util.jar.JarFile;
+import java.util.logging.Logger;
+
+public class TestJavaPluginProvider implements PluginProvider<PaperTestPlugin> {
+
+ private final TestPluginMeta testPluginConfiguration;
+
+ public TestJavaPluginProvider(TestPluginMeta testPluginConfiguration) {
+ this.testPluginConfiguration = testPluginConfiguration;
+ }
+
+ @Override
+ public @NotNull Path getSource() {
+ return Path.of("dummy");
+ }
+
+ @Override
+ public JarFile file() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PaperTestPlugin createInstance() {
+ return new PaperTestPlugin(this.testPluginConfiguration);
+ }
+
+ @Override
+ public TestPluginMeta getMeta() {
+ return this.testPluginConfiguration;
+ }
+
+ @Override
+ public Logger getLogger() {
+ return Logger.getLogger("TestPlugin");
+ }
+}
diff --git a/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba271c35eb2804f94cfc893bf94affb9ae13d3ba
--- /dev/null
+++ b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java
@@ -0,0 +1,114 @@
+package io.papermc.paper.plugin;
+
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import org.bukkit.permissions.Permission;
+import org.bukkit.permissions.PermissionDefault;
+import org.bukkit.plugin.PluginLoadOrder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class TestPluginMeta implements PluginMeta {
+
+ private final String identifier;
+ private List<String> hardDependencies = List.of();
+ private List<String> softDependencies = List.of();
+ private List<String> loadBefore = List.of();
+
+ public TestPluginMeta(String identifier) {
+ this.identifier = identifier;
+ }
+
+ @Override
+ public @NotNull String getName() {
+ return this.identifier;
+ }
+
+ @Override
+ public @NotNull String getMainClass() {
+ return "null";
+ }
+
+ @Override
+ public @NotNull PluginLoadOrder getLoadOrder() {
+ return PluginLoadOrder.POSTWORLD;
+ }
+
+ @Override
+ public @NotNull String getVersion() {
+ return "1.0";
+ }
+
+ @Override
+ public @Nullable String getLoggerPrefix() {
+ return this.identifier;
+ }
+
+ public void setHardDependencies(List<String> hardDependencies) {
+ this.hardDependencies = hardDependencies;
+ }
+
+ @Override
+ public @NotNull List<String> getPluginDependencies() {
+ return this.hardDependencies;
+ }
+
+ public void setSoftDependencies(List<String> softDependencies) {
+ this.softDependencies = softDependencies;
+ }
+
+ @Override
+ public @NotNull List<String> getPluginSoftDependencies() {
+ return this.softDependencies;
+ }
+
+ public void setLoadBefore(List<String> loadBefore) {
+ this.loadBefore = loadBefore;
+ }
+
+ @Override
+ public @NotNull List<String> getLoadBeforePlugins() {
+ return this.loadBefore;
+ }
+
+ @Override
+ public @NotNull List<String> getProvidedPlugins() {
+ return List.of();
+ }
+
+ @Override
+ public @NotNull List<String> getAuthors() {
+ return List.of();
+ }
+
+ @Override
+ public @NotNull List<String> getContributors() {
+ return List.of();
+ }
+
+ @Override
+ public @Nullable String getDescription() {
+ return "null";
+ }
+
+ @Override
+ public @Nullable String getWebsite() {
+ return "null";
+ }
+
+ @Override
+ public @NotNull List<Permission> getPermissions() {
+ return List.of();
+ }
+
+ @Override
+ public @NotNull PermissionDefault getPermissionDefault() {
+ return PermissionDefault.TRUE;
+ }
+
+ @Override
+ public @NotNull String getAPIVersion() {
+ return "null";
+ }
+}
diff --git a/src/test/java/io/papermc/paper/testing/DummyServer.java b/src/test/java/io/papermc/paper/testing/DummyServer.java
index 286790061c9d81c872108ef63e1a1aba2fbec809..53ac8df47a2c9a84c751955fd4fbb352a3ded16b 100644
--- a/src/test/java/io/papermc/paper/testing/DummyServer.java
+++ b/src/test/java/io/papermc/paper/testing/DummyServer.java
@@ -57,7 +57,7 @@ public final class DummyServer {
return new LazyRegistry(() -> CraftRegistry.createRegistry(invocation.getArgument(0, Class.class), AbstractTestingBase.REGISTRY_CUSTOM));
});
- final PluginManager pluginManager = new SimplePluginManager(dummyServer, new SimpleCommandMap(dummyServer));
+ final PluginManager pluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(dummyServer, new SimpleCommandMap(dummyServer), null);
when(dummyServer.getPluginManager()).thenReturn(pluginManager);
Bukkit.setServer(dummyServer);