From eeebfb95c4d1ecaf897a6b99f9b9adf03b951cc2 Mon Sep 17 00:00:00 2001 From: Daniel Naylor Date: Mon, 20 Jul 2020 18:47:20 +0100 Subject: [PATCH] Add more Sponge commands: * /sponge which * /sponge version * /sponge plugin Also ensure suggestions that are returned get filtered by startsWith Also also: generics are a bit better now --- SpongeAPI | 2 +- .../common/command/SpongeCommandFactory.java | 11 +- .../argument/AbstractArgumentParser.java | 3 +- .../argument/CustomArgumentParser.java | 3 +- .../context/SpongeCommandContext.java | 6 +- .../context/SpongeCommandContextBuilder.java | 9 +- .../tree/SpongeArgumentCommandNode.java | 9 +- .../command/manager/SpongeCommandManager.java | 12 +- .../sponge/CommandAliasesParameter.java | 62 +++++ .../common/command/sponge/SpongeCommand.java | 234 +++++++++++++++--- 10 files changed, 301 insertions(+), 50 deletions(-) create mode 100644 src/main/java/org/spongepowered/common/command/sponge/CommandAliasesParameter.java diff --git a/SpongeAPI b/SpongeAPI index 992ef50c171..09c3d13fd55 160000 --- a/SpongeAPI +++ b/SpongeAPI @@ -1 +1 @@ -Subproject commit 992ef50c171183070bede2d234379052613d830e +Subproject commit 09c3d13fd555834ea94491483bd4be9c3472bf69 diff --git a/invalid/main/java/org/spongepowered/common/command/SpongeCommandFactory.java b/invalid/main/java/org/spongepowered/common/command/SpongeCommandFactory.java index 432d0303f6e..6c1c3c56b26 100644 --- a/invalid/main/java/org/spongepowered/common/command/SpongeCommandFactory.java +++ b/invalid/main/java/org/spongepowered/common/command/SpongeCommandFactory.java @@ -219,11 +219,11 @@ public static CommandSpec createSpongeCommand() { nonFlagChildren.register(createSpongeVersionCommand(), "version"); nonFlagChildren.register(createSpongeBlockInfoCommand(), "blockInfo"); nonFlagChildren.register(createSpongeEntityInfoCommand(), "entityInfo"); - nonFlagChildren.register(createSpongeAuditCommand(), "audit"); - nonFlagChildren.register(createSpongeHeapCommand(), "heap"); - nonFlagChildren.register(createSpongePluginsCommand(), "plugins"); - nonFlagChildren.register(createSpongeTimingsCommand(), "timings"); - nonFlagChildren.register(createSpongeWhichCommand(), "which"); + // nonFlagChildren.register(createSpongeAuditCommand(), "audit"); + // nonFlagChildren.register(createSpongeHeapCommand(), "heap"); + // nonFlagChildren.register(createSpongePluginsCommand(), "plugins"); + // nonFlagChildren.register(createSpongeTimingsCommand(), "timings"); + // nonFlagChildren.register(createSpongeWhichCommand(), "which"); nonFlagChildren.register(createSpongeMetricsCommand(), "metrics"); flagChildren.register(createSpongeChunksCommand(), "chunks"); flagChildren.register(createSpongeTPSCommand(), "tps"); @@ -525,6 +525,7 @@ private static CommandSpec createSpongeVersionCommand() { .build(); } + // need ray equivalent private static CommandSpec createSpongeBlockInfoCommand() { return CommandSpec.builder() .description(Text.of("Display the tracked information of the Block you are looking at.")) diff --git a/src/main/java/org/spongepowered/common/command/brigadier/argument/AbstractArgumentParser.java b/src/main/java/org/spongepowered/common/command/brigadier/argument/AbstractArgumentParser.java index f4c47bca46c..41a2f44bc09 100644 --- a/src/main/java/org/spongepowered/common/command/brigadier/argument/AbstractArgumentParser.java +++ b/src/main/java/org/spongepowered/common/command/brigadier/argument/AbstractArgumentParser.java @@ -43,6 +43,7 @@ import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; @@ -83,7 +84,7 @@ public CompletableFuture listSuggestions( } catch (final NumberFormatException ex) { builder.suggest(s); } - } else { + } else if (s.toLowerCase(Locale.ROOT).startsWith(builder.getRemaining())) { builder.suggest(s); } } diff --git a/src/main/java/org/spongepowered/common/command/brigadier/argument/CustomArgumentParser.java b/src/main/java/org/spongepowered/common/command/brigadier/argument/CustomArgumentParser.java index b1a66c827ce..ca757ab0289 100644 --- a/src/main/java/org/spongepowered/common/command/brigadier/argument/CustomArgumentParser.java +++ b/src/main/java/org/spongepowered/common/command/brigadier/argument/CustomArgumentParser.java @@ -49,6 +49,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; @@ -134,7 +135,7 @@ public CompletableFuture listSuggestions( } catch (final NumberFormatException ex) { builder.suggest(s); } - } else { + } else if (s.toLowerCase(Locale.ROOT).startsWith(builder.getRemaining())) { builder.suggest(s); } } diff --git a/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContext.java b/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContext.java index 9be06147b2c..3f118e26fad 100644 --- a/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContext.java +++ b/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContext.java @@ -112,13 +112,13 @@ public boolean hasAny(final Parameter.@NonNull Key key) { @Override @NonNull - public Optional getOne(final Parameter.@NonNull Key key) { + public Optional getOne(final Parameter.@NonNull Key key) { return Optional.ofNullable(this.getValue(key)); } @Override @NonNull - public T requireOne(final Parameter.@NonNull Key key) throws NoSuchElementException { + public T requireOne(final Parameter.@NonNull Key key) throws NoSuchElementException { final T value = this.getValue(key); if (value == null) { throw new NoSuchElementException("No value exists for key " + key.key()); @@ -130,7 +130,7 @@ public T requireOne(final Parameter.@NonNull Key key) throws NoSu @Override @NonNull @SuppressWarnings("unchecked") - public Collection getAll(final Parameter.@NonNull Key key) { + public Collection getAll(final Parameter.@NonNull Key key) { final Collection values = (Collection) this.argumentMap.get(key); if (values == null) { return ImmutableList.of(); diff --git a/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContextBuilder.java b/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContextBuilder.java index 89bbbb8996b..5efd771c1b9 100644 --- a/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContextBuilder.java +++ b/src/main/java/org/spongepowered/common/command/brigadier/context/SpongeCommandContextBuilder.java @@ -37,7 +37,6 @@ import com.mojang.brigadier.context.SuggestionContext; import com.mojang.brigadier.tree.CommandNode; import net.minecraft.command.CommandSource; -import net.minecraft.command.Commands; import org.checkerframework.checker.nullness.qual.NonNull; import org.spongepowered.api.command.CommandCause; import org.spongepowered.api.command.parameter.Parameter; @@ -281,7 +280,7 @@ public boolean hasAny(final Parameter.@NonNull Key key) { @Override @NonNull @SuppressWarnings("unchecked") - public Optional getOne(final Parameter.@NonNull Key key) { + public Optional getOne(final Parameter.@NonNull Key key) { final SpongeParameterKey spongeParameterKey = SpongeParameterKey.getSpongeKey(key); final Collection collection = this.getFrom(spongeParameterKey); if (collection.size() > 1) { @@ -294,7 +293,7 @@ public Optional getOne(final Parameter.@NonNull Key key) { @Override @NonNull @SuppressWarnings("unchecked") - public T requireOne(final Parameter.@NonNull Key key) throws NoSuchElementException, IllegalArgumentException { + public T requireOne(final Parameter.@NonNull Key key) throws NoSuchElementException, IllegalArgumentException { final SpongeParameterKey spongeParameterKey = SpongeParameterKey.getSpongeKey(key); final Collection collection = this.getFrom(spongeParameterKey); if (collection.size() > 1) { @@ -309,7 +308,7 @@ public T requireOne(final Parameter.@NonNull Key key) throws NoSu @Override @NonNull @SuppressWarnings("unchecked") - public Collection getAll(final Parameter.@NonNull Key key) { + public Collection getAll(final Parameter.@NonNull Key key) { return (Collection) this.getFrom(SpongeParameterKey.getSpongeKey(key)); } @@ -326,7 +325,7 @@ Collection getFrom(final SpongeParameterKey key) { } @Override - public void putEntry(final Parameter.@NonNull Key key, @NonNull final T object) { + public void putEntry(final Parameter.@NonNull Key key, @NonNull final T object) { if (this.transaction != null && !this.transaction.isEmpty()) { this.transaction.peek().putEntry(key, object); } else { diff --git a/src/main/java/org/spongepowered/common/command/brigadier/tree/SpongeArgumentCommandNode.java b/src/main/java/org/spongepowered/common/command/brigadier/tree/SpongeArgumentCommandNode.java index cb48d8a661b..75627ff7928 100644 --- a/src/main/java/org/spongepowered/common/command/brigadier/tree/SpongeArgumentCommandNode.java +++ b/src/main/java/org/spongepowered/common/command/brigadier/tree/SpongeArgumentCommandNode.java @@ -52,6 +52,7 @@ import java.util.Collection; import java.util.Iterator; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -68,7 +69,13 @@ private static SuggestionProvider createSuggestionProvider(@Nulla } return (context, builder) -> { - completer.complete((org.spongepowered.api.command.parameter.CommandContext) context).forEach(builder::suggest); + final String remaining = builder.getRemaining().toLowerCase(Locale.ROOT); + completer.complete((org.spongepowered.api.command.parameter.CommandContext) context) + .forEach(suggestion -> { + if (suggestion.toLowerCase(Locale.ROOT).startsWith(remaining)) { + builder.suggest(suggestion); + } + }); return builder.buildFuture(); }; } diff --git a/src/main/java/org/spongepowered/common/command/manager/SpongeCommandManager.java b/src/main/java/org/spongepowered/common/command/manager/SpongeCommandManager.java index 75057fc67f2..70f07448eeb 100644 --- a/src/main/java/org/spongepowered/common/command/manager/SpongeCommandManager.java +++ b/src/main/java/org/spongepowered/common/command/manager/SpongeCommandManager.java @@ -29,6 +29,7 @@ import com.google.common.collect.Multimap; import com.google.common.reflect.TypeToken; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.exceptions.CommandSyntaxException; @@ -91,6 +92,7 @@ public final class SpongeCommandManager implements CommandManager { private final Game game; + private final Provider spongeCommand; private final Map commandMappings = new HashMap<>(); private final Multimap inverseCommandMappings = HashMultimap.create(); private final Multimap pluginToCommandMap = HashMultimap.create(); @@ -98,8 +100,14 @@ public final class SpongeCommandManager implements CommandManager { private boolean hasStarted = false; @Inject - public SpongeCommandManager(final Game game) { + public SpongeCommandManager(final Game game, final Provider spongeCommand) { this.game = game; + this.spongeCommand = spongeCommand; + } + + @Override + public Set getKnownAliases() { + return ImmutableSet.copyOf(this.commandMappings.keySet()); } @NonNull @@ -497,7 +505,7 @@ public void init() { try { SpongeParameterizedCommandRegistrar.INSTANCE.register( Launcher.getInstance().getCommonPlugin(), - SpongeCommand.createSpongeCommand(), + this.spongeCommand.get().createSpongeCommand(), "sponge" ); } catch (final CommandFailedRegistrationException ex) { diff --git a/src/main/java/org/spongepowered/common/command/sponge/CommandAliasesParameter.java b/src/main/java/org/spongepowered/common/command/sponge/CommandAliasesParameter.java new file mode 100644 index 00000000000..68a120d284c --- /dev/null +++ b/src/main/java/org/spongepowered/common/command/sponge/CommandAliasesParameter.java @@ -0,0 +1,62 @@ +/* + * This file is part of Sponge, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.common.command.sponge; + +import com.google.common.collect.ImmutableList; +import net.kyori.adventure.text.TextComponent; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.command.exception.ArgumentParseException; +import org.spongepowered.api.command.manager.CommandMapping; +import org.spongepowered.api.command.parameter.ArgumentReader; +import org.spongepowered.api.command.parameter.CommandContext; +import org.spongepowered.api.command.parameter.Parameter; +import org.spongepowered.api.command.parameter.managed.ValueParameter; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +public final class CommandAliasesParameter implements ValueParameter { + + @Override + public List complete(final CommandContext context) { + return ImmutableList.copyOf(Sponge.getGame().getCommandManager().getKnownAliases()); + } + + @Override + public Optional getValue( + final Parameter.Key parameterKey, + final ArgumentReader.Mutable reader, + final CommandContext.Builder context) throws ArgumentParseException { + final String alias = reader.parseString().toLowerCase(Locale.ROOT); + final Optional mapping = + Sponge.getGame().getCommandManager().getCommandMapping(alias); + if (mapping.isPresent()) { + return mapping; + } + throw reader.createException(TextComponent.of("A command with alias " + alias + " does not exist.")); + } + +} diff --git a/src/main/java/org/spongepowered/common/command/sponge/SpongeCommand.java b/src/main/java/org/spongepowered/common/command/sponge/SpongeCommand.java index 4a5d28d687a..9bdb9a8ce89 100644 --- a/src/main/java/org/spongepowered/common/command/sponge/SpongeCommand.java +++ b/src/main/java/org/spongepowered/common/command/sponge/SpongeCommand.java @@ -26,16 +26,20 @@ import co.aikar.timings.Timings; 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 net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextDecoration; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.spongepowered.api.command.Command; import org.spongepowered.api.command.CommandResult; +import org.spongepowered.api.command.manager.CommandMapping; import org.spongepowered.api.command.parameter.CommandContext; import org.spongepowered.api.command.parameter.Parameter; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.lifecycle.RefreshGameEvent; +import org.spongepowered.api.util.TypeTokens; import org.spongepowered.common.SpongeCommon; import org.spongepowered.common.event.SpongeEventManager; import org.spongepowered.common.event.tracking.PhaseTracker; @@ -43,66 +47,102 @@ import org.spongepowered.common.relocate.co.aikar.timings.SpongeTimingsFactory; import org.spongepowered.common.util.SpongeHooks; import org.spongepowered.plugin.PluginContainer; +import org.spongepowered.plugin.metadata.PluginContributor; +import org.spongepowered.plugin.metadata.PluginMetadata; import java.io.File; +import java.net.URL; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Collection; import java.util.Optional; +import java.util.stream.Collectors; -public final class SpongeCommand { +public class SpongeCommand { - private final static Parameter.Value PLUGIN_CONTAINER_PARAMETER = - Parameter.builder(PluginContainer.class) - .optional() - .parser(new FilteredPluginContainerParameter()) - .setKey("plugin") - .build(); + protected static final String INDENT = " "; + protected static final String LONG_INDENT = SpongeCommand.INDENT + SpongeCommand.INDENT; + protected static final TextComponent INDENT_COMPONENT = TextComponent.of(SpongeCommand.INDENT); + protected static final TextComponent LONG_INDENT_COMPONENT = TextComponent.of(SpongeCommand.LONG_INDENT); + private final Parameter.Key pluginContainerKey = Parameter.key("plugin", TypeTokens.PLUGIN_CONTAINER_TOKEN); + private final Parameter.Key commandMappingKey = Parameter.key("command", TypeTokens.COMMAND_MAPPING); - private SpongeCommand() { - } + @Nullable private TextComponent versionText = null; - public static Command.Parameterized createSpongeCommand() { + public Command.Parameterized createSpongeCommand() { // /sponge audit final Command.Parameterized auditCommand = Command.builder() .setPermission("sponge.command.audit") - .setExecutor(SpongeCommand::auditSubcommandExecutor) + .setShortDescription(TextComponent.of("Audit mixin classes for implementation")) + .setExecutor(this::auditSubcommandExecutor) .build(); // /sponge heap final Command.Parameterized heapCommand = Command.builder() .setPermission("sponge.command.heap") - .setExecutor(SpongeCommand::heapSubcommandExecutor) + .setShortDescription(TextComponent.of("Dump live JVM heap")) + .setExecutor(this::heapSubcommandExecutor) .build(); // /sponge plugins final Command.Parameterized pluginsReloadCommand = Command.builder() .setPermission("sponge.command.plugins.refresh") - .setExecutor(SpongeCommand::pluginsRefreshSubcommandExecutor) - .parameter(SpongeCommand.PLUGIN_CONTAINER_PARAMETER) + .setShortDescription(TextComponent.of("Refreshes supported plugin, typically causing plugin configuration reloads.")) + .parameter(Parameter.builder(PluginContainer.class) + .optional() + .parser(new FilteredPluginContainerParameter()) + .setKey(this.pluginContainerKey) + .build()) + .setExecutor(this::pluginsRefreshSubcommandExecutor) .build(); final Command.Parameterized pluginsCommand = Command.builder() - .setPermission("sponge.command.plugins") + .setPermission("sponge.command.plugins.root") + .setShortDescription(TextComponent.of("Lists all currently installed plugins.")) .child(pluginsReloadCommand, "refresh") - .setExecutor(SpongeCommand::pluginsSubcommand) + .parameter(Parameter.plugin().setKey(this.pluginContainerKey).optional().build()) + .setExecutor(this::pluginsSubcommand) .build(); // /sponge timings - final Command.Parameterized timingsCommand = SpongeCommand.timingsSubcommand(); + final Command.Parameterized timingsCommand = this.timingsSubcommand(); + + // /sponge version + final Command.Parameterized versionCommand = Command.builder() + .setPermission("sponge.command.version") + .setShortDescription(TextComponent.of("Display Sponge's current version")) + .setExecutor(this::versionExecutor) + .build(); + + // /sponge which + final Command.Parameterized whichCommand = Command.builder() + .setPermission("sponge.command.which") + .parameter(Parameter.builder(CommandMapping.class).setKey(this.commandMappingKey).parser(new CommandAliasesParameter()).build()) + .setShortDescription(TextComponent.of("Find the plugin that owns a specific command")) + .setExecutor(this::whichExecutor) + .build(); // /sponge - return Command.builder() + final Command.Builder commandBuilder = Command.builder() .setPermission("sponge.command.root") - .setExecutor(SpongeCommand::rootCommand) + .setExecutor(this::rootCommand) .child(auditCommand, "audit") .child(heapCommand, "heap") .child(pluginsCommand, "plugins") .child(timingsCommand, "timings") - .build(); + .child(versionCommand, "version") + .child(whichCommand, "which"); + + this.additionalActions(commandBuilder); + return commandBuilder.build(); + } + + protected void additionalActions(final Command.Builder builder) { + // no-op for vanilla, SF might like to add a /sponge mods command, for example. } @NonNull - public static CommandResult rootCommand(final CommandContext context) { + private CommandResult rootCommand(final CommandContext context) { final PluginContainer platformPlugin = Launcher.getInstance().getPlatformPlugin(); final PluginContainer apiPlugin = Launcher.getInstance().getApiPlugin(); final PluginContainer minecraftPlugin = Launcher.getInstance().getMinecraftPlugin(); @@ -120,14 +160,14 @@ public static CommandResult rootCommand(final CommandContext context) { } @NonNull - public static CommandResult auditSubcommandExecutor(final CommandContext context) { + private CommandResult auditSubcommandExecutor(final CommandContext context) { SpongeCommon.getLogger().info("Starting Mixin Audit"); Launcher.getInstance().auditMixins(); return CommandResult.success(); } @NonNull - private static CommandResult heapSubcommandExecutor(final CommandContext context) { + private CommandResult heapSubcommandExecutor(final CommandContext context) { final File file = new File(new File(new File("."), "dumps"), "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + "-server.hprof"); // src.sendMessage(Text.of("Writing JVM heap data to: ", file)); @@ -139,15 +179,29 @@ private static CommandResult heapSubcommandExecutor(final CommandContext context } @NonNull - private static CommandResult pluginsSubcommand(final CommandContext context) { - // we'll make it better in a bit. - Launcher.getInstance().getPluginManager().getPlugins().forEach(x -> SpongeCommon.getLogger().info(x.getMetadata().getId())); + private CommandResult pluginsSubcommand(final CommandContext context) { + final Optional pluginContainer = context.getOne(this.pluginContainerKey); + if (pluginContainer.isPresent()) { + context.sendMessage(this.createContainerMeta(pluginContainer.get().getMetadata())); + } else { + final Collection plugins = Launcher.getInstance().getPluginManager().getPlugins(); + context.sendMessage(this.title("Plugins (" + plugins.size() + ")")); + for (final PluginContainer specificContainer : plugins) { + final PluginMetadata metadata = specificContainer.getMetadata(); + final TextComponent.Builder builder = TextComponent.builder(); + this.createShortContainerMeta(builder.append(INDENT_COMPONENT), metadata); + // builder.clickEvent(SpongeComponents.executeCallback(cause -> + // cause.sendMessage(this.createContainerMeta(metadata)))); + context.sendMessage(builder.build()); + } + } + return CommandResult.success(); } @NonNull - private static CommandResult pluginsRefreshSubcommandExecutor(final CommandContext context) { - final Optional pluginContainer = context.getOne(SpongeCommand.PLUGIN_CONTAINER_PARAMETER); + private CommandResult pluginsRefreshSubcommandExecutor(final CommandContext context) { + final Optional pluginContainer = context.getOne(this.pluginContainerKey); final RefreshGameEvent event = SpongeEventFactory.createRefreshGameEvent( PhaseTracker.getCauseStackManager().getCurrentCause(), SpongeCommon.getGame() @@ -166,10 +220,10 @@ private static CommandResult pluginsRefreshSubcommandExecutor(final CommandConte return CommandResult.success(); } - private static Command.Parameterized timingsSubcommand() { + private Command.@NonNull Parameterized timingsSubcommand() { return Command.builder() .setPermission("sponge.command.timings") - // .setShortDescription(Text.of("Manages Sponge Timings data to see performance of the server.")) //TODO: when text comes back + .setShortDescription(TextComponent.of("Manages Sponge Timings data to see performance of the server.")) .child(Command.builder() .setExecutor(context -> { if (!Timings.isTimingsEnabled()) { @@ -239,4 +293,122 @@ private static Command.Parameterized timingsSubcommand() { .build(), "cost") .build(); } + + @NonNull + private CommandResult versionExecutor(final CommandContext context) { + if (this.versionText == null) { + final PluginContainer platformPlugin = Launcher.getInstance().getPlatformPlugin(); + + final TextComponent.Builder builder = TextComponent.builder() + .append( + TextComponent.of(platformPlugin.getMetadata().getName().get(), Style.of(NamedTextColor.YELLOW, TextDecoration.BOLD)) + ); + + final TextComponent colon = TextComponent.of(": ", NamedTextColor.GRAY); + for (final PluginContainer container : Launcher.getInstance().getLauncherPlugins()) { + final PluginMetadata metadata = container.getMetadata(); + builder.append( + TextComponent.newline(), + SpongeCommand.INDENT_COMPONENT, + TextComponent.of(metadata.getName().orElseGet(metadata::getId), NamedTextColor.GRAY), + colon, + TextComponent.of(container.getMetadata().getVersion()) + ); + } + + final String arch = System.getProperty("sun.arch.data.model"); + final String javaArch = arch != null ? arch + "-bit" : "UNKNOWN"; + + final String javaVendor = System.getProperty("java.vendor"); + final String javaVersion = System.getProperty("java.version"); + final String osName = System.getProperty("os.name"); + final String osVersion = System.getProperty("os.version"); + final String osArch = System.getProperty("os.arch"); + + builder.append( + TextComponent.newline(), + SpongeCommand.INDENT_COMPONENT, + TextComponent.of("JVM", NamedTextColor.GRAY), + colon, + TextComponent.of(javaVersion + "/" + javaArch + " (" + javaVendor + ")"), + TextComponent.newline(), + SpongeCommand.INDENT_COMPONENT, + TextComponent.of("OS", NamedTextColor.GRAY), + colon, + TextComponent.of(osName + "/" + osVersion + " (" + osArch + ")") + ); + this.versionText = builder.build(); + } + + context.sendMessage(this.versionText); + return CommandResult.success(); + } + + @NonNull + private CommandResult whichExecutor(final CommandContext context) { + final CommandMapping mapping = context.requireOne(this.commandMappingKey); + context.sendMessage(TextComponent.builder().append( + this.title("Aliases: "), + TextComponent.join(TextComponent.of(", "), + mapping.getAllAliases().stream().map(x -> TextComponent.of(x, NamedTextColor.YELLOW)).collect(Collectors.toList())), + TextComponent.newline(), + this.title("Owned by: "), + this.hl(mapping.getPlugin().getMetadata().getName().orElseGet(() -> mapping.getPlugin().getMetadata().getId()))) + .build()); + return CommandResult.success(); + } + + // -- + + private TextComponent title(final String title) { + return TextComponent.of(title, NamedTextColor.GREEN); + } + + private TextComponent hl(final String toHighlight) { + return TextComponent.of(toHighlight, NamedTextColor.DARK_GREEN); + } + + private void appendPluginMeta(final TextComponent.Builder builder, final String key, final String value) { + this.appendPluginMeta(builder, key, TextComponent.of(value)); + } + + private void appendPluginMeta(final TextComponent.Builder builder, final String key, final URL value) { + final String url = value.toString(); + this.appendPluginMeta(builder, key, TextComponent.builder(url).clickEvent(ClickEvent.openUrl(url)) + .decoration(TextDecoration.UNDERLINED, true).build()); + } + + private void appendPluginMeta(final TextComponent.Builder builder, final String key, final TextComponent value) { + builder.append(TextComponent.newline()) + .append() + .append(SpongeCommand.INDENT_COMPONENT, this.title(key + ": "), value); + } + + private void createShortContainerMeta(final TextComponent.Builder builder, final PluginMetadata pluginMetadata) { + builder.append(this.title(pluginMetadata.getName().orElse(pluginMetadata.getId()))); + builder.append(" v" + pluginMetadata.getVersion()); + } + + private TextComponent createContainerMeta(final PluginMetadata pluginMetadata) { + final TextComponent.Builder builder = TextComponent.builder(); + this.createShortContainerMeta(builder, pluginMetadata); + + this.appendPluginMeta(builder, "ID", pluginMetadata.getId()); + pluginMetadata.getDescription().ifPresent(x -> this.appendPluginMeta(builder, "Description", x)); + pluginMetadata.getLinks().getHomepage().ifPresent(x -> this.appendPluginMeta(builder, "Homepage", x)); + pluginMetadata.getLinks().getIssues().ifPresent(x -> this.appendPluginMeta(builder, "Issues", x)); + pluginMetadata.getLinks().getSource().ifPresent(x -> this.appendPluginMeta(builder, "Source", x)); + final Collection contributors = pluginMetadata.getContributors(); + if (!contributors.isEmpty()) { + builder.append(TextComponent.newline()).append(SpongeCommand.INDENT_COMPONENT).append(this.title("Contributors:")); + for (final PluginContributor contributor : contributors) { + builder.append(TextComponent.newline()).append(SpongeCommand.LONG_INDENT_COMPONENT).append(contributor.getName()); + contributor.getDescription().ifPresent(x -> builder.append(" (" + x + ")")); + } + } + + this.appendPluginMeta(builder, "Main class", pluginMetadata.getMainClass()); + return builder.build(); + } + }