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) { + + } +}