diff --git a/paper-api/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java b/paper-api/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java
index 023cc52a9e..da46e2b175 100644
--- a/paper-api/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java
+++ b/paper-api/src/main/java/com/destroystokyo/paper/util/VersionFetcher.java
@@ -21,12 +21,24 @@ public interface VersionFetcher {
/**
* Gets the version message to cache and show to command senders.
*
- *
NOTE: This is run in a new thread separate from that of the command processing thread
+ * @return the message to show when requesting a version
+ * @apiNote This method may involve a web request which will block the executing thread
+ */
+ Component getVersionMessage();
+
+ /**
+ * Gets the version message to cache and show to command senders.
*
* @param serverVersion the current version of the server (will match {@link Bukkit#getVersion()})
* @return the message to show when requesting a version
+ * @apiNote This method may involve a web request which will block the current thread
+ * @see #getVersionMessage()
+ * @deprecated {@code serverVersion} is not required
*/
- Component getVersionMessage(String serverVersion);
+ @Deprecated
+ default Component getVersionMessage(String serverVersion) {
+ return getVersionMessage();
+ }
@ApiStatus.Internal
class DummyVersionFetcher implements VersionFetcher {
@@ -37,7 +49,7 @@ public interface VersionFetcher {
}
@Override
- public Component getVersionMessage(final String serverVersion) {
+ public Component getVersionMessage() {
Bukkit.getLogger().warning("Version provider has not been set, cannot check for updates!");
Bukkit.getLogger().info("Override the default implementation of org.bukkit.UnsafeValues#getVersionFetcher()");
new Throwable().printStackTrace();
diff --git a/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java b/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java
index 5df19bd701..4acda947b7 100644
--- a/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java
+++ b/paper-api/src/main/java/org/bukkit/command/SimpleCommandMap.java
@@ -5,7 +5,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@@ -14,32 +13,26 @@ import org.bukkit.Location;
import org.bukkit.Server;
import org.bukkit.command.defaults.BukkitCommand;
import org.bukkit.command.defaults.HelpCommand;
-import org.bukkit.command.defaults.PluginsCommand;
import org.bukkit.command.defaults.ReloadCommand;
-import org.bukkit.command.defaults.VersionCommand;
import org.bukkit.entity.Player;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SimpleCommandMap implements CommandMap {
- protected final Map knownCommands; // Paper
+ protected final Map knownCommands;
private final Server server;
- // Paper start
@org.jetbrains.annotations.ApiStatus.Internal
public SimpleCommandMap(@NotNull final Server server, Map backing) {
this.knownCommands = backing;
- // Paper end
this.server = server;
setDefaultCommands();
}
private void setDefaultCommands() {
- register("bukkit", new VersionCommand("version"));
register("bukkit", new ReloadCommand("reload"));
- //register("bukkit", new PluginsCommand("plugins")); // Paper
- register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Paper
+ register("bukkit", new co.aikar.timings.TimingsCommand("timings"));
}
public void setFallbackCommands() {
diff --git a/paper-api/src/main/java/org/bukkit/command/defaults/VersionCommand.java b/paper-api/src/main/java/org/bukkit/command/defaults/VersionCommand.java
index 26bc02a534..29756f4ea6 100644
--- a/paper-api/src/main/java/org/bukkit/command/defaults/VersionCommand.java
+++ b/paper-api/src/main/java/org/bukkit/command/defaults/VersionCommand.java
@@ -32,6 +32,7 @@ import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+@Deprecated(forRemoval = true)
public class VersionCommand extends BukkitCommand {
private VersionFetcher versionFetcher; // Paper - version command 2.0
private VersionFetcher getVersionFetcher() { // lazy load because unsafe isn't available at command registration
diff --git a/paper-server/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/paper-server/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
index bad9410543..d0554ed663 100644
--- a/paper-server/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
+++ b/paper-server/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java
@@ -43,7 +43,7 @@ public class PaperVersionFetcher implements VersionFetcher {
}
@Override
- public Component getVersionMessage(final String serverVersion) {
+ public Component getVersionMessage() {
final Component updateMessage;
final ServerBuildInfo build = ServerBuildInfo.buildInfo();
if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) {
diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java
index 8d04711538..33b10d4eb6 100644
--- a/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java
+++ b/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java
@@ -1,7 +1,15 @@
package io.papermc.paper.command;
import io.papermc.paper.FeatureHooks;
-import io.papermc.paper.command.subcommands.*;
+import io.papermc.paper.command.subcommands.DumpItemCommand;
+import io.papermc.paper.command.subcommands.DumpListenersCommand;
+import io.papermc.paper.command.subcommands.DumpPluginsCommand;
+import io.papermc.paper.command.subcommands.EntityCommand;
+import io.papermc.paper.command.subcommands.HeapDumpCommand;
+import io.papermc.paper.command.subcommands.MobcapsCommand;
+import io.papermc.paper.command.subcommands.ReloadCommand;
+import io.papermc.paper.command.subcommands.SyncLoadInfoCommand;
+import io.papermc.paper.command.subcommands.VersionCommand;
import it.unimi.dsi.fastutil.Pair;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java b/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java
index 6ca8b5fcaa..f3f466ee58 100644
--- a/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java
+++ b/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java
@@ -33,13 +33,14 @@ public final class PaperCommands {
}
public static void registerCommands() {
- // Paper commands go here
+ // Paper commands go here
+ registerInternalCommand(PaperVersionCommand.create(), "bukkit", PaperVersionCommand.DESCRIPTION, List.of("ver", "about"), Set.of());
}
- private static void registerInternalCommand(final LiteralCommandNode node, final String description, final List aliases, final Set flags) {
+ private static void registerInternalCommand(final LiteralCommandNode node, final String namespace, final String description, final List aliases, final Set flags) {
io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.registerWithFlagsInternal(
null,
- "paper",
+ namespace,
"Paper",
node,
description,
diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java
new file mode 100644
index 0000000000..a7d1a959af
--- /dev/null
+++ b/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java
@@ -0,0 +1,184 @@
+package io.papermc.paper.command;
+
+import com.destroystokyo.paper.PaperVersionFetcher;
+import com.destroystokyo.paper.util.VersionFetcher;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import io.papermc.paper.command.brigadier.CommandSourceStack;
+import io.papermc.paper.command.brigadier.Commands;
+import io.papermc.paper.plugin.configuration.PluginMeta;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+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.TextDecoration;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import net.minecraft.server.MinecraftServer;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.util.StringUtil;
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public class PaperVersionCommand {
+ public static final String DESCRIPTION = "Gets the version of this server including any plugins in use";
+
+ private static final Component NOT_RUNNING = Component.text()
+ .append(Component.text("This server is not running any plugin by that name."))
+ .appendNewline()
+ .append(Component.text("Use /plugins to get a list of plugins.").clickEvent(ClickEvent.suggestCommand("/plugins")))
+ .build();
+ private static final JoinConfiguration PLAYER_JOIN_CONFIGURATION = JoinConfiguration.separators(
+ Component.text(", ", NamedTextColor.WHITE),
+ Component.text(", and ", NamedTextColor.WHITE)
+ );
+ private static final Component FAILED_TO_FETCH = Component.text("Could not fetch version information!", NamedTextColor.RED);
+ private static final Component FETCHING = Component.text("Checking version, please wait...", NamedTextColor.WHITE, TextDecoration.ITALIC);
+
+ private final VersionFetcher versionFetcher = new PaperVersionFetcher();
+ private CompletableFuture computedVersion = CompletableFuture.completedFuture(new ComputedVersion(Component.empty(), -1)); // Precompute-- someday move that stuff out of bukkit
+
+ public static LiteralCommandNode create() {
+ final PaperVersionCommand command = new PaperVersionCommand();
+
+ return Commands.literal("version")
+ .requires(source -> source.getSender().hasPermission("bukkit.command.version"))
+ .then(Commands.argument("plugin", StringArgumentType.word())
+ .suggests(command::suggestPlugins)
+ .executes(command::pluginVersion))
+ .executes(command::serverVersion)
+ .build();
+ }
+
+ private int pluginVersion(final CommandContext context) {
+ final CommandSender sender = context.getSource().getSender();
+ final String pluginName = context.getArgument("plugin", String.class).toLowerCase(Locale.ROOT);
+
+ Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName);
+ if (plugin == null) {
+ plugin = Arrays.stream(Bukkit.getPluginManager().getPlugins())
+ .filter(checkPlugin -> checkPlugin.getName().toLowerCase(Locale.ROOT).contains(pluginName))
+ .findAny()
+ .orElse(null);
+ }
+
+ if (plugin != null) {
+ this.sendPluginInfo(plugin, sender);
+ } else {
+ sender.sendMessage(NOT_RUNNING);
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private CompletableFuture suggestPlugins(final CommandContext context, final SuggestionsBuilder builder) {
+ for (final Plugin plugin : Bukkit.getPluginManager().getPlugins()) {
+ final String name = plugin.getName();
+ if (StringUtil.startsWithIgnoreCase(name, builder.getRemainingLowerCase())) {
+ builder.suggest(name);
+ }
+ }
+
+ return CompletableFuture.completedFuture(builder.build());
+ }
+
+ private void sendPluginInfo(final Plugin plugin, final CommandSender sender) {
+ final PluginMeta meta = plugin.getPluginMeta();
+
+ final TextComponent.Builder builder = Component.text()
+ .append(Component.text(meta.getName()))
+ .append(Component.text(" version "))
+ .append(Component.text(meta.getVersion(), NamedTextColor.GREEN)
+ .hoverEvent(Component.translatable("chat.copy.click"))
+ .clickEvent(ClickEvent.copyToClipboard(meta.getVersion()))
+ );
+
+ if (meta.getDescription() != null) {
+ builder
+ .appendNewline()
+ .append(Component.text(meta.getDescription()));
+ }
+
+ if (meta.getWebsite() != null) {
+ Component websiteComponent = Component.text(meta.getWebsite(), NamedTextColor.GREEN).clickEvent(ClickEvent.openUrl(meta.getWebsite()));
+ builder.appendNewline().append(Component.text("Website: ").append(websiteComponent));
+ }
+
+ if (!meta.getAuthors().isEmpty()) {
+ String prefix = meta.getAuthors().size() == 1 ? "Author: " : "Authors: ";
+ builder.appendNewline().append(Component.text(prefix).append(formatNameList(meta.getAuthors())));
+ }
+
+ if (!meta.getContributors().isEmpty()) {
+ builder.appendNewline().append(Component.text("Contributors: ").append(formatNameList(meta.getContributors())));
+ }
+ sender.sendMessage(builder.build());
+ }
+
+ private static Component formatNameList(final List names) {
+ return Component.join(PLAYER_JOIN_CONFIGURATION, names.stream().map(Component::text).toList()).color(NamedTextColor.GREEN);
+ }
+
+ private int serverVersion(CommandContext context) {
+ sendVersion(context.getSource().getSender());
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private void sendVersion(final CommandSender sender) {
+ final CompletableFuture version = getVersionOrFetch();
+ if (!version.isDone()) {
+ sender.sendMessage(FETCHING);
+ }
+
+ version.whenComplete((computedVersion, throwable) -> {
+ if (computedVersion != null) {
+ sender.sendMessage(computedVersion.message);
+ } else if (throwable != null) {
+ sender.sendMessage(FAILED_TO_FETCH);
+ MinecraftServer.LOGGER.warn("Could not fetch version information!", throwable);
+ }
+ });
+ }
+
+ private CompletableFuture getVersionOrFetch() {
+ if (!this.computedVersion.isDone()) {
+ return this.computedVersion;
+ }
+
+ if (this.computedVersion.isCompletedExceptionally() || System.currentTimeMillis() - this.computedVersion.resultNow().computedTime() > this.versionFetcher.getCacheTime()) {
+ this.computedVersion = this.fetchVersionMessage();
+ }
+
+ return this.computedVersion;
+ }
+
+ private CompletableFuture fetchVersionMessage() {
+ return CompletableFuture.supplyAsync(() -> {
+ final Component message = Component.textOfChildren(
+ Component.text(Bukkit.getVersionMessage(), NamedTextColor.WHITE),
+ Component.newline(),
+ this.versionFetcher.getVersionMessage()
+ );
+
+ return new ComputedVersion(
+ message.hoverEvent(Component.translatable("chat.copy.click", NamedTextColor.WHITE))
+ .clickEvent(ClickEvent.copyToClipboard(PlainTextComponentSerializer.plainText().serialize(message))),
+ System.currentTimeMillis()
+ );
+ });
+ }
+
+ record ComputedVersion(Component message, long computedTime) {
+
+ }
+}